mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
5d6b8a0ab6
Keep dependency execution in read-only release jobs and validate the original qualified archive with Node built-ins before publication. Refresh registry state on every publisher attempt; retain exact bytes/channel verification, v1 read-only recovery, and installed CLI/MCP checks before MCP publication. Static/docs checks, artifact/registry/recovery tests and real package/rehearsal verification pass. All four hosted package platforms pass. Portable CI retains the independently reproduced main PTX failure in #6675 and fail-fast missing coverage; use the owner-authorized existing PR-only exception, with limitations documented in the PR. Refs #6480.
174 lines
6.4 KiB
JavaScript
174 lines
6.4 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { createReadStream } from "node:fs";
|
|
import { execFile } from "node:child_process";
|
|
import { readFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
export const MANIFEST_NAME = "manifest.json";
|
|
export const TARBALL_NAME = "package.tgz";
|
|
const execFileAsync = promisify(execFile);
|
|
const MANIFEST_KEYS = [
|
|
"schemaVersion",
|
|
"sourceSha",
|
|
"runId",
|
|
"runAttempt",
|
|
"platform",
|
|
"architecture",
|
|
"packageVersion",
|
|
"nodeVersion",
|
|
"releaseTag",
|
|
"sha256",
|
|
].sort();
|
|
|
|
/** Construct the immutable envelope; semantic release intent belongs to qualification. */
|
|
export function createVerifiedPackageEnvelope(metadata) {
|
|
const manifest = { schemaVersion: 2, ...metadata };
|
|
validateManifestShape(manifest);
|
|
return manifest;
|
|
}
|
|
|
|
/** Validate a retained package against the current checkout and workflow attempt. */
|
|
export async function validateVerifiedPackageEnvelope({
|
|
directory,
|
|
sourceSha,
|
|
runId,
|
|
runAttempt,
|
|
packageVersion,
|
|
nodeVersion,
|
|
allowLegacyManifest = false,
|
|
}) {
|
|
if (!/^[a-f0-9]{40}$/u.test(sourceSha))
|
|
throw new Error("Expected source SHA must be a full commit SHA");
|
|
if (!Number.isSafeInteger(runId) || runId <= 0)
|
|
throw new Error("Expected workflow run ID must be a positive integer");
|
|
if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0)
|
|
throw new Error("Expected workflow run attempt must be a positive integer");
|
|
const manifestPath = join(directory, MANIFEST_NAME);
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
} catch (cause) {
|
|
throw new Error(`Could not read verified package manifest: ${cause}`, {
|
|
cause,
|
|
});
|
|
}
|
|
validateManifestShape(manifest, allowLegacyManifest);
|
|
assertEqual(manifest.sourceSha, sourceSha, "source SHA");
|
|
assertEqual(manifest.runId, runId, "workflow run ID");
|
|
// A successful producer may belong to an earlier attempt when only failed
|
|
// publisher jobs are rerun. The workflow still requires same-run qualification.
|
|
if (manifest.runAttempt > runAttempt)
|
|
throw new Error("Verified package workflow run attempt is from the future");
|
|
assertEqual(manifest.platform, "linux", "verifier platform");
|
|
assertEqual(manifest.architecture, "x64", "verifier architecture");
|
|
assertEqual(manifest.packageVersion, packageVersion, "package version");
|
|
assertEqual(manifest.nodeVersion, nodeVersion, "Node.js version");
|
|
|
|
// The artifact format deliberately fixes the name; no manifest value can
|
|
// redirect validation to another path.
|
|
const tarball = resolve(directory, TARBALL_NAME);
|
|
const digest = await sha256File(tarball);
|
|
assertEqual(manifest.sha256, digest, "tarball SHA-256");
|
|
return { tarball, manifest };
|
|
}
|
|
|
|
function validateManifestShape(manifest, allowLegacyManifest = false) {
|
|
if (
|
|
typeof manifest !== "object" ||
|
|
manifest === null ||
|
|
Array.isArray(manifest)
|
|
)
|
|
throw new Error("Verified package manifest must be a JSON object");
|
|
const legacy = allowLegacyManifest && manifest.schemaVersion === 1;
|
|
const expectedKeys = legacy
|
|
? MANIFEST_KEYS.filter((key) => key !== "releaseTag")
|
|
: MANIFEST_KEYS;
|
|
const keys = Object.keys(manifest).sort();
|
|
if (
|
|
keys.length !== expectedKeys.length ||
|
|
keys.some((key, index) => key !== expectedKeys[index])
|
|
)
|
|
throw new Error("Verified package manifest has missing or unknown fields");
|
|
if (manifest.schemaVersion !== 2 && !legacy)
|
|
throw new Error("Unsupported verified package manifest schema version");
|
|
if (
|
|
typeof manifest.sourceSha !== "string" ||
|
|
!/^[a-f0-9]{40}$/u.test(manifest.sourceSha)
|
|
)
|
|
throw new Error("Verified package manifest has an invalid source SHA");
|
|
if (!Number.isSafeInteger(manifest.runId) || manifest.runId <= 0)
|
|
throw new Error("Verified package manifest has an invalid workflow run ID");
|
|
if (!Number.isSafeInteger(manifest.runAttempt) || manifest.runAttempt <= 0)
|
|
throw new Error(
|
|
"Verified package manifest has an invalid workflow run attempt",
|
|
);
|
|
for (const key of [
|
|
"platform",
|
|
"architecture",
|
|
"packageVersion",
|
|
"nodeVersion",
|
|
])
|
|
if (typeof manifest[key] !== "string" || manifest[key].length === 0)
|
|
throw new Error(`Verified package manifest has an invalid ${key}`);
|
|
if (!legacy && !["latest", "next"].includes(manifest.releaseTag))
|
|
throw new Error("Verified package manifest has an invalid release tag");
|
|
if (
|
|
typeof manifest.sha256 !== "string" ||
|
|
!/^[a-f0-9]{64}$/u.test(manifest.sha256)
|
|
)
|
|
throw new Error("Verified package manifest has an invalid tarball SHA-256");
|
|
}
|
|
|
|
function assertEqual(actual, expected, label) {
|
|
if (actual !== expected)
|
|
throw new Error(`Verified package ${label} mismatch`);
|
|
}
|
|
|
|
export async function sha256File(path) {
|
|
const hash = createHash("sha256");
|
|
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
/** Require the registry to serve the exact archive selected for publication. */
|
|
export async function verifyPublishedPackageIntegrity(tarball, integrity) {
|
|
const hash = createHash("sha512");
|
|
for await (const chunk of createReadStream(tarball)) hash.update(chunk);
|
|
if (integrity !== `sha512-${hash.digest("base64")}`)
|
|
throw new Error(
|
|
"Published npm integrity differs from the original qualified package",
|
|
);
|
|
}
|
|
|
|
/** Bind each consumer to the checked-out source and the same workflow run. */
|
|
export async function workflowPackageIdentity() {
|
|
const sourceSha = process.env.GITHUB_SHA;
|
|
if (sourceSha === undefined || !/^[a-f0-9]{40}$/u.test(sourceSha))
|
|
throw new Error("GITHUB_SHA must be a full commit SHA");
|
|
const checkoutSha = (
|
|
await execFileAsync("git", ["rev-parse", "HEAD"])
|
|
).stdout.trim();
|
|
assertEqual(checkoutSha, sourceSha, "checked out source SHA");
|
|
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
|
|
return {
|
|
sourceSha,
|
|
runId: parsePositiveInteger(process.env.GITHUB_RUN_ID, "GITHUB_RUN_ID"),
|
|
runAttempt: parsePositiveInteger(
|
|
process.env.GITHUB_RUN_ATTEMPT,
|
|
"GITHUB_RUN_ATTEMPT",
|
|
),
|
|
packageVersion: packageJson.version,
|
|
nodeVersion: process.version,
|
|
};
|
|
}
|
|
|
|
export function parsePositiveInteger(value, name) {
|
|
if (value === undefined || !/^[1-9]\d*$/u.test(value))
|
|
throw new Error(`${name} must be a positive integer`);
|
|
const parsed = Number(value);
|
|
if (!Number.isSafeInteger(parsed))
|
|
throw new Error(`${name} must be a safe positive integer`);
|
|
return parsed;
|
|
}
|