ci: simplify release trust checks

This commit is contained in:
guokexin.02
2026-07-28 15:47:18 +08:00
parent e8a464e2d8
commit 5d18b4ebce
4 changed files with 274 additions and 2773 deletions

View File

@@ -1,673 +0,0 @@
#!/usr/bin/env node
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const { isDeepStrictEqual } = require("node:util");
const MANIFEST_NAME = "candidate-manifest.json";
const RELEASE_VERSION_PATTERN =
/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-beta\.(0|[1-9][0-9]*))?$/;
const SHA_PATTERN = /^[0-9a-fA-F]{40}$/;
const SHA256_PATTERN = /^[0-9a-fA-F]{64}$/;
function fail(message) {
throw new Error(message);
}
function hasOwn(value, key) {
return Object.prototype.hasOwnProperty.call(value, key);
}
function assertObject(value, label) {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
fail(`${label} must be an object`);
}
}
function assertExactKeys(value, expectedKeys, label) {
assertObject(value, label);
const expected = new Set(expectedKeys);
const unexpected = Object.keys(value).filter((key) => !expected.has(key)).sort();
const missing = expectedKeys.filter((key) => !hasOwn(value, key));
if (unexpected.length > 0) {
fail(`${label} has unexpected field: ${unexpected.join(", ")}`);
}
if (missing.length > 0) {
fail(`${label} is missing required field: ${missing.join(", ")}`);
}
}
function normalizeSourceSha(sourceSha, label = "sourceSha") {
if (typeof sourceSha !== "string" || !SHA_PATTERN.test(sourceSha)) {
fail(`${label} must be exactly 40 hexadecimal characters`);
}
return sourceSha.toLowerCase();
}
function parseReleaseVersion(version) {
const match = typeof version === "string" ? RELEASE_VERSION_PATTERN.exec(version) : null;
if (!match) {
fail(
"release version must use Stable X.Y.Z or Beta X.Y.Z-beta.N; "
+ "other prerelease labels, build metadata, and leading zeros are not allowed",
);
}
return {
version,
channel: match[4] === undefined ? "stable" : "beta",
major: match[1],
minor: match[2],
patch: match[3],
beta: match[4] === undefined ? null : match[4],
};
}
function validateChannel(version, channel, label = "metadata") {
const parsed = parseReleaseVersion(version);
if (channel !== "stable" && channel !== "beta") {
fail(`${label}.channel must be stable or beta`);
}
if (parsed.channel !== channel) {
fail(`${label}.version requires channel ${parsed.channel}, received ${channel}`);
}
return parsed;
}
function assertSafeFilename(name, label) {
if (
typeof name !== "string"
|| name.length === 0
|| name === "."
|| name === ".."
|| name.includes("/")
|| name.includes("\\")
|| path.basename(name) !== name
) {
fail(`${label} must be a safe basename without path separators`);
}
}
function filePath(directory, name, label) {
assertSafeFilename(name, label);
return path.join(directory, name);
}
function assertRegularFile(directory, name, label) {
const target = filePath(directory, name, label);
let stat;
try {
stat = fs.lstatSync(target);
} catch (error) {
fail(`${label} ${name} could not be inspected: ${error.message}`);
}
if (stat.isSymbolicLink() || !stat.isFile()) {
fail(`${label} ${name} must be a regular file, not a symlink or other file type`);
}
return target;
}
function listRegularFiles(directory) {
let stat;
try {
stat = fs.lstatSync(directory);
} catch (error) {
fail(`candidate directory could not be inspected: ${error.message}`);
}
if (stat.isSymbolicLink() || !stat.isDirectory()) {
fail("candidate directory must be a directory and must not be a symlink");
}
let names;
try {
names = fs.readdirSync(directory);
} catch (error) {
fail(`candidate directory could not be read: ${error.message}`);
}
names.sort();
for (const name of names) {
assertRegularFile(directory, name, "candidate entry");
}
return names;
}
function hashFile(target, algorithms) {
const hashes = algorithms.map((algorithm) => crypto.createHash(algorithm));
const buffer = Buffer.allocUnsafe(64 * 1024);
let descriptor;
try {
descriptor = fs.openSync(target, "r");
for (;;) {
const length = fs.readSync(descriptor, buffer, 0, buffer.length, null);
if (length === 0) break;
const chunk = buffer.subarray(0, length);
for (const hash of hashes) hash.update(chunk);
}
} catch (error) {
fail(`could not hash ${path.basename(target)}: ${error.message}`);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
return hashes.map((hash) => hash.digest());
}
function sha256File(target) {
return hashFile(target, ["sha256"])[0].toString("hex");
}
function npmDigests(target) {
const [sha256, sha512] = hashFile(target, ["sha256", "sha512"]);
return {
sha256: sha256.toString("hex"),
integrity: `sha512-${sha512.toString("base64")}`,
};
}
function validateCreateMetadata(metadata) {
assertObject(metadata, "metadata");
const sourceSha = normalizeSourceSha(metadata.sourceSha, "metadata.sourceSha");
if (typeof metadata.version !== "string") {
fail("metadata.version must be a string");
}
validateChannel(metadata.version, metadata.channel);
assertSafeFilename(metadata.npmPackage, "metadata.npmPackage");
if (!metadata.npmPackage.endsWith(".tgz")) {
fail("metadata.npmPackage must designate an npm .tgz file");
}
return {
sourceSha,
version: metadata.version,
channel: metadata.channel,
npmPackage: metadata.npmPackage,
};
}
function createCandidateManifest(directory, metadata) {
const normalized = validateCreateMetadata(metadata);
const names = listRegularFiles(directory);
if (!names.includes(normalized.npmPackage)) {
fail(`designated npm package is missing: ${normalized.npmPackage}`);
}
const releaseAssetNames = names.filter(
(name) => name !== MANIFEST_NAME && name !== normalized.npmPackage,
);
const releaseAssets = releaseAssetNames.map((name) => ({
name,
sha256: sha256File(assertRegularFile(directory, name, "release asset")),
}));
const npmTarget = assertRegularFile(
directory,
normalized.npmPackage,
"designated npm package",
);
return {
schemaVersion: 1,
sourceSha: normalized.sourceSha,
version: normalized.version,
channel: normalized.channel,
releaseAssets,
npmPackage: {
name: normalized.npmPackage,
...npmDigests(npmTarget),
},
};
}
function validateSha256(value, label) {
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
fail(`${label} must be exactly 64 hexadecimal characters`);
}
return value.toLowerCase();
}
function validateIntegrity(value, label) {
if (typeof value !== "string" || !value.startsWith("sha512-") || value.length === 7) {
fail(`${label} must contain one canonical SHA-512 digest`);
}
const encoded = value.slice(7);
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
fail(`${label} must contain one canonical SHA-512 digest`);
}
const decoded = Buffer.from(encoded, "base64");
if (decoded.length !== 64 || decoded.toString("base64") !== encoded) {
fail(`${label} must contain one canonical SHA-512 digest`);
}
return value;
}
function validateManifest(manifest) {
assertExactKeys(
manifest,
[
"schemaVersion",
"sourceSha",
"version",
"channel",
"releaseAssets",
"npmPackage",
],
"manifest",
);
if (manifest.schemaVersion !== 1) {
fail("manifest.schemaVersion must be 1");
}
const sourceSha = normalizeSourceSha(manifest.sourceSha, "manifest.sourceSha");
if (typeof manifest.version !== "string") {
fail("manifest.version must be a string");
}
validateChannel(manifest.version, manifest.channel, "manifest");
if (!Array.isArray(manifest.releaseAssets)) {
fail("manifest.releaseAssets must be an array");
}
const seen = new Set();
let previousName = null;
const releaseAssets = manifest.releaseAssets.map((asset, index) => {
const label = `manifest.releaseAssets[${index}]`;
assertExactKeys(asset, ["name", "sha256"], label);
assertSafeFilename(asset.name, `${label}.name`);
if (asset.name === MANIFEST_NAME) {
fail(`${MANIFEST_NAME} is reserved and must not appear in releaseAssets`);
}
if (seen.has(asset.name)) {
fail(`manifest contains duplicate release asset: ${asset.name}`);
}
if (previousName !== null && previousName >= asset.name) {
fail("manifest.releaseAssets must be sorted by name");
}
seen.add(asset.name);
previousName = asset.name;
return {
name: asset.name,
sha256: validateSha256(asset.sha256, `${label}.sha256`),
};
});
assertExactKeys(manifest.npmPackage, ["name", "sha256", "integrity"], "manifest.npmPackage");
assertSafeFilename(manifest.npmPackage.name, "manifest.npmPackage.name");
if (seen.has(manifest.npmPackage.name)) {
fail(`release asset ${manifest.npmPackage.name} duplicates npm package`);
}
if (!manifest.npmPackage.name.endsWith(".tgz")) {
fail("manifest.npmPackage.name must designate an npm .tgz file");
}
return {
sourceSha,
version: manifest.version,
channel: manifest.channel,
releaseAssets,
npmPackage: {
name: manifest.npmPackage.name,
sha256: validateSha256(
manifest.npmPackage.sha256,
"manifest.npmPackage.sha256",
),
integrity: validateIntegrity(
manifest.npmPackage.integrity,
"manifest.npmPackage.integrity",
),
},
};
}
function validateExpectedMetadata(expectedMetadata) {
assertObject(expectedMetadata, "expected metadata");
const sourceSha = normalizeSourceSha(
expectedMetadata.sourceSha,
"expected metadata.sourceSha",
);
if (typeof expectedMetadata.version !== "string") {
fail("expected metadata.version must be a string");
}
validateChannel(expectedMetadata.version, expectedMetadata.channel, "expected metadata");
return {
sourceSha,
version: expectedMetadata.version,
channel: expectedMetadata.channel,
};
}
function describeSetMismatch(label, expected, actual) {
const expectedSet = new Set(expected);
const actualSet = new Set(actual);
const missing = expected.filter((name) => !actualSet.has(name));
const unexpected = actual.filter((name) => !expectedSet.has(name));
if (missing.length === 0 && unexpected.length === 0) return;
fail(
`${label} set does not match manifest `
+ `(missing: ${missing.length > 0 ? missing.join(", ") : "none"}; `
+ `unexpected: ${unexpected.length > 0 ? unexpected.join(", ") : "none"})`,
);
}
function verifyCandidateManifest(directory, manifest, expectedMetadata, scope) {
if (scope !== "artifact" && scope !== "release") {
fail("verification scope must be artifact or release");
}
const validated = validateManifest(manifest);
const expected = validateExpectedMetadata(expectedMetadata);
if (validated.sourceSha !== expected.sourceSha) {
fail(
`manifest sourceSha does not match expected sourceSha `
+ `(${validated.sourceSha} != ${expected.sourceSha})`,
);
}
if (validated.version !== expected.version) {
fail(
`manifest version does not match expected version `
+ `(${validated.version} != ${expected.version})`,
);
}
if (validated.channel !== expected.channel) {
fail(
`manifest channel does not match expected channel `
+ `(${validated.channel} != ${expected.channel})`,
);
}
const actualNames = listRegularFiles(directory);
const releaseNames = validated.releaseAssets.map((asset) => asset.name);
const expectedNames = scope === "artifact"
? [...releaseNames, validated.npmPackage.name, MANIFEST_NAME].sort()
: [...releaseNames].sort();
describeSetMismatch(
scope === "artifact" ? "artifact file" : "release asset",
expectedNames,
actualNames,
);
if (scope === "artifact") {
const inDirectoryManifest = readManifest(
filePath(directory, MANIFEST_NAME, "candidate manifest"),
);
if (!isDeepStrictEqual(inDirectoryManifest, manifest)) {
fail(
"in-directory candidate manifest does not match the manifest supplied for verification",
);
}
}
for (const asset of validated.releaseAssets) {
const actual = sha256File(assertRegularFile(directory, asset.name, "release asset"));
if (actual !== asset.sha256) {
fail(
`SHA-256 mismatch for release asset ${asset.name}: `
+ `expected ${asset.sha256}, observed ${actual}`,
);
}
}
if (scope === "artifact") {
const target = assertRegularFile(
directory,
validated.npmPackage.name,
"npm package",
);
const actual = npmDigests(target);
if (actual.sha256 !== validated.npmPackage.sha256) {
fail(
`npm package SHA-256 mismatch for ${validated.npmPackage.name}: `
+ `expected ${validated.npmPackage.sha256}, observed ${actual.sha256}`,
);
}
if (actual.integrity !== validated.npmPackage.integrity) {
fail(
`npm package integrity mismatch for ${validated.npmPackage.name}: `
+ `expected ${validated.npmPackage.integrity}, observed ${actual.integrity}`,
);
}
}
return true;
}
function compareReleaseVersions(left, right) {
const leftMatch = RELEASE_VERSION_PATTERN.exec(left);
const rightMatch = RELEASE_VERSION_PATTERN.exec(right);
const leftParts = leftMatch.slice(1, 4).map((part) => BigInt(part));
const rightParts = rightMatch.slice(1, 4).map((part) => BigInt(part));
for (let index = 0; index < leftParts.length; index += 1) {
if (leftParts[index] < rightParts[index]) return -1;
if (leftParts[index] > rightParts[index]) return 1;
}
if (leftMatch[4] === undefined && rightMatch[4] === undefined) return 0;
const leftBeta = BigInt(leftMatch[4]);
const rightBeta = BigInt(rightMatch[4]);
if (leftBeta < rightBeta) return -1;
if (leftBeta > rightBeta) return 1;
return 0;
}
function validateObservedDistTags(distTags) {
if (distTags === undefined) return {};
assertObject(distTags, "observed.distTags");
for (const [distTag, channel] of [["latest", "stable"], ["beta", "beta"]]) {
if (!hasOwn(distTags, distTag)) continue;
let parsed;
try {
parsed = parseReleaseVersion(distTags[distTag]);
} catch {
fail(`observed dist-tag ${distTag} must contain a valid ${channel} version`);
}
if (parsed.channel !== channel) {
fail(`observed dist-tag ${distTag} must contain a valid ${channel} version`);
}
}
return distTags;
}
function evaluateNpmState(target, observed) {
assertExactKeys(target, ["version", "channel", "integrity"], "target");
assertObject(observed, "observed");
if (typeof target.version !== "string") {
fail("target.version must be a string");
}
validateChannel(target.version, target.channel, "target");
validateIntegrity(target.integrity, "target.integrity");
if (hasOwn(observed, "versionPresent") && typeof observed.versionPresent !== "boolean") {
fail("observed.versionPresent must be a boolean");
}
if (
hasOwn(observed, "publishedVersion")
&& observed.publishedVersion !== undefined
&& observed.publishedVersion !== target.version
) {
fail("observed.publishedVersion must equal target.version when provided");
}
const hasPublishedIntegrity = hasOwn(observed, "publishedIntegrity");
if (hasPublishedIntegrity) {
validateIntegrity(observed.publishedIntegrity, "observed.publishedIntegrity");
}
const distTags = validateObservedDistTags(observed.distTags);
const distTag = target.channel === "stable" ? "latest" : "beta";
const versionPresent =
observed.versionPresent === true
|| observed.publishedVersion === target.version
|| hasPublishedIntegrity;
if (observed.versionPresent === false && hasPublishedIntegrity) {
fail("observed npm state is inconsistent: version is absent but integrity is present");
}
if (versionPresent) {
if (!hasPublishedIntegrity) {
fail(`npm version ${target.version} is present but published integrity is missing`);
}
if (observed.publishedIntegrity !== target.integrity) {
fail(`npm version ${target.version} already exists with different integrity`);
}
if (!hasOwn(distTags, distTag)) {
fail(
`cannot reuse npm version ${target.version}: `
+ `dist-tag ${distTag} is missing; repair registry state manually`,
);
}
if (compareReleaseVersions(distTags[distTag], target.version) < 0) {
fail(
`cannot reuse npm version ${target.version}: dist-tag ${distTag} is behind `
+ `(${distTags[distTag]} < ${target.version}); repair registry state manually`,
);
}
return { distTag, action: "reuse" };
}
if (hasOwn(distTags, distTag)) {
const comparison = compareReleaseVersions(distTags[distTag], target.version);
if (comparison > 0) {
fail(
`npm dist-tag ${distTag} must not move backwards from `
+ `${distTags[distTag]} to ${target.version}`,
);
}
if (comparison === 0) {
fail(
`npm dist-tag ${distTag} already points to target version ${target.version}, `
+ "but the registry reports that version absent",
);
}
}
return { distTag, action: "publish" };
}
function parseCliOptions(args, allowedOptions) {
const allowed = new Set(allowedOptions);
const options = {};
for (let index = 0; index < args.length; index += 2) {
const option = args[index];
const value = args[index + 1];
if (typeof option !== "string" || !option.startsWith("--") || !allowed.has(option)) {
fail(`unknown option: ${option === undefined ? "(missing)" : option}`);
}
if (value === undefined || value.startsWith("--")) {
fail(`option ${option} requires a value`);
}
if (hasOwn(options, option)) {
fail(`option ${option} must not be repeated`);
}
options[option] = value;
}
for (const option of allowedOptions) {
if (!hasOwn(options, option)) fail(`required option is missing: ${option}`);
}
return options;
}
function readManifest(manifestPath) {
let stat;
try {
stat = fs.lstatSync(manifestPath);
} catch (error) {
fail(`manifest could not be inspected: ${error.message}`);
}
if (stat.isSymbolicLink() || !stat.isFile()) {
fail("manifest must be a regular file and must not be a symlink");
}
try {
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
} catch (error) {
fail(`manifest must contain valid JSON: ${error.message}`);
}
}
function writeSuccess(value) {
process.stdout.write(`${JSON.stringify(value)}\n`);
}
function writeFailure(error) {
process.stderr.write(`${JSON.stringify({
ok: false,
error: {
type: "release_candidate",
message: error.message,
},
})}\n`);
process.exitCode = 1;
}
function main() {
try {
const [command, ...args] = process.argv.slice(2);
if (command !== "create" && command !== "verify") {
fail("command must be create or verify");
}
if (command === "create") {
const options = parseCliOptions(args, [
"--directory",
"--manifest",
"--source-sha",
"--version",
"--channel",
"--npm-package",
]);
const directory = path.resolve(options["--directory"]);
const manifestPath = path.resolve(options["--manifest"]);
const expectedManifestPath = path.join(directory, MANIFEST_NAME);
if (manifestPath !== expectedManifestPath) {
fail(`--manifest must be ${expectedManifestPath} for create`);
}
const manifest = createCandidateManifest(directory, {
sourceSha: options["--source-sha"],
version: options["--version"],
channel: options["--channel"],
npmPackage: options["--npm-package"],
});
try {
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
} catch (error) {
fail(`candidate manifest could not be written: ${error.message}`);
}
writeSuccess({ ok: true, manifest });
return;
}
const options = parseCliOptions(args, [
"--directory",
"--manifest",
"--scope",
"--source-sha",
"--version",
"--channel",
]);
const directory = path.resolve(options["--directory"]);
const manifestPath = path.resolve(options["--manifest"]);
const scope = options["--scope"];
if (scope === "artifact" && manifestPath !== path.join(directory, MANIFEST_NAME)) {
fail(
`--manifest must be ${path.join(directory, MANIFEST_NAME)} `
+ "inside --directory for artifact scope",
);
}
const manifest = readManifest(manifestPath);
verifyCandidateManifest(
directory,
manifest,
{
sourceSha: options["--source-sha"],
version: options["--version"],
channel: options["--channel"],
},
scope,
);
writeSuccess({
ok: true,
scope,
version: manifest.version,
channel: manifest.channel,
sourceSha: normalizeSourceSha(manifest.sourceSha),
});
} catch (error) {
writeFailure(error);
}
}
module.exports = {
createCandidateManifest,
verifyCandidateManifest,
evaluateNpmState,
parseReleaseVersion,
};
if (require.main === module) main();

