Skip to content

feat(ci): make this repo's own suite a gating context, and pin what could un-require it [DO NOT MERGE: gate refused] - #45

Open
NSchatz wants to merge 4 commits into
mainfrom
feat/self-check-required
Open

feat(ci): make this repo's own suite a gating context, and pin what could un-require it [DO NOT MERGE: gate refused]#45
NSchatz wants to merge 4 commits into
mainfrom
feat/self-check-required

Conversation

@NSchatz

@NSchatz NSchatz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

CI-REQUIRED-CHECKS, the org / .github leg.

The item

#39 named a PRE-EXISTING and left it: ruleset ci-required-checks did not require
self-check.yml's scripts job, so this repository's own unit suite ran on every pull request and
could block none of them. A green check that cannot block a merge is documentation.

What this changes

scripts becomes a required context. The precondition was checked rather than assumed, in this
order:

  1. self-check.yml has no paths: filter and no job-level if:, so the job is unconditional. The
    standing record read it as path conditional; it is not, and never was. It simply postdated three
    branches that have since gone, which is the corrected cause and the one that makes this reversible
    without a workflow change.
  2. The job has completed on main at 41f883e. A context required before its workflow has completed
    on main leaves every pull request pending and unmergeable with nothing saying why.
  3. scripts was read off the real pull_request check runs of every open pull request, and off the
    last twelve consecutive pull_request runs of the workflow, all of which carry both jobs.
    Contexts are read off check runs here, never off a workflow's name:: this workflow is titled
    "Self-check" and its contexts are the bare job ids.

test/self-check.test.mjs pins what a ruleset cannot see. Nothing inside a repository can observe
its own ruleset, so two edits un-require a job with no ruleset change and no error anywhere:

  • Renaming the job id detaches the ruleset entry silently. The old context is then required and
    emitted by nothing, so every pull request sits pending, which is worse than red because nothing says
    why. The job ids are pinned, so a rename reds in the pull request that made it, beside a comment
    saying to move the ruleset entry too.
  • Narrowing what selects the work. A required job gates its steps, not the selection those steps
    operate on. node --test "test/*.test.mjs" exits 0 as happily over one file as over all of them.
    The glob is read out of the workflow and compared against the test files actually on disk, so this
    is a real comparison and not a spelling check.

A paths: filter and a job-level if: are refused too, on a reason that needs no theory about how a
skip resolves: a context that does not run on every pull request cannot gate every pull request.

What a skipped required context does to a merge is deliberately stated nowhere in this change.
This repository has not settled that against a primary source and a live run, deid's ci.yml
records the same question as open against run-actionlint and says not to change that input until it
is settled, and whatever the answer is it governs thirteen repositories at @main. It is raised as
its own item rather than answered in a comment here.

Every assertion was mutation checked. Narrowing the glob, adding paths:, adding a job if:,
dropping the push trigger, unquoting the glob, dropping the checkout, renaming the job, appending
--test-name-pattern, replacing the command with echo, dropping a test file into a subdirectory,
and a ** widening that does not actually cross a separator all red, each on the assertion that names
it. A correct test/**/*.test.mjs widening passes, which is deliberate: the file's own comments
tell a future author to widen the glob when a directory of tests appears, and a guard that reds on its
own documented remedy gets deleted rather than obeyed.

