Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 38 additions & 61 deletions ui/scripts/__tests__/check-lockfile-dedupe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ import { DEDUPE_SENTINEL } from '../check-lockfile-dedupe.mjs';
import { FLOORS, TEST_FILE } from '../check-test-path-roots.mjs';
import { NOT_A_GATE, OPTIONAL_CONTEXTS, REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs';
import { maskComments } from '../js-comment-mask.mjs';
import { pullRequestTrigger, readWorkflows, repoRoot, subscribesMergeGroup } from './workflow-checks.js';
import { rootWorkflow, rootWorkflowFiles, rootWorkflowsDir, withoutComments } from './helpers/root-ci';
import { repoRoot } from './workflow-checks.js';

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

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

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

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

describe('the lockfile-dedupe gate is wired the way its header claims', () => {
const workflows = readWorkflows();
const workflow = workflows.find((w) => w.file === WORKFLOW);
/** The workflow with comment lines stripped — prose may not satisfy wiring. */
const body = () => workflow!.lines.join('\n');

it('has a workflow that runs the checker', () => {
expect(workflow, `${WORKFLOW} not found — the gate would never run`).toBeDefined();
// Comment-stripped throughout: a header comment mentioning the script must
// not be able to satisfy a wiring assertion about what the job RUNS.
expect(body()).toContain(`node ${SCRIPT}`);
expect(body()).toContain(`name: ${CONTEXT}`);
});
describe('the lockfile-dedupe gate is fenced and classified the way its header claims', () => {
// Retired with objectstack#25712: `has a workflow that runs the checker`, the
// `--report-only` pin, `is path-filtered, and the filter lists the gate’s own
// runtime closure`, `declares no merge_group trigger` and `uses the shared
// pnpm setup rather than a bare corepack call`. Each read objectui's
// `lockfile-dedupe.yml`, which runs nowhere under the monorepo: GitHub runs
// the root `.github/workflows/` alone. That workflow is row 18 of the
// objectstack#25697 workflow roster, "kept, follow-up port", re-aimed at the
// one root `pnpm-lock.yaml`; the port brings these pins back against its root
// workflow, by path.

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

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

it('passes `--report-only`, which is where objectui#9562’s ruling actually lands', () => {
// Comment-stripped: the header explains the mode at length, and prose may
// not satisfy wiring. The flag on the step is the entire deliverable — the
// script supports both modes, so the workflow is what chooses one.
expect(body(), 'the gate is blocking again — objectui#9562, undone').toContain(
`node ${SCRIPT} --report-only`,
);
});

it('is path-filtered, and the filter lists the gate’s own runtime closure', () => {
const trigger = pullRequestTrigger(workflow!);
expect(trigger.subscribes, 'the gate must run on pull requests').toBe(true);
expect(trigger.filtered, 'a path filter is what keeps it out of the REQUIRED set').toBe(true);
// Without these the checker can ship having never run once, and the wiring
// bug then surfaces on somebody else's next lockfile pull request.
for (const file of ['pnpm-lock.yaml', `.github/workflows/${WORKFLOW}`, SCRIPT, 'scripts/invoked-as.mjs']) {
expect(body(), `${file} missing from the path filter`).toContain(file);
}
});

it('declares no merge_group trigger, per objectui#3523', () => {
// A path-filtered, non-required gate that subscribes the queue stalls it
// until the ruleset timeout.
expect(subscribesMergeGroup(workflow!)).toBe(false);
});

it('uses the shared pnpm setup rather than a bare corepack call', () => {
expect(body()).toContain('bash scripts/ci-setup-pnpm.sh');
expect(body()).not.toMatch(/^\s*run:.*corepack/m);
});

it('stays an OPTIONAL context, because the JOB still gates even though its verdict does not', () => {
// ⚠️ The load-bearing assertion, and objectui#9562 did NOT move it: the
// ruling relaxed the VERDICT, not the job. A failed checkout, a broken
Expand Down
23 changes: 9 additions & 14 deletions ui/scripts/__tests__/check-lockfile-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { lockfilePath } from '../workspace-root.mjs';

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

describe('the gate is wired, and wired the way the dispatch reserved', () => {
const workflow = readWorkflows().find((w) => w.file === 'lockfile-integrity.yml');

it('exists and runs the gate', () => {
expect(workflow, '.github/workflows/lockfile-integrity.yml is missing').toBeTruthy();
expect(workflow?.text).toContain('node scripts/check-lockfile-integrity.mjs');
});

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

it('⛔ is classified as NOT a blocking context (the maintainer floor)', () => {
expect(Object.keys(NOT_A_GATE)).toContain(CHECK_NAME);
Expand Down
117 changes: 29 additions & 88 deletions ui/scripts/__tests__/ci-setup-pnpm-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { jobBlock, rootCi, UI_JOBS, withoutComments } from './helpers/root-ci';

const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..');
const WORKFLOW_DIR = path.join(ROOT, '.github', 'workflows');
const SETUP = 'scripts/ci-setup-pnpm.sh';

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

/** Every workflow file, as `{ file, yaml }`. */
function workflows(): { file: string; yaml: string }[] {
return fs
.readdirSync(WORKFLOW_DIR)
.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
.sort()
.map((file) => ({ file, yaml: fs.readFileSync(path.join(WORKFLOW_DIR, file), 'utf8') }));
}

/**
* Every `run:` step body in a piece of workflow YAML — single-line and block
* scalar alike, and nothing else.
Expand Down Expand Up @@ -119,89 +111,38 @@ describe('the shared CI pnpm setup is wired everywhere pnpm is used', () => {
expect(run.stdout).toContain('self-test OK');
});

it('no workflow runs corepack outside the shared script', () => {
const files = workflows();
expect(files.length, 'no workflow files found — this whole file would be vacuous').toBeGreaterThan(0);

const offenders = files
.filter(({ yaml }) => runSteps(yaml).some((s) => /\bcorepack\b/.test(s)))
.map(({ file }) => file);

expect(
offenders,
'these workflows invoke corepack directly instead of `bash ' +
SETUP +
'`. The download it performs is a registry read that has reddened a REQUIRED check ' +
'before any gate logic ran (objectui#8099); route it through the shared script so the ' +
'retry and the self-describing step name apply here too.',
).toEqual([]);

// Anti-vacuity for the reader above: the comment-only mentions must still be
// THERE, or this assertion is passing because the corpus emptied out.
const commentOnly = files.filter(
({ yaml }) => /\bcorepack\b/.test(yaml) && !runSteps(yaml).some((s) => /\bcorepack\b/.test(s)),
);
expect(
commentOnly.map((w) => w.file),
'the two comment-only corepack mentions must stay visible to this scan and stay unjudged — ' +
'if they are gone, re-derive whether this rule still reads what it claims to read',
).toEqual(['changelog.yml', 'dependabot-auto-merge.yml']);
});

it('every workflow that invokes pnpm also runs the shared setup', () => {
const files = workflows();

const usesPnpm = files.filter(({ yaml }) => runSteps(yaml).some((s) => /\bpnpm\b/.test(s)));
expect(
usesPnpm.length,
'no workflow appears to invoke pnpm, which cannot be true — the step reader has drifted',
).toBeGreaterThan(0);
// `no workflow runs corepack outside the shared script`, `every workflow that
// invokes pnpm also runs the shared setup` and `every job that calls the
// setup script checks the repository out first` retired with
// objectstack#25712. They swept objectui's workflows under
// `ui/.github/workflows/` for calls to this script, and those run nowhere
// under the monorepo: GitHub runs the root `.github/workflows/` alone. What
// objectui's CI became is the four root `* UI` jobs (objectstack#25697),
// which set pnpm up through the root composite action
// `./.github/actions/setup-pnpm`, objectstack's answer to the same Corepack
// fetch. The describe below asks the same three questions of those jobs, by
// path. This script and its self-test stay; no root job calls it.
});

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

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

it('every job that calls the setup script checks the repository out first', () => {
const files = workflows().filter(({ yaml }) => yaml.includes(SETUP));
expect(files.length, `no workflow references ${SETUP} — the sweep was undone`).toBeGreaterThan(0);

const bad: string[] = [];
for (const { file, yaml } of files) {
const lines = yaml.split('\n');
// Job blocks: two-space keys inside the `jobs:` mapping.
const jobsAt = lines.findIndex((l) => /^jobs:[ \t]*$/.test(l));
expect(jobsAt, `${file} must have a top-level \`jobs:\` mapping`).toBeGreaterThan(-1);

const starts: number[] = [];
for (let i = jobsAt + 1; i < lines.length; i++) {
if (/^ {2}[a-z0-9][a-z0-9-]*:[ \t]*$/.test(lines[i])) starts.push(i);
}
starts.push(lines.length);

for (let s = 0; s + 1 < starts.length; s++) {
const block = lines.slice(starts[s], starts[s + 1]);
const setupAt = block.findIndex((l) => l.includes(SETUP));
if (setupAt === -1) continue;
const checkoutAt = block.findIndex((l) => /uses:\s*actions\/checkout@/.test(l));
if (checkoutAt === -1 || checkoutAt > setupAt) {
bad.push(`${file} :: ${block[0].trim()}`);
}
}
}
it.each(jobs)('$key checks the repository out, then sets pnpm up, before its first pnpm', ({ block }) => {
const checkoutAt = block.search(/uses:\s*actions\/checkout@/);
const setupAt = block.indexOf(SHARED);
expect(checkoutAt, 'the job checks nothing out').toBeGreaterThan(-1);
expect(setupAt, `the job has no \`${SHARED}\` step after its checkout`).toBeGreaterThan(checkoutAt);

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