mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
fix(ci): detect stale workspace lockfile entries (#11714)
Removes stale nested browser-pool lock entries pinning Playwright 1.61.1/@types/node 22 despite the workspace declaring 1.62.1/26, and extends the lockfile gate with npm ls --workspaces --depth=0 so future manifest/lock drift fails visibly. 21/21 focused tests passing. Thanks!
This commit is contained in:
@@ -3761,11 +3761,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"tests/unit/build/check-lockfile.test.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"tests/unit/bypass-handler.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
35
package-lock.json
generated
35
package-lock.json
generated
@@ -38790,41 +38790,6 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^26"
|
||||
}
|
||||
},
|
||||
"packages/browser-pool/node_modules/@types/node": {
|
||||
"version": "22.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
|
||||
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"packages/browser-pool/node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"packages/browser-pool/node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// --validate-https → toda URL "resolved" deve usar HTTPS (bloqueia http://)
|
||||
// --validate-integrity → todo pacote deve ter hash de integridade sha512
|
||||
// --allowed-hosts npm → apenas registry.npmjs.org é host permitido
|
||||
// npm ls --workspaces → entradas do lockfile satisfazem cada workspace
|
||||
//
|
||||
// Complementa check-deps (Fase 2 / allowlist de nomes): aquele garante que só
|
||||
// nomes aprovados entram; este garante que os pacotes instalados vieram do registry
|
||||
@@ -57,10 +58,7 @@ export function getLockfileLintConfig() {
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function buildLockfileLintArgs(cfg) {
|
||||
const args = [
|
||||
"--path", cfg.lockfilePath,
|
||||
"--type", cfg.type,
|
||||
];
|
||||
const args = ["--path", cfg.lockfilePath, "--type", cfg.type];
|
||||
if (cfg.validateHttps) args.push("--validate-https");
|
||||
if (cfg.validateIntegrity) args.push("--validate-integrity");
|
||||
if (cfg.allowedHosts.length) {
|
||||
@@ -69,6 +67,67 @@ export function buildLockfileLintArgs(cfg) {
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cross-platform npm command that verifies direct workspace
|
||||
* dependencies from package-lock.json, independent of the installed tree.
|
||||
*
|
||||
* @param {NodeJS.Platform} [platform]
|
||||
* @param {string | undefined} [comSpec]
|
||||
* @returns {{ command: string, args: string[] }}
|
||||
*/
|
||||
export function getWorkspaceDependencyCheckCommand(
|
||||
platform = process.platform,
|
||||
comSpec = process.env.ComSpec
|
||||
) {
|
||||
const npmArgs = ["ls", "--workspaces", "--depth=0", "--package-lock-only"];
|
||||
if (platform === "win32") {
|
||||
return {
|
||||
command: comSpec || "cmd.exe",
|
||||
args: ["/d", "/s", "/c", "npm.cmd", ...npmArgs],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npm",
|
||||
args: npmArgs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the workspace dependency consistency check while keeping the process
|
||||
* boundary injectable for deterministic success and failure tests.
|
||||
*
|
||||
* @param {{
|
||||
* platform?: NodeJS.Platform,
|
||||
* comSpec?: string,
|
||||
* execFile?: typeof execFileSync,
|
||||
* }} [options]
|
||||
* @returns {{ ok: true } | { ok: false, stdout: string, stderr: string }}
|
||||
*/
|
||||
export function runWorkspaceDependencyCheck(options = {}) {
|
||||
const {
|
||||
platform = process.platform,
|
||||
comSpec = process.env.ComSpec,
|
||||
execFile = execFileSync,
|
||||
} = options;
|
||||
const workspaceCheck = getWorkspaceDependencyCheckCommand(platform, comSpec);
|
||||
|
||||
try {
|
||||
execFile(workspaceCheck.command, workspaceCheck.args, {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
stdout: err.stdout ?? "",
|
||||
stderr: err.stderr ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const cfg = getLockfileLintConfig();
|
||||
|
||||
@@ -111,6 +170,17 @@ function main() {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const workspaceResult = runWorkspaceDependencyCheck();
|
||||
if (workspaceResult.ok) {
|
||||
console.log("[check-lockfile] OK — workspace lock entries match their manifests");
|
||||
} else {
|
||||
console.error("[check-lockfile] FAIL — workspace lock entries are inconsistent:");
|
||||
if (workspaceResult.stdout) console.error(workspaceResult.stdout);
|
||||
if (workspaceResult.stderr) console.error(workspaceResult.stderr);
|
||||
console.error("\n → Regenerate the affected lock entries and verify with a clean `npm ci`");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// TDD tests for check-lockfile.mjs — lockfile policy gate (Task 7.7).
|
||||
//
|
||||
// Strategy: the lockfile-lint binary is an external CLI tool; we do not spawn it
|
||||
// in unit tests. Instead, we test the two exported pure functions:
|
||||
// in unit tests. Instead, we test the exported policy helpers and inject the
|
||||
// process boundary used by the workspace consistency runner:
|
||||
// - getLockfileLintConfig() — returns the policy configuration object
|
||||
// - buildLockfileLintArgs() — maps a config object to the argv array
|
||||
//
|
||||
@@ -11,10 +12,15 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
import {
|
||||
getLockfileLintConfig,
|
||||
buildLockfileLintArgs,
|
||||
getWorkspaceDependencyCheckCommand,
|
||||
runWorkspaceDependencyCheck,
|
||||
} from "../../../scripts/check/check-lockfile.mjs";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -179,3 +185,131 @@ test("buildLockfileLintArgs: --allowed-hosts values follow immediately after the
|
||||
assert.equal(args[hostIdx + 1], "npm");
|
||||
assert.equal(args[hostIdx + 2], "verdaccio");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// workspace dependency consistency
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("getWorkspaceDependencyCheckCommand: checks every workspace at direct depth", () => {
|
||||
const command = getWorkspaceDependencyCheckCommand("linux");
|
||||
assert.deepEqual(command.args, ["ls", "--workspaces", "--depth=0", "--package-lock-only"]);
|
||||
});
|
||||
|
||||
test("getWorkspaceDependencyCheckCommand: invokes npm directly outside Windows", () => {
|
||||
const command = getWorkspaceDependencyCheckCommand("linux");
|
||||
assert.equal(command.command, "npm");
|
||||
});
|
||||
|
||||
test("getWorkspaceDependencyCheckCommand: invokes npm.cmd through cmd.exe on Windows", () => {
|
||||
const command = getWorkspaceDependencyCheckCommand("win32", "C:\\Windows\\System32\\cmd.exe");
|
||||
assert.equal(command.command, "C:\\Windows\\System32\\cmd.exe");
|
||||
assert.deepEqual(command.args, [
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
"npm.cmd",
|
||||
"ls",
|
||||
"--workspaces",
|
||||
"--depth=0",
|
||||
"--package-lock-only",
|
||||
]);
|
||||
});
|
||||
|
||||
test("runWorkspaceDependencyCheck: executes the selected command and returns success", () => {
|
||||
const calls: unknown[][] = [];
|
||||
const result = runWorkspaceDependencyCheck({
|
||||
platform: "linux",
|
||||
execFile: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
return "tree is valid";
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { ok: true });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0]?.[0], "npm");
|
||||
assert.deepEqual(calls[0]?.[1], ["ls", "--workspaces", "--depth=0", "--package-lock-only"]);
|
||||
});
|
||||
|
||||
test("runWorkspaceDependencyCheck: reports npm ls failures without masking diagnostics", () => {
|
||||
const failure = Object.assign(new Error("ELSPROBLEMS"), {
|
||||
stdout: "invalid playwright",
|
||||
stderr: "npm error code ELSPROBLEMS",
|
||||
});
|
||||
|
||||
const result = runWorkspaceDependencyCheck({
|
||||
platform: "linux",
|
||||
execFile: () => {
|
||||
throw failure;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
ok: false,
|
||||
stdout: "invalid playwright",
|
||||
stderr: "npm error code ELSPROBLEMS",
|
||||
});
|
||||
});
|
||||
|
||||
test("workspace check validates lock entries independently of node_modules", (t) => {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "omniroute-lockfile-check-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
mkdirSync(path.join(root, "packages", "example"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(root, "package.json"),
|
||||
JSON.stringify({ name: "root", version: "1.0.0", private: true, workspaces: ["packages/*"] })
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(root, "packages", "example", "package.json"),
|
||||
JSON.stringify({ name: "example", version: "1.0.0", dependencies: { semver: "7.7.4" } })
|
||||
);
|
||||
const lock = {
|
||||
name: "root",
|
||||
version: "1.0.0",
|
||||
lockfileVersion: 3,
|
||||
requires: true,
|
||||
packages: {
|
||||
"": { name: "root", version: "1.0.0", workspaces: ["packages/*"] },
|
||||
"node_modules/example": { resolved: "packages/example", link: true },
|
||||
"node_modules/semver": { version: "7.6.0" },
|
||||
"packages/example": {
|
||||
name: "example",
|
||||
version: "1.0.0",
|
||||
dependencies: { semver: "7.7.4" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const lockPath = path.join(root, "package-lock.json");
|
||||
writeFileSync(lockPath, JSON.stringify(lock));
|
||||
|
||||
const command = getWorkspaceDependencyCheckCommand(process.platform, process.env.ComSpec);
|
||||
assert.throws(
|
||||
() =>
|
||||
execFileSync(command.command, command.args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}),
|
||||
(error: unknown) => {
|
||||
const diagnostics = `${String((error as { stdout?: string }).stdout ?? "")}\n${String(
|
||||
(error as { stderr?: string }).stderr ?? ""
|
||||
)}`;
|
||||
return /ELSPROBLEMS|invalid/i.test(diagnostics);
|
||||
}
|
||||
);
|
||||
|
||||
lock.packages["node_modules/semver"].version = "7.7.4";
|
||||
writeFileSync(lockPath, JSON.stringify(lock));
|
||||
mkdirSync(path.join(root, "node_modules", "semver"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(root, "node_modules", "semver", "package.json"),
|
||||
JSON.stringify({ name: "semver", version: "7.6.0" })
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
execFileSync(command.command, command.args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user