From 5f3bc0436a35660377ebf1722f4a424d318988fd Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Mon, 21 Sep 2026 19:26:18 +0000 Subject: [PATCH] S0336-shared-scripts-sweep: run both gates as thin callers of the shared reusable workflows The em-dash and public-surface gates now delegate their plumbing (checkout, pnpm, Node) to cosyte/.github, pinned to the 40 character commit the reusable workflows were published at. The scanners stay here: this repo's own scripts/check-no-emdash.sh and scripts/check-no-internal-refs.sh are still what decides each job, and neither script changed. The shared workflow runs the em-dash halves as two jobs, which is the split this file recorded as its deferred fix: the tracked-file context is requirable on its own terms and the messages context stays exempt. The check-run contexts move with the shape, from no-emdash to no-emdash / tracked-files and from no-internal-refs to no-internal-refs / public-surface, so the ruleset that requires the public-surface context moves with it. --- .github/workflows/no-emdash.yml | 139 ++++++++++--------------- .github/workflows/no-internal-refs.yml | 46 ++++---- 2 files changed, 81 insertions(+), 104 deletions(-) diff --git a/.github/workflows/no-emdash.yml b/.github/workflows/no-emdash.yml index edeb29b..d82717c 100644 --- a/.github/workflows/no-emdash.yml +++ b/.github/workflows/no-emdash.yml @@ -10,10 +10,29 @@ name: Em-dash gate # without the gate grows back; a gate without the sweep would have red-flagged CI on # arrival. Both are one change. # +# WHAT ACTUALLY SCANS, AND WHERE THE RULE LIVES. `pnpm check:no-emdash`, which is this +# repository's own gate script. The jobs below delegate the plumbing (checkout, pnpm, +# Node) to the shared gate workflow, and that workflow carries no pattern, no allow-list +# and no file selection of its own: it prepares the tree, runs the command, and lets the +# command's exit status decide the job. So the rule and every measurement above stay in +# this repository, and only the harness is shared. +# +# TWO CONTEXTS, NOT ONE, AND THE JOB ID IS LOAD-BEARING. A called job's check run is named +# ` / `, so the job id `no-emdash` below publishes +# `no-emdash / tracked-files` and `no-emdash / messages`. A branch ruleset binds to those +# exact strings: renaming the job detaches the requirement with no error anywhere, and a +# required context nothing emits blocks every pull request. Do not rename it. +# +# `no-emdash / tracked-files` is the half that is safe to require and the one that blocks +# a merge. `no-emdash / messages` runs, reds its own context, and is deliberately NOT +# required: Dependabot composes a pull request body out of upstream release notes, em +# dashes included, and a gate that fails on someone else's clock becomes a tax. The +# detection is unchanged either way; only what blocks is narrowed. +# # WHY A SEPARATE WORKFLOW, and not a job in ci.yml. Same two reasons this repo's # `no-internal-refs.yml` gives, and the second is the deciding one here: # -# The reusable pipeline is not an option. synth's `ci` job calls +# The shared ci pipeline is not an option. synth's `ci` job calls # cosyte/.github/.github/workflows/ci.yml@main, which runs a fixed ladder and no # arbitrary repo script. # @@ -25,50 +44,38 @@ name: Em-dash gate # Re-running all of that every time someone fixes a typo in a PR description is a large # cost for no extra safety. Same call hl7, astm and website made, for the same reason. # -# ▶ THIS JOB IS DELIBERATELY NOT A REQUIRED CONTEXT, AND THE REASON IS NOT THAT IT IS -# WEAK. It scans the PR TITLE, BODY and COMMIT MESSAGES, and Dependabot composes a PR -# body by pasting the dependency's UPSTREAM RELEASE NOTES into it, em dashes included. -# Requiring this context would block a dependency bump on prose nobody in this org -# wrote and nobody in this org can edit without rewriting the PR by hand. That is the -# same refusal this ecosystem already made for a CI `pnpm audit`: a gate that fails on -# someone else's clock stops being a signal and starts being a tax. `website` reached -# the same conclusion and exempts its `no-emdash-messages` context on purpose. -# -# THE PRECEDENT IS NOT EXACTLY THIS SHAPE, AND THE DIFFERENCE COSTS SOMETHING REAL. SAY -# WHAT IT COSTS RATHER THAN INHERIT THE CONCLUSION. `website` exempts a context that -# scans MESSAGES ONLY, so its tracked-file half stays required. This job bundles both -# halves into one, so exempting it un-requires the tracked-file half as well, and that -# is the half that protects the published surface. +# ▶ THE TWO HALVES ARE TWO CONTEXTS, AND THAT SPLIT IS WHAT MAKES ONE OF THEM REQUIRABLE. +# The messages half scans the PR TITLE, BODY and COMMIT MESSAGES, and Dependabot +# composes a PR body by pasting the dependency's UPSTREAM RELEASE NOTES into it, em +# dashes included. Requiring that context would block a dependency bump on prose nobody +# in this org wrote and nobody in this org can edit without rewriting the PR by hand. +# That is the same refusal this ecosystem already made for a CI `pnpm audit`: a gate +# that fails on someone else's clock stops being a signal and starts being a tax. +# `website` reached the same conclusion and exempts its messages context on purpose. # -# NOTHING BUYS THAT HALF BACK TODAY, AND DO NOT WRITE THAT IT DOES. `ci / verify` runs a -# FIXED ladder in `cosyte/.github` (typecheck, lint, format:check, phi-scan, test, -# coverage, build, attw, dual ESM/CJS smoke) and no arbitrary repo script, so it does -# NOT run `pnpm check:no-emdash`. This workflow is the only place that script runs in -# CI. So the tracked-file half is a VISIBLE RED IN THE CHECKS LIST AND NOT A MERGE -# BLOCKER. That is the accepted cost, stated plainly, and it is what the sweep plus -# review carry until it is fixed. +# THE COST OF BUNDLING, STATED BECAUSE IT WAS PAID HERE. While both halves were one job, +# exempting it un-required the tracked-file half as well, and that is the half that +# protects the published surface. The shared gate workflow this file now calls runs the +# halves as two jobs, so `no-emdash / tracked-files` is requirable on its own terms and +# `no-emdash / messages` stays exempt. That was the deferred fix; it is done. # -# THE FIX IS TO SPLIT THIS JOB IN TWO, and it is deferred rather than rejected: a -# tracked-file job that can be required, and a messages job that stays exempt, which is -# exactly `website`'s shape and exactly how `no-internal-refs` is already required in -# this repo. It is not done in this change because a context may not be required before -# its workflow has completed on `main`, and this workflow has never run on `main`. -# Requiring it now would leave every PR pending with nothing saying why. +# `ci / verify` runs a FIXED ladder in `cosyte/.github` (typecheck, lint, format:check, +# phi-scan, test, coverage, build, attw, dual ESM/CJS smoke) and no arbitrary repo +# script, so it does NOT run `pnpm check:no-emdash`. This workflow is the only place +# that script runs in CI. # -# What still holds without the required flag: the job runs on every PR and on every -# push to `main`, and it is red in the checks list where a reviewer sees it. And on the -# Dependabot case specifically, the exemption gives up nothing on the tracked-file -# half. Dependabot certainly DOES write tracked files (`package.json` and +# On the Dependabot case specifically, the exemption gives up nothing on the +# tracked-file half. Dependabot certainly DOES write tracked files (`package.json` and # `pnpm-lock.yaml` are its entire job); what it cannot do is put an em dash in one, # because it writes version specifiers and lockfile records, never prose. The upstream # release notes it pastes go in the PR BODY and nowhere else. # -# Do not "fix" this by making it required and then adding an actor exemption. An -# `if: github.actor != 'dependabot[bot]'` on a REQUIRED context leaves the check -# permanently pending on exactly those PRs, which is worse than red: nothing says why. +# Do not "fix" the Dependabot case by requiring the messages context and then adding an +# actor exemption. An `if: github.actor != 'dependabot[bot]'` on a REQUIRED context +# leaves the check permanently pending on exactly those PRs, which is worse than red: +# nothing says why. # -# Run it locally with `pnpm check:no-emdash`, which is also on `scripts/verify.sh synth`'s -# ladder in the meta-repo. +# Run it locally with `pnpm check:no-emdash`. on: push: @@ -82,52 +89,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - jobs: - # The job id is the check-run context name a branch ruleset would require. Renaming it - # silently detaches any such requirement, so the ruleset stops protecting anything - # while still looking configured. Rename both together or neither. + # The check-run context prefix. See the note above before touching this id. no-emdash: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - # Full history: the commit-message step below reads the PR's commit range, - # and the default shallow clone does not contain the base commit. - fetch-depth: 0 - - - name: Check tracked files - run: bash scripts/check-no-emdash.sh - - # The rule names commit messages explicitly, and a squash merge composes its - # message from the PR title, the PR body, and the branch commits, so all three are - # checked. Pull requests only: on a push to main the message is already written, - # and this gate exists to stop it landing. - # - # This half is also the one a local pre-commit scan structurally cannot cover, and - # two slices elsewhere have lost a review pass to that: a NEW file is untracked, so - # a scan of the index does not see it, and no local hook sees a PR body at all. - - name: Check PR title, body, and commit messages - if: github.event_name == 'pull_request' - env: - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - # Collect first, scan second, and set the shell options here rather than trust - # the runner's default: piping the collection straight into the scanner would - # hide a failed `git log` (a rewritten base commit, or a future edit that drops - # fetch-depth) behind a clean title and body, and the gate would print OK over - # commits it never read. The title and body arrive through env and are never - # interpolated into the script, so a PR description cannot inject shell. - set -euo pipefail - { - printf '%s\n' "$PR_TITLE" - printf '%s\n' "$PR_BODY" - git log --format=%B "$BASE_SHA..$HEAD_SHA" - } > "$RUNNER_TEMP/pr-messages.txt" - bash scripts/check-no-emdash.sh --stdin "the PR title, body, or commit messages" \ - < "$RUNNER_TEMP/pr-messages.txt" + # Pinned to the 40 character commit, not to the tag beside it: a tag can be moved + # or deleted by anyone who gains write access to the workflow's repository, so a + # tag is not an immutable reference and is not a pin. The trailing comment is the + # published reference that commit was released as, so the release note stays + # findable from here. + uses: cosyte/.github/.github/workflows/gate-no-emdash.yml@84ecccd771cbc9c1ece003a3f8ff61415360395b # workflows-2026-09-18-84ecccd771cb + # No more than the scan needs. A called workflow's GITHUB_TOKEN can only be + # downgraded by its caller, and this gate reads the tree and nothing else. + permissions: + contents: read diff --git a/.github/workflows/no-internal-refs.yml b/.github/workflows/no-internal-refs.yml index e521e76..d56af41 100644 --- a/.github/workflows/no-internal-refs.yml +++ b/.github/workflows/no-internal-refs.yml @@ -14,10 +14,24 @@ name: Public-surface gate # workflow accordingly. This needs to not happen again." A sweep regresses the first time # someone writes `(SYNTH-10)` into a README; a job that reds does not. # +# WHAT ACTUALLY SCANS, AND WHERE THE RULE LIVES. `pnpm check:no-internal-refs`, which is +# this repository's own checker. The job below delegates the plumbing (checkout, pnpm, +# Node) to the shared gate workflow, which carries no pattern, no allow-list and no file +# selection of its own: it prepares the tree, runs the command, and lets the command's +# exit status decide the job. The detection rules, the self-tests and the known project +# prefixes all stay here, because what counts as a project prefix and what counts as a +# domain designator is a property of this package. +# +# THE JOB ID IS LOAD-BEARING. A called job's check run is named +# ` / `, so the job id `no-internal-refs` below publishes +# `no-internal-refs / public-surface`, which is the context a branch ruleset must require. +# Renaming the job detaches that requirement with no error anywhere, and a required +# context nothing emits blocks every pull request. +# # WHY A SEPARATE WORKFLOW, and not a job in ci.yml. Same reasoning the sibling copies give, # and the first half is the deciding one: # -# The reusable pipeline is not an option. synth's `ci` job calls +# The shared ci pipeline is not an option. synth's `ci` job calls # cosyte/.github/.github/workflows/ci.yml@main, which runs a fixed ladder and no arbitrary # repo script. Adding this there would also flip every other caller red on its next run: # the rule is still un-remediated in most of the ecosystem, and each repo's sweep is its @@ -50,24 +64,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - jobs: - # The job id is the check-run context name a branch ruleset requires. Renaming it - # silently detaches the required check, so the ruleset stops protecting anything while - # still looking configured. Rename both together or neither. + # The check-run context prefix. See the note above before touching this id. no-internal-refs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - # The script self-tests before it reports: it proves each rule still matches its own - # positive sample AND still lets through the reference material it was most likely to - # destroy (`NCPDP-SCRIPT`, `HL7-V2`, `FHIR-R4`, `ASTM-E1394`, `ICD-10-CM`, a Phase III - # trial) and the synthetic-provenance markers this package stamps into its own output - # (`SYNTH-FAC`, `SYNTH-LIS`, `SYNTH-ANALYZER`). It refuses to print OK from a scan - # that did not read all of its input. No `|| true`, no `continue-on-error`: a check - # that cannot fail is documentation. - - name: Check the public surface - run: bash scripts/check-no-internal-refs.sh + # Pinned to the 40 character commit, not to the tag beside it: a tag can be moved + # or deleted by anyone who gains write access to the workflow's repository, so a + # tag is not an immutable reference and is not a pin. The trailing comment is the + # published reference that commit was released as, so the release note stays + # findable from here. + uses: cosyte/.github/.github/workflows/gate-no-internal-refs.yml@84ecccd771cbc9c1ece003a3f8ff61415360395b # workflows-2026-09-18-84ecccd771cb + # No more than the scan needs. A called workflow's GITHUB_TOKEN can only be + # downgraded by its caller, and this gate reads the tree and nothing else. + permissions: + contents: read