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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
with:
node-version: ${{ matrix.node }}
cache: npm
- if: matrix.node == 24
run: npm install --global npm@12.0.1
- run: npm ci
- run: npm run typecheck
- run: npm run lint
Expand All @@ -32,8 +34,7 @@ jobs:
- run: npm run test:coverage
- run: npm run build
- run: npm run test:mcp:packed
- if: matrix.node == 22
run: npm run test:packages:packed
- run: npm run test:packages:packed
- run: npm run check:mcp:client-skill
- run: npm run check:mcp:examples
- run: npm run check:skill
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"size:check": "node scripts/check-package-size.mjs",
"audit:high": "npm audit --audit-level=high",
"check:workflows": "node scripts/check-release-workflows.mjs",
"test:release-supply-chain": "node --test tests/release-supply-chain.test.mjs",
"test:release-supply-chain": "node --test tests/release-supply-chain.test.mjs tests/npm-pack-output.test.mjs",
"check:mcp-registry": "node scripts/check-mcp-registry-metadata.mjs",
"check:api-docs": "node scripts/check-api-docs.mjs",
"check:docs": "node scripts/check-public-docs.mjs",
Expand Down
10 changes: 10 additions & 0 deletions scripts/check-release-workflows.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ for (const command of [
]) {
assert.ok(ci.includes(`run: ${command}`), `CI must run ${command}`);
}
assert.match(
ci,
/- if: matrix\.node == 24\n\s+run: npm install --global npm@12\.0\.1/,
'Node 24 CI must use the exact npm CLI pinned by the release workflow',
);
assert.match(
ci,
/- run: npm run test:packages:packed/,
'Every supported Node CI job must exercise the complete packed-package suite',
);

