Skip to content

fix(runs): mark job/step runs cancelled when a run is cancelled - #145

Merged
KrisPowers merged 7 commits into
mainfrom
fix/silent-job-failure-visibility
Jul 27, 2026
Merged

fix(runs): mark job/step runs cancelled when a run is cancelled#145
KrisPowers merged 7 commits into
mainfrom
fix/silent-job-failure-visibility

Conversation

@KrisPowers

Copy link
Copy Markdown
Owner

Summary

  • Cancelling a run only ever updated workflow_runs.status; the nested job_runs/step_runs rows the run detail page renders (job/step tree, GitHub-style) stayed stuck at running/pending forever instead of showing cancelled/skipped/failed.
  • Guarded set_run_status/set_job_status/set_step_status against overwriting an already-terminal row, so a background job process finishing after the fact can't silently flip a cancelled run back to succeeded/failed.

Test plan

  • cargo test -p atk-db --lib queries::runs:: passes
  • cargo check -p atk-db -p actions-toolkit-backend passes
  • Manually cancel a running workflow and confirm its job/step rows in the run detail page show as cancelled instead of stuck running/pending

Cancelling a run only ever updated workflow_runs.status; the nested
job_runs/step_runs rows the run detail page actually renders stayed
stuck at running/pending forever. Also guard set_run_status/
set_job_status/set_step_status against overwriting an already-terminal
row, so a background job process finishing after the fact can't
silently flip a cancelled run back to succeeded/failed.
A step failing with PowerShell's CLIXML "Cannot find drive" error came
back with failure_hint: null, since diagnose() only recognized
missing-command patterns. Give it a name and point at the runner log
instead of leaving cargo build step failed as the only visible signal.
A failed run showed the job's first step starting nearly 9 minutes
after the job itself started, with nothing in between to say why.
Log elapsed time for checkout and shard/sandbox setup so the next
occurrence shows where the time actually went instead of leaving
another unexplained gap to guess at.
Adds a lifecycle_events table plus RunClient/RCP plumbing
(start_phase/finish_phase) so a shell can record when each phase of
the event pipeline starts and ends, even though it has no direct
database access of its own. finished_at stays null until a phase
actually ends, so a hung phase is directly queryable instead of only
inferable after the fact from a job's total duration.
…/step

Wraps bucket_create, rcp_listener_bind (dispatch.rs), the shell
capability probe and RCP handshake (shell_run.rs), and checkout,
shard_create, step_exec (executor.rs) with start_phase/finish_phase
calls. Each phase's row is written before the work starts, so a hang
in any of them shows up as finished_at IS NULL immediately, rather
than only being inferable from a job's total duration after the fact.
Adds atk_bucket::PhaseRecorder, a sync callback create_job_shard/
exec_step invoke around AppContainer profile creation, each ACL grant
(including one per ancestor directory, since a single missing grant
anywhere in that chain is what actually breaks PowerShell's working
directory resolution), the extra-mount grants, and the sandboxed
process's own spawn/wait. Existing call sites are unaffected: the
plain create_job_shard/exec_step now delegate to *_with_recorder
variants with a no-op recorder, so no existing test needed to change
behavior, only its call site's arity.

RunClientPhaseRecorder in core bridges these sync callbacks into
RunClient's async start_phase/finish_phase, fire-and-forget the same
way exec_step's on_line callback already reaches a RunClient from a
blocking-pool thread.

Also switches start_phase to a caller-supplied id (matching
shards::create's convention) instead of a server-generated one
returned from the call: a sync callback deep inside blocking OS code
has no way to await a response before handing a token back to its own
caller, so the id has to already be known before the write is fired.

Linux is left uninstrumented for now: this crate's Linux backend can't
be exercised or verified from a Windows dev machine, and the bug this
was built to diagnose is Windows-specific anyway.
RunTopology (GET /runs/:id/topology) now includes every recorded
phase for the run, oldest first. The UI's exportRunReport already
nests this response under backend.topology, so the downloadable
failed-run report gains a phases[] timeline for free: bucket_create,
rcp_listener_bind, checkout, shard_create (and its AppContainer/ACL
sub-phases), step_exec (and its process spawn/wait sub-phases), each
with started_at/finished_at so a gap between two phases, or a phase
with no finished_at at all, is readable directly instead of inferred
from job/step durations after the fact.
@KrisPowers
KrisPowers merged commit 6feab42 into main Jul 27, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant