Drive run outcome from RunFinished event alone, drop exit code check
Since quire-ci exits 0 for all expected outcomes, the exit code is no
longer needed to distinguish pass from fail. RunFinished present means
clean execution (Success → Complete, PipelineFailure → Failed); absent
means the process crashed or was killed (None → process-crashed, Err).
The fake quire-ci in tests now writes RunFinished(success) to the
--events file so the server can resolve the outcome correctly.
https://claude.ai/code/session_0135zw5K7qsn9thYkkpjX6HE
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 3e94d7d..06ec670 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -441,8 +441,27 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir for fake quire-ci");
if cfg!(unix) {
let path = dir.path().join("quire-ci");
- fs_err::write(&path, format!("#!/bin/sh\nexit {exit_code}\n"))
- .expect("write fake quire-ci");
+ // For a clean exit, write RunFinished(success) to the --events
+ // file so the server can determine the outcome from events alone.
+ let script = if exit_code == 0 {
+ r#"#!/bin/sh
+events=
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --events) events="$2"; shift 2 ;;
+ *) shift ;;
+ esac
+done
+if [ -n "$events" ] && [ "$events" != "null" ]; then
+ printf '{"at_ms":0,"type":"run_finished","outcome":"success"}\n' > "$events"
+fi
+exit 0
+"#
+ .to_string()
+ } else {
+ format!("#!/bin/sh\nexit {exit_code}\n")
+ };
+ fs_err::write(&path, script).expect("write fake quire-ci");
use std::os::unix::fs::PermissionsExt;
fs_err::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
.expect("chmod fake quire-ci");
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 1ed6b73..a683c1f 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -397,9 +397,9 @@ impl Run {
);
}
- // Ingest events regardless of exit — partial results are still
- // useful in the UI. A failure to read or parse goes to the log
- // but doesn't mask the run's own pass/fail outcome.
+ // Ingest events before checking outcome — partial results from a
+ // crashed run are still useful in the UI. A parse failure is
+ // logged but doesn't mask the run outcome.
let run_outcome = match self.ingest_events(&events_path) {
Ok(outcome) => outcome,
Err(e) => {
@@ -412,19 +412,21 @@ impl Run {
}
};
- if !status.success() {
- self.transition(RunState::Failed, Some("process-crashed"))?;
- return Err(Error::ProcessFailed {
- exit: status.code(),
- });
- }
-
+ // RunFinished is the sole signal for outcome: present means quire-ci
+ // reached the end cleanly; absent means it crashed or was killed.
+ // The exit code is kept in the error for diagnostics only.
match run_outcome {
+ Some(quire_core::ci::event::RunOutcome::Success) => {
+ self.transition(RunState::Complete, None)?;
+ }
Some(quire_core::ci::event::RunOutcome::PipelineFailure) => {
self.transition(RunState::Failed, Some("pipeline-failure"))?;
}
- _ => {
- self.transition(RunState::Complete, None)?;
+ None => {
+ self.transition(RunState::Failed, Some("process-crashed"))?;
+ return Err(Error::ProcessFailed {
+ exit: status.code(),
+ });
}
}
Ok(())