Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
659 lines
22 KiB
JavaScript
659 lines
22 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { appendFileSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { basename, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { createHash } from 'node:crypto';
|
|
import { CLI_RELEASE_ARTIFACT_LIMITS } from './release-cli-artifact-policy.mjs';
|
|
import {
|
|
assertProductNightlyVersion,
|
|
parseProductNightlyVersion,
|
|
parseProductReleaseVersion,
|
|
} from './release-version.mjs';
|
|
|
|
const PACKAGE_NAME = 'maka-agent';
|
|
const REGISTRY_ORIGIN = 'https://registry.npmjs.org';
|
|
const REPOSITORY = 'apache/maka';
|
|
const PUBLICATION_WORKFLOW_PATH = '.github/workflows/npm-publication.yml';
|
|
const REGISTRY_REQUEST_TIMEOUT_MS = 30_000;
|
|
const RELEASE_RECORD_KEYS = [
|
|
'schemaVersion',
|
|
'packageName',
|
|
'version',
|
|
'productTag',
|
|
'distTag',
|
|
'tarball',
|
|
'sha256',
|
|
'checksum',
|
|
'inventory',
|
|
'source',
|
|
'publisher',
|
|
];
|
|
|
|
export function parseCliReleaseVersion(version) {
|
|
const { prerelease } = parseProductReleaseVersion(version);
|
|
if (prerelease.length > 0) {
|
|
throw new Error('Formal CLI releases must use a stable product version');
|
|
}
|
|
return {
|
|
version,
|
|
distTag: 'latest',
|
|
tarball: `${PACKAGE_NAME}-${version}.tgz`,
|
|
};
|
|
}
|
|
|
|
export function parseCliNightlyVersion(version, productVersion) {
|
|
assertProductNightlyVersion(version, productVersion);
|
|
return {
|
|
version,
|
|
distTag: 'nightly',
|
|
tarball: `${PACKAGE_NAME}-${version}.tgz`,
|
|
};
|
|
}
|
|
|
|
export function validateRegistryChannels({ releaseVersion, releaseDistTag, distTags }) {
|
|
if (!distTags || typeof distTags !== 'object' || Array.isArray(distTags)) {
|
|
throw new Error('Registry package metadata has no valid dist-tags');
|
|
}
|
|
if (distTags[releaseDistTag] !== releaseVersion) {
|
|
throw new Error(`Registry dist-tag ${releaseDistTag} does not point to ${releaseVersion}`);
|
|
}
|
|
}
|
|
|
|
export function prepareNightlyRelease({ repoRoot, releaseDirectory, expectedVersion }) {
|
|
const cliManifest = readJson(join(repoRoot, 'packages/cli/package.json'), 'CLI manifest');
|
|
if (cliManifest.name !== PACKAGE_NAME) {
|
|
throw new Error(`CLI package name must be ${PACKAGE_NAME}`);
|
|
}
|
|
const identity = parseCliNightlyVersion(expectedVersion, cliManifest.version);
|
|
const candidate = validateCandidateFiles(releaseDirectory, identity);
|
|
return { ...identity, tarballPath: candidate.tarballPath, sha256: candidate.sha256 };
|
|
}
|
|
|
|
export function prepareStageRelease({
|
|
repoRoot,
|
|
releaseDirectory,
|
|
expectedVersion,
|
|
productTag,
|
|
sourceSha,
|
|
publisherSha,
|
|
runId,
|
|
runAttempt,
|
|
repository,
|
|
workflowPath,
|
|
}) {
|
|
const cliManifest = readJson(join(repoRoot, 'packages/cli/package.json'), 'CLI manifest');
|
|
if (cliManifest.name !== PACKAGE_NAME) {
|
|
throw new Error(`CLI package name must be ${PACKAGE_NAME}`);
|
|
}
|
|
const identity = parseCliReleaseVersion(expectedVersion);
|
|
if (productTag !== `v${identity.version}`) {
|
|
throw new Error(`Product tag ${productTag} does not match ${identity.version}`);
|
|
}
|
|
validateProductSource({ sourceSha, repository });
|
|
validatePublisherIdentity({
|
|
publisherSha,
|
|
runId,
|
|
runAttempt,
|
|
workflowPath,
|
|
});
|
|
const candidate = validateCandidateFiles(releaseDirectory, identity);
|
|
const record = {
|
|
schemaVersion: 4,
|
|
packageName: PACKAGE_NAME,
|
|
...identity,
|
|
productTag,
|
|
sha256: candidate.sha256,
|
|
checksum: `${identity.tarball}.sha256`,
|
|
inventory: `${identity.tarball}.files.json`,
|
|
source: {
|
|
repository,
|
|
commit: sourceSha,
|
|
},
|
|
publisher: {
|
|
workflow: workflowPath,
|
|
commit: publisherSha,
|
|
runId,
|
|
runAttempt,
|
|
},
|
|
};
|
|
writeFileSync(join(releaseDirectory, 'release.json'), `${JSON.stringify(record, null, 2)}\n`, {
|
|
flag: 'wx',
|
|
mode: 0o644,
|
|
});
|
|
return { record, tarballPath: candidate.tarballPath };
|
|
}
|
|
|
|
export function validateStageRun({ releaseDirectory, expectedVersion, run }) {
|
|
const record = loadReleaseRecord(releaseDirectory);
|
|
if (expectedVersion !== record.version) {
|
|
throw new Error(
|
|
`Finalization version confirmation ${expectedVersion} does not match ${record.version}`,
|
|
);
|
|
}
|
|
if (
|
|
!run ||
|
|
String(run.id) !== record.publisher.runId ||
|
|
String(run.run_attempt) !== record.publisher.runAttempt ||
|
|
run.path !== record.publisher.workflow ||
|
|
run.event !== 'workflow_dispatch' ||
|
|
run.head_branch !== 'main' ||
|
|
run.head_sha !== record.publisher.commit ||
|
|
run.conclusion !== 'success' ||
|
|
run.head_repository?.full_name !== record.source.repository
|
|
) {
|
|
throw new Error(
|
|
'Release record does not belong to the exact successful main-branch stage workflow run',
|
|
);
|
|
}
|
|
return record;
|
|
}
|
|
|
|
export async function fetchRegistryRelease({
|
|
releaseDirectory,
|
|
registryDirectory,
|
|
fetchImpl = fetch,
|
|
}) {
|
|
const record = loadReleaseRecord(releaseDirectory);
|
|
const versionUrl = `${REGISTRY_ORIGIN}/${PACKAGE_NAME}/${encodeURIComponent(record.version)}`;
|
|
const metadata = await fetchJson(
|
|
fetchImpl,
|
|
versionUrl,
|
|
'package version metadata',
|
|
'application/json',
|
|
);
|
|
if (metadata.name !== PACKAGE_NAME || metadata.version !== record.version) {
|
|
throw new Error('Registry package identity does not match the staged release');
|
|
}
|
|
const tags = await fetchJson(fetchImpl, `${REGISTRY_ORIGIN}/${PACKAGE_NAME}`, 'package metadata');
|
|
validateRegistryChannels({
|
|
releaseVersion: record.version,
|
|
releaseDistTag: record.distTag,
|
|
distTags: tags['dist-tags'],
|
|
});
|
|
const tarballUrl = parseRegistryTarballUrl(metadata.dist?.tarball, record.tarball);
|
|
const response = await fetchImpl(tarballUrl, { redirect: 'error' });
|
|
if (!response.ok) {
|
|
throw new Error(`Registry tarball request failed with status ${response.status}`);
|
|
}
|
|
const bytes = await readBoundedBytes(
|
|
response,
|
|
CLI_RELEASE_ARTIFACT_LIMITS.compressedBytes,
|
|
'Registry tarball exceeds the reviewed compressed size limit',
|
|
);
|
|
const sha256 = digest('sha256', bytes, 'hex');
|
|
if (sha256 !== record.sha256) {
|
|
throw new Error('Registry tarball does not match the staged release checksum');
|
|
}
|
|
if (metadata.dist?.integrity !== `sha512-${digest('sha512', bytes, 'base64')}`) {
|
|
throw new Error('Registry tarball does not match its published integrity');
|
|
}
|
|
if (metadata.dist?.shasum !== digest('sha1', bytes, 'hex')) {
|
|
throw new Error('Registry tarball does not match its published shasum');
|
|
}
|
|
|
|
mkdirSync(registryDirectory, { recursive: true, mode: 0o755 });
|
|
const tarballPath = join(registryDirectory, record.tarball);
|
|
writeFileSync(tarballPath, bytes, { flag: 'wx', mode: 0o644 });
|
|
for (const name of [record.checksum, record.inventory, 'release.json']) {
|
|
copyFileSync(join(releaseDirectory, name), join(registryDirectory, name));
|
|
}
|
|
return { ...record, tarballPath, sha256 };
|
|
}
|
|
|
|
export async function resolveRegistryNightlyPredecessor({ fetchImpl = fetch } = {}) {
|
|
const packageMetadata = await fetchJson(
|
|
fetchImpl,
|
|
`${REGISTRY_ORIGIN}/${PACKAGE_NAME}`,
|
|
'package metadata',
|
|
);
|
|
const version = packageMetadata?.['dist-tags']?.nightly;
|
|
parseProductNightlyVersion(version);
|
|
|
|
const versionMetadata = await fetchJson(
|
|
fetchImpl,
|
|
`${REGISTRY_ORIGIN}/${PACKAGE_NAME}/${encodeURIComponent(version)}`,
|
|
'package version metadata',
|
|
'application/json',
|
|
);
|
|
if (versionMetadata.name !== PACKAGE_NAME || versionMetadata.version !== version) {
|
|
throw new Error('Registry Nightly identity does not match its dist-tag');
|
|
}
|
|
|
|
const tarball = `${PACKAGE_NAME}-${version}.tgz`;
|
|
const tarballUrl = parseRegistryTarballUrl(versionMetadata.dist?.tarball, tarball);
|
|
const integrity = parseSha512Integrity(versionMetadata.dist?.integrity);
|
|
return {
|
|
version,
|
|
tarballUrl,
|
|
integrity,
|
|
};
|
|
}
|
|
|
|
export async function assertRegistryNightlyPredecessor({
|
|
expectedVersion,
|
|
expectedTarballUrl,
|
|
expectedIntegrity,
|
|
fetchImpl = fetch,
|
|
}) {
|
|
const current = await resolveRegistryNightlyPredecessor({ fetchImpl });
|
|
if (
|
|
current.version !== expectedVersion ||
|
|
current.tarballUrl !== expectedTarballUrl ||
|
|
current.integrity !== expectedIntegrity
|
|
) {
|
|
throw new Error(
|
|
`Qualified npm Nightly predecessor ${expectedVersion} is no longer current; found ${current.version}`,
|
|
);
|
|
}
|
|
return current;
|
|
}
|
|
|
|
export function validateSignatureAudit({ releaseDirectory, audit }) {
|
|
const record = loadReleaseRecord(releaseDirectory);
|
|
if (!Array.isArray(audit?.invalid) || !Array.isArray(audit?.missing)) {
|
|
throw new Error('npm signature audit did not return its bounded result arrays');
|
|
}
|
|
if (audit.invalid.length > 0 || audit.missing.length > 0) {
|
|
throw new Error('npm signature audit found invalid or missing signatures');
|
|
}
|
|
const verified = Array.isArray(audit.verified) ? audit.verified : [];
|
|
const own = verified.find(
|
|
(entry) => entry?.name === PACKAGE_NAME && entry.version === record.version,
|
|
);
|
|
if (!own?.attestations?.provenance) {
|
|
throw new Error(
|
|
`npm signature audit did not include verified provenance for ${record.version}`,
|
|
);
|
|
}
|
|
const statements = (own.attestationBundles ?? []).map(parseProvenanceStatement);
|
|
if (!statements.some((statement) => matchesReleaseProvenance(statement, record))) {
|
|
throw new Error(`npm signature audit provenance does not match ${record.productTag}`);
|
|
}
|
|
return record;
|
|
}
|
|
|
|
export function prepareSignatureAuditTree({ releaseDirectory, auditDirectory }) {
|
|
const record = loadReleaseRecord(releaseDirectory);
|
|
const packageDirectory = join(auditDirectory, 'node_modules', PACKAGE_NAME);
|
|
mkdirSync(packageDirectory, { recursive: true, mode: 0o755 });
|
|
writeJson(
|
|
join(auditDirectory, 'package.json'),
|
|
{
|
|
name: 'maka-cli-signature-audit',
|
|
private: true,
|
|
dependencies: { [PACKAGE_NAME]: record.version },
|
|
},
|
|
0o644,
|
|
);
|
|
writeJson(
|
|
join(packageDirectory, 'package.json'),
|
|
{ name: PACKAGE_NAME, version: record.version },
|
|
0o644,
|
|
);
|
|
return record;
|
|
}
|
|
|
|
function loadReleaseRecord(releaseDirectory) {
|
|
const record = readJson(join(releaseDirectory, 'release.json'), 'release record');
|
|
exactKeys(record, RELEASE_RECORD_KEYS, 'release record');
|
|
if (record.schemaVersion !== 4 || record.packageName !== PACKAGE_NAME) {
|
|
throw new Error('Unsupported CLI release record');
|
|
}
|
|
const identity = parseCliReleaseVersion(record.version);
|
|
const derivedFields = {
|
|
productTag: `v${identity.version}`,
|
|
distTag: identity.distTag,
|
|
tarball: identity.tarball,
|
|
checksum: `${identity.tarball}.sha256`,
|
|
inventory: `${identity.tarball}.files.json`,
|
|
};
|
|
for (const [key, expected] of Object.entries(derivedFields)) {
|
|
if (record[key] !== expected) throw new Error(`Release record ${key} is inconsistent`);
|
|
}
|
|
if (!/^[0-9a-f]{64}$/u.test(record.sha256)) {
|
|
throw new Error('Release record sha256 is invalid');
|
|
}
|
|
exactKeys(record.source, ['repository', 'commit'], 'release source');
|
|
validateProductSource({
|
|
sourceSha: record.source.commit,
|
|
repository: record.source.repository,
|
|
});
|
|
exactKeys(record.publisher, ['workflow', 'commit', 'runId', 'runAttempt'], 'release publisher');
|
|
validatePublisherIdentity({
|
|
publisherSha: record.publisher.commit,
|
|
runId: record.publisher.runId,
|
|
runAttempt: record.publisher.runAttempt,
|
|
workflowPath: record.publisher.workflow,
|
|
});
|
|
const candidate = validateCandidateFiles(releaseDirectory, identity);
|
|
if (candidate.sha256 !== record.sha256) {
|
|
throw new Error('Release record checksum does not match the candidate');
|
|
}
|
|
return record;
|
|
}
|
|
|
|
function validateCandidateFiles(releaseDirectory, identity) {
|
|
const tarballPath = join(releaseDirectory, identity.tarball);
|
|
const bytes = readFileSync(tarballPath);
|
|
if (bytes.length > CLI_RELEASE_ARTIFACT_LIMITS.compressedBytes) {
|
|
throw new Error('CLI release candidate exceeds the reviewed compressed size limit');
|
|
}
|
|
const checksum = readFileSync(`${tarballPath}.sha256`, 'utf8');
|
|
const match = /^([0-9a-f]{64}) {2}([^\r\n]+)\r?\n?$/u.exec(checksum);
|
|
if (!match || match[2] !== identity.tarball) {
|
|
throw new Error('CLI release candidate checksum sidecar is malformed');
|
|
}
|
|
const sha256 = digest('sha256', bytes, 'hex');
|
|
if (match[1] !== sha256) {
|
|
throw new Error('CLI release candidate checksum does not match');
|
|
}
|
|
const inventory = readJson(`${tarballPath}.files.json`, 'CLI release file inventory');
|
|
if (!Array.isArray(inventory)) throw new Error('CLI release file inventory must be an array');
|
|
return { tarballPath, sha256 };
|
|
}
|
|
|
|
function validateProductSource({ sourceSha, repository }) {
|
|
if (!/^[0-9a-f]{40}$/u.test(sourceSha)) throw new Error('Release source SHA is invalid');
|
|
if (repository !== REPOSITORY) throw new Error(`Release repository must be ${REPOSITORY}`);
|
|
}
|
|
|
|
function validatePublisherIdentity({ publisherSha, runId, runAttempt, workflowPath }) {
|
|
if (!/^[0-9a-f]{40}$/u.test(publisherSha)) throw new Error('Release publisher SHA is invalid');
|
|
if (!/^[1-9]\d*$/u.test(runId)) throw new Error('Release workflow run ID is invalid');
|
|
if (!/^[1-9]\d*$/u.test(runAttempt)) throw new Error('Release workflow run attempt is invalid');
|
|
if (workflowPath !== PUBLICATION_WORKFLOW_PATH) {
|
|
throw new Error(`Release workflow must be ${PUBLICATION_WORKFLOW_PATH}`);
|
|
}
|
|
}
|
|
|
|
function parseProvenanceStatement(attestation) {
|
|
if (attestation?.predicateType !== 'https://slsa.dev/provenance/v1') return null;
|
|
const envelope = attestation.bundle?.dsseEnvelope;
|
|
if (
|
|
envelope?.payloadType !== 'application/vnd.in-toto+json' ||
|
|
typeof envelope.payload !== 'string'
|
|
) {
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(Buffer.from(envelope.payload, 'base64').toString('utf8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function matchesReleaseProvenance(statement, record) {
|
|
const repository = `https://github.com/${record.source.repository}`;
|
|
const ref = 'refs/heads/main';
|
|
const definition = statement?.predicate?.buildDefinition;
|
|
const workflow = definition?.externalParameters?.workflow;
|
|
const dependencies = definition?.resolvedDependencies;
|
|
const invocationId = `${repository}/actions/runs/${record.publisher.runId}/attempts/${record.publisher.runAttempt}`;
|
|
return (
|
|
statement?._type === 'https://in-toto.io/Statement/v1' &&
|
|
statement?.predicateType === 'https://slsa.dev/provenance/v1' &&
|
|
definition?.buildType ===
|
|
'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1' &&
|
|
workflow?.repository === repository &&
|
|
workflow?.ref === ref &&
|
|
workflow?.path === record.publisher.workflow &&
|
|
Array.isArray(dependencies) &&
|
|
dependencies.some(
|
|
(dependency) =>
|
|
dependency?.uri === `git+${repository}@${ref}` &&
|
|
dependency?.digest?.gitCommit === record.publisher.commit,
|
|
) &&
|
|
definition?.internalParameters?.github?.event_name === 'workflow_dispatch' &&
|
|
statement?.predicate?.runDetails?.builder?.id?.startsWith(
|
|
'https://github.com/actions/runner/',
|
|
) &&
|
|
statement?.predicate?.runDetails?.metadata?.invocationId === invocationId
|
|
);
|
|
}
|
|
|
|
async function fetchJson(fetchImpl, url, label, accept = 'application/vnd.npm.install-v1+json') {
|
|
const response = await fetchImpl(url, {
|
|
headers: { accept },
|
|
redirect: 'error',
|
|
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
});
|
|
if (!response.ok)
|
|
throw new Error(`Registry ${label} request failed with status ${response.status}`);
|
|
const bytes = await readBoundedBytes(
|
|
response,
|
|
4 * 1024 * 1024,
|
|
`Registry ${label} exceeds the bounded response size`,
|
|
);
|
|
try {
|
|
return JSON.parse(bytes.toString('utf8'));
|
|
} catch (error) {
|
|
throw new Error(`Registry ${label} is not valid JSON`, { cause: error });
|
|
}
|
|
}
|
|
|
|
async function readBoundedBytes(response, limit, errorMessage) {
|
|
const contentLength = response.headers.get('content-length');
|
|
if (contentLength !== null && Number(contentLength) > limit) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
if (!response.body) return Buffer.alloc(0);
|
|
|
|
const reader = response.body.getReader();
|
|
const chunks = [];
|
|
let total = 0;
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
total += value.byteLength;
|
|
if (total > limit) {
|
|
await reader.cancel();
|
|
throw new Error(errorMessage);
|
|
}
|
|
chunks.push(Buffer.from(value));
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
return Buffer.concat(chunks, total);
|
|
}
|
|
|
|
function parseRegistryTarballUrl(value, expectedName) {
|
|
if (typeof value !== 'string' || !URL.canParse(value)) {
|
|
throw new Error('Registry package metadata has no valid tarball URL');
|
|
}
|
|
const url = new URL(value);
|
|
if (
|
|
url.origin !== REGISTRY_ORIGIN ||
|
|
url.username ||
|
|
url.password ||
|
|
basename(url.pathname) !== expectedName
|
|
) {
|
|
throw new Error('Registry package metadata points outside the npm registry release path');
|
|
}
|
|
return url.href;
|
|
}
|
|
|
|
function parseSha512Integrity(value) {
|
|
if (typeof value !== 'string' || !value.startsWith('sha512-')) {
|
|
throw new Error('Registry package metadata has no valid SHA-512 integrity');
|
|
}
|
|
const encoded = value.slice('sha512-'.length);
|
|
const bytes = Buffer.from(encoded, 'base64');
|
|
if (bytes.byteLength !== 64 || bytes.toString('base64') !== encoded) {
|
|
throw new Error('Registry package metadata has no valid SHA-512 integrity');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function exactKeys(value, keys, label) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new Error(`${label} must be an object`);
|
|
}
|
|
const actual = Object.keys(value).sort();
|
|
const expected = [...keys].sort();
|
|
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
throw new Error(`${label} fields are invalid`);
|
|
}
|
|
}
|
|
|
|
function readJson(path, label) {
|
|
try {
|
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
} catch (error) {
|
|
throw new Error(`${label} is unavailable or invalid`, { cause: error });
|
|
}
|
|
}
|
|
|
|
function writeJson(path, value, mode) {
|
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx', mode });
|
|
}
|
|
|
|
function digest(algorithm, bytes, encoding) {
|
|
return createHash(algorithm).update(bytes).digest(encoding);
|
|
}
|
|
|
|
function appendOutputs(path, values) {
|
|
for (const [name, value] of Object.entries(values)) {
|
|
const text = String(value);
|
|
if (!/^[a-z_]+$/u.test(name) || /[\r\n]/u.test(text)) {
|
|
throw new Error('Unsafe GitHub Actions output');
|
|
}
|
|
appendFileSync(path, `${name}=${text}\n`, 'utf8');
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const [command, ...args] = process.argv.slice(2);
|
|
if (command === 'prepare-stage' && args.length === 10) {
|
|
const [
|
|
releaseDirectory,
|
|
expectedVersion,
|
|
productTag,
|
|
sourceSha,
|
|
publisherSha,
|
|
runId,
|
|
runAttempt,
|
|
repository,
|
|
workflowPath,
|
|
output,
|
|
] = args;
|
|
const result = prepareStageRelease({
|
|
repoRoot: resolve(import.meta.dirname, '..'),
|
|
releaseDirectory: resolve(releaseDirectory),
|
|
expectedVersion,
|
|
productTag,
|
|
sourceSha,
|
|
publisherSha,
|
|
runId,
|
|
runAttempt,
|
|
repository,
|
|
workflowPath,
|
|
});
|
|
appendOutputs(output, {
|
|
version: result.record.version,
|
|
dist_tag: result.record.distTag,
|
|
tarball: result.tarballPath,
|
|
});
|
|
return;
|
|
}
|
|
if (command === 'prepare-nightly' && args.length === 3) {
|
|
const [releaseDirectory, expectedVersion, output] = args;
|
|
const result = prepareNightlyRelease({
|
|
repoRoot: resolve(import.meta.dirname, '..'),
|
|
releaseDirectory: resolve(releaseDirectory),
|
|
expectedVersion,
|
|
});
|
|
appendOutputs(output, {
|
|
version: result.version,
|
|
dist_tag: result.distTag,
|
|
tarball: result.tarballPath,
|
|
});
|
|
return;
|
|
}
|
|
if (command === 'validate-stage-run' && (args.length === 3 || args.length === 4)) {
|
|
const [releaseDirectory, runPath, expectedVersion, output] = args;
|
|
const record = validateStageRun({
|
|
releaseDirectory: resolve(releaseDirectory),
|
|
expectedVersion,
|
|
run: readJson(resolve(runPath), 'stage workflow run'),
|
|
});
|
|
if (output) {
|
|
appendOutputs(output, {
|
|
product_tag: record.productTag,
|
|
source_commit: record.source.commit,
|
|
version: record.version,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (command === 'prepare-audit' && args.length === 2) {
|
|
const [releaseDirectory, auditDirectory] = args;
|
|
prepareSignatureAuditTree({
|
|
releaseDirectory: resolve(releaseDirectory),
|
|
auditDirectory: resolve(auditDirectory),
|
|
});
|
|
return;
|
|
}
|
|
if (command === 'fetch-registry' && args.length === 2) {
|
|
const [releaseDirectory, registryDirectory] = args;
|
|
await fetchRegistryRelease({
|
|
releaseDirectory: resolve(releaseDirectory),
|
|
registryDirectory: resolve(registryDirectory),
|
|
});
|
|
return;
|
|
}
|
|
if (command === 'resolve-nightly-predecessor' && args.length === 1) {
|
|
const [output] = args;
|
|
const predecessor = await resolveRegistryNightlyPredecessor();
|
|
appendOutputs(output, {
|
|
version: predecessor.version,
|
|
tarball_url: predecessor.tarballUrl,
|
|
integrity: predecessor.integrity,
|
|
});
|
|
return;
|
|
}
|
|
if (command === 'assert-nightly-predecessor' && args.length === 3) {
|
|
const [expectedVersion, expectedTarballUrl, expectedIntegrity] = args;
|
|
await assertRegistryNightlyPredecessor({
|
|
expectedVersion,
|
|
expectedTarballUrl,
|
|
expectedIntegrity,
|
|
});
|
|
return;
|
|
}
|
|
if (command === 'validate-audit' && args.length === 2) {
|
|
const [releaseDirectory, auditPath] = args;
|
|
validateSignatureAudit({
|
|
releaseDirectory: resolve(releaseDirectory),
|
|
audit: readJson(resolve(auditPath), 'npm signature audit'),
|
|
});
|
|
return;
|
|
}
|
|
throw new Error(
|
|
`Usage: release-cli-publication.mjs <prepare-stage|prepare-nightly|prepare-audit|validate-stage-run|fetch-registry|resolve-nightly-predecessor|assert-nightly-predecessor|validate-audit> ...`,
|
|
);
|
|
}
|
|
|
|
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
await main();
|
|
}
|