Mark superseded commits honestly instead of skipped (closes #152)
All checks were successful
check / check (push) Successful in 3m5s
All checks were successful
check / check (push) Successful in 3m5s
Gitea cancels an in-flight run when a newer commit lands on the same branch and records the cancellation as `failure` / "Has been cancelled". The workflow rewrote that to `skipped`, but Gitea's Combine() folds `skipped` into `success`, so the combined-status API returned green for a commit nothing had ever tested. Rewrite it to `failure` / "Superseded by a newer commit; never tested" instead: red-but-honest, and never `pending`, which would block the commit forever. Re-running the superseded commit would have been better still, but is not reachable on this Gitea (1.25.4): its API exposes no rerun endpoint, workflow dispatch takes a ref rather than a SHA, and every replay would be a full uncached build with no bound on how many pile up behind a burst of merges. The step also stops hardcoding its status context: the logic moves into script/ci-mark-superseded, which derives the context from the workflow name, job id and event, and fails loudly when no status on the commit being built carries that context, so renaming the workflow or the job cannot silently disable the rewrite. The derivation is not byte-exact with Gitea's own rule -- Gitea uses the job's display `name:` where the runner exports the job id -- so adding a `name:` to the job turns every push red rather than quietly doing nothing; the script header says so, because that loud failure is the point. That is item 2 of #147; item 1 there is untouched. Nothing about the walk may fail quietly, since the script exists to stop CI lying quietly. An ANCESTOR_LIMIT that is set but not a positive integer aborts instead of passing an unusable value to git and discarding the error. A shallow clone aborts on `git rev-parse --is-shallow-repository`: the graft makes the parent unresolvable, so a shallow checkout is indistinguishable from a root commit and the walk would exit 0 having marked nothing -- one dropped `fetch-depth: 0` away, which the workflow comment now records. A genuine root commit still exits 0, an unknown SHA has already been rejected by the context read's 404, and the walk carries no `|| true`, so a rev-list failure aborts. Per-ancestor status reads carry the same `--retry 3 --max-time 30` as the head-commit read and abort on failure rather than losing curl's exit status through a pipe. Tests drive the script against a fake Gitea covering the cancelled, laundered-skipped, genuinely-failed, passing and renamed cases, an unparseable ANCESTOR_LIMIT, an ancestor whose status read answers HTTP 500, and a depth-1 clone, so jq joins the builder image to run them.
This commit is contained in:
33
README.md
33
README.md
@@ -282,6 +282,8 @@ are inline commands with no script behind them. We provide:
|
||||
- `script/docker` — build the Docker image tagged via `script/projectname`
|
||||
- `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile
|
||||
runs the checks, so a green build implies a green repo)
|
||||
- `script/ci-mark-superseded` — CI helper: mark the commits whose run a
|
||||
newer push cancelled (see [CI gate honesty](#ci-gate-honesty))
|
||||
- `script/precommit` — pre-commit checks (`go mod tidy` guard, then
|
||||
`script/check`)
|
||||
- `script/install-precommit` — install the git pre-commit hook that
|
||||
@@ -1445,11 +1447,32 @@ way.
|
||||
A separate workflow step, run before the fingerprint is written, covers
|
||||
a second way the gate lied: Gitea cancels an in-flight run when a newer
|
||||
commit lands on the same branch and records that cancellation as a
|
||||
`failure` status, marking a commit red that was never tested.
|
||||
Cancellation is unconditional server-side for
|
||||
push events, so the superseding run rewrites the exact
|
||||
`Has been cancelled` status to `skipped`. Genuine failures are never
|
||||
touched.
|
||||
`failure` status, so a commit nothing ever tested reads as a test
|
||||
result. Cancellation is unconditional server-side for push events, so
|
||||
the superseding run calls `script/ci-mark-superseded`, which rewrites
|
||||
that exact status to `failure` /
|
||||
`Superseded by a newer commit; never tested`.
|
||||
|
||||
The state stays `failure` on purpose: Gitea's combined status folds
|
||||
`skipped` into `success`, so marking a never-tested commit `skipped`
|
||||
made the status API report green for it, indistinguishable from a commit
|
||||
that passed. Reading a commit's status on this repo therefore goes:
|
||||
|
||||
- `success` / `Successful in ...` — the checks ran and passed.
|
||||
- `failure` / `Failing after ...` — the checks ran and failed.
|
||||
- `failure` / `Superseded by a newer commit; never tested` — the run was
|
||||
cancelled, by a newer push or by hand, and nothing was verified about
|
||||
this commit. Test the commit itself before concluding anything about
|
||||
it.
|
||||
|
||||
Genuine failures and successes are never touched, and no status is left
|
||||
`pending`, which would block the commit indefinitely. The step derives
|
||||
its context string from the workflow name, the job **id** and the event.
|
||||
That is deliberately not byte-identical to Gitea's own rule, which uses
|
||||
the job's display `name:` where the runner exports the id, so giving the
|
||||
job a `name:` — or renaming the workflow — makes the derived context
|
||||
stop matching. The step fails loudly when no status on the commit
|
||||
carries that context, so no rename can silently disable the rewrite.
|
||||
|
||||
## TODO
|
||||
|
||||
|
||||
Reference in New Issue
Block a user