mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ql3): bind alpha artifacts to CI evidence
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -124,3 +124,44 @@ test('rejects removal of the fresh Owner journey or two-image manifest', () => {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects Alpha evidence recorded before all native gates', () => {
|
||||
const temporaryRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-local-alpha-evidence-order-'),
|
||||
);
|
||||
try {
|
||||
fs.mkdirSync(path.join(temporaryRoot, 'deploy/containers'), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.cpSync(
|
||||
path.join(root, 'deploy/containers/ql3-local-operator'),
|
||||
path.join(temporaryRoot, 'deploy/containers/ql3-local-operator'),
|
||||
{ recursive: true },
|
||||
);
|
||||
fs.mkdirSync(path.join(temporaryRoot, '.github/workflows'), {
|
||||
recursive: true,
|
||||
});
|
||||
const workflow = fs
|
||||
.readFileSync(path.join(root, '.github/workflows/ql3-ci.yml'), 'utf8')
|
||||
.replace('--mode=record-verification', '--mode=temporary-marker')
|
||||
.replace('--mode=audit', '--mode=record-verification')
|
||||
.replace('--mode=temporary-marker', '--mode=audit');
|
||||
fs.writeFileSync(
|
||||
path.join(temporaryRoot, '.github/workflows/ql3-ci.yml'),
|
||||
workflow,
|
||||
);
|
||||
fs.copyFileSync(
|
||||
path.join(root, 'ql3-release.json'),
|
||||
path.join(temporaryRoot, 'ql3-release.json'),
|
||||
);
|
||||
const report = auditLocalOperatorImageContract(temporaryRoot);
|
||||
assert.equal(report.compatible, false);
|
||||
assert.ok(
|
||||
report.findings.some(
|
||||
({ code }) => code === 'LOCAL_OPERATOR_CI_GATE_ORDER_DRIFT',
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user