Harden dependency isolation check and run it in CI
npm pack runs the prepare script for directory specs even with --ignore-scripts and its output can pollute stdout, so locate the packed tarball on disk instead of parsing --json output. Run npm with a minimal environment so caller npm config cannot leak into the synthetic consumers, and require exact versions for the consumers' dependencies. Add a CI job that runs the check on Node 22.
This commit is contained in:
parent
90123733bb
commit
f28f5ddb3c
2 changed files with 57 additions and 18 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -114,6 +114,21 @@ jobs:
|
|||
grep -q "Ready" /tmp/mcpb-smoke.log
|
||||
working-directory: examples/pdf-server
|
||||
|
||||
dependency-isolation:
|
||||
name: Dependency isolation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- run: npm run test:dependency-isolation
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
|
|
@ -25,33 +26,56 @@ for (const role of [client, server]) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact version for a synthetic consumer dependency. Read from
|
||||
* devDependencies so the consumers exercise the same SDK version the
|
||||
* repository tests against; the check would silently drift if a range were
|
||||
* allowed here.
|
||||
*/
|
||||
function exactDevDependency(name) {
|
||||
const version = packageJson.devDependencies?.[name];
|
||||
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version ?? "")) {
|
||||
throw new Error(
|
||||
`devDependencies["${name}"] must be an exact version, got ${JSON.stringify(version)}`,
|
||||
);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-"));
|
||||
try {
|
||||
// Minimal environment: nothing inherited from the caller's npm config
|
||||
// (registry overrides, auth tokens, npm_config_* set by an outer `npm run`)
|
||||
// can leak into the synthetic consumers.
|
||||
const npmEnvironment = {
|
||||
...process.env,
|
||||
PATH: process.env.PATH ?? process.env.Path,
|
||||
HOME: process.env.HOME ?? process.env.USERPROFILE,
|
||||
npm_config_cache: join(temporaryRoot, "npm-cache"),
|
||||
};
|
||||
const packOutput = JSON.parse(
|
||||
execFileSync(
|
||||
"npm",
|
||||
[
|
||||
"pack",
|
||||
"--ignore-scripts",
|
||||
"--json",
|
||||
"--pack-destination",
|
||||
temporaryRoot,
|
||||
],
|
||||
{ cwd: root, encoding: "utf8", env: npmEnvironment },
|
||||
),
|
||||
const packDestination = join(temporaryRoot, "pack");
|
||||
mkdirSync(packDestination);
|
||||
// `npm pack` runs the package's `prepare` script even with --ignore-scripts
|
||||
// (pacote's directory fetcher), and its output can pollute stdout, so do not
|
||||
// rely on `--json`: locate the tarball on disk instead.
|
||||
execFileSync(
|
||||
"npm",
|
||||
["pack", "--ignore-scripts", "--pack-destination", packDestination],
|
||||
{ cwd: root, stdio: "pipe", env: npmEnvironment },
|
||||
);
|
||||
const tarball = join(temporaryRoot, packOutput[0].filename);
|
||||
const tarballs = readdirSync(packDestination).filter((name) =>
|
||||
name.endsWith(".tgz"),
|
||||
);
|
||||
if (tarballs.length !== 1) {
|
||||
throw new Error(`expected exactly one tarball, found ${tarballs}`);
|
||||
}
|
||||
const tarball = join(packDestination, tarballs[0]);
|
||||
|
||||
const consumers = [
|
||||
{
|
||||
name: "app-only",
|
||||
dependencies: {
|
||||
"@types/node": packageJson.devDependencies["@types/node"],
|
||||
[client]: packageJson.devDependencies[client],
|
||||
"@types/node": exactDevDependency("@types/node"),
|
||||
[client]: exactDevDependency(client),
|
||||
"@modelcontextprotocol/ext-apps": `file:${tarball}`,
|
||||
},
|
||||
absent: server,
|
||||
|
|
@ -61,9 +85,9 @@ try {
|
|||
{
|
||||
name: "server-only",
|
||||
dependencies: {
|
||||
"@types/node": packageJson.devDependencies["@types/node"],
|
||||
"@types/node": exactDevDependency("@types/node"),
|
||||
"@modelcontextprotocol/ext-apps": `file:${tarball}`,
|
||||
[server]: packageJson.devDependencies[server],
|
||||
[server]: exactDevDependency(server),
|
||||
},
|
||||
absent: client,
|
||||
entry:
|
||||
|
|
|
|||
Loading…
Reference in a new issue