Skip to content

Commit 5ccddff

Browse files
docs(ci): sketch required-check hygiene (human + machine) (#651)
## What Documents the CI failure class diagnosed and fixed in #645 (merged), so it doesn't have to be re-derived next time. - **`docs/ci/required-checks.adoc`** (new) — the *"Expected — Waiting for status to be reported"* trap: what it means (a required context never reported ≠ a failure), the three mechanisms that cause it (branch-filtered workflow / renamed-migrated job / external app check), a diagnosis recipe (`get_check_runs` vs the required pins), the guardrail rule, and the estate required-vs-emitted matrix. - **`.machine_readable/6a2/PLAYBOOK.a2ml`** — new `[ci-required-checks]` operational entry pointing at the doc + tracking issue. ## Why This is the "sketch area + human/machine documentation" half of the close-out for the required-checks work. The fix itself shipped in #645; this preserves the *reasoning* and the *rule* (a context may be marked Required only if it is emitted unconditionally on every PR that can target the protected branch). Refs #645 · #650 (pin reconciliation) · hyperpolymath/hypatia#519 (`Hypatia` app check). DOC-FORMAT-compliant (`.adoc`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UXXpaoiATzxcn3kW3eTM26 --- _Generated by [Claude Code](https://claude.ai/code/session_01UXXpaoiATzxcn3kW3eTM26)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 81caf8e commit 5ccddff

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

.machine_readable/6a2/PLAYBOOK.a2ml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,16 @@ last-updated = "2026-05-26"
5959
#
6060
# Token rotation: rotate NPM_TOKEN after any wire-up where the token
6161
# value transited a session transcript.
62+
63+
[ci-required-checks]
64+
# A required status check must be emitted UNCONDITIONALLY on every PR that can
65+
# target the protected branch. Otherwise the unproduced context sits forever at
66+
# "Expected — Waiting for status to be reported" — a silent merge block.
67+
# Causes: (1) a producing workflow with an on.pull_request.branches filter;
68+
# (2) a pin naming a renamed/migrated job (e.g. a standards reusable job name);
69+
# (3) an external GitHub App check that doesn't post on every PR.
70+
# Diagnosed + fixed 2026-06-21 (#645). Human doc: docs/ci/required-checks.adoc.
71+
rule = "required-check implies emitted on every PR base (no pull_request.branches filter)"
72+
diagnosed = "2026-06-21"
73+
reference = "docs/ci/required-checks.adoc"
74+
tracking-issue = "650"

docs/ci/required-checks.adoc

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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

Comments
 (0)