Skip to content

Commit 249928d

Browse files
committed
test(ui-merge): the Repo Gates readers pin the root lint-ui job
objectui's `repo-gates.yml` sits under `ui/.github/workflows/`, where GitHub never runs it. The root `ci.yml`'s `lint-ui` job now runs the same gates through `pnpm --dir ui run <alias>`, so the four tests that pinned the nested workflow read the root job by path instead: - check-control-bytes and check-docs-route-eager-closure: the alias is a package script, the root Lint UI job runs it, the root trigger gates pull requests with no paths filter, and the `ui` filter matches the files the gate reads. - check-doc-snippet-emitted-census: the census leg runs after the gate build and before the gate, in the root job, and the alias runs this script. - check-doc-fence-languages: one step runs both legs, no other job or root workflow runs the guard, the job's install comes after it, and its header comments state no count. The markdown-test-inputs ledger records the new markdown path literals as `notRead`: they probe the `ui` filter's globs and open no document. The fence-languages entry drops its `walker`, since its only walk was of the nested workflows directory. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS
1 parent 8350373 commit 249928d

5 files changed

Lines changed: 162 additions & 73 deletions

‎ui/scripts/__tests__/check-control-bytes.test.ts‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
99
// `tsconfig.scripts.json` (`allowJs`), so no `@ts-expect-error` here —
1010
// re-adding one is now itself an error (TS2578). See objectui#3494.
1111
import { classify, scan, locate, SCANNED_BYTES, KNOWN_OFFENDERS } from '../check-control-bytes.mjs';
12+
import { globMatches, jobBlock, repoPath, rootCi, triggerBlock, uiFilterGlobs, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci';
1213

1314
/**
1415
* objectstack#5425. Two raw U+0000 bytes in
@@ -473,39 +474,40 @@ describe('objectstack#5450 — the four baselined files are clean', () => {
473474
});
474475
});
475476

477+
/**
478+
* The home is the repository root's `ci.yml` now (objectstack#25712). GitHub
479+
* runs only the root `.github/workflows/`, and objectstack#25902 ported
480+
* objectui's `repo-gates.yml`, this gate's old home, into the root `lint-ui`
481+
* job, which spells it through its alias. Both pins read that job and the root
482+
* triggers, by path.
483+
*/
476484
describe('wiring — the gate is actually reachable and actually runs', () => {
477485
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
478-
const workflowPath = path.join(repoRoot, '.github/workflows/repo-gates.yml');
486+
const ci = rootCi();
479487

480488
it('is exposed as a root package script', () => {
481489
expect(pkg.scripts['check:control-bytes']).toBe('node scripts/check-control-bytes.mjs');
482490
});
483491

484-
it('has a workflow that gates pull requests', () => {
485-
expect(fs.existsSync(workflowPath), 'a check nothing runs is not a gate').toBe(true);
486-
expect(fs.readFileSync(workflowPath, 'utf8')).toMatch(/pull_request:/);
487-
});
488-
489-
it('does NOT filter that workflow by path', () => {
490-
// This is the whole reason it is its own workflow rather than a step in
491-
// ci.yml or lint.yml: on a markdown-only change both of those skip every
492-
// expensive step, so a gate inside either would never see it. Markdown is
493-
// precisely the carrier objectstack#4890 was found in, so a path filter here
494-
// would rebuild the hole this guard exists to close. `changeset-guard.yml`
495-
// is in the repo for the same reason and says so in its own header.
496-
//
497-
// Not the reason this comment used to give — "both of those `paths-ignore`
498-
// markdown, `content/**`, `docs/**` and `.changeset/**`" — which
499-
// objectui#4381 corrected here and in the workflow's own header. Since
500-
// objectui#3523 step 2 that filter is gone from their `pull_request` trigger
501-
// and survives only on `push`, so a markdown-only PR does start both and
502-
// does produce their contexts (measured — PR #3856, one markdown file, 16
503-
// checks). What still skips is the work inside them: the `Decide whether
504-
// this change needs a full run` step excludes markdown and skips every step
505-
// below itself, and the `push` lane is filtered at the trigger as before.
506-
// The conclusion outlived its premise; objectui#3857 pinned the correction.
507-
const workflow = fs.readFileSync(workflowPath, 'utf8');
508-
expect(workflow).not.toMatch(/paths-ignore:/);
509-
expect(workflow).not.toMatch(/^\s+paths:/m);
492+
it('the root Lint UI job runs it, and the root ci.yml gates pull requests', () => {
493+
expect(withoutComments(jobBlock(ci, UI_JOBS.lint)), 'a check nothing runs is not a gate').toContain(
494+
`run: ${uiRun('check:control-bytes')}`,
495+
);
496+
expect(triggerBlock(ci)).toMatch(/^ {2}pull_request:/m);
497+
});
498+
499+
it('is filtered by no path a markdown-only change could miss', () => {
500+
// Markdown is precisely the carrier objectstack#4890 was found in, so a
501+
// filter that skips a markdown-only change would rebuild the hole this guard
502+
// exists to close. objectui's `repo-gates.yml` met that with no path filter
503+
// at all. The root ci.yml filters no paths at the trigger either, and
504+
// `lint-ui` runs on the `filter` job's `ui` globs, which must keep matching
505+
// a markdown-only change anywhere under `ui/`.
506+
expect(withoutComments(triggerBlock(ci))).not.toMatch(/^\s+paths(-ignore)?:/m);
507+
const globs = uiFilterGlobs(ci);
508+
const missed = ['README.md', 'content/docs/guide/ci-cd-pipeline.md', 'packages/core/README.md']
509+
.map(repoPath)
510+
.filter((file) => !globs.some((glob) => globMatches(glob, file)));
511+
expect(missed, 'a markdown-only change under ui/ would skip Lint UI').toEqual([]);
510512
});
511513
});

‎ui/scripts/__tests__/check-doc-fence-languages.test.ts‎

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,27 @@ import {
3434
} from '../check-doc-component-types.mjs';
3535

3636
import { selfTestCases, stripAnsi } from './helpers/child-verdict';
37+
import {
38+
globMatches,
39+
jobBlock,
40+
repoPath,
41+
rootCi,
42+
rootCiPath,
43+
rootWorkflow,
44+
rootWorkflowFiles,
45+
uiFilterGlobs,
46+
uiRun,
47+
UI_JOBS,
48+
} from './helpers/root-ci';
3749

3850
const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..');
3951
const GUARD = 'scripts/check-doc-fence-languages.mjs';
40-
const WORKFLOW = 'repo-gates.yml';
52+
const ALIAS = 'check:doc-fences';
53+
/**
54+
* The guard's home, as failure messages name it: the root `lint-ui` job, where
55+
* objectstack#25902 ported objectui's `repo-gates.yml` (objectstack#25712).
56+
*/
57+
const WORKFLOW = `ci.yml (${UI_JOBS.lint})`;
4158

4259
/**
4360
* A numeral that qualifies a document-population noun, as `match: text`.
@@ -439,42 +456,58 @@ describe('check-doc-fence-languages: non-vacuity, through the shipped module', (
439456
});
440457
});
441458

459+
/**
460+
* GitHub runs only the repository root's `.github/workflows/`, so the wiring is
461+
* the root `ci.yml`'s: objectstack#25902 ported objectui's `repo-gates.yml` into
462+
* its `lint-ui` job, which runs the guard through its alias (objectstack#25712).
463+
* Every pin below reads that file by path.
464+
*/
442465
describe('check-doc-fence-languages is wired, not merely present', () => {
443-
const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows', WORKFLOW), 'utf8'));
444-
const steps: Array<Record<string, unknown>> = workflow.jobs['repo-gates'].steps;
445-
const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GUARD));
466+
const workflow = parseYaml(rootCi());
467+
const steps: Array<Record<string, unknown>> = workflow.jobs[UI_JOBS.lint].steps;
468+
const runs = (s: Record<string, unknown>) => typeof s.run === 'string' && (s.run as string).includes(uiRun(ALIAS));
469+
const gateSteps = steps.filter(runs);
446470

447471
it('the guard script exists', () => {
448472
expect(fs.existsSync(path.join(ROOT, GUARD))).toBe(true);
449473
});
450474

451475
it('package.json aliases it, and the alias points at the script that exists', () => {
452476
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
453-
expect(pkg.scripts['check:doc-fences']).toContain(GUARD);
477+
expect(pkg.scripts[ALIAS]).toBe(`node ${GUARD}`);
454478
});
455479

456-
it('the workflow runs it — one step, both legs', () => {
480+
it('the root Lint UI job runs it — one step, both legs', () => {
457481
expect(gateSteps).toHaveLength(1);
458482
const run = gateSteps[0].run as string;
459-
expect(run).toContain(`node ${GUARD} --self-test`);
460-
expect(run.split('\n').some((l) => l.trim() === `node ${GUARD}`)).toBe(true);
483+
expect(run).toContain(`${uiRun(ALIAS)} --self-test`);
484+
expect(run.split('\n').some((l) => l.trim() === uiRun(ALIAS))).toBe(true);
461485
});
462486

463-
it('one gate, one home — no other workflow runs the same script', () => {
464-
const dir = path.join(ROOT, '.github/workflows');
465-
const others = fs
466-
.readdirSync(dir)
467-
.filter((f) => f.endsWith('.yml') && f !== WORKFLOW)
468-
.filter((f) => fs.readFileSync(path.join(dir, f), 'utf8').includes(GUARD));
487+
it('one gate, one home — no other job or workflow runs the same script', () => {
488+
const jobs = Object.entries(workflow.jobs as Record<string, { steps?: Array<Record<string, unknown>> }>)
489+
.filter(([, job]) => (job.steps ?? []).some(runs))
490+
.map(([key]) => key);
491+
expect(jobs).toEqual([UI_JOBS.lint]);
492+
const others = rootWorkflowFiles()
493+
.filter((f) => f !== path.basename(rootCiPath))
494+
.filter((f) => rootWorkflow(f).includes(ALIAS) || rootWorkflow(f).includes(GUARD));
469495
expect(others).toEqual([]);
470496
});
471497

472498
it('has no paths filter — the defect it catches arrives in a docs-only pull request', () => {
473499
// `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`.
474500
const on = workflow.on ?? workflow[true as unknown as string];
475501
expect(Object.keys(on)).toContain('pull_request');
476-
expect(on.pull_request).not.toHaveProperty('paths');
477-
expect(on.pull_request).not.toHaveProperty('paths-ignore');
502+
expect(on.pull_request ?? {}).not.toHaveProperty('paths');
503+
expect(on.pull_request ?? {}).not.toHaveProperty('paths-ignore');
504+
// The root job runs on the `filter` job's `ui` globs, so those globs are the
505+
// path filter that matters, and a docs-only change under `ui/` must match.
506+
const globs = uiFilterGlobs(rootCi());
507+
const missed = ['content/docs/guide/ci-cd-pipeline.md', 'packages/core/README.md']
508+
.map(repoPath)
509+
.filter((file) => !globs.some((glob) => globMatches(glob, file)));
510+
expect(missed, 'a docs-only change under ui/ would skip Lint UI').toEqual([]);
478511
});
479512

480513
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', () => {
483516
});
484517

485518
it('needs no install — no pnpm runs before this gate in its job', () => {
486-
// Since objectui#12214 the home is `repo-gates.yml`, ONE job that installs
487-
// after its install-free gates: the property is now an ORDER -- nothing
488-
// before this gate's step runs `pnpm install` -- and no longer an absence.
489-
const gateAt = steps.findIndex((s) => typeof s.run === 'string' && (s.run as string).includes(GUARD));
519+
// Since objectui#12214 the home is ONE job that installs after its
520+
// install-free gates (`repo-gates.yml` then, the root `lint-ui` now): the
521+
// property is an ORDER -- nothing before this gate's step runs
522+
// `pnpm install` -- and no longer an absence.
523+
const gateAt = steps.findIndex(runs);
490524
expect(gateAt).toBeGreaterThan(-1);
491525
const before = steps
492526
.slice(0, gateAt)
@@ -561,7 +595,7 @@ describe('check-doc-fence-languages is wired, not merely present', () => {
561595
* number is written here that a document being added would falsify.
562596
*/
563597
it('its header states the population and never counts it — no count can rot here', () => {
564-
const header = headerComments(fs.readFileSync(path.join(ROOT, '.github/workflows', WORKFLOW), 'utf8'));
598+
const header = headerComments(jobBlock(rootCi(), UI_JOBS.lint));
565599

566600
// objectui#7901 — the floor first, or the assertion below is vacuous. The
567601
// count half cannot distinguish a header that carries no counts from a

‎ui/scripts/__tests__/check-doc-snippet-emitted-census.test.ts‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
listEmittedSources,
2626
scanEmittedTemplates,
2727
} from '../check-doc-snippet-types.mjs';
28+
import { jobBlock, rootCi, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci';
2829

2930
/**
3031
* objectui#7864 — the emitted-code census.
@@ -404,20 +405,31 @@ describe('on this repository — the walk is non-vacuous and reaches both known
404405
});
405406
});
406407

408+
/**
409+
* The home is the repository root's `ci.yml` now (objectstack#25712). GitHub
410+
* runs only the root `.github/workflows/`, and objectstack#25902 ported
411+
* objectui's `repo-gates.yml` into the root `lint-ui` job, which runs the
412+
* census and the gate through the `check:doc-snippets` alias, after the build
413+
* that serves them. The pins read that job by path, and the alias case ties the
414+
* spelling to this script.
415+
*/
407416
describe('wiring — a census nothing runs is not a census', () => {
408-
const workflow = fs.readFileSync(
409-
path.join(repoRoot, '.github/workflows/repo-gates.yml'),
410-
'utf8',
411-
);
417+
const workflow = withoutComments(jobBlock(rootCi(), UI_JOBS.lint));
418+
const ALIAS = 'check:doc-snippets';
419+
420+
it('the alias the job runs is this script', () => {
421+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
422+
expect(pkg.scripts[ALIAS]).toBe(`node ${SCRIPT}`);
423+
});
412424

413-
it('runs the census in the workflow that already builds this gate\'s closure', () => {
414-
expect(workflow).toContain('--emit-census');
425+
it('runs the census in the job that already builds this gate\'s closure', () => {
426+
expect(workflow).toContain(`${uiRun(ALIAS)} --emit-census`);
415427
});
416428

417429
it('runs it AFTER the build and BEFORE the blocking gate, so a red gate cannot hide the number', () => {
418-
const build = workflow.indexOf('turbo run build');
419-
const census = workflow.indexOf(`node ${SCRIPT} --emit-census`);
420-
const gate = workflow.search(new RegExp(`run: node ${SCRIPT.replace(/[.\\/]/g, '\\$&')}\\s*$`, 'm'));
430+
const build = workflow.indexOf('id: ui-gate-build');
431+
const census = workflow.indexOf(`${uiRun(ALIAS)} --emit-census`);
432+
const gate = workflow.search(new RegExp(`run: ${uiRun(ALIAS).replace(/[.\\/:-]/g, '\\$&')}\\s*$`, 'm'));
421433
expect(build).toBeGreaterThan(-1);
422434
expect(census).toBeGreaterThan(build);
423435
expect(

‎ui/scripts/__tests__/check-docs-route-eager-closure.test.ts‎

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
MEASURED_PAYLOAD,
1717
REGISTRAR,
1818
} from '../check-docs-route-eager-closure.mjs';
19+
import { globMatches, jobBlock, repoPath, rootCi, triggerBlock, uiFilterGlobs, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci';
1920

2021
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
2122

@@ -368,31 +369,49 @@ describe('the real tree -- the properties that keep this gate from being decorat
368369
// teach readers to fix the test rather than the import.
369370
});
370371

372+
/**
373+
* The home is the repository root's `ci.yml` now (objectstack#25712). GitHub
374+
* runs only the root `.github/workflows/`, and objectstack#25902 ported
375+
* objectui's `repo-gates.yml`, this gate's old home, into the root `lint-ui`
376+
* job, which spells it through its alias. Both pins read that job and the root
377+
* triggers, by path.
378+
*/
371379
describe('wiring -- the gate is actually reachable and actually runs', () => {
372380
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
373-
const workflowPath = path.join(repoRoot, '.github/workflows/repo-gates.yml');
381+
const ci = rootCi();
374382

375383
it('is exposed as a root package script', () => {
376384
expect(pkg.scripts['check:docs-route-closure']).toBe(
377385
'node scripts/check-docs-route-eager-closure.mjs',
378386
);
379387
});
380388

381-
it('has a workflow that gates pull requests and reports on a queue build', () => {
382-
expect(fs.existsSync(workflowPath), 'a check nothing runs is not a gate').toBe(true);
383-
const workflow = fs.readFileSync(workflowPath, 'utf8');
384-
expect(workflow).toMatch(/pull_request:/);
385-
expect(workflow).toMatch(/merge_group:/);
386-
expect(workflow).toContain('pnpm check:docs-route-closure');
389+
it('the root Lint UI job runs it, on pull requests and on a queue build', () => {
390+
expect(withoutComments(jobBlock(ci, UI_JOBS.lint)), 'a check nothing runs is not a gate').toContain(
391+
`run: ${uiRun('check:docs-route-closure')}`,
392+
);
393+
const triggers = triggerBlock(ci);
394+
expect(triggers).toMatch(/^ {2}pull_request:/m);
395+
expect(triggers).toMatch(/^ {2}merge_group:/m);
387396
});
388397

389-
it('does NOT filter that workflow by path', () => {
398+
it('is filtered by no path its inputs could miss', () => {
390399
// Its inputs are the whole route graph -- `packages/**`, `apps/site/**`,
391-
// `content/docs/**` -- plus its own closure in `scripts/`. A filter listing
392-
// all of that is indistinguishable from no filter, and a filter that misses
393-
// one of them cannot be exercised by the PR that changes it (objectui#6321).
394-
const workflow = fs.readFileSync(workflowPath, 'utf8');
395-
expect(workflow).not.toMatch(/paths-ignore:/);
396-
expect(workflow).not.toMatch(/^\s+paths:/m);
400+
// `content/docs/**` -- plus its own closure in `scripts/`. A filter that
401+
// misses one of them cannot be exercised by the PR that changes it
402+
// (objectui#6321). The root ci.yml filters no paths at the trigger, and
403+
// `lint-ui` runs on the `filter` job's `ui` globs, which must keep matching
404+
// every one of those inputs under `ui/`.
405+
expect(withoutComments(triggerBlock(ci))).not.toMatch(/^\s+paths(-ignore)?:/m);
406+
const globs = uiFilterGlobs(ci);
407+
const missed = [
408+
'packages/core/src/index.ts',
409+
'apps/site/source.config.ts',
410+
'content/docs/guide/ci-cd-pipeline.md',
411+
'scripts/check-docs-route-eager-closure.mjs',
412+
]
413+
.map(repoPath)
414+
.filter((file) => !globs.some((glob) => globMatches(glob, file)));
415+
expect(missed, 'a change to one of these inputs would skip Lint UI').toEqual([]);
397416
});
398417
});

‎ui/scripts/markdown-test-inputs.mjs‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,16 @@ export const ADJUDICATED = new Map([
764764
notRead: ['.changeset/README.md', 'CHANGELOG.md', 'README.md', 'apps/console/README.md'],
765765
},
766766
],
767+
// Reads no markdown: its three markdown literals are probes of the root `ci.yml`
768+
// `ui` filter's globs, asserting a markdown-only change still starts Lint UI
769+
// (objectstack#25712). The gate's own corpus is reached through the script.
770+
[
771+
'scripts/__tests__/check-control-bytes.test.ts',
772+
{
773+
reads: [],
774+
notRead: ['README.md', 'content/docs/guide/ci-cd-pipeline.md', 'packages/core/README.md'],
775+
},
776+
],
767777
[
768778
'scripts/__tests__/check-doc-component-types.test.ts',
769779
{
@@ -811,9 +821,11 @@ export const ADJUDICATED = new Map([
811821
[
812822
'scripts/__tests__/check-doc-fence-languages.test.ts',
813823
{
824+
// `packages/core/README.md` is a probe of the root `ci.yml` `ui` filter's
825+
// globs, not a read (objectstack#25712). The test's only directory walk was
826+
// of objectui's own `.github/workflows`, which it no longer reads.
814827
reads: ['apps/console/docs/**', 'content/docs/**', 'docs/**'],
815-
notRead: ['README.md'],
816-
walker: 'markdown-tree',
828+
notRead: ['README.md', 'packages/core/README.md'],
817829
},
818830
],
819831
[
@@ -859,6 +871,16 @@ export const ADJUDICATED = new Map([
859871
walker: 'markdown-tree',
860872
},
861873
],
874+
// Reads no markdown: its one markdown literal is a probe of the root `ci.yml`
875+
// `ui` filter's globs, asserting a docs-only change still starts Lint UI
876+
// (objectstack#25712).
877+
[
878+
'scripts/__tests__/check-docs-route-eager-closure.test.ts',
879+
{
880+
reads: [],
881+
notRead: ['content/docs/guide/ci-cd-pipeline.md'],
882+
},
883+
],
862884
// Reads no markdown: the root-document literals are fixture INPUTS to a pure path classifier.
863885
[
864886
'scripts/__tests__/check-governed-queue-guard.test.ts',

0 commit comments

Comments
 (0)