mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
ci(nx): enforce lockstep dependency versions
This commit is contained in:
1 parent
e86e988e72
commit
a4b8a85b7d
4 files changed
+260
No files matched your search
@@ -147,6 +147,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Validate Nx dependency version policy
|
||||
run: pnpm run deps:nx:validate
|
||||
|
||||
- name: Typecheck web and Electron entry points
|
||||
run: pnpm run typecheck:ci
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
"test:backend": "nx test electron-backend",
|
||||
"test:unit:all": "nx run-many --target=test --all --parallel=3",
|
||||
"test:unit:ci": "nx run-many --target=test --all --parallel=3 --output-style=static",
|
||||
"deps:nx:test": "node --test tools/dependencies/check-nx-version-sync.test.mjs",
|
||||
"deps:nx:check": "node tools/dependencies/check-nx-version-sync.mjs",
|
||||
"deps:nx:validate": "pnpm run deps:nx:test && pnpm run deps:nx:check",
|
||||
"coverage:tools:test": "node --test tools/coverage/coverage-integrity.test.mjs",
|
||||
"coverage:unit:ci": "node tools/coverage/run-tier-a-coverage.mjs",
|
||||
"coverage:merge": "node tools/coverage/merge-coverage.mjs",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { parse } from 'yaml';
|
||||
|
||||
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
export function validateNxVersionSync({ packageJson, lockfile }) {
|
||||
const diagnostics = [];
|
||||
const expectedVersion = packageJson.devDependencies?.nx;
|
||||
|
||||
if (!expectedVersion) {
|
||||
diagnostics.push('package.json: devDependencies.nx is required');
|
||||
} else if (!EXACT_SEMVER.test(expectedVersion)) {
|
||||
diagnostics.push(
|
||||
`package.json: nx must use an exact semantic version, found "${expectedVersion}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (!expectedVersion || !EXACT_SEMVER.test(expectedVersion)) {
|
||||
return { diagnostics, expectedVersion };
|
||||
}
|
||||
|
||||
for (const sectionName of [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
'optionalDependencies',
|
||||
]) {
|
||||
const section = packageJson[sectionName] ?? {};
|
||||
for (const [packageName, specifier] of Object.entries(section)) {
|
||||
if (
|
||||
packageName.startsWith('@nx/') &&
|
||||
specifier !== expectedVersion
|
||||
) {
|
||||
diagnostics.push(
|
||||
`package.json: ${sectionName}["${packageName}"] must match nx@${expectedVersion}, found "${specifier}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let foundExpectedNx = false;
|
||||
for (const packageKey of Object.keys(lockfile.packages ?? {}).sort()) {
|
||||
const nxMatch = /^nx@(.+)$/.exec(packageKey);
|
||||
if (nxMatch) {
|
||||
const resolvedVersion = nxMatch[1];
|
||||
if (resolvedVersion === expectedVersion) {
|
||||
foundExpectedNx = true;
|
||||
} else {
|
||||
diagnostics.push(
|
||||
`pnpm-lock.yaml: resolved nx@${resolvedVersion} must match nx@${expectedVersion}`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const officialPackageMatch = /^(@nx\/[^@]+)@(.+)$/.exec(packageKey);
|
||||
if (
|
||||
officialPackageMatch &&
|
||||
officialPackageMatch[2] !== expectedVersion
|
||||
) {
|
||||
diagnostics.push(
|
||||
`pnpm-lock.yaml: resolved ${officialPackageMatch[1]}@${officialPackageMatch[2]} must match nx@${expectedVersion}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundExpectedNx) {
|
||||
diagnostics.push(
|
||||
`pnpm-lock.yaml: expected nx@${expectedVersion} to be resolved`
|
||||
);
|
||||
}
|
||||
|
||||
return { diagnostics, expectedVersion };
|
||||
}
|
||||
|
||||
const isMain =
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
||||
|
||||
if (isMain) {
|
||||
const rootDir = process.cwd();
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(resolve(rootDir, 'package.json'), 'utf8')
|
||||
);
|
||||
const lockfile = parse(
|
||||
await readFile(resolve(rootDir, 'pnpm-lock.yaml'), 'utf8')
|
||||
);
|
||||
const { diagnostics, expectedVersion } = validateNxVersionSync({
|
||||
packageJson,
|
||||
lockfile,
|
||||
});
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
console.error('Nx dependency version policy failed:');
|
||||
for (const diagnostic of diagnostics) console.error(`- ${diagnostic}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(
|
||||
`Nx dependency versions are synchronized at ${expectedVersion}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
const validatorPath = fileURLToPath(
|
||||
new URL('./check-nx-version-sync.mjs', import.meta.url)
|
||||
);
|
||||
|
||||
const alignedPackageJson = {
|
||||
devDependencies: {
|
||||
'@nx/angular': '22.7.1',
|
||||
nx: '22.7.1',
|
||||
'nx-electron': '22.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
const alignedPackages = {
|
||||
'@nx/angular@22.7.1': {},
|
||||
'nx@22.7.1': {},
|
||||
'nx-electron@22.0.0': {},
|
||||
};
|
||||
|
||||
async function createFixture(t, { packageJson, packages }) {
|
||||
const rootDir = await mkdtemp(join(tmpdir(), 'nx-version-sync-'));
|
||||
t.after(() => rm(rootDir, { recursive: true, force: true }));
|
||||
await writeFile(
|
||||
join(rootDir, 'package.json'),
|
||||
`${JSON.stringify(packageJson, null, 2)}\n`
|
||||
);
|
||||
await writeFile(
|
||||
join(rootDir, 'pnpm-lock.yaml'),
|
||||
stringify({ lockfileVersion: '9.0', packages })
|
||||
);
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
function runValidator(rootDir) {
|
||||
return spawnSync(process.execPath, [validatorPath], {
|
||||
cwd: rootDir,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
test('accepts aligned official Nx packages and ignores nx-electron', async (t) => {
|
||||
const rootDir = await createFixture(t, {
|
||||
packageJson: alignedPackageJson,
|
||||
packages: alignedPackages,
|
||||
});
|
||||
|
||||
const result = runValidator(rootDir);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/Nx dependency versions are synchronized at 22\.7\.1\./
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a direct official Nx package mismatch', async (t) => {
|
||||
const rootDir = await createFixture(t, {
|
||||
packageJson: {
|
||||
devDependencies: {
|
||||
'@nx/angular': '22.7.1',
|
||||
nx: '22.7.2',
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@nx/angular@22.7.1': {},
|
||||
'nx@22.7.2': {},
|
||||
},
|
||||
});
|
||||
|
||||
const result = runValidator(rootDir);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(
|
||||
result.stderr,
|
||||
/devDependencies\["@nx\/angular"\] must match nx@22\.7\.2, found "22\.7\.1"/
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a non-exact root Nx version', async (t) => {
|
||||
const rootDir = await createFixture(t, {
|
||||
packageJson: {
|
||||
devDependencies: {
|
||||
'@nx/angular': '^22.7.1',
|
||||
nx: '^22.7.1',
|
||||
},
|
||||
},
|
||||
packages: alignedPackages,
|
||||
});
|
||||
|
||||
const result = runValidator(rootDir);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(
|
||||
result.stderr,
|
||||
/nx must use an exact semantic version, found "\^22\.7\.1"/
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects an extra resolved Nx version', async (t) => {
|
||||
const rootDir = await createFixture(t, {
|
||||
packageJson: {
|
||||
devDependencies: {
|
||||
'@nx/angular': '22.7.2',
|
||||
nx: '22.7.2',
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@nx/angular@22.7.2': {},
|
||||
'nx@22.7.1': {},
|
||||
'nx@22.7.2': {},
|
||||
},
|
||||
});
|
||||
|
||||
const result = runValidator(rootDir);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /resolved nx@22\.7\.1 must match nx@22\.7\.2/);
|
||||
});
|
||||
|
||||
test('rejects a transitive official Nx lockfile mismatch', async (t) => {
|
||||
const rootDir = await createFixture(t, {
|
||||
packageJson: {
|
||||
devDependencies: {
|
||||
'@nx/angular': '22.7.2',
|
||||
nx: '22.7.2',
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@nx/angular@22.7.1': {},
|
||||
'nx@22.7.2': {},
|
||||
},
|
||||
});
|
||||
|
||||
const result = runValidator(rootDir);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(
|
||||
result.stderr,
|
||||
/resolved @nx\/angular@22\.7\.1 must match nx@22\.7\.2/
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user