Skip to content

Commit c65ee39

Browse files
committed
ci(ui-merge): the pre-install import graph derives its population from the repository root's workflows
GitHub runs only the root .github/workflows/. The gate read objectui's nested ui/.github/workflows/, which runs nowhere since the monorepo merge, and went red on ten dead steps that still name the deleted scripts/objectstack-pin.mjs. The population is now the root workflow steps, ahead of their job's install, that run an objectui gate: `pnpm --dir ui run <alias>` resolved through ui/package.json, `node ui/scripts/<x>`, or `pnpm --dir ui exec node scripts/<x>`. A bare `node scripts/<x>` in a root workflow is the root's own script and stays out. The self-test gains the four spelling cases. The test's floor lists the root rows: type-check-ui's coverage check and lint-ui's three gates. The rest of objectui#6148's table ran in workflows nothing runs. Each one is named with the roster row of the #25697 audit that carries it, or with the port that brings it back. The gate is not in its own derived population until the root Lint UI job runs it, so the test walks its graph by name. The header's pointer resolves against the root. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS
1 parent d65cbb6 commit c65ee39

2 files changed

Lines changed: 266 additions & 64 deletions

File tree

‎ui/scripts/__tests__/check-pre-install-import-graph.test.ts‎

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
scan,
1515
staticSpecifiers,
1616
walkImportGraph,
17+
workflowRoot,
1718
} from '../check-pre-install-import-graph.mjs';
1819
import { REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs';
1920

@@ -33,15 +34,16 @@ import { selfTestCases, stripAnsi } from './helpers/child-verdict';
3334
* What this file pins, in the order the gate can go wrong:
3435
*
3536
* 1. **the derivation is a derivation** — the population comes from the
36-
* workflows, so moving a step across `pnpm install` moves the population. A
37+
* workflows GitHub runs (the repository root's, since objectstack#25712),
38+
* so moving a step across `pnpm install` moves the population. A
3739
* gate reporting "13 steps" while reading a constant is the defect it was
3840
* written to prevent, and the only way to tell the two apart is to move a
3941
* step in a fixture and watch the number follow;
4042
* 2. **the walk follows the graph** — a package ONE HOP away must be caught
4143
* and its chain named, because the assertion this generalises could not see
4244
* one;
43-
* 3. **the floor over the real tree** — every step objectui#6148 measured is
44-
* still derived, by workflow and job rather than by count;
45+
* 3. **the floor over the real tree** — every step objectui#6148 measured
46+
* that still runs is still derived, by workflow and job rather than by count;
4547
* 4. **the wiring** — a gate nobody runs is indistinguishable from a gate that
4648
* passes, so the workflow, the alias and the Dependabot classification are
4749
* asserted here rather than trusted by reading;
@@ -59,6 +61,8 @@ import { selfTestCases, stripAnsi } from './helpers/child-verdict';
5961
*/
6062
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
6163
const workflowDir = path.join(repoRoot, '.github/workflows');
64+
/** Where the gate reads `.github/workflows/`: the repository root, which GitHub runs. */
65+
const rootWorkflowRoot = workflowRoot(repoRoot);
6266
const SCRIPT = 'scripts/check-pre-install-import-graph.mjs';
6367
const WORKFLOW = 'repo-gates.yml';
6468
const CHECK_NAME = 'Pre-Install Import Graph Check';
@@ -76,7 +80,7 @@ const readWorkflow = (file: string): string => fs.readFileSync(path.join(workflo
7680

7781
describe('the population follows the workflows', () => {
7882
const fixture = (installStepIndex: number): string => {
79-
const gate = ' - name: Gate\n run: node scripts/probe.mjs\n';
83+
const gate = ' - name: Gate\n run: node ui/scripts/probe.mjs\n';
8084
const install = ' - name: Install dependencies\n run: pnpm install --frozen-lockfile\n';
8185
const filler = ' - name: Checkout code\n uses: actions/checkout@v7\n';
8286
const steps = [filler, gate, filler];
@@ -98,7 +102,7 @@ describe('the population follows the workflows', () => {
98102
});
99103

100104
it('counts a job that never installs entirely', () => {
101-
const noInstall = 'jobs:\n j:\n steps:\n - name: Gate\n run: node scripts/probe.mjs\n';
105+
const noInstall = 'jobs:\n j:\n steps:\n - name: Gate\n run: node ui/scripts/probe.mjs\n';
102106
expect(derive(noInstall)).toEqual(['scripts/probe.mjs']);
103107
});
104108

@@ -109,11 +113,11 @@ describe('the population follows the workflows', () => {
109113
- name: Install dependencies
110114
run: pnpm install --frozen-lockfile
111115
- name: Late
112-
run: node scripts/late.mjs
116+
run: node ui/scripts/late.mjs
113117
does-not:
114118
steps:
115119
- name: Early
116-
run: node scripts/early.mjs
120+
run: node ui/scripts/early.mjs
117121
`;
118122
expect(derive(twoJobs)).toEqual(['scripts/early.mjs']);
119123
});
@@ -140,7 +144,7 @@ describe('the population follows the workflows', () => {
140144
- name: Browsers are not node_modules
141145
run: pnpm exec playwright install chromium
142146
- name: Gate
143-
run: node scripts/probe.mjs
147+
run: node ui/scripts/probe.mjs
144148
`;
145149
expect(derive(tricky)).toEqual(['scripts/probe.mjs']);
146150
});
@@ -151,18 +155,43 @@ describe('the population follows the workflows', () => {
151155
steps:
152156
- name: Two legs
153157
run: |
154-
node scripts/probe.mjs --self-test
155-
node scripts/probe.mjs
158+
node ui/scripts/probe.mjs --self-test
159+
node ui/scripts/probe.mjs
156160
- name: Commented out
157161
run: |
158-
# node scripts/ghost.mjs
162+
# node ui/scripts/ghost.mjs
159163
echo done
160164
`;
161165
// One row per (step, script) — two invocations of one script in one step is
162166
// one step, not two.
163167
expect(derive(block)).toEqual(['scripts/probe.mjs']);
164168
});
165169

170+
it("reads the root workflows' two spellings, and leaves the root's own scripts out", () => {
171+
// objectstack#25712: the root `ci.yml` runs objectui's gates as
172+
// `pnpm --dir ui run <alias>` (resolved through objectui's package.json) or
173+
// `node ui/scripts/<x>`. A bare `node scripts/<x>` there runs objectstack's
174+
// own file of that name, and judging objectui's would be judging a file the
175+
// step never loads.
176+
const root = `jobs:
177+
j:
178+
steps:
179+
- name: The root's own gate
180+
run: node scripts/probe.mjs
181+
- name: Through an alias
182+
run: pnpm --dir ui run probe:alias
183+
- name: Directly
184+
run: node ui/scripts/direct.mjs
185+
`;
186+
const rows = derivePreInstallSteps([{ file: 'root.yml', text: root }], {
187+
aliases: { 'probe:alias': 'node scripts/aliased.mjs --flag' },
188+
}) as Array<{ script: string; alias: string | null }>;
189+
expect(rows.map((r) => [r.script, r.alias])).toEqual([
190+
['scripts/aliased.mjs', 'probe:alias'],
191+
['scripts/direct.mjs', null],
192+
]);
193+
});
194+
166195
it('parses jobs and steps in order — the index is what "before" means', () => {
167196
const jobs = parseWorkflowJobs(fixture(2)) as Array<{ id: string; steps: Array<{ name: string }> }>;
168197
expect(jobs.map((j) => j.id)).toEqual(['probe']);
@@ -254,23 +283,32 @@ describe('the real tree — every step objectui#6148 measured is still derived',
254283
const rows = new Set(result.steps.map((s) => `${s.workflow} : ${s.job} -> ${s.script}`));
255284

256285
/**
257-
* The table objectui#6148 measured, plus `lint.yml`'s entry-guard step, which
258-
* the card named separately. A FLOOR, not an inventory: the assertion is that
259-
* none of these silently leaves the population. The gate's own `--list` is
260-
* where the current total lives, and pinning a total here would drift.
286+
* The steps of objectui#6148's table that still run, plus the root `Lint UI`
287+
* job's other two pre-install gates. A FLOOR, not an inventory: the assertion
288+
* is that none of these silently leaves the population. The gate's own
289+
* `--list` is where the current total lives, and pinning a total here would
290+
* drift.
291+
*
292+
* Since objectstack#25712 the rows are the repository root's `ci.yml`, the
293+
* workflow GitHub runs. Two of objectui#6148's rows run there:
294+
* `ci.yml : type-check` is now `type-check-ui`, and `lint.yml : lint` is now
295+
* `lint-ui` (roster rows 7 and 16 of objectstack#25697). The others ran in
296+
* workflows nothing runs since the monorepo merge:
297+
*
298+
* - `changeset-guard.yml : no-major`, `changeset-presence.yml :
299+
* changeset-presence` and `ci.yml : changeset-check`: roster rows 3, 4
300+
* and 7 (→ sub-issue 4 of objectstack#25677);
301+
* - the four `repo-gates.yml : repo-gates` rows (`check-control-bytes`,
302+
* `check-doc-component-types`, `check-doc-links`, `check-skills-paths`):
303+
* objectstack#25880 ports those gates into `lint-ui` ahead of its
304+
* install, and they rejoin this floor under that job when
305+
* `ui/.github/workflows/` is deleted (objectstack#25712).
261306
*/
262307
const MEASURED = [
263-
'changeset-guard.yml : no-major -> scripts/check-changeset-no-major.mjs',
264-
'changeset-presence.yml : changeset-presence -> scripts/check-changeset-presence.mjs',
265-
'ci.yml : changeset-check -> scripts/check-changeset-fixed.mjs',
266-
'ci.yml : type-check -> scripts/check-type-check-coverage.mjs',
267-
// objectui#12214 folded these four (and eleven more) into `repo-gates.yml`,
268-
// ahead of that job's install step, so they stay in the population.
269-
'repo-gates.yml : repo-gates -> scripts/check-control-bytes.mjs',
270-
'repo-gates.yml : repo-gates -> scripts/check-doc-component-types.mjs',
271-
'repo-gates.yml : repo-gates -> scripts/check-doc-links.mjs',
272-
'repo-gates.yml : repo-gates -> scripts/check-skills-paths.mjs',
273-
'lint.yml : lint -> scripts/check-entry-guard.mjs',
308+
'ci.yml : type-check-ui -> scripts/check-type-check-coverage.mjs',
309+
'ci.yml : lint-ui -> scripts/check-lint-coverage.mjs',
310+
'ci.yml : lint-ui -> scripts/check-entry-guard.mjs',
311+
'ci.yml : lint-ui -> scripts/check-bash32-floor.mjs',
274312
];
275313

276314
it.each(MEASURED)('still derives %s', (row) => {
@@ -285,9 +323,27 @@ describe('the real tree — every step objectui#6148 measured is still derived',
285323
expect(result.modules.length).toBeGreaterThan(result.scripts.length);
286324
});
287325

288-
it('is in its own population — the gate walks its own import graph', () => {
326+
it('holds its own import graph to the floor — the gate walks itself', () => {
289327
// A floor that exempted its own enforcer would be the first thing to rot.
290-
expect(result.scripts).toContain(SCRIPT);
328+
// Its home was objectui's `repo-gates.yml`, which runs nowhere since the
329+
// monorepo merge, so it is not in its own DERIVED population until the root
330+
// `Lint UI` job runs it (objectstack#25712 adds that step when it deletes
331+
// `ui/.github/workflows/`). Until then it is walked by name.
332+
const read = (rel: string): string | null => {
333+
try {
334+
return fs.readFileSync(path.join(repoRoot, rel), 'utf8');
335+
} catch {
336+
return null;
337+
}
338+
};
339+
const graph = walkImportGraph(SCRIPT, { read }) as {
340+
modules: string[];
341+
violations: unknown[];
342+
unresolved: unknown[];
343+
};
344+
expect(graph.modules).toContain('scripts/invoked-as.mjs');
345+
expect(graph.violations).toEqual([]);
346+
expect(graph.unresolved).toEqual([]);
291347
});
292348

293349
it('finds no pre-install gate reaching a package', () => {
@@ -523,10 +579,12 @@ describe("the header's population is a pointer, not a copy", () => {
523579
expect(prose, `${WORKFLOW}'s header must name \`${dir}\` — the population's one live source`).toContain(dir);
524580

525581
// …and the pointer must point at something. A named directory that holds no
526-
// workflows is a measurement point in form only.
527-
const present = fs.readdirSync(path.join(repoRoot, dir)).filter((f) => /\.ya?ml$/.test(f));
582+
// workflows is a measurement point in form only. The gate resolves it
583+
// against the repository root (objectstack#25712), so this does too, and
584+
// the root `ci.yml`, whose UI jobs carry the population, must be there.
585+
const present = fs.readdirSync(path.join(rootWorkflowRoot, dir)).filter((f) => /\.ya?ml$/.test(f));
528586
expect(present.length, `${dir} holds no workflow files — the pointer above resolves to nothing`).toBeGreaterThan(0);
529-
expect(present).toContain(WORKFLOW);
587+
expect(present).toContain('ci.yml');
530588
});
531589

532590
/**

0 commit comments

Comments
 (0)