mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
690 lines
21 KiB
JavaScript
690 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const {
|
|
auditLocalAlphaTrialKit,
|
|
sha256File,
|
|
VARIANTS,
|
|
} = require('./ql3-local-alpha-trial-kit-bundle.cjs');
|
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
|
|
|
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
|
const SCHEMA = 'qinglong/alpha-local-milestone@v3';
|
|
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
|
const FILES = Object.freeze({
|
|
readme: 'README.md',
|
|
manifest: 'manifest.json',
|
|
checksums: 'SHA256SUMS',
|
|
});
|
|
const WORKFLOW_IDENTITY = Object.freeze({
|
|
repository: 'whyour/qinglong',
|
|
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
|
|
event: 'workflow_dispatch',
|
|
job: 'local-alpha-milestone',
|
|
});
|
|
const REQUIRED_WORKFLOW_NEEDS = Object.freeze([
|
|
'backend',
|
|
'service-manager-bridge',
|
|
'linux-resource-envelopes',
|
|
'linux-resource-release-evidence',
|
|
'supply-chain',
|
|
'local-image',
|
|
'cluster-image',
|
|
'cluster-console-capacity-release-evidence',
|
|
'image-oci',
|
|
'worker-runtime',
|
|
'local-profiles',
|
|
'cluster-postgres',
|
|
'cluster-postgres-ha',
|
|
'cluster-cloudnativepg-live',
|
|
'cluster-provider-credential-test-kubernetes-live',
|
|
'cluster-secret-binding-mounted-provider-kubernetes-live',
|
|
'cluster-vault-kv-worker-secret-live',
|
|
'cluster-plugin-package-kubernetes-live',
|
|
'cluster-plugin-package-recovery-e2e',
|
|
]);
|
|
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
|
const DECIMAL_ID_PATTERN = /^[1-9][0-9]{0,19}$/u;
|
|
const ATTEMPT_PATTERN = /^[1-9][0-9]{0,5}$/u;
|
|
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
|
const MAX_README_BYTES = 512 * 1024;
|
|
|
|
function fail(message) {
|
|
throw new Error(message);
|
|
}
|
|
|
|
function exactKeys(value, expected) {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
JSON.stringify(Object.keys(value)) === JSON.stringify(expected)
|
|
);
|
|
}
|
|
|
|
function assertCanonicalFile(filePath, maximumBytes, label) {
|
|
const resolved = path.resolve(filePath);
|
|
const stat = fs.lstatSync(resolved);
|
|
if (
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.size < 2 ||
|
|
stat.size > maximumBytes ||
|
|
fs.realpathSync(resolved) !== resolved
|
|
) {
|
|
fail(`${label} must be one bounded canonical regular file`);
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
function readBoundedJson(filePath, label) {
|
|
const resolved = assertCanonicalFile(filePath, MAX_JSON_BYTES, label);
|
|
try {
|
|
return JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
} catch {
|
|
fail(`${label} must contain valid JSON`);
|
|
}
|
|
}
|
|
|
|
function writeExclusive(filePath, contents, mode = 0o600) {
|
|
const descriptor = fs.openSync(
|
|
filePath,
|
|
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
mode,
|
|
);
|
|
try {
|
|
fs.writeFileSync(descriptor, contents);
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function copyExclusive(source, destination) {
|
|
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
|
|
fs.chmodSync(destination, 0o600);
|
|
}
|
|
|
|
function fileRecord(filePath, name) {
|
|
const stat = fs.lstatSync(filePath);
|
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 2) {
|
|
fail(`milestone file is invalid: ${name}`);
|
|
}
|
|
return Object.freeze({
|
|
file: name,
|
|
sha256: sha256File(filePath),
|
|
bytes: stat.size,
|
|
});
|
|
}
|
|
|
|
function checksumContents(root, names) {
|
|
return `${names
|
|
.map((name) => `${sha256File(path.join(root, name)).slice(7)} ${name}`)
|
|
.join('\n')}\n`;
|
|
}
|
|
|
|
function artifactName(sourceRevision, architecture, variant = 'headless') {
|
|
return `ql3-alpha-${sourceRevision}-local-${variant}-${architecture}`;
|
|
}
|
|
|
|
function validateIdentity(options) {
|
|
if (
|
|
!REVISION_PATTERN.test(options.sourceRevision || '') ||
|
|
options.repository !== WORKFLOW_IDENTITY.repository ||
|
|
options.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
|
|
options.workflowSha !== options.sourceRevision ||
|
|
options.eventName !== WORKFLOW_IDENTITY.event ||
|
|
!DECIMAL_ID_PATTERN.test(options.runId || '') ||
|
|
!ATTEMPT_PATTERN.test(options.runAttempt || '')
|
|
) {
|
|
fail('milestone workflow identity is invalid');
|
|
}
|
|
}
|
|
|
|
function validateFinalizeOptions(options) {
|
|
validateIdentity(options);
|
|
const root = fs.realpathSync(path.resolve(options.root || DEFAULT_ROOT));
|
|
const outputRoot = path.resolve(options.outputRoot || '');
|
|
const parent = path.dirname(outputRoot);
|
|
if (
|
|
!VARIANTS.includes(options.variant) ||
|
|
!path.isAbsolute(outputRoot) ||
|
|
fs.existsSync(outputRoot) ||
|
|
fs.realpathSync(parent) !== parent
|
|
) {
|
|
fail('milestone output is invalid');
|
|
}
|
|
const bundles = {};
|
|
for (const architecture of ARCHITECTURES) {
|
|
const bundleRoot = fs.realpathSync(
|
|
path.resolve(options.bundles?.[architecture] || ''),
|
|
);
|
|
if (!fs.lstatSync(bundleRoot).isDirectory()) {
|
|
fail(`${architecture} bundle root is invalid`);
|
|
}
|
|
bundles[architecture] = bundleRoot;
|
|
}
|
|
if (bundles.amd64 === bundles.arm64) {
|
|
fail('milestone architectures must use distinct bundles');
|
|
}
|
|
return Object.freeze({
|
|
root,
|
|
outputRoot,
|
|
bundles: Object.freeze(bundles),
|
|
readme: assertCanonicalFile(
|
|
options.readme,
|
|
MAX_README_BYTES,
|
|
'milestone README',
|
|
),
|
|
sourceRevision: options.sourceRevision,
|
|
variant: options.variant,
|
|
repository: options.repository,
|
|
workflowRef: options.workflowRef,
|
|
workflowSha: options.workflowSha,
|
|
eventName: options.eventName,
|
|
runId: options.runId,
|
|
runAttempt: options.runAttempt,
|
|
});
|
|
}
|
|
|
|
function bundleRecord(options, architecture) {
|
|
const bundleRoot = options.bundles[architecture];
|
|
const report = auditLocalAlphaTrialKit({ bundleRoot });
|
|
if (
|
|
report.compatible !== true ||
|
|
report.architecture !== architecture ||
|
|
report.sourceRevision !== options.sourceRevision ||
|
|
report.workflowRunId !== options.runId ||
|
|
report.workflowRunAttempt !== options.runAttempt ||
|
|
report.variant !== options.variant
|
|
) {
|
|
fail(`${architecture} trial kit is detached from the milestone run`);
|
|
}
|
|
return Object.freeze({
|
|
artifactName: artifactName(
|
|
options.sourceRevision,
|
|
architecture,
|
|
options.variant,
|
|
),
|
|
architecture,
|
|
bundleManifest: fileRecord(
|
|
path.join(bundleRoot, 'manifest.json'),
|
|
'manifest.json',
|
|
),
|
|
archiveSha256: report.archiveSha256,
|
|
applicationImageId: report.applicationImageId,
|
|
operatorImageId: report.operatorImageId,
|
|
verificationSha256: report.verificationSha256,
|
|
upgradeReadinessSha256: report.upgradeReadinessSha256,
|
|
});
|
|
}
|
|
|
|
function validateArtifactRecord(record, architecture, manifest) {
|
|
if (
|
|
!exactKeys(record, [
|
|
'artifactName',
|
|
'architecture',
|
|
'bundleManifest',
|
|
'archiveSha256',
|
|
'applicationImageId',
|
|
'operatorImageId',
|
|
'verificationSha256',
|
|
'upgradeReadinessSha256',
|
|
]) ||
|
|
record.artifactName !==
|
|
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
|
|
record.architecture !== architecture ||
|
|
!exactKeys(record.bundleManifest, ['file', 'sha256', 'bytes']) ||
|
|
record.bundleManifest.file !== 'manifest.json' ||
|
|
!SHA256_PATTERN.test(record.bundleManifest.sha256 || '') ||
|
|
!Number.isSafeInteger(record.bundleManifest.bytes) ||
|
|
record.bundleManifest.bytes < 2 ||
|
|
!SHA256_PATTERN.test(record.archiveSha256 || '') ||
|
|
!SHA256_PATTERN.test(record.applicationImageId || '') ||
|
|
!SHA256_PATTERN.test(record.operatorImageId || '') ||
|
|
!SHA256_PATTERN.test(record.verificationSha256 || '') ||
|
|
!SHA256_PATTERN.test(record.upgradeReadinessSha256 || '') ||
|
|
record.applicationImageId === record.operatorImageId
|
|
) {
|
|
fail(`${architecture} milestone artifact record is incompatible`);
|
|
}
|
|
}
|
|
|
|
function validateWorkflow(document, sourceRevision, runId, runAttempt) {
|
|
if (
|
|
!exactKeys(document, [
|
|
'repository',
|
|
'workflowRef',
|
|
'workflowSha',
|
|
'event',
|
|
'job',
|
|
'runId',
|
|
'runAttempt',
|
|
]) ||
|
|
document.repository !== WORKFLOW_IDENTITY.repository ||
|
|
document.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
|
|
document.workflowSha !== sourceRevision ||
|
|
document.event !== WORKFLOW_IDENTITY.event ||
|
|
document.job !== WORKFLOW_IDENTITY.job ||
|
|
document.runId !== runId ||
|
|
document.runAttempt !== runAttempt ||
|
|
!DECIMAL_ID_PATTERN.test(document.runId || '') ||
|
|
!ATTEMPT_PATTERN.test(document.runAttempt || '')
|
|
) {
|
|
fail('milestone manifest workflow identity is incompatible');
|
|
}
|
|
}
|
|
|
|
function auditLocalAlphaMilestone(options) {
|
|
const milestoneRoot = fs.realpathSync(
|
|
path.resolve(options.milestoneRoot || ''),
|
|
);
|
|
if (!fs.lstatSync(milestoneRoot).isDirectory()) {
|
|
fail('milestone root must be a canonical directory');
|
|
}
|
|
const expectedFiles = Object.values(FILES).sort();
|
|
const actualFiles = fs
|
|
.readdirSync(milestoneRoot, { withFileTypes: true })
|
|
.map((entry) => {
|
|
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
fail(`milestone contains a non-regular entry: ${entry.name}`);
|
|
}
|
|
return entry.name;
|
|
})
|
|
.sort();
|
|
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
|
|
fail('milestone file set is not closed');
|
|
}
|
|
const manifest = readBoundedJson(
|
|
path.join(milestoneRoot, FILES.manifest),
|
|
'milestone manifest',
|
|
);
|
|
if (
|
|
!exactKeys(manifest, [
|
|
'schemaVersion',
|
|
'schema',
|
|
'maturity',
|
|
'product',
|
|
'variant',
|
|
'version',
|
|
'sourceRevision',
|
|
'workflow',
|
|
'artifacts',
|
|
'readme',
|
|
]) ||
|
|
manifest.schemaVersion !== 3 ||
|
|
manifest.schema !== SCHEMA ||
|
|
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
|
manifest.product !== 'local' ||
|
|
!VARIANTS.includes(manifest.variant) ||
|
|
typeof manifest.version !== 'string' ||
|
|
manifest.version.length < 3 ||
|
|
manifest.version.length > 64 ||
|
|
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
|
|
!exactKeys(manifest.artifacts, ARCHITECTURES) ||
|
|
!exactKeys(manifest.readme, ['file', 'sha256', 'bytes']) ||
|
|
manifest.readme.file !== FILES.readme ||
|
|
!SHA256_PATTERN.test(manifest.readme.sha256 || '') ||
|
|
!Number.isSafeInteger(manifest.readme.bytes) ||
|
|
manifest.readme.bytes < 2
|
|
) {
|
|
fail('milestone manifest identity or shape is incompatible');
|
|
}
|
|
validateWorkflow(
|
|
manifest.workflow,
|
|
manifest.sourceRevision,
|
|
manifest.workflow?.runId,
|
|
manifest.workflow?.runAttempt,
|
|
);
|
|
for (const architecture of ARCHITECTURES) {
|
|
validateArtifactRecord(
|
|
manifest.artifacts[architecture],
|
|
architecture,
|
|
manifest,
|
|
);
|
|
}
|
|
const records = ARCHITECTURES.map(
|
|
(architecture) => manifest.artifacts[architecture],
|
|
);
|
|
const imageIds = records.flatMap((record) => [
|
|
record.applicationImageId,
|
|
record.operatorImageId,
|
|
]);
|
|
if (
|
|
new Set(imageIds).size !== imageIds.length ||
|
|
new Set(records.map((record) => record.archiveSha256)).size !==
|
|
ARCHITECTURES.length ||
|
|
new Set(records.map((record) => record.verificationSha256)).size !==
|
|
ARCHITECTURES.length ||
|
|
new Set(records.map((record) => record.upgradeReadinessSha256)).size !==
|
|
ARCHITECTURES.length
|
|
) {
|
|
fail('milestone architecture subjects are not distinct');
|
|
}
|
|
const actualReadme = fileRecord(
|
|
path.join(milestoneRoot, FILES.readme),
|
|
FILES.readme,
|
|
);
|
|
if (
|
|
actualReadme.sha256 !== manifest.readme.sha256 ||
|
|
actualReadme.bytes !== manifest.readme.bytes
|
|
) {
|
|
fail('milestone README differs from manifest');
|
|
}
|
|
const expectedChecksums = checksumContents(milestoneRoot, [
|
|
FILES.readme,
|
|
FILES.manifest,
|
|
]);
|
|
const actualChecksums = fs.readFileSync(
|
|
path.join(milestoneRoot, FILES.checksums),
|
|
'utf8',
|
|
);
|
|
if (actualChecksums !== expectedChecksums) {
|
|
fail('milestone SHA256SUMS differs from the closed file set');
|
|
}
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
schema: 'qinglong/alpha-local-milestone-audit@v3',
|
|
sourceRevision: manifest.sourceRevision,
|
|
version: manifest.version,
|
|
variant: manifest.variant,
|
|
workflowRunId: manifest.workflow.runId,
|
|
workflowRunAttempt: manifest.workflow.runAttempt,
|
|
architectures: [...ARCHITECTURES],
|
|
compatible: true,
|
|
});
|
|
}
|
|
|
|
function finalizeLocalAlphaMilestone(options) {
|
|
const normalized = validateFinalizeOptions(options);
|
|
const artifacts = {
|
|
amd64: bundleRecord(normalized, 'amd64'),
|
|
arm64: bundleRecord(normalized, 'arm64'),
|
|
};
|
|
const versions = new Set(
|
|
ARCHITECTURES.map((architecture) => {
|
|
const manifest = readBoundedJson(
|
|
path.join(normalized.bundles[architecture], 'manifest.json'),
|
|
`${architecture} trial kit manifest`,
|
|
);
|
|
return manifest.version;
|
|
}),
|
|
);
|
|
const release = readReleaseIdentity(normalized.root);
|
|
if (versions.size !== 1 || !versions.has(release.version)) {
|
|
fail('milestone trial kits must have one release version');
|
|
}
|
|
const allImageIds = ARCHITECTURES.flatMap((architecture) => [
|
|
artifacts[architecture].applicationImageId,
|
|
artifacts[architecture].operatorImageId,
|
|
]);
|
|
if (
|
|
new Set(allImageIds).size !== allImageIds.length ||
|
|
artifacts.amd64.archiveSha256 === artifacts.arm64.archiveSha256 ||
|
|
artifacts.amd64.verificationSha256 === artifacts.arm64.verificationSha256 ||
|
|
artifacts.amd64.upgradeReadinessSha256 ===
|
|
artifacts.arm64.upgradeReadinessSha256
|
|
) {
|
|
fail('milestone architecture subjects must be distinct');
|
|
}
|
|
let created = false;
|
|
try {
|
|
fs.mkdirSync(normalized.outputRoot, { mode: 0o700 });
|
|
created = true;
|
|
copyExclusive(
|
|
normalized.readme,
|
|
path.join(normalized.outputRoot, FILES.readme),
|
|
);
|
|
const manifest = {
|
|
schemaVersion: 3,
|
|
schema: SCHEMA,
|
|
maturity: 'alpha_candidate_not_public_release',
|
|
product: 'local',
|
|
variant: normalized.variant,
|
|
version: [...versions][0],
|
|
sourceRevision: normalized.sourceRevision,
|
|
workflow: {
|
|
repository: normalized.repository,
|
|
workflowRef: normalized.workflowRef,
|
|
workflowSha: normalized.workflowSha,
|
|
event: normalized.eventName,
|
|
job: WORKFLOW_IDENTITY.job,
|
|
runId: normalized.runId,
|
|
runAttempt: normalized.runAttempt,
|
|
},
|
|
artifacts,
|
|
readme: fileRecord(
|
|
path.join(normalized.outputRoot, FILES.readme),
|
|
FILES.readme,
|
|
),
|
|
};
|
|
writeExclusive(
|
|
path.join(normalized.outputRoot, FILES.manifest),
|
|
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
);
|
|
writeExclusive(
|
|
path.join(normalized.outputRoot, FILES.checksums),
|
|
checksumContents(normalized.outputRoot, [FILES.readme, FILES.manifest]),
|
|
);
|
|
auditLocalAlphaMilestone({ milestoneRoot: normalized.outputRoot });
|
|
return Object.freeze(manifest);
|
|
} catch (error) {
|
|
if (created) {
|
|
fs.rmSync(normalized.outputRoot, { recursive: true, force: true });
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function countOccurrences(contents, token) {
|
|
return contents.split(token).length - 1;
|
|
}
|
|
|
|
function jobBlock(workflow, jobName) {
|
|
const header = `\n ${jobName}:\n`;
|
|
const start = workflow.indexOf(header);
|
|
if (start < 0) return '';
|
|
const remaining = workflow.slice(start + header.length);
|
|
const nextMatch = /\n [a-z0-9-]+:\n/u.exec(remaining);
|
|
const end = nextMatch
|
|
? start + header.length + nextMatch.index
|
|
: workflow.length;
|
|
return workflow.slice(start, end);
|
|
}
|
|
|
|
function auditLocalAlphaMilestoneWorkflow(root = DEFAULT_ROOT) {
|
|
const workflowPath = path.join(
|
|
fs.realpathSync(path.resolve(root)),
|
|
'.github/workflows/ql3-ci.yml',
|
|
);
|
|
const workflow = fs.readFileSync(workflowPath, 'utf8');
|
|
const findings = [];
|
|
const milestoneStart = workflow.indexOf('\n local-alpha-milestone:\n');
|
|
const milestone = jobBlock(workflow, 'local-alpha-milestone');
|
|
const requiredWorkflowTokens = [
|
|
'alpha_artifact_scope:',
|
|
'default: local',
|
|
'- local',
|
|
'- cluster',
|
|
'- all',
|
|
'local_alpha_variant:',
|
|
'default: headless',
|
|
'- headless',
|
|
'- console',
|
|
"github.run_id || 'validation'",
|
|
"cancel-in-progress: ${{ !(github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts) }}",
|
|
];
|
|
if (requiredWorkflowTokens.some((token) => !workflow.includes(token))) {
|
|
findings.push('MILESTONE_DISPATCH_OR_CONCURRENCY_DRIFT');
|
|
}
|
|
const localScopeCondition =
|
|
"github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')";
|
|
const clusterScopeCondition =
|
|
"github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'cluster' || inputs.alpha_artifact_scope == 'all')";
|
|
if (
|
|
countOccurrences(workflow, localScopeCondition) !== 3 ||
|
|
countOccurrences(workflow, clusterScopeCondition) !== 3
|
|
) {
|
|
findings.push('MILESTONE_SCOPE_CONTRACT_DRIFT');
|
|
}
|
|
const milestoneTokens = [
|
|
' name: Finalize the Local Alpha milestone',
|
|
' needs:',
|
|
'pnpm/action-setup@v6',
|
|
'cache-dependency-path: pnpm-lock.yaml',
|
|
'pnpm install --frozen-lockfile --ignore-scripts',
|
|
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c',
|
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-amd64`,
|
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-arm64`,
|
|
'scripts/ql3-local-alpha-milestone.cjs',
|
|
'--mode=finalize',
|
|
'--variant=${{ inputs.local_alpha_variant }}',
|
|
'--mode=audit',
|
|
`name: ql3-alpha-${'${{ github.sha }}'}-local-${'${{ inputs.local_alpha_variant }}'}-milestone`,
|
|
'retention-days: 30',
|
|
'overwrite: false',
|
|
];
|
|
if (
|
|
milestoneStart < 0 ||
|
|
milestoneTokens.some((token) => !milestone.includes(token)) ||
|
|
REQUIRED_WORKFLOW_NEEDS.some(
|
|
(job) => !milestone.includes(` - ${job}\n`),
|
|
)
|
|
) {
|
|
findings.push('MILESTONE_FINALIZER_CONTRACT_DRIFT');
|
|
}
|
|
const dependencyInstallIndex = milestone.indexOf(
|
|
'pnpm install --frozen-lockfile --ignore-scripts',
|
|
);
|
|
const finalizerIndex = milestone.indexOf('--mode=finalize');
|
|
const auditIndex = milestone.indexOf('--mode=audit');
|
|
const uploadIndex = milestone.lastIndexOf('actions/upload-artifact@');
|
|
if (
|
|
dependencyInstallIndex < 0 ||
|
|
finalizerIndex <= dependencyInstallIndex ||
|
|
auditIndex <= finalizerIndex ||
|
|
uploadIndex <= auditIndex
|
|
) {
|
|
findings.push('MILESTONE_FINALIZER_GATE_ORDER_DRIFT');
|
|
}
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
schema: 'qinglong/alpha-local-milestone-workflow-audit@v1',
|
|
requiredNeeds: [...REQUIRED_WORKFLOW_NEEDS],
|
|
findings: Object.freeze(findings),
|
|
compatible: findings.length === 0,
|
|
});
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const values = {};
|
|
for (const argument of argv) {
|
|
const match = /^--([a-z0-9-]+)=(.+)$/u.exec(argument);
|
|
if (!match || Object.hasOwn(values, match[1])) {
|
|
fail('arguments are invalid');
|
|
}
|
|
values[match[1]] = match[2];
|
|
}
|
|
if (values.mode === 'audit') {
|
|
if (
|
|
JSON.stringify(Object.keys(values).sort()) !==
|
|
JSON.stringify(['milestone', 'mode'])
|
|
) {
|
|
fail('audit arguments are invalid');
|
|
}
|
|
return { mode: 'audit', milestoneRoot: path.resolve(values.milestone) };
|
|
}
|
|
if (values.mode === 'audit-workflow') {
|
|
if (
|
|
JSON.stringify(Object.keys(values).sort()) !==
|
|
JSON.stringify(['mode', 'root'])
|
|
) {
|
|
fail('workflow audit arguments are invalid');
|
|
}
|
|
return { mode: 'audit-workflow', root: path.resolve(values.root) };
|
|
}
|
|
const expected = [
|
|
'amd64-bundle',
|
|
'arm64-bundle',
|
|
'event',
|
|
'mode',
|
|
'output',
|
|
'readme',
|
|
'repository',
|
|
'run-attempt',
|
|
'run-id',
|
|
'source-revision',
|
|
'variant',
|
|
'workflow-ref',
|
|
'workflow-sha',
|
|
];
|
|
if (
|
|
values.mode !== 'finalize' ||
|
|
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
|
|
) {
|
|
fail('finalize arguments are invalid');
|
|
}
|
|
return {
|
|
mode: 'finalize',
|
|
outputRoot: path.resolve(values.output),
|
|
bundles: {
|
|
amd64: path.resolve(values['amd64-bundle']),
|
|
arm64: path.resolve(values['arm64-bundle']),
|
|
},
|
|
readme: path.resolve(values.readme),
|
|
sourceRevision: values['source-revision'],
|
|
variant: values.variant,
|
|
repository: values.repository,
|
|
workflowRef: values['workflow-ref'],
|
|
workflowSha: values['workflow-sha'],
|
|
eventName: values.event,
|
|
runId: values['run-id'],
|
|
runAttempt: values['run-attempt'],
|
|
};
|
|
}
|
|
|
|
function runCli(argv) {
|
|
const options = parseArguments(argv);
|
|
let report;
|
|
if (options.mode === 'finalize') {
|
|
report = finalizeLocalAlphaMilestone(options);
|
|
} else if (options.mode === 'audit-workflow') {
|
|
report = auditLocalAlphaMilestoneWorkflow(options.root);
|
|
if (!report.compatible) fail(JSON.stringify(report));
|
|
} else {
|
|
report = auditLocalAlphaMilestone(options);
|
|
}
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
return report;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
runCli(process.argv.slice(2));
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${
|
|
error instanceof Error ? error.message : 'Local Alpha milestone failed'
|
|
}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
module.exports = Object.freeze({
|
|
ARCHITECTURES,
|
|
FILES,
|
|
REQUIRED_WORKFLOW_NEEDS,
|
|
SCHEMA,
|
|
artifactName,
|
|
auditLocalAlphaMilestone,
|
|
auditLocalAlphaMilestoneWorkflow,
|
|
finalizeLocalAlphaMilestone,
|
|
parseArguments,
|
|
runCli,
|
|
});
|