A build is queued against a commit sha. A force-push orphans the old head, and
every build still queued for it fails at clone with "unable to read tree" —
failures indistinguishable from real ones in build list. That is routine here:
signed commits mean fast-forward-only merges, so any branch whose target
advances gets rebased and force-pushed. This merge request's own branch did it
twice.
A runner claiming a build now checks the sha is still reachable, cancels it if
not, and claims the next one instead, so a runner gets a real build or nothing —
never an impossible one. Claim time rather than push time catches deleted
branches and pruned objects too, and yields cancelled rather than failure,
which is the actual complaint.
The check fails open on anything ambiguous. Only two outcomes cancel: cat-file -e exiting exactly 1, which is the documented "object absent" contract, and the
object existing while no ref of any kind contains it. A missing directory, a
directory that is not a repository, a missing git binary, or any unexpected exit
returns an error and the build runs. A build that fails at clone is a nuisance; a
CI system that quietly refuses to run builds is not.
Two review findings shaped that. Peeling to ^{commit} made a missing object and
a non-repository both exit 128, so a corrupted or mid-restore repository would
have had its whole queue cancelled. And restricting for-each-ref --contains to
refs/heads and refs/tags cancelled every fork merge request's build, since a
fork head lives at refs/merge-requests/<n>/head — caught by the full suite, not
by any selector either of us would have chosen.
runBuildCancel's status-resolution block is now shared rather than copied, so
the rule that a cancelled build points at an earlier passing build of the same
commit lives in one place.
Closes #152