blob: c0af65092eaeb41d510c3660f266cad1b17354b7 [file]
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import test from 'node:test';
import {
fetchRegistryRelease,
parseCliReleaseVersion,
prepareSignatureAuditTree,
prepareStageRelease,
validateGitHubRelease,
validateRegistryChannels,
validateSignatureAudit,
validateStageRun,
} from './release-cli-publication.mjs';
const SOURCE_SHA = 'a'.repeat(40);
const WORKFLOW_PATH = '.github/workflows/release-cli-stage.yml';
test('release versions map prereleases and stable versions to distinct channels', () => {
assert.deepEqual(parseCliReleaseVersion('0.1.0-beta.1'), {
version: '0.1.0-beta.1',
distTag: 'next',
gitTag: 'cli-v0.1.0-beta.1',
tarball: 'maka-agent-0.1.0-beta.1.tgz',
});
assert.equal(parseCliReleaseVersion('0.1.0').distTag, 'latest');
for (const version of ['01.0.0', '0.1', '0.1.0+local', '0.1.0-beta..1', '../0.1.0']) {
assert.throws(() => parseCliReleaseVersion(version), /valid CLI release version/u);
}
});
test('release channels never leave next behind latest', () => {
for (const next of ['0.1.0', '0.2.0-beta.1']) {
assert.doesNotThrow(() =>
validateRegistryChannels({
releaseVersion: '0.1.0',
releaseDistTag: 'latest',
distTags: { latest: '0.1.0', next },
}),
);
}
for (const next of [undefined, '0.1.0-beta.1']) {
assert.throws(
() =>
validateRegistryChannels({
releaseVersion: '0.1.0',
releaseDistTag: 'latest',
distTags: { latest: '0.1.0', ...(next ? { next } : {}) },
}),
/npm dist-tag add "maka-agent@0\.1\.0" next/u,
);
}
assert.doesNotThrow(() =>
validateRegistryChannels({
releaseVersion: '0.2.0-beta.1',
releaseDistTag: 'next',
distTags: { latest: '0.1.0', next: '0.2.0-beta.1' },
}),
);
assert.throws(
() =>
validateRegistryChannels({
releaseVersion: '0.1.0-beta.2',
releaseDistTag: 'next',
distTags: { latest: '0.1.0', next: '0.1.0-beta.2' },
}),
/cannot advance the next channel/u,
);
});
test('stage records bind the checked candidate to one source workflow run', () => {
const fixture = createCandidate();
const prepared = prepareStageRelease({
repoRoot: fixture.root,
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'maka-agent/maka-agent',
workflowPath: WORKFLOW_PATH,
});
assert.equal(prepared.record.sha256, fixture.sha256);
assert.equal(prepared.record.source.commit, SOURCE_SHA);
assert.equal(prepared.record.source.runId, '321');
assert.equal(prepared.record.source.runAttempt, '1');
assert.deepEqual(
JSON.parse(readFileSync(join(fixture.releaseDirectory, 'release.json'), 'utf8')),
prepared.record,
);
});
test('stage preparation rejects confirmation and checksum drift', () => {
const fixture = createCandidate();
assert.throws(
() =>
prepareStageRelease({
repoRoot: fixture.root,
releaseDirectory: fixture.releaseDirectory,
expectedVersion: '0.1.0-beta.2',
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'maka-agent/maka-agent',
workflowPath: WORKFLOW_PATH,
}),
/confirmation/u,
);
writeFileSync(`${fixture.tarballPath}.sha256`, `${'0'.repeat(64)} ${fixture.tarball}\n`);
assert.throws(
() =>
prepareStageRelease({
repoRoot: fixture.root,
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'maka-agent/maka-agent',
workflowPath: WORKFLOW_PATH,
}),
/checksum does not match/u,
);
});
test('finalization accepts only the exact successful main stage run', () => {
const fixture = createPreparedCandidate();
const run = {
id: 321,
run_attempt: 1,
path: WORKFLOW_PATH,
event: 'workflow_dispatch',
head_branch: 'main',
head_sha: SOURCE_SHA,
conclusion: 'success',
head_repository: { full_name: 'maka-agent/maka-agent' },
};
assert.equal(
validateStageRun({
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
run,
}).source.commit,
SOURCE_SHA,
);
for (const drift of [
{ path: '.github/workflows/other.yml' },
{ event: 'pull_request' },
{ head_branch: 'feature' },
{ conclusion: 'failure' },
{ head_sha: 'b'.repeat(40) },
{ run_attempt: 2 },
]) {
assert.throws(
() =>
validateStageRun({
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
run: { ...run, ...drift },
}),
/stage workflow run/u,
);
}
});
test('registry finalization requires the exact staged bytes and dist-tag', async () => {
const fixture = createPreparedCandidate();
const registryDirectory = mkdtempSync(join(tmpdir(), 'maka-cli-registry-release-'));
const fetchImpl = registryFetch({ fixture });
const result = await fetchRegistryRelease({
releaseDirectory: fixture.releaseDirectory,
registryDirectory,
fetchImpl,
});
assert.equal(result.sha256, fixture.sha256);
assert.deepEqual(readFileSync(result.tarballPath), fixture.bytes);
assert.deepEqual(
readFileSync(`${result.tarballPath}.files.json`),
readFileSync(`${fixture.tarballPath}.files.json`),
);
assert.match(
readFileSync(join(registryDirectory, 'release-notes.md'), 'utf8'),
/Stage workflow run: .* \(attempt 1\)/u,
);
await assert.rejects(
fetchRegistryRelease({
releaseDirectory: fixture.releaseDirectory,
registryDirectory: mkdtempSync(join(tmpdir(), 'maka-cli-registry-drift-')),
fetchImpl: registryFetch({ fixture, bytes: Buffer.from('different release') }),
}),
/Registry tarball does not match/u,
);
});
test('registry downloads stop reading as soon as the tarball exceeds its bound', async () => {
const fixture = createPreparedCandidate();
const fallback = registryFetch({ fixture });
const tarballUrl = `https://registry.npmjs.org/maka-agent/-/${fixture.tarball}`;
let pulls = 0;
const fetchImpl = async (input, options) => {
if (String(input) !== tarballUrl) return fallback(input, options);
return new Response(
new ReadableStream({
pull(controller) {
pulls += 1;
if (pulls > 30) return controller.close();
controller.enqueue(new Uint8Array(1024 * 1024));
},
}),
);
};
await assert.rejects(
fetchRegistryRelease({
releaseDirectory: fixture.releaseDirectory,
registryDirectory: mkdtempSync(join(tmpdir(), 'maka-cli-registry-oversized-')),
fetchImpl,
}),
/exceeds the reviewed compressed size limit/u,
);
assert.ok(pulls < 30, `expected an early bounded read, consumed ${pulls} chunks`);
});
test('signature audit must contain Maka provenance for the finalized version', () => {
const fixture = createPreparedCandidate();
const verified = {
invalid: [],
missing: [],
verified: [
{
name: 'maka-agent',
version: fixture.version,
attestations: { provenance: { predicateType: 'https://slsa.dev/provenance/v1' } },
},
],
};
assert.doesNotThrow(() =>
validateSignatureAudit({
releaseDirectory: fixture.releaseDirectory,
audit: verified,
}),
);
assert.throws(
() =>
validateSignatureAudit({
releaseDirectory: fixture.releaseDirectory,
audit: { ...verified, verified: [] },
}),
/verified provenance/u,
);
assert.throws(
() =>
validateSignatureAudit({
releaseDirectory: fixture.releaseDirectory,
audit: { ...verified, invalid: [{ name: 'dependency' }] },
}),
/invalid or missing signatures/u,
);
});
test('signature audit tree exposes only the top-level registry package', () => {
const fixture = createPreparedCandidate();
const auditDirectory = mkdtempSync(join(tmpdir(), 'maka-cli-signature-audit-'));
prepareSignatureAuditTree({
releaseDirectory: fixture.releaseDirectory,
auditDirectory,
});
assert.deepEqual(JSON.parse(readFileSync(join(auditDirectory, 'package.json'), 'utf8')), {
name: 'maka-cli-signature-audit',
private: true,
dependencies: { 'maka-agent': fixture.version },
});
assert.deepEqual(
JSON.parse(readFileSync(join(auditDirectory, 'node_modules/maka-agent/package.json'), 'utf8')),
{ name: 'maka-agent', version: fixture.version },
);
});
test('GitHub finalization accepts only the exact published metadata and asset digests', async () => {
const fixture = createPreparedCandidate();
const registryDirectory = mkdtempSync(join(tmpdir(), 'maka-cli-github-release-'));
await fetchRegistryRelease({
releaseDirectory: fixture.releaseDirectory,
registryDirectory,
fetchImpl: registryFetch({ fixture }),
});
const release = createGitHubReleaseFixture(fixture, registryDirectory);
assert.doesNotThrow(() =>
validateGitHubRelease({ releaseDirectory: registryDirectory, release }),
);
assert.throws(
() =>
validateGitHubRelease({
releaseDirectory: registryDirectory,
release: { ...release, draft: true },
}),
/metadata does not match/u,
);
assert.throws(
() =>
validateGitHubRelease({
releaseDirectory: registryDirectory,
release: {
...release,
assets: release.assets.map((asset, index) =>
index === 0 ? { ...asset, digest: `sha256:${'0'.repeat(64)}` } : asset,
),
},
}),
/asset does not match/u,
);
});
test('prepare-stage CLI emits only consumed GitHub Actions outputs', () => {
const fixture = createCandidate();
const output = join(fixture.root, 'github-output.txt');
const result = spawnSync(
process.execPath,
[
resolve(import.meta.dirname, 'release-cli-publication.mjs'),
'prepare-stage',
fixture.releaseDirectory,
fixture.version,
SOURCE_SHA,
'321',
'1',
'maka-agent/maka-agent',
WORKFLOW_PATH,
output,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(readFileSync(output, 'utf8').trim().split('\n'), [
`version=${fixture.version}`,
'dist_tag=next',
`git_tag=cli-v${fixture.version}`,
`tarball=${fixture.tarballPath}`,
]);
});
test('validate-stage-run CLI emits the canonical cross-job release identity', () => {
const fixture = createPreparedCandidate();
const runPath = join(fixture.root, 'stage-run.json');
const output = join(fixture.root, 'github-output.txt');
writeFileSync(
runPath,
JSON.stringify({
id: 321,
run_attempt: 1,
path: WORKFLOW_PATH,
event: 'workflow_dispatch',
head_branch: 'main',
head_sha: SOURCE_SHA,
conclusion: 'success',
head_repository: { full_name: 'maka-agent/maka-agent' },
}),
);
const result = spawnSync(
process.execPath,
[
resolve(import.meta.dirname, 'release-cli-publication.mjs'),
'validate-stage-run',
fixture.releaseDirectory,
runPath,
fixture.version,
output,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(readFileSync(output, 'utf8').trim().split('\n'), [
`version=${fixture.version}`,
'dist_tag=next',
`git_tag=cli-v${fixture.version}`,
`source_sha=${SOURCE_SHA}`,
`tarball=${fixture.tarball}`,
]);
});
function createPreparedCandidate() {
const fixture = createCandidate();
prepareStageRelease({
repoRoot: fixture.root,
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'maka-agent/maka-agent',
workflowPath: WORKFLOW_PATH,
});
return fixture;
}
function createCandidate() {
const root = mkdtempSync(join(tmpdir(), 'maka-cli-publication-'));
const releaseDirectory = join(root, 'packages/cli/release');
const version = '0.1.0-beta.1';
const tarball = `maka-agent-${version}.tgz`;
const tarballPath = join(releaseDirectory, tarball);
const bytes = Buffer.from('immutable cli tarball');
const sha256 = digest('sha256', bytes, 'hex');
mkdirSync(releaseDirectory, { recursive: true });
writeFileSync(join(root, 'package.json'), '{"packageManager":"npm@11.19.0"}\n');
writeFileSync(
join(root, 'packages/cli/package.json'),
`${JSON.stringify({ name: 'maka-agent', version })}\n`,
);
writeFileSync(tarballPath, bytes);
writeFileSync(`${tarballPath}.sha256`, `${sha256} ${tarball}\n`);
writeFileSync(`${tarballPath}.files.json`, '[{"path":"dist/cli.js","size":1}]\n');
return { root, releaseDirectory, version, tarball, tarballPath, bytes, sha256 };
}
function registryFetch({ fixture, bytes = fixture.bytes }) {
const integrity = `sha512-${digest('sha512', bytes, 'base64')}`;
const shasum = digest('sha1', bytes, 'hex');
const tarballUrl = `https://registry.npmjs.org/maka-agent/-/${fixture.tarball}`;
return async (input, options = {}) => {
const url = String(input);
if (url === `https://registry.npmjs.org/maka-agent/${fixture.version}`) {
assert.equal(options.headers?.accept, 'application/json');
return Response.json({
name: 'maka-agent',
version: fixture.version,
dist: { tarball: tarballUrl, integrity, shasum },
});
}
if (url === 'https://registry.npmjs.org/maka-agent') {
assert.equal(options.headers?.accept, 'application/vnd.npm.install-v1+json');
return Response.json({ 'dist-tags': { next: fixture.version } });
}
if (url === tarballUrl) return new Response(bytes);
return new Response('not found', { status: 404 });
};
}
function createGitHubReleaseFixture(fixture, releaseDirectory) {
const names = [
fixture.tarball,
`${fixture.tarball}.sha256`,
`${fixture.tarball}.files.json`,
'release.json',
];
return {
tag_name: `cli-v${fixture.version}`,
name: `Maka CLI ${fixture.version}`,
body: readFileSync(join(releaseDirectory, 'release-notes.md'), 'utf8'),
draft: false,
prerelease: true,
assets: names.map((name) => {
const bytes = readFileSync(join(releaseDirectory, name));
return {
name,
state: 'uploaded',
size: bytes.length,
digest: `sha256:${digest('sha256', bytes, 'hex')}`,
};
}),
};
}
function digest(algorithm, bytes, encoding) {
return createHash(algorithm).update(bytes).digest(encoding);
}