-
Notifications
You must be signed in to change notification settings - Fork 10
191 lines (179 loc) · 10.5 KB
/
Copy pathgoverned-surface-guard.yml
File metadata and controls
191 lines (179 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# The PREVENTION half of the governed-surface regime (#11704). The rule, the
# event split, the ruled approval predicate and the exit contract all live in
# `scripts/pm/check-governed-queue-guard.mjs`; that header is authoritative and
# this file is the invocation. Only the wiring decisions are argued here.
#
# Its sibling `check:pm-governed-merges` is the DETECTION half — a report-only
# post-merge audit. Three incidents (#9550, #10580, the #9319 landing) each
# ended with a governed diff enqueued or merged with zero reviews and each was
# caught by ACCIDENT, which is the expected behaviour of a regime whose only
# pre-merge line of defence is seat discipline. This is the missing refusal.
name: Governed Surface Guard
on:
# BOTH legs are load-bearing, and they mean DIFFERENT things — see the script
# header. `merge_group` is the leg that REFUSES: the queue build is the last
# thing between a speculative merge and `main`, and it is the path a seat
# uses. `pull_request` is an EARLY WARNING that deliberately exits 0, because
# a governed PR held as a draft for the maintainer to merge by hand is the
# regime's healthy end state, and a check that reddens on the healthy case is
# the permanently-red gate the 2026-08-18 ruling retired.
pull_request:
branches:
- main
# Naming `types:` REPLACES GitHub's default set rather than extending it,
# so all three defaults are restated here (#8304). `ready_for_review` is
# the addition and it is the point: flipping a governed draft to ready is
# the first move of the exact sequence this guard exists to interrupt, and
# it is not in the default set — without it the warning would not re-fire
# at the one moment a seat most needs to read it.
types: [opened, synchronize, reopened, ready_for_review]
merge_group:
# ⛔ NO `paths:` filter, on either leg, and this is not an oversight. A skipped
# job counts as SUCCESS in branch protection, so a path filter would hand the
# queue a green "Governed Surface Queue Guard" for a PR the filter mis-scoped —
# the #4928 shape, on the one check whose entire job is to refuse. The path
# test belongs INSIDE the script, where "nothing governed" is a verdict that
# says so and costs zero API calls. (`merge_group` has no paths support at all,
# so a filter would also make the two legs disagree about what they cover.)
concurrency:
group: governed-surface-guard-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# The default read scopes and nothing beyond them. `pull-requests: read` is
# what the review list needs; there is no write scope here, and adding one to
# make this check "fix" anything would be a widening no ruling covers.
permissions:
contents: read
pull-requests: read
jobs:
governed-surface-guard:
# ⚠️ THIS LITERAL IS THE CHECK-RUN NAME branch protection would pin, and it
# is duplicated in `CHECK_CONTEXT_NAME` in the script — deliberately, and
# pinned in both directions: the script's `--self-test` reads THIS FILE and
# fails if the two ever disagree. Renaming a job silently detaches a
# required context (#6865); this is the cheap half of the two-step that
# makes that impossible to do by accident.
#
# ⛔ Making it a required context is NOT this PR's step. Branch protection
# is the maintainer's: it takes a `REQUIRED_CONTEXTS` row in
# `scripts/check-required-contexts.mjs` PLUS the entry in Settings →
# Rulesets, in one sitting (#6865). Either half alone is an outage — a row
# without the setting reads as `direction A — registered here, NOT in the
# live required set`, which is #5617's own defect.
name: Governed Surface Queue Guard
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# A governed-surface question answered over a truncated history
# answers with SILENCE, and silence reads as compliance (#9902). The
# merge group's base sha can predate a shallow fetch's floor, so the
# whole history is the only depth that cannot under-report. The
# script refuses outright (exit 1) if either sha is missing, rather
# than diffing what it happens to have.
fetch-depth: 0
# Pinned to the same major and spelling as every other setup-node in this
# repo, and it keeps its literal `node-version` (rather than
# `node-version-file`) for the measured reason the Part-of Closing-Keyword
# Guard records: `actions/setup-node@v5`+ defaults
# `package-manager-cache: true`, which shells out to pnpm and hard-errors
# before any `run:` line when pnpm is not yet on PATH. This job now DOES
# acquire pnpm, but strictly after this step — the same order lint.yml has
# been green on for months — so nothing here may start depending on pnpm
# existing yet.
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
# The self-test runs FIRST and in its own step, because it is the
# precondition for trusting the run after it: this guard's predicates
# decide whether a merge lands, so a rotted predicate must redden here
# rather than quietly wave a governed diff through. `bash -e` aborting the
# step on the first failure is the correct semantics for a
# precondition — the collector idiom next door in lint.yml is for
# INDEPENDENT self-tests, which these are not.
- name: Guard predicate self-test
run: node scripts/pm/check-governed-queue-guard.mjs --self-test
# ── the generator toolchain — the #11705 rows can now actually recompute ─
#
# The register's `verify` rows (generator-owned files inside `skills/**`)
# recompute by running the tree's OWN generator through `pnpm … exec tsx`.
# Until now this job installed nothing, so that recompute answered "the
# generator toolchain is not available in this environment" on every run
# and the path stayed governed — a spec PR carrying its regenerated
# `references/_index.md` needed a pinned maintainer approval at merge-group
# time even though the seat-side `--test` lifted the same diff in a dev
# container. The script header filed that trade as not taken; the
# maintainer took it (2026-09-01, verbatim):
#
# > 纯生成的指针行(spec 源变更后再生成的 references/_index.md) 不需要我审核吧
#
# ⭐ NO `--filter` here, deliberately. The set of packages the register's
# recompute needs is `GENERATED_SURFACE_EXCEPTIONS[].verify.pkg`, and a
# filter list in this file would be a SECOND copy of it — one that goes
# stale silently the day a row is added, in the direction that reads as
# compliance (the new row's generator fails to spawn, the path stays
# governed, and nothing says the install was the reason). A full install
# restates nothing, so a register row reaches this job for free — the same
# reason the script imports the predicate instead of reimplementing it.
# The self-test pins that: every `verify.pkg` in the register must be
# covered by the install line below.
#
# ⛔ NO build step, and that is measured rather than assumed: both `verify`
# rows run through `tsx`, which reads the generator's TypeScript sources
# and their relative `./lib/*` imports directly — nothing consults any
# package's `dist/`. Measured on a warm store: install 7s, the skill-refs
# `--check` 1s. A build would be pure cost with nothing depending on it.
#
# ⭐ EVERY step in this block is `continue-on-error` — this is the load-
# bearing half, not defensive decoration. The guard's own promise is that
# "a diff that touches nothing governed is never blocked by an outage", and
# a hard-failing install would newly make EVERY ordinary queue build depend
# on the npm registry and the cache service. With `continue-on-error`, a
# broken install degrades to exactly the behaviour this job had before this
# block existed: the generator cannot spawn, `runSinkGenerator` returns its
# stated "toolchain is not available" reason, and the path stays GOVERNED.
# Fail-closed on the governed path, unaffected on the clear one, and the
# verdict stays inside the script — where the exit contract lives — instead
# of becoming an opaque red step nobody can read a governance answer out of.
#
# Both legs install. The `pull_request` leg is a FORECAST of the queue
# verdict ("the merge-queue run of this same check will REFUSE it"), so a
# leg that cannot recompute forecasts a refusal that will not happen — the
# false-alarm direction, which trains seats to hold PRs that need no
# holding. Same diff, same answer, on both legs and at the seat.
- name: Setup pnpm
uses: ./.github/actions/setup-pnpm
continue-on-error: true
- name: Get pnpm store directory
shell: bash
continue-on-error: true
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
# Deliberately the same key as lint.yml's: one store, one entry, and this
# job is a cache READER in the steady state rather than a second writer.
- name: Setup pnpm cache
uses: actions/cache@v6
continue-on-error: true
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-v3-
- name: Install dependencies
continue-on-error: true
run: pnpm install --frozen-lockfile
# The live judgment. Everything it reads arrives through `env:` or the
# event payload on disk — no `${{ }}` interpolation into the shell line,
# so no PR-controlled text ever becomes part of a command.
#
# GITHUB_TOKEN is the workflow's own default token at the read scopes
# declared above; it is what makes the review read possible at all, and
# an unreadable review list is a REFUSAL with its own exit code, never a
# pass. ⛔ Fail-open is wrong in this file specifically — it exists
# because every other layer in this chain failed open.
- name: Governed surfaces may not enter the merge queue unreviewed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/pm/check-governed-queue-guard.mjs