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
17 changes: 17 additions & 0 deletions .github/workflows/lint.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -193,6 +193,23 @@ jobs:
- name: PM dispatch-gates self-test
run: pnpm check:pm-dispatch-gates

# Part-of/closing-keyword guard self-test (#8476). The guard itself is a
# PR-scoped blocking check in its own workflow — it needs a pull request
# body to judge, which this job does not have — so what runs HERE is its
# self-test, which is the half with a verdict independent of any PR.
# Unconditional for the same reason as the two steps above: a self-test
# that runs only when someone remembers is a check whose coverage is a
# function of who remembered, and the failure it hides is quiet — a break
# in the verdict layer lands green and surfaces later as a card silently
# closed by the sentence written to keep it open.
#
# The self-test also pins the WIRING (the guard workflow still invokes
# the script, still subscribes to `edited`, still passes the body through
# `env:`), so unwiring the gate reddens here rather than going quiet.
# Imports one sibling module, reads two files; ~0.1s.
- name: Part-of closing-keyword guard self-test
run: pnpm check:partof-closing-keyword

# Docs/skills authoring guard (#2035 / ADR-0059): TS code blocks in
# Markdown/MDX are not type-checked or ESLinted, so skills/ and
# content/docs/ can drift back to teaching the bare `: Page = {}` literal
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/partof-closing-keyword-guard.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
# A PR that declares itself only `Part of #N` must not also tell GitHub to
# close `#N`. GitHub's closing-keyword parser matches the keyword plus the
# number and ignores the surrounding prose entirely — negations and modals
# included — so the sentence an author writes to PREVENT an auto-close is
# exactly what performs it on merge. That happened: a half-delivered card was
# closed `completed` two seconds after its PR merged, by its own warning
# sentence, and a closed card reads as finished, so it was found only by a
# post-merge inventory re-pull. The author wrote the warning correctly and
# still lost the card, which is why this is mechanical and not advice.
#
# The rule, the wording of the failure, and the code-stripping this depends on
# all live in `scripts/check-partof-closing-keyword.mjs` and the predicate it
# reuses; that header is authoritative, this file is the invocation.
#
# Sibling shape, deliberately copied rather than reinvented: the Duplicate Fix
# Guard is this repo's other PR-body-scoped blocking check, and it takes the
# same trigger set for the same measured reason.
name: Part-of Closing-Keyword Guard

# `edited` is load-bearing, not decoration. The body is this check's whole
# input, and GitHub does not re-deliver a `pull_request` event when a body
# changes under any other activity type — while `rerun_failed_jobs` replays the
# frozen payload, so a re-run of a fixed body stays red forever. With `edited`
# subscribed, rewording the sentence fires a fresh event carrying the fresh
# body and the check goes green with no push and no re-run.
#
# No `merge_group:` trigger, and that is not an oversight: a merge-queue event
# carries no pull request and therefore no body, so this check has nothing to
# judge there. That also keeps it out of the required-context registry, whose
# entries must report on queue builds; see the script header on where branch
# protection fits.
on:
pull_request:
types: [opened, edited, reopened, synchronize]

permissions:
contents: read

concurrency:
group: partof-closing-keyword-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
partof-closing-keyword:
name: Part-of PR must not also close its card
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

# Pinned to the same major and spelling as every other setup-node in this
# repo, and for a measured reason rather than tidiness. The first draft of
# this file used `actions/setup-node@v5` with `node-version-file`, and the
# job died IN THIS STEP, before the script ever ran:
#
# ##[error]Unable to locate executable file: pnpm.
#
# v5 defaults `package-manager-cache: true` (the run log echoes it as an
# input), which reads `packageManager: pnpm@...` out of package.json and
# shells out to pnpm to locate the store to cache. This job installs no
# package manager on purpose — the script is dependency-free and imports
# one sibling module — so pnpm is not on PATH and the action hard-errors.
# The failure is worth naming because nothing in the step that failed
# mentions pnpm: it is an implicit default of the action, invisible in the
# workflow source, and the misleading first read is that the `run:` line
# below invoked a package manager. It does not, and never did.
#
# The adr-merge-approval gate is the known-good shape this now matches
# exactly: checkout, setup-node, one `node scripts/check-*.mjs` call, no
# install and no corepack. It is green on this repo today.
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'

# The body reaches the script through `env:`, never through `${{ }}`
# inside the `run:` script. An expression interpolated into a shell line
# is substituted before bash ever sees it, so a PR body is arbitrary
# attacker-controlled text landing in a command; through `env:` it is
# inert data. The script's self-test pins this spelling.
#
# `PR_NUMBER` is not only for the message: GitHub renders a null body as
# an EMPTY value, so it is the witness that separates "this PR has an
# empty body" (a real, clean verdict) from "this step was handed nothing"
# (a wiring failure, which exits 2 and says so).
#
# No install step: the script imports one sibling module and reads no
# workspace package, so `node` on the pinned runtime is the whole
# toolchain it needs.
- name: A Part-of PR body may not carry a closing keyword for the same card
env:
PR_BODY: ${{ github.event.pull_request.body }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: node scripts/check-partof-closing-keyword.mjs
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@
"check:pm-skill-ratchet": "node scripts/pm/check-skill-line-ratchet.mjs --self-test && node scripts/pm/check-skill-line-ratchet.mjs",
"check:pm-skill-id-lint": "node scripts/pm/check-skill-id-lint.mjs --self-test && node scripts/pm/check-skill-id-lint.mjs",
"check:pm-dispatch-gates": "node scripts/pm/check-dispatch-gates.mjs",
"check:partof-closing-keyword": "node scripts/check-partof-closing-keyword.mjs --self-test",
"check:adr-anchors": "node scripts/check-adr-anchors.mjs --self-test && node scripts/check-adr-anchors.mjs",
"check:adr-links": "node scripts/check-adr-links.mjs --self-test && node scripts/check-adr-links.mjs",
"check:adr-merge-approval": "node scripts/check-adr-merge-approval.mjs --self-test && node scripts/check-adr-merge-approval.mjs",
Expand Down
Loading
Loading