fix(labels): date a run by when it began, not by its newest stamp
Round 4 of #128. @claude-bot-andresmgsl and @codex-bot-andresmgsl again converged on the same defect, in the round-3 dating expression itself. `max` over [startedAt, createdAt, completedAt] resolves to completedAt for a finished run and startedAt for a live one. Those are different quantities, so the comparison was never an ordering on runs — it was "newest stamp of any kind". A run cancelled by the concurrency group does not stop the instant its replacement starts; the runner has to wind down, so predecessor.completedAt > successor.startedAt is the ordinary case rather than a corner. On box's aa5a6ba the superseding run started 15:19:38 and the run it cancelled did not finish until 15:19:51 — thirteen seconds in which the dead predecessor out-dated the live run that replaced it, and the collapse discarded the wrong one. That narrowed round 3's two failures without closing them: a CANCELLED predecessor read FAILURE and a SUCCESS predecessor read SUCCESS, where both should be PENDING. The second is #136 restored — needs-human over a tree whose merge button branch protection has disabled. The list is already in preference order and the select leaves only stamps the run actually carries, so `first` is exactly "date it by when it began, falling back only if it never recorded a beginning". Fixtures 49 -> 51. None of the existing 49 could see this: every one spaces the predecessor's completion before the successor's start, and run_() carries no startedAt at all, so the overlap needed explicit payloads. Both new fixtures fail under `max` and the other 49 do not. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5d39783b77
commit
3766a15022
3 changed files with 51 additions and 16 deletions
24
.github/scripts/labels-reconcile.sh
vendored
24
.github/scripts/labels-reconcile.sh
vendored
|
|
@ -88,18 +88,28 @@ checks_state() { # rollup JSON on stdin → SUCCESS | FAILURE | PENDING | NONE
|
|||
# pick the very run it superseded — reporting the old SUCCESS while a
|
||||
# replacement was still running, which is #136 again.
|
||||
#
|
||||
# So: take the newest timestamp a run actually carries, discarding both
|
||||
# spellings of absent (null, and the zero sentinel). An entry that carries
|
||||
# no usable timestamp at all sorts LAST rather than first — something we
|
||||
# cannot date is most likely the thing just created, and treating it as
|
||||
# newest keeps an undateable in-flight run from being discarded in favour
|
||||
# of a stale success. Every ambiguity here resolves toward "not settled".
|
||||
# So: date a run by when it BEGAN, discarding both spellings of absent
|
||||
# (null, and the zero sentinel) and falling back only if it never recorded
|
||||
# a beginning. NOT by the newest stamp of any kind: `max` compares the
|
||||
# completion of a finished run against the start of a live one, which are
|
||||
# different quantities and not an ordering on runs. A run cancelled by the
|
||||
# concurrency group does not stop the instant its replacement starts — the
|
||||
# runner has to wind down — so predecessor.completedAt > successor.startedAt
|
||||
# is the ordinary case, and `max` dated the dead predecessor newer than the
|
||||
# live run that replaced it, narrowing both failures above without closing
|
||||
# them. The list is already in preference order, so `first` IS that rule.
|
||||
#
|
||||
# An entry that carries no usable timestamp at all sorts LAST rather than
|
||||
# first — something we cannot date is most likely the thing just created,
|
||||
# and treating it as newest keeps an undateable in-flight run from being
|
||||
# discarded in favour of a stale success. Every ambiguity resolves toward
|
||||
# "not settled".
|
||||
| [ (.statusCheckRollup // [])[]
|
||||
| { ctx: [.workflowName // "", .name // .context // ""],
|
||||
at: ([.startedAt, .createdAt, .completedAt]
|
||||
| map(select(type == "string" and . != ""
|
||||
and (startswith("0001-01-01") | not)))
|
||||
| max // ""),
|
||||
| first // ""),
|
||||
outcome: ((.conclusion // .state // "") | ascii_upcase) } ]
|
||||
| group_by(.ctx)
|
||||
| map(sort_by([(.at == ""), .at]) | last | .outcome) as $latest
|
||||
|
|
|
|||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -64,12 +64,18 @@ actually cutting it, and this file starts there.
|
|||
a replacement mid-flight read `SUCCESS` — the original bug restored, pointing
|
||||
a human at a disabled merge button — and a `CANCELLED` original whose
|
||||
replacement was still running read `FAILURE`, the flap the collapse exists to
|
||||
prevent. So a run is dated by the newest timestamp it actually carries, with
|
||||
both spellings of absent discarded (`null`, and the zero sentinel), and an
|
||||
entry that carries no usable timestamp at all sorts **last** rather than
|
||||
first: something undateable is most likely the thing just created, and every
|
||||
ambiguity here resolves toward "not settled" rather than toward a stale
|
||||
success.
|
||||
prevent. So a run is dated by when it **began**, with both spellings of
|
||||
absent discarded (`null`, and the zero sentinel) and a fallback only for a
|
||||
run that never recorded a beginning — not by the newest stamp of any kind,
|
||||
which compares the completion of a finished run against the start of a live
|
||||
one. Those are different quantities, and a run cancelled by the concurrency
|
||||
group does not stop the instant its replacement starts: the runner winds
|
||||
down, so a predecessor routinely finishes *after* its successor began, and
|
||||
dating by "newest stamp" let the dead run out-rank the live one that
|
||||
replaced it. An entry carrying no usable timestamp at all sorts **last**
|
||||
rather than first: something undateable is most likely the thing just
|
||||
created, and every ambiguity here resolves toward "not settled" rather than
|
||||
toward a stale success.
|
||||
|
||||
`UNKNOWN` mergeability is deliberately not treated as unmergeable: GitHub
|
||||
reports it for about a minute after every merge while it recomputes, and
|
||||
|
|
@ -83,9 +89,9 @@ actually cutting it, and this file starts there.
|
|||
**clears** it once the PR stops being mergeable-by-a-human. Ported from
|
||||
heavy-duty/box#137 so the three repos' reconcilers stay byte-identical; both
|
||||
live shapes, the mixed round, the in-flight run superseding a finished one —
|
||||
in both spellings of an absent completion, and in both directions — and the
|
||||
whole check-outcome enum are pinned in `test/labels-reconcile.sh`
|
||||
(fixtures 19 → 49).
|
||||
in both spellings of an absent completion, in both directions, and across the
|
||||
wind-down window where the two overlap — and the whole check-outcome enum are
|
||||
pinned in `test/labels-reconcile.sh` (fixtures 19 → 51).
|
||||
|
||||
## 0.1.1 — 2026-07-19
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,25 @@ expect "a finished re-run supersedes an earlier in-flight run" SUCCESS \
|
|||
"$(rollup "[$(inflight_ build 2026-07-20T15:19:00Z),\
|
||||
$(run_ build SUCCESS 2026-07-20T15:19:45Z)]" | checks_state)"
|
||||
|
||||
# -- the wind-down window. A predecessor cancelled by the concurrency group
|
||||
# does not stop the instant its replacement starts, so its completion
|
||||
# routinely lands AFTER the successor's start — on box's aa5a6ba the
|
||||
# replacement started 15:19:38 and the run it cancelled finished 15:19:51.
|
||||
# Dating by "newest stamp of any kind" compares the dead run's completion
|
||||
# against the live run's start, which is not an ordering on runs, and the
|
||||
# predecessor wins. Every fixture above spaces completion before start, so
|
||||
# none of them can see it. run_() cannot express the overlap either — it
|
||||
# carries no startedAt — hence the explicit payloads.
|
||||
overlap_() { jq -n --arg n "$1" --arg o "$2" --arg s "$3" --arg c "$4" \
|
||||
'{__typename:"CheckRun", workflowName:"ci", name:$n, conclusion:$o,
|
||||
startedAt:$s, completedAt:$c}'; }
|
||||
expect "a predecessor finishing after its replacement started is still older (CANCELLED)" PENDING \
|
||||
"$(rollup "[$(overlap_ scope CANCELLED 2026-07-20T15:19:00Z 2026-07-20T15:19:51Z),\
|
||||
$(inflight_ scope 2026-07-20T15:19:38Z)]" | checks_state)"
|
||||
expect "...and the same when it finished green — mid-flight is not mergeable" PENDING \
|
||||
"$(rollup "[$(overlap_ build SUCCESS 2026-07-20T15:19:00Z 2026-07-20T15:19:51Z),\
|
||||
$(inflight_ build 2026-07-20T15:19:38Z)]" | checks_state)"
|
||||
|
||||
# -- the classifier feeds the state machine: a cancelled required check must
|
||||
# take the PR off the human's plate, which is the whole point of #136.
|
||||
DRAFT=false HEAD_SHA=head1 REQUESTED="$HUMAN" REVIEWS_JSON="$ALL_APPROVE" MERGEABLE=MERGEABLE
|
||||
|
|
|
|||
Loading…
Reference in a new issue