|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | += Required status checks: the "Expected — Waiting" trap |
| 3 | +:toc: macro |
| 4 | +:icons: font |
| 5 | + |
| 6 | +[NOTE] |
| 7 | +==== |
| 8 | +*Sketch / operational note* (seeded 2026-06-21). Captures a CI failure class |
| 9 | +that blocked merges across the estate and the rule that prevents it. Pairs with |
| 10 | +the machine entry in `.machine_readable/6a2/PLAYBOOK.a2ml` (`[ci-required-checks]`). |
| 11 | +==== |
| 12 | + |
| 13 | +toc::[] |
| 14 | + |
| 15 | +== The symptom |
| 16 | + |
| 17 | +A PR's merge box lists a *required* check stuck at: |
| 18 | + |
| 19 | + Expected — Waiting for status to be reported |
| 20 | + |
| 21 | +This is **not** a failure and **not** "still running". It means a required |
| 22 | +status-check *context name* was *never reported* on the head commit, so the |
| 23 | +gate never resolves and the PR is blocked indefinitely — visually |
| 24 | +indistinguishable from a slow job. |
| 25 | + |
| 26 | +On affinescript the server states it plainly on push: |
| 27 | + |
| 28 | + - 4 of 4 required status checks are expected. |
| 29 | + |
| 30 | +== Root cause |
| 31 | + |
| 32 | +Branch protection pins required checks by *exact context string*, but the CI |
| 33 | +only emits some of those strings *conditionally*. Any required-but-unproduced |
| 34 | +context becomes a permanent "Expected". Three mechanisms produce it: |
| 35 | + |
| 36 | +[cols="1,2,3",options="header"] |
| 37 | +|=== |
| 38 | +| Mechanism | Example context | Why it isn't emitted |
| 39 | + |
| 40 | +| *Branch-filtered workflow* |
| 41 | +| `analyze (actions, none)`, `hypatia / Hypatia Neurosymbolic Analysis` |
| 42 | +| The workflow's `on.pull_request.branches` filter excludes the PR's base, so |
| 43 | + the workflow never runs and the check is *never created*. (A *skipped job* via |
| 44 | + `if:` reports neutral and does **not** block; a *filtered-out workflow* |
| 45 | + reports nothing and **does** block.) |
| 46 | + |
| 47 | +| *Renamed / migrated job* |
| 48 | +| `governance / Validate Hypatia baseline` |
| 49 | +| The pinned name was the `hyperpolymath/standards` governance *reusable* job |
| 50 | + (`<caller-job-id> / <reusable-job-name>`). affinescript migrated to a |
| 51 | + standalone `governance` job (#603/#604), which emits `governance` instead — |
| 52 | + the pinned name is orphaned and can never report. |
| 53 | + |
| 54 | +| *External app check* |
| 55 | +| `Hypatia` |
| 56 | +| Posted by a GitHub App, not a repo workflow. The repo cannot force it to |
| 57 | + appear; it is absent on forks / when the app doesn't post for that SHA. |
| 58 | +|=== |
| 59 | + |
| 60 | +== The rule (guardrail) |
| 61 | + |
| 62 | +[IMPORTANT] |
| 63 | +==== |
| 64 | +A context may be marked *Required* only if it is emitted **unconditionally on |
| 65 | +every PR that can target the protected branch**. Pin to job names your own |
| 66 | +workflows emit unconditionally — never to reusable-workflow job names that can |
| 67 | +change out from under you on a SHA bump, nor to app checks. |
| 68 | +==== |
| 69 | + |
| 70 | +Corollaries: |
| 71 | + |
| 72 | +* Required check-producing workflows must **not** carry an |
| 73 | + `on.pull_request.branches` filter (the `push:` filter is fine). |
| 74 | +* If a required check is produced by a reusable whose job name you do not |
| 75 | + control, either vendor a *local* reusable that emits the pinned name, or |
| 76 | + repoint the pin to a name you do emit. |
| 77 | +* App checks should be advisory, or backed by an always-running workflow job, |
| 78 | + unless the app guarantees a check on every PR. |
| 79 | + |
| 80 | +== Diagnosing a stuck PR |
| 81 | + |
| 82 | +. `pull_request_read` → `get_check_runs`: list the *names actually produced* on |
| 83 | + the head commit. |
| 84 | +. Compare against the *required* names in branch protection. |
| 85 | +. Any required name **not** in the produced list is the culprit. Classify it by |
| 86 | + the table above (filtered workflow / renamed job / app check). |
| 87 | + |
| 88 | +== What was done (2026-06-21) |
| 89 | + |
| 90 | +* *#645 (merged)* — affinescript: de-gated `codeql.yml` + `hypatia-scan.yml` |
| 91 | + `pull_request` triggers; added a local reusable |
| 92 | + (`governance-baseline.yml` + `governance-baseline-impl.yml`) that re-emits |
| 93 | + `governance / Validate Hypatia baseline` without the cross-repo coupling that |
| 94 | + #603/#604 removed. All four required checks now report green. |
| 95 | +* Sibling de-gates: `hyperpolymath/hypatia#518`, `hyperpolymath/gitbot-fleet#308`, |
| 96 | + `hyperpolymath/.git-private-farm#83`. |
| 97 | +* *Follow-ups* — pin reconciliation (admin) `hyperpolymath/affinescript#650`; |
| 98 | + `Hypatia` app-check reliability `hyperpolymath/hypatia#519`. |
| 99 | + |
| 100 | +== Required-vs-emitted, across the estate |
| 101 | + |
| 102 | +`⚠` marks the same latent trap (a required producer gated to `main`/`master`). |
| 103 | + |
| 104 | +[cols="1,1,1,1,1",options="header"] |
| 105 | +|=== |
| 106 | +| Repo | `analyze (actions, none)` | `hypatia / …Neurosymbolic` | `Hypatia` app | `governance / Validate Hypatia baseline` |
| 107 | + |
| 108 | +| affinescript | emitted (de-gated #645) | emitted (de-gated #645) | posts | bridged #645 (standalone emits `governance`) |
| 109 | +| hypatia | emitted, was gated ⚠ | emitted, was gated ⚠ | posts | native (reusable) + 7 siblings |
| 110 | +| gitbot-fleet | emitted, was gated ⚠ | absent on #307 ⚠ | absent on #307 ⚠ | native (reusable) |
| 111 | +| .git-private-farm | was gated ⚠ | was gated ⚠ | rides scan | reusable, governance itself was gated ⚠ |
| 112 | +|=== |
| 113 | + |
| 114 | +Only affinescript's *required pins* are API-confirmed; the others' emitted |
| 115 | +truth is from live runs, but their pin lists need branch-protection (admin) |
| 116 | +access to diff exactly — tracked in `#650`. |
0 commit comments