fix(labels): state:needs-human means a human could merge it right now
#128
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
|
# pick the very run it superseded — reporting the old SUCCESS while a
|
||||||
# replacement was still running, which is #136 again.
|
# replacement was still running, which is #136 again.
|
||||||
#
|
#
|
||||||
# So: take the newest timestamp a run actually carries, discarding both
|
# So: date a run by when it BEGAN, discarding both spellings of absent
|
||||||
# spellings of absent (null, and the zero sentinel). An entry that carries
|
# (null, and the zero sentinel) and falling back only if it never recorded
|
||||||
# no usable timestamp at all sorts LAST rather than first — something we
|
# a beginning. NOT by the newest stamp of any kind: `max` compares the
|
||||||
# cannot date is most likely the thing just created, and treating it as
|
# completion of a finished run against the start of a live one, which are
|
||||||
# newest keeps an undateable in-flight run from being discarded in favour
|
# different quantities and not an ordering on runs. A run cancelled by the
|
||||||
# of a stale success. Every ambiguity here resolves toward "not settled".
|
# 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 // [])[]
|
| [ (.statusCheckRollup // [])[]
|
||||||
| { ctx: [.workflowName // "", .name // .context // ""],
|
| { ctx: [.workflowName // "", .name // .context // ""],
|
||||||
at: ([.startedAt, .createdAt, .completedAt]
|
at: ([.startedAt, .createdAt, .completedAt]
|
||||||
| map(select(type == "string" and . != ""
|
| map(select(type == "string" and . != ""
|
||||||
and (startswith("0001-01-01") | not)))
|
and (startswith("0001-01-01") | not)))
|
||||||
| max // ""),
|
| first // ""),
|
||||||
outcome: ((.conclusion // .state // "") | ascii_upcase) } ]
|
outcome: ((.conclusion // .state // "") | ascii_upcase) } ]
|
||||||
| group_by(.ctx)
|
| group_by(.ctx)
|
||||||
| map(sort_by([(.at == ""), .at]) | last | .outcome) as $latest
|
| 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 replacement mid-flight read `SUCCESS` — the original bug restored, pointing
|
||||||
a human at a disabled merge button — and a `CANCELLED` original whose
|
a human at a disabled merge button — and a `CANCELLED` original whose
|
||||||
replacement was still running read `FAILURE`, the flap the collapse exists to
|
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
|
prevent. So a run is dated by when it **began**, with both spellings of
|
||||||
both spellings of absent discarded (`null`, and the zero sentinel), and an
|
absent discarded (`null`, and the zero sentinel) and a fallback only for a
|
||||||
entry that carries no usable timestamp at all sorts **last** rather than
|
run that never recorded a beginning — not by the newest stamp of any kind,
|
||||||
first: something undateable is most likely the thing just created, and every
|
which compares the completion of a finished run against the start of a live
|
||||||
ambiguity here resolves toward "not settled" rather than toward a stale
|
one. Those are different quantities, and a run cancelled by the concurrency
|
||||||
success.
|
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
|
`UNKNOWN` mergeability is deliberately not treated as unmergeable: GitHub
|
||||||
reports it for about a minute after every merge while it recomputes, and
|
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
|
**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
|
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 —
|
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
|
in both spellings of an absent completion, in both directions, and across the
|
||||||
whole check-outcome enum are pinned in `test/labels-reconcile.sh`
|
wind-down window where the two overlap — and the whole check-outcome enum are
|
||||||
(fixtures 19 → 49).
|
pinned in `test/labels-reconcile.sh` (fixtures 19 → 51).
|
||||||
|
|
||||||
## 0.1.1 — 2026-07-19
|
## 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),\
|
"$(rollup "[$(inflight_ build 2026-07-20T15:19:00Z),\
|
||||||
$(run_ build SUCCESS 2026-07-20T15:19:45Z)]" | checks_state)"
|
$(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
|
# -- 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.
|
# 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
|
DRAFT=false HEAD_SHA=head1 REQUESTED="$HUMAN" REVIEWS_JSON="$ALL_APPROVE" MERGEABLE=MERGEABLE
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue