fix(generate): judge a retries ladder on its effective result - #627
Merged
Conversation
A GitHub Actions job result is immutable, so a callback declaring retries whose base job failed reported needs.<job>.result == 'failure' for the rest of the run even after a retry shim re-invoked the workflow and succeeded. Four sinks read that frozen result: the finalize failure gate, the manifest update's <NAME>_RESULT, the run summary, and the native Deployment status. A deploy rescued by a retry therefore failed the run, and because the manifest update runs before the failure gate it first pushed a state commit recording the environment's sha and version while refusing to record the deploy that had actually happened. Each now consults the ladder's effective result: did any attempt succeed. The disjunction is over success rather than failure because a shim that never ran reports 'skipped', not 'failure'. Effective failure keeps the failure/cancelled anchor so a base job skipped by non-matching triggers stays a routine skip instead of becoming a spurious failure, and the ladder clause is parenthesized rather than resting on operator precedence. The step order is deliberately unchanged: finalize runs under always() to record the state a run actually reached, and gating the write correctly is what makes that record honest. Reordering would suppress it instead. A manifest without retries emits byte-identical output. Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
joshua-temple
force-pushed
the
fix/retries-effective-result
branch
from
July 17, 2026 09:44
7d751f6 to
23bc70e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A GitHub Actions job result is immutable. When a deploy declares
retries: N, a base job that fails and is then rescued by a retry shim leavesneeds.deploy-web.resultpinned atfailurefor the whole run, even though the environment really was deployed.Four sinks read that frozen result: the finalize failure gate, the manifest update's
<NAME>_RESULT, the run summary, and the native Deployment status. Verified on emitted output atorigin/main:So the retry deployed, the manifest denied it, and the run went red. Update Manifest (161) runs before Check for Failures (280), so it first pushed a state commit recording the environment's sha and version without
deploys.web, and then failed the run. The redness masked the wrong-but-green state write.docs/.../callbacks.mdpromised "retry up to N times before final failure"; the emitted output contradicted it.e2e/scenarios/73missed this because its callback always fails, so base-fail then retry-SUCCEED was never exercised.Fix
An effective result per ladder: did any attempt succeed?
success, not over failure. A shim that never ran reportsskipped, notfailure, so onlysuccesspositively signals an attempt completed the work.failure/cancelledanchor and adds "unless a retry rescued it". A bare!successwould turn a base job skipped by non-matching triggers into a spurious failure.&&binding tighter than||.retries: 0it collapses to the bare base result, so output is byte-identical.Ordering is deliberately unchanged. Finalize runs under
always()to record the state a run actually reached; gating the write correctly is what makes that record honest. Reordering would suppress the write thatalways()exists to guarantee.retriesin promote is scoped, not implemented.promote.goreferences none ofRetries/OnFailure/RunPolicy. A promote deploy declaringinputscompiles to a matrix job fanned across environments whoseresultis a single aggregate, so a caller-side shim would redeploy environments that already succeeded, and GHA cannot re-run only the failed legs of a dependency's matrix. Retrying a healthy production environment because an unrelated one failed is worse than not retrying. Docs now scope the claim to trunk runs; design tracked in #626.Verification
internal/generate/effective_result_test.go, written first and confirmed failing: 4 red at baseline, with the zero-retries and all-attempts-fail controls green throughout. Cases pinned: N=0, base succeeds, a middle shim succeeds, all attempts fail, plus skipped-vs-failure and the precedence grouping.diff -rreports identical. No golden churn occurred, because no golden fixture declaresretries.go build,go test(3371),go test -race(3371),golangci-lint: clean.e2e: build + vet clean.cascade verify --own-repo: 3 files, no drift.actionlinton emitted output: no expression errors.with:inputs;github.jobis the inner id for all three). Rather than fabricate a scenario, scenario 73 now pins the effective-result expressions as emitted text and records why the runtime half is absent.