Release jobs ran `npm install -g npm@latest` before each publish step, adding ~30 s and version drift risk on every run; removed both occurrences, relying on Node 24's bundled npm pinned via setup-node. Added a policy test (tests/policy-release-no-npm-self-upgrade.test.cjs) that will fail RED if the antipattern is re-introduced in release.yml or hotfix.yml. Fixes #318. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -200,8 +200,7 @@ jobs:
|
||||
git add package.json package-lock.json
|
||||
git commit -m "chore: bump to ${PRE_VERSION}"
|
||||
|
||||
- name: Ensure npm supports trusted publishing
|
||||
run: npm install -g npm@latest
|
||||
# npm bundled with Node 24 (pinned via setup-node) already supports trusted publishing (#318)
|
||||
|
||||
- name: Dry-run publish validation
|
||||
run: npm publish --dry-run --tag next
|
||||
@@ -326,8 +325,7 @@ jobs:
|
||||
scripts/check-npm-integrity.sh
|
||||
npm run test:coverage:unit
|
||||
|
||||
- name: Ensure npm supports trusted publishing
|
||||
run: npm install -g npm@latest
|
||||
# npm bundled with Node 24 (pinned via setup-node) already supports trusted publishing (#318)
|
||||
|
||||
- name: Dry-run publish validation
|
||||
run: npm publish --dry-run
|
||||
|
||||
67
tests/policy-release-no-npm-self-upgrade.test.cjs
Normal file
67
tests/policy-release-no-npm-self-upgrade.test.cjs
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
// Workflow YAML is a runtime contract; these assertions verify that the
|
||||
// anti-pattern of runtime global npm self-upgrade never re-enters release lanes.
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const WORKFLOWS_DIR = path.join(REPO_ROOT, '.github', 'workflows');
|
||||
|
||||
// Matches: npm install -g npm@..., npm i -g npm, npm install --global npm@11, etc.
|
||||
// Does NOT match: npm ci, npm install (no -g / --global followed by npm)
|
||||
const NPM_SELF_UPGRADE_RE = /\bnpm\s+(install|i)\s+(-g|--global)\b[^\n]*\bnpm(@|\b)/;
|
||||
|
||||
describe('policy: no runtime npm self-upgrade in release lanes (#318)', () => {
|
||||
const releaseFile = path.join(WORKFLOWS_DIR, 'release.yml');
|
||||
const hotfixFile = path.join(WORKFLOWS_DIR, 'hotfix.yml');
|
||||
|
||||
test('release.yml must not contain a runtime global npm self-upgrade step', () => {
|
||||
const content = fs.readFileSync(releaseFile, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const violations = lines
|
||||
.map((line, idx) => ({ line, lineNo: idx + 1 }))
|
||||
.filter(({ line }) => NPM_SELF_UPGRADE_RE.test(line));
|
||||
|
||||
assert.strictEqual(
|
||||
violations.length,
|
||||
0,
|
||||
`release.yml contains ${violations.length} runtime npm self-upgrade line(s) — ` +
|
||||
`remove them and rely on Node 24 bundled npm (pinned via setup-node). ` +
|
||||
`Violations: ${violations.map(({ lineNo, line }) => `line ${lineNo}: ${line.trim()}`).join('; ')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('hotfix.yml must not contain a runtime global npm self-upgrade step', () => {
|
||||
const content = fs.readFileSync(hotfixFile, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const violations = lines
|
||||
.map((line, idx) => ({ line, lineNo: idx + 1 }))
|
||||
.filter(({ line }) => NPM_SELF_UPGRADE_RE.test(line));
|
||||
|
||||
assert.strictEqual(
|
||||
violations.length,
|
||||
0,
|
||||
`hotfix.yml contains ${violations.length} runtime npm self-upgrade line(s) — ` +
|
||||
`Violations: ${violations.map(({ lineNo, line }) => `line ${lineNo}: ${line.trim()}`).join('; ')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('NPM_SELF_UPGRADE_RE correctly matches the antipattern', () => {
|
||||
// Positive cases — must match
|
||||
assert.ok(NPM_SELF_UPGRADE_RE.test('npm install -g npm@latest'), 'should match npm install -g npm@latest');
|
||||
assert.ok(NPM_SELF_UPGRADE_RE.test('npm i -g npm'), 'should match npm i -g npm');
|
||||
assert.ok(NPM_SELF_UPGRADE_RE.test('npm install --global npm@11'), 'should match npm install --global npm@11');
|
||||
assert.ok(NPM_SELF_UPGRADE_RE.test(' run: npm install -g npm@latest'), 'should match indented run step');
|
||||
|
||||
// Negative cases — must NOT match
|
||||
assert.ok(!NPM_SELF_UPGRADE_RE.test('npm ci'), 'should not match npm ci');
|
||||
assert.ok(!NPM_SELF_UPGRADE_RE.test('npm install'), 'should not match plain npm install');
|
||||
assert.ok(!NPM_SELF_UPGRADE_RE.test('npm install -g some-other-tool'), 'should not match -g some-other-tool');
|
||||
assert.ok(!NPM_SELF_UPGRADE_RE.test('npm run build'), 'should not match npm run');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user