Skip to content

Commit 9d49a84

Browse files
pRizzcursoragent
andcommitted
fix(update): surface failures when updating opencode
Capture exec exit codes and command output during opencode updates so failures are visible. Validate the expected commit after update to avoid reporting success when the update did not apply. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1e7a71b commit 9d49a84

3 files changed

Lines changed: 120 additions & 13 deletions

File tree

packages/cli-rust/src/commands/update.rs

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ use opencode_cloud_core::config::load_config;
1212
use opencode_cloud_core::docker::update::tag_current_as_previous;
1313
use opencode_cloud_core::docker::{
1414
CONTAINER_NAME, DockerClient, IMAGE_TAG_DEFAULT, ImageState, ProgressReporter, build_image,
15-
container_exists, container_is_running, exec_command, get_cli_version, has_previous_image,
16-
pull_image, rollback_image, save_state, setup_and_start, stop_service,
15+
container_exists, container_is_running, exec_command, exec_command_with_status,
16+
get_cli_version, has_previous_image, pull_image, rollback_image, save_state, setup_and_start,
17+
stop_service,
1718
};
1819

1920
/// Arguments for the update command
@@ -394,10 +395,8 @@ pub(crate) async fn cmd_update_opencode(
394395
style(current_version.unwrap_or_else(|| "unknown".to_string())).dim(),
395396
style(current_commit.unwrap_or_else(|| "unknown".to_string())).dim()
396397
);
397-
eprintln!(
398-
"Next hash: {}",
399-
style(next_commit.unwrap_or_else(|| "unknown".to_string())).dim()
400-
);
398+
let next_hash = next_commit.as_deref().unwrap_or("unknown");
399+
eprintln!("Next hash: {}", style(next_hash).dim());
401400
eprintln!();
402401
}
403402

@@ -422,9 +421,22 @@ pub(crate) async fn cmd_update_opencode(
422421
pkill -f "/opt/opencode/bin/opencode" || true
423422
pkill -f "opencode-broker" || true
424423
"#;
425-
exec_command(&client, CONTAINER_NAME, vec!["bash", "-lc", stop_cmd])
426-
.await
427-
.map_err(|e| anyhow!("Failed to stop opencode processes: {e}"))?;
424+
let (stop_output, stop_status) =
425+
exec_command_with_status(&client, CONTAINER_NAME, vec!["bash", "-lc", stop_cmd])
426+
.await
427+
.map_err(|e| anyhow!("Failed to stop opencode processes: {e}"))?;
428+
if !quiet && !stop_output.trim().is_empty() {
429+
eprintln!(
430+
"{} Stop output:\n{}",
431+
style("[info]").cyan(),
432+
stop_output.trim()
433+
);
434+
}
435+
if stop_status != 0 {
436+
return Err(anyhow!(
437+
"Failed to stop opencode processes (exit {stop_status}).\n{stop_output}"
438+
));
439+
}
428440

429441
let update_script = format!(
430442
r#"set -euo pipefail
@@ -455,9 +467,32 @@ rm -rf "$REPO"
455467
"#
456468
);
457469

458-
exec_command(&client, CONTAINER_NAME, vec!["bash", "-lc", &update_script])
459-
.await
460-
.map_err(|e| anyhow!("Failed to update opencode: {e}"))?;
470+
let (update_output, update_status) =
471+
exec_command_with_status(&client, CONTAINER_NAME, vec!["bash", "-lc", &update_script])
472+
.await
473+
.map_err(|e| anyhow!("Failed to update opencode: {e}"))?;
474+
if !quiet && !update_output.trim().is_empty() {
475+
eprintln!(
476+
"{} Update output:\n{}",
477+
style("[info]").cyan(),
478+
update_output.trim()
479+
);
480+
}
481+
if update_status != 0 {
482+
return Err(anyhow!(
483+
"Opencode update failed (exit {update_status}).\n{update_output}"
484+
));
485+
}
486+
487+
if let Some(expected) = next_commit.as_deref() {
488+
let updated_commit = get_current_opencode_commit(&client).await;
489+
if updated_commit.as_deref() != Some(expected) {
490+
let found = updated_commit.unwrap_or_else(|| "unknown".to_string());
491+
return Err(anyhow!(
492+
"Opencode update did not apply (expected {expected}, found {found}).\n{update_output}"
493+
));
494+
}
495+
}
461496

462497
spinner.success("Opencode updated, restarting service...");
463498

packages/core/src/docker/exec.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,76 @@ pub async fn exec_command(
8282
Ok(output)
8383
}
8484

85+
/// Execute a command and capture output plus exit code
86+
///
87+
/// Returns a tuple of (output, exit_code). Exit code is -1 if not available.
88+
pub async fn exec_command_with_status(
89+
client: &DockerClient,
90+
container: &str,
91+
cmd: Vec<&str>,
92+
) -> Result<(String, i64), DockerError> {
93+
let exec_config = CreateExecOptions {
94+
attach_stdout: Some(true),
95+
attach_stderr: Some(true),
96+
cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
97+
user: Some("root".to_string()),
98+
..Default::default()
99+
};
100+
101+
let exec = client
102+
.inner()
103+
.create_exec(container, exec_config)
104+
.await
105+
.map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
106+
107+
let exec_id = exec.id.clone();
108+
let start_config = StartExecOptions {
109+
detach: false,
110+
..Default::default()
111+
};
112+
113+
let mut output = String::new();
114+
115+
match client
116+
.inner()
117+
.start_exec(&exec.id, Some(start_config))
118+
.await
119+
.map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
120+
{
121+
StartExecResults::Attached {
122+
output: mut stream, ..
123+
} => {
124+
while let Some(result) = stream.next().await {
125+
match result {
126+
Ok(log_output) => {
127+
output.push_str(&log_output.to_string());
128+
}
129+
Err(e) => {
130+
return Err(DockerError::Container(format!(
131+
"Error reading exec output: {e}"
132+
)));
133+
}
134+
}
135+
}
136+
}
137+
StartExecResults::Detached => {
138+
return Err(DockerError::Container(
139+
"Exec unexpectedly detached".to_string(),
140+
));
141+
}
142+
}
143+
144+
let inspect = client
145+
.inner()
146+
.inspect_exec(&exec_id)
147+
.await
148+
.map_err(|e| DockerError::Container(format!("Failed to inspect exec: {e}")))?;
149+
150+
let exit_code = inspect.exit_code.unwrap_or(-1);
151+
152+
Ok((output, exit_code))
153+
}
154+
85155
/// Execute a command with stdin input and capture output
86156
///
87157
/// Creates an exec instance with stdin attached, writes the provided data to

packages/core/src/docker/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ pub use update::{UpdateResult, has_previous_image, rollback_image, update_image}
5050
pub use version::{VERSION_LABEL, get_cli_version, get_image_version, versions_compatible};
5151

5252
// Container exec operations
53-
pub use exec::{exec_command, exec_command_exit_code, exec_command_with_stdin};
53+
pub use exec::{
54+
exec_command, exec_command_exit_code, exec_command_with_status, exec_command_with_stdin,
55+
};
5456

5557
// User management operations
5658
pub use users::{

0 commit comments

Comments
 (0)