Mark superseded commits honestly instead of skipped (closes #152)
All checks were successful
check / check (push) Successful in 3m28s
All checks were successful
check / check (push) Successful in 3m28s
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; the root-commit case is detected explicitly so every other rev-list failure aborts too; and 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 and an ancestor whose status read answers HTTP 500, so jq joins the builder image to run them.
This commit is contained in:
30
README.md
30
README.md
@@ -258,6 +258,8 @@ 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
|
||||
@@ -1229,11 +1231,29 @@ way.
|
||||
|
||||
The workflow's first step 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.
|
||||
and records that cancellation as a `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, 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, job name and event — the same
|
||||
three values Gitea builds the context from — and fails loudly when no
|
||||
status carries that context, so renaming the workflow or the job cannot
|
||||
silently disable the rewrite.
|
||||
|
||||
## TODO
|
||||
|
||||
|
||||
Reference in New Issue
Block a user