源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user