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
72 changes: 68 additions & 4 deletions rust/src/application/commands/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,13 @@ pub(super) fn command() -> RuntimeCommandSpec {

tap_deploy_err(
&sender,
finalize_deploy_activation(&client, &sender, &application_id, &release_id).await,
activate_release(&client, &sender, &application_id, &release_id).await,
)
.await?;

tap_deploy_err(
&sender,
sync_manifest_metadata(&client, &sender, &application_id, &config).await,
)
.await?;

Expand All @@ -254,9 +260,9 @@ pub(super) fn command() -> RuntimeCommandSpec {
)
}

/// Finalize a deploy by activating the release. Do not follow activation with
/// an `updateApplication` status mutation.
async fn finalize_deploy_activation(
/// Activate the release. The App Registry lifecycle owns the parent
/// application's status transition; deploy must not directly mutate it.
async fn activate_release(
client: &ApplicationClient,
sender: &StreamSender,
application_id: &str,
Expand All @@ -272,9 +278,40 @@ async fn finalize_deploy_activation(
sender
.send(json!({ "type": "step", "name": "release.activate", "status": "completed" }))
.await;

Ok(())
}

/// Synchronize application-level manifest fields without changing lifecycle
/// status. Actions and subscriptions belong to the release and are handled by
/// `platform app release`.
async fn sync_manifest_metadata(
client: &ApplicationClient,
sender: &StreamSender,
application_id: &str,
config: &crate::config::Config,
) -> cli_engine::Result<()> {
sender
.send(json!({ "type": "step", "name": "application.sync", "status": "started" }))
.await;
client
.update_application(application_id, manifest_metadata_input(config))
.await
.map_err(super::client_err)?;
sender
.send(json!({ "type": "step", "name": "application.sync", "status": "completed" }))
.await;
Ok(())
}

fn manifest_metadata_input(config: &crate::config::Config) -> Value {
json!({
"url": config.url,
"proxyUrl": config.proxy_url,
"authorizationScopes": config.authorization_scopes,
})
}

#[cfg(test)]
mod tests {
/// `deploy --follow` must end with exactly one terminal line. `StreamSender`
Expand Down Expand Up @@ -383,4 +420,31 @@ mod tests {
"first next action should enable on a store: {event}"
);
}

#[test]
fn manifest_metadata_input_syncs_all_application_fields_without_status() {
let config = crate::config::Config {
name: "my-app".to_owned(),
client_id: "550e8400-e29b-41d4-a716-446655440000".to_owned(),
description: Some("test".to_owned()),
version: "1.2.3".to_owned(),
url: "https://app.example.com".to_owned(),
proxy_url: "https://api.example.com".to_owned(),
authorization_scopes: vec!["openid".to_owned(), "profile".to_owned()],
actions: vec![],
subscriptions: None,
dependencies: vec![],
extensions: None,
};

let input = super::manifest_metadata_input(&config);

assert_eq!(input["url"], "https://app.example.com");
assert_eq!(input["proxyUrl"], "https://api.example.com");
assert_eq!(
input["authorizationScopes"],
serde_json::json!(["openid", "profile"])
);
assert!(input.get("status").is_none());
}
}
27 changes: 25 additions & 2 deletions rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mod truncation;
mod update;
mod webhook;

use std::{process::ExitCode, sync::Arc};
use std::{io::Write as _, process::ExitCode, sync::Arc};

use cli_engine::{BuildInfo, Cli, CliConfig, Module};

Expand Down Expand Up @@ -98,7 +98,30 @@ async fn main() -> ExitCode {
.with_modules(all_modules()),
);

cli.execute().await
execute_without_stdout_lock(&cli).await
}

/// Execute without holding stdout's global lock for the entire command.
///
/// Streaming commands write their progress directly through Tokio's stdout.
/// `Cli::execute` keeps a `StdoutLock` alive until the command has returned,
/// which prevents that writer from acquiring stdout and leaves the command
/// waiting for its own stream to drain. Passing `Stdout`/`Stderr` directly
/// keeps the final envelope writes synchronized without blocking streaming
/// progress events.
async fn execute_without_stdout_lock(cli: &Cli) -> ExitCode {
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
match cli
.execute_from(std::env::args_os(), &mut stdout, &mut stderr)
.await
{
Ok(code) => code,
Err(err) => {
drop(writeln!(stderr, "{err}"));
ExitCode::from(1)
}
}
}

#[cfg(test)]
Expand Down
Loading