Files

172 lines
14 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { realpathSync, readFileSync, writeFileSync, lstatSync, mkdirSync, existsSync } from "node:fs";
import { join, relative, isAbsolute, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const stableVersion = (value) => typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value);
import { fullSha, numericId, CLI_REPOSITORY, TOOLKIT_REPOSITORY, github } from "./release-github.mjs";
export const NPM_VERSION = "11.12.1";
export function windowsNpmInvocation(wrapper, args, { cwd = process.cwd(), env = process.env } = {}) {
// Match npm.cmd: its prefix helper can redirect a setup-node bundled npm to
// the globally upgraded installation. Use the same Node and execution context.
const directory = dirname(wrapper), localNode = join(directory, "node.exe");
const command = existsSync(localNode) ? localNode : "node";
const bin = join(directory, "node_modules", "npm", "bin");
const prefix = execFileSync(command, [join(bin, "npm-prefix.js")], {
cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30000, maxBuffer: 1024 * 1024,
}).trim();
if (!prefix || /[\r\n]/.test(prefix)) throw new Error("Cannot resolve the installed npm prefix");
const globalCli = join(prefix, "node_modules", "npm", "bin", "npm-cli.js");
const cli = existsSync(globalCli) ? globalCli : join(bin, "npm-cli.js");
if (!existsSync(cli)) throw new Error("Cannot resolve the installed npm JavaScript entry");
return { command, args: [cli, ...args] };
}
export function npmInvocation(args, { cwd = process.cwd(), env = process.env } = {}) {
if (process.platform !== "win32") return { command: "npm", args };
// Execute npm's JavaScript entry with Node; .cmd wrappers cannot be execFile'd,
// and a shell would reinterpret package/output paths containing metacharacters.
const [wrapper] = execFileSync("where.exe", ["npm.cmd"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30000 }).trim().split(/\r?\n/);
if (!wrapper) throw new Error("Cannot resolve the installed npm launcher");
return windowsNpmInvocation(wrapper, args, { cwd, env });
}
const run = (command, args, cwd, env = process.env) => {
const invocation = command === "npm" ? npmInvocation(args, { cwd, env }) : { command, args };
return execFileSync(invocation.command, invocation.args, { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000, maxBuffer: 8 * 1024 * 1024 }).trim();
};
export const integrity = (bytes) => `sha512-${createHash("sha512").update(bytes).digest("base64")}`;
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
export function exactCheckout(cwd, sha) {
if (!fullSha(sha) || run("git", ["rev-parse", "HEAD"], cwd) !== sha || run("git", ["status", "--porcelain", "--untracked-files=no"], cwd)) throw new Error("Clean exact tested checkout required");
run("git", ["merge-base", "--is-ancestor", sha, "refs/remotes/origin/master"], cwd);
}
export function packDirectory(cwd, directory, sourceSha) {
exactCheckout(cwd, sourceSha);
if (run("npm", ["--version"], cwd) !== NPM_VERSION) throw new Error("Pinned npm 11.12.1 required");
const rel = relative(cwd, directory);
if (!rel.startsWith("..") && !isAbsolute(rel)) throw new Error("Tarball and credential config must stay outside package directory");
mkdirSync(directory, { recursive: true });
const [pack] = JSON.parse(run("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", directory], cwd));
if (!pack || !stableVersion(pack.version) || pack.name !== "@przeprogramowani/10x-cli" || !Array.isArray(pack.files)) throw new Error("Canonical package pack required");
const inventory = pack.files.map(({ path }) => {
if (typeof path !== "string" || path.includes("..") || !/^(package\.json|README\.md|LICENSE|dist\/index\.mjs|skills\/.+)$/.test(path)) throw new Error("Unexpected npm package member");
const stat = lstatSync(join(cwd, path));
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Regular package inputs required");
return { path, sha256: sha256(readFileSync(join(cwd, path))), mode: stat.mode & 0o777 };
}).sort((a, b) => a.path.localeCompare(b.path));
const bytes = readFileSync(join(directory, pack.filename));
if (integrity(bytes) !== pack.integrity) throw new Error("Prepack integrity mismatch");
return { version: pack.version, sourceSha, filename: pack.filename, expectedIntegrity: integrity(bytes), inventory };
}
export function assertFrozenInputs(cwd, candidate) {
exactCheckout(cwd, candidate.sourceSha);
for (const item of candidate.inventory) {
const stat = lstatSync(join(cwd, item.path));
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== item.mode || sha256(readFileSync(join(cwd, item.path))) !== item.sha256) throw new Error("Package inputs changed after packing");
}
const [pack] = JSON.parse(run("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], cwd));
if (JSON.stringify(pack.files.map((file) => file.path).sort()) !== JSON.stringify(candidate.inventory.map((item) => item.path).sort())) throw new Error("Package membership changed after packing");
}
export function validateRegistryResult(metadata, bytes, candidate, tagSha) {
if (metadata?.name !== "@przeprogramowani/10x-cli" || metadata.version !== candidate.version || metadata.gitHead !== candidate.sourceSha || tagSha !== candidate.sourceSha || metadata.dist?.integrity !== candidate.expectedIntegrity || integrity(bytes) !== candidate.expectedIntegrity) throw new Error("Published immutable package differs; release incomplete, never republish");
return { version: metadata.version, sourceSha: metadata.gitHead, expectedIntegrity: candidate.expectedIntegrity, actualIntegrity: integrity(bytes) };
}
export async function publishDirectoryOnce(candidate, io) {
// Even a partial previous publication is immutable. Verification is read-only.
const existing = await io.registry(candidate.version);
if (existing) {
validateRegistryResult(existing.metadata, existing.bytes, candidate, await io.tag());
return { status: "already-published-manual-completion", published: false };
}
await io.freeze();
await io.lease();
await io.ensureTag();
await io.freeze();
await io.lease();
await io.publishDirectory();
await io.freeze();
const actual = await io.registry(candidate.version);
if (!actual) throw new Error("Publication unconfirmed; retain original artifacts, never blindly retry");
return { status: "published-verified", published: true, ...validateRegistryResult(actual.metadata, actual.bytes, candidate, await io.tag()) };
}
export async function assertReleaseLease(identity, { toolkit, cli, runId, runAttempt }) {
if (![identity.cliSha, identity.toolkitSha, identity.leaseGeneration, identity.operation].every(fullSha) || !numericId(runId) || !Number.isSafeInteger(runAttempt)) throw new Error("Exact release dispatch identity required");
if ((await cli("git/ref/heads/master"))?.object?.sha !== identity.cliSha || (await toolkit("git/ref/heads/master"))?.object?.sha !== identity.toolkitSha) throw new Error("Selected master pair advanced");
const ref = await toolkit("git/ref/heads/automation/cli-release-lease");
if (ref?.object?.type !== "commit" || !fullSha(ref.object.sha)) throw new Error("Active lease unavailable");
const commit = await toolkit(`git/commits/${ref.object.sha}`), tree = await toolkit(`git/trees/${commit.tree.sha}`);
if (tree.truncated || tree.tree?.length !== 1 || tree.tree[0].path !== "lease.json" || tree.tree[0].mode !== "100644" || tree.tree[0].type !== "blob") throw new Error("Unexpected private lease content");
const blob = await toolkit(`git/blobs/${tree.tree[0].sha}`);
if (blob.encoding !== "base64" || blob.size > 8192) throw new Error("Bounded lease JSON required");
const bytes = Buffer.from(blob.content, "base64");
if (bytes.length !== blob.size) throw new Error("Lease size mismatch");
const state = JSON.parse(bytes.toString("utf8"));
if (state.schemaVersion !== 1 || state.generation !== identity.leaseGeneration || state.pair?.cliSha !== identity.cliSha || state.pair?.toolkitSha !== identity.toolkitSha || state.phase !== "release-running" || state.release?.operation !== identity.operation || (state.release.runId !== runId || state.release.runAttempt !== runAttempt)) throw new Error("Release does not own current lease generation");
const owner = await toolkit(`actions/runs/${state.owner.runId}`);
if (owner?.repository?.full_name !== TOOLKIT_REPOSITORY || owner.head_repository?.full_name !== TOOLKIT_REPOSITORY || owner.path !== ".github/workflows/release-coordinator.yml" || owner.head_branch !== "master" || owner.run_attempt !== state.owner.runAttempt) throw new Error("Lease owner identity mismatch");
const child = await cli(`actions/runs/${runId}`);
if (child?.repository?.full_name !== CLI_REPOSITORY || child.head_repository?.full_name !== CLI_REPOSITORY || child.path !== ".github/workflows/ci.yml" || child.head_sha !== identity.cliSha || child.head_branch !== "master" || child.event !== "workflow_dispatch" || child.run_attempt !== runAttempt || child.display_title !== `release-release-${identity.operation}`) throw new Error("Release child identity mismatch");
const current = await toolkit("git/ref/heads/automation/cli-release-lease");
if (current.object?.sha !== ref.object.sha) throw new Error("Lease advanced while authorizing mutation");
return state;
}
export function releaseManifest({ candidate, identity, runId, runAttempt, binaries, preparation }) {
const expected = ["10x-linux-x64", "10x-linux-arm64", "10x-darwin-arm64", "10x-darwin-x64", "10x-windows-x64.exe"].sort();
if (JSON.stringify(binaries.map((b) => b.name).sort()) !== JSON.stringify(expected) || binaries.some((b) => !/^[a-f0-9]{64}$/.test(b.sha256) || !numericId(b.artifactId))) throw new Error("Exact complete retained binary identities required");
const result = { schemaVersion: 1, repository: CLI_REPOSITORY, sourceSha: candidate.sourceSha, version: candidate.version, tag: `v${candidate.version}`, npmVersion: NPM_VERSION, runId, runAttempt, evidence: identity, expectedIntegrity: candidate.expectedIntegrity, binaries, preparation };
if (Buffer.byteLength(JSON.stringify(result)) > 8192) throw new Error("Public release manifest exceeds bound");
return result;
}
export async function registryPackage(version) {
const response = await fetch(`https://registry.npmjs.org/@przeprogramowani%2f10x-cli/${version}`, { signal: AbortSignal.timeout(30000) });
if (response.status === 404) return null;
if (!response.ok) throw new Error("Cannot determine immutable registry state");
const metadata = await response.json();
const url = new URL(metadata.dist?.tarball);
if (url.origin !== "https://registry.npmjs.org" || !url.pathname.startsWith("/@przeprogramowani/10x-cli/-/")) throw new Error("Unexpected public registry tarball URL");
const tarball = await fetch(url, { signal: AbortSignal.timeout(60000), redirect: "error" });
if (!tarball.ok || Number(tarball.headers.get("content-length")) > 50 * 1024 * 1024) throw new Error("Cannot verify actual registry tarball");
const bytes = Buffer.from(await tarball.arrayBuffer());
if (bytes.length > 50 * 1024 * 1024) throw new Error("Registry tarball exceeds bound");
return { metadata, bytes };
}
export function releaseInputs(env) {
return { cliSha: env.RELEASE_CLI_SHA, toolkitSha: env.RELEASE_TOOLKIT_SHA, runId: env.TOOLKIT_COORDINATED_RUN_ID, runAttempt: Number(env.TOOLKIT_COORDINATED_RUN_ATTEMPT), artifactId: env.TOOLKIT_COORDINATED_ARTIFACT_ID, leaseGeneration: env.RELEASE_LEASE_GENERATION, operation: env.RELEASE_OPERATION };
}
// Node resolves module symlinks, while argv can retain /var or a linked worktree path.
// Imports (including node -e/stdin) must remain side-effect free.
function isEntrypoint() {
try {
return Boolean(process.argv[1]) && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
} catch {
return false;
}
}
if (isEntrypoint()) {
try {
const identity = releaseInputs(process.env), toolkit = github(process.env.TOOLKIT_DISPATCH_TOKEN, TOOLKIT_REPOSITORY), cli = github(process.env.GH_TOKEN);
const authorize = () => assertReleaseLease(identity, { toolkit, cli, runId: process.env.GITHUB_RUN_ID, runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT) });
if (process.argv[2] === "lease") await authorize();
else if (process.argv[2] === "pack") {
const candidate = packDirectory(process.cwd(), process.env.RELEASE_OUTPUT_DIR, identity.cliSha);
writeFileSync(join(process.env.RELEASE_OUTPUT_DIR, "candidate.json"), JSON.stringify(candidate));
} else if (process.argv[2] === "publish") {
const candidate = JSON.parse(readFileSync(join(process.env.RELEASE_OUTPUT_DIR, "candidate.json"), "utf8"));
const tag = async () => {
let object = (await cli(`git/ref/tags/v${candidate.version}`))?.object;
if (object?.type === "tag") object = (await cli(`git/tags/${object.sha}`))?.object;
return object?.sha;
};
const result = await publishDirectoryOnce(candidate, {
registry: registryPackage, tag, freeze: async () => assertFrozenInputs(process.cwd(), candidate), lease: authorize,
ensureTag: async () => {
const current = await tag(); if (current && current !== candidate.sourceSha) throw new Error("Existing tag conflict");
if (!current) await cli("git/refs", "POST", { ref: `refs/tags/v${candidate.version}`, sha: candidate.sourceSha });
},
publishDirectory: async () => run("npm", ["publish", ".", "--ignore-scripts", "--access", "public"], process.cwd()),
});
writeFileSync(join(process.env.RELEASE_OUTPUT_DIR, "published-result.json"), JSON.stringify(result));
} else throw new Error("Expected lease, pack or publish");
} catch { console.error("Release identity rejected. Preserve original artifacts; no automatic republish or rebuild recovery."); process.exitCode = 1; }
}