Files
qinglong/scripts/ql3-release-candidate-contract.cjs
T

384 lines
11 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { auditPackageBoundaries } = require('./ql3-package-boundary-audit.cjs');
const {
VERSION_PATTERN,
readReleaseIdentity,
} = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/release-candidate-contract@v1';
const PREDICATE_TYPE =
'https://qinglong.dev/attestations/release-candidate-contract/v1';
const MAX_REPORT_BYTES = 1024 * 1024;
const RELEASE_SCOPES = Object.freeze(['all', 'cluster', 'local']);
const LOCAL_IMAGES = Object.freeze([
Object.freeze({
image: 'local',
repository: 'qinglong3-local-application',
dockerfile: 'deploy/containers/ql3-local-application/Dockerfile',
target: 'runtime',
runtime_root:
'deploy/containers/ql3-local-application/runtime-dependencies',
}),
]);
const CLUSTER_IMAGES = Object.freeze([
Object.freeze({
image: 'control',
repository: 'qinglong3-cluster-control',
dockerfile: 'deploy/containers/ql3-cluster-control/Dockerfile',
target: 'runtime',
runtime_root: 'deploy/containers/ql3-cluster-control/runtime-dependencies',
}),
Object.freeze({
image: 'control-ai',
repository: 'qinglong3-cluster-control-ai',
dockerfile: 'deploy/containers/ql3-cluster-control/Dockerfile',
target: 'runtime-ai',
runtime_root: 'deploy/containers/ql3-cluster-control/runtime-dependencies',
}),
Object.freeze({
image: 'admin',
repository: 'qinglong3-cluster-admin',
dockerfile: 'deploy/containers/ql3-cluster-admin/Dockerfile',
target: 'runtime',
runtime_root: 'deploy/containers/ql3-cluster-admin/runtime-dependencies',
}),
Object.freeze({
image: 'worker',
repository: 'qinglong3-worker',
dockerfile: 'deploy/containers/ql3-worker/Dockerfile',
target: 'runtime',
runtime_root: 'deploy/containers/ql3-worker/runtime-dependencies',
}),
]);
class ReleaseCandidateContractError extends Error {
constructor(message) {
super(`QingLong release candidate contract failed: ${message}`);
this.name = 'ReleaseCandidateContractError';
}
}
function fail(message) {
throw new ReleaseCandidateContractError(message);
}
function readJson(filePath, maximumBytes = MAX_REPORT_BYTES) {
const stat = fs.lstatSync(filePath);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 2 ||
stat.size > maximumBytes
) {
fail(`invalid bounded JSON file: ${filePath}`);
}
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function sha256(value) {
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
}
function selectedImages(scope) {
if (scope === 'local') return [...LOCAL_IMAGES];
if (scope === 'cluster') return [...CLUSTER_IMAGES];
return [...CLUSTER_IMAGES, ...LOCAL_IMAGES];
}
function validateIdentity(options) {
if (
typeof options.version !== 'string' ||
!VERSION_PATTERN.test(options.version)
) {
fail('version must be an exact QingLong 3 SemVer');
}
if (!/^[a-f0-9]{40}$/u.test(options.sourceRevision || '')) {
fail('source revision must be an exact Git SHA-1 commit');
}
if (options.sourceRef !== `refs/tags/v${options.version}`) {
fail('source ref must be the exact version tag');
}
if (!RELEASE_SCOPES.includes(options.releaseScope)) {
fail('release scope must be all, cluster or local');
}
}
function createReleaseCandidateContract(options) {
const root = path.resolve(options.root || DEFAULT_ROOT);
validateIdentity(options);
const releaseIdentity = readReleaseIdentity(root);
if (options.version !== releaseIdentity.version) {
fail('requested version differs from the repository release identity');
}
const boundaries = auditPackageBoundaries(root);
if (
!boundaries.compatible ||
boundaries.workspacePackageCount !==
releaseIdentity.workspacePackageCount ||
boundaries.workspacePackageHardCap !==
releaseIdentity.workspacePackageCount ||
boundaries.singleSourcePackages.length !== 0 ||
boundaries.shallowSourcePackages.length !== 0
) {
fail('workspace package boundary is incompatible');
}
const workspacePackages = boundaries.packages
.map((entry) => {
const manifest = readJson(path.join(root, entry.path, 'package.json'));
if (
manifest.name !== entry.name ||
manifest.version !== options.version ||
manifest.engines?.node !== releaseIdentity.node.engine
) {
fail(`workspace release identity differs: ${entry.path}`);
}
return Object.freeze({
name: manifest.name,
path: entry.path,
version: manifest.version,
});
})
.sort((left, right) => left.name.localeCompare(right.name, 'en'));
const images = selectedImages(options.releaseScope);
const imageManifests = images.map((image) => {
const manifest = readJson(
path.join(root, image.runtime_root, 'package.json'),
);
if (
manifest.version !== options.version ||
manifest.engines?.node !== releaseIdentity.node.engine
) {
fail(`image release identity differs: ${image.runtime_root}`);
}
const dockerfile = fs.readFileSync(
path.join(root, image.dockerfile),
'utf8',
);
if (
!dockerfile.includes(
`node:${releaseIdentity.node.version}-bookworm-slim@sha256:`,
) ||
!dockerfile.includes(
`org.opencontainers.image.version=\"${options.version}\"`,
)
) {
fail(`image Dockerfile release identity differs: ${image.dockerfile}`);
}
return Object.freeze({
image: image.image,
repository: image.repository,
dockerfile: image.dockerfile,
target: image.target,
runtimeRoot: image.runtime_root,
manifestName: manifest.name,
version: manifest.version,
});
});
const publishMatrix = images.map(({ dockerfile, target, ...image }) => image);
const osMatrix = images.flatMap((image) => [
{
image: image.image,
runner: 'ubuntu-24.04',
node_arch: 'x64',
image_arch: 'amd64',
dockerfile: image.dockerfile,
target: image.target,
},
{
image: image.image,
runner: 'ubuntu-24.04-arm',
node_arch: 'arm64',
image_arch: 'arm64',
dockerfile: image.dockerfile,
target: image.target,
},
]);
const unsigned = {
schemaVersion: 1,
schema: SCHEMA,
release: {
version: options.version,
sourceRevision: options.sourceRevision,
sourceRef: options.sourceRef,
scope: options.releaseScope,
},
compatibility: {
legacyRootPackageVersion: readJson(path.join(root, 'package.json'))
.version,
legacyRootExcludedFromReleaseIdentity:
releaseIdentity.legacyRootPackageExcluded,
releaseIdentitySchema: releaseIdentity.schema,
releaseIdentityDigest: sha256(
Buffer.from(JSON.stringify(releaseIdentity)),
),
nodeVersion: releaseIdentity.node.version,
nodeEngine: releaseIdentity.node.engine,
platforms: ['linux/amd64', 'linux/arm64'],
},
workspace: {
packageCount: workspacePackages.length,
packageHardCap: boundaries.workspacePackageHardCap,
packages: workspacePackages,
},
deploymentFamilies: {
local: {
selected: options.releaseScope !== 'cluster',
profiles: ['edge', 'standalone'],
requiresClusterPrivateEvidence: false,
},
cluster: {
selected: options.releaseScope !== 'local',
profiles: ['cluster', 'worker-edge', 'worker-node'],
requiresClusterPrivateEvidence: true,
},
},
images: imageManifests,
releasePlan: {
clusterEvidenceRequired: options.releaseScope !== 'local',
osMatrix,
publishMatrix,
},
requiredGates: [
'package-boundary',
'source-tag-version-identity',
'native-os-vulnerability',
'multiarch-oci-layout',
'production-dependency-audit',
'digest-signature-and-attestations',
'cross-image-release-set',
...(options.releaseScope !== 'cluster'
? ['edge-and-standalone-rollout']
: []),
...(options.releaseScope !== 'local'
? [
'worker-management-production-evidence',
'cloudnativepg-disaster-recovery-evidence',
]
: []),
],
};
return Object.freeze({
...unsigned,
contractDigest: sha256(Buffer.from(JSON.stringify(unsigned))),
});
}
function auditReleaseCandidateContract(actual, options) {
const expected = createReleaseCandidateContract(options);
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
fail('report differs from the source-derived contract');
}
return Object.freeze({
compatible: true,
contractDigest: actual.contractDigest,
releaseScope: actual.release.scope,
workspacePackageCount: actual.workspace.packageCount,
images: Object.freeze(actual.images.map((entry) => entry.image)),
clusterEvidenceRequired: actual.releasePlan.clusterEvidenceRequired,
});
}
function writeNoReplace(filePath, value) {
const resolved = path.resolve(filePath || '');
if (
!path.isAbsolute(filePath || '') ||
fs.existsSync(resolved) ||
fs.realpathSync(path.dirname(resolved)) !== path.dirname(resolved)
) {
fail('output must be unused in one canonical directory');
}
fs.writeFileSync(resolved, `${JSON.stringify(value)}\n`, {
encoding: 'utf8',
mode: 0o600,
flag: 'wx',
});
}
function parseArguments(argv) {
const values = {};
for (const argument of argv) {
const match = /^--([a-z-]+)=(.+)$/u.exec(argument);
if (!match || Object.hasOwn(values, match[1]))
fail('arguments are invalid');
values[match[1]] = match[2];
}
const common = [
'mode',
'release-scope',
'source-ref',
'source-revision',
'version',
];
const expected =
values.mode === 'create'
? [...common, 'output']
: values.mode === 'audit'
? [...common, 'report']
: [];
if (
expected.length === 0 ||
JSON.stringify(Object.keys(values).sort()) !==
JSON.stringify(expected.sort())
) {
fail('arguments are invalid');
}
return Object.freeze({
mode: values.mode,
version: values.version,
sourceRevision: values['source-revision'],
sourceRef: values['source-ref'],
releaseScope: values['release-scope'],
...(values.output ? { output: values.output } : {}),
...(values.report ? { report: values.report } : {}),
});
}
function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) {
const options = parseArguments(argv);
if (options.mode === 'create') {
const report = createReleaseCandidateContract({ ...options, root });
writeNoReplace(options.output, report);
output.write(`${JSON.stringify(report)}\n`);
return report;
}
const report = readJson(path.resolve(options.report));
const audit = auditReleaseCandidateContract(report, { ...options, root });
output.write(`${JSON.stringify(audit)}\n`);
return audit;
}
if (require.main === module) {
try {
runCli(process.argv.slice(2));
} catch (error) {
process.stderr.write(
`${
error instanceof Error
? error.message
: 'release candidate contract failed'
}\n`,
);
process.exitCode = 1;
}
}
module.exports = Object.freeze({
CLUSTER_IMAGES,
LOCAL_IMAGES,
PREDICATE_TYPE,
RELEASE_SCOPES,
SCHEMA,
ReleaseCandidateContractError,
auditReleaseCandidateContract,
createReleaseCandidateContract,
parseArguments,
runCli,
});