Skip to content

Commit 17c5e68

Browse files
test(ui-merge): the governance pins on objectui's workflows retire to their roster rows and root gates; the Node anchor reads the root workflows (#25940)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoAqVfFyhsxKuFcEP9YTBj25)_ Part of #25712 (4b PR 2d-2b: the last `ui/scripts/__tests__` files that scan `ui/.github/workflows/` as a population stop reading it). Part of objectstack#25677. Clause-②: no Metadata-protocol: none ## 维护者速读(草稿) - **改了什么:** 2d-2a(#25904)之后,还剩 8 个测试文件把嵌套的 `ui/.github/workflows/` 当作一个总体来扫描。这个目录在 monorepo 里不运行。 - `doc-version-claims` 和 `check-lockfile-dedupe` 的扫描改成读根目录的 `.github/workflows/`。 - `ci-setup-pnpm-wiring` 改为断言根目录 4 个 UI job 都通过根目录的 `./.github/actions/setup-pnpm` 安装 pnpm。 - `dependabot-merge-gate`、`merge-queue-reporting`、`check-lockfile-integrity` 里读 objectui workflow 的 pin 退役,每条都写明由谁承接:路线表的行号,或者根目录现在承担这件事的 gate。其余用例保留。 - `workflow-cache-save-bound` 和 `labeler-package-coverage` 两个文件整个删除,因为里面每个用例都读 objectui 的 workflow 或 labeler 配置。 - 按 dispatcher 对 F12 的裁定 **B**,根目录 `labeler.yml` 不加 `ui/` 规则(原因见下)。 - **为什么改:** 2d-3 要删除 `ui/.github/workflows/`,删之前这 8 个文件必须先不依赖它。改完以后,无论 `ui/.github/workflows/` 在不在,保留和修改的 6 个文件都是绿的(本地两种布局都是 96 个用例通过)。 - **风险与代价(含回滚):** 只动测试和一个测试 ledger,不动任何会运行的代码、workflow 或受管面。退役的 pin 都写明了承接方。回滚 = revert 本 PR。 - **席位意见:**(留空) - **你要做的:** 无。Tier S,由席位落地。 ## 席位核对 (left for the dispatch seat) ## Why GitHub runs only the repository root's `.github/workflows/`. Since the cutover (#25825), objectui's workflows sit at `ui/.github/workflows/` and run nowhere. 2d-1 (#25879) moved the readers of `ui/.github/workflows/ci.yml`. 2d-2a (#25904) took 18 of the 27 files that scan the workflow directory as a population. This PR takes the other 8, the governance files. `check-pre-install-import-graph` stays with 2d-3, because its gate, not just its test, derives its population from the nested workflows. The rule is the same as in 2d-1 and 2d-2a. A pin is either retargeted to the root workflows by path, or retired with a comment naming who carries its subject. Nothing is skipped and nothing is folded into a root workflow. ## What changes - **`dependabot-merge-gate`.** Three groups of pins read objectui's `pull_request` workflows through `./workflow-checks.ts`: the bucket partition, the exemption control for `RENAMED_SINCE_4959`, and the six pins on `dependabot-auto-merge.yml`. They retire as roster **row 9** (→ sub-issue 4). The gate's one caller is that workflow, which runs nowhere, and no root workflow merges Dependabot pull requests. Everything that judges a check-run list stays: the #4959 counterfactual, absence, the optional bucket, `waitForGate`, `main()`, the verdict text, the check-runs-only boundary, and the reasons. - **`merge-queue-reporting`.** The #3523 step 1 pins, the floor derived from `REQUIRED_CONTEXTS` (`deriveMergeGroupFloor()` and its three cases) and the step 2 pins all read objectui's workflows. The root gates already carry both #3523 holes, for every context the repository requires today: `scripts/check-required-contexts.mjs` (`check:required-contexts`) holds each required context's workflow to a `merge_group:` trigger (assertion 6) and to a `pull_request` trigger with no path filter (assertion 7), and `check:workflow-status-functions` pins the filter contract's status-function half. Both run in `Lint · CI, release & engine gates`. The CI page describe stays and now scans `MUST_SUBSCRIBE_MERGE_GROUP` alone. - **`check-lockfile-dedupe`, `check-lockfile-integrity`.** The wiring pins on objectui's `lockfile-dedupe.yml` and `lockfile-integrity.yml` retire as roster **rows 18 and 19** (kept, follow-up port, re-aimed at the one root `pnpm-lock.yaml`). The dedupe test's live-reading scan now reads the root workflows through `helpers/root-ci.ts`, and `LIVE_READING_SITES` drops the nested workflow. The population controls and the classification cases stay. - **`ci-setup-pnpm-wiring`.** Three population cases swept objectui's workflows for calls to `scripts/ci-setup-pnpm.sh`, and they retire. A new describe asks the same questions of the four root `* UI` jobs, by path: no `corepack` outside the shared setup, and checkout before `./.github/actions/setup-pnpm` before the first `pnpm`. All four hold at this head. The script and its self-test stay; no root job calls the script. - **`doc-version-claims`.** The `node-version` anchor (`WORKFLOWS_DIR`) is now the root `.github/workflows/` (`rootWorkflowsDir`). All 56 root declarations read `'22'`. The census cross-check's key counter now skips whole-line YAML comments: root `half-state-patrol.yml:111` quotes a commented setup example, and counting it would read the parser's correct silence as a lost lane. - **Deleted: `workflow-cache-save-bound.test.ts` (11 cases) and `labeler-package-coverage.test.ts` (7 cases).** Every case read objectui's workflows or `ui/.github/labeler.yml`. Their carriers are in rows 7 and 8 of the table. - **`ui/scripts/markdown-test-inputs.mjs`.** It drops the deleted labeler test's entry, and the `walker:` of `merge-queue-reporting` (its `readdirSync(workflowDir)` is gone). ### F12 as ruled → B, labels absent The dispatcher first ruled that this PR should add the `ui/` globs to the root `.github/labeler.yml` and retarget `labeler-package-coverage` to it. Measuring first showed that this would turn root Auto Label red on every pull request touching `ui/packages`: - Root `Auto Label` (`pr-automation.yml`) is not `actions/labeler`. It is `node scripts/pr-labels.mjs --paths`, an additive POST with `pull-requests: write` only, and `pr-labels.mjs` treats any 4xx as fatal ("a `422` is a label that does not exist in the repo"). - 28 of the 32 labels in `ui/.github/labeler.yml` do not exist in this repository's label registry: - `package: core`, `package: types`, `package: react`, `package: components`, `package: app-shell`, `package: auth`, `package: cli`, `package: collaboration`, `package: create-plugin`, `package: fields`, `package: i18n`, `package: layout`, `package: mobile`, `package: permissions`, `package: providers`, `package: react-runtime`, `package: runner`, `package: sdui-parser`; - `plugin`, `plugin: charts`, `plugin: editor`, `plugin: kanban`, `plugin: markdown`; - `data-adapter`, `configuration`, `examples`, `apps`, `breaking-change`. Only `documentation`, `tests`, `dependencies` and `ci/cd` exist. The dispatcher re-ruled **B**: - no `ui/` globs go into the root labeler; - the test's pins retire as roster **row 14**, because the monorepo label taxonomy belongs to sub-issue 4; - `ui/.github/labeler.yml` stays as an inert config until sub-issue 4 rules, and 2d-3 lists it as "kept, pending sub-issue 4". ### Disposition table (8 files) | # | File | Retired | Kept / retargeted | Carried by now | |---|---|---|---|---| | 1 | `dependabot-merge-gate` | `found the workflows and the shard matrix`, `classifies every produced check exactly once`, `requires only contexts whose workflow carries NO path filter`, `marks optional exactly those blocking contexts whose workflow IS path-filtered`; the exemption-control half of `classifies every context the incident produced`; `the workflow cannot merge without consulting the gate` (6 cases on `dependabot-auto-merge.yml`) | the #4959 counterfactual (incident classification included), absence, optional bucket, `waitForGate`, `main()`, legibility, boundary, reasons | roster **row 9** (→ sub-issue 4) | | 2 | `merge-queue-reporting` | step 1 (3 cases), the derived floor (3 cases and `deriveMergeGroupFloor()`), step 2 (3 `it.each` over `ci.yml`/`lint.yml` and the 2 fail-OPEN cases) | the CI page describe (4 cases), scanning the map | root `check:required-contexts` assertions 6 and 7, and `check:workflow-status-functions` (`Lint · CI, release & engine gates`) | | 3 | `check-lockfile-dedupe` | has a workflow, `--report-only`, path-filtered, no `merge_group`, shared pnpm setup | live-reading scan **retargeted** to root `.github/workflows/`; population controls; classification | roster **row 18** (kept, follow-up port) | | 4 | `check-lockfile-integrity` | `exists and runs the gate`, `checks out deep enough to have a merge base` | classification | roster **row 19** (kept, follow-up port) | | 5 | `ci-setup-pnpm-wiring` | `no workflow runs corepack outside the shared script`, `every workflow that invokes pnpm also runs the shared setup`, `every job that calls the setup script checks the repository out first` | **retargeted:** the four root `* UI` jobs, by path, against `./.github/actions/setup-pnpm`; the script's own cases | root `type-check-ui`, `test-ui`, `lint-ui`, `build-e2e-ui` | | 6 | `doc-version-claims` | (none) | **retargeted:** the `node-version` anchor reads root `.github/workflows/`; the census skips comment lines | root `.github/workflows/` | | 7 | `workflow-cache-save-bound` (deleted) | the 7 population cases (split restore/save, bounded and non-fatal saves, save after the last command, crowding) | (none) | #25869 row F1 (bound the root ui jobs' saves the objectui way; F11, the combined pnpm-store cache, is F1's scope) | | | | ceiling pins: `ci.yml::e2e` ≤30, `lint.yml::lint` 25 | | root `build-e2e-ui` (ceiling 45) and `lint-ui` (ceiling 30); see follow-up F13 | | | | `ci.yml::docs` ≤15, `live-e2e.yml` ≤40, `performance-budget.yml` 25 | | roster rows **7** (Build Docs), **17**, **22** (kept, follow-up port) | | | | `check-links.yml` 20, `cross-repo-issue-closer.yml` 20, `labeler.yml` 20, `changelog.yml` unbounded | | roster rows **6**, **8**, **14**, **2** (→ sub-issue 4) | | 8 | `labeler-package-coverage` (deleted) | all 7 (lit control, every package labelled, no dangling rule, `package: X` spelling, exemption lists, docs tree, `issues: write`) | (none); `ui/.github/labeler.yml` stays, inert | roster **row 14** (→ sub-issue 4); F12 → B, above | ### Follow-up rows (not folded here) - **F13.** The root `lint-ui` (`timeout-minutes: 30`) and `build-e2e-ui` (`45`) ceilings came over without the derivations objectui wrote beside its 25 and 30, and nothing pins them. #25869's F1 seat touches the same jobs. - **F14.** The CI page's `## Merge Queue` section (`ui/content/docs/guide/ci-cd-pipeline.md`) still says `merge-queue-reporting.test.ts` derives the floor from `REQUIRED_CONTEXTS`. This PR retires that derivation. The page as a whole describes objectui's CI. It is for the dispatcher to route. - **F15.** `ui/scripts/dependabot-merge-gate.mjs` has no caller, and its `REQUIRED_CONTEXTS` names objectui's check names. Eight other test files still import it as a classification source (`check-required-check-set`, `check-governed-queue-guard` and `check-readme-exports` among them). Sub-issue 4 decides whether to port it or delete it. ### What 2d-3 still owes (not here) - `check-pre-install-import-graph`, `check-action-ref-convention` (its gate scans the nested workflow directory), `check-merge-queue-head` and `workflow-checks.ts`, and the 24 single-workflow readers. - It moves the three `ui/.github/prompts/*.prompt.md` files to root `.github/prompts/`, with a separate prompts root for `check:prompt-keys`. - It deletes `ui/.github/dependabot.yml`, and deletes `ui/.github/workflows/**` once #25880 lands. - It lists `ui/.github/labeler.yml` as kept, pending sub-issue 4. ## Verification This head is `main` `2a728ba2` (with #25904) plus this commit. The test runs below were taken on `main` `cb0a2fcf` with 2d-2a and this commit applied. That tree's `ui/` and `scripts/pm/` are byte-identical to this head's: the only commit between them is #25830, which touches neither. The derived gates were re-run on this head. Test runs are from `ui/`, per `ui/AGENTS.md`. | Run | Result | |---|---| | The 6 modified test files, `ui/.github/workflows/` present | 6 files, 96 tests, all passed | | The same 6 with `ui/.github/workflows/` moved aside (the 2d-3 layout) | 6 files, 96 tests, all passed | | Whole `scripts/__tests__` suite, `--shard=1/4` through `4/4`, under `scripts/pm/os-verify-lock.sh` | 5,370 passed, 1 failed, 2 skipped | | `pnpm type-check:scripts` | the one error is `packages/components/src/renderers/action/resolve-icon.ts(265,69)` TS2339, identical on unmodified `main` here; none in a touched file | | `pnpm exec eslint --max-warnings=0` (touched files), `lint:coverage`, `check:test-path-roots`, `check:vi-mock-override-shape`; root `pnpm check:test-source-alias` | all exit 0 | The one suite failure is the base red riding `main`: `check-pre-install-import-graph`, `unresolved: scripts/objectstack-pin.mjs`. Main's push run 40264138159 is red on the same row. It clears in 2d-3 with the nested workflows. Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`): 18 families from this PR's 9 paths, 1830 changed lines. All 18 exit 0 at this head, and `--ran` reconciles 18 run, 0 NOT-MEASURED, 0 UNRUN. The same 18 also exited 0 on the `cb0a2fcf` tree. The slowest were `check:query-options-erasure` (567 s), `check-comment-mask-corpus` (326 s) and `check:engine-double-contract` (140 s), measured on a shared box. Push-environment battery (`GITHUB_ACTIONS=true GITHUB_EVENT_NAME=push node scripts/pm/check-dispatch-gates.mjs`, both tiers): "✓ dispatch-gates self-test: 2182 cases pass." (fast 1917 in 403.6 s, slow 265 in 1124.4 s). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS Co-authored-by: Claude <noreply@anthropic.com>
1 parent de2b7e0 commit 17c5e68

9 files changed

Lines changed: 175 additions & 1655 deletions

‎ui/scripts/__tests__/check-lockfile-dedupe.test.ts‎

Lines changed: 38 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ import { DEDUPE_SENTINEL } from '../check-lockfile-dedupe.mjs';
118118
import { FLOORS, TEST_FILE } from '../check-test-path-roots.mjs';
119119
import { NOT_A_GATE, OPTIONAL_CONTEXTS, REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs';
120120
import { maskComments } from '../js-comment-mask.mjs';
121-
import { pullRequestTrigger, readWorkflows, repoRoot, subscribesMergeGroup } from './workflow-checks.js';
121+
import { rootWorkflow, rootWorkflowFiles, rootWorkflowsDir, withoutComments } from './helpers/root-ci';
122+
import { repoRoot } from './workflow-checks.js';
122123

123124
const SCRIPT = 'scripts/check-lockfile-dedupe.mjs';
124-
const WORKFLOW = 'lockfile-dedupe.yml';
125125
const CONTEXT = 'Lockfile Dedupe Check';
126126

127127
/** This file, by its own path — a rename may not silently drop what it pins. */
@@ -177,16 +177,19 @@ interface LiveScan {
177177
*
178178
* Two carriers, one population (objectui#9693): the workflows, which say what a
179179
* job runs, and the files those jobs execute — every tracked test file plus the
180-
* vitest runtime modules. Workflow comment LINES are stripped by `readWorkflows`;
181-
* source comments are blanked by `scripts/js-comment-mask.mjs`, the tree's one
182-
* answer to "comment, or code?", so the paragraphs in this very file explaining
183-
* the hazard cannot count as the hazard.
180+
* vitest runtime modules. The workflows are the root `.github/workflows/`, the
181+
* only ones GitHub runs under the monorepo (objectstack#25712), read with their
182+
* comment LINES stripped; `ui/.github/workflows/` runs nowhere, so a live
183+
* reading there is no reading at all. Source comments are blanked by
184+
* `scripts/js-comment-mask.mjs`, the tree's one answer to "comment, or code?",
185+
* so the paragraphs in this very file explaining the hazard cannot count as the
186+
* hazard.
184187
*/
185188
function scanForLiveReading(): LiveScan {
186-
const workflows = readWorkflows();
189+
const workflows = rootWorkflowFiles();
187190
const sites = workflows
188-
.filter((workflow) => takesLiveReading(workflow.lines.join('\n')))
189-
.map((workflow) => `.github/workflows/${workflow.file}`);
191+
.filter((file) => takesLiveReading(withoutComments(rootWorkflow(file))))
192+
.map((file) => path.relative(repoRoot, path.join(rootWorkflowsDir, file)));
190193

191194
const population = trackedFiles().filter((file) => TEST_FILE.test(file) || VITEST_RUNTIME.test(file));
192195
for (const file of population) {
@@ -208,16 +211,22 @@ function scanForLiveReading(): LiveScan {
208211
/**
209212
* The only places a live reading may be taken, and why each may:
210213
*
211-
* - the path-filtered, non-required workflow this gate was built around;
212214
* - this file, which drives the checker ONLY through the `PATH` stub above —
213215
* every run asserts `pnpmArgv`, so a leg that reached the real pnpm reds
214216
* rather than going quiet, which is what makes this exemption checkable
215217
* instead of trusted.
216218
*
219+
* The second entry this list carried, objectui's path-filtered
220+
* `lockfile-dedupe.yml`, left with objectstack#25712: it sits under
221+
* `ui/.github/workflows/`, which runs nowhere. It is row 18 of the
222+
* objectstack#25697 workflow roster, "kept, follow-up port", and the port adds
223+
* its root workflow back here. Until then the equality below has no workflow
224+
* member, so its workflow half rests on the read floor alone.
225+
*
217226
* ⛔ Nothing else belongs here. An addition is a live, registry-dependent
218227
* reading inside a REQUIRED context — objectui#9562, again.
219228
*/
220-
const LIVE_READING_SITES = [`.github/workflows/${WORKFLOW}`, THIS_FILE].sort();
229+
const LIVE_READING_SITES = [THIS_FILE];
221230

222231
/**
223232
* Real `pnpm@10.31.0` output, same capture objectui#8333 took and the checker's
@@ -499,23 +508,21 @@ describe('--report-only — the mode objectui#9562 ruled (letter A, comment 5717
499508
});
500509
});
501510

502-
describe('the lockfile-dedupe gate is wired the way its header claims', () => {
503-
const workflows = readWorkflows();
504-
const workflow = workflows.find((w) => w.file === WORKFLOW);
505-
/** The workflow with comment lines stripped — prose may not satisfy wiring. */
506-
const body = () => workflow!.lines.join('\n');
507-
508-
it('has a workflow that runs the checker', () => {
509-
expect(workflow, `${WORKFLOW} not found — the gate would never run`).toBeDefined();
510-
// Comment-stripped throughout: a header comment mentioning the script must
511-
// not be able to satisfy a wiring assertion about what the job RUNS.
512-
expect(body()).toContain(`node ${SCRIPT}`);
513-
expect(body()).toContain(`name: ${CONTEXT}`);
514-
});
511+
describe('the lockfile-dedupe gate is fenced and classified the way its header claims', () => {
512+
// Retired with objectstack#25712: `has a workflow that runs the checker`, the
513+
// `--report-only` pin, `is path-filtered, and the filter lists the gate’s own
514+
// runtime closure`, `declares no merge_group trigger` and `uses the shared
515+
// pnpm setup rather than a bare corepack call`. Each read objectui's
516+
// `lockfile-dedupe.yml`, which runs nowhere under the monorepo: GitHub runs
517+
// the root `.github/workflows/` alone. That workflow is row 18 of the
518+
// objectstack#25697 workflow roster, "kept, follow-up port", re-aimed at the
519+
// one root `pnpm-lock.yaml`; the port brings these pins back against its root
520+
// workflow, by path.
515521

516522
it('is the only place the LIVE reading runs, in workflows AND in what they run (objectui#9562, objectui#9693)', () => {
517-
// The live, registry-dependent execution belongs to one path-filtered
518-
// workflow and nowhere else. ⭐ "Nowhere else" is the whole assertion, and
523+
// The live, registry-dependent execution belongs to this file's stubbed legs
524+
// and, once roster row 18 is ported, to one path-filtered root workflow:
525+
// nowhere else. ⭐ "Nowhere else" is the whole assertion, and
519526
// it used to be measured over the workflows alone — while the way a
520527
// required job actually grew a `pnpm dedupe`, the time it happened, was
521528
// through a TEST FILE, which is not a workflow. So the population is both
@@ -531,14 +538,15 @@ describe('the lockfile-dedupe gate is wired the way its header claims', () => {
531538
expect(scan.workflows, 'the workflow read collapsed').toBeGreaterThan(20);
532539
expect(scan.population.length, 'the source read collapsed').toBeGreaterThan(FLOORS.testFiles);
533540
// …and it reached this file's PEERS and the runtime modules, not just the
534-
// two files the expectation names — a scan that only ever looked at itself
541+
// file the expectation names — a scan that only ever looked at itself
535542
// would satisfy the equality below exactly as well.
536543
expect(scan.population.filter((f) => f.startsWith('scripts/__tests__/') && f !== THIS_FILE).length).toBeGreaterThan(50);
537544
expect(scan.population).toContain('vitest.setup.base.ts');
538545

539-
// The equality is also the positive control for both halves: one member is
540-
// a workflow and the other a test file, so a carrier that stopped being
541-
// read goes red here rather than going quiet.
546+
// The equality was also the positive control for both halves while one
547+
// member was a workflow and the other a test file. Until the row-18 port
548+
// restores the workflow member, the workflow half rests on the floor above
549+
// (see `LIVE_READING_SITES`).
542550
expect(scan.sites, 'a live, registry-dependent reading inside a REQUIRED context — objectui#9562, again').toEqual(
543551
LIVE_READING_SITES,
544552
);
@@ -568,37 +576,6 @@ describe('the lockfile-dedupe gate is wired the way its header claims', () => {
568576
expect(takesLiveReading(maskComments('const dedupe = new Set<string>();'))).toBe(false);
569577
});
570578

571-
it('passes `--report-only`, which is where objectui#9562’s ruling actually lands', () => {
572-
// Comment-stripped: the header explains the mode at length, and prose may
573-
// not satisfy wiring. The flag on the step is the entire deliverable — the
574-
// script supports both modes, so the workflow is what chooses one.
575-
expect(body(), 'the gate is blocking again — objectui#9562, undone').toContain(
576-
`node ${SCRIPT} --report-only`,
577-
);
578-
});
579-
580-
it('is path-filtered, and the filter lists the gate’s own runtime closure', () => {
581-
const trigger = pullRequestTrigger(workflow!);
582-
expect(trigger.subscribes, 'the gate must run on pull requests').toBe(true);
583-
expect(trigger.filtered, 'a path filter is what keeps it out of the REQUIRED set').toBe(true);
584-
// Without these the checker can ship having never run once, and the wiring
585-
// bug then surfaces on somebody else's next lockfile pull request.
586-
for (const file of ['pnpm-lock.yaml', `.github/workflows/${WORKFLOW}`, SCRIPT, 'scripts/invoked-as.mjs']) {
587-
expect(body(), `${file} missing from the path filter`).toContain(file);
588-
}
589-
});
590-
591-
it('declares no merge_group trigger, per objectui#3523', () => {
592-
// A path-filtered, non-required gate that subscribes the queue stalls it
593-
// until the ruleset timeout.
594-
expect(subscribesMergeGroup(workflow!)).toBe(false);
595-
});
596-
597-
it('uses the shared pnpm setup rather than a bare corepack call', () => {
598-
expect(body()).toContain('bash scripts/ci-setup-pnpm.sh');
599-
expect(body()).not.toMatch(/^\s*run:.*corepack/m);
600-
});
601-
602579
it('stays an OPTIONAL context, because the JOB still gates even though its verdict does not', () => {
603580
// ⚠️ The load-bearing assertion, and objectui#9562 did NOT move it: the
604581
// ruling relaxed the VERDICT, not the job. A failed checkout, a broken

‎ui/scripts/__tests__/check-lockfile-integrity.test.ts‎

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import path from 'node:path';
2424
import { describe, expect, it } from 'vitest';
2525
import { lockfilePath } from '../workspace-root.mjs';
2626

27-
import { readWorkflows, repoRoot } from './workflow-checks';
27+
import { repoRoot } from './workflow-checks';
2828
import { NOT_A_GATE, OPTIONAL_CONTEXTS, REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs';
2929
import {
3030
compareLockfiles,
@@ -190,19 +190,14 @@ describe('cause 2 — a declared dependency forks with no version moving (object
190190
});
191191
});
192192

193-
describe('the gate is wired, and wired the way the dispatch reserved', () => {
194-
const workflow = readWorkflows().find((w) => w.file === 'lockfile-integrity.yml');
195-
196-
it('exists and runs the gate', () => {
197-
expect(workflow, '.github/workflows/lockfile-integrity.yml is missing').toBeTruthy();
198-
expect(workflow?.text).toContain('node scripts/check-lockfile-integrity.mjs');
199-
});
200-
201-
it('checks out deep enough to have a merge base', () => {
202-
// A shallow checkout has no merge base and the script exits 2 rather than
203-
// guessing — which would be a check that reports nothing and looks fine.
204-
expect(workflow?.text).toContain('fetch-depth: 0');
205-
});
193+
describe('the gate is classified the way the dispatch reserved', () => {
194+
// `exists and runs the gate` and `checks out deep enough to have a merge
195+
// base` retired with objectstack#25712. They read objectui's
196+
// `lockfile-integrity.yml`, which runs nowhere under the monorepo: GitHub
197+
// runs the root `.github/workflows/` alone. That workflow is row 19 of the
198+
// objectstack#25697 workflow roster, "kept, follow-up port", re-aimed at the
199+
// one root `pnpm-lock.yaml`; the port brings both pins back against the root
200+
// job that runs the gate.
206201

207202
it('⛔ is classified as NOT a blocking context (the maintainer floor)', () => {
208203
expect(Object.keys(NOT_A_GATE)).toContain(CHECK_NAME);

‎ui/scripts/__tests__/ci-setup-pnpm-wiring.test.ts‎

Lines changed: 29 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import fs from 'node:fs';
44
import path from 'node:path';
55
import { fileURLToPath } from 'node:url';
66

7+
import { jobBlock, rootCi, UI_JOBS, withoutComments } from './helpers/root-ci';
8+
79
const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..');
8-
const WORKFLOW_DIR = path.join(ROOT, '.github', 'workflows');
910
const SETUP = 'scripts/ci-setup-pnpm.sh';
1011

1112
/**
@@ -62,15 +63,6 @@ const SETUP = 'scripts/ci-setup-pnpm.sh';
6263
* which is the failure mode this repository has recorded more than once.
6364
*/
6465

65-
/** Every workflow file, as `{ file, yaml }`. */
66-
function workflows(): { file: string; yaml: string }[] {
67-
return fs
68-
.readdirSync(WORKFLOW_DIR)
69-
.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
70-
.sort()
71-
.map((file) => ({ file, yaml: fs.readFileSync(path.join(WORKFLOW_DIR, file), 'utf8') }));
72-
}
73-
7466
/**
7567
* Every `run:` step body in a piece of workflow YAML — single-line and block
7668
* scalar alike, and nothing else.
@@ -119,89 +111,38 @@ describe('the shared CI pnpm setup is wired everywhere pnpm is used', () => {
119111
expect(run.stdout).toContain('self-test OK');
120112
});
121113

122-
it('no workflow runs corepack outside the shared script', () => {
123-
const files = workflows();
124-
expect(files.length, 'no workflow files found — this whole file would be vacuous').toBeGreaterThan(0);
125-
126-
const offenders = files
127-
.filter(({ yaml }) => runSteps(yaml).some((s) => /\bcorepack\b/.test(s)))
128-
.map(({ file }) => file);
129-
130-
expect(
131-
offenders,
132-
'these workflows invoke corepack directly instead of `bash ' +
133-
SETUP +
134-
'`. The download it performs is a registry read that has reddened a REQUIRED check ' +
135-
'before any gate logic ran (objectui#8099); route it through the shared script so the ' +
136-
'retry and the self-describing step name apply here too.',
137-
).toEqual([]);
138-
139-
// Anti-vacuity for the reader above: the comment-only mentions must still be
140-
// THERE, or this assertion is passing because the corpus emptied out.
141-
const commentOnly = files.filter(
142-
({ yaml }) => /\bcorepack\b/.test(yaml) && !runSteps(yaml).some((s) => /\bcorepack\b/.test(s)),
143-
);
144-
expect(
145-
commentOnly.map((w) => w.file),
146-
'the two comment-only corepack mentions must stay visible to this scan and stay unjudged — ' +
147-
'if they are gone, re-derive whether this rule still reads what it claims to read',
148-
).toEqual(['changelog.yml', 'dependabot-auto-merge.yml']);
149-
});
150-
151-
it('every workflow that invokes pnpm also runs the shared setup', () => {
152-
const files = workflows();
153-
154-
const usesPnpm = files.filter(({ yaml }) => runSteps(yaml).some((s) => /\bpnpm\b/.test(s)));
155-
expect(
156-
usesPnpm.length,
157-
'no workflow appears to invoke pnpm, which cannot be true — the step reader has drifted',
158-
).toBeGreaterThan(0);
114+
// `no workflow runs corepack outside the shared script`, `every workflow that
115+
// invokes pnpm also runs the shared setup` and `every job that calls the
116+
// setup script checks the repository out first` retired with
117+
// objectstack#25712. They swept objectui's workflows under
118+
// `ui/.github/workflows/` for calls to this script, and those run nowhere
119+
// under the monorepo: GitHub runs the root `.github/workflows/` alone. What
120+
// objectui's CI became is the four root `* UI` jobs (objectstack#25697),
121+
// which set pnpm up through the root composite action
122+
// `./.github/actions/setup-pnpm`, objectstack's answer to the same Corepack
123+
// fetch. The describe below asks the same three questions of those jobs, by
124+
// path. This script and its self-test stay; no root job calls it.
125+
});
159126

160-
const missing = usesPnpm
161-
.filter(({ yaml }) => !runSteps(yaml).some((s) => s.includes(SETUP)))
162-
.map(({ file }) => file);
127+
describe('the root ui jobs set pnpm up through the root shared setup', () => {
128+
const SHARED = 'uses: ./.github/actions/setup-pnpm';
129+
const jobs = Object.values(UI_JOBS).map((key) => ({ key, block: withoutComments(jobBlock(rootCi(), key)) }));
163130

164-
expect(
165-
missing,
166-
`these workflows run pnpm without calling \`bash ${SETUP}\` first. pnpm is not on the runner: ` +
167-
'the first invocation downloads it from registry.npmjs.org, and unrouted that download lands ' +
168-
"inside whichever step happens to touch pnpm first — including `actions/setup-node`'s own " +
169-
'cache probe, where a red is hardest to attribute (objectui#8099).',
170-
).toEqual([]);
131+
it.each(jobs)('$key runs no corepack outside the shared setup', ({ block }) => {
132+
expect(runSteps(block).filter((s) => /\bcorepack\b/.test(s))).toEqual([]);
171133
});
172134

173-
it('every job that calls the setup script checks the repository out first', () => {
174-
const files = workflows().filter(({ yaml }) => yaml.includes(SETUP));
175-
expect(files.length, `no workflow references ${SETUP} — the sweep was undone`).toBeGreaterThan(0);
176-
177-
const bad: string[] = [];
178-
for (const { file, yaml } of files) {
179-
const lines = yaml.split('\n');
180-
// Job blocks: two-space keys inside the `jobs:` mapping.
181-
const jobsAt = lines.findIndex((l) => /^jobs:[ \t]*$/.test(l));
182-
expect(jobsAt, `${file} must have a top-level \`jobs:\` mapping`).toBeGreaterThan(-1);
183-
184-
const starts: number[] = [];
185-
for (let i = jobsAt + 1; i < lines.length; i++) {
186-
if (/^ {2}[a-z0-9][a-z0-9-]*:[ \t]*$/.test(lines[i])) starts.push(i);
187-
}
188-
starts.push(lines.length);
189-
190-
for (let s = 0; s + 1 < starts.length; s++) {
191-
const block = lines.slice(starts[s], starts[s + 1]);
192-
const setupAt = block.findIndex((l) => l.includes(SETUP));
193-
if (setupAt === -1) continue;
194-
const checkoutAt = block.findIndex((l) => /uses:\s*actions\/checkout@/.test(l));
195-
if (checkoutAt === -1 || checkoutAt > setupAt) {
196-
bad.push(`${file} :: ${block[0].trim()}`);
197-
}
198-
}
199-
}
135+
it.each(jobs)('$key checks the repository out, then sets pnpm up, before its first pnpm', ({ block }) => {
136+
const checkoutAt = block.search(/uses:\s*actions\/checkout@/);
137+
const setupAt = block.indexOf(SHARED);
138+
expect(checkoutAt, 'the job checks nothing out').toBeGreaterThan(-1);
139+
expect(setupAt, `the job has no \`${SHARED}\` step after its checkout`).toBeGreaterThan(checkoutAt);
200140

141+
const firstPnpm = runSteps(block).find((s) => /\bpnpm\b/.test(s));
142+
expect(firstPnpm, 'the job runs no pnpm at all, so this reading has drifted').toBeDefined();
201143
expect(
202-
bad,
203-
`${SETUP} is a file in this repository, so a job that runs it before \`actions/checkout\` ` +
204-
'dies with "No such file or directory" — a failure that reads nothing like a toolchain problem.',
205-
).toEqual([]);
144+
block.indexOf(firstPnpm!.split('\n')[0]),
145+
'the job runs pnpm before the shared setup has put it on the runner',
146+
).toBeGreaterThan(setupAt);
206147
});
207148
});

0 commit comments

Comments
 (0)