diff --git a/ui/scripts/__tests__/bash32-floor-wiring.test.ts b/ui/scripts/__tests__/bash32-floor-wiring.test.ts index d8e51c6c02d..cf749f1c255 100644 --- a/ui/scripts/__tests__/bash32-floor-wiring.test.ts +++ b/ui/scripts/__tests__/bash32-floor-wiring.test.ts @@ -6,9 +6,11 @@ import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; import { selfTestCases, stripAnsi } from './helpers/child-verdict'; +import { rootCi, uiRun, UI_JOBS } from './helpers/root-ci'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-bash32-floor.mjs'; +const ALIAS = 'check:bash32-floor'; /** * objectui#7692: this repository held its own shell to nothing. @@ -36,10 +38,12 @@ const GATE = 'scripts/check-bash32-floor.mjs'; * passes: * * 1. the script exists and `package.json`'s alias points at it; - * 2. `lint.yml` really runs it, in a step that executes, on pull requests; - * 3. the workflow spelling and the alias spelling name the SAME script, so - * `pnpm check:bash32-floor` and the pre-install `node` invocation cannot - * drift into checking different things; + * 2. the root `Lint UI` job (`lint-ui` in the repository root's `ci.yml`, + * where objectstack#25697 ported `lint.yml`) really runs it, in a step + * that executes, on pull requests; + * 3. that step runs the alias, and the alias names the script, so + * `pnpm check:bash32-floor` and the pre-install CI step cannot drift into + * checking different things; * 4. it runs BEFORE `pnpm install` — placed after, an install failure takes * the gate with it and the tree goes unjudged with the job red for an * unrelated reason; @@ -50,9 +54,13 @@ const GATE = 'scripts/check-bash32-floor.mjs'; * whose loss would put the card straight back. */ describe('check-bash32-floor is wired, not merely present', () => { - const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows/lint.yml'), 'utf8')); - const steps: Array> = workflow.jobs.lint.steps; - const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GATE)); + // objectstack#25712: the job that runs it is the root `ci.yml`'s `lint-ui`, + // where objectstack#25697 ported objectui's `lint.yml` (roster row 16), read + // by path. It spells the gate through its alias, so `ALIAS` is what a step + // runs and the alias case below is what ties that to the file. + const workflow = parseYaml(rootCi()); + const steps: Array> = workflow.jobs[UI_JOBS.lint].steps; + const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(uiRun(ALIAS))); it('the gate script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); @@ -60,16 +68,16 @@ describe('check-bash32-floor is wired, not merely present', () => { it('package.json aliases it, and the alias points at the script that exists', () => { const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); - const alias = pkg.scripts['check:bash32-floor']; + const alias = pkg.scripts[ALIAS]; expect(alias).toBeTruthy(); expect(alias).toContain(GATE); }); - it('lint.yml runs it — exactly one step, both legs', () => { + it('the root Lint UI job runs it — exactly one step, both legs', () => { expect(gateSteps).toHaveLength(1); const run = gateSteps[0].run as string; - expect(run).toContain(`node ${GATE} --self-test`); - expect(run.split('\n').some((l) => l.trim() === `node ${GATE}`)).toBe(true); + expect(run).toContain(`${uiRun(ALIAS)} --self-test`); + expect(run.split('\n').some((l) => l.trim() === uiRun(ALIAS))).toBe(true); }); it('that step is not disabled — it runs whenever the job runs its other steps', () => { @@ -82,7 +90,7 @@ describe('check-bash32-floor is wired, not merely present', () => { expect(others).toContain(condition); }); - it('lint.yml still gates pull requests, or the step above is inert', () => { + it('the root ci.yml still gates pull requests, or the step above is inert', () => { // `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`. const on = workflow.on ?? workflow[true as unknown as string]; expect(Object.keys(on)).toContain('pull_request'); diff --git a/ui/scripts/__tests__/check-changeset-claims.test.ts b/ui/scripts/__tests__/check-changeset-claims.test.ts index 7db3554ba5d..b5759a01742 100644 --- a/ui/scripts/__tests__/check-changeset-claims.test.ts +++ b/ui/scripts/__tests__/check-changeset-claims.test.ts @@ -35,11 +35,14 @@ import { * What this file pins, in the order the gate can fail: * * 1. **It can start on the pull request that needs it.** The falsifying change - * is an ordinary source change, so the gate lives in the workflow with NO - * path filter. That absence is the load-bearing property and is pinned here: - * adding a `paths:` filter to that workflow would blind this gate silently, - * which is the exact failure mode `changeset-guard.yml` can afford and this - * one cannot. + * is an ordinary source change, so the gate lived in the workflow with NO + * path filter, and that absence was pinned here. Retired with + * objectstack#25712, with the delivery pins of section 1b and the comment + * step's count pins in the corpus section: they read objectui's + * `changeset-presence.yml`, which runs nowhere under the monorepo (GitHub + * runs the root `.github/workflows/` alone). It is row 4 of the + * objectstack#25697 workflow roster (→ sub-issue 4), and a port brings them + * back against its root workflow, by path. * 2. **Both directions of the verdict**, against throwaway repositories rather * than this one's history, so they stay decidable when the history moves. A * report that fires, and a FIRING CONTROL — the same fixture with the named @@ -61,24 +64,6 @@ import { */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const GATE = 'scripts/check-changeset-claims.mjs'; -const WORKFLOW = '.github/workflows/changeset-presence.yml'; - -/** - * A workflow's YAML with whole-line comments removed. - * - * Required, not cosmetic: this workflow's header discusses its own trigger set - * at length, so a scan that counted the prose would report filter entries the - * file does not have. Same helper, same reason, as in the sibling changeset - * gate tests. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -const workflowYaml = withoutComments(fs.readFileSync(path.join(repoRoot, WORKFLOW), 'utf8')); // ── fixture repositories ───────────────────────────────────────────────────── @@ -200,88 +185,27 @@ function lastCommitRange(fixture: Fixture): string[] { } // ── 1. the gate can start on the pull request that needs it ────────────────── - -describe('changeset-presence.yml — where this gate has to live', () => { - it('exists, and the workflow runs it', () => { - expect(fs.existsSync(path.join(repoRoot, GATE)), `${GATE} must exist for the workflow to run it`).toBe(true); - expect(workflowYaml).toMatch(new RegExp(`run:\\s*node\\s+${GATE.replace(/[.]/g, '\\.')}`)); - }); - - it('gives the job the history its diff needs', () => { - // The gate compares this change against its MERGE BASE. checkout's default - // is a depth-1 clone where `git merge-base` has nothing to find, and an - // unresolvable base is exit 1 in the script rather than a skip — so getting - // this wrong is a red build, not a silent pass. Pinned so it stays that way. - expect(workflowYaml).toMatch(/fetch-depth:\s*0/); - }); - - it('carries NO paths filter — the property this gate depends on', () => { - // `changeset-guard.yml` can afford a `paths: ['.changeset/**']` filter - // because a change that modifies a changeset touches `.changeset/**` BY - // DEFINITION. This gate's subject is the opposite: an ordinary source change - // that falsifies somebody else's pending claim, which is under no obligation - // to touch `.changeset/**` at all. A path filter added here would blind the - // gate with no red anywhere, so its ABSENCE is pinned. - expect(workflowYaml).not.toMatch(/^\s*paths(-ignore)?:/m); - }); -}); - +// +// Retired with objectstack#25712: `exists, and the workflow runs it`, `gives the +// job the history its diff needs` and `carries NO paths filter — the property +// this gate depends on`. +// // ── 1b. the finding is DELIVERED, not archived (objectui#9140) ─────────────── - -describe('delivery — the ruling that the finding reaches the pull request', () => { - // objectui#9140 measured the "REQUEST TO READ" at zero answers out of four - // live instances, because it was addressed to a job log nobody opens on a - // green check. The director's ruling (maintainer 「同意」) moves WHERE the - // finding is read and ⛔ nothing else: exit 0, not a required context, - // nothing blocks. - it('hands this run\'s finding set to the renderer instead of re-measuring it', () => { - // One measurement, two consumers. A report that re-derives its own subject - // can disagree with the log it claims to report, and the disagreement is - // invisible to both sides. - expect(workflowYaml).toMatch(/node\s+scripts\/check-changeset-claims\.mjs\s+--json\s+claims\.json/); - expect(workflowYaml).toMatch( - /node\s+scripts\/render-changeset-claims-comment\.mjs\s+--from\s+claims\.json/, - ); - }); - - it('posts through the channel the Console Performance Budget report uses', () => { - expect(workflowYaml).toMatch(/uses:\s*actions\/github-script@/); - const budget = withoutComments( - fs.readFileSync(path.join(repoRoot, '.github/workflows/performance-budget.yml'), 'utf8'), - ); - expect(budget).toMatch(/uses:\s*actions\/github-script@/); - }); - - it('holds `pull-requests: write` on the JOB, leaving the declaration gate with a read-only token', () => { - // A job-level block REPLACES the workflow-level one, so `contents: read` - // has to be named again or checkout loses it. The workflow-level block must - // stay read-only: the declaration gate above needs nothing but a checkout, - // and a grant it does not use is a grant nobody is auditing. - expect(workflowYaml).toMatch(/permissions:\n\s+contents: read\n\s+pull-requests: write/); - expect(workflowYaml).toMatch(/^permissions:\n\s+contents: read\n/m); - expect(workflowYaml).not.toMatch(/^permissions:\n\s+contents: read\n\s+pull-requests/m); - }); - - it('⛔ never subscribes `pull_request_target`, which would run this on a fork\'s branch with a write token', () => { - expect(workflowYaml).not.toMatch(/pull_request_target/); - }); - - it('comments only on a pull request', () => { - // A `merge_group` build has no pull request to comment on, and the gate - // still has to REPORT there — so the delivery steps are conditioned and the - // measuring step is not. - const conditions = workflowYaml.match(/if: \$\{\{ github\.event_name == 'pull_request' \}\}/g) ?? []; - expect(conditions.length).toBe(2); - }); - - it('cannot paint this job red by failing to post', () => { - // A fork's pull request gets a read-only token and the API call fails for a - // reason that says nothing about changesets. Report-only means the gate - // declines to fail on its FINDINGS; it must not start failing on its - // PLUMBING either. - expect(workflowYaml).toMatch(/continue-on-error: true/); - }); -}); +// +// Retired with objectstack#25712: the six delivery cases (the run hands its +// `--json` finding set to `render-changeset-claims-comment.mjs`, posts through +// `actions/github-script` as `performance-budget.yml` does, holds +// `pull-requests: write` on the job alone, never subscribes +// `pull_request_target`, comments only on a pull request, and cannot paint the +// job red by failing to post). The ruling they pinned stands: the finding +// reaches the pull request, exit 0, not a required context, nothing blocks. +// +// Every one of them read objectui's `changeset-presence.yml` (one also read its +// `performance-budget.yml`, roster row 22). Both run nowhere under the +// monorepo: GitHub runs the root `.github/workflows/` alone, and no root +// workflow runs this gate. `changeset-presence.yml` is row 4 of the +// objectstack#25697 workflow roster (→ sub-issue 4); a port brings these pins +// back against its root workflow, by path. // ── 2. both directions of the verdict ──────────────────────────────────────── @@ -953,35 +877,16 @@ describe('the corpus is the prose this change publishes about itself', () => { expect(gate).toContain('check-new-cross-file-line-citations.mjs'); }); - it('delivers a born-false-only finding instead of leaving it in the job log', () => { - // objectui#9140 measured the job-log channel at zero answers out of four. - // The comment step decides whether to post from the finding COUNT, so a run - // whose only finding is born-false has to be counted there too. - expect(workflowYaml).toContain('(measured.bornFalse ?? []).length'); - }); - - it('⭐ delivers a self-contradiction-only finding too (objectui#9841)', () => { - // ⚠️ ASSERTED AS ITS OWN TERM, ⛔ never as the whole sum. A pin that matched - // the sum verbatim would redden on a harmless rewording and — worse — would - // be repaired by pasting whatever the file now says, which is how a dropped - // term gets blessed. This one goes red for exactly one reason: this reading - // stopped being counted, and its findings stopped creating the comment. - expect(workflowYaml).toContain('(measured.selfContradiction ?? []).length'); - }); - - it('counts EVERY reading the gate carries — one term per reading, none left out', () => { - // The standing obligation, stated where the next reading's author will read - // it: a reading that is measured, rendered and then not counted here is - // delivered to a job log nobody opens. That is objectui#9842's shape, and it - // is the reason this surface was extended rather than left for later. - for (const term of [ - 'measured.findings.length', - '(measured.bornFalse ?? []).length', - '(measured.selfContradiction ?? []).length', - ]) { - expect(workflowYaml).toContain(term); - } - }); + // Retired with objectstack#25712: `delivers a born-false-only finding instead + // of leaving it in the job log`, `⭐ delivers a self-contradiction-only finding + // too (objectui#9841)` and `counts EVERY reading the gate carries — one term + // per reading, none left out`. Each read the comment step's finding count in + // objectui's `changeset-presence.yml` (`measured.findings.length`, + // `(measured.bornFalse ?? []).length`, + // `(measured.selfContradiction ?? []).length`), which runs nowhere under the + // monorepo. The obligation is the port's: the root workflow that delivers + // this gate's findings counts every reading it carries, and these pins come + // back against it, by path (roster row 4, → sub-issue 4). }); describe('the boundary control the first ablation of this change exposed', () => { diff --git a/ui/scripts/__tests__/check-changeset-no-major.test.ts b/ui/scripts/__tests__/check-changeset-no-major.test.ts index d34eae6e74e..7ce92d29a89 100644 --- a/ui/scripts/__tests__/check-changeset-no-major.test.ts +++ b/ui/scripts/__tests__/check-changeset-no-major.test.ts @@ -48,14 +48,15 @@ const mask: (source: string) => string = maskComments; * group's own manifests (objectui#12334, under the maintainer's ruling on * objectui#12030). The fixtures below pin both directions of that computation. * - * These tests are the second lock. `.github/workflows/changeset-guard.yml` - * catches it on the PR that adds the changeset; the repo-state test below - * catches it in `pnpm test` even if that workflow is ever removed or its - * trigger stops matching. + * These tests are the second lock. objectui's `changeset-guard.yml` caught it + * on the PR that added the changeset; the repo-state test below catches it in + * `pnpm test` even if that workflow is removed or its trigger stops matching. + * Under the monorepo that workflow runs nowhere (GitHub runs the root + * `.github/workflows/` alone), so the repo-state test, in `Test UI`, is the + * lock that runs until sub-issue 4 ports the guard (objectstack#25712). */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const changesetDir = path.join(repoRoot, '.changeset'); -const workflowDir = path.join(repoRoot, '.github/workflows'); const GATE = path.join(repoRoot, 'scripts/check-changeset-no-major.mjs'); interface Entry { @@ -410,37 +411,11 @@ describe('the repository itself', () => { }); }); -describe('changeset-guard.yml', () => { - const guard = fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8'); - - it('runs the guard script', () => { - expect(guard).toContain('node scripts/check-changeset-no-major.mjs'); - }); - - it('triggers on `.changeset/**` — the paths every other workflow ignores', () => { - // The inverse pin, and the reason `ci.yml` still cannot host this check: on - // a changeset-only PR its `Decide whether this change needs a full run` step - // finds nothing outside its exclusion list (markdown and `.changeset/**` - // among it) and skips every step below itself, so no gate in that workflow - // ever reads the changeset. This one has to run outside that decision. - // - // Not the reason this comment used to give — "a changeset-only PR matches - // its `paths-ignore` twice over, so no job in it would ever run". Since - // objectui#3523 step 2, `paths-ignore` is gone from the `pull_request` - // trigger and survives only on `push`: such a PR does start `ci.yml` and - // does produce its contexts (measured — PR #3856, one markdown file, 16 - // checks; PR #4339, one line added to AGENTS.md, 17). The conclusion - // outlived the premise; objectui#3857 pinned the correction. - // - // That surviving `push` copy is what the two assertions below read, and it - // is why they stay green — `ci.yml` genuinely still declares `paths-ignore` - // with markdown in it, on `push`. The in-job exclusion list is held - // identical to that filter by `scripts/__tests__/merge-queue-reporting.test.ts`, - // so the pair cannot drift apart silently. - expect(guard).toMatch(/paths:\s*\n\s*- '\.changeset\/\*\*'/); - - const ci = fs.readFileSync(path.join(workflowDir, 'ci.yml'), 'utf8'); - expect(ci).toContain("paths-ignore:"); - expect(ci).toContain("- '**/*.md'"); - }); -}); +// `changeset-guard.yml` (`runs the guard script`, and `triggers on +// .changeset/** — the paths every other workflow ignores`) retired with +// objectstack#25712. Both cases read objectui's `changeset-guard.yml` and its +// `ci.yml`, which run nowhere under the monorepo: GitHub runs the root +// `.github/workflows/` alone, and no root workflow runs this gate. The guard is +// row 3 of the objectstack#25697 workflow roster (→ sub-issue 4); a port brings +// these pins back against its root workflow, by path. The repo-state cases +// above are the lock that runs meanwhile. diff --git a/ui/scripts/__tests__/check-changeset-overwrite.test.ts b/ui/scripts/__tests__/check-changeset-overwrite.test.ts index d909cf86c08..1b00eb2be51 100644 --- a/ui/scripts/__tests__/check-changeset-overwrite.test.ts +++ b/ui/scripts/__tests__/check-changeset-overwrite.test.ts @@ -20,12 +20,13 @@ import { declaredEntries } from '../check-changeset-overwrite.mjs'; * * What this file pins, in the order the gate can fail: * - * 1. **The workflow can start on the pull request that needs it.** A path - * filter is normally a false-negative surface; here it cannot be, because a - * change that modifies a `.changeset/*.md` touches `.changeset/**` by - * definition. The filter must still carry the gate's OWN closure — its YAML - * and the script it runs (objectui#6321) — or the pull request that changes - * the gate is not the one that runs it. + * 1. **The workflow can start on the pull request that needs it.** Retired + * with objectstack#25712. These pins read objectui's `changeset-guard.yml` + * (the gate's run step, `fetch-depth: 0`, its own closure in both `paths:` + * filters, the `.changeset/**` entry), and that workflow runs nowhere under + * the monorepo: GitHub runs the root `.github/workflows/` alone. It is row 3 + * of the objectstack#25697 workflow roster (→ sub-issue 4), and a port + * brings these pins back against its root workflow, by path. * 2. **The verdicts**, against throwaway repositories rather than this one's * history, so they stay decidable when the history moves. Both directions: * a modification of a pre-existing changeset is REPORTED, and a change that @@ -45,7 +46,6 @@ import { declaredEntries } from '../check-changeset-overwrite.mjs'; */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const GATE = 'scripts/check-changeset-overwrite.mjs'; -const WORKFLOW = '.github/workflows/changeset-guard.yml'; /** * The peer release declaration every fixture repository starts with — the file @@ -67,23 +67,6 @@ const WORKFLOW = '.github/workflows/changeset-guard.yml'; */ const PEER_CHANGESET = '.changeset/fixture-olive-donkeys-smile.md'; -/** - * A workflow's YAML with whole-line comments removed. - * - * Required, not cosmetic: this workflow's header discusses `paths` and its own - * self-coverage at length, so a scan that counted the prose would report filter - * entries the file does not have. Same helper, same reason, as in - * `check-changeset-presence.test.ts` and `merge-queue-reporting.test.ts`. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -const workflowYaml = withoutComments(fs.readFileSync(path.join(repoRoot, WORKFLOW), 'utf8')); - // ── fixture repositories ───────────────────────────────────────────────────── const fixtures: string[] = []; @@ -177,40 +160,15 @@ function lastCommitRange(fixture: Fixture): string[] { } // ── 1. the workflow is reachable, and covers itself ────────────────────────── - -describe('changeset-guard.yml — the gate can start on the pull request that needs it', () => { - it('exists, and runs the gate script', () => { - expect(fs.existsSync(path.join(repoRoot, GATE)), `${GATE} must exist for the workflow to run it`).toBe(true); - expect(workflowYaml).toMatch(new RegExp(`run:\\s*node\\s+${GATE.replace(/[.]/g, '\\.')}`)); - }); - - it('gives the job the history its diff needs', () => { - // The gate compares this change against its MERGE BASE with the target - // branch. checkout's default is a depth-1 clone where `git merge-base` has - // nothing to find, and an unresolvable base is a hard failure in the script - // — so getting this wrong is a red build rather than a silent pass. Pinned - // so it stays that way. - expect(workflowYaml).toMatch(/fetch-depth:\s*0/); - }); - - it("carries the gate's own closure in the paths filter (objectui#6321)", () => { - // Without this, the pull request that edits the gate is not the pull request - // that runs it, and the first real execution lands on somebody else's - // unrelated `.changeset/**` change. `paths:` appears twice — `pull_request` - // and `push` — and both must carry it. - const occurrences = workflowYaml.split(GATE).length - 1; - expect(occurrences, `${GATE} must appear in BOTH paths: filters and the run: step`).toBeGreaterThanOrEqual(3); - expect(workflowYaml.split(WORKFLOW).length - 1, 'the workflow must list its own YAML').toBeGreaterThanOrEqual(2); - }); - - it('still triggers on .changeset/** — the filter this gate depends on', () => { - // A path filter is normally a false-negative surface. It is not here, and - // only because of this entry: a change that modifies or deletes a - // `.changeset/*.md` touches `.changeset/**` BY DEFINITION, so the filter - // cannot skip the case the gate exists for. - expect(workflowYaml).toMatch(/- '\.changeset\/\*\*'/); - }); -}); +// +// Retired with objectstack#25712: `exists, and runs the gate script`, `gives the +// job the history its diff needs`, `carries the gate's own closure in the paths +// filter (objectui#6321)` and `still triggers on .changeset/**`. Each read +// objectui's `changeset-guard.yml`, which runs nowhere under the monorepo: +// GitHub runs the root `.github/workflows/` alone, and no root workflow runs +// this gate. That workflow is row 3 of the objectstack#25697 workflow roster +// (→ sub-issue 4); a port brings these pins back against its root workflow, by +// path. The verdicts below never read a workflow and stand as they were. // ── 2. the verdicts ────────────────────────────────────────────────────────── diff --git a/ui/scripts/__tests__/check-changeset-presence.test.ts b/ui/scripts/__tests__/check-changeset-presence.test.ts index 2010053dc67..a91dd064a5f 100644 --- a/ui/scripts/__tests__/check-changeset-presence.test.ts +++ b/ui/scripts/__tests__/check-changeset-presence.test.ts @@ -34,6 +34,12 @@ import { * start is the shape objectui#3523 spent a P0 on, and this gate's own subject * matter makes it acute: `changeset-guard.yml` is invisible to a PR that * forgot its changeset precisely because it filters on `.changeset/**`. + * Retired with objectstack#25712: these pins read objectui's + * `changeset-presence.yml` and `changeset-guard.yml`, which run nowhere + * under the monorepo (GitHub runs the root `.github/workflows/` alone). The + * two are rows 4 and 3 of the objectstack#25697 workflow roster + * (→ sub-issue 4), and a port brings them back against its root workflow, + * by path. * 2. **The guarded surface is DERIVED**, not a hand-written glob. The issue * proposed one glob under `packages/`; `@object-ui/console` lives at * `apps/console` and would have been missed — the most-edited published @@ -63,25 +69,6 @@ import { */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const GATE = 'scripts/check-changeset-presence.mjs'; -const WORKFLOW = '.github/workflows/changeset-presence.yml'; -const workflowDir = path.join(repoRoot, '.github/workflows'); - -/** - * A workflow's YAML with whole-line comments removed. - * - * Required, not cosmetic: this workflow's header discusses `paths`, - * `paths-ignore` and `push` at length — a scan that counted the prose would - * report filters and triggers the file does not have. Same helper, same reason, - * as in `docs-links-workflow.test.ts` and `merge-queue-reporting.test.ts`. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -const workflowYaml = withoutComments(fs.readFileSync(path.join(repoRoot, WORKFLOW), 'utf8')); // ── fixture repositories ───────────────────────────────────────────────────── @@ -191,94 +178,20 @@ function lastCommitRange(fixture: Fixture): string[] { } // ── 1. the workflow is reachable ───────────────────────────────────────────── - -describe('changeset-presence.yml — the gate can start on the PR that needs it', () => { - it('exists, and runs the gate script', () => { - expect(fs.existsSync(path.join(repoRoot, WORKFLOW)), 'a check nothing runs is not a gate').toBe(true); - expect(fs.existsSync(path.join(repoRoot, GATE)), `${GATE} must exist for the workflow to run it`).toBe(true); - expect(workflowYaml).toMatch(new RegExp(`run:\\s*node\\s+${GATE.replace(/[.]/g, '\\.')}`)); - }); - - it('carries NO path filter of any kind', () => { - // The reason it is its own workflow AND unfiltered. A filter on the trigger - // skips the whole workflow (GitHub has no per-job path filter), so the - // context is never CREATED on a PR that does not match — and a required - // context that is never created leaves the PR pending rather than failing - // it, which in the queue means the ruleset's 60-minute timeout - // (objectui#3523, #3509). It would also be a second copy of the script's - // guarded surface, free to drift from the `.changeset/config.json` the - // script derives it from. - expect(workflowYaml).not.toMatch(/paths-ignore:/); - expect(workflowYaml).not.toMatch(/^\s+paths:/m); - }); - - it('reports on pull requests and on merge-queue builds', () => { - expect(workflowYaml).toMatch(/^\s*pull_request:/m); - expect( - workflowYaml, - 'A gate with no path filter reports on every PR and can therefore be REQUIRED. A required ' + - 'context that does not report on a queue build does not fail the queue, it stalls it until ' + - "the ruleset's 60-minute status-check timeout (objectui#3523).", - ).toMatch(/^\s*merge_group:/m); - }); - - it('does not run on push to main, where nothing can be declared any more', () => { - // Not tidiness: the change has already landed, so a red push paints `main` - // red at whoever committed next, and no declaration can still be written - // into that change. The PR and the queue build are the two moments it can. - expect(workflowYaml).not.toMatch(/^\s*push:/m); - }); - - it('checks out enough history for a merge base to exist', () => { - // `fetch-depth: 0`. The default depth-1 clone gives `git merge-base` nothing - // to find, and the script treats an unresolvable base as a hard failure — so - // omitting this is a red build, not a silent pass. Pinned so it stays that - // way rather than being "fixed" by making the script skip. - expect(workflowYaml).toMatch(/fetch-depth:\s*0/); - }); - - it('is the only workflow that runs this gate, and needs no install to do it', () => { - const runners = fs - .readdirSync(workflowDir) - .filter((file) => file.endsWith('.yml')) - .filter((file) => withoutComments(fs.readFileSync(path.join(workflowDir, file), 'utf8')).includes(GATE)); - expect(runners).toEqual(['changeset-presence.yml']); - expect( - workflowYaml, - 'This gate reads `.changeset/config.json` and `git diff` only. A `pnpm install` here would ' + - 'turn a few-second check into a full install on every pull request.', - ).not.toMatch(/pnpm install/); - }); - - it('leaves changeset-guard.yml alone — the two gates face opposite directions', () => { - // `changeset-guard.yml`'s `paths: ['.changeset/**']` is deliberate: on a PR - // that adds ONLY a changeset, every gate inside `ci.yml` and `lint.yml` - // skips, so nothing in either of them ever reads the changeset — and that - // guard exists to see exactly that PR. A PR that FORGOT its changeset does - // not touch `.changeset/**`, so widening those paths would have broken the - // case it was built for while still not catching this one. Two workflows, - // one direction each. - // - // Not the reason this comment used to give — "a PR adding ONLY a changeset - // starts no other workflow". That is the sentence PR #4371 rewrote in the - // docblock of `check-changeset-presence.mjs`, the script this file - // accompanies, and the pair disagreed across the two until objectui#4381. - // objectui#3523 step 2 deleted `paths-ignore` from `ci.yml`'s and - // `lint.yml`'s `pull_request` trigger; it survives only on `push`. Such a PR - // therefore DOES start both and DOES produce their contexts — measured: PR - // #3856 (one markdown file) 16 checks, PR #4339 (one line added to - // AGENTS.md) 17. What still skips is the expensive work: the `Decide whether - // this change needs a full run` step excludes markdown and `.changeset/**` - // and skips every step below itself, and its exclusion list is that `push` - // filter unchanged, held identical to it by - // `scripts/__tests__/merge-queue-reporting.test.ts`. The conclusion outlived - // its premise; objectui#3857 pinned the correction. - const guard = withoutComments(fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8')); - expect(guard).toMatch(/^\s+paths:/m); - expect(guard).toMatch(/\.changeset\//); - expect(guard).toMatch(/check-changeset-no-major\.mjs/); - }); -}); +// +// Retired with objectstack#25712: `exists, and runs the gate script`, `carries +// NO path filter of any kind`, `reports on pull requests and on merge-queue +// builds`, `does not run on push to main, where nothing can be declared any +// more`, `checks out enough history for a merge base to exist`, `is the only +// workflow that runs this gate, and needs no install to do it` and `leaves +// changeset-guard.yml alone — the two gates face opposite directions`. Each read +// objectui's `changeset-presence.yml` or `changeset-guard.yml` from +// `ui/.github/workflows/`, which runs nowhere under the monorepo: GitHub runs +// the root `.github/workflows/` alone, and no root workflow runs this gate. +// They are rows 4 and 3 of the objectstack#25697 workflow roster +// (→ sub-issue 4), where the root `pr-automation.yml` `Check Changeset` job is +// the counterpart; a port brings these pins back against its root workflow, by +// path. // ── 2. the guarded surface is derived from the release configuration ───────── diff --git a/ui/scripts/__tests__/check-control-bytes.test.ts b/ui/scripts/__tests__/check-control-bytes.test.ts index 8f6e9174a44..8d4f6dcfe9b 100644 --- a/ui/scripts/__tests__/check-control-bytes.test.ts +++ b/ui/scripts/__tests__/check-control-bytes.test.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'; // `tsconfig.scripts.json` (`allowJs`), so no `@ts-expect-error` here — // re-adding one is now itself an error (TS2578). See objectui#3494. import { classify, scan, locate, SCANNED_BYTES, KNOWN_OFFENDERS } from '../check-control-bytes.mjs'; +import { globMatches, jobBlock, repoPath, rootCi, triggerBlock, uiFilterGlobs, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci'; /** * objectstack#5425. Two raw U+0000 bytes in @@ -473,39 +474,40 @@ describe('objectstack#5450 — the four baselined files are clean', () => { }); }); +/** + * The home is the repository root's `ci.yml` now (objectstack#25712). GitHub + * runs only the root `.github/workflows/`, and objectstack#25902 ported + * objectui's `repo-gates.yml`, this gate's old home, into the root `lint-ui` + * job, which spells it through its alias. Both pins read that job and the root + * triggers, by path. + */ describe('wiring — the gate is actually reachable and actually runs', () => { const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); - const workflowPath = path.join(repoRoot, '.github/workflows/repo-gates.yml'); + const ci = rootCi(); it('is exposed as a root package script', () => { expect(pkg.scripts['check:control-bytes']).toBe('node scripts/check-control-bytes.mjs'); }); - it('has a workflow that gates pull requests', () => { - expect(fs.existsSync(workflowPath), 'a check nothing runs is not a gate').toBe(true); - expect(fs.readFileSync(workflowPath, 'utf8')).toMatch(/pull_request:/); - }); - - it('does NOT filter that workflow by path', () => { - // This is the whole reason it is its own workflow rather than a step in - // ci.yml or lint.yml: on a markdown-only change both of those skip every - // expensive step, so a gate inside either would never see it. Markdown is - // precisely the carrier objectstack#4890 was found in, so a path filter here - // would rebuild the hole this guard exists to close. `changeset-guard.yml` - // is in the repo for the same reason and says so in its own header. - // - // Not the reason this comment used to give — "both of those `paths-ignore` - // markdown, `content/**`, `docs/**` and `.changeset/**`" — which - // objectui#4381 corrected here and in the workflow's own header. Since - // objectui#3523 step 2 that filter is gone from their `pull_request` trigger - // and survives only on `push`, so a markdown-only PR does start both and - // does produce their contexts (measured — PR #3856, one markdown file, 16 - // checks). What still skips is the work inside them: the `Decide whether - // this change needs a full run` step excludes markdown and skips every step - // below itself, and the `push` lane is filtered at the trigger as before. - // The conclusion outlived its premise; objectui#3857 pinned the correction. - const workflow = fs.readFileSync(workflowPath, 'utf8'); - expect(workflow).not.toMatch(/paths-ignore:/); - expect(workflow).not.toMatch(/^\s+paths:/m); + it('the root Lint UI job runs it, and the root ci.yml gates pull requests', () => { + expect(withoutComments(jobBlock(ci, UI_JOBS.lint)), 'a check nothing runs is not a gate').toContain( + `run: ${uiRun('check:control-bytes')}`, + ); + expect(triggerBlock(ci)).toMatch(/^ {2}pull_request:/m); + }); + + it('is filtered by no path a markdown-only change could miss', () => { + // Markdown is precisely the carrier objectstack#4890 was found in, so a + // filter that skips a markdown-only change would rebuild the hole this guard + // exists to close. objectui's `repo-gates.yml` met that with no path filter + // at all. The root ci.yml filters no paths at the trigger either, and + // `lint-ui` runs on the `filter` job's `ui` globs, which must keep matching + // a markdown-only change anywhere under `ui/`. + expect(withoutComments(triggerBlock(ci))).not.toMatch(/^\s+paths(-ignore)?:/m); + const globs = uiFilterGlobs(ci); + const missed = ['README.md', 'content/docs/guide/ci-cd-pipeline.md', 'packages/core/README.md'] + .map(repoPath) + .filter((file) => !globs.some((glob) => globMatches(glob, file))); + expect(missed, 'a markdown-only change under ui/ would skip Lint UI').toEqual([]); }); }); diff --git a/ui/scripts/__tests__/check-doc-fence-languages.test.ts b/ui/scripts/__tests__/check-doc-fence-languages.test.ts index 21f6353fb9a..b4a8e046f64 100644 --- a/ui/scripts/__tests__/check-doc-fence-languages.test.ts +++ b/ui/scripts/__tests__/check-doc-fence-languages.test.ts @@ -34,10 +34,27 @@ import { } from '../check-doc-component-types.mjs'; import { selfTestCases, stripAnsi } from './helpers/child-verdict'; +import { + globMatches, + jobBlock, + repoPath, + rootCi, + rootCiPath, + rootWorkflow, + rootWorkflowFiles, + uiFilterGlobs, + uiRun, + UI_JOBS, +} from './helpers/root-ci'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GUARD = 'scripts/check-doc-fence-languages.mjs'; -const WORKFLOW = 'repo-gates.yml'; +const ALIAS = 'check:doc-fences'; +/** + * The guard's home, as failure messages name it: the root `lint-ui` job, where + * objectstack#25902 ported objectui's `repo-gates.yml` (objectstack#25712). + */ +const WORKFLOW = `ci.yml (${UI_JOBS.lint})`; /** * A numeral that qualifies a document-population noun, as `match: text`. @@ -439,10 +456,17 @@ describe('check-doc-fence-languages: non-vacuity, through the shipped module', ( }); }); +/** + * GitHub runs only the repository root's `.github/workflows/`, so the wiring is + * the root `ci.yml`'s: objectstack#25902 ported objectui's `repo-gates.yml` into + * its `lint-ui` job, which runs the guard through its alias (objectstack#25712). + * Every pin below reads that file by path. + */ describe('check-doc-fence-languages is wired, not merely present', () => { - const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows', WORKFLOW), 'utf8')); - const steps: Array> = workflow.jobs['repo-gates'].steps; - const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GUARD)); + const workflow = parseYaml(rootCi()); + const steps: Array> = workflow.jobs[UI_JOBS.lint].steps; + const runs = (s: Record) => typeof s.run === 'string' && (s.run as string).includes(uiRun(ALIAS)); + const gateSteps = steps.filter(runs); it('the guard script exists', () => { expect(fs.existsSync(path.join(ROOT, GUARD))).toBe(true); @@ -450,22 +474,24 @@ describe('check-doc-fence-languages is wired, not merely present', () => { it('package.json aliases it, and the alias points at the script that exists', () => { const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); - expect(pkg.scripts['check:doc-fences']).toContain(GUARD); + expect(pkg.scripts[ALIAS]).toBe(`node ${GUARD}`); }); - it('the workflow runs it — one step, both legs', () => { + it('the root Lint UI job runs it — one step, both legs', () => { expect(gateSteps).toHaveLength(1); const run = gateSteps[0].run as string; - expect(run).toContain(`node ${GUARD} --self-test`); - expect(run.split('\n').some((l) => l.trim() === `node ${GUARD}`)).toBe(true); + expect(run).toContain(`${uiRun(ALIAS)} --self-test`); + expect(run.split('\n').some((l) => l.trim() === uiRun(ALIAS))).toBe(true); }); - it('one gate, one home — no other workflow runs the same script', () => { - const dir = path.join(ROOT, '.github/workflows'); - const others = fs - .readdirSync(dir) - .filter((f) => f.endsWith('.yml') && f !== WORKFLOW) - .filter((f) => fs.readFileSync(path.join(dir, f), 'utf8').includes(GUARD)); + it('one gate, one home — no other job or workflow runs the same script', () => { + const jobs = Object.entries(workflow.jobs as Record> }>) + .filter(([, job]) => (job.steps ?? []).some(runs)) + .map(([key]) => key); + expect(jobs).toEqual([UI_JOBS.lint]); + const others = rootWorkflowFiles() + .filter((f) => f !== path.basename(rootCiPath)) + .filter((f) => rootWorkflow(f).includes(ALIAS) || rootWorkflow(f).includes(GUARD)); expect(others).toEqual([]); }); @@ -473,8 +499,15 @@ describe('check-doc-fence-languages is wired, not merely present', () => { // `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`. const on = workflow.on ?? workflow[true as unknown as string]; expect(Object.keys(on)).toContain('pull_request'); - expect(on.pull_request).not.toHaveProperty('paths'); - expect(on.pull_request).not.toHaveProperty('paths-ignore'); + expect(on.pull_request ?? {}).not.toHaveProperty('paths'); + expect(on.pull_request ?? {}).not.toHaveProperty('paths-ignore'); + // The root job runs on the `filter` job's `ui` globs, so those globs are the + // path filter that matters, and a docs-only change under `ui/` must match. + const globs = uiFilterGlobs(rootCi()); + const missed = ['content/docs/guide/ci-cd-pipeline.md', 'packages/core/README.md'] + .map(repoPath) + .filter((file) => !globs.some((glob) => globMatches(glob, file))); + expect(missed, 'a docs-only change under ui/ would skip Lint UI').toEqual([]); }); it('subscribes to the merge queue, so it cannot stall one if it becomes required', () => { @@ -483,10 +516,11 @@ describe('check-doc-fence-languages is wired, not merely present', () => { }); it('needs no install — no pnpm runs before this gate in its job', () => { - // Since objectui#12214 the home is `repo-gates.yml`, ONE job that installs - // after its install-free gates: the property is now an ORDER -- nothing - // before this gate's step runs `pnpm install` -- and no longer an absence. - const gateAt = steps.findIndex((s) => typeof s.run === 'string' && (s.run as string).includes(GUARD)); + // Since objectui#12214 the home is ONE job that installs after its + // install-free gates (`repo-gates.yml` then, the root `lint-ui` now): the + // property is an ORDER -- nothing before this gate's step runs + // `pnpm install` -- and no longer an absence. + const gateAt = steps.findIndex(runs); expect(gateAt).toBeGreaterThan(-1); const before = steps .slice(0, gateAt) @@ -561,7 +595,7 @@ describe('check-doc-fence-languages is wired, not merely present', () => { * number is written here that a document being added would falsify. */ it('its header states the population and never counts it — no count can rot here', () => { - const header = headerComments(fs.readFileSync(path.join(ROOT, '.github/workflows', WORKFLOW), 'utf8')); + const header = headerComments(jobBlock(rootCi(), UI_JOBS.lint)); // objectui#7901 — the floor first, or the assertion below is vacuous. The // count half cannot distinguish a header that carries no counts from a diff --git a/ui/scripts/__tests__/check-doc-snippet-emitted-census.test.ts b/ui/scripts/__tests__/check-doc-snippet-emitted-census.test.ts index 17e360c2be6..2bcef954458 100644 --- a/ui/scripts/__tests__/check-doc-snippet-emitted-census.test.ts +++ b/ui/scripts/__tests__/check-doc-snippet-emitted-census.test.ts @@ -25,6 +25,7 @@ import { listEmittedSources, scanEmittedTemplates, } from '../check-doc-snippet-types.mjs'; +import { jobBlock, rootCi, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci'; /** * objectui#7864 — the emitted-code census. @@ -404,20 +405,31 @@ describe('on this repository — the walk is non-vacuous and reaches both known }); }); +/** + * The home is the repository root's `ci.yml` now (objectstack#25712). GitHub + * runs only the root `.github/workflows/`, and objectstack#25902 ported + * objectui's `repo-gates.yml` into the root `lint-ui` job, which runs the + * census and the gate through the `check:doc-snippets` alias, after the build + * that serves them. The pins read that job by path, and the alias case ties the + * spelling to this script. + */ describe('wiring — a census nothing runs is not a census', () => { - const workflow = fs.readFileSync( - path.join(repoRoot, '.github/workflows/repo-gates.yml'), - 'utf8', - ); + const workflow = withoutComments(jobBlock(rootCi(), UI_JOBS.lint)); + const ALIAS = 'check:doc-snippets'; + + it('the alias the job runs is this script', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); + expect(pkg.scripts[ALIAS]).toBe(`node ${SCRIPT}`); + }); - it('runs the census in the workflow that already builds this gate\'s closure', () => { - expect(workflow).toContain('--emit-census'); + it('runs the census in the job that already builds this gate\'s closure', () => { + expect(workflow).toContain(`${uiRun(ALIAS)} --emit-census`); }); it('runs it AFTER the build and BEFORE the blocking gate, so a red gate cannot hide the number', () => { - const build = workflow.indexOf('turbo run build'); - const census = workflow.indexOf(`node ${SCRIPT} --emit-census`); - const gate = workflow.search(new RegExp(`run: node ${SCRIPT.replace(/[.\\/]/g, '\\$&')}\\s*$`, 'm')); + const build = workflow.indexOf('id: ui-gate-build'); + const census = workflow.indexOf(`${uiRun(ALIAS)} --emit-census`); + const gate = workflow.search(new RegExp(`run: ${uiRun(ALIAS).replace(/[.\\/:-]/g, '\\$&')}\\s*$`, 'm')); expect(build).toBeGreaterThan(-1); expect(census).toBeGreaterThan(build); expect( diff --git a/ui/scripts/__tests__/check-docs-route-eager-closure.test.ts b/ui/scripts/__tests__/check-docs-route-eager-closure.test.ts index 8fa497b86dc..af0f8275599 100644 --- a/ui/scripts/__tests__/check-docs-route-eager-closure.test.ts +++ b/ui/scripts/__tests__/check-docs-route-eager-closure.test.ts @@ -16,6 +16,7 @@ import { MEASURED_PAYLOAD, REGISTRAR, } from '../check-docs-route-eager-closure.mjs'; +import { globMatches, jobBlock, repoPath, rootCi, triggerBlock, uiFilterGlobs, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); @@ -368,9 +369,16 @@ describe('the real tree -- the properties that keep this gate from being decorat // teach readers to fix the test rather than the import. }); +/** + * The home is the repository root's `ci.yml` now (objectstack#25712). GitHub + * runs only the root `.github/workflows/`, and objectstack#25902 ported + * objectui's `repo-gates.yml`, this gate's old home, into the root `lint-ui` + * job, which spells it through its alias. Both pins read that job and the root + * triggers, by path. + */ describe('wiring -- the gate is actually reachable and actually runs', () => { const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); - const workflowPath = path.join(repoRoot, '.github/workflows/repo-gates.yml'); + const ci = rootCi(); it('is exposed as a root package script', () => { expect(pkg.scripts['check:docs-route-closure']).toBe( @@ -378,21 +386,32 @@ describe('wiring -- the gate is actually reachable and actually runs', () => { ); }); - it('has a workflow that gates pull requests and reports on a queue build', () => { - expect(fs.existsSync(workflowPath), 'a check nothing runs is not a gate').toBe(true); - const workflow = fs.readFileSync(workflowPath, 'utf8'); - expect(workflow).toMatch(/pull_request:/); - expect(workflow).toMatch(/merge_group:/); - expect(workflow).toContain('pnpm check:docs-route-closure'); + it('the root Lint UI job runs it, on pull requests and on a queue build', () => { + expect(withoutComments(jobBlock(ci, UI_JOBS.lint)), 'a check nothing runs is not a gate').toContain( + `run: ${uiRun('check:docs-route-closure')}`, + ); + const triggers = triggerBlock(ci); + expect(triggers).toMatch(/^ {2}pull_request:/m); + expect(triggers).toMatch(/^ {2}merge_group:/m); }); - it('does NOT filter that workflow by path', () => { + it('is filtered by no path its inputs could miss', () => { // Its inputs are the whole route graph -- `packages/**`, `apps/site/**`, - // `content/docs/**` -- plus its own closure in `scripts/`. A filter listing - // all of that is indistinguishable from no filter, and a filter that misses - // one of them cannot be exercised by the PR that changes it (objectui#6321). - const workflow = fs.readFileSync(workflowPath, 'utf8'); - expect(workflow).not.toMatch(/paths-ignore:/); - expect(workflow).not.toMatch(/^\s+paths:/m); + // `content/docs/**` -- plus its own closure in `scripts/`. A filter that + // misses one of them cannot be exercised by the PR that changes it + // (objectui#6321). The root ci.yml filters no paths at the trigger, and + // `lint-ui` runs on the `filter` job's `ui` globs, which must keep matching + // every one of those inputs under `ui/`. + expect(withoutComments(triggerBlock(ci))).not.toMatch(/^\s+paths(-ignore)?:/m); + const globs = uiFilterGlobs(ci); + const missed = [ + 'packages/core/src/index.ts', + 'apps/site/source.config.ts', + 'content/docs/guide/ci-cd-pipeline.md', + 'scripts/check-docs-route-eager-closure.mjs', + ] + .map(repoPath) + .filter((file) => !globs.some((glob) => globMatches(glob, file))); + expect(missed, 'a change to one of these inputs would skip Lint UI').toEqual([]); }); }); diff --git a/ui/scripts/__tests__/check-eager-closure-budget.test.ts b/ui/scripts/__tests__/check-eager-closure-budget.test.ts index e57eccb7434..b98294d84cf 100644 --- a/ui/scripts/__tests__/check-eager-closure-budget.test.ts +++ b/ui/scripts/__tests__/check-eager-closure-budget.test.ts @@ -43,7 +43,6 @@ import { import { OPTIONAL_CONTEXTS, REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const workflowPath = path.join(repoRoot, '.github/workflows/performance-budget.yml'); const viteConfigPath = path.join(repoRoot, 'apps/console/vite.config.ts'); const checkerPath = path.join(repoRoot, 'scripts/check-eager-closure-budget.mjs'); @@ -2057,7 +2056,7 @@ describe('main', () => { * not readable from inside a checkout. `evaluateCeilingFreshness`'s own docblock * carries what the tree can re-derive instead, cited to * `scripts/dependabot-merge-gate.mjs` and to the workflow's `on:` block; the pin - * at the bottom of this file keeps all three files off the claim. + * at the bottom of this file keeps the gate's prose files off the claim. * * Not hypothetical: run 32804357171 started 6m50s after `0409b766d` lowered the * aggregate ceiling from 4,086,000 to 3,345,000 and published @@ -2458,36 +2457,20 @@ describe('ceiling freshness (objectui#6245)', () => { /** * The checker can only be correct if the workflow keeps feeding it and the * build keeps emitting the report. Neither half is reachable from a unit test. + * + * The workflow half (`runs the closure checker in the budget step`, `keeps the + * entry-chunk budget alongside it`, `measures the closure even when the entry + * chunk is over budget` and `maps the gauge-failure exit code to \`error\`, not + * to a size verdict`) retired with objectstack#25712. It read objectui's + * `performance-budget.yml`, which runs nowhere under the monorepo: GitHub runs + * the root `.github/workflows/` alone, and no root workflow runs this checker. + * That workflow is row 22 of the objectstack#25697 workflow roster, "kept, + * follow-up port" into `Build & E2E UI`; the port brings these pins back + * against the root job, by path. The build half stands. */ -describe('performance-budget.yml + vite.config.ts contract', () => { - const workflow = fs.readFileSync(workflowPath, 'utf8'); +describe('vite.config.ts contract', () => { const viteConfig = fs.readFileSync(viteConfigPath, 'utf8'); - it('runs the closure checker in the budget step', () => { - expect(workflow).toContain('node scripts/check-eager-closure-budget.mjs'); - }); - - it('keeps the entry-chunk budget alongside it', () => { - // Replacing a blind gauge is not licence to drop the check already there. - expect(workflow).toContain('MAX_ENTRY_GZIP_KB=350'); - }); - - it('measures the closure even when the entry chunk is over budget', () => { - // The entry check used to `exit 1` on breach. If it still did, a fat entry - // chunk would hide the number that actually governs a page load. - const step = workflow.slice(workflow.indexOf('MAX_ENTRY_GZIP_KB=350')); - const entryVerdict = step.indexOf('ENTRY BUDGET EXCEEDED'); - const closureRun = step.indexOf('node scripts/check-eager-closure-budget.mjs'); - expect(entryVerdict).toBeGreaterThan(-1); - expect(closureRun).toBeGreaterThan(entryVerdict); - }); - - it('maps the gauge-failure exit code to `error`, not to a size verdict', () => { - expect(workflow).toContain('if [ "$CLOSURE_CODE" -eq 2 ]; then'); - const branch = workflow.slice(workflow.indexOf('if [ "$CLOSURE_CODE" -eq 2 ]; then')); - expect(branch.slice(0, 400)).toContain('budget_status=error'); - }); - it('emits the report from a plugin that is not skipped on CI', () => { // `compression` and `visualizer` sit behind `...(!isCI ? [` — the budget // runs ON CI, so the report emitter must not join them there. @@ -2764,7 +2747,11 @@ describe('the prose attached to the baselines (objectui#7046)', () => { * `.github/workflows/performance-budget.yml` joined this population in * objectui#7850 — the same rule, the same reader, no second matcher. Its prose * is `#` comments rather than `/**` blocks, so it reaches the reader through - * `yamlProse` below, which translates the marker and nothing else. + * `yamlProse` below, which translates the marker and nothing else. It left the + * population with objectstack#25712, with its must-hit control: that workflow + * runs nowhere under the monorepo (roster row 22, "kept, follow-up port"). The + * translation and its self-test stay for the root job the port lands in, which + * rejoins the population then. * * Its one obstacle was settled by REWORDING, not by an exemption. The workflow * counted chunks once as the OUTCOME of a hazard — a `"sideEffects": false` @@ -2788,9 +2775,9 @@ describe('the prose attached to the baselines (objectui#7046)', () => { */ describe("chunk counts in this gate's prose (objectui#7528)", () => { /** - * The population. Two script files whose prose is JS comments, and the - * workflow whose prose is `#` comments (objectui#7850) — the same rule and the - * same reader, with only the comment marker translated for the third. + * The population: two script files whose prose is JS comments. The workflow + * whose prose is `#` comments (objectui#7850) left it with objectstack#25712, + * and its root successor joins with the marker translated by `yamlProse`. */ const PROSE_FILES: Record string }> = { 'check-eager-closure-budget.mjs': { path: checkerPath, toProse: (source) => source }, @@ -2798,13 +2785,12 @@ describe("chunk counts in this gate's prose (objectui#7528)", () => { path: fileURLToPath(import.meta.url), toProse: (source) => source, }, - 'performance-budget.yml': { path: workflowPath, toProse: yamlProse }, }; /** A chunk population written as a numeral: "N chunks", or "N of M chunks". */ const CHUNK_COUNT = /\b\d[\d,_]*\s+(?:of\s+\d[\d,_]*\s+)?chunks?\b/gi; - /** A commit named the way all three files name one: a backticked short hash. */ + /** A commit named the way these files name one: a backticked short hash. */ const NAMES_A_COMMIT = /`[0-9a-f]{7,40}`/; /** @@ -2984,18 +2970,9 @@ describe("chunk counts in this gate's prose (objectui#7528)", () => { expect(anchored).toContain('58 of 507 chunks'); }); - /** - * The same must-hit control for the workflow entry (objectui#7850). Its prose - * reaches the reader through a translation, and a translation that stopped - * working would show up as a population that passes because it is empty. This - * fails instead. - */ - it('sees the anchored measurement the workflow must not be refused for', () => { - const anchored = chunkCounts(yamlProse(fs.readFileSync(workflowPath, 'utf8'))) - .filter((count) => count.anchored) - .map((count) => count.text); - expect(anchored).toContain('58 of 507 chunks'); - }); + // `sees the anchored measurement the workflow must not be refused for`, the + // same must-hit control for the workflow entry (objectui#7850), retired with + // that entry (objectstack#25712, roster row 22). it.each(Object.entries(PROSE_FILES))( '%s writes no chunk count that no commit anchors', @@ -3139,15 +3116,19 @@ describe('no prose here classifies `Bundle Analysis` against branch protection ( */ const CLASSIFIES = new RegExp(String.raw`\b${REQ}\s+contexts?\b|\bis\s+not\s+${REQ}\b`, 'gi'); - /** The three files objectui#9155 is about, by the name a failure prints. */ + /** + * The files objectui#9155 is about, by the name a failure prints. The third, + * objectui's `performance-budget.yml`, left with objectstack#25712: it runs + * nowhere under the monorepo (roster row 22, "kept, follow-up port"), and the + * root job the port lands in joins here. + */ const POPULATION: Record = { 'check-eager-closure-budget.mjs': checkerPath, 'check-eager-closure-budget.test.ts': fileURLToPath(import.meta.url), - 'performance-budget.yml': workflowPath, }; /** - * The must-HIT control, with a known direction. Without it the three zeroes + * The must-HIT control, with a known direction. Without it the zeroes * below are equally consistent with a matcher that stopped matching anything * — which is how a pin on absence goes quietly green forever. */ @@ -3165,7 +3146,7 @@ describe('no prose here classifies `Bundle Analysis` against branch protection ( ).toBeNull(); }); - it('reads three files that are really there, not three empty strings', () => { + it('reads files that are really there, not empty strings', () => { for (const [name, file] of Object.entries(POPULATION)) { const source = fs.readFileSync(file, 'utf8'); expect(source.length, `${name} read as empty`).toBeGreaterThan(1000); @@ -3204,22 +3185,9 @@ describe('no prose here classifies `Bundle Analysis` against branch protection ( expect(OPTIONAL_CONTEXTS['Bundle Analysis']).toContain('performance-budget.yml filters on paths'); }); - /** - * The other cited source, re-derived rather than restated: the prose may say - * this job has no `merge_group` leg only for as long as that is true of the - * `on:` block. Guarded by locating a block that is found and substantial - * first, so a renamed section cannot pass by scanning nothing. - */ - it('leaves the workflow without the `merge_group` leg the prose cites the absence of', () => { - const source = fs.readFileSync(workflowPath, 'utf8'); - const start = source.indexOf('\non:\n'); - const end = source.indexOf('\npermissions:\n'); - expect(start, 'the `on:` block is not where this pin looks for it').toBeGreaterThanOrEqual(0); - expect(end).toBeGreaterThan(start); - const onBlock = source.slice(start, end); - expect(onBlock.length).toBeGreaterThan(200); - expect(onBlock).toContain(' push:'); - expect(onBlock).toContain(' pull_request:'); - expect(onBlock).not.toMatch(/^\s{2}merge_group:/m); - }); + // `leaves the workflow without the \`merge_group\` leg the prose cites the + // absence of` retired with objectstack#25712. It re-derived the other cited + // source from objectui's `performance-budget.yml` `on:` block, and that + // workflow runs nowhere under the monorepo (roster row 22, "kept, follow-up + // port"); the port brings it back against the root job, by path. }); diff --git a/ui/scripts/__tests__/check-governed-queue-guard.test.ts b/ui/scripts/__tests__/check-governed-queue-guard.test.ts index a97f137c9c0..3a4c65f68e6 100644 --- a/ui/scripts/__tests__/check-governed-queue-guard.test.ts +++ b/ui/scripts/__tests__/check-governed-queue-guard.test.ts @@ -3,12 +3,10 @@ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import { REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; import { CHECK_CONTEXT_NAME, - CHECK_JOB_ID, CHECK_WORKFLOW, GOVERNED_SURFACES, governedPathsIn, @@ -36,15 +34,24 @@ const WORKFLOW = `.github/workflows/${CHECK_WORKFLOW}`; * set. That is repository settings, which no test here can read and no agent may * change. What IS asserted is that this repository has written the answer down * where its own tooling reads it, and that every piece of the chain exists. + * + * ⚠️ The workflow half of the chain retired with objectstack#25712: `the gate + * script and its workflow both exist` (its workflow leg), `the job publishes + * exactly the check name the script declares`, `subscribes BOTH legs`, + * `re-fires on ready_for_review`, `carries no path filter on either leg`, + * `checks out full history`, `grants pull-requests: read and no write scope + * anywhere` and `runs the self-test BEFORE the live judgment`. Each read + * objectui's `governed-surface-guard.yml`, which runs nowhere under the + * monorepo: GitHub runs the root `.github/workflows/` alone. The root + * `governed-surface-guard.yml` covers the queue guard with its own register, + * which governs `ui/.github/workflows/**` (roster row 11, → sub-issue 4, which + * decides the rest of the `ui/**` surface). `CHECK_JOB_ID` and + * `CHECK_WORKFLOW` still name objectui's job; a port brings these pins back + * against the root workflow that runs this gate, by path. */ describe('check-governed-queue-guard is wired, not merely present', () => { - const workflowText = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); - const workflow = parseYaml(workflowText); - const job = workflow.jobs[CHECK_JOB_ID]; - - it('the gate script and its workflow both exist', () => { + it('the gate script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); - expect(fs.existsSync(path.join(ROOT, WORKFLOW))).toBe(true); }); it('package.json aliases it, and the alias points at the script that exists', () => { @@ -55,14 +62,6 @@ describe('check-governed-queue-guard is wired, not merely present', () => { } }); - it('the job publishes exactly the check name the script declares', () => { - // The #6865-shape defect one repo over: renaming a job detaches a required - // context with nothing to say so. The name lives in two files because a name - // in one file is a name nothing can pin; this is the assertion that keeps - // the two equal. - expect(job.name).toBe(CHECK_CONTEXT_NAME); - }); - it('this repository has written the context down as blocking', () => { // `REQUIRED_CONTEXTS` is objectui's own answer to "which checks are // blocking" (`scripts/dependabot-merge-gate.mjs`), and it is what @@ -71,58 +70,6 @@ describe('check-governed-queue-guard is wired, not merely present', () => { expect(REQUIRED_CONTEXTS).toContain(CHECK_CONTEXT_NAME); }); - it('subscribes BOTH legs — the queue build refuses, the pull request warns', () => { - // Both, not either. Without `merge_group` there is no refusal at all; - // without `pull_request` a seat gets no warning before the queue, which is - // the moment #6183 could still have been stopped. - expect(workflow.on).toHaveProperty('merge_group'); - expect(workflow.on).toHaveProperty('pull_request'); - }); - - it('re-fires on ready_for_review — the first move of the incident', () => { - // Naming `types:` REPLACES GitHub's default set rather than extending it, - // so the three defaults have to be restated alongside the addition. This - // asserts the addition AND that restating did not drop one. - expect(workflow.on.pull_request.types).toEqual([ - 'opened', - 'synchronize', - 'reopened', - 'ready_for_review', - ]); - }); - - it('carries no path filter on either leg — a skipped job counts as SUCCESS', () => { - // The one direction this gate may not be wrong in. A path filter that - // mis-scopes hands branch protection a green verdict from a job that never - // ran, on the one check whose entire purpose is to refuse. - for (const leg of ['pull_request', 'merge_group'] as const) { - const on = workflow.on[leg] ?? {}; - expect(on).not.toHaveProperty('paths'); - expect(on).not.toHaveProperty('paths-ignore'); - } - }); - - it('checks out full history — a truncated diff answers with silence', () => { - const checkout = job.steps.find((s: Record) => String(s.uses ?? '').startsWith('actions/checkout')); - expect(checkout, 'the job must check out the repository').toBeDefined(); - expect(checkout.with['fetch-depth']).toBe(0); - }); - - it('grants pull-requests: read and no write scope anywhere', () => { - expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'read' }); - }); - - it('runs the self-test BEFORE the live judgment, as its precondition', () => { - const runs: string[] = job.steps - .filter((s: Record) => typeof s.run === 'string') - .map((s: Record) => s.run); - const selfTestAt = runs.findIndex((r) => r.includes(`${GATE} --self-test`)); - const liveAt = runs.findIndex((r) => r.includes(GATE) && !r.includes('--self-test')); - expect(selfTestAt, 'the workflow must run the self-test').toBeGreaterThan(-1); - expect(liveAt, 'the workflow must run the live judgment').toBeGreaterThan(-1); - expect(selfTestAt).toBeLessThan(liveAt); - }); - it('its self-test passes — the half that makes a green run mean something', () => { const out = execFileSync('node', [GATE, '--self-test'], { cwd: ROOT, encoding: 'utf8' }); // objectui#7897 — the COUNT, not the shape. `\d+ cases pass` is satisfied diff --git a/ui/scripts/__tests__/check-links-workflow.test.ts b/ui/scripts/__tests__/check-links-workflow.test.ts index 27d7777c991..52be428bace 100644 --- a/ui/scripts/__tests__/check-links-workflow.test.ts +++ b/ui/scripts/__tests__/check-links-workflow.test.ts @@ -45,44 +45,14 @@ import { fileURLToPath } from 'node:url'; * right), and `push` / `pull_request` must NOT — an external link checker walks * the network, and gating PRs on third-party uptime makes other people's work * fail for reasons they cannot fix. + * + * ⚠️ Half 1, the workflow surface of half 3 and the trigger set retired with + * objectstack#25712; the note above the first describe names the cases. Half 2 + * and this header's own count pin stand. */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const workflowPath = path.join(repoRoot, '.github/workflows/check-links.yml'); const lycheeConfigPath = path.join(repoRoot, 'lychee.toml'); -const siteConfigPath = path.join(repoRoot, 'apps/site/source.config.ts'); - -/** - * The workflow YAML with whole-line comments removed. - * - * Required, not cosmetic: the header explains at length why `push` and - * `pull_request` stay disabled and names both of them, and the commented-out - * trigger block is itself part of the record. A scan that counted either would - * report triggers this workflow does not have. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -/** - * The workflow's comment prose, with the leading `#` markers stripped. - * - * Stripped, not kept: a sentence that wraps across two comment lines has a `#` - * sitting in the middle of it, and a scan that reads the raw lines cannot see a - * numeral and the noun it qualifies as adjacent when the line break falls - * between them. Removing the marker is what makes the count pin below read the - * header the way a person does. - */ -function commentProse(yaml: string): string { - return yaml - .split('\n') - .filter((line) => /^\s*#/.test(line)) - .map((line) => line.replace(/^\s*#\s?/, '')) - .join('\n'); -} /** * This file's own leading block comment — the second surface the count pin reads. @@ -136,103 +106,21 @@ function documentCounts(text: string): string[] { return [...text.matchAll(new RegExp(POPULATION_COUNT.source, 'gi'))].map((m) => m[0].replace(/\s+/g, ' ').trim()); } -/** The quoted input patterns from the workflow's `args:` block. */ -function scannedPatterns(yaml: string): string[] { - return [...withoutComments(yaml).matchAll(/^\s*'([^']+)'\s*$/gm)].map((m) => m[1]); -} - -/** Every `.md` / `.mdx` file below `dir`, as repo-relative paths. */ -function markdownFilesUnder(dir: string): string[] { - const absolute = path.join(repoRoot, dir); - if (!fs.existsSync(absolute)) return []; - const found: string[] = []; - const walk = (current: string) => { - for (const entry of fs.readdirSync(current, { withFileTypes: true })) { - const full = path.join(current, entry.name); - if (entry.isDirectory()) walk(full); - else if (/\.mdx?$/.test(entry.name)) found.push(path.relative(repoRoot, full)); - } - }; - walk(absolute); - return found; -} - /** - * Files matched by one input pattern. - * - * Only the two shapes the workflow uses are supported — `/ ** / *.` - * and a literal path. Anything else is reported rather than silently counted as - * zero, because "matches nothing" is the very defect this file exists to catch. + * Retired with objectstack#25712: `scans every markdown file the site is built + * from`, `still sweeps the repo-root internal docs and README`, `states its + * inputs in a shape this test can actually evaluate`, `runs on a schedule as + * well as on demand`, `does not gate pull requests`, and the workflow surface + * of the count pin below. Each read objectui's `check-links.yml` (its `args:` + * block against the docs tree `apps/site/source.config.ts` declares, its + * triggers, its header prose), which runs nowhere under the monorepo: GitHub + * runs the root `.github/workflows/` alone. The root `check-links.yml` sweeps + * the root's own trees, not `ui/`. objectui's is row 6 of the objectstack#25697 + * workflow roster (→ sub-issue 4, which widens the root sweep to `ui/` or keeps + * objectui's); either way the port brings these pins back against the root + * workflow, by path. */ -function filesMatching(pattern: string): string[] { - const glob = /^(.+)\/\*\*\/\*\.([a-z]+)$/.exec(pattern); - if (glob) { - const [, base, extension] = glob; - return markdownFilesUnder(base).filter((file) => file.endsWith(`.${extension}`)); - } - if (/[*?[\]]/.test(pattern)) { - throw new Error(`unsupported glob shape in check-links.yml args: ${pattern}`); - } - return fs.existsSync(path.join(repoRoot, pattern)) ? [pattern] : []; -} - -/** The docs directory the site itself declares, as a repo-relative path. */ -function declaredDocsDir(): string { - const source = fs.readFileSync(siteConfigPath, 'utf8'); - const match = /\bdir:\s*'([^']+)'/.exec(source); - expect(match, `${siteConfigPath} no longer declares a docs \`dir\``).not.toBeNull(); - const resolved = path.resolve(path.dirname(siteConfigPath), match![1]); - return path.relative(repoRoot, resolved); -} - -const workflowYaml = () => fs.readFileSync(workflowPath, 'utf8'); - -describe('check-links.yml — the external link sweep looks at the published docs', () => { - it('scans every markdown file the site is built from', () => { - // The #3449 assertion. Derived from the site config on purpose: see the - // header. `content/docs` is never spelled out here. - const docsDir = declaredDocsDir(); - const published = markdownFilesUnder(docsDir); - expect(published.length, `no markdown found under the declared docs dir '${docsDir}'`).toBeGreaterThan(100); - - const scanned = new Set(scannedPatterns(workflowYaml()).flatMap(filesMatching)); - const missed = published.filter((file) => !scanned.has(file)); - expect(missed.slice(0, 5), `${missed.length} published doc(s) are not covered by check-links.yml args`).toEqual([]); - }); - - it('still sweeps the repo-root internal docs and README', () => { - // Ruling on #3449: `docs/**` (ADRs, audits) and README keep their coverage. - // Widening to the published tree was never meant to narrow anything. - const scanned = new Set(scannedPatterns(workflowYaml()).flatMap(filesMatching)); - const internal = markdownFilesUnder('docs'); - expect(internal.length, "the repo-root internal docs tree is gone — re-read this workflow's scope").toBeGreaterThan( - 0, - ); - - const missed = internal.filter((file) => !scanned.has(file)); - expect(missed.slice(0, 5), `${missed.length} internal doc(s) are not covered by check-links.yml args`).toEqual([]); - expect(scanned.has('README.md')).toBe(true); - }); - - it('states its inputs in a shape this test can actually evaluate', () => { - // Coverage above is asserted per FILE, not per pattern, and that is - // deliberate: `docs/**/*.mdx` currently matches nothing (the internal tree - // is all `.md`) and should stay anyway, so that an internal `.mdx` landing - // tomorrow is swept rather than becoming a fresh blind spot. What must not - // happen is an args block this test cannot read — it would assert coverage - // over an empty pattern list and pass while checking nothing. - const patterns = scannedPatterns(workflowYaml()); - expect(patterns.length, 'no input patterns found in the args block').toBeGreaterThan(2); - for (const pattern of patterns) expect(() => filesMatching(pattern)).not.toThrow(); - }); - - it('runs on a schedule as well as on demand', () => { - const yaml = withoutComments(workflowYaml()); - expect(yaml, 'the periodic sweep #3213 ruling B intended').toMatch(/^\s*schedule:/m); - expect(yaml).toMatch(/^\s*-\s*cron:\s*'[^']+'/m); - expect(yaml).toMatch(/^\s*workflow_dispatch:/m); - }); - +describe('check-links — the header count pin', () => { /** * objectui#7825 — the third copy of objectui#7448's pin, in the home the card * named. This header stated two document counts as literals: the repo-root @@ -259,10 +147,7 @@ describe('check-links.yml — the external link sweep looks at the published doc * before their emptiness means anything. */ it('its header states the population and never counts it — no count can rot here', () => { - const surfaces = [ - ['check-links.yml', commentProse(workflowYaml())], - ["this test file's own header", ownHeaderComment()], - ] as const; + const surfaces = [["this test file's own header", ownHeaderComment()]] as const; for (const [label, prose] of surfaces) { expect( @@ -322,15 +207,6 @@ describe('check-links.yml — the external link sweep looks at the published doc expect(documentCounts(line), `the pin must NOT fire on: ${line}`).toEqual([]); } }); - - it('does not gate pull requests', () => { - // #3213 ruling B. Lychee goes over the network; a third-party 502 must not - // be able to redden somebody's unrelated PR. The commented-out block in the - // workflow stays commented — `withoutComments` is what makes this real. - const yaml = withoutComments(workflowYaml()); - expect(yaml, 'external link checking must not become a PR gate — see #3213').not.toMatch(/^\s*pull_request:/m); - expect(yaml).not.toMatch(/^\s*push:/m); - }); }); describe('lychee.toml — site-absolute routes are skipped, not resolved', () => { diff --git a/ui/scripts/__tests__/check-merge-queue-head.test.ts b/ui/scripts/__tests__/check-merge-queue-head.test.ts index 6cb386e6159..2efecbb95b0 100644 --- a/ui/scripts/__tests__/check-merge-queue-head.test.ts +++ b/ui/scripts/__tests__/check-merge-queue-head.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import { CONFIRMATION_DELAY_MS, @@ -24,7 +23,6 @@ import { selectHeadEntry, selfTest, } from '../check-merge-queue-head.mjs'; -import { producedCheckNames, readWorkflows, triggerBlock } from './workflow-checks'; /** * objectui#7010 — a merge-queue head entry for which GitHub never dispatches @@ -58,8 +56,6 @@ import { producedCheckNames, readWorkflows, triggerBlock } from './workflow-chec */ const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-merge-queue-head.mjs'; -const WORKFLOW_FILE = 'merge-queue-head-patrol.yml'; -const WORKFLOW = `.github/workflows/${WORKFLOW_FILE}`; const gateSource = fs.readFileSync(path.join(ROOT, GATE), 'utf8'); const NOW = Date.parse('2026-09-05T23:40:00Z'); @@ -322,14 +318,25 @@ describe('the report describes calls that were actually made', () => { }); }); +/** + * The workflow half of this describe retired with objectstack#25712: `the + * script and its workflow both exist` (its workflow leg), `the workflow + * actually runs the script`, `runs on a clock`, `subscribes no pull_request + * leg, so it owes no entry in the merge gate`, `grants exactly the three scopes + * it uses`, `serialises itself and never cancels a run mid-reading`, `captures + * the exit code with no pipe in between`, `lands the reading BEFORE it raises + * the alarm`, `never opens an issue`, the no-install leg of `needs no install` + * and the `QUEUE_BASE_BRANCH` leg of `reads the branch it is installed to + * watch`. Each read objectui's `merge-queue-head-patrol.yml`, which runs + * nowhere under the monorepo: GitHub runs the root `.github/workflows/` alone, + * and no root workflow runs this patrol. It is row 20 of the objectstack#25697 + * workflow roster (→ sub-issue 4), where the root `merge-queue-triage.yml` is + * the counterpart; a port brings these pins back against its root workflow, by + * path. What stays reads the script and `package.json`. + */ describe('the patrol is wired, not merely present', () => { - const workflowText = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); - const workflow = parseYaml(workflowText); - const job = workflow.jobs.patrol; - - it('the script and its workflow both exist', () => { + it('the script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); - expect(fs.existsSync(path.join(ROOT, WORKFLOW))).toBe(true); }); it('package.json aliases the offline check and the live read', () => { @@ -338,92 +345,14 @@ describe('the patrol is wired, not merely present', () => { expect(pkg.scripts['merge-queue-head']).toBe(`node ${GATE}`); }); - it('the workflow actually runs the script', () => { - const runs = job.steps.map((s: { run?: string }) => s.run ?? '').join('\n'); - expect(runs).toContain(`node ${GATE}`); - }); - - it('runs on a clock, because the failure is an absence nothing can trigger on', () => { - // A wedged entry emits no `merge_group`; `main` never moves, so no `push` - // fires; the pull-request events all happened before it was enqueued. There - // is no event to hang this on — that is the whole reason for the cron. - const crons = (workflow.on.schedule ?? []).map((s: { cron: string }) => s.cron); - expect(crons).toHaveLength(1); - expect(crons[0]).toMatch(/^[\d,]+ \* \* \* \*$/); - const minutes = crons[0].split(' ')[0].split(',').map(Number); - expect(minutes.length, 'four firings an hour — see the cadence arithmetic in the workflow header').toBe(4); - expect(minutes, 'offset off :00, where every other repository schedules').not.toContain(0); - expect(workflow.on).toHaveProperty('workflow_dispatch'); - }); - - it('subscribes no pull_request leg, so it owes no entry in the merge gate', () => { - // Every job of a `pull_request` workflow produces a check run, and - // `dependabot-merge-gate.mjs` requires every produced name to be classified - // in REQUIRED_CONTEXTS / OPTIONAL_CONTEXTS / NOT_A_GATE — a partition - // `dependabot-merge-gate.test.ts` asserts exactly. This patrol has no leg, - // so it produces nothing and owes nothing. If a leg is ever added, this goes - // red and names the other half of the work. - expect(workflow.on).not.toHaveProperty('pull_request'); - expect(workflow.on).not.toHaveProperty('merge_group'); - const self = readWorkflows().find((w) => w.file === WORKFLOW_FILE); - expect(self, 'the patrol workflow must be readable by the shared parser').toBeTruthy(); - expect(triggerBlock(self!).some((line) => /^ {2}pull_request:/.test(line))).toBe(false); - for (const [name, file] of producedCheckNames()) { - expect(file, `${name} must not come from the patrol — it can never gate a pull request`).not.toBe(WORKFLOW_FILE); - } - }); - - it('grants exactly the three scopes it uses, and no other write', () => { - expect(workflow.permissions).toEqual({ contents: 'read', actions: 'read', issues: 'write' }); - }); - - it('serialises itself and never cancels a run mid-reading', () => { - expect(workflow.concurrency.group).toBe('merge-queue-head-patrol'); - expect(workflow.concurrency['cancel-in-progress']).toBe(false); - }); - - it('captures the exit code with no pipe in between', () => { - // `node … | tail` reports the PIPE's status, and `tail` essentially never - // fails — so a wedge (3), a clean reading (0) and an unreadable one (2) - // would all read as 0. That silent collapse is the class this patrol exists - // to catch, and it would be sitting inside the patrol. - const step = job.steps.find((s: { id?: string }) => s.id === 'patrol'); - expect(step, 'the reading step must keep its `id: patrol`').toBeTruthy(); - expect(step.run).toMatch(/code=\$\?/); - expect(step.run).not.toMatch(/node scripts\/check-merge-queue-head\.mjs[^\n]*\|/); - }); - - it('lands the reading BEFORE it raises the alarm', () => { - const ids = job.steps.map((s: { name?: string }) => s.name ?? ''); - const summaryAt = ids.findIndex((n: string) => /run summary/i.test(n)); - const failAt = ids.findIndex((n: string) => /^Fail the run/i.test(n)); - expect(summaryAt).toBeGreaterThan(-1); - expect(failAt).toBeGreaterThan(summaryAt); - expect(job.steps[summaryAt].if).toBe('always()'); - }); - - it('never opens an issue — one anchor, refreshed, or nothing at all', () => { - // A patrol that filed a card per firing would file four an hour for an hour, - // on one incident. objectui#7010's own history is the precedent: a live - // recurrence was posted there as a comment "per the one-anchor rule rather - // than as a new card". - expect(workflowText).not.toMatch(/issues\.create/); - expect(workflowText).not.toMatch(/issues\.createComment/); - expect(workflowText).toMatch(/issues\.update/); - }); - - it('needs no install, so it stays inside the pre-install import population', () => { - const runs = job.steps.map((s: { run?: string; uses?: string }) => `${s.uses ?? ''} ${s.run ?? ''}`).join('\n'); - expect(runs).not.toMatch(/pnpm install/); - // The gate imports one repo-relative module and nothing else; anything from - // `node_modules` would make the install-free step throw at import time. + it('imports one repo-relative module and nothing else, so it can run before an install', () => { + // Anything from `node_modules` would make an install-free step throw at + // import time. const imports = [...gateSource.matchAll(/^import .* from '([^']+)';$/gm)].map((m) => m[1]); expect(imports).toEqual(['./invoked-as.mjs']); }); - it('reads the branch it is installed to watch, with a default that is real', () => { - const step = job.steps.find((s: { id?: string }) => s.id === 'patrol'); - expect(step.env.QUEUE_BASE_BRANCH).toContain('default_branch'); + it('defaults to a branch that is real', () => { expect(DEFAULT_BASE_BRANCH).toBe('main'); }); }); diff --git a/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts b/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts index b4b01d91853..ceb24e59f62 100644 --- a/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts +++ b/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts @@ -38,7 +38,6 @@ import { judge } from '../cross-file-line-citation-census.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const gatePath = path.join(repoRoot, 'scripts/check-new-cross-file-line-citations.mjs'); -const workflowPath = path.join(repoRoot, '.github/workflows/line-citation-gate.yml'); const docPath = path.join(repoRoot, 'content/docs/guide/ci-cd-pipeline.md'); /** Judges a hit against the real tree, the way the gate's own run does. */ @@ -420,27 +419,18 @@ describe('the instrument does not count itself', () => { }); }); +/** + * The workflow half of this describe retired with objectstack#25712: `runs the + * gate in its own workflow`, `checks out deep enough to have a merge base`, + * `filters no pull request at the trigger` and `the workflow job name matches + * the classified context name`. Each read objectui's `line-citation-gate.yml`, + * which runs nowhere under the monorepo: GitHub runs the root + * `.github/workflows/` alone, and no root workflow runs this gate. It is row 15 + * of the objectstack#25697 workflow roster (→ sub-issue 4); a port brings these + * pins back against its root workflow, by path. The page and the classifier + * pins below read no workflow and stand. + */ describe('wiring — a gate nothing runs is not a gate', () => { - const workflow = fs.readFileSync(workflowPath, 'utf8'); - - it('runs the gate in its own workflow', () => { - expect(workflow).toContain('node scripts/check-new-cross-file-line-citations.mjs'); - }); - - it('checks out deep enough to have a merge base', () => { - expect(workflow).toContain('fetch-depth: 0'); - }); - - it('filters no pull request at the trigger', () => { - // The population is comments, message strings, documentation and changesets. - // A path filter on any of those is the hole the gate exists to close, and - // `lint.yml`'s in-job short-circuit excludes exactly them, which is why this - // is its own workflow. - const trigger = workflow.slice(workflow.indexOf('\non:'), workflow.indexOf('\nconcurrency:')); - expect(trigger).toContain('pull_request'); - expect(trigger).not.toMatch(/^\s*paths(-ignore)?:/m); - }); - it('is written up on the page objectui#3653 pins, and declared report-only there', () => { const doc = fs.readFileSync(docPath, 'utf8'); expect(doc).toContain('node scripts/check-new-cross-file-line-citations.mjs'); @@ -473,10 +463,6 @@ describe('wiring — a gate nothing runs is not a gate', () => { const gate = fs.readFileSync(path.join(repoRoot, 'scripts/dependabot-merge-gate.mjs'), 'utf8'); expect(gate).toContain("'Line Citation Gate':"); }); - - it('the workflow job name matches the classified context name', () => { - expect(workflow).toMatch(/^\s*name: Line Citation Gate$/m); - }); }); /** diff --git a/ui/scripts/__tests__/check-required-check-set.test.ts b/ui/scripts/__tests__/check-required-check-set.test.ts index a52b21b8c33..28bb010f081 100644 --- a/ui/scripts/__tests__/check-required-check-set.test.ts +++ b/ui/scripts/__tests__/check-required-check-set.test.ts @@ -3,7 +3,6 @@ import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import { EXIT_BREACHED, @@ -19,7 +18,6 @@ import { REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-required-check-set.mjs'; -const WORKFLOW = '.github/workflows/required-check-set-patrol.yml'; const FIXTURES = 'scripts/__tests__/fixtures/required-check-set'; /** * The verbatim live answer of 2026-10-12, taken the day objectui#12240 was @@ -73,6 +71,8 @@ const ABLATED_FIXTURE = `${FIXTURES}/type-check-removed.json`; * 4. the WIRING — the workflow exists, is scheduled, and runs this script. * A patrol nobody runs is indistinguishable from a patrol that passes, * and this is the half of the patrol's own liveness that a diff can see. + * Its workflow half retired with objectstack#25712 (the wiring describe + * names the cases); the `package.json` half stands. * * ⚠️ What this file deliberately does NOT assert: that the live ruleset is * intact. That reading needs the network and belongs to the scheduled run. A @@ -315,56 +315,16 @@ describe('check-required-check-set — the wiring', () => { expect(pkg.scripts['check:required-check-set']).toContain(GATE); }); - const workflow = (): Record => parseYaml(fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8')); - - /** - * YAML 1.1 reads a bare `on:` key as the BOOLEAN true, so `doc.on` is - * `undefined` and an assertion reaching for it would pass over an empty - * object — a wiring pin that asserts nothing. Both spellings are read, and - * the triggers assertion below fails loudly if neither resolves. - */ - const triggers = (): Record => { - const doc = workflow(); - const on = doc[String(true)] ?? doc.on; - if (!on || typeof on !== 'object') { - throw new Error(`could not read the \`on:\` block of ${WORKFLOW} — keys were ${Object.keys(doc).join(', ')}`); - } - return on as Record; - }; - - it('the patrol workflow exists and is SCHEDULED — the half of its liveness a diff can see', () => { - const on = triggers(); - const schedule = on.schedule as Array<{ cron?: string }> | undefined; - expect(Array.isArray(schedule)).toBe(true); - expect(schedule).not.toHaveLength(0); - expect(schedule?.[0]?.cron).toMatch(/^\S+ \S+ \S+ \S+ \S+$/); - expect(on).toHaveProperty('workflow_dispatch'); - }); - - it('the patrol has NO pull_request leg — it must never gate a pull request', () => { - const on = triggers(); - expect(on).not.toHaveProperty('pull_request'); - expect(on).not.toHaveProperty('pull_request_target'); - expect(on).not.toHaveProperty('merge_group'); - }); - - it('the workflow actually invokes this script', () => { - expect(fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8')).toContain(GATE); - }); - - it('the patrol asks for no write permission at all — it READS the ruleset', () => { - const perms = workflow().permissions; - expect(perms).toEqual({ contents: 'read' }); - }); - - it('a run that could not read must fail the job, so a broken transport is never a silent green', () => { - const yml = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); - expect(yml).toContain(String(EXIT_CANNOT_RUN)); - expect(yml).toContain(String(EXIT_BREACHED)); - // Captured with no pipe in between — `node … | tail` reports the pipe's - // status and every verdict would collapse to 0. - expect(yml).toMatch(/exit_code=\$code/); - }); + // Retired with objectstack#25712: `the patrol workflow exists and is + // SCHEDULED`, `the patrol has NO pull_request leg`, `the workflow actually + // invokes this script`, `the patrol asks for no write permission at all` and + // `a run that could not read must fail the job`. Each read objectui's + // `required-check-set-patrol.yml`, which runs nowhere under the monorepo: + // GitHub runs the root `.github/workflows/` alone, and the root + // `required-set-patrol.yml` patrols the monorepo's own ruleset. objectui's + // patrol is row 25 of the objectstack#25697 workflow roster (→ sub-issue 4); a + // port brings these pins back against the root workflow that runs this + // script, by path. }); /** @@ -381,7 +341,11 @@ describe('check-required-check-set — the wiring', () => { * block asserts the pointer rather than the answer: nothing it reads could go * stale the next time a maintainer edits the ruleset. * - * ⚠️ The population is named in words: the three files objectui#9502 repaired. + * ⚠️ The population is named in words: the files objectui#9502 repaired that + * still say anything. The third, objectui's `dependabot-auto-merge.yml`, left it + * with objectstack#25712: that workflow runs nowhere under the monorepo (row 9 + * of the objectstack#25697 workflow roster, → sub-issue 4), so its prose + * guides no one. * `AGENTS.md` carried a fourth instance and is deliberately NOT in that set. * objectui#9502 left it standing — it is governed surface, and its * parenthetical also covered who may bypass the ruleset, which the endpoint @@ -395,12 +359,8 @@ describe('check-required-check-set — the wiring', () => { * decoration. */ describe('check-required-check-set — the prose points here instead of answering (#9502)', () => { - /** The three files objectui#9502 repaired. */ - const REPAIRED = [ - 'content/docs/guide/ci-cd-pipeline.md', - '.github/workflows/dependabot-auto-merge.yml', - 'scripts/dependabot-merge-gate.mjs', - ]; + /** The files objectui#9502 repaired, less the workflow the note above retires. */ + const REPAIRED = ['content/docs/guide/ci-cd-pipeline.md', 'scripts/dependabot-merge-gate.mjs']; /** * The carrier objectui#9502 left standing and objectui#9520 split: the one diff --git a/ui/scripts/__tests__/check-test-path-roots.test.ts b/ui/scripts/__tests__/check-test-path-roots.test.ts index 42dfd552c11..b40f9bea252 100644 --- a/ui/scripts/__tests__/check-test-path-roots.test.ts +++ b/ui/scripts/__tests__/check-test-path-roots.test.ts @@ -38,6 +38,8 @@ import { sitesIn, } from '../check-test-path-roots.mjs'; +import { jobBlock, rootCi, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci'; + /** * The repo root, derived from THIS FILE's own location — never from * `process.cwd()`. That is the rule this file's subject enforces, and a test @@ -236,13 +238,12 @@ describe('check-test-path-roots — the gate itself', () => { const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) as { scripts: Record }; expect(pkg.scripts['check:test-path-roots']).toBe('node scripts/check-test-path-roots.mjs'); - // `Lint` is in `REQUIRED_CONTEXTS` and subscribes `merge_group`; a step + // objectstack#25712: the job is the root `ci.yml`'s `lint-ui` (`Lint UI`, a + // required context of a workflow that subscribes `merge_group`), where + // objectstack#25697 ported objectui's `lint.yml`; read by path. A step // inside it gates without adding a context to the queue's required set. - const lint = readFileSync(join(REPO_ROOT, '.github/workflows/lint.yml'), 'utf8') - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); - expect(lint).toContain('node scripts/check-test-path-roots.mjs'); + const lint = withoutComments(jobBlock(rootCi(), UI_JOBS.lint)); + expect(lint).toContain(uiRun('check:test-path-roots')); }); it('it goes through the one entry-guard predicate rather than hand-typing one', () => { diff --git a/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts b/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts index 3e4f8064103..27c56439a5a 100644 --- a/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts +++ b/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts @@ -18,6 +18,9 @@ import { summarise, } from '../check-vi-mock-override-shape.mjs'; import { COVERED_SPECIFIERS } from '../check-vi-mock-inherit.mjs'; +import { scan as preInstallScan } from '../check-pre-install-import-graph.mjs'; + +import { globMatches, jobBlock, repoPath, rootCi, uiFilterGlobs, uiRun, UI_JOBS } from './helpers/root-ci'; /** * objectui#8903 — the test for `scripts/check-vi-mock-override-shape.mjs`. @@ -454,54 +457,53 @@ describe('wiring', () => { const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { scripts: Record; }; - const lint = fs.readFileSync(path.join(repoRoot, '.github/workflows/lint.yml'), 'utf8'); + // objectstack#25712: the job is the root `ci.yml`'s `lint-ui`, where + // objectstack#25697 ported objectui's `lint.yml` (roster row 16), read by path. + const lint = jobBlock(rootCi(), UI_JOBS.lint); it('is reachable as a pnpm script', () => { expect(pkg.scripts['check:vi-mock-override-shape']).toBe(`node scripts/${GATE}`); }); - it('runs in lint.yml, AFTER the install it needs', () => { - const gateAt = lint.indexOf(`node scripts/${GATE}`); + it('runs in the root Lint UI job, AFTER the install it needs', () => { + const gateAt = lint.indexOf(uiRun('check:vi-mock-override-shape')); const installAt = lint.indexOf('pnpm install --frozen-lockfile'); expect(gateAt, 'the gate must be run by a workflow').toBeGreaterThan(-1); expect(installAt).toBeGreaterThan(-1); expect(gateAt, 'the gate imports `typescript`; before the install there is no node_modules').toBeGreaterThan(installAt); }); - it('is NOT run by the pre-install vi-mock workflow', () => { + it('is NOT among the steps any workflow runs before an install', () => { // `check-pre-install-import-graph.mjs` would fail the moment it were: this - // gate reaches the package `typescript`, and that workflow deliberately has - // no install step. Measured on this tree by injecting the same import into - // `check-vi-mock-inherit.mjs`: that gate went from exit 0 to exit 1. - const sibling = fs.readFileSync(path.join(repoRoot, '.github/workflows/repo-gates.yml'), 'utf8'); - expect(sibling).not.toContain(GATE); + // gate reaches the package `typescript`. Measured on this tree by injecting + // the same import into `check-vi-mock-inherit.mjs`: that gate went from + // exit 0 to exit 1. This used to read objectui's install-free + // `repo-gates.yml`; since objectstack#25712 the pre-install population is + // derived from the root workflows, so this asks that derivation. + const { scripts } = preInstallScan(repoRoot) as { scripts: string[] }; + expect(scripts.length, 'the derivation read no pre-install step at all').toBeGreaterThan(0); + expect(scripts).not.toContain(`scripts/${GATE}`); }); - it('the lint.yml skip switch cannot hide a change this gate judges', () => { - // The switch skips the expensive steps when only ignored paths changed. - // Both halves of what this gate compares live in TypeScript files, so no - // ignored pattern may match one. If a pattern that does is ever added, the - // gate silently stops seeing the pull requests most likely to trip it. - const patterns = [...lint.matchAll(/:\(exclude,glob\)([^'"\s]+)/g)].map((m) => m[1]); - expect(patterns.length, 'the ignore list moved — re-read this switch').toBeGreaterThan(0); - - const toRe = (glob: string) => - new RegExp( - `^${glob - .split('**') - .map((part) => part.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')) - .join('.*')}$`, - ); + it('the filter that starts the root Lint UI job cannot hide a change this gate judges', () => { + // objectui's `lint.yml` skipped its expensive steps when only ignored paths + // changed. The root `lint-ui` runs when the `filter` job's `ui` globs + // match, so that list is the switch now (objectstack#25712). Both halves of + // what this gate compares live in TypeScript files, so every one of them + // must start the job, or the gate silently stops seeing the pull requests + // most likely to trip it. + const globs = uiFilterGlobs(rootCi()); const tsPaths = [ 'packages/app-shell/src/views/RecordDetailView.headerRefresh.test.tsx', 'packages/collaboration/src/PresenceProvider.tsx', 'scripts/check-vi-mock-override-shape.mjs', 'packages/collaboration/src/index.ts', ]; - for (const glob of patterns) { - for (const p of tsPaths) { - expect(toRe(glob).test(p), `ignore pattern ${glob} matches ${p} — this gate would go blind`).toBe(false); - } + for (const p of tsPaths) { + expect( + globs.some((glob) => globMatches(glob, repoPath(p))), + `no \`ui\` filter glob matches ${repoPath(p)} — this gate would go blind`, + ).toBe(true); } }); }); diff --git a/ui/scripts/__tests__/entry-guard-wiring.test.ts b/ui/scripts/__tests__/entry-guard-wiring.test.ts index 289138c87e9..d44aedec702 100644 --- a/ui/scripts/__tests__/entry-guard-wiring.test.ts +++ b/ui/scripts/__tests__/entry-guard-wiring.test.ts @@ -6,9 +6,11 @@ import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; import { selfTestCases, stripAnsi } from './helpers/child-verdict'; +import { rootCi, uiRun, UI_JOBS } from './helpers/root-ci'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-entry-guard.mjs'; +const ALIAS = 'check:entry-guard'; /** * objectui#6092: `scripts/invoked-as.mjs` arrived from objectstack (#5984) @@ -32,11 +34,12 @@ const GATE = 'scripts/check-entry-guard.mjs'; * 1. the script exists and its `package.json` alias points at it — the alias * is what a developer runs, and an alias naming a moved file fails loudly * only if something runs it; - * 2. `lint.yml` really runs it, on pull requests, in a step that executes — - * the wiring claim itself; - * 3. the workflow's spelling and the alias's spelling name the SAME script, - * so the pre-install `node` invocation and `pnpm check:entry-guard` cannot - * drift into checking different things; + * 2. the root `Lint UI` job (`lint-ui` in the repository root's `ci.yml`, + * where objectstack#25697 ported `lint.yml`) really runs it, on pull + * requests, in a step that executes — the wiring claim itself; + * 3. that step runs the alias, and the alias names the script, so the + * pre-install CI step and `pnpm check:entry-guard` cannot drift into + * checking different things; * 4. it runs BEFORE `pnpm install`, which is not decoration: placed after, * an install failure would take the gate with it and the tree would go * unjudged with the job red for an unrelated reason; @@ -50,9 +53,13 @@ const GATE = 'scripts/check-entry-guard.mjs'; * records at length for this same workflow. */ describe('check-entry-guard is wired, not merely present', () => { - const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows/lint.yml'), 'utf8')); - const steps: Array> = workflow.jobs.lint.steps; - const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GATE)); + // objectstack#25712: the job that runs it is the root `ci.yml`'s `lint-ui`, + // where objectstack#25697 ported objectui's `lint.yml` (roster row 16), read + // by path. It spells the gate through its alias, so `ALIAS` is what a step + // runs and the alias case below is what ties that to the file. + const workflow = parseYaml(rootCi()); + const steps: Array> = workflow.jobs[UI_JOBS.lint].steps; + const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(uiRun(ALIAS))); it('the gate script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); @@ -60,16 +67,16 @@ describe('check-entry-guard is wired, not merely present', () => { it('package.json aliases it, and the alias points at the script that exists', () => { const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); - const alias = pkg.scripts['check:entry-guard']; + const alias = pkg.scripts[ALIAS]; expect(alias).toBeTruthy(); expect(alias).toContain(GATE); }); - it('lint.yml runs it — exactly one step, both legs', () => { + it('the root Lint UI job runs it — exactly one step, both legs', () => { expect(gateSteps).toHaveLength(1); const run = gateSteps[0].run as string; - expect(run).toContain(`node ${GATE} --self-test`); - expect(run.split('\n').some((l) => l.trim() === `node ${GATE}`)).toBe(true); + expect(run).toContain(`${uiRun(ALIAS)} --self-test`); + expect(run.split('\n').some((l) => l.trim() === uiRun(ALIAS))).toBe(true); }); it('that step is not disabled — it runs whenever the job runs its other steps', () => { @@ -84,7 +91,7 @@ describe('check-entry-guard is wired, not merely present', () => { expect(others).toContain(condition); }); - it('lint.yml still gates pull requests, or the step above is inert', () => { + it('the root ci.yml still gates pull requests, or the step above is inert', () => { // `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`. const on = workflow.on ?? workflow[true as unknown as string]; expect(Object.keys(on)).toContain('pull_request'); diff --git a/ui/scripts/__tests__/lint-workflow.test.ts b/ui/scripts/__tests__/lint-workflow.test.ts index dfa6272affc..dce1f34a1d4 100644 --- a/ui/scripts/__tests__/lint-workflow.test.ts +++ b/ui/scripts/__tests__/lint-workflow.test.ts @@ -3,6 +3,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { globMatches, jobBlock, repoPath, rootCi, triggerBlock, uiFilterGlobs, UI_JOBS, withoutComments } from './helpers/root-ci'; + /** * objectui#3261: the header comment in `.github/workflows/lint.yml` said * `eslint.config.js` sets *three* `object-ui/*` rules to `error` and named the @@ -78,17 +80,29 @@ import { fileURLToPath } from 'node:url'; * `eslint.config.js`, where each rule's ADR/issue sits in the comment beside it. * A reference cannot drift out of step with its target; a copy of its contents * is the thing that had already gone stale three times. + * + * ## The workflow is the root's now (objectstack#25712) + * + * GitHub runs only the repository root's `.github/workflows/`, and + * objectstack#25697 ported `lint.yml`'s `Lint` job to the root `ci.yml` as + * `lint-ui` (roster row 16). objectui's own `lint.yml` runs nowhere, so the + * workflow surface is that job: its text for the rule-name check, its comments + * for the count check. The trigger claims are the root's too. Its `on:` block + * filters no paths at all, so the ignore list the second claim read is gone; a + * pull request reaches `lint-ui` through the `filter` job's `ui` globs, and + * those are what may not skip a TypeScript source. */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const read = (relative: string): string => fs.readFileSync(path.join(repoRoot, relative), 'utf8'); /** Repo-relative, so the same string labels the surface in failure messages. */ -const WORKFLOW = '.github/workflows/lint.yml'; +const WORKFLOW = `ci.yml (${UI_JOBS.lint})`; const COVERAGE_SCRIPT = 'scripts/check-lint-coverage.mjs'; const DOC = 'content/docs/guide/ci-cd-pipeline.md'; -const workflow = read(WORKFLOW); +const ci = rootCi(); +const workflow = jobBlock(ci, UI_JOBS.lint); const coverageScript = read(COVERAGE_SCRIPT); const doc = read(DOC); @@ -120,16 +134,14 @@ const objectUiErrorRules = eslintConfig .map(([name]) => name); /** - * The `# …` prose block between `name:` and `on:` — the workflow comment this - * issue is about. Scoped so the assertions below read the explanation, not the - * YAML. + * The job's `# …` comment lines, the explanation its steps carry. Scoped so the + * count assertion reads the prose, not the YAML. */ -function workflowHeaderComment(): string { - const start = workflow.indexOf('\n#'); - const end = workflow.indexOf('\non:'); - expect(start, `${WORKFLOW} must still carry a header comment above \`on:\``).toBeGreaterThan(-1); - expect(end, `${WORKFLOW} must still have a top-level \`on:\` block`).toBeGreaterThan(start); - return workflow.slice(start, end); +function workflowComments(): string { + return workflow + .split('\n') + .filter((line) => /^\s*#/.test(line)) + .join('\n'); } /** @@ -191,7 +203,7 @@ const SURFACES: ProseSurface[] = [ { label: WORKFLOW, text: workflow, - prose: () => unwrap(workflowHeaderComment(), /^[ \t]*#[ \t]?/gm), + prose: () => unwrap(workflowComments(), /^[ \t]*#[ \t]?/gm), }, { label: COVERAGE_SCRIPT, @@ -211,8 +223,10 @@ const SURFACES: ProseSurface[] = [ * word `pull_request` also appears in `concurrency`/`github.ref` expressions. */ function onBlock(): string { - const start = workflow.indexOf('\non:'); - const rest = workflow.slice(start + 1); + const triggers = withoutComments(triggerBlock(ci)); + const start = triggers.indexOf('\non:'); + expect(start, 'the root ci.yml must still have a top-level `on:` block').toBeGreaterThan(-1); + const rest = triggers.slice(start + 1); const next = rest.search(/\n[a-z_]+:/); return next === -1 ? rest : rest.slice(0, next); } @@ -296,54 +310,52 @@ describe('the premise both comments rest on', () => { }); }); -describe('lint.yml — the trigger claims its header comment still makes', () => { +describe('the root Lint UI job — the trigger claims the ratchets rest on', () => { it('still runs on pull requests, so those rules are not inert again (#2923)', () => { const on = onBlock(); expect( on, - `lint.yml no longer triggers on \`pull_request\`. That is exactly the state ` + - `#2923 fixed: every \`object-ui/*\` rule \`eslint.config.js\` sets to \`error\` ` + - `goes silently inert, because the only thing that runs them on a PR is this ` + - `workflow. The header comment describes that as history — keep it history.`, + `the root ci.yml no longer triggers on \`pull_request\`. That is exactly the ` + + `state #2923 fixed: every \`object-ui/*\` rule \`eslint.config.js\` sets to ` + + `\`error\` goes silently inert, because the only thing that runs them on a PR ` + + `is the ${UI_JOBS.lint} job.`, ).toMatch(/^\s{2}pull_request:/m); + + const condition = workflow.match(/^ {4}if: (.*)$/m)?.[1] ?? ''; + expect( + condition, + `${WORKFLOW}'s \`if:\` reads the event name, so some event no longer lints: ${condition}`, + ).not.toMatch(/event_name/); }); it('does not skip the TypeScript sources those rules lint', () => { - // A narrow tripwire, not a glob engine (the repo has no glob matcher at the - // root, and vendoring one for this would cost more than it protects). The - // realistic way to un-gate the ratchets without touching the triggers above - // is adding a TypeScript pattern to the ignore list; today's entries are all - // markdown, `content/**`, `docs/**` and `.changeset/**`. - // - // That list now lives in TWO places and both have to be read, which is the - // objectui#3523 change: `paths-ignore` stayed on the `push` trigger, but for - // pull requests it moved INTO the job, as `:(exclude,glob)…` pathspecs in - // the `Decide whether this change needs a full run` step. Reading only the - // `on:` block would have gone on passing while a `**/*.ts` exclusion was - // added to the job — the gate reporting green having linted nothing. - // `merge-queue-reporting.test.ts` pins the two lists to each other and pins - // why the trigger may not filter pull requests at all; this one stays on its - // own question, which is what may be in the list. - const ignored = [ - ...[...onBlock().matchAll(/^\s*-\s*'([^']+)'/gm)].map((m) => m[1]), - ...[...workflow.matchAll(/':\(exclude,glob\)([^']+)'/g)].map((m) => m[1]), - ]; - - expect( - ignored.length, - 'lint.yml must still declare an ignore list — on the `push` trigger, in the job, or both', - ).toBeGreaterThan(0); + // The realistic way to un-gate the ratchets without touching the triggers + // above is a filter that stops matching TypeScript. objectui's `lint.yml` + // kept an ignore list in two places (`paths-ignore` on `push`, and + // `:(exclude,glob)` pathspecs in the job for pull requests, objectui#3523). + // The root ci.yml keeps neither: its `on:` block filters no paths, and the + // `filter` job's `ui` globs decide whether `lint-ui` runs. So both are read + // here: no paths filter comes back on the trigger, no exclusion appears in + // the job, and a ui `.ts` or `.tsx` change still matches a `ui` glob. + expect(onBlock(), 'the root ci.yml\'s `on:` block filters paths again').not.toMatch( + /^\s+paths(-ignore)?:/m, + ); + const excluded = [...withoutComments(workflow).matchAll(/':\(exclude,glob\)([^']+)'/g)].map((m) => m[1]); + expect(excluded, `${WORKFLOW} excludes paths from its run`).toEqual([]); - const swallowsSource = ignored.filter((pattern) => /\.tsx?$/.test(pattern)); + const globs = uiFilterGlobs(ci); + const missed = ['packages/core/src/index.ts', 'packages/components/src/index.ts', 'apps/console/src/App.tsx'] + .map(repoPath) + .filter((file) => !globs.some((glob) => globMatches(glob, file))); expect( - swallowsSource, - `lint.yml skips TypeScript changes:\n` + - swallowsSource.map((p) => ` - ${p}`).join('\n') + + missed, + `the root \`filter\` job's \`ui\` globs no longer match these TypeScript sources:\n` + + missed.map((p) => ` - ${p}`).join('\n') + `\n\nEvery \`object-ui/*\` rule \`eslint.config.js\` sets to \`error\` only gates ` + - `a PR because this workflow lints the .ts/.tsx it touches. Excluding them is the ` + - `pre-#2923 inert state with extra steps — and since objectui#3523 it is the quieter ` + - `version of it, because the \`Lint\` check still reports, green.`, + `a PR because ${UI_JOBS.lint} lints the .ts/.tsx it touches. A filter that skips ` + + `them is the pre-#2923 inert state with extra steps, and the quieter version of ` + + `it, because \`Lint UI\` still reports, skipped, which counts as success.`, ).toEqual([]); }); }); diff --git a/ui/scripts/__tests__/markdown-test-inputs.test.ts b/ui/scripts/__tests__/markdown-test-inputs.test.ts index f351366b623..5b7974d3926 100644 --- a/ui/scripts/__tests__/markdown-test-inputs.test.ts +++ b/ui/scripts/__tests__/markdown-test-inputs.test.ts @@ -43,14 +43,31 @@ * moves the inert leg to what the exclusion list still buys: a NON-markdown * change under `apps/site/**`, which is the shape of the one commit in the * measured 513-merge window that still skips and should. + * + * ## ⚠️ objectstack#25712: the step is gone, and its legs with it + * + * The decision step lived in objectui's `ci.yml`, which runs nowhere under the + * monorepo: GitHub runs the repository root's `.github/workflows/` alone. The + * root `ci.yml` ported objectui's `Test` job as `test-ui` (row 7 of the + * objectstack#25697 workflow roster) without the step, so every leg that ran it + * retired: the four cases of `the decision step, as \`ci.yml\` writes it`, the + * five of `the objectui#8857 pull request shape`, objectui#9142's `FIRES: the + * planted root document runs the shards` and the real-step half of its depth-1 + * case. The property the step carried, that a markdown-only pull request runs + * the tests that read the markdown, is the root's in two halves now: the + * `filter` job's `ui` globs start `Test UI` on any change under `ui/` (pinned + * below, by path), and `select-affected-tests.mjs` picks the tests, a package's + * own for a file it owns and every `scripts/**` test always + * (`select-affected-tests.test.ts`). Neither reads this ledger, which keeps its + * audit and its derivation here. */ -import { describe, expect, it, beforeAll, afterAll } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { execFileSync } from 'node:child_process'; -import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { globMatches, jobBlock, repoPath, rootCi, uiFilterGlobs, UI_JOBS } from './helpers/root-ci'; + import { ADJUDICATED, SCAN_ROOTS, @@ -63,239 +80,25 @@ import { } from '../markdown-test-inputs.mjs'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const CI_YML = path.join(REPO_ROOT, '.github', 'workflows', 'ci.yml'); /** The document objectui#8857 changed, and the test that reads it. */ const INSTANCE_DOCUMENT = 'packages/plugin-dashboard/README.md'; const INSTANCE_READER = 'packages/plugin-dashboard/src/__tests__/readme-dashboard-examples-spec-valid.test.ts'; -/* ── extracting the real step ────────────────────────────────────────────── */ - -const STEP_HEADER = - ' - name: Decide whether this change needs a full run\n id: relevant\n run: |\n'; - -/** - * The `run:` bodies of every copy of the decision step, in file order. - * - * Textual rather than YAML-parsed for the same reason `workflow-checks.ts` gives: - * what is under test is the bytes CI runs, and a parser that normalised them - * would be testing its own normalisation. - */ -function decisionSteps(yaml: string): string[] { - const out: string[] = []; - let from = 0; - for (;;) { - const at = yaml.indexOf(STEP_HEADER, from); - if (at === -1) break; - const start = at + STEP_HEADER.length; - const body: string[] = []; - for (const line of yaml.slice(start).split('\n')) { - if (line.trim() === '') { - body.push(line); - continue; - } - if (!line.startsWith(' ')) break; - body.push(line); - } - out.push(`${body.join('\n').trimEnd()}\n`); - from = start; - } - return out; -} - -const YAML = readFileSync(CI_YML, 'utf8'); -const STEPS = decisionSteps(YAML); -/** `ci.yml` declares the gated jobs in this order. */ -const [TYPE_CHECK_STEP, TEST_STEP, DIST_PINS_STEP, E2E_STEP] = STEPS; - -/* ── a real repository, a real bash run ──────────────────────────────────── */ - -type Outcome = { shouldRun: string; log: string }; +/* ── the root job that runs the readers ───────────────────────────────────── */ -let scratch: string; - -function git(cwd: string, ...args: string[]): void { - execFileSync('git', args, { cwd, stdio: 'pipe' }); -} - -/** - * Build a repository whose pull request changes exactly `files`, and run one - * copy of the step against it. - * - * The scripts the step calls are copied in AFTER both commits, so they are - * untracked and cannot appear in the diff the step reads — the pull request - * under test really does change nothing but `files`. - */ -function runStep(step: string, files: Record): Outcome { - const repo = mkdtempSync(path.join(scratch, 'pr-')); - git(repo, 'init', '-q', '-b', 'main'); - git(repo, 'config', 'user.email', 'ci@example.invalid'); - git(repo, 'config', 'user.name', 'CI'); - writeFileSync(path.join(repo, 'seed.txt'), 'base\n'); - git(repo, 'add', '-A'); - git(repo, 'commit', '-q', '-m', 'base'); - const base = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim(); - - for (const [rel, body] of Object.entries(files)) { - mkdirSync(path.join(repo, path.dirname(rel)), { recursive: true }); - writeFileSync(path.join(repo, rel), body); - } - git(repo, 'add', '-A'); - git(repo, 'commit', '-q', '-m', 'pull request'); - const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim(); - - mkdirSync(path.join(repo, 'scripts'), { recursive: true }); - for (const name of ['markdown-test-inputs.mjs', 'invoked-as.mjs', 'js-comment-mask.mjs']) { - copyFileSync(path.join(REPO_ROOT, 'scripts', name), path.join(repo, 'scripts', name)); - } - - const script = step - .split('${{ github.event_name }}') - .join('pull_request') - .split('${{ github.event.pull_request.base.sha }}') - .join(base) - .split('${{ github.event.pull_request.head.sha }}') - .join(head); - const scriptPath = path.join(repo, 'step.sh'); - writeFileSync(scriptPath, script); - const outputPath = path.join(repo, 'github-output'); - writeFileSync(outputPath, ''); - - // `bash -e {0}` is the shell GitHub runs a `run:` block with on Linux. - const log = execFileSync('bash', ['-e', scriptPath], { - cwd: repo, - encoding: 'utf8', - env: { ...process.env, GITHUB_OUTPUT: outputPath }, +describe('the root Test UI job starts on a markdown-only pull request', () => { + it('the `ui` filter matches the documents the objectui#8857, #9096 and #9142 shapes changed', () => { + const globs = uiFilterGlobs(rootCi()); + const missed = [INSTANCE_DOCUMENT, 'AGENTS.md', 'SECURITY.md'] + .map(repoPath) + .filter((file) => !globs.some((glob) => globMatches(glob, file))); + expect(missed, 'a markdown-only change under ui/ would skip Test UI').toEqual([]); }); - const written = readFileSync(outputPath, 'utf8').trim(); - return { shouldRun: written.replace(/^should_run=/, ''), log }; -} - -beforeAll(() => { - scratch = mkdtempSync(path.join(tmpdir(), 'markdown-test-inputs-')); -}); -afterAll(() => { - if (scratch) rmSync(scratch, { recursive: true, force: true }); -}); -/* ── the step this file is about ─────────────────────────────────────────── */ - -describe('the decision step, as `ci.yml` writes it', () => { - it('carries four copies, and only the two test jobs ask about markdown inputs', () => { - expect(STEPS).toHaveLength(4); - expect(TEST_STEP).toContain('scripts/markdown-test-inputs.mjs'); - expect(DIST_PINS_STEP).toContain('scripts/markdown-test-inputs.mjs'); - expect(TYPE_CHECK_STEP).not.toContain('markdown-test-inputs'); - expect(E2E_STEP).not.toContain('markdown-test-inputs'); - }); - - it('gives the dist-pin job the `test` job\'s gate VERBATIM (objectui#9499)', () => { - // The built-artifact pin lane ran as a step of `test` shard 1 until - // objectui#9499 gave it a job. Inside that job it inherited the widened - // gate; a narrower one here would silently stop running the pins on exactly - // the markdown-only pull requests objectui#9096 brought back into scope, - // and a green suite that stopped measuring something is what this whole - // file is about. Equality of the CODE rather than "also contains the - // script": the second stage is only an improvement while it asks the same - // question, and "contains" is satisfied by a copy that asks it differently. - // - // Shell comments are stripped from both sides first, and only they: the - // `test` job's copy carries ~70 lines of them recording objectui#8857, - // #9096 and #9241, which are the reasoning for the stage and not the stage. - // Every executable line, including the exclusion pathspecs and every - // fail-open branch, is compared verbatim. - const code = (step: string) => - step - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n') - .trim(); - - expect(code(DIST_PINS_STEP)).toEqual(code(TEST_STEP)); - // The control: stripping comments must not have emptied either side. - expect(code(TEST_STEP)).toContain('markdown-test-inputs.mjs'); - expect(code(TEST_STEP).split('\n').length).toBeGreaterThan(20); - }); - - it('asks the markdown question only AFTER the unwidened diff came back empty', () => { - // Order is the optimisation. If the markdown stage ran first, or ran - // unconditionally, every pull request would pay for it. - const exclusions = TEST_STEP.indexOf(":(exclude,glob)**/*.md'"); - const markdown = TEST_STEP.indexOf('markdown-test-inputs.mjs'); - expect(exclusions).toBeGreaterThan(-1); - expect(markdown).toBeGreaterThan(exclusions); - }); - - it('fails toward the full run, in both of the new failure branches', () => { - // The posture this step already had (objectstack#4928): a question that - // cannot be answered means RUN. Every `if !` capture in the widened step - // must reach a `should_run=true`, never a skip. - const failures = TEST_STEP.split('\n').filter((line) => /^\s*if ! /.test(line)); - expect(failures.length).toBeGreaterThanOrEqual(3); - for (const failure of failures) { - const at = TEST_STEP.indexOf(failure); - const branch = TEST_STEP.slice(at, TEST_STEP.indexOf('fi', at)); - expect(branch, `${failure.trim()} must run everything when it fails`).toContain( - "echo 'should_run=true'", - ); - } - }); -}); - -/* ── the firing control, all three directions ────────────────────────────── */ - -describe('the objectui#8857 pull request shape', () => { - const FILES = { [INSTANCE_DOCUMENT]: '# Dashboard\n\nA fence a test evaluates.\n' }; - - it('FIRES: the widened `test` step runs everything, and says which document', () => { - const outcome = runStep(TEST_STEP, FILES); - expect(outcome.shouldRun).toBe('true'); - expect(outcome.log).toContain(INSTANCE_DOCUMENT); - }); - - it('BEFORE: the same input through the unwidened copy of the step still skips', () => { - // Not a reconstruction of the old step — the `type-check` job's copy IS the - // shape the `test` job's copy had, and this change left it alone. - const outcome = runStep(TYPE_CHECK_STEP, FILES); - expect(outcome.shouldRun).toBe('false'); - }); - - it('INERT: an excluded NON-markdown change still skips, through the WIDENED step', () => { - // objectui#9096 took markdown off this leg — see the header. What is left - // is the half of the exclusion list that still pays: `apps/site/**` and the - // non-markdown files under `content/**` and `docs/**`. Without this leg a - // step that answered `true` unconditionally would pass everything above. - // - // This is not a hypothetical shape: `8011852dc` ("fix(site): drop the - // count-based claims from the homepage") is exactly it, and it is the one - // commit of the 36 that reached this stage in the measured window which - // still skips after the widening. - const outcome = runStep(TEST_STEP, { - 'apps/site/src/app/page.tsx': 'export default function Page() { return null; }\n', - 'docs/diagrams/architecture.svg': '\n', - }); - expect(outcome.shouldRun).toBe('false'); - }); - - it('FIRES on the objectui#9096 shape: a root document only `scripts/__tests__` reads', () => { - // The card's own named instance, re-run as a fixture: the commit whose - // subject begins `docs(agents): narrow the package-level test claim` - // changed only `AGENTS.md`, three tests under `scripts/__tests__` read it, - // and the shards skipped. `AGENTS.md` is in no product test's class, so - // this leg fires ONLY because `scripts` is a scan root. - const files = { 'AGENTS.md': '# AGENTS\n\nA sentence a gate test reads.\n' }; - expect(runStep(TEST_STEP, files).shouldRun).toBe('true'); - // …and the unwidened copy of the same step, untouched by this change, is - // the "before" reading for it. - expect(runStep(TYPE_CHECK_STEP, files).shouldRun).toBe('false'); - }); - - it('CONTROL: the widened step still runs everything for an ordinary source change', () => { - // The harness has to be able to say `true` for the boring reason, or the - // `false` legs above prove nothing about the harness. - const outcome = runStep(TEST_STEP, { 'packages/plugin-grid/src/index.ts': 'export {};\n' }); - expect(outcome.shouldRun).toBe('true'); + it('and the job reads that filter', () => { + expect(jobBlock(rootCi(), UI_JOBS.test)).toMatch(/^ {4}if: .*needs\.filter\.outputs\.ui != 'false'/m); }); }); @@ -419,22 +222,8 @@ describe('objectui#9142 — a root-level markdown file a pull request ADDS', () expect(tracked).not.toContain(PLANTED_ROOT_DOCUMENT); }); - it('FIRES: the planted root document runs the shards, and the step says so', () => { - // The lit control, through the REAL step out of `ci.yml` — not through the - // class derivation, which cannot tell a correct class from a step that - // never asks it. Before the `./*` spelling this read `should_run=false` - // with no matched line at all, which is the whole defect: the pull request - // page went green having run nothing, and `check-doc-links.test.ts` — the - // test whose stated job is to fail on a root document with no `SCAN_ROOTS` - // row — is the one that did not run. - const files = { [PLANTED_ROOT_DOCUMENT]: '# Security\n\nA root document nobody declared.\n' }; - const outcome = runStep(TEST_STEP, files); - expect(outcome.shouldRun).toBe('true'); - expect(outcome.log).toContain(PLANTED_ROOT_DOCUMENT); - // …and the unwidened copy of the same step, untouched by this change, is - // the "before" reading taken from the file rather than from history. - expect(runStep(TYPE_CHECK_STEP, files).shouldRun).toBe('false'); - }); + // `FIRES: the planted root document runs the shards, and the step says so` + // retired with objectstack#25712, with the step it ran (see the header). it('names the reader that is waiting on it — the invariant that replaces the list', () => { const hits = markdownTestInputsAmong([PLANTED_ROOT_DOCUMENT]); @@ -463,13 +252,6 @@ describe('objectui#9142 — a root-level markdown file a pull request ADDS', () for (const notMarkdown of ['SECURITY.txt', 'pnpm-lock.yaml', 'turbo.json']) { expect(matchesEntry(notMarkdown, './*')).toBe(false); } - // And through the real step: a root file that is not markdown never reaches - // the markdown stage at all — it is not on the exclusion list, so the first - // diff already runs everything. Both copies agree, which is what makes this - // a statement about the exclusions rather than about the markdown class. - const notMarkdown = { 'SECURITY.txt': 'not markdown\n' }; - expect(runStep(TEST_STEP, notMarkdown).shouldRun).toBe('true'); - expect(runStep(TYPE_CHECK_STEP, notMarkdown).shouldRun).toBe('true'); }); it('the declared population is unchanged — every tracked root document is still an input', () => { diff --git a/ui/scripts/__tests__/render-budget-comment.test.ts b/ui/scripts/__tests__/render-budget-comment.test.ts index 03127a317ee..07c1df38c04 100644 --- a/ui/scripts/__tests__/render-budget-comment.test.ts +++ b/ui/scripts/__tests__/render-budget-comment.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest'; -import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -9,9 +8,6 @@ import { fileURLToPath } from 'node:url'; import { renderBudgetComment, renderFromEnv } from '../render-budget-comment.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const workflowPath = path.join(repoRoot, '.github/workflows/performance-budget.yml'); -const rendererPath = path.join(repoRoot, 'scripts/render-budget-comment.mjs'); -const checkerPath = path.join(repoRoot, 'scripts/check-eager-closure-budget.mjs'); const NO_SUCH_REPORT = path.join(repoRoot, 'scripts/__tests__/__no-size-report__.md'); /** @@ -439,202 +435,19 @@ describe('renderFromEnv', () => { }); }); -/** - * The renderer can only be correct if the workflow keeps feeding it. These pin - * the two workflow-level halves of the fix, which no unit test can exercise: - * the step conditions, and the env contract between the two files. - */ -describe('performance-budget.yml contract', () => { - const workflow = fs.readFileSync(workflowPath, 'utf8'); - const renderer = fs.readFileSync(rendererPath, 'utf8'); - // Only the step conditions — prose in `#` comments discusses `always()` and - // must not be mistaken for a use of it. - const conditions = [...workflow.matchAll(/^\s*if:\s*(.+)$/gm)].map((m) => m[1]); - - it('never gates a step on always() — it fires on cancelled runs, which measure nothing', () => { - expect(conditions.filter((c) => c.includes('always()'))).toEqual([]); - }); - - it('gates the size report, the renderer and the comment on !cancelled()', () => { - // Five: those three, plus the turbo cache report (objectui#12276), which is - // bookkeeping after the verdict and reports on a red run for the same reason, - // plus the pinned @objectstack build's save (objectui#12957), bookkeeping after - // the verdict that keeps the build whatever the verdict was. - expect(conditions.filter((c) => c.includes('!cancelled()'))).toHaveLength(5); - }); - - it('generates the size report only from a complete package build', () => { - expect(workflow).toContain( - "if: ${{ !cancelled() && steps.build_packages.outcome == 'success' }}", - ); - expect(workflow).toContain('id: build_packages'); - }); - - it('never interpolates a step output into the comment JS source', () => { - // `const status = '${{ steps.budget.outputs.budget_status }}'` is how an - // absent output became the empty string that rendered as FAIL. - expect(workflow).not.toMatch(/=\s*'\$\{\{\s*steps\.budget\.outputs/); - }); - - it('passes every env var the renderer reads, and reads every one it passes', () => { - // The render step's own block: another step's `env:` (the turbo cache - // report's RESTORED_CACHE_KEY, objectui#12276) is not the renderer's contract. - const renderStep = workflow.slice(workflow.indexOf('- name: Render performance budget comment')); - const block = renderStep.slice(0, renderStep.indexOf('- name:', 1)); - expect(block, 'the render step is gone').toContain('scripts/render-budget-comment.mjs'); - const declared = [...block.matchAll(/^\s{10}([A-Z_]+): \$\{\{ steps\./gm)].map((m) => m[1]); - - expect(declared.length).toBeGreaterThan(0); - for (const name of declared) { - expect(renderer, `renderer must read env.${name}`).toContain(`env.${name}`); - } - }); - - /** - * objectui#6230: `closure_chunk_status` (objectui#5490) and - * `closure_headroom_status` (objectui#5924) were both published to - * `$GITHUB_OUTPUT` and never read, each following the precedent of the last. - * The card's explicit ask was that a THIRD round of this not happen — so the - * obligation is pinned mechanically rather than left to review: a half added - * to the checker fails here until it reaches the comment. - */ - it('passes every closure verdict the checker publishes into the comment step', () => { - const checker = fs.readFileSync(checkerPath, 'utf8'); - const call = checker.slice(checker.lastIndexOf('writeGithubOutput({')); - const published = [...call.slice(0, call.indexOf('});')).matchAll(/(closure_\w+):/g)].map( - (m) => m[1], - ); - - // Guard the guard: a regex that matched nothing would pass silently. - expect(published).toContain('closure_status'); - expect(published).toContain('closure_chunk_status'); - expect(published).toContain('closure_headroom_status'); - expect(published).toContain('closure_freshness_status'); - expect(published).toContain('closure_membership_status'); - - for (const key of published) { - expect(workflow, `workflow must pass steps.budget.outputs.${key} to the comment step`) - .toContain(`steps.budget.outputs.${key}`); - } - }); - - /** - * objectui#6245. The freshness half's inputs do NOT travel as `steps.*` env — - * they are the checker's, not the comment's — so the env-contract test above - * cannot see them, and without these three the wiring could rot silently while - * every other test in this file stayed green. The half would then report - * `error` on every PR, or, if `GITHUB_EVENT_NAME` stopped being the - * discriminator, quietly stop asking. - */ - describe('ceiling freshness inputs', () => { - const checker = fs.readFileSync(checkerPath, 'utf8'); - - it('fetches deep enough to see the base commit the merge ref was built on', () => { - expect(workflow).toContain('fetch-depth: 2'); - }); - - it('resolves both base-branch readings on pull_request runs only', () => { - expect(workflow).toContain('- name: Resolve the base-branch ceiling constants'); - expect(workflow).toContain("if: ${{ github.event_name == 'pull_request' }}"); - }); - - it('exports exactly the variables the checker reads', () => { - for (const name of [ - 'EAGER_CLOSURE_PR_BASE_SOURCE', - 'EAGER_CLOSURE_BASE_SOURCE', - 'EAGER_CLOSURE_PR_BASE_SHA', - 'EAGER_CLOSURE_BASE_SHA', - 'EAGER_CLOSURE_BASE_REF', - ]) { - expect(workflow, `workflow must export ${name}`).toContain(`echo "${name}=`); - expect(checker, `checker must read env.${name}`).toContain(`env.${name}`); - } - }); - - /** - * objectui#6245, second and third pass. The freshness half shipped a - * workflow half that `Bundle Analysis` could never run on: the PR editing it - * touched no `packages/**` path, so the gate never appeared on its own PR. - * Adding the YAML to the filters fixed that — and was still not enough. A PR - * touching only `check-eager-closure-budget.mjs`, the file that computes the - * verdict and the one this card is about, ALSO did not trigger the gate. - * - * The convention measured on `origin/main` is the gate's RUNTIME CLOSURE, - * not merely its own YAML: `half-state-patrol.yml` lists both the script it - * runs and `scripts/invoked-as.mjs`, that script's dependency. - * - * A wiring bug in any of these is fail-CLOSED — freshness `error`, exit 2 — - * so an untriggered gate does not fail quietly; it turns a REQUIRED context - * red on the next `packages/**` PR, belonging to another seat, reading to - * them as a bundle problem in their own diff. - */ - const TRIGGER_CLOSURE = [ - '.github/workflows/performance-budget.yml', - 'scripts/check-eager-closure-budget.mjs', - // The COMPOSITION half (objectui#7479) — WHICH locale catalogues a page - // load pulls, which the byte half above cannot answer. - 'scripts/check-eager-locale-catalogues.mjs', - 'scripts/render-budget-comment.mjs', - // `isEntrypoint` decides whether `main()` runs at all — a regression here - // makes the checker exit 0 having measured nothing. - 'scripts/invoked-as.mjs', - // The turbo cache report and prune the job runs after its verdict (objectui#12276). - 'scripts/report-turbo-cache.mjs', - // The pinned @objectstack build the job restores or builds before install (objectui#12957). - 'scripts/objectstack-pin.mjs', - ]; - - it('triggers on its whole runtime closure, so no part of this gate ships unexercised', () => { - // `#` lines are part of the block — an entry that needs explaining carries - // its reason inline — and `[ \t]*` rather than `\s*` so the run cannot walk - // past the end of the list into the next key. - const filters = [...workflow.matchAll(/^[ \t]*paths:\n((?:[ \t]*(?:- |#).*\n)+)/gm)].map( - (m) => m[1], - ); - // Guard the guard: `push` and `pull_request` both carry one, and they must - // not drift apart — a gate that runs on a PR but not on the merge (or the - // reverse) is worse than one that runs on neither. - expect(filters).toHaveLength(2); - for (const filter of filters) { - for (const entry of TRIGGER_CLOSURE) { - expect(filter, `every paths: filter must list ${entry}`).toContain(entry); - } - } - }); - - /** - * The closure is defined by what the JOB EXECUTES, which is why the - * `__tests__` files are deliberately absent from it: this job runs two - * `node scripts/*.mjs` commands and never vitest, so a test-only edit cannot - * move this gate's verdict, and those tests already run on every PR in the - * root vitest `unit` project. This asserts the definition rather than the - * list, so the two stay in step. - */ - it('lists every script the job runs, and nothing it merely tests', () => { - const invoked = [...workflow.matchAll(/^\s*(?:run: )?node (scripts\/[\w./-]+\.mjs)/gm)].map( - (m) => m[1], - ); - expect(invoked.length).toBeGreaterThan(0); - for (const script of new Set(invoked)) { - expect(TRIGGER_CLOSURE, `${script} is executed by this job, so it belongs in the closure`) - .toContain(script); - } - expect(TRIGGER_CLOSURE.some((e) => e.includes('__tests__'))).toBe(false); - }); - - it('lets the bundle be measured even when the resolve step fails', () => { - // The checker turns absent inputs into a freshness ERROR, so failing the - // job here instead would only cost the run its numbers (objectui#3152). - const step = workflow.slice(workflow.indexOf('- name: Resolve the base-branch ceiling')); - const budgetStep = step.indexOf('- name: Check console performance budget'); - expect(step.slice(0, budgetStep)).toContain('continue-on-error: true'); - expect(budgetStep).toBeGreaterThan(-1); - }); - }); - - it('writes budget_status on every path the budget step can exit through', () => { - // pass, fail, and the two "nothing to measure" errors. - const statuses = [...workflow.matchAll(/budget_status=(\w+)/g)].map((m) => m[1]); - expect(new Set(statuses)).toEqual(new Set(['pass', 'fail', 'error'])); - }); -}); +// `performance-budget.yml contract` retired with objectstack#25712. Its +// thirteen cases pinned the workflow halves no unit test can exercise: no step +// gated on `always()`; the size report, the renderer and the comment gated on +// `!cancelled()`; the size report only from a complete package build; no step +// output interpolated into the comment's JS source; the env contract with the +// renderer, both directions; every closure verdict the checker publishes passed +// to the comment step; the ceiling-freshness inputs (`fetch-depth: 2`, both +// base-branch readings on `pull_request` only, exactly the variables the checker +// reads); the trigger carrying the gate's whole runtime closure; the job listing +// every script it runs and nothing it merely tests; the bundle measured even +// when the resolve step fails; and `budget_status` written on every exit path. +// Each read objectui's `performance-budget.yml`, which runs nowhere under the +// monorepo: GitHub runs the root `.github/workflows/` alone, and no root +// workflow runs this renderer. That workflow is row 22 of the objectstack#25697 +// workflow roster, "kept, follow-up port" into `Build & E2E UI`; the port +// brings these pins back against the root job, by path. diff --git a/ui/scripts/__tests__/render-changeset-claims-comment.test.ts b/ui/scripts/__tests__/render-changeset-claims-comment.test.ts index 6560cb4713c..77e5b4fd11f 100644 --- a/ui/scripts/__tests__/render-changeset-claims-comment.test.ts +++ b/ui/scripts/__tests__/render-changeset-claims-comment.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { MARKER, @@ -31,7 +30,11 @@ import { * the whole body is pinned free of tag-shaped fragments. * 2. **One copy of the marker.** The workflow reads it off the body it is * about to post. A second spelling in the YAML is drift waiting to happen, - * so its absence is pinned. + * so its absence was pinned. Retired with objectstack#25712: the pin read + * objectui's `changeset-presence.yml`, which runs nowhere under the + * monorepo (GitHub runs the root `.github/workflows/` alone). It is row 4 + * of the objectstack#25697 workflow roster (→ sub-issue 4), and a port + * brings the pin back against its root workflow, by path. * 3. **An empty finding set never reads as a request.** That inversion is * objectui#3152's bug in this gate's costume, and it would mute the channel * the ruling exists to un-mute. @@ -40,9 +43,6 @@ import { * 「REQUEST TO READ」self-description becomes true by delivery, and it stays * true only while the comment keeps saying what the gate does not do. */ -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const WORKFLOW = '.github/workflows/changeset-presence.yml'; - /** The shape GitHub deletes on save — the reason the marker is plain text. */ const TAG_SHAPED = /<[^<>\s][^<>]*>/; @@ -119,10 +119,10 @@ describe('the marker the in-place update depends on', () => { expect(body).not.toContain('