Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 55 additions & 84 deletions .github/workflows/no-emdash.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# `<caller job id> / <inner job id>`, 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.
#
Expand All @@ -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:
Expand All @@ -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
46 changes: 26 additions & 20 deletions .github/workflows/no-internal-refs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# `<caller job id> / <inner job id>`, 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
Expand Down Expand Up @@ -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
Loading