blob: 63685a66b307f5f7498ad9ea73bdb889b9c6d14a [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,
validateRegistryChannels,
validateSignatureAudit,
validateStageRun,
} from './release-cli-publication.mjs';
const SOURCE_SHA = 'a'.repeat(40);
const WORKFLOW_PATH = '.github/workflows/release-cli-stage.yml';
const CURRENT_CLI_VERSION = JSON.parse(
readFileSync(resolve(import.meta.dirname, '../packages/cli/package.json'), 'utf8'),
).version;
const PRODUCT_TAG = 'v0.1.0-beta.1';
const STAGE_RUN = {
id: 321,
run_attempt: 1,
path: WORKFLOW_PATH,
event: 'workflow_dispatch',
head_branch: PRODUCT_TAG,
head_sha: SOURCE_SHA,
conclusion: 'success',
head_repository: { full_name: 'apache/maka' },
};
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',
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 product 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,
productTag: PRODUCT_TAG,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'apache/maka',
workflowPath: WORKFLOW_PATH,
});
assert.equal(prepared.record.sha256, fixture.sha256);
assert.equal(prepared.record.schemaVersion, 3);
assert.equal(prepared.record.productTag, PRODUCT_TAG);
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 a product tag that does not match the version', () => {
const fixture = createCandidate();
assert.throws(
() =>
prepareStageRelease({
repoRoot: fixture.root,
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
productTag: 'v9.9.9',
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'apache/maka',
workflowPath: WORKFLOW_PATH,
}),
/Product tag .* does not match/u,
);
});
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',
productTag: PRODUCT_TAG,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'apache/maka',
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,
productTag: PRODUCT_TAG,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'apache/maka',
workflowPath: WORKFLOW_PATH,
}),
/checksum does not match/u,
);
});
test('finalization accepts only the exact successful product-tag stage run', () => {
const fixture = createPreparedCandidate();
assert.equal(
validateStageRun({
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
run: STAGE_RUN,
}).source.commit,
SOURCE_SHA,
);
for (const drift of [
{ path: '.github/workflows/other.yml' },
{ event: 'pull_request' },
{ head_branch: 'v0.1.0-beta.2' },
{ conclusion: 'failure' },
{ head_sha: 'c'.repeat(40) },
{ run_attempt: 2 },
]) {
assert.throws(
() =>
validateStageRun({
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
run: { ...STAGE_RUN, ...drift },
}),
/stage workflow run/u,
);
}
});
test('finalization rejects a release record whose product tag does not match its version', () => {
const fixture = createPreparedCandidate();
const recordPath = join(fixture.releaseDirectory, 'release.json');
const record = JSON.parse(readFileSync(recordPath, 'utf8'));
writeFileSync(recordPath, `${JSON.stringify({ ...record, productTag: 'v9.9.9' }, null, 2)}\n`);
assert.throws(
() =>
validateStageRun({
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
run: STAGE_RUN,
}),
/productTag is inconsistent/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`),
);
const registryRecord = JSON.parse(readFileSync(join(registryDirectory, 'release.json'), 'utf8'));
assert.equal(registryRecord.version, result.version);
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' } },
attestationBundles: [provenanceBundle()],
},
],
};
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 binds provenance to the exact tag, source, workflow, and run', () => {
const fixture = createPreparedCandidate();
const audit = (mutate) => ({
invalid: [],
missing: [],
verified: [
{
name: 'maka-agent',
version: fixture.version,
attestations: { provenance: { predicateType: 'https://slsa.dev/provenance/v1' } },
attestationBundles: [provenanceBundle(mutate)],
},
],
});
for (const mutate of [
(statement) => {
statement.predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit = 'b'.repeat(40);
},
(statement) => {
statement.predicate.buildDefinition.externalParameters.workflow.ref = 'refs/heads/main';
},
(statement) => {
statement.predicate.buildDefinition.externalParameters.workflow.path =
'.github/workflows/other.yml';
},
(statement) => {
statement.predicate.runDetails.metadata.invocationId =
'https://github.com/apache/maka/actions/runs/999/attempts/1';
},
(statement) => {
statement.predicate.buildDefinition.externalParameters.workflow.repository =
'https://github.com/other/repository';
},
]) {
assert.throws(
() =>
validateSignatureAudit({
releaseDirectory: fixture.releaseDirectory,
audit: audit(mutate),
}),
/provenance does not match/u,
);
}
const wrongPredicate = provenanceBundle();
wrongPredicate.predicateType = 'https://example.invalid/provenance';
assert.throws(
() =>
validateSignatureAudit({
releaseDirectory: fixture.releaseDirectory,
audit: {
invalid: [],
missing: [],
verified: [
{
name: 'maka-agent',
version: fixture.version,
attestations: { provenance: {} },
attestationBundles: [wrongPredicate],
},
],
},
}),
/provenance does not match/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('prepare-stage CLI emits only consumed GitHub Actions outputs', () => {
const fixture = createCandidate(CURRENT_CLI_VERSION);
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,
`v${fixture.version}`,
SOURCE_SHA,
'321',
'1',
'apache/maka',
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=latest',
`tarball=${fixture.tarballPath}`,
]);
});
test('validate-stage-run CLI accepts the canonical staged 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: PRODUCT_TAG,
head_sha: SOURCE_SHA,
conclusion: 'success',
head_repository: { full_name: 'apache/maka' },
}),
);
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'), [
`product_tag=${PRODUCT_TAG}`,
`source_commit=${SOURCE_SHA}`,
]);
});
function createPreparedCandidate() {
const fixture = createCandidate();
prepareStageRelease({
repoRoot: fixture.root,
releaseDirectory: fixture.releaseDirectory,
expectedVersion: fixture.version,
productTag: `v${fixture.version}`,
sourceSha: SOURCE_SHA,
runId: '321',
runAttempt: '1',
repository: 'apache/maka',
workflowPath: WORKFLOW_PATH,
});
return fixture;
}
function provenanceBundle(mutate = () => {}) {
const statement = {
_type: 'https://in-toto.io/Statement/v1',
predicateType: 'https://slsa.dev/provenance/v1',
predicate: {
buildDefinition: {
buildType: 'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1',
externalParameters: {
workflow: {
repository: 'https://github.com/apache/maka',
ref: `refs/tags/${PRODUCT_TAG}`,
path: WORKFLOW_PATH,
},
},
resolvedDependencies: [
{
uri: `git+https://github.com/apache/maka@refs/tags/${PRODUCT_TAG}`,
digest: { gitCommit: SOURCE_SHA },
},
],
internalParameters: { github: { event_name: 'workflow_dispatch' } },
},
runDetails: {
builder: { id: 'https://github.com/actions/runner/github-hosted' },
metadata: {
invocationId: 'https://github.com/apache/maka/actions/runs/321/attempts/1',
},
},
},
};
mutate(statement);
return {
predicateType: 'https://slsa.dev/provenance/v1',
bundle: {
dsseEnvelope: {
payloadType: 'application/vnd.in-toto+json',
payload: Buffer.from(JSON.stringify(statement)).toString('base64'),
signatures: [{ keyid: '', sig: 'verified-by-npm' }],
},
},
};
}
function createCandidate(version = '0.1.0-beta.1') {
const root = mkdtempSync(join(tmpdir(), 'maka-cli-publication-'));
const releaseDirectory = join(root, 'packages/cli/release');
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 digest(algorithm, bytes, encoding) {
return createHash(algorithm).update(bytes).digest(encoding);
}