scripts/generate-release-attestation.mjs
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env node
// Generate the D9 source-OTA attestation for a release checkout.
//
// Run by .github/workflows/release.yml after SHA256SUMS exists and before the
// attestation is signed with cosign. The output is deterministic: fixed field
// order and sorted asset names, so re-running on the same bytes produces the
// same file and therefore the same signature payload.
//
// Usage:
// node scripts/generate-release-attestation.mjs \
// --tag v1.2.3 --commit <40-hex> --tree <40-hex> \
// [--root .] [--out release-attestation.json] [--repository dwgx/WindsurfAPI] \
// [--asset name=path ...]
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import {
DEFAULT_ATTESTATION_REPOSITORY,
DEFAULT_RELEASE_ASSETS,
ReleaseAttestationError,
buildReleaseAttestation,
serializeReleaseAttestation,
} from '../src/release-attestation.js';
class CliError extends Error {
constructor(message, exitCode = 2) {
super(message);
this.name = 'CliError';
this.exitCode = exitCode;
}
}
export function parseArgs(argv) {
const options = {
tag: '',
commit: '',
tree: '',
root: '.',
out: '',
repository: DEFAULT_ATTESTATION_REPOSITORY,
assets: {},
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === '-h' || arg === '--help') return { help: true };
if (!['--tag', '--commit', '--tree', '--root', '--out', '--repository', '--asset'].includes(arg)) {
throw new CliError(`unknown option: ${arg}`);
}
const value = argv[index + 1];
if (value === undefined || value === '') throw new CliError(`${arg} needs a value`);
index += 1;
if (arg === '--tag') options.tag = value;
if (arg === '--commit') options.commit = value;
if (arg === '--tree') options.tree = value;
if (arg === '--root') options.root = value;
if (arg === '--out') options.out = value;
if (arg === '--repository') options.repository = value;
if (arg === '--asset') {
const separator = value.indexOf('=');
if (separator <= 0 || separator === value.length - 1) {
throw new CliError(`--asset must be name=path, got ${value}`);
}
options.assets[value.slice(0, separator)] = value.slice(separator + 1);
}
}
if (!options.tag) throw new CliError('--tag is required');
if (!options.commit) throw new CliError('--commit is required');
if (!options.tree) throw new CliError('--tree is required');
return options;
}
export function usage() {
return `Generate the WindsurfAPI release attestation.
Usage:
node scripts/generate-release-attestation.mjs --tag <vX.Y.Z> --commit <sha> --tree <sha>
[options]
Options:
--tag <vX.Y.Z> release tag (required)
--commit <sha> commit the tag peels to (required)
--tree <sha> git tree of that commit (required)
--root <dir> checkout root (default: .)
--out <file> output file (default: stdout)
--repository <owner/name> repository recorded in the attestation
--asset <name=path> release-asset name -> path relative to --root
(repeatable; default: the release job layout)
-h, --help show this help`;
}
export async function main(argv) {
const options = parseArgs(argv);
if (options.help) {
console.log(usage());
return 0;
}
const assets = Object.keys(options.assets).length ? options.assets : DEFAULT_RELEASE_ASSETS;
const attestation = await buildReleaseAttestation({
tag: options.tag,
commit: options.commit,
tree: options.tree,
root: resolve(options.root),
assets,
repository: options.repository,
});
const text = serializeReleaseAttestation(attestation);
if (options.out) {
writeFileSync(resolve(options.out), text, 'utf8');
console.log(`wrote ${resolve(options.out)}`);
} else {
process.stdout.write(text);
}
return 0;
}
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
if (import.meta.url === invokedPath) {
main(process.argv.slice(2))
.then((code) => {
process.exitCode = code;
})
.catch((err) => {
console.error(`error: ${err.message}`);
process.exitCode = err instanceof CliError || err instanceof ReleaseAttestationError ? err.exitCode || 1 : 1;
});
}