| /* |
| * 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(); |
| } |