Relationships
#3221 workflow recover misjudges whether an unknown step is guarded: forEach iterations read as unguarded, and a same-named step in another job is matched
Opened by zack · 10/9/2026· Shipped 10/9/2026
Description
assessRecoveryForRun (src/domain/workflows/recovery_assessment.ts) decides whether an interrupted run can be recovered without operator acknowledgement by checking that every step left in unknown status has a guard. It finds each step's definition by the run step's name alone, across all jobs:
const step = workflow.jobs.flatMap((j) => j.steps).find((s) => s.name === stepName);That lookup is wrong in two ways.
1. A guarded step expanded by forEach reads as unguarded
A forEach iteration is stored on the run under its expanded name (for example deploy-prod), which no step of the definition has, so the lookup misses and the step is counted as unguarded. Recovery is refused and asks for acknowledgement even though the step it was expanded from has a guard. The step run does record the name of the step it was expanded from (forEachTemplate), which is not consulted.
2. An unguarded step is treated as guarded when another job has a guarded step of the same name
run.unknownSteps() returns step names without their job, and the lookup takes the first step of that name in any job. With job a holding a guarded step deploy and job b holding an unguarded step deploy, an unknown deploy in job b is reported as guarded and canAutoRecover is true. This direction skips the operator acknowledgement that an unguarded unknown step is meant to require.
Steps to reproduce
Found by reading the code during swamp-club#3218 and confirmed in-process by calling assessRecoveryForRun on interrupted runs built with WorkflowRun.fromData (not yet reproduced through the CLI, which needs a run interrupted mid-step):
| Case | canAutoRecover | guardedSteps | unguardedSteps |
|---|---|---|---|
guarded plain step deploy-prod unknown |
true | deploy-prod | (none) |
| same step expanded by forEach, unknown | false | (none) | deploy-prod |
unguarded deploy in job b unknown, guarded deploy in job a |
true | deploy | (none) |
Expected
- A forEach iteration is judged by the guard of the step it was expanded from.
- A step is looked up in its own job, so the third case reports
deployas unguarded and refuses automatic recovery.
Notes
Same root cause family as swamp-club#3217 and swamp-club#3218: a run step resolved against the definition by name. Callers are swamp workflow recover (src/cli/commands/workflow_recover.ts) and WorkflowExecutionService.assessRecovery.
Environment
- swamp at main 8d880ec7 plus the swamp-club#3218 branch (which does not touch this file)
- Linux x86_64
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.