The glob translator follows node's rule rather than a guess, measured on a real run over the same
tree: test/**/*.test.mjs selects 347 tests and test/**.test.mjs selects 346, so ** crosses a
separator only as a whole segment. Reading a bare ** as crossing one fails in the dangerous
direction, reporting a selection wider than the one CI runs.

The case that asserts the invocation owns the shape (node --test, one quoted argument, no
selector smuggled in beside it). Whether the selection is complete is the next case's job. Pinning
the glob's literal value in the first would have conflated the two and reded on a widening as loudly
as on a narrowing.

The ci / prepublish census, recorded rather than fixed

Turning both pre-publish layers on by default made them run in all thirteen callers. It did not
make them block in any. The layers live in their own job, so ci / prepublish is a separate
check-run context, and a caller whose ruleset requires the verify matrix and ci / actionlint does
not thereby require it: a red pre-publish gate shows a red X and the merge button stays green.

Censused 2026-08-07 against real pull_request check runs rather than workflow names. All thirteen
callers emit ci / prepublish, as expected now that both layers default on and no caller passes
either input. cli is the only one whose ruleset requires it. That is one line per caller, in
that caller's own ruleset, and it is not reachable from this repository, so it is written down with
its derivation command instead of being fixed here. It belongs to each caller's own
CI-REQUIRED-CHECKS pass.

The backlog's reading that cli requires "only the two verify matrix contexts plus actionlint" is
stale: cli is in fact the one caller that does require the pre-publish context.

No context count is written anywhere, and nothing asserts a context IS required

Nothing in a repository can observe its own ruleset, so a list or a count goes stale the next time
this workflow grows a job, and prose asserting the current state is the same defect wearing different
clothes. Every addition here states the rule and gives the derivation command instead; the test file
says outright that whether a context is required is not knowable from inside a repository, so nothing
in it asserts that.

The long "how to require a context" procedure previously duplicated in no-emdash.yml is replaced by
a pointer to the one place it is now set out in full. The precondition itself is short and
load-bearing at each site, so it stays stated at each; only the procedure is stated once. That file
keeps the half that is genuinely local to it: which of its two jobs may ever be required.

No workflow logic moved

ci.yml and release.yml, the two called at @main by thirteen repositories, are untouched.
self-check.yml and no-emdash.yml were parsed with a real YAML parser at base and at head and
flattened to leaf paths. Both are identical leaf for leaf: comments only, no job id, step, condition,
run: body, uses: ref, input or output name moved.

Gate

scripts/verify.sh .github has no ladder here (no lockfile) and exits 66, as documented. The real
gate, run by hand:

  • node --test "test/*.test.mjs": 346 pass, 0 fail (339 before this change).
  • actionlint -shellcheck shellcheck: clean, exit 0.
  • node scripts/check-no-emdash.mjs: clean over every tracked file, none exempt.

Deliberately not done here

  • no-emdash-messages is not required and must not be, per the standing exemption. Dependabot
    pastes the dependency's upstream release notes into the pull request body, and dependabot.yml
    keeps a standing weekly github-actions schedule, so such a pull request arrives on someone else's
    clock indefinitely.
  • The twelve callers that emit ci / prepublish without requiring it. Reported above, not
    changed: each is that repository's own ruleset.
  • The resolvedSelection() divergence (vitest list --filesOnly in the gate against vitest run
    in a different CI job) belongs to whichever repository owns that policy, not here.
  • cli's release.yml comment, which carries a wrong context count and a claim about Version PRs
    arriving with zero check runs. That claim was true when written and was wrong for about six days,
    not months, which is exactly why it is its own slice with the dated instance kept.
  • The open Dependabot pull requests, including the codeql-action init / upload-sarif pair.
    A set that must move together has to land on one sha in one pull request, and that is not this one.

🛑 DO NOT MERGE WITHOUT A HUMAN DECISION

Three gate-refuter passes, the ADR 0016 maximum, and the last one refused. Reported as they
came, including the refusals.

  • Pass 1 on 7f14fe7: NOT REFUTED, four minor INTRODUCED findings, no blockers.
  • Pass 2 on 5b81279: REFUTED, one INTRODUCED major. The remedy for pass 1 had claimed a
    job-level if: strands a pull request while a step-level one goes green. GitHub documents that a
    skipped job reports success and does not prevent a merge even as a required check, so the contrast
    was false. This repo ships ci.yml with actionlint behind if: inputs.run-actionlint while
    ci / actionlint is a required context in caller rulesets, so a maintainer setting that input
    false was being told to expect a loud strand across thirteen repos.
  • Pass 3 on 3bc2dad: REFUTED, INTRODUCED major, called a blocker. The skip-semantics
    theory had been deleted, but a claim that the page and the file now state nothing about it was
    written in its place, and both files still did, further down. A rewrite wearing a deletion's
    clothes.

7a762d9 is the ADR 0016 cut-back and it is UNGRADED, because there is no fourth pass. It is
purely subtractive: every sentence characterising what a skipped required context does to a merge is
deleted, and no sentence replaces it, including the meta-sentence that claimed they were gone. It
also deletes the two pre-existing copies of the same false claim, here and in no-emdash.yml.

The guard, the ruleset act and the census are unaffected by any of that and were never the subject of
a finding. Every finding across all three passes was about prose.

The question that has to be settled by a human, in its own change, and that this pull request
deliberately no longer answers:
what does a skipped required context do to a merge? deid's
ci.yml records the same question against run-actionlint and says not to change that input until
it is settled. It is live across thirteen repos through ci.yml's if: ${{ inputs.run-actionlint }}.

NSchatz added 4 commits August 7, 2026 07:12
…ould un-require it

The 339-case suite in `scripts/` ran on every pull request and could block none of
them. A green check that cannot block a merge is documentation.

`scripts` is added to ruleset `ci-required-checks`, alongside the contexts already
there. The precondition was met and checked rather than assumed: `self-check.yml`
has no `paths:` filter and no job-level `if:`, the job has completed on `main`, and
`scripts` was read off the real `pull_request` check runs of every open pull request
before the ruleset moved. The standing record read the job as path conditional; it is
not, and never was. It simply postdated three branches that have since gone.

`test/self-check.test.mjs` closes the two ways a required job stops gating with no
ruleset edit and no error anywhere:

  - Renaming the job id detaches the ruleset entry silently, leaving a context that
    is required and emitted by nothing, so every pull request strands pending. The
    job ids are pinned, so the rename reds in the pull request that made it.
  - A required job gates its steps, not the selection those steps operate on.
    `node --test "test/*.test.mjs"` exits 0 as happily over one file as over all of
    them. The glob is read out of the workflow and compared against the test files
    on disk, and a plausible narrowing is exhibited as a failing case rather than
    asserted to be catchable.

A `paths:` filter and a job-level `if:` are refused for the same reason: a
conditional required context strands every pull request that does not match it.

Every assertion was mutation checked. Seven mutations, seven reds, each on the
assertion that names it: rename, narrow, add `paths:`, add a job `if:`, drop the
`push` trigger, unquote the glob, drop the checkout.

Also records the pre-publish census. Turning both layers on made them RUN in all
thirteen callers; it did not make them BLOCK in any. They live in their own job, so
`ci / prepublish` is a separate check-run context and a caller requiring the `verify`
matrix does not thereby require it. Censused 2026-08-07 off real check runs: all
thirteen emit it, one requires it. That is one line per caller in that caller's own
ruleset and is not reachable from here, so it is written down rather than fixed.

No workflow logic moved. `ci.yml` and `release.yml`, the two called at `@main` by
thirteen repositories, are untouched. `self-check.yml` and `no-emdash.yml` were
parsed with a real YAML parser at base and at head and flattened to leaf paths: both
are identical leaf for leaf, comments only.

Gate run by hand, since `verify.sh` has no ladder here and exits 66: `node --test`
346/346, `actionlint -shellcheck` clean, `no-emdash` clean over every tracked file.
…refusing its own remedy

Pass 1 of the `gate-refuter` returned NOT REFUTED with four minor `INTRODUCED`
findings, all sentences rather than machinery. Applied as prescribed. No guard was
grown to chase them.

1. The diff wrote the rule against in-repo ruleset state and then asserted in-repo
   ruleset state, which is the defect it was correcting. Nothing now claims a context
   IS required: the test header, the workflow comment and the derivation comment all
   state the intent and the derivation, and the test file says outright that whether a
   context is required is not knowable from inside a repository.

2. "THE TWO WAYS a required job stops gating" was an exhaustiveness claim and there is
   a third, one line of YAML away. A STEP-level `if:`, and `continue-on-error: true` on
   a step, do not strand anything: the job succeeds and the required context reports
   GREEN OVER NOTHING, which is the worse direction. The README said an actor `if:`
   "leaves the check permanently pending", true at job level and false at step level,
   where it is the milder characterisation of the worse case. Both are now named as
   open and unguarded rather than implied to be covered. Not closed, deliberately:
   closing them is one rule per step attribute, and this repo has already paid twice
   for a deny-list whose second round closed nothing new.

3. The "one owner" claim was not achieved. The precondition sits at three sites and
   the procedure at one; that is now what the pointer says, rather than claiming a
   single owner for both.

4. The guard forbade the growth its own comment prescribes. `globToRegExp` could not
   read `**`, so widening to `test/**/*.test.mjs`, the remedy the comments ask for when
   a directory of tests appears, reded. A guard that refuses its own documented remedy
   gets deleted rather than obeyed. `**` now translates, in ONE pass with no sentinel
   character round-tripped through the string, and both depths are asserted.

   The same finding surfaced a conflation: pinning the glob's literal value made the
   shape case red on a widening as loudly as on a narrowing. The shape case now owns
   the invocation (`node --test`, one quoted argument, no selector smuggled in beside
   it) and the load-bearing case owns whether the selection is complete.

Mutation checked again, wider than before. Narrowing the glob, adding `paths:`, adding
a job `if:`, dropping `push`, unquoting, dropping the checkout, renaming the job,
appending `--test-name-pattern`, replacing the command with `echo`, and dropping a test
file into a subdirectory all still red. WIDENING now passes, which is the correction.

Gate: `node --test` 346/346, `actionlint -shellcheck` clean, `no-emdash` clean over all
49 tracked files. `ci.yml` and `release.yml` remain untouched; `self-check.yml` and
`no-emdash.yml` re-parsed at base and head and still identical leaf for leaf.
…rrect the glob translator

`gate-refuter` pass 2 returned REFUTED on `5b81279` with one INTRODUCED major. It was
right, and the remedy is the one it prescribed: cut, do not rewrite.

THE MAJOR. `5b81279` claimed a job-level `if:` strands a pull request while a
step-level one goes green, and used that contrast to justify leaving the step-level
case open. GitHub's own documentation says a skipped job reports its status as
success and does not prevent a pull request from merging, even as a required check.
So the contrast does not exist and the ranking was meaningless. Worse, this repo
ships `ci.yml` with `actionlint` behind `if: inputs.run-actionlint`, and
`ci / actionlint` is a required context in caller rulesets, so a maintainer setting
`run-actionlint: false` was being told by this README to expect a loud strand across
13 repos.

Every sentence about which direction a skipped context fails is now DELETED, from the
README and from the test file. Nothing replaces it with a better answer, because this
repo has not settled that question against a primary source and a live run, and
`deid`'s `ci.yml` records the same question as open against the same input. It is
escalated as its own item. `cli#35` is the precedent: that note was rewritten twice,
produced a fresh false claim each time, and was correctly cut out rather than rewritten
a third time.