const proof = workflows.find(({ path }) => path.endsWith('/prove-mcp-published.yml'))?.body ?? '';
for (const input of ['expected_version', 'expected_integrity', 'expected_git_head']) {
Expand Down
48 changes: 48 additions & 0 deletions scripts/lib/npm-pack-output.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict';

function parseJsonTail(output) {
const offsets = [0];
for (let index = 0; index < output.length; index += 1) {
if (output[index] === '\n') {
offsets.push(index + 1);
}
}

for (const offset of offsets.reverse()) {
const candidate = output.slice(offset).trim();
if (!candidate.startsWith('[') && !candidate.startsWith('{')) {
continue;
}
try {
return JSON.parse(candidate);
} catch {
// Lifecycle output may contain earlier JSON-looking lines. Keep searching for the final result.
}
}
return undefined;
}

export function parseNpmPackArtifacts(output, workspace) {
const parsed = parseJsonTail(output);
assert.notEqual(parsed, undefined, `Could not parse npm pack JSON output for ${workspace}`);

let artifacts;
if (Array.isArray(parsed)) {
artifacts = parsed;
} else {
assert(
parsed && typeof parsed === 'object' && Object.hasOwn(parsed, workspace),
`Expected npm pack output for ${workspace}`,
);
assert.equal(Object.keys(parsed).length, 1, `Expected one packed artifact for ${workspace}`);
artifacts = [parsed[workspace]];
}

assert.equal(artifacts.length, 1, `Expected one packed artifact for ${workspace}`);
assert(
artifacts[0] && typeof artifacts[0] === 'object',
`Expected one packed artifact for ${workspace}`,
);
assert.equal(artifacts[0].name, workspace, `Packed the wrong workspace for ${workspace}`);
return artifacts;
}
6 changes: 3 additions & 3 deletions scripts/pack-release-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { promisify } from 'node:util';

import { parseNpmPackArtifacts } from './lib/npm-pack-output.mjs';

const execFileAsync = promisify(execFile);
const argumentsByName = new Map();
for (let index = 2; index < process.argv.length; index += 2) {
Expand Down Expand Up @@ -47,9 +49,7 @@ async function pack(entry) {
const { stdout } = await execFileAsync(command.command, command.args, {
maxBuffer: 20 * 1_024 * 1_024,
});
const jsonStart = stdout.lastIndexOf('\n[');
const packed = JSON.parse(jsonStart === -1 ? stdout : stdout.slice(jsonStart + 1));
assert.equal(packed.length, 1, `expected one artifact for ${entry.name}`);
const packed = parseNpmPackArtifacts(stdout, entry.name);
const artifact = packed[0];
assert.equal(artifact.name, entry.name, `packed the wrong workspace for ${entry.name}`);
assert.equal(artifact.version, entry.version, `${entry.name} packed at the wrong version`);
Expand Down
6 changes: 3 additions & 3 deletions scripts/test-mcp-packed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { JSONRPCMessageSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';

import { parseNpmPackArtifacts } from './lib/npm-pack-output.mjs';

const execFileAsync = promisify(execFile);
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const workspaces = [
Expand Down Expand Up @@ -91,9 +93,7 @@ async function packWorkspace(workspace, destination, cache) {
['pack', '--workspace', workspace, '--json', '--pack-destination', destination],
{ cwd: root, env: commandEnvironment(cache), maxBuffer: 10 * 1024 * 1024 },
);
const jsonStart = stdout.lastIndexOf('\n[');
const results = JSON.parse(jsonStart === -1 ? stdout : stdout.slice(jsonStart + 1));
assert.equal(results.length, 1, `Expected one packed artifact for ${workspace}`);
const results = parseNpmPackArtifacts(stdout, workspace);
return results[0];
}

Expand Down
6 changes: 3 additions & 3 deletions scripts/test-release-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { dirname, join, resolve } from 'node:path';
import { promisify } from 'node:util';
import { fileURLToPath } from 'node:url';

import { parseNpmPackArtifacts } from './lib/npm-pack-output.mjs';

const execFileAsync = promisify(execFile);
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const packages = [
Expand Down Expand Up @@ -67,9 +69,7 @@ async function packWorkspace(packageName, destination, environment) {
['pack', '--workspace', packageName, '--json', '--pack-destination', destination],
{ env: environment },
);
const jsonStart = stdout.lastIndexOf('\n[');
const result = JSON.parse(jsonStart === -1 ? stdout : stdout.slice(jsonStart + 1));
assert.equal(result.length, 1, `Expected one packed artifact for ${packageName}`);
const result = parseNpmPackArtifacts(stdout, packageName);
assert.equal(result[0].name, packageName, `Packed the wrong workspace for ${packageName}`);
assert(
result[0].files.some((file) => file.path === 'package.json'),
Expand Down
52 changes: 52 additions & 0 deletions tests/npm-pack-output.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';

import { parseNpmPackArtifacts } from '../scripts/lib/npm-pack-output.mjs';

const artifact = {
name: '@buzzr/mcp',
version: '5.1.0',
filename: 'buzzr-mcp-5.1.0.tgz',
files: [{ path: 'package.json', mode: 0o644 }],
};

test('parses the npm 10 and npm 11 array response after lifecycle output', () => {
const output = `build completed\n${JSON.stringify([artifact], null, 2)}\n`;
assert.deepEqual(parseNpmPackArtifacts(output, '@buzzr/mcp'), [artifact]);
});

test('normalizes the npm 12 workspace-keyed response', () => {
const output = JSON.stringify({ '@buzzr/mcp': artifact }, null, 2);
assert.deepEqual(parseNpmPackArtifacts(output, '@buzzr/mcp'), [artifact]);
});

test('rejects a keyed response for a different workspace', () => {
const output = JSON.stringify({ '@buzzr/dfs-engine': artifact });
assert.throws(
() => parseNpmPackArtifacts(output, '@buzzr/mcp'),
/Expected npm pack output for @buzzr\/mcp/,
);
});

test('rejects an array response for a different workspace', () => {
const output = JSON.stringify([{ ...artifact, name: '@buzzr/dfs-engine' }]);
assert.throws(
() => parseNpmPackArtifacts(output, '@buzzr/mcp'),
/Packed the wrong workspace for @buzzr\/mcp/,
);
});

test('rejects multiple packed artifacts', () => {
const output = JSON.stringify([artifact, { ...artifact, name: '@buzzr/dfs-engine' }]);
assert.throws(
() => parseNpmPackArtifacts(output, '@buzzr/mcp'),
/Expected one packed artifact for @buzzr\/mcp/,
);
});

test('rejects output without a JSON result', () => {
assert.throws(
() => parseNpmPackArtifacts('npm lifecycle output only', '@buzzr/mcp'),
/Could not parse npm pack JSON output for @buzzr\/mcp/,
);
});
Loading