View File

@@ -1,753 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { afterEach, describe, it } = require("node:test");
const {
createCandidateManifest,
evaluateNpmState,
parseReleaseVersion,
verifyCandidateManifest,
} = require("./release-candidate");
const SOURCE_SHA = "ABCDEF0123456789ABCDEF0123456789ABCDEF01";
const tempDirectories = [];
function tempDirectory() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "release-candidate-"));
tempDirectories.push(directory);
return directory;
}
function writeCandidate(version = "1.2.3", channel = "stable") {
const directory = tempDirectory();
const npmPackage = `larksuite-cli-${version}.tgz`;
fs.writeFileSync(path.join(directory, "z-checksums.txt"), "checksums\n");
fs.writeFileSync(path.join(directory, `lark-cli-${version}-linux-amd64.tar.gz`), "linux\n");
fs.writeFileSync(path.join(directory, npmPackage), "npm package\n");
return {
directory,
metadata: { sourceSha: SOURCE_SHA, version, channel, npmPackage },
};
}
function sha256(value) {
return crypto.createHash("sha256").update(value).digest("hex");
}
function sha512Integrity(value) {
return `sha512-${crypto.createHash("sha512").update(value).digest("base64")}`;
}
function writeManifest(directory, manifest) {
fs.writeFileSync(
path.join(directory, "candidate-manifest.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
);
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function copyReleaseAssets(sourceDirectory, manifest) {
const releaseDirectory = tempDirectory();
for (const asset of manifest.releaseAssets) {
fs.copyFileSync(
path.join(sourceDirectory, asset.name),
path.join(releaseDirectory, asset.name),
);
}
return releaseDirectory;
}
afterEach(() => {
while (tempDirectories.length > 0) {
fs.rmSync(tempDirectories.pop(), { recursive: true, force: true });
}
});
describe("parseReleaseVersion", () => {
it("accepts exact stable and beta versions", () => {
assert.deepEqual(parseReleaseVersion("1.2.3"), {
version: "1.2.3",
channel: "stable",
major: "1",
minor: "2",
patch: "3",
beta: null,
});
assert.deepEqual(parseReleaseVersion("1.2.3-beta.4"), {
version: "1.2.3-beta.4",
channel: "beta",
major: "1",
minor: "2",
patch: "3",
beta: "4",
});
assert.deepEqual(parseReleaseVersion("9007199254740993.9007199254740995.0"), {
version: "9007199254740993.9007199254740995.0",
channel: "stable",
major: "9007199254740993",
minor: "9007199254740995",
patch: "0",
beta: null,
});
});
it("rejects unsupported labels, build metadata, and leading zeros", () => {
for (const version of [
"1.2.3-alpha.1",
"1.2.3-rc.1",
"1.2.3-beta",
"1.2.3-beta.01",
"1.2.3+build.1",
"01.2.3",
"1.02.3",
"1.2.03",
]) {
assert.throws(() => parseReleaseVersion(version), /Stable X\.Y\.Z or Beta X\.Y\.Z-beta\.N/);
}
});
});
describe("candidate manifest", () => {
it("creates a normalized, sorted stable manifest with exact digests", () => {
const { directory, metadata } = writeCandidate();
const manifest = createCandidateManifest(directory, metadata);
assert.deepEqual(manifest, {
schemaVersion: 1,
sourceSha: SOURCE_SHA.toLowerCase(),
version: "1.2.3",
channel: "stable",
releaseAssets: [
{
name: "lark-cli-1.2.3-linux-amd64.tar.gz",
sha256: sha256("linux\n"),
},
{
name: "z-checksums.txt",
sha256: sha256("checksums\n"),
},
],
npmPackage: {
name: "larksuite-cli-1.2.3.tgz",
sha256: sha256("npm package\n"),
integrity: sha512Integrity("npm package\n"),
},
});
});
it("creates and verifies a beta artifact candidate", () => {
const candidate = writeCandidate("2.0.0-beta.7", "beta");
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
writeManifest(candidate.directory, manifest);
assert.equal(manifest.channel, "beta");
assert.equal(
verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
true,
);
});
it("verifies a release directory containing only release assets", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const releaseDirectory = copyReleaseAssets(candidate.directory, manifest);
assert.equal(
verifyCandidateManifest(releaseDirectory, manifest, candidate.metadata, "release"),
true,
);
});
it("rejects a modified candidate file", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
writeManifest(candidate.directory, manifest);
fs.appendFileSync(
path.join(candidate.directory, manifest.releaseAssets[0].name),
"tampered",
);
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/SHA-256 mismatch/,
);
});
it("rejects modified npm package content and integrity", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
writeManifest(candidate.directory, manifest);
fs.appendFileSync(path.join(candidate.directory, manifest.npmPackage.name), "tampered");
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/npm package SHA-256 mismatch/,
);
fs.writeFileSync(
path.join(candidate.directory, manifest.npmPackage.name),
"npm package\n",
);
manifest.npmPackage.integrity = sha512Integrity("different npm package");
writeManifest(candidate.directory, manifest);
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/npm package integrity mismatch/,
);
});
it("rejects missing and unexpected release assets", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const releaseDirectory = copyReleaseAssets(candidate.directory, manifest);
fs.rmSync(path.join(releaseDirectory, manifest.releaseAssets[0].name));
assert.throws(
() => verifyCandidateManifest(releaseDirectory, manifest, candidate.metadata, "release"),
/release asset set does not match.*missing:/,
);
fs.copyFileSync(
path.join(candidate.directory, manifest.releaseAssets[0].name),
path.join(releaseDirectory, manifest.releaseAssets[0].name),
);
fs.writeFileSync(path.join(releaseDirectory, "unexpected.zip"), "unexpected");
assert.throws(
() => verifyCandidateManifest(releaseDirectory, manifest, candidate.metadata, "release"),
/release asset set does not match.*unexpected:/,
);
});
it("rejects symlinks and unsafe designated package names", () => {
const candidate = writeCandidate();
fs.symlinkSync(
path.join(candidate.directory, candidate.metadata.npmPackage),
path.join(candidate.directory, "linked.tgz"),
);
assert.throws(
() => createCandidateManifest(candidate.directory, candidate.metadata),
/linked\.tgz.*regular file/,
);
fs.rmSync(path.join(candidate.directory, "linked.tgz"));
for (const npmPackage of ["../package.tgz", "nested/package.tgz", "nested\\package.tgz", ".", ".."]) {
assert.throws(
() => createCandidateManifest(
candidate.directory,
{ ...candidate.metadata, npmPackage },
),
/safe basename/,
);
}
});
it("rejects symlinked candidate directories and candidate manifests", () => {
const candidate = writeCandidate();
const linkParent = tempDirectory();
const directoryLink = path.join(linkParent, "candidate-link");
fs.symlinkSync(candidate.directory, directoryLink, "dir");
assert.throws(
() => createCandidateManifest(directoryLink, candidate.metadata),
/candidate directory.*symlink/,
);
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const externalDirectory = tempDirectory();
const externalManifest = path.join(externalDirectory, "manifest.json");
fs.writeFileSync(externalManifest, `${JSON.stringify(manifest)}\n`);
fs.symlinkSync(
externalManifest,
path.join(candidate.directory, "candidate-manifest.json"),
);
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/candidate-manifest\.json must be a regular file/,
);
});
it("rejects path traversal and duplicate manifest entries", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const releaseDirectory = copyReleaseAssets(candidate.directory, manifest);
const traversing = clone(manifest);
traversing.releaseAssets[0].name = "../outside";
assert.throws(
() => verifyCandidateManifest(releaseDirectory, traversing, candidate.metadata, "release"),
/safe basename/,
);
const duplicate = clone(manifest);
duplicate.releaseAssets.push({ ...duplicate.releaseAssets[0] });
assert.throws(
() => verifyCandidateManifest(releaseDirectory, duplicate, candidate.metadata, "release"),
/duplicate release asset/,
);
const collision = clone(manifest);
collision.npmPackage.name = collision.releaseAssets[0].name;
assert.throws(
() => verifyCandidateManifest(releaseDirectory, collision, candidate.metadata, "release"),
/duplicates npm package/,
);
});
it("rejects metadata, schema, and channel mismatches", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const releaseDirectory = copyReleaseAssets(candidate.directory, manifest);
assert.throws(
() => verifyCandidateManifest(
releaseDirectory,
manifest,
{ ...candidate.metadata, sourceSha: "0".repeat(40) },
"release",
),
/sourceSha does not match/,
);
assert.throws(
() => verifyCandidateManifest(
releaseDirectory,
manifest,
{ ...candidate.metadata, version: "1.2.4" },
"release",
),
/version does not match/,
);
assert.throws(
() => createCandidateManifest(
candidate.directory,
{ ...candidate.metadata, channel: "beta" },
),
/version requires channel stable/,
);
assert.throws(
() => verifyCandidateManifest(
releaseDirectory,
{ ...manifest, schemaVersion: 2 },
candidate.metadata,
"release",
),
/schemaVersion must be 1/,
);
assert.throws(
() => verifyCandidateManifest(
releaseDirectory,
{ ...manifest, unexpected: true },
candidate.metadata,
"release",
),
/unexpected field/,
);
});
it("requires the exact artifact directory set", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/artifact file set does not match.*missing: candidate-manifest\.json/,
);
writeManifest(candidate.directory, manifest);
fs.writeFileSync(path.join(candidate.directory, "unexpected"), "unexpected");
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/artifact file set does not match.*unexpected: unexpected/,
);
});
it("rejects an artifact manifest object that differs from the in-directory manifest", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const tamperedManifest = clone(manifest);
tamperedManifest.sourceSha = "0".repeat(40);
writeManifest(candidate.directory, tamperedManifest);
assert.throws(
() => verifyCandidateManifest(
candidate.directory,
manifest,
candidate.metadata,
"artifact",
),
/in-directory candidate manifest does not match/,
);
});
it("rejects candidate-manifest.json as a release asset entry", () => {
const candidate = writeCandidate();
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const releaseDirectory = copyReleaseAssets(candidate.directory, manifest);
const injectedManifest = clone(manifest);
const injectedContent = "not an authoritative manifest\n";
injectedManifest.releaseAssets.unshift({
name: "candidate-manifest.json",
sha256: sha256(injectedContent),
});
fs.writeFileSync(
path.join(releaseDirectory, "candidate-manifest.json"),
injectedContent,
);
assert.throws(
() => verifyCandidateManifest(
releaseDirectory,
injectedManifest,
candidate.metadata,
"release",
),
/candidate-manifest\.json is reserved/,
);
});
});
describe("evaluateNpmState", () => {
it("publishes stable and beta versions to their fixed dist-tags", () => {
const stableIntegrity = sha512Integrity("stable package");
const betaIntegrity = sha512Integrity("beta package");
assert.deepEqual(
evaluateNpmState(
{ version: "1.2.3", channel: "stable", integrity: stableIntegrity },
{ distTags: { latest: "1.2.2", beta: "1.3.0-beta.1" } },
),
{ distTag: "latest", action: "publish" },
);
assert.deepEqual(
evaluateNpmState(
{ version: "1.3.0-beta.2", channel: "beta", integrity: betaIntegrity },
{ distTags: { latest: "1.2.3", beta: "1.3.0-beta.1" } },
),
{ distTag: "beta", action: "publish" },
);
});
it("reuses a published stable version only when latest is equal or higher", () => {
const integrity = sha512Integrity("stable package");
const target = { version: "1.2.3", channel: "stable", integrity };
for (const distTags of [undefined, { latest: "1.2.2" }]) {
assert.throws(
() => evaluateNpmState(target, {
versionPresent: true,
publishedIntegrity: integrity,
...(distTags === undefined ? {} : { distTags }),
}),
/cannot reuse.*dist-tag latest (is missing|is behind)/,
);
}
for (const latest of ["1.2.3", "1.2.4"]) {
assert.deepEqual(
evaluateNpmState(target, {
versionPresent: true,
publishedIntegrity: integrity,
distTags: { latest },
}),
{ distTag: "latest", action: "reuse" },
);
}
});
it("reuses a published beta version only when beta is equal or higher", () => {
const integrity = sha512Integrity("beta package");
const target = { version: "2.0.0-beta.3", channel: "beta", integrity };
for (const distTags of [undefined, { beta: "2.0.0-beta.2" }]) {
assert.throws(
() => evaluateNpmState(target, {
versionPresent: true,
publishedIntegrity: integrity,
...(distTags === undefined ? {} : { distTags }),
}),
/cannot reuse.*dist-tag beta (is missing|is behind)/,
);
}
for (const beta of ["2.0.0-beta.3", "2.0.0-beta.4"]) {
assert.deepEqual(
evaluateNpmState(target, {
versionPresent: true,
publishedIntegrity: integrity,
distTags: { beta },
}),
{ distTag: "beta", action: "reuse" },
);
}
});
it("rejects same npm version with different or missing integrity", () => {
const target = {
version: "1.2.3",
channel: "stable",
integrity: sha512Integrity("target package"),
};
assert.throws(
() => evaluateNpmState(target, {
versionPresent: true,
publishedIntegrity: sha512Integrity("different package"),
distTags: { latest: "1.2.3" },
}),
/different integrity/,
);
assert.throws(
() => evaluateNpmState(target, { versionPresent: true }),
/published integrity is missing/,
);
});
it("validates every observed publishedIntegrity property", () => {
const target = {
version: "1.2.3",
channel: "stable",
integrity: sha512Integrity("target package"),
};
for (const publishedIntegrity of [
undefined,
null,
42,
"",
"sha256-YQ==",
"sha512-YQ==",
]) {
assert.throws(
() => evaluateNpmState(target, {
versionPresent: false,
publishedIntegrity,
distTags: { latest: "1.2.2" },
}),
/observed\.publishedIntegrity must contain one canonical SHA-512 digest/,
);
}
assert.throws(
() => evaluateNpmState(target, {
versionPresent: false,
publishedIntegrity: sha512Integrity("unexpected package"),
distTags: { latest: "1.2.2" },
}),
/version is absent but integrity is present/,
);
});
it("does not move latest or beta backwards or overwrite an absent equal version", () => {
const integrity = sha512Integrity("target package");
assert.throws(
() => evaluateNpmState(
{ version: "1.2.3", channel: "stable", integrity },
{ distTags: { latest: "1.2.4" } },
),
/must not move backwards/,
);
assert.throws(
() => evaluateNpmState(
{ version: "1.2.3-beta.2", channel: "beta", integrity },
{ distTags: { beta: "1.2.3-beta.3" } },
),
/must not move backwards/,
);
assert.throws(
() => evaluateNpmState(
{ version: "1.2.3", channel: "stable", integrity },
{ distTags: { latest: "1.2.3" } },
),
/already points to target version.*registry reports that version absent/,
);
});
it("rejects malformed and cross-channel dist-tags", () => {
const target = {
version: "1.2.3",
channel: "stable",
integrity: sha512Integrity("target package"),
};
for (const distTags of [
{ latest: "1.2.3-beta.1" },
{ beta: "1.2.3" },
{ latest: "v1.2.2" },
{ beta: "1.2.3-rc.1" },
]) {
assert.throws(
() => evaluateNpmState(target, { distTags }),
/dist-tag (latest|beta).*must contain a valid (stable|beta) version/,
);
}
});
});
describe("CLI", () => {
it("creates and verifies an artifact manifest with JSON stdout", () => {
const candidate = writeCandidate("3.0.0-beta.1", "beta");
const script = path.join(__dirname, "release-candidate.js");
const manifestPath = path.join(candidate.directory, "candidate-manifest.json");
const createResult = spawnSync(
process.execPath,
[
script,
"create",
"--directory", candidate.directory,
"--manifest", manifestPath,
"--source-sha", SOURCE_SHA,
"--version", candidate.metadata.version,
"--channel", candidate.metadata.channel,
"--npm-package", candidate.metadata.npmPackage,
],
{ encoding: "utf8" },
);
assert.equal(createResult.status, 0, createResult.stderr);
assert.equal(createResult.stderr, "");
const createOutput = JSON.parse(createResult.stdout);
assert.equal(createOutput.ok, true);
assert.equal(createOutput.manifest.channel, "beta");
assert.deepEqual(JSON.parse(fs.readFileSync(manifestPath, "utf8")), createOutput.manifest);
const verifyResult = spawnSync(
process.execPath,
[
script,
"verify",
"--directory", candidate.directory,
"--manifest", manifestPath,
"--scope", "artifact",
"--source-sha", SOURCE_SHA,
"--version", candidate.metadata.version,
"--channel", candidate.metadata.channel,
],
{ encoding: "utf8" },
);
assert.equal(verifyResult.status, 0, verifyResult.stderr);
assert.equal(verifyResult.stderr, "");
assert.deepEqual(JSON.parse(verifyResult.stdout), {
ok: true,
scope: "artifact",
version: "3.0.0-beta.1",
channel: "beta",
sourceSha: SOURCE_SHA.toLowerCase(),
});
});
it("writes deterministic CLI failures to stderr", () => {
const script = path.join(__dirname, "release-candidate.js");
const result = spawnSync(process.execPath, [script, "unknown"], { encoding: "utf8" });
assert.equal(result.status, 1);
assert.equal(result.stdout, "");
assert.deepEqual(JSON.parse(result.stderr), {
ok: false,
error: {
type: "release_candidate",
message: "command must be create or verify",
},
});
});
it("rejects an external artifact manifest that could mask a tampered candidate manifest", () => {
const candidate = writeCandidate();
const script = path.join(__dirname, "release-candidate.js");
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const externalDirectory = tempDirectory();
const externalManifestPath = path.join(externalDirectory, "candidate-manifest.json");
fs.writeFileSync(externalManifestPath, `${JSON.stringify(manifest)}\n`);
const tamperedManifest = clone(manifest);
tamperedManifest.sourceSha = "0".repeat(40);
writeManifest(candidate.directory, tamperedManifest);
const result = spawnSync(
process.execPath,
[
script,
"verify",
"--directory", candidate.directory,
"--manifest", externalManifestPath,
"--scope", "artifact",
"--source-sha", SOURCE_SHA,
"--version", candidate.metadata.version,
"--channel", candidate.metadata.channel,
],
{ encoding: "utf8" },
);
assert.equal(result.status, 1);
assert.equal(result.stdout, "");
assert.match(
JSON.parse(result.stderr).error.message,
/--manifest must be .*candidate-manifest\.json inside --directory for artifact scope/,
);
});
it("allows release scope to verify assets with an external manifest", () => {
const candidate = writeCandidate();
const script = path.join(__dirname, "release-candidate.js");
const manifest = createCandidateManifest(candidate.directory, candidate.metadata);
const releaseDirectory = copyReleaseAssets(candidate.directory, manifest);
const externalDirectory = tempDirectory();
const externalManifestPath = path.join(externalDirectory, "candidate-manifest.json");
fs.writeFileSync(externalManifestPath, `${JSON.stringify(manifest)}\n`);
const result = spawnSync(
process.execPath,
[
script,
"verify",
"--directory", releaseDirectory,
"--manifest", externalManifestPath,
"--scope", "release",
"--source-sha", SOURCE_SHA,
"--version", candidate.metadata.version,
"--channel", candidate.metadata.channel,
],
{ encoding: "utf8" },
);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stderr, "");
assert.equal(JSON.parse(result.stdout).scope, "release");
});
});