The `if:` and `paths:` guard STAYS, on a reason that does not need the answer: a
context that does not run on every pull request cannot gate every pull request,
whichever way a skip resolves.

Also from pass 2:

  - The precondition was said to be "load-bearing at three sites". It is stated at more
    than three. A count is exactly what this commit series forbids elsewhere, so it is
    removed rather than corrected to a new number.
  - `globToRegExp` mapped a bare `**` to `.*`, which crosses a separator. Node does not:
    measured on a real run, `test/**/*.test.mjs` selects 347 tests and `test/**.test.mjs`
    selects 346, over the same tree. The old mapping failed in the dangerous direction,
    reporting a selection wider than the one CI runs. `**` now crosses a separator only
    as a whole segment, and both readings are pinned.
  - "both belong in the set this repository requires" was the fourth site of pass 1's
    ruleset-state finding and had been missed.

Mutation checked again. Narrowing, `paths:`, a job `if:`, dropping `push`, unquoting,
dropping the checkout, renaming the job, `--test-name-pattern`, replacing the command
with `echo`, a test file dropped into a subdirectory, and now a `**` widening that does
NOT cross a separator all red. A correct `test/**/*.test.mjs` widening passes.

Gate: `node --test` 346/346, `actionlint -shellcheck` clean, `no-emdash` clean over all
49 tracked files, zero control bytes in any tracked file. `ci.yml` and `release.yml`
remain untouched.
⚠ THIS COMMIT IS UNGRADED. `gate-refuter` pass 3 of 3 returned REFUTED on `3bc2dad`
with an INTRODUCED blocker, and ADR 0016 allows no fourth pass. Per that ADR the
action is stop, escalate and cut back, so this is the cut-back and nothing else. THE
SLICE IS NOT TO BE MERGED ON THIS COMMIT'S AUTHORITY; it stops for a human.

