fix(ql3): bind alpha artifacts to CI evidence

This commit is contained in:
whyour
2026-08-27 06:57:08 +08:00
parent 4239464af6
commit 238df17fdf
10 changed files with 509 additions and 41 deletions
+250 -11
View File
@@ -10,7 +10,8 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/alpha-local-trial-kit@v1';
const SCHEMA = 'qinglong/alpha-local-trial-kit@v2';
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
const ARCHIVE_MIN_BYTES = 1024;
const MAX_JSON_BYTES = 4 * 1024 * 1024;
@@ -20,6 +21,7 @@ const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
const FILES = Object.freeze({
applicationSbom: 'qinglong3-local-application.cdx.json',
operatorSbom: 'qinglong3-local-operator.cdx.json',
verificationEvidence: 'verification-evidence.json',
readme: 'README.md',
manifest: 'manifest.json',
checksums: 'SHA256SUMS',
@@ -35,6 +37,14 @@ const VERIFICATION = Object.freeze({
standaloneFreshLifecycle: 'passed',
localApiCancellation: 'passed',
});
const WORKFLOW_IDENTITY = Object.freeze({
repository: 'whyour/qinglong',
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
event: 'workflow_dispatch',
job: 'local-image',
});
const DECIMAL_ID_PATTERN = /^[1-9][0-9]{0,19}$/u;
const ATTEMPT_PATTERN = /^[1-9][0-9]{0,5}$/u;
function fail(message) {
throw new Error(message);
@@ -230,6 +240,145 @@ function validateOfflineSbom(document, profile, version) {
}
}
function validateVerificationEvidence(document, expected) {
if (
!exactKeys(document, [
'schemaVersion',
'schema',
'subject',
'workflow',
'gates',
]) ||
document.schemaVersion !== 1 ||
document.schema !== VERIFICATION_SCHEMA ||
!exactKeys(document.subject, [
'version',
'sourceRevision',
'architecture',
'applicationImageId',
'operatorImageId',
]) ||
document.subject.version !== expected.version ||
document.subject.sourceRevision !== expected.sourceRevision ||
document.subject.architecture !== expected.architecture ||
document.subject.applicationImageId !== expected.applicationImageId ||
document.subject.operatorImageId !== expected.operatorImageId ||
document.subject.applicationImageId === document.subject.operatorImageId ||
!exactKeys(document.workflow, [
'repository',
'workflowRef',
'workflowSha',
'event',
'job',
'runId',
'runAttempt',
]) ||
document.workflow.repository !== WORKFLOW_IDENTITY.repository ||
document.workflow.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
document.workflow.workflowSha !== expected.sourceRevision ||
document.workflow.event !== WORKFLOW_IDENTITY.event ||
document.workflow.job !== WORKFLOW_IDENTITY.job ||
!DECIMAL_ID_PATTERN.test(document.workflow.runId || '') ||
!ATTEMPT_PATTERN.test(document.workflow.runAttempt || '') ||
!exactKeys(document.gates, Object.keys(VERIFICATION)) ||
JSON.stringify(document.gates) !== JSON.stringify(VERIFICATION)
) {
fail('trial kit verification evidence is incompatible');
}
return document;
}
function validateVerificationOptions(options) {
const root = fs.realpathSync(path.resolve(options.root || DEFAULT_ROOT));
const output = path.resolve(options.output || '');
const parent = path.dirname(output);
if (
!ARCHITECTURES.includes(options.architecture) ||
!REVISION_PATTERN.test(options.sourceRevision || '') ||
!path.isAbsolute(output) ||
fs.existsSync(output) ||
fs.realpathSync(parent) !== parent ||
options.repository !== WORKFLOW_IDENTITY.repository ||
options.workflowRef !== WORKFLOW_IDENTITY.workflowRef ||
options.workflowSha !== options.sourceRevision ||
options.eventName !== WORKFLOW_IDENTITY.event ||
options.job !== WORKFLOW_IDENTITY.job ||
!DECIMAL_ID_PATTERN.test(options.runId || '') ||
!ATTEMPT_PATTERN.test(options.runAttempt || '')
) {
fail('verification evidence identity or output is invalid');
}
return {
root,
output,
architecture: options.architecture,
sourceRevision: options.sourceRevision,
applicationImage: validateImageReference(
options.applicationImage,
'application',
),
operatorImage: validateImageReference(options.operatorImage, 'operator'),
repository: options.repository,
workflowRef: options.workflowRef,
workflowSha: options.workflowSha,
eventName: options.eventName,
job: options.job,
runId: options.runId,
runAttempt: options.runAttempt,
};
}
function createLocalAlphaTrialKitVerificationEvidence(options, adapters = {}) {
const normalized = validateVerificationOptions(options);
const release = readReleaseIdentity(normalized.root);
const inspectImage = adapters.inspectImage || inspectDockerImage;
const application = normalizeImageInspection(
inspectImage(normalized.applicationImage),
{
architecture: normalized.architecture,
reference: normalized.applicationImage,
revision: normalized.sourceRevision,
role: 'application',
version: release.version,
},
);
const operator = normalizeImageInspection(
inspectImage(normalized.operatorImage),
{
architecture: normalized.architecture,
reference: normalized.operatorImage,
revision: normalized.sourceRevision,
role: 'operator',
version: release.version,
},
);
if (application.id === operator.id) fail('trial kit images must be distinct');
const evidence = {
schemaVersion: 1,
schema: VERIFICATION_SCHEMA,
subject: {
version: release.version,
sourceRevision: normalized.sourceRevision,
architecture: normalized.architecture,
applicationImageId: application.id,
operatorImageId: operator.id,
},
workflow: {
repository: normalized.repository,
workflowRef: normalized.workflowRef,
workflowSha: normalized.workflowSha,
event: normalized.eventName,
job: normalized.job,
runId: normalized.runId,
runAttempt: normalized.runAttempt,
},
gates: { ...VERIFICATION },
};
validateVerificationEvidence(evidence, evidence.subject);
writeExclusive(normalized.output, `${JSON.stringify(evidence, null, 2)}\n`);
return evidence;
}
function archiveName(architecture) {
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
}
@@ -288,6 +437,11 @@ function validateCreateOptions(options) {
MAX_JSON_BYTES,
'operator SBOM',
),
verificationEvidence: assertCanonicalFile(
options.verificationEvidence,
MAX_JSON_BYTES,
'trial kit verification evidence',
),
readme: assertCanonicalFile(
options.readme,
MAX_README_BYTES,
@@ -309,6 +463,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
normalized.operatorSbom,
'operator SBOM',
);
const verificationEvidence = readBoundedJson(
normalized.verificationEvidence,
'trial kit verification evidence',
);
validateSbom(applicationSbom, {
root: normalized.root,
profile: 'local',
@@ -340,6 +498,13 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
},
);
if (application.id === operator.id) fail('trial kit images must be distinct');
validateVerificationEvidence(verificationEvidence, {
version: release.version,
sourceRevision: normalized.sourceRevision,
architecture: normalized.architecture,
applicationImageId: application.id,
operatorImageId: operator.id,
});
let created = false;
try {
@@ -368,12 +533,16 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
normalized.operatorSbom,
path.join(normalized.outputRoot, FILES.operatorSbom),
);
copyExclusive(
normalized.verificationEvidence,
path.join(normalized.outputRoot, FILES.verificationEvidence),
);
copyExclusive(
normalized.readme,
path.join(normalized.outputRoot, FILES.readme),
);
const manifest = {
schemaVersion: 2,
schemaVersion: 3,
schema: SCHEMA,
maturity: 'alpha_candidate_not_public_release',
product: 'local',
@@ -387,7 +556,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
},
readme: fileRecord(normalized.outputRoot, FILES.readme),
verification: { ...VERIFICATION },
verification: fileRecord(
normalized.outputRoot,
FILES.verificationEvidence,
),
};
writeExclusive(
path.join(normalized.outputRoot, FILES.manifest),
@@ -397,6 +569,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
archive,
FILES.applicationSbom,
FILES.operatorSbom,
FILES.verificationEvidence,
FILES.readme,
FILES.manifest,
];
@@ -467,7 +640,7 @@ function auditLocalAlphaTrialKit(options) {
'readme',
'verification',
]) ||
manifest.schemaVersion !== 2 ||
manifest.schemaVersion !== 3 ||
manifest.schema !== SCHEMA ||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
manifest.product !== 'local' ||
@@ -475,9 +648,7 @@ function auditLocalAlphaTrialKit(options) {
!REVISION_PATTERN.test(manifest.sourceRevision || '') ||
!ARCHITECTURES.includes(manifest.architecture) ||
!exactKeys(manifest.images, ['application', 'operator']) ||
!exactKeys(manifest.sboms, ['application', 'operator']) ||
!exactKeys(manifest.verification, Object.keys(VERIFICATION)) ||
JSON.stringify(manifest.verification) !== JSON.stringify(VERIFICATION)
!exactKeys(manifest.sboms, ['application', 'operator'])
) {
fail('trial kit manifest identity or shape is incompatible');
}
@@ -497,6 +668,11 @@ function auditLocalAlphaTrialKit(options) {
bundleRoot,
);
validateFileRecord(manifest.sboms.operator, FILES.operatorSbom, bundleRoot);
validateFileRecord(
manifest.verification,
FILES.verificationEvidence,
bundleRoot,
);
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
validateOfflineSbom(
readBoundedJson(
@@ -511,12 +687,26 @@ function auditLocalAlphaTrialKit(options) {
'local-operator',
manifest.version,
);
const verificationEvidence = validateVerificationEvidence(
readBoundedJson(
path.join(bundleRoot, FILES.verificationEvidence),
'trial kit verification evidence',
),
{
version: manifest.version,
sourceRevision: manifest.sourceRevision,
architecture: manifest.architecture,
applicationImageId: manifest.images.application.id,
operatorImageId: manifest.images.operator.id,
},
);
const expectedFiles = [
FILES.checksums,
FILES.manifest,
FILES.readme,
FILES.applicationSbom,
FILES.operatorSbom,
FILES.verificationEvidence,
expectedArchive,
].sort();
const actualFiles = fs
@@ -535,6 +725,7 @@ function auditLocalAlphaTrialKit(options) {
expectedArchive,
FILES.applicationSbom,
FILES.operatorSbom,
FILES.verificationEvidence,
FILES.readme,
FILES.manifest,
];
@@ -555,6 +746,9 @@ function auditLocalAlphaTrialKit(options) {
archiveSha256: manifest.archive.sha256,
applicationImageId: manifest.images.application.id,
operatorImageId: manifest.images.operator.id,
verificationSha256: manifest.verification.sha256,
workflowRunId: verificationEvidence.workflow.runId,
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
compatible: true,
});
}
@@ -576,6 +770,43 @@ function parseArguments(argv) {
}
return { mode: 'audit', bundleRoot: path.resolve(values.bundle) };
}
if (values.mode === 'record-verification') {
const expected = [
'application-image',
'architecture',
'event',
'job',
'mode',
'operator-image',
'output',
'repository',
'run-attempt',
'run-id',
'source-revision',
'workflow-ref',
'workflow-sha',
];
if (
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
) {
fail('record-verification arguments are invalid');
}
return {
mode: 'record-verification',
output: path.resolve(values.output),
architecture: values.architecture,
sourceRevision: values['source-revision'],
applicationImage: values['application-image'],
operatorImage: values['operator-image'],
repository: values.repository,
workflowRef: values['workflow-ref'],
workflowSha: values['workflow-sha'],
eventName: values.event,
job: values.job,
runId: values['run-id'],
runAttempt: values['run-attempt'],
};
}
if (values.mode === 'create') {
const expected = [
'application-image',
@@ -587,6 +818,7 @@ function parseArguments(argv) {
'output',
'readme',
'source-revision',
'verification-evidence',
];
if (
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
@@ -602,6 +834,7 @@ function parseArguments(argv) {
operatorImage: values['operator-image'],
applicationSbom: path.resolve(values['application-sbom']),
operatorSbom: path.resolve(values['operator-sbom']),
verificationEvidence: path.resolve(values['verification-evidence']),
readme: path.resolve(values.readme),
};
}
@@ -610,10 +843,14 @@ function parseArguments(argv) {
function runCli(argv) {
const options = parseArguments(argv);
const report =
options.mode === 'create'
? createLocalAlphaTrialKit(options)
: auditLocalAlphaTrialKit(options);
let report;
if (options.mode === 'record-verification') {
report = createLocalAlphaTrialKitVerificationEvidence(options);
} else if (options.mode === 'create') {
report = createLocalAlphaTrialKit(options);
} else {
report = auditLocalAlphaTrialKit(options);
}
process.stdout.write(`${JSON.stringify(report)}\n`);
return report;
}
@@ -633,9 +870,11 @@ module.exports = Object.freeze({
FILES,
SCHEMA,
VERIFICATION,
VERIFICATION_SCHEMA,
archiveName,
auditLocalAlphaTrialKit,
createLocalAlphaTrialKit,
createLocalAlphaTrialKitVerificationEvidence,
parseArguments,
runCli,
sha256File,
@@ -196,16 +196,41 @@ function auditWorkflow(contents, findings) {
'"${OPERATOR_IMAGE}" --version',
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
'--mode=record-verification',
'--mode=create',
'--mode=audit',
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
'--readme=docs/operations/ql3-local-alpha-trial-kit.md',
'--repository=${{ github.repository }}',
'--workflow-ref="${{ github.workflow_ref }}"',
'--workflow-sha=${{ github.workflow_sha }}',
'--event=${{ github.event_name }}',
'--job=${{ github.job }}',
'--run-id=${{ github.run_id }}',
'--run-attempt=${{ github.run_attempt }}',
];
for (const value of required) {
if (!contents.includes(value))
finding(findings, 'LOCAL_OPERATOR_CI_CONTRACT_DRIFT', value);
}
let cursor = -1;
for (const value of [
'name: Run the downloadable Local Alpha trial kit journey',
'name: Run authenticated Local API cancellation through real Linux processes',
'--mode=record-verification',
'--mode=create',
'--mode=audit',
'name: Upload the tested native Local Alpha trial kit',
]) {
const index = contents.indexOf(value, cursor + 1);
if (index <= cursor) {
finding(findings, 'LOCAL_OPERATOR_CI_GATE_ORDER_DRIFT', value);
break;
}
cursor = index;
}
}
function auditLocalOperatorImageContract(root) {