From 93a3f94b7b0646aa265ace9b2260625dc9bca516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AE=B7=E4=BA=AE=E8=BE=89?= Date: Mon, 10 Aug 2026 15:19:13 +0000 Subject: [PATCH] docs(ci): stop the Merge Queue section keeping its own copy of the subscriber list (#4154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section opened with "Five workflows subscribe:" and named five while six carry a `merge_group:` trigger — it went stale the moment objectui#3735 added `skills-paths.yml`, and a stale list reads exactly as authoritative as a fresh one. objectui#3261's defect, one subsystem over, on the paragraph written for the reader deciding whether a new gate of theirs has to subscribe. Option C per the ruling on the card. The live claim converges on a pointer to `MUST_SUBSCRIBE_MERGE_GROUP` in scripts/__tests__/merge-queue-reporting.test.ts — the only copy an assertion reads — and keeps teaching the selection rule that decides membership. The dated clause keeps its four names: those four did not subscribe until objectui#3523 (PR #3722), a past fact that cannot drift. The page is pinned against that map in the same file: no enumeration of current subscribers outside the dated paragraph, no cardinality anywhere in the section, and the pointer itself is required so the section cannot pass by saying nothing. Co-authored-by: Claude --- content/docs/guide/ci-cd-pipeline.md | 25 ++- .../__tests__/merge-queue-reporting.test.ts | 158 ++++++++++++++++++ 2 files changed, 175 insertions(+), 8 deletions(-) diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index b1fc460ef8..f6699bc50e 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -73,11 +73,18 @@ checks it requires are green **on that rebuilt commit**. Those runs are a distin `merge_group`, on a throwaway `gh-readonly-queue/**` branch — a workflow that does not subscribe to that event simply does not run there. -Five workflows subscribe: `ci.yml`, `lint.yml`, `control-bytes.yml`, `docs-links.yml` and -`changeset-presence.yml` (the last added with the gate itself, in -[#3387](https://github.com/objectstack-ai/objectui/issues/3387) — a gate that carries no path -filter reports on every pull request and is therefore requirable, which is exactly the property -this list tracks). None of the first four did until +Which workflows subscribe is deliberately not listed here. `MUST_SUBSCRIBE_MERGE_GROUP` in +`scripts/__tests__/merge-queue-reporting.test.ts` is the maintained list, and the only copy +anything reads — it records why each entry is on it, and an assertion fails when one of them drops +the trigger. A copy of it on this page would be right the day it was written and quietly wrong +after the next subscriber landed, which is exactly what this paragraph used to do +([#4154](https://github.com/objectstack-ai/objectui/issues/4154)). What is worth knowing here is +the rule that decides membership, not the instances: a gate that carries no path filter reports on +every pull request and is therefore requirable — and a requirable context that skips the queue +build does not fail it, it stalls it. + +That rule was learned the expensive way. `ci.yml`, `lint.yml`, `control-bytes.yml` and +`docs-links.yml` did not subscribe at all until [#3523](https://github.com/objectstack-ai/objectui/issues/3523), and the consequence was not subtle. A queue whose required set is empty validates nothing: it rebuilds the PR, sees no failing required check because there are no required checks, and merges. On 2026-08-07 three pull requests @@ -100,9 +107,11 @@ queued PR burns an hour and fails, with nothing red to point at. Two things follow for anyone editing this directory: -- **A workflow producing a context that could ever be required must subscribe `merge_group`.** - `scripts/__tests__/merge-queue-reporting.test.ts` holds the list, along with the reason each - entry is on it, and fails when one drops the trigger. +- **A workflow producing a context that could ever be required must subscribe `merge_group`**, + and takes an entry in `MUST_SUBSCRIBE_MERGE_GROUP` (above) naming the context it produces. That + entry is what fails the build if the workflow later drops the trigger; nothing derives the set, + because "may this context be required?" is a property of the repository's settings, which no + test here can read. - **Some contexts can never be required, structurally**, and no amount of triggering changes that: **Changeset Bump Policy** (`changeset-guard.yml`, inverse path filter — absent unless the PR touches `.changeset/**`), **Bundle Analysis** (`performance-budget.yml`, path filter), diff --git a/scripts/__tests__/merge-queue-reporting.test.ts b/scripts/__tests__/merge-queue-reporting.test.ts index f3e8345a50..007b25b245 100644 --- a/scripts/__tests__/merge-queue-reporting.test.ts +++ b/scripts/__tests__/merge-queue-reporting.test.ts @@ -45,6 +45,8 @@ import { fileURLToPath } from 'node:url'; */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const workflowDir = path.join(repoRoot, '.github/workflows'); +const DOC = 'content/docs/guide/ci-cd-pipeline.md'; +const doc = fs.readFileSync(path.join(repoRoot, DOC), 'utf8'); /** * `filename -> why this workflow must subscribe merge_group`. @@ -353,3 +355,159 @@ describe('every context reports on every pull request (#3523 step 2)', () => { } }); }); + +/** + * The page's copy of the same list (objectui#4154). + * + * `content/docs/guide/ci-cd-pipeline.md` opened its `## Merge Queue` section with + * "Five workflows subscribe:" and named five, while the map above held six — the + * page went stale the moment objectui#3735 added `skills-paths.yml`, and stale + * prose reads exactly as authoritative as fresh prose. That is objectui#3261's + * defect one subsystem over, and it lands on the one reader the paragraph is + * written for: someone deciding whether a new gate of theirs has to subscribe. + * + * The page now points at `MUST_SUBSCRIBE_MERGE_GROUP` instead of copying it, and + * these assertions keep it that way. The honest caveat, recorded rather than + * hidden: this pins a copy to a copy, since the map is itself hand-maintained. + * What makes that trade worth taking is that the map is read by an assertion — + * a member that stops subscribing fails the first test in this file, and a member + * that stops existing fails the second — while prose is read by no one until it + * has already misled someone. + * + * Two paragraphs, two different rules, because they are two different kinds of + * claim: + * + * - the LIVE claim ("which workflows subscribe") may not enumerate and may not + * count. It cannot go stale if it holds no instances; + * - the DATED clause (those four did not subscribe until objectui#3523, PR + * #3722) keeps its four names. A past fact cannot drift, and the sentence + * needs them: "none of the first four" has no antecedent once the live list + * is gone. + * + * The exemption is therefore granted per PARAGRAPH, anchored on the #3523 link, + * rather than to the four names anywhere in the section — otherwise a future + * present-tense sentence naming exactly those four would pass while being short + * by every subscriber added since, which is this issue verbatim. + * + * The residual hole, stated rather than left for the next reader to find: a + * present-tense list of exactly those four names, written INSIDE the dated + * paragraph and carrying no count, still passes. That is a sentence someone has + * to author deliberately and falsely; what these assertions exist to stop is + * DRIFT, and a page holding no live list cannot drift — a seventh subscriber + * changes nothing on it. Every other spelling of the old sentence is red: five of + * the six names outside the dated paragraph, any of the two later ones inside it, + * and any cardinality anywhere in the section. + */ +const HISTORY_ANCHOR = 'objectui/issues/3523'; + +/** + * The workflows the dated clause names — the ones PR #3722 subscribed for + * objectui#3523 step 1. Asserted below to be a subset of the map, so the + * exemption cannot be widened into a second live list by adding names to it. + */ +const HISTORY_NON_SUBSCRIBERS = ['ci.yml', 'lint.yml', 'control-bytes.yml', 'docs-links.yml']; + +/** + * The `## Merge Queue` section, from its heading to the next `## `. + * + * The scoping is load-bearing, not tidiness: `ci.yml` has a section of its own + * further down the same page and is named dozens of times there, so a whole-file + * scan for subscriber names would report all of them. + */ +function mergeQueueSection(): string { + const start = doc.indexOf('\n## Merge Queue\n'); + expect(start, `${DOC} must still have a "## Merge Queue" section`).toBeGreaterThan(-1); + const rest = doc.slice(start + 1); + const next = rest.indexOf('\n## '); + return next === -1 ? rest : rest.slice(0, next); +} + +/** Blank-line-separated paragraphs; a bullet list is one paragraph, as authored. */ +const paragraphsOf = (section: string): string[] => + section + .split(/\n[ \t]*\n/) + .map((p) => p.trim()) + .filter(Boolean); + +/** `ci.yml` but not `eslint.yml`, and not the tail of a longer path. */ +const namesWorkflow = (text: string, file: string): boolean => + new RegExp(`(? { + it('names no current subscriber outside the dated #3523 paragraph', () => { + const offenders: string[] = []; + for (const paragraph of paragraphsOf(mergeQueueSection())) { + const dated = paragraph.includes(HISTORY_ANCHOR); + for (const file of MUST_SUBSCRIBE_MERGE_GROUP.keys()) { + if (!namesWorkflow(paragraph, file)) continue; + if (dated && HISTORY_NON_SUBSCRIBERS.includes(file)) continue; + offenders.push(`${file} — in ${dated ? 'the dated #3523 paragraph' : 'a live paragraph'}: "${paragraph.split('\n')[0].slice(0, 72)}…"`); + } + } + + expect( + offenders, + `The "## Merge Queue" section of ${DOC} enumerates workflows that subscribe ` + + `\`merge_group\` today:\n` + + offenders.map((o) => ` - ${o}`).join('\n') + + `\n\nThat list belongs in exactly one place, \`MUST_SUBSCRIBE_MERGE_GROUP\` in this file, ` + + `which is the copy an assertion reads. A second copy in prose is short by one subscriber ` + + `the day the next gate lands and still reads as authoritative — the page said "Five ` + + `workflows subscribe" for as long as it took someone to count the YAML (objectui#4154, ` + + `the same defect objectui#3261 removed from lint.yml). Point at the map instead. The one ` + + `exemption is the dated paragraph linking #3523, which names the four workflows that did ` + + `not subscribe before it: that is a past fact and cannot drift.`, + ).toEqual([]); + }); + + it('states no count of subscribing workflows', () => { + // "Five workflows subscribe" is the drift itself: a number is wrong the + // moment the set changes and nothing on the page can notice. + const counted = [ + ...mergeQueueSection().matchAll( + /\b(?:one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|\d+)\b[^.\n]{0,24}?workflows?\b/gi, + ), + ].map((m) => m[0]); + + expect( + counted, + `The "## Merge Queue" section of ${DOC} hard-codes how many workflows subscribe ` + + `\`merge_group\`:\n` + + counted.map((c) => ` - "${c}"`).join('\n') + + `\n\nThe map in this file is the list; the page states no cardinality, for the same ` + + `reason the Core CI section states no job count (objectui#3451) and this page's workflow ` + + `inventory states no workflow count (objectui#3212). A count that nothing reads drifts ` + + `silently — this one said five while six subscribed (objectui#4154).`, + ).toEqual([]); + }); + + it('keeps the pointer that replaced the list', () => { + // Without this, deleting the pointer passes both assertions above: no names, + // no count, and nothing telling the reader where the answer actually lives. + // Green because nothing was produced is the failure mode this whole file is + // about. + const section = mergeQueueSection(); + for (const marker of ['MUST_SUBSCRIBE_MERGE_GROUP', 'scripts/__tests__/merge-queue-reporting.test.ts']) { + expect( + section, + `The "## Merge Queue" section of ${DOC} no longer names \`${marker}\`. The section may ` + + `not enumerate subscribers (above), so the pointer is the only thing left that answers ` + + `"which workflows subscribe, and why that one?" — dropping it leaves the reader with a ` + + `rule and no way to check an instance (objectui#4154).`, + ).toContain(marker); + } + }); + + it('keeps the dated exemption honest — its four names are all map entries', () => { + for (const file of HISTORY_NON_SUBSCRIBERS) { + expect( + [...MUST_SUBSCRIBE_MERGE_GROUP.keys()], + `HISTORY_NON_SUBSCRIBERS names ${file}, which is not in MUST_SUBSCRIBE_MERGE_GROUP. The ` + + `exemption exists for one dated sentence about the four workflows PR #3722 subscribed ` + + `(objectui#3523 step 1); it is not a place to park names the live rule would reject. ` + + `If ${file} genuinely stopped being a requirable gate, check that the #3523 paragraph ` + + `still reads correctly, then update both lists together.`, + ).toContain(file); + } + }); +});