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
+89 -5
View File
@@ -8,6 +8,7 @@ const test = require('node:test');
const {
auditLocalAlphaTrialKit,
createLocalAlphaTrialKit,
createLocalAlphaTrialKitVerificationEvidence,
parseArguments,
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
const {
@@ -61,6 +62,10 @@ function fixture(t) {
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
const applicationSbom = path.join(fixtureRoot, 'application.json');
const operatorSbom = path.join(fixtureRoot, 'operator.json');
const verificationEvidence = path.join(
fixtureRoot,
'verification-evidence-source.json',
);
const readme = path.join(fixtureRoot, 'README-source.md');
fs.writeFileSync(
applicationSbom,
@@ -73,13 +78,38 @@ function fixture(t) {
)}\n`,
);
fs.writeFileSync(readme, '# Local Alpha Trial Kit\n');
return {
const paths = {
fixtureRoot,
applicationSbom,
operatorSbom,
verificationEvidence,
readme,
outputRoot: path.join(fixtureRoot, 'bundle'),
};
createLocalAlphaTrialKitVerificationEvidence(
verificationOptions(paths),
adapters(),
);
return paths;
}
function verificationOptions(paths, overrides = {}) {
return {
root,
output: paths.verificationEvidence,
architecture: 'arm64',
sourceRevision: revision,
applicationImage: 'qinglong3-local-application:test-arm64',
operatorImage: 'qinglong3-local-operator:test-arm64',
repository: 'whyour/qinglong',
workflowRef: 'whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
workflowSha: revision,
eventName: 'workflow_dispatch',
job: 'local-image',
runId: '32990652047',
runAttempt: '1',
...overrides,
};
}
function createOptions(paths) {
@@ -92,6 +122,7 @@ function createOptions(paths) {
operatorImage: 'qinglong3-local-operator:test-arm64',
applicationSbom: paths.applicationSbom,
operatorSbom: paths.operatorSbom,
verificationEvidence: paths.verificationEvidence,
readme: paths.readme,
};
}
@@ -117,15 +148,17 @@ function adapters(overrides = {}) {
test('materializes and offline-audits one closed two-image trial kit', (t) => {
const paths = fixture(t);
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v1');
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v2');
assert.equal(manifest.sourceRevision, revision);
assert.equal(manifest.architecture, 'arm64');
assert.equal(manifest.images.application.architecture, 'arm64');
assert.equal(manifest.images.operator.architecture, 'arm64');
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
assert.equal(manifest.verification.file, 'verification-evidence.json');
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
assert.equal(report.compatible, true);
assert.equal(report.sourceRevision, revision);
assert.equal(report.workflowRunId, '32990652047');
assert.deepEqual(fs.readdirSync(paths.outputRoot).sort(), [
'README.md',
'SHA256SUMS',
@@ -133,6 +166,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
'qinglong3-local-application.cdx.json',
'qinglong3-local-operator.cdx.json',
'qinglong3-local-trial-kit-arm64.docker.tar',
'verification-evidence.json',
]);
});
@@ -159,8 +193,8 @@ test('fails closed and removes a partial output on incompatible image identity',
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('offline audit rejects archive mutation, extra files and SBOM substitution', (t) => {
for (const mutation of ['archive', 'extra', 'sbom']) {
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
for (const mutation of ['archive', 'extra', 'sbom', 'verification']) {
const paths = fixture(t);
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
createLocalAlphaTrialKit(createOptions(paths), adapters());
@@ -174,11 +208,16 @@ test('offline audit rejects archive mutation, extra files and SBOM substitution'
);
} else if (mutation === 'extra') {
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
} else {
} else if (mutation === 'sbom') {
fs.copyFileSync(
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
path.join(paths.outputRoot, 'qinglong3-local-operator.cdx.json'),
);
} else {
fs.appendFileSync(
path.join(paths.outputRoot, 'verification-evidence.json'),
'tamper',
);
}
assert.throws(
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
@@ -188,6 +227,34 @@ test('offline audit rejects archive mutation, extra files and SBOM substitution'
}
});
test('create rejects verification detached from the reviewed workflow', (t) => {
const paths = fixture(t);
const evidence = JSON.parse(
fs.readFileSync(paths.verificationEvidence, 'utf8'),
);
evidence.workflow.job = 'unreviewed-job';
fs.writeFileSync(paths.verificationEvidence, `${JSON.stringify(evidence)}\n`);
assert.throws(
() => createLocalAlphaTrialKit(createOptions(paths), adapters()),
/verification evidence is incompatible/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('verification recorder rejects non-milestone workflow provenance', (t) => {
const paths = fixture(t);
const output = path.join(paths.fixtureRoot, 'unreviewed-verification.json');
assert.throws(
() =>
createLocalAlphaTrialKitVerificationEvidence(
verificationOptions(paths, { output, eventName: 'push' }),
adapters(),
),
/verification evidence identity or output is invalid/,
);
assert.equal(fs.existsSync(output), false);
});
test('CLI grammar is exact and separates create from offline audit', () => {
assert.deepEqual(
parseArguments(['--mode=audit', '--bundle=/tmp/ql3-bundle']),
@@ -206,4 +273,21 @@ test('CLI grammar is exact and separates create from offline audit', () => {
() => parseArguments(['--mode=create', '--output=/tmp/output']),
/create arguments are invalid/,
);
const recorded = parseArguments([
'--mode=record-verification',
'--application-image=qinglong3-local-application:test-arm64',
'--operator-image=qinglong3-local-operator:test-arm64',
'--architecture=arm64',
`--source-revision=${revision}`,
'--repository=whyour/qinglong',
'--workflow-ref=whyour/qinglong/.github/workflows/ql3-ci.yml@refs/heads/next',
`--workflow-sha=${revision}`,
'--event=workflow_dispatch',
'--job=local-image',
'--run-id=32990652047',
'--run-attempt=1',
'--output=/tmp/verification-evidence.json',
]);
assert.equal(recorded.mode, 'record-verification');
assert.equal(recorded.runId, '32990652047');
});