245
scripts/release-workflow.test.sh Normal file → Executable file
View File

@@ -2,146 +2,129 @@
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
# SPDX-License-Identifier: MIT
# Keep the release pipeline's trust boundaries visible in a fast, dependency-free
# test. This deliberately inspects the workflow as text: GitHub Actions has no
# stable local schema validator for all of the expression and inline-script
# constructs used here.
set -euo pipefail
workflow=".github/workflows/release.yml"
goreleaser=".goreleaser.yml"
# This verifies the release workflow's declarative contract. The shell commands
# inside individual steps are exercised by the beta release rehearsal instead.
ruby -ryaml <<'RUBY'
workflow = YAML.load_file(".github/workflows/release.yml")
goreleaser = YAML.load_file(".goreleaser.yml")
fail() {
echo "release workflow contract: $*" >&2
exit 1
def fail(message)
abort("release workflow contract: #{message}")
end
def expect_equal(actual, expected, description)
return if actual == expected
fail("#{description}; expected #{expected.inspect}, got #{actual.inspect}")
end
def scalar_values(value)
case value
when Hash then value.values.flat_map { |item| scalar_values(item) }
when Array then value.flat_map { |item| scalar_values(item) }
else [value]
end
end
def action_references(value)
case value
when Hash
value.flat_map { |key, item| key == "uses" ? [item] : action_references(item) }
when Array
value.flat_map { |item| action_references(item) }
else
[]
end
end
jobs = workflow.fetch("jobs")
expected_jobs = %w[preflight build-sign-notarize create-draft-release verify-macos publish-github publish-npm]
expect_equal(jobs.keys.sort, expected_jobs.sort, "release jobs")
expect_equal(workflow.fetch("concurrency"), {
"group" => "release-${{ github.ref_name }}",
"cancel-in-progress" => false,
}, "release concurrency")
expected_needs = {
"preflight" => nil,
"build-sign-notarize" => "preflight",
"create-draft-release" => %w[preflight build-sign-notarize],
"verify-macos" => %w[preflight create-draft-release],
"publish-github" => %w[preflight create-draft-release verify-macos],
"publish-npm" => %w[preflight build-sign-notarize publish-github],
}
expected_needs.each do |job_name, needs|
expect_equal(jobs.fetch(job_name)["needs"], needs, "#{job_name} dependencies")
end
require() {
local needle="$1"
local haystack="$2"
local message="$3"
grep -Fq -- "$needle" <<<"$haystack" || fail "$message (missing: $needle)"
expected_permissions = {
"preflight" => { "contents" => "read" },
"build-sign-notarize" => { "contents" => "read" },
"create-draft-release" => { "contents" => "write" },
"verify-macos" => { "contents" => "write" },
"publish-github" => { "contents" => "write" },
"publish-npm" => { "contents" => "read", "id-token" => "write" },
}
expected_permissions.each do |job_name, permissions|
expect_equal(jobs.fetch(job_name)["permissions"], permissions, "#{job_name} permissions")
end
job_section() {
local job="$1"
awk -v job="$job" '
$0 == " " job ":" { in_job = 1; print; next }
in_job && /^ [A-Za-z0-9_-]+:$/ { exit }
in_job { print }
' "$workflow"
}
signing_references = %w[
secrets.MACOS_SIGN_P12
secrets.MACOS_SIGN_PASSWORD
secrets.MACOS_NOTARY_KEY
vars.MACOS_NOTARY_KEY_ID
vars.MACOS_NOTARY_ISSUER_ID
]
team_reference = "vars.MACOS_TEAM_ID"
jobs.each do |job_name, job|
references = scalar_values(job).grep(String).flat_map do |value|
(signing_references + [team_reference]).select { |reference| value.include?(reference) }
end.uniq.sort
expected_references = case job_name
when "build-sign-notarize" then signing_references + [team_reference]
when "verify-macos" then [team_reference]
else []
end
expect_equal(
references,
expected_references.sort,
"#{job_name} Apple credential scope",
)
end
[[ -f "$workflow" ]] || fail "missing $workflow"
[[ -f "$goreleaser" ]] || fail "missing $goreleaser"
macos = jobs.fetch("verify-macos")
expect_equal(macos.fetch("strategy").fetch("matrix").fetch("include"), [
{ "runner" => "macos-15-intel", "arch" => "amd64" },
{ "runner" => "macos-15", "arch" => "arm64" },
], "macOS verification matrix")
expect_equal(macos.fetch("runs-on"), "${{ matrix.runner }}", "macOS matrix runner")
mapfile -t jobs < <(awk '
/^jobs:$/ { in_jobs = 1; next }
in_jobs && /^ [A-Za-z0-9_-]+:$/ {
name = $0
sub(/^ /, "", name)
sub(/:$/, "", name)
print name
}
' "$workflow")
expected_jobs=(preflight build-sign-notarize create-draft-release publish-github publish-npm verify-macos)
if [[ "${jobs[*]}" != "${expected_jobs[*]}" ]]; then
fail "expected exactly the six release jobs: ${expected_jobs[*]}; got: ${jobs[*]:-(none)}"
fi
npm_steps = jobs.fetch("publish-npm").fetch("steps")
pinned_npm = npm_steps.find { |step| step["name"] == "Install pinned npm" }
fail("publish-npm must install npm 11.16.0 for trusted publishing") unless pinned_npm&.fetch("run", nil) == "npm install --global npm@11.16.0"
preflight_section="$(job_section preflight)"
build_section="$(job_section build-sign-notarize)"
draft_section="$(job_section create-draft-release)"
github_section="$(job_section publish-github)"
npm_section="$(job_section publish-npm)"
macos_section="$(job_section verify-macos)"
action_references(workflow).each do |reference|
fail("action is not pinned to a full commit SHA: #{reference}") unless reference.match?(%r{\A[^@]+@[0-9a-f]{40}\z})
end
[[ -n "$preflight_section" && -n "$build_section" && -n "$draft_section" && -n "$github_section" && -n "$npm_section" && -n "$macos_section" ]] || fail "every release job must have a nonempty section"
if grep -Eq '^[[:space:]]*needs:' <<<"$preflight_section"; then
fail "preflight must start the release dependency graph"
fi
for requirement in 'needs: preflight'; do require "$requirement" "$build_section" "build-sign-notarize must follow preflight"; done
for requirement in ' - preflight' ' - build-sign-notarize'; do require "$requirement" "$draft_section" "create-draft-release must wait for the signed candidate"; done
for requirement in ' - preflight' ' - create-draft-release'; do require "$requirement" "$macos_section" "verify-macos must verify the draft Release"; done
for requirement in ' - preflight' ' - build-sign-notarize' ' - create-draft-release' ' - verify-macos'; do require "$requirement" "$github_section" "publish-github must wait for every verification gate"; done
for requirement in ' - preflight' ' - build-sign-notarize' ' - publish-github'; do require "$requirement" "$npm_section" "publish-npm must happen after GitHub publication"; done
notarize = goreleaser.fetch("notarize").fetch("macos")
expect_equal(notarize.length, 1, "number of macOS notarization configurations")
macos_notarize = notarize.first
expect_equal(macos_notarize.fetch("ids"), ["lark-cli"], "notarized build IDs")
expect_equal(macos_notarize.fetch("sign"), {
"certificate" => "{{ .Env.MACOS_SIGN_P12 }}",
"password" => "{{ .Env.MACOS_SIGN_PASSWORD }}",
}, "macOS signing inputs")
expect_equal(macos_notarize.fetch("notarize"), {
"issuer_id" => "{{ .Env.MACOS_NOTARY_ISSUER_ID }}",
"key_id" => "{{ .Env.MACOS_NOTARY_KEY_ID }}",
"key" => "{{ .Env.MACOS_NOTARY_KEY_PATH }}",
"wait" => true,
"timeout" => "20m",
}, "macOS notarization inputs")
concurrency_section="$(awk '
/^concurrency:$/ { in_concurrency = 1; print; next }
in_concurrency && /^[^[:space:]]/ { exit }
in_concurrency { print }
' "$workflow")"
require 'group: release-${{ github.ref_name }}' "$concurrency_section" "release concurrency must be per tag"
require 'cancel-in-progress: false' "$concurrency_section" "release tags must not cancel an active publication"
if grep -Eiq 'channel|lark-cli-release|release-[[:space:]]*$' <<<"$concurrency_section"; then
fail "release concurrency must not use a channel-wide lock"
fi
# Every third-party action must be immutable: action tags can be retargeted.
awk '
/^[[:space:]]*[-]?[[:space:]]*uses:[[:space:]]*/ {
action = $0
sub(/^.*uses:[[:space:]]*/, "", action)
sub(/[[:space:]]*(#.*)?$/, "", action)
split(action, parts, "@")
if (length(parts) != 2 || length(parts[2]) != 40 || parts[2] !~ /^[0-9a-f]{40}$/) {
printf "un-pinned action: %s\\n", action > "/dev/stderr"
bad = 1
}
count++
}
END { if (count == 0 || bad) exit 1 }
' "$workflow" || fail "all release actions must be pinned to 40-hex commit SHAs"
require 'version: 2' "$(head -n 1 "$goreleaser")" "GoReleaser config must use v2 schema"
require 'version: v2.17.1' "$build_section" "build must use the approved GoReleaser version"
require 'args: release --clean --skip=publish' "$build_section" "GoReleaser must build only; publication is separately gated"
for requirement in 'notarize:' 'enabled:' 'MACOS_SIGN_P12' 'MACOS_NOTARY_KEY_PATH'; do require "$requirement" "$(<"$goreleaser")" "GoReleaser must retain macOS signing/notarization configuration"; done
for requirement in ' - darwin' ' - linux' ' - windows' ' - amd64' ' - arm64' ' - riscv64' 'formats: [tar.gz]' 'formats: [zip]'; do require "$requirement" "$(<"$goreleaser")" "GoReleaser must produce the supported release archive matrix"; done
require 'contents: read' "$build_section" "the signing build job must remain read-only"
if grep -Eq '^[[:space:]]*(contents|actions|packages|id-token):[[:space:]]*write' <<<"$build_section"; then
fail "the signing build job must not receive write permissions"
fi
for secret in MACOS_NOTARY_KEY MACOS_SIGN_P12 MACOS_SIGN_PASSWORD; do
require "secrets.${secret}" "$build_section" "build-sign-notarize must receive ${secret}"
for job in preflight create-draft-release publish-github publish-npm verify-macos; do
if grep -Fq "secrets.${secret}" <<<"$(job_section "$job")"; then
fail "${secret} must be available only to build-sign-notarize"
fi
done
done
for requirement in 'umask 077' 'mktemp "${RUNNER_TEMP}/macos-notary-key.XXXXXX"' 'chmod 0600 "$notary_key"' 'trap cleanup EXIT'; do require "$requirement" "$build_section" "Apple key preparation must securely handle the temporary key"; done
require 'name: Clean up Apple notarization key' "$build_section" "Apple key cleanup must always run"
require 'if: ${{ always() }}' "$build_section" "Apple key cleanup must run after failures"
require 'rm -f -- "$MACOS_NOTARY_KEY_PATH"' "$build_section" "Apple key cleanup must remove the temporary key"
require 'uses: actions/upload-artifact@' "$build_section" "the signed release candidate must be uploaded as an artifact"
require 'if-no-files-found: error' "$build_section" "candidate upload must fail closed"
for section_name in draft_section github_section npm_section; do
section="${!section_name}"
require 'uses: actions/download-artifact@' "$section" "each release gate must download the exact candidate artifact"
require 'digest-mismatch: error' "$section" "candidate artifact digests must fail closed"
done
require 'draft: true' "$draft_section" "the candidate must first be created as a draft GitHub Release"
require 'matrix:' "$macos_section" "macOS verification must cover both supported architectures"
for requirement in 'runner: macos-15-intel' 'arch: amd64' 'runner: macos-15' 'arch: arm64' 'codesign --verify --strict --verbose=4' 'spctl --assess --type execute --verbose=4' 'source=Notarized Developer ID'; do require "$requirement" "$macos_section" "macOS verification must retain signing and Gatekeeper checks"; done
require 'Install the candidate through the public Release' "$github_section" "GitHub publication must be followed by a public candidate install"
require 'npm install --global --prefix' "$github_section" "candidate install must exercise the packed npm artifact"
require 'id-token: write' "$npm_section" "npm trusted publishing requires GitHub OIDC"
require 'path.resolve("release-candidate", manifest.npmPackage.name)' "$npm_section" "npm publish must use the original verified candidate tarball"
require 'npm", ["publish", tgz, "--access", "public", "--provenance", "--tag", before.distTag]' "$npm_section" "npm publish must use provenance and the evaluated dist-tag"
for requirement in '"dist-tags"' 'evaluateNpmState' 'afterState.distTags?.[before.distTag] !== env.VERSION'; do require "$requirement" "$npm_section" "npm publication must validate the target dist-tag"; done
checkout_jobs=0
while IFS= read -r job; do
section="$(job_section "$job")"
if grep -Fq 'actions/checkout@' <<<"$section"; then
checkout_jobs=$((checkout_jobs + 1))
require 'persist-credentials: false' "$section" "checkout in ${job} must not persist credentials"
fi
done < <(printf '%s\n' "${jobs[@]}")
(( checkout_jobs > 0 )) || fail "release workflow must explicitly check out source where needed"
echo "release workflow contract passed"
puts "release workflow contract passed"
RUBY