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
79 changes: 60 additions & 19 deletions src/commands/runtime/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,14 @@ impl RuntimeBuildCommand {
}

// Generate TUF delegation staging and write into the build volume.
// If signing keys are not configured this step is skipped with a warning.
// Deliberately nonfatal: many projects have no signing configuration,
// so build must succeed without staging; a Level-2 project whose
// staging fails here surfaces a self-explaining error at deploy or
// upload time instead. The skip notice routes through the active
// renderer when one exists — print_* is suppressed in TUI/JSON
// modes, so it would otherwise vanish exactly where builds usually
// run. TUI queues it above the task region; JSON mode writes it to
// stderr without corrupting the NDJSON stream.
let project_dir = std::path::Path::new(&self.config_path)
.parent()
.unwrap_or(std::path::Path::new("."));
Expand All @@ -807,10 +814,17 @@ impl RuntimeBuildCommand {
)
.await
{
print_info(
&format!("Skipping TUF delegation staging: {e:#}"),
OutputLevel::Normal,
);
let msg = format!("Skipping TUF delegation staging: {e:#}");
if let Some(renderer) = crate::utils::tui::get_active_renderer() {
renderer.print_above(&msg);
} else if crate::utils::output_format::is_json_output_active() {
// Verbose JSON builds create no renderer, and print_info is
// suppressed in JSON mode — write to stderr directly so the
// notice survives without touching the NDJSON stdout stream.
eprintln!("{msg}");
} else {
print_info(&msg, OutputLevel::Normal);
}
}

Ok(())
Expand Down Expand Up @@ -1002,10 +1016,22 @@ echo -n '}}'
env_vars: self.runtime_env_vars(),
..Default::default()
};
let hash_output =
run_container_command_with_output(container_helper, hash_run_config, runs_on_context)
.await?
.context("Hash collection script produced no output")?;
let hash_result =
run_container_command_capture(container_helper, hash_run_config, runs_on_context)
.await?;
if !hash_result.success {
if self.verbose {
crate::utils::container::print_failure_notice(&format!(
"Full container stderr:\n{}",
hash_result.stderr
));
}
anyhow::bail!(crate::utils::container::container_failure_message(
"Hash collection failed in the SDK container",
&hash_result.stderr,
));
}
let hash_output = hash_result.stdout.trim().to_string();

let collection: update_repo::HashCollectionOutput =
serde_json::from_str(&hash_output).context("Failed to parse hash collection output")?;
Expand Down Expand Up @@ -1110,10 +1136,22 @@ cp /opt/src/.tuf-staging-tmp/delegations/runtime-{runtime_uuid}.json \
env_vars: self.runtime_env_vars(),
..Default::default()
};
let hash_output =
run_container_command_with_output(container_helper, hash_run_config, runs_on_context)
.await?
.context("Hash collection script produced no output")?;
let hash_result =
run_container_command_capture(container_helper, hash_run_config, runs_on_context)
.await?;
if !hash_result.success {
if self.verbose {
crate::utils::container::print_failure_notice(&format!(
"Full container stderr:\n{}",
hash_result.stderr
));
}
anyhow::bail!(crate::utils::container::container_failure_message(
"Hash collection failed in the SDK container",
&hash_result.stderr,
));
}
let hash_output = hash_result.stdout.trim().to_string();

let collection: update_repo::HashCollectionOutput =
serde_json::from_str(&hash_output).context("Failed to parse hash collection output")?;
Expand Down Expand Up @@ -2911,19 +2949,22 @@ async fn run_container_command(
}
}

/// Helper function to run a container command and capture its output,
/// using shared context if available.
async fn run_container_command_with_output(
/// Run a container command and keep the full result so callers can put the
/// script's stderr into their own error — required wherever the error must
/// self-explain in TUI/JSON modes, where printing side effects are
/// suppressed. Dispatches to the remote capture variant when a
/// RunsOnContext is active.
async fn run_container_command_capture(
container_helper: &SdkContainer,
config: RunConfig,
runs_on_context: Option<&RunsOnContext>,
) -> Result<Option<String>> {
) -> Result<crate::utils::container::ContainerRunOutput> {
if let Some(context) = runs_on_context {
container_helper
.run_in_container_with_output_remote(&config, context)
.run_in_container_capture_remote(&config, context)
.await
} else {
container_helper.run_in_container_with_output(config).await
container_helper.run_in_container_capture(config).await
}
}

Expand Down
55 changes: 44 additions & 11 deletions src/commands/runtime/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ impl RuntimeDeployCommand {
}
}

/// Compose a phase failure message that carries the container script's
/// own stderr diagnostics, so both the CLI error and the JSON step_error
/// event name the real problem.
fn container_failure_message(context: &str, stderr: &str) -> String {
crate::utils::container::container_failure_message(context, stderr)
}

pub async fn execute(&self) -> Result<()> {
let composed = match &self.composed_config {
Some(cc) => Arc::clone(cc),
Expand Down Expand Up @@ -267,19 +274,33 @@ impl RuntimeDeployCommand {
..Default::default()
};

let output = match container_helper
.run_in_container_with_output(run_config)
.await
{
Ok(o) => o,
// Capture the full result so a container failure is reported as
// such, not silently converted into a "missing stamps" error.
let output = match container_helper.run_in_container_capture(run_config).await {
Ok(out) if out.success => out.stdout,
Ok(out) => {
if self.verbose {
crate::utils::container::print_failure_notice(&format!(
"Full container stderr:\n{}",
out.stderr
));
}
let msg = Self::container_failure_message(
"Reading build stamps failed in the SDK container",
&out.stderr,
);
Self::emit_phase_error(PHASE_STAMPS, &msg);
Self::emit_phase_status(PHASE_STAMPS, "failed");
return Err(anyhow::anyhow!(msg));
}
Err(e) => {
Self::emit_phase_error(PHASE_STAMPS, &e.to_string());
Self::emit_phase_status(PHASE_STAMPS, "failed");
return Err(e);
}
};

let validation = validate_stamps_batch(&required, output.as_deref().unwrap_or(""), &[]);
let validation = validate_stamps_batch(&required, output.trim(), &[]);

if !validation.is_satisfied() {
let msg = format!("Cannot deploy runtime '{}'", self.runtime_name);
Expand Down Expand Up @@ -318,14 +339,26 @@ impl RuntimeDeployCommand {
..Default::default()
};

// Capture the full result so the script's own diagnostics (e.g. the
// signing-key prerequisite for --connect-sign) reach the user instead
// of a generic "produced no output" error.
let hash_output = match container_helper
.run_in_container_with_output(hash_run_config)
.run_in_container_capture(hash_run_config)
.await
{
Ok(Some(out)) => out,
Ok(None) => {
let msg = "Hash collection script produced no output";
Self::emit_phase_error(PHASE_HASH, msg);
Ok(out) if out.success => out.stdout.trim().to_string(),
Ok(out) => {
if self.verbose {
crate::utils::container::print_failure_notice(&format!(
"Full container stderr:\n{}",
out.stderr
));
}
let msg = Self::container_failure_message(
"Hash collection failed in the SDK container",
&out.stderr,
);
Self::emit_phase_error(PHASE_HASH, &msg);
Self::emit_phase_status(PHASE_HASH, "failed");
return Err(anyhow::anyhow!(msg));
}
Expand Down
Loading
Loading