Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/src/application/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl ApplicationClient {

pub async fn create_release(&self, input: Value) -> Result<Value, ClientError> {
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
Expand Down
133 changes: 133 additions & 0 deletions rust/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value> {
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<Vec<serde_json::Value>> {
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")
Expand Down Expand Up @@ -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<serde_json::Value> = config
.actions
.iter()
.map(|a| json!({ "name": a.name, "url": a.url }))
.collect();
let subscriptions: Vec<serde_json::Value> = 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(
Expand Down Expand Up @@ -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}"
);
}
}