@@ -14,6 +14,7 @@ import {
1414 scan ,
1515 staticSpecifiers ,
1616 walkImportGraph ,
17+ workflowRoot ,
1718} from '../check-pre-install-import-graph.mjs' ;
1819import { 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 */
6062const repoRoot = path . resolve ( path . dirname ( fileURLToPath ( import . meta. url ) ) , '../..' ) ;
6163const workflowDir = path . join ( repoRoot , '.github/workflows' ) ;
64+ /** Where the gate reads `.github/workflows/`: the repository root, which GitHub runs. */
65+ const rootWorkflowRoot = workflowRoot ( repoRoot ) ;
6266const SCRIPT = 'scripts/check-pre-install-import-graph.mjs' ;
6367const WORKFLOW = 'repo-gates.yml' ;
6468const CHECK_NAME = 'Pre-Install Import Graph Check' ;
@@ -76,7 +80,7 @@ const readWorkflow = (file: string): string => fs.readFileSync(path.join(workflo
7680
7781describe ( '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 ) => / \. y a ? m l $ / . 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 ) => / \. y a ? m l $ / . 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