Skip to content
Draft
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
26 changes: 26 additions & 0 deletions rust/src/application/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,32 @@ mod tests {
assert_eq!(data["updateApplication"]["label"], "Updated app");
}

#[tokio::test]
async fn update_application_can_activate_an_application() {
let server = MockServer::start_async().await;
let mock = server
.mock_async(|when, then| {
when.method(POST)
.path("/v1/apps/app-registry-subgraph")
.is_true(|req| {
let body = req.body_string();
body.contains("updateApplication") && body.contains(r#""status":"ACTIVE""#)
});
then.status(200).json_body(json!({
"data": { "updateApplication": { "id": "app-1", "status": "ACTIVE" } }
}));
})
.await;

let data = ApplicationClient::new(server.base_url(), "test-token")
.update_application("app-1", json!({ "status": "ACTIVE" }))
.await
.expect("activate application");

mock.assert_async().await;
assert_eq!(data["updateApplication"]["status"], "ACTIVE");
}

// httpmock can't sequence responses, so retries are verified by hit count
// (exhaustion) rather than a fail-then-succeed sequence.

Expand Down
70 changes: 66 additions & 4 deletions rust/src/application/commands/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ fn deploy_result_event(
"releaseId": release_id,
"extensions": extensions,
"releaseStatus": "ACTIVE",
"applicationStatus": "ACTIVE",
},
"next_actions": deploy_next_actions(name),
})
Expand Down Expand Up @@ -120,8 +121,10 @@ pub(super) fn command() -> RuntimeCommandSpec {
(SEC001–SEC010/SEC012 AST + SEC011 package scripts) and the \
post-bundle regex scanner (SEC101–SEC115) on each extension, then \
upload the artifacts to the latest release of the named \
application. Progress is streamed as JSON events. A release must \
exist before deploying; create one with \
application and activate both the release and application. An \
already-active application skips the lifecycle mutation. Progress \
is streamed as JSON events. A release must exist before deploying; \
create one with \
`gddy platform app release`.",
)
.with_system("applications")
Expand Down Expand Up @@ -175,6 +178,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
return Err(fail_deploy(&sender, err).await);
}
let application_id = app["id"].as_str().unwrap_or("").to_owned();
let application_status = app["status"].as_str().map(str::to_owned);
sender
.send(json!({ "type": "step", "name": "application.lookup", "status": "completed", "id": application_id }))
.await;
Expand Down Expand Up @@ -233,6 +237,18 @@ pub(super) fn command() -> RuntimeCommandSpec {
)
.await?;

tap_deploy_err(
&sender,
activate_application(
&client,
&sender,
&application_id,
application_status.as_deref(),
)
.await,
)
.await?;

tap_deploy_err(
&sender,
sync_manifest_metadata(&client, &sender, &application_id, &config).await,
Expand Down Expand Up @@ -262,8 +278,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
)
}

/// Activate the release. The App Registry lifecycle owns the parent
/// application's status transition; deploy must not directly mutate it.
/// Activate the release before activating the parent application.
async fn activate_release(
client: &ApplicationClient,
sender: &StreamSender,
Expand All @@ -284,6 +299,44 @@ async fn activate_release(
Ok(())
}

/// Promote the parent application to `ACTIVE` after its release is active.
/// Avoid the lifecycle mutation when lookup already returned `ACTIVE`.
async fn activate_application(
client: &ApplicationClient,
sender: &StreamSender,
application_id: &str,
current_status: Option<&str>,
) -> cli_engine::Result<()> {
if !application_needs_activation(current_status) {
sender
.send(json!({
"type": "step",
"name": "application.activate",
"status": "skipped",
"reason": "already active",
}))
.await;
return Ok(());
}

sender
.send(json!({ "type": "step", "name": "application.activate", "status": "started" }))
.await;
client
.update_application(application_id, json!({ "status": "ACTIVE" }))
.await
.map_err(super::client_err)?;
sender
.send(json!({ "type": "step", "name": "application.activate", "status": "completed" }))
.await;

Ok(())
}

fn application_needs_activation(current_status: Option<&str>) -> bool {
current_status != Some("ACTIVE")
}

/// Synchronize application-level manifest fields without changing lifecycle
/// status. Actions and subscriptions belong to the release and are handled by
/// `platform app release`.
Expand Down Expand Up @@ -404,6 +457,7 @@ mod tests {
assert_eq!(event["result"]["releaseId"], "rel-456");
assert_eq!(event["result"]["extensions"], 2);
assert_eq!(event["result"]["releaseStatus"], "ACTIVE");
assert_eq!(event["result"]["applicationStatus"], "ACTIVE");
assert_eq!(
event["next_actions"].as_array().map(|a| a.len()),
Some(3),
Expand All @@ -423,6 +477,14 @@ mod tests {
);
}

#[test]
fn application_activation_is_skipped_only_when_already_active() {
assert!(!super::application_needs_activation(Some("ACTIVE")));
assert!(super::application_needs_activation(Some("INACTIVE")));
assert!(super::application_needs_activation(Some("VERIFYING")));
assert!(super::application_needs_activation(None));
}

#[test]
fn manifest_metadata_input_syncs_all_application_fields_without_status() {
let config = crate::config::Config {
Expand Down
2 changes: 1 addition & 1 deletion rust/src/platform/guides/platform-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Resends every action, subscription, UI extension, and settings entry currently i
gddy platform app deploy
```

Bundles, security-scans, and uploads the extensions declared in `godaddy.toml`, streaming progress as JSON events.
Bundles, security-scans, and uploads the extensions declared in `godaddy.toml`, then activates the latest release and its parent application. An application that is already active skips the application lifecycle mutation. Progress is streamed as JSON events.

## 5. Enable / disable per store

Expand Down
Loading