From c9275f7d3d5c4554edaf40331e44d81a063af032 Mon Sep 17 00:00:00 2001 From: "Sanket M." Date: Tue, 14 Jul 2026 19:08:59 +0530 Subject: [PATCH] fix(release): include actions, subscriptions, UI extensions in release (DEVX-540) `application release` sent only { applicationId, version, description }, so every action, webhook subscription, and UI extension configured in godaddy.toml (via `application add`) was silently dropped from the release. Ports the TS release input (PR #58). - release_command: read godaddy.toml and include `actions`, `subscriptions` (flattened webhook list), and `uiExtensions` in the createRelease input. Missing config is non-fatal (empty arrays); >1 target per extension is a hard error (API allows one target per extension per release). - build_ui_extensions / ui_extension_entry: map embed/checkout/blocks to the release shape (blocks -> name "Blocks"/handle "blocks"), target optional. - client: createRelease now also returns uiExtensions for verification (PR #58). - tests: field/target mapping + multi-target rejection. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/src/application/client.rs | 2 +- rust/src/application/commands/mod.rs | 133 +++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/rust/src/application/client.rs b/rust/src/application/client.rs index 433d957..7673b56 100644 --- a/rust/src/application/client.rs +++ b/rust/src/application/client.rs @@ -122,7 +122,7 @@ impl ApplicationClient { pub async fn create_release(&self, input: Value) -> Result { self.query(json!({ - "query": "mutation CreateRelease($input: MutationCreateReleaseInput!) { createRelease(input: $input) { id version description createdAt } }", + "query": "mutation CreateRelease($input: MutationCreateReleaseInput!) { createRelease(input: $input) { id version description createdAt uiExtensions { id name handle type source target } } }", "variables": { "input": input } })) .await diff --git a/rust/src/application/commands/mod.rs b/rust/src/application/commands/mod.rs index d558f0e..9fa053c 100644 --- a/rust/src/application/commands/mod.rs +++ b/rust/src/application/commands/mod.rs @@ -654,6 +654,57 @@ fn archive_command() -> RuntimeCommandSpec { ) } +/// Build one `uiExtensions` release entry, enforcing the API's one-target-per- +/// extension limit. `target` is omitted when the extension has no targets. +fn ui_extension_entry( + name: &str, + handle: &str, + source: &str, + kind: &str, + targets: &[crate::config::ExtensionTarget], +) -> cli_engine::Result { + if targets.len() > 1 { + return Err(cli_engine::CliCoreError::message(format!( + "UI extension '{name}' has {} targets, but only one target is supported per extension during release", + targets.len() + ))); + } + let mut entry = json!({ "name": name, "handle": handle, "source": source, "type": kind }); + if let Some(t) = targets.first() { + entry["target"] = json!(t.target); + } + Ok(entry) +} + +/// Map godaddy.toml extensions (embed / checkout / blocks) to the release +/// `uiExtensions` input. Mirrors the TS release mapping (single target each). +fn build_ui_extensions( + config: &crate::config::Config, +) -> cli_engine::Result> { + let mut out = Vec::new(); + let Some(exts) = &config.extensions else { + return Ok(out); + }; + for e in &exts.embed { + out.push(ui_extension_entry( + &e.name, &e.handle, &e.source, "embed", &e.targets, + )?); + } + for e in &exts.checkout { + out.push(ui_extension_entry( + &e.name, &e.handle, &e.source, "checkout", &e.targets, + )?); + } + if let Some(b) = &exts.blocks { + // Blocks carries no name/handle/targets in config; use the same fixed + // identifiers the TS release path uses. + out.push( + json!({ "name": "Blocks", "handle": "blocks", "source": b.source, "type": "blocks" }), + ); + } + Ok(out) +} + fn release_command() -> RuntimeCommandSpec { RuntimeCommandSpec::new_with_context( CommandSpec::new("release", "Create a new application release") @@ -699,6 +750,42 @@ fn release_command() -> RuntimeCommandSpec { if let Some(desc) = description { input["description"] = json!(desc); } + + // Include actions, webhook subscriptions, and UI extensions from + // godaddy.toml so configured behavior is captured in the release. + // Without this, everything added via `application add` was silently + // dropped. A missing config is non-fatal (empty arrays); a config + // with too many targets per extension is a hard error. + let (actions, subscriptions, ui_extensions) = match crate::config::read_config( + &crate::config::config_path(Some(&ctx.middleware.env)), + ) { + Ok(config) => { + let actions: Vec = config + .actions + .iter() + .map(|a| json!({ "name": a.name, "url": a.url })) + .collect(); + let subscriptions: Vec = config + .subscriptions + .as_ref() + .map(|s| { + s.webhook + .iter() + .map( + |w| json!({ "name": w.name, "events": w.events, "url": w.url }), + ) + .collect() + }) + .unwrap_or_default(); + let ui_extensions = build_ui_extensions(&config)?; + (actions, subscriptions, ui_extensions) + } + Err(_) => (Vec::new(), Vec::new(), Vec::new()), + }; + input["actions"] = json!(actions); + input["subscriptions"] = json!(subscriptions); + input["uiExtensions"] = json!(ui_extensions); + let client = make_client(&ctx).await?; let data = client.create_release(input).await.map_err(client_err)?; Ok( @@ -1314,4 +1401,50 @@ mod tests { output.rendered ); } + + #[test] + fn ui_extension_entry_maps_fields_and_target() { + use crate::config::ExtensionTarget; + + // No targets: `target` is omitted. + let none = super::ui_extension_entry("Widget", "widget", "src/w.ts", "embed", &[]) + .expect("entry builds"); + assert_eq!(none["name"], "Widget"); + assert_eq!(none["handle"], "widget"); + assert_eq!(none["type"], "embed"); + assert_eq!(none["source"], "src/w.ts"); + assert!(none.get("target").is_none()); + + // Exactly one target: `target` is set to that value. + let one = super::ui_extension_entry( + "Widget", + "widget", + "src/w.ts", + "checkout", + &[ExtensionTarget { + target: "checkout.block".to_owned(), + }], + ) + .expect("entry builds"); + assert_eq!(one["target"], "checkout.block"); + } + + #[test] + fn ui_extension_entry_rejects_multiple_targets() { + use crate::config::ExtensionTarget; + let targets = vec![ + ExtensionTarget { + target: "a".to_owned(), + }, + ExtensionTarget { + target: "b".to_owned(), + }, + ]; + let err = super::ui_extension_entry("Widget", "widget", "src/w.ts", "embed", &targets) + .expect_err("more than one target must be rejected"); + assert!( + err.to_string().contains("only one target is supported"), + "unexpected error: {err}" + ); + } }