The green ledger is a complement, not a list
A red main here has an audience: one ci-main-red issue and one signed event to the control
portal. ci-failure opens or appends to it; ci-green closes it. The pair is only correct if a
green run closes exactly what a red one opened β and that property has now failed in both
directions inside twenty-four hours, each time because someone reasoned about the needs: list
instead of about the condition.
Direction 1 β nothing ever closed (#1807 as filed)
ci-green was guarded by the implicit success().
π¨ GitHub's success() is TRANSITIVE. It is false when any job anywhere in the dependency graph
was skipped, not only a direct need. publish-bake is one of ci-green's needs and legitimately
skips β the build queue did not admit the commit, or a direct gate of its own was skipped β so a
fully green main could not close the ledger.
ci-failure uses failure(), which a skip does not block. So reds filed and greens never
closed. The ledger could only ever grow: #1785's timeline carried exactly one event, a labeled
at 2026-09-13T08:37:37Z, while main had a fully green run at 16:16Z in between.
Fixed by replacing the implicit guard with !cancelled() plus an explicit per-need list.
Direction 2 β a RED run closed it (measured the next morning)
Of the six runs that posted "green again" and closed #1785 after that fix,
two were conclusion: failure:
| run | sha | failed jobs | ci-green |
ci-failure |
|---|---|---|---|---|
| 34784805295 | c249bdc5 |
Module bundle (batch 8/8), All selected bundles built |
success | success |
| 34787401082 | cb6d0e86 |
Module bundle (batch 4/8), All selected bundles built |
success | success |
| 34792068713 | c3b0224c |
publish-bake itself |
skipped β | success |
| 34798196752 / 34804443987 / 34807156235 | β | none | success β | skipped β |
On the first two, the ledger was commented "green again" and closed, then reopened by
ci-failure seconds later β 22:48:23Z close, 22:48:26Z reopen.
Why the explicit list could not see it. modules is not one of ci-green's seven direct needs.
Its failure reaches this job only as a skipped publish-bake β publish-bake's own explicit guard
rejects a failed direct need β and a skip is precisely what the list forgives, deliberately, because
a held publication is not a red main.
So the second defect is the exact mirror of the first:
| conflates | consequence | |
|---|---|---|
implicit success() |
"skipped" with "failed" | nothing ever closes |
| explicit direct-need list alone | "skipped because held" with "skipped because upstream failed" | a red run closes |
Note that row three of the table above is the half that already worked: when publish-bake itself
failed, the explicit clause caught it. The list is necessary. It was never sufficient.
The rule
ci-failurefires onfailure().ci-greenfires on!failure(). Both are evaluated over the same dependency graph, so between them they partition every run. The direct-need clauses onci-greenNARROW it further β a cancelled need, agates-executedthat did not prove the run ran what it owed β but they cannot express the complement, because a job that is in the GRAPH but not in the LIST reachesci-greenonly as a downstream SKIP.
Change one side's condition and the other must change with it. Overlap and both fire β a "green
again" comment on a red main. Leave a gap and neither fires β a run nobody is told about.
The same needs: list on both jobs remains necessary: it is what makes the two conditions range over
the same graph in the first place. It is simply not the property that makes the pair correct.
Why this cannot be caught on a pull request
ci-green and ci-failure are main-only (the event test comes first in both guards), and they
carry the org webhook pair. Nothing about this pair can be exercised on a PR by construction, so
the evidence for a change to it is the measurement of past runs, named with their ids β not a green
check. That is also why the condition, not the list, is written down here: the next edit will have no
test to learn it from.
Related
- How a red main reaches triage β the events, the inbox, the ledger they feed.
- Systemorph/MeshWeaver.Plugins#1807 (both directions) Β· #1779, #1782 (the same transitive-skip trap
on
tag-modulesandpublish-bake) Β· #1785 (the ledger issue).