WHAT PASS 3 CAUGHT, and it was right. `3bc2dad` deleted the false claim that a
job-level `if:` strands a pull request while a step-level one goes green, and then
wrote in its place that this page and this file state nothing about what a skipped
required context does. Both files still did, 56 lines later in one case. That is a
rewrite wearing a deletion's clothes, and it is the third round in a row in which this
slice shipped a checkable wrong self-description.

So this commit is PURELY SUBTRACTIVE. Every sentence characterising what a SKIPPED
required context does to a merge is gone, and no sentence replaces it, including the
meta-sentence claiming they are gone. Cut:

  - the two paragraphs at README "Two ways a required job stops gating", down to the
    one clause that survives all three passes and needs no theory: a context that does
    not run on every pull request cannot gate every pull request;
  - the same claim in `test/self-check.test.mjs`, in both the header and the
    conditional case;
  - the pre-existing false ones at README "Two jobs, and only one of them may ever be
    required" and in `no-emdash.yml`, which asserted an actor `if:` leaves a required
    context permanently pending. GitHub documents the opposite. They are pre-existing
    and out of this slice's obligation, but they are false, they govern thirteen repos
    at `@main`, and deleting a false sentence adds no claim;
  - the "load-bearing wherever a context is discussed" assertion, which was still
    over-general;
  - the "it is being escalated on its own" promise, which had no artifact. The
    escalation is raised to a human instead of asserted in a comment.

What survives about PENDING is a different mechanism and is correct: a context that is
required and emitted by NOTHING (never run, or detached by a rename) has no check run
at all. That is not a skipped job, and pass 3 confirmed the distinction.

`globToRegExp` keeps the measured fix and now discloses its own residual rather than
claiming to encode node's rule: a `**` that is neither a whole segment nor the whole
pattern is not translated faithfully and is not refused either.

Guard strength unchanged. Narrowing, `paths:`, a job `if:`, dropping `push`,
unquoting, dropping the checkout, renaming the job, `--test-name-pattern`, replacing
the command with `echo`, a subdirectory test file, and a non-spanning `**` widening
all still red. A correct `test/**/*.test.mjs` widening still passes.

Gate: `node --test` 346/346, `actionlint -shellcheck` clean, `no-emdash` clean over 49
tracked files. `ci.yml` 78 leaves, `release.yml` 104, `self-check.yml` 14,
`no-emdash.yml` 28: all identical to `origin/main`, comments only.
@NSchatz NSchatz changed the title feat(ci): make this repo's own suite a gating context, and pin what could un-require it feat(ci): make this repo's own suite a gating context, and pin what could un-require it [DO NOT MERGE: gate refused] Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant