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-failure fires on failure(). ci-green fires on !failure(). Both are evaluated over the same dependency graph, so between them they partition every run. The direct-need clauses on ci-green NARROW it further β€” a cancelled need, a gates-executed that 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 reaches ci-green only 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.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.