mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): publish durable release catalog
This commit is contained in:
@@ -89,6 +89,25 @@ test('rejects verifier, embedded artifact and release workflow drift', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects an Admin release without the durable OCI catalog', () => {
|
||||
const result = auditClusterCopilotConsoleDistribution({
|
||||
root: ROOT,
|
||||
readFile: intercept('.github/workflows/ql3-image-release.yml', (source) =>
|
||||
source.replace(
|
||||
'Publish and round-trip the durable OCI release catalog',
|
||||
'Durable catalog removed',
|
||||
),
|
||||
),
|
||||
});
|
||||
assert.equal(result.compatible, false);
|
||||
assert.equal(
|
||||
result.findings.some(
|
||||
(entry) => entry.code === 'QL3_CLUSTER_ADMIN_RELEASE_WORKFLOW_DRIFT',
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects external workstation ceremony and offline audit widening', () => {
|
||||
const fixtures = [
|
||||
[
|
||||
|
||||
@@ -96,11 +96,24 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
sourceDerived: true,
|
||||
sameRunRecords: true,
|
||||
exactScopeClosure: true,
|
||||
standaloneInspection: true,
|
||||
tagPromotionAuthority: 'complete_verified_release_set',
|
||||
fileProvenanceAttested: true,
|
||||
artifactRetentionDays: 90,
|
||||
crossRepositoryAtomicity: false,
|
||||
},
|
||||
durableCatalog: {
|
||||
repository: 'qinglong3-release-catalog',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v1+json',
|
||||
basenameOnly: true,
|
||||
crossRunnerDeterministic: true,
|
||||
byteExactRoundTrip: true,
|
||||
keylessSignatureVerified: true,
|
||||
githubProvenanceVerified: true,
|
||||
discoveryTagAuthority: 'none',
|
||||
immutableDigestAuthority: 'verified',
|
||||
receiptAttested: true,
|
||||
},
|
||||
localRolloutPreflight: true,
|
||||
localRolloutApply: true,
|
||||
postPublishVerification: [
|
||||
@@ -111,6 +124,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
'os-vulnerability',
|
||||
'release-candidate',
|
||||
'release-set',
|
||||
'durable-catalog',
|
||||
'release-tags',
|
||||
],
|
||||
},
|
||||
@@ -128,6 +142,17 @@ test('rejects removal of the source-derived release version audit', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the durable release-catalog contract tests', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'test/back/ql3ReleaseCatalogContract.test.cjs',
|
||||
'test/back/catalog-tests-removed.test.cjs',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/durable catalog and workflow negative tests/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the native arm64 image gate', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: arm64\n image: control',
|
||||
@@ -714,6 +739,78 @@ test('rejects release-set aggregation without independent audit', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a deployment lock without standalone inspection', () => {
|
||||
const releaseSetOffset = releaseSource.indexOf('\n release-set:');
|
||||
assert.notEqual(releaseSetOffset, -1);
|
||||
const releaseSetSource = releaseSource.slice(releaseSetOffset);
|
||||
const mutated = `${releaseSource.slice(
|
||||
0,
|
||||
releaseSetOffset,
|
||||
)}${releaseSetSource.replace(
|
||||
' --mode=inspect \\',
|
||||
' --mode=audit \\',
|
||||
)}`;
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/independently inspect, durably publish/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a catalog title that leaks the runner temporary path', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' --strip-dirs \\',
|
||||
' --index \\',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/independently inspect, durably publish/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a catalog publication without byte-exact round trip', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' cmp --silent "${RELEASE_SET}" "${roundtrip}"',
|
||||
' echo roundtrip-not-checked',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/independently inspect, durably publish/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects using the mutable catalog discovery tag as deployment authority', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'artifact get --file "${file_name}" "${immutable_reference}"',
|
||||
'artifact get --file "${file_name}" "${discovery_tag}"',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/independently inspect, durably publish/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects durable catalog provenance detached from its manifest digest', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' subject-digest: ${{ steps.catalog.outputs.digest }}',
|
||||
' subject-digest: ${{ github.sha }}',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/independently inspect, durably publish/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a release-catalog receipt without file provenance', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' subject-path: ${{ steps.catalog-receipt.outputs.receipt }}',
|
||||
' subject-path: ${{ steps.release-set.outputs.report }}',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/independently inspect, durably publish/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a short-lived deployment digest lock', () => {
|
||||
const marker = ' retention-days: 90';
|
||||
assert.equal(releaseSource.includes(marker), true);
|
||||
|
||||
@@ -105,6 +105,10 @@ test('combines local and cluster families without weakening either gate', () =>
|
||||
contract.requiredGates.includes('cross-image-release-set'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('durable-oci-release-catalog'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('worker-management-production-evidence'),
|
||||
true,
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const {
|
||||
ARTIFACT_TYPE,
|
||||
OCI_EMPTY_CONFIG_DIGEST,
|
||||
OCI_EMPTY_CONFIG_MEDIA_TYPE,
|
||||
OCI_MANIFEST_MEDIA_TYPE,
|
||||
auditCatalogPlan,
|
||||
auditCatalogReceipt,
|
||||
createCatalogPlan,
|
||||
createCatalogReceipt,
|
||||
parseArguments,
|
||||
runCli,
|
||||
} = require('../../scripts/ql3-release-catalog-contract.cjs');
|
||||
const {
|
||||
createReleaseSet,
|
||||
createVerifiedImageRecord,
|
||||
} = require('../../scripts/ql3-release-set-contract.cjs');
|
||||
const {
|
||||
createReleaseCandidateContract,
|
||||
} = require('../../scripts/ql3-release-candidate-contract.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
const identity = Object.freeze({
|
||||
version,
|
||||
sourceRevision: 'd'.repeat(40),
|
||||
sourceRef: `refs/tags/v${version}`,
|
||||
repositoryOwner: 'qinglong-release',
|
||||
sourceRepository: 'qinglong-release/qinglong',
|
||||
});
|
||||
|
||||
function sha256(value) {
|
||||
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
||||
}
|
||||
|
||||
function releaseSet(scope) {
|
||||
const candidate = createReleaseCandidateContract({
|
||||
root,
|
||||
version,
|
||||
sourceRevision: identity.sourceRevision,
|
||||
sourceRef: identity.sourceRef,
|
||||
releaseScope: scope,
|
||||
});
|
||||
const records = candidate.images.map((entry, index) =>
|
||||
createVerifiedImageRecord({
|
||||
root,
|
||||
candidate,
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
image: entry.image,
|
||||
digest: `sha256:${String(index + 1).repeat(64)}`,
|
||||
}),
|
||||
);
|
||||
return createReleaseSet({
|
||||
root,
|
||||
candidate,
|
||||
records,
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
}
|
||||
|
||||
function manifestFor(plan, mutate = () => {}) {
|
||||
const manifest = {
|
||||
schemaVersion: 2,
|
||||
mediaType: OCI_MANIFEST_MEDIA_TYPE,
|
||||
artifactType: ARTIFACT_TYPE,
|
||||
config: {
|
||||
mediaType: OCI_EMPTY_CONFIG_MEDIA_TYPE,
|
||||
digest: OCI_EMPTY_CONFIG_DIGEST,
|
||||
size: 2,
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
mediaType: ARTIFACT_TYPE,
|
||||
digest: plan.releaseSet.contentDigest,
|
||||
size: plan.releaseSet.bytes,
|
||||
annotations: {
|
||||
'org.opencontainers.image.title': plan.releaseSet.fileName,
|
||||
},
|
||||
},
|
||||
],
|
||||
annotations: { ...plan.catalog.annotations },
|
||||
};
|
||||
mutate(manifest);
|
||||
return JSON.stringify(manifest);
|
||||
}
|
||||
|
||||
function temporaryDirectory(t) {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-catalog-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return directory;
|
||||
}
|
||||
|
||||
function writeCanonical(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
test('plans deterministic Local, Cluster and All OCI catalog entries', () => {
|
||||
for (const scope of ['local', 'cluster', 'all']) {
|
||||
const set = releaseSet(scope);
|
||||
const plan = createCatalogPlan(set, { ...identity, releaseScope: scope });
|
||||
assert.equal(
|
||||
plan.catalog.discoveryTag,
|
||||
`ghcr.io/qinglong-release/qinglong3-release-catalog:v${version}-${scope}`,
|
||||
);
|
||||
assert.equal(plan.catalog.artifactType, ARTIFACT_TYPE);
|
||||
assert.equal(
|
||||
plan.releaseSet.bytes,
|
||||
Buffer.byteLength(`${JSON.stringify(set)}\n`),
|
||||
);
|
||||
assert.equal(
|
||||
auditCatalogPlan(plan, set, { ...identity, releaseScope: scope })
|
||||
.planDigest,
|
||||
plan.planDigest,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('creates one digest-addressed receipt from the exact OCI manifest', () => {
|
||||
const set = releaseSet('cluster');
|
||||
const plan = createCatalogPlan(set, {
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
});
|
||||
const manifest = manifestFor(plan);
|
||||
const manifestDigest = sha256(manifest);
|
||||
const receipt = createCatalogReceipt(plan, manifest, manifestDigest);
|
||||
assert.equal(
|
||||
receipt.catalog.immutableReference,
|
||||
`ghcr.io/qinglong-release/qinglong3-release-catalog@${manifestDigest}`,
|
||||
);
|
||||
assert.equal(receipt.verification.discoveryTagAuthority, 'none');
|
||||
assert.equal(
|
||||
auditCatalogReceipt(receipt, plan, manifest, manifestDigest).compatible,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects source ownership, mutable identity and release-set drift', () => {
|
||||
const set = releaseSet('local');
|
||||
assert.throws(
|
||||
() =>
|
||||
createCatalogPlan(set, {
|
||||
...identity,
|
||||
sourceRepository: 'other/qinglong',
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/owned by the publisher/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createCatalogPlan(set, {
|
||||
...identity,
|
||||
version: '3.0.0',
|
||||
sourceRef: 'refs/tags/v3.0.0',
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/standalone release set/,
|
||||
);
|
||||
const drifted = JSON.parse(JSON.stringify(set));
|
||||
drifted.images[0].reference = 'ghcr.io/other/image@sha256:' + '1'.repeat(64);
|
||||
assert.throws(
|
||||
() => createCatalogPlan(drifted, { ...identity, releaseScope: 'local' }),
|
||||
/standalone release set/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects OCI media, blob, title, annotation and raw digest drift', () => {
|
||||
const set = releaseSet('local');
|
||||
const plan = createCatalogPlan(set, { ...identity, releaseScope: 'local' });
|
||||
for (const mutate of [
|
||||
(value) => {
|
||||
value.mediaType = 'application/json';
|
||||
},
|
||||
(value) => {
|
||||
value.layers[0].digest = `sha256:${'0'.repeat(64)}`;
|
||||
},
|
||||
(value) => {
|
||||
value.layers[0].annotations['org.opencontainers.image.title'] =
|
||||
'/tmp/release-set.json';
|
||||
},
|
||||
(value) => {
|
||||
value.annotations['dev.qinglong.release.scope'] = 'all';
|
||||
},
|
||||
]) {
|
||||
const manifest = manifestFor(plan, mutate);
|
||||
assert.throws(
|
||||
() => createCatalogReceipt(plan, manifest, sha256(manifest)),
|
||||
/manifest differs/,
|
||||
);
|
||||
}
|
||||
const manifest = manifestFor(plan);
|
||||
assert.throws(
|
||||
() => createCatalogReceipt(plan, manifest, `sha256:${'0'.repeat(64)}`),
|
||||
/manifest digest is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI plans, receipts and audits canonical no-replace evidence', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const set = releaseSet('local');
|
||||
const setPath = path.join(
|
||||
directory,
|
||||
`qinglong3-release-set-${version}-local.json`,
|
||||
);
|
||||
writeCanonical(setPath, set);
|
||||
const planPath = path.join(directory, 'plan.json');
|
||||
const planArgs = [
|
||||
'--mode=plan',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=local',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--source-repository=${identity.sourceRepository}`,
|
||||
`--release-set=${setPath}`,
|
||||
`--output=${planPath}`,
|
||||
];
|
||||
const output = { write() {} };
|
||||
const plan = runCli(planArgs, output);
|
||||
assert.equal(fs.statSync(planPath).mode & 0o777, 0o600);
|
||||
const manifest = manifestFor(plan);
|
||||
const manifestPath = path.join(directory, 'manifest.json');
|
||||
fs.writeFileSync(manifestPath, manifest, { mode: 0o600 });
|
||||
const manifestDigest = sha256(manifest);
|
||||
const receiptPath = path.join(directory, 'receipt.json');
|
||||
runCli(
|
||||
[
|
||||
'--mode=receipt',
|
||||
`--plan=${planPath}`,
|
||||
`--manifest=${manifestPath}`,
|
||||
`--manifest-digest=${manifestDigest}`,
|
||||
`--output=${receiptPath}`,
|
||||
],
|
||||
output,
|
||||
);
|
||||
assert.equal(
|
||||
runCli(
|
||||
[
|
||||
'--mode=audit',
|
||||
`--plan=${planPath}`,
|
||||
`--manifest=${manifestPath}`,
|
||||
`--manifest-digest=${manifestDigest}`,
|
||||
`--receipt=${receiptPath}`,
|
||||
],
|
||||
output,
|
||||
).compatible,
|
||||
true,
|
||||
);
|
||||
assert.throws(() => runCli(planArgs, output), /output must be unused/);
|
||||
});
|
||||
|
||||
test('CLI rejects renamed inputs, symlinks and open modes', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const renamed = path.join(directory, 'set.json');
|
||||
writeCanonical(renamed, releaseSet('local'));
|
||||
const args = [
|
||||
'--mode=plan',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=local',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--source-repository=${identity.sourceRepository}`,
|
||||
`--release-set=${renamed}`,
|
||||
`--output=${path.join(directory, 'plan.json')}`,
|
||||
];
|
||||
assert.throws(
|
||||
() => runCli(args, { write() {} }),
|
||||
/filename must be deterministic/,
|
||||
);
|
||||
const target = path.join(directory, 'target.json');
|
||||
fs.renameSync(renamed, target);
|
||||
fs.symlinkSync('target.json', renamed);
|
||||
assert.throws(() => runCli(args, { write() {} }), /canonical regular file/);
|
||||
assert.throws(
|
||||
() => parseArguments(['--mode=publish', '--extra=true']),
|
||||
/arguments are invalid/,
|
||||
);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
auditReleaseSet,
|
||||
createReleaseSet,
|
||||
createVerifiedImageRecord,
|
||||
inspectReleaseSet,
|
||||
parseArguments,
|
||||
runCli,
|
||||
} = require('../../scripts/ql3-release-set-contract.cjs');
|
||||
@@ -216,6 +217,69 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
|
||||
);
|
||||
});
|
||||
|
||||
test('inspects a release set without short-lived candidate or image records', () => {
|
||||
for (const scope of ['local', 'cluster', 'all']) {
|
||||
const releaseCandidate = candidate(scope);
|
||||
const releaseSet = createReleaseSet({
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: recordsFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
const inspection = inspectReleaseSet(releaseSet, {
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
assert.equal(inspection.compatible, true);
|
||||
assert.equal(inspection.imageCount, releaseCandidate.images.length);
|
||||
assert.equal(inspection.sourceRecordsReplayed, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('standalone inspection rejects image, family and self-digest drift', () => {
|
||||
const releaseCandidate = candidate('cluster');
|
||||
const releaseSet = createReleaseSet({
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: recordsFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
});
|
||||
for (const mutate of [
|
||||
(value) => {
|
||||
value.images[0].repository = 'other';
|
||||
},
|
||||
(value) => {
|
||||
value.deploymentFamilies.cluster.images.pop();
|
||||
},
|
||||
(value) => {
|
||||
value.releaseSetDigest = `sha256:${'0'.repeat(64)}`;
|
||||
},
|
||||
]) {
|
||||
const drifted = JSON.parse(JSON.stringify(releaseSet));
|
||||
mutate(drifted);
|
||||
assert.throws(
|
||||
() =>
|
||||
inspectReleaseSet(drifted, {
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
/standalone release set/,
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() =>
|
||||
inspectReleaseSet(releaseSet, {
|
||||
...identity,
|
||||
version: '3.0.0+unreviewed',
|
||||
sourceRef: 'refs/tags/v3.0.0+unreviewed',
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
/expected release identity is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const recordsDirectory = path.join(directory, 'records');
|
||||
@@ -269,6 +333,22 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
).compatible,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
runCli(
|
||||
[
|
||||
'--mode=inspect',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
`--source-ref=${identity.sourceRef}`,
|
||||
'--release-scope=local',
|
||||
`--repository-owner=${identity.repositoryOwner}`,
|
||||
`--report=${setPath}`,
|
||||
],
|
||||
root,
|
||||
output,
|
||||
).verification,
|
||||
'standalone_structure_identity_and_self_digest',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
|
||||
Reference in New Issue
Block a user