mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): bind private release evidence receipts
This commit is contained in:
@@ -54,13 +54,15 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
sourceAware: true,
|
||||
privateEphemeralRunner: true,
|
||||
maximumAgeSeconds: 86400,
|
||||
artifactUpload: false,
|
||||
privateReportUpload: false,
|
||||
contentFreeReceiptPublished: true,
|
||||
},
|
||||
cloudNativePgDisasterRecoveryEvidence: {
|
||||
sourceAware: true,
|
||||
privateEphemeralRunner: true,
|
||||
maximumAgeSeconds: 86400,
|
||||
artifactUpload: false,
|
||||
privateReportUpload: false,
|
||||
contentFreeReceiptPublished: true,
|
||||
staticLocksReaudited: true,
|
||||
},
|
||||
osVulnerabilityScan: {
|
||||
@@ -96,6 +98,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
releaseSet: {
|
||||
sourceDerived: true,
|
||||
sameRunRecords: true,
|
||||
sameRunPrivateEvidenceReceipts: true,
|
||||
exactScopeClosure: true,
|
||||
standaloneInspection: true,
|
||||
tagPromotionAuthority: 'complete_verified_release_set',
|
||||
@@ -105,7 +108,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
},
|
||||
durableCatalog: {
|
||||
repository: 'qinglong3-release-catalog',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v1+json',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v2+json',
|
||||
basenameOnly: true,
|
||||
crossRunnerDeterministic: true,
|
||||
byteExactRoundTrip: true,
|
||||
@@ -175,6 +178,17 @@ test('rejects removal of the durable release-catalog contract tests', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the private release evidence receipt contract tests', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'test/back/ql3PrivateReleaseEvidenceReceiptContract.test.cjs',
|
||||
'test/back/private-receipt-tests-removed.test.cjs',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/private evidence receipt/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the deployment-lock materialization contract tests', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'test/back/ql3DeploymentLockContract.test.cjs',
|
||||
@@ -470,14 +484,14 @@ test('rejects a reusable private evidence runner', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects private evidence uploaded as a workflow artifact', () => {
|
||||
test('rejects private report content uploaded instead of its digest-only receipt', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' - name: Re-audit commit-scoped private production evidence',
|
||||
' - uses: actions/upload-artifact@v4\n - name: Re-audit commit-scoped private production evidence',
|
||||
'path: ${{ runner.temp }}/ql3-private-release-evidence-receipts/worker-management.json',
|
||||
'path: /run/qinglong3-release-evidence/${{ github.sha }}/worker-management-release-evidence.json',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/only the reviewed|never persist private evidence/,
|
||||
/content-free receipt and upload steps/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -655,8 +669,8 @@ test('rejects scanning a daemon tag instead of the exact OCI tar', () => {
|
||||
|
||||
test('rejects retaining scanned native release artifacts for more than one day', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' retention-days: 1',
|
||||
' retention-days: 2',
|
||||
'name: ql3-release-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.image }}-${{ matrix.image_arch }}\n path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 1',
|
||||
'name: ql3-release-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.image }}-${{ matrix.image_arch }}\n path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 2',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
@@ -666,8 +680,8 @@ test('rejects retaining scanned native release artifacts for more than one day',
|
||||
|
||||
test('rejects overwrite authority on a scanned native artifact', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' overwrite: false',
|
||||
' overwrite: true',
|
||||
'path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 1\n compression-level: 0\n overwrite: false',
|
||||
'path: ${{ runner.temp }}/ql3-native\n if-no-files-found: error\n retention-days: 1\n compression-level: 0\n overwrite: true',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
@@ -676,10 +690,16 @@ test('rejects overwrite authority on a scanned native artifact', () => {
|
||||
});
|
||||
|
||||
test('rejects a movable upload-artifact action', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1',
|
||||
'actions/upload-artifact@v7.0.1',
|
||||
);
|
||||
const marker =
|
||||
' - name: Upload the scanned immutable native OCI artifact';
|
||||
const offset = releaseSource.indexOf(marker);
|
||||
assert.notEqual(offset, -1);
|
||||
const mutated = `${releaseSource.slice(0, offset)}${releaseSource
|
||||
.slice(offset)
|
||||
.replace(
|
||||
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1',
|
||||
'actions/upload-artifact@v7.0.1',
|
||||
)}`;
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/upload only bound immutable evidence/,
|
||||
@@ -864,6 +884,34 @@ test('rejects release-set closure before every image publisher succeeds', () =>
|
||||
assert.throws(() => auditReleaseWorkflow(mutated), /complete publish matrix/);
|
||||
});
|
||||
|
||||
test('rejects release-set closure detached from same-run private evidence receipts', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'ql3-private-release-evidence-${{ github.run_id }}-${{ github.run_attempt }}-*',
|
||||
'ql3-private-release-evidence-${{ github.run_id }}-*',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/release-set job must download only same-run records/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects release-set aggregation that ignores private evidence receipts', () => {
|
||||
const marker =
|
||||
' - name: Aggregate and independently audit the complete release set';
|
||||
const offset = releaseSource.indexOf(marker);
|
||||
assert.notEqual(offset, -1);
|
||||
const mutated = `${releaseSource.slice(0, offset)}${releaseSource
|
||||
.slice(offset)
|
||||
.replace(
|
||||
' --evidence-receipts="${evidence_receipts}" \\',
|
||||
' --evidence-receipts="${RUNNER_TEMP}/empty" \\',
|
||||
)}`;
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/release-set job must download only same-run records/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects image records detached from the same workflow attempt', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'ql3-release-record-${{ github.run_id }}-${{ github.run_attempt }}-*',
|
||||
|
||||
@@ -42,6 +42,9 @@ const {
|
||||
catalogLockedArtifacts,
|
||||
deploymentArtifacts,
|
||||
} = require('../../scripts/ql3-kubernetes-deployment-live-contract.cjs');
|
||||
const {
|
||||
privateReleaseEvidenceReceipts,
|
||||
} = require('./ql3ReleaseEvidenceFixture.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
@@ -97,6 +100,7 @@ function releaseSet(scope) {
|
||||
root,
|
||||
candidate,
|
||||
records,
|
||||
evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release),
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const {
|
||||
createCloudNativePgReceipt,
|
||||
createWorkerReceipt,
|
||||
inspectPrivateReleaseEvidenceReceipt,
|
||||
parseArguments,
|
||||
runCli,
|
||||
} = require('../../scripts/ql3-private-release-evidence-receipt-contract.cjs');
|
||||
const {
|
||||
privateReleaseEvidenceReceipt,
|
||||
privateReleaseEvidenceReceipts,
|
||||
} = require('./ql3ReleaseEvidenceFixture.cjs');
|
||||
|
||||
const RELEASE = Object.freeze({
|
||||
version: '3.0.0-alpha.0',
|
||||
sourceRevision: 'a'.repeat(40),
|
||||
sourceRef: 'refs/tags/v3.0.0-alpha.0',
|
||||
scope: 'cluster',
|
||||
});
|
||||
|
||||
function options(evidenceKind) {
|
||||
return Object.freeze({
|
||||
version: RELEASE.version,
|
||||
sourceRevision: RELEASE.sourceRevision,
|
||||
sourceRef: RELEASE.sourceRef,
|
||||
releaseScope: RELEASE.scope,
|
||||
evidenceKind,
|
||||
});
|
||||
}
|
||||
|
||||
function temporaryDirectory(t) {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-private-release-receipt-')),
|
||||
);
|
||||
fs.chmodSync(directory, 0o700);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return directory;
|
||||
}
|
||||
|
||||
test('accepts only the exact Worker and disaster-recovery receipt projections', () => {
|
||||
const receipts = privateReleaseEvidenceReceipts(RELEASE);
|
||||
assert.deepEqual(
|
||||
receipts.map((receipt) => receipt.evidenceKind),
|
||||
['worker-management', 'cloudnativepg-disaster-recovery'],
|
||||
);
|
||||
for (const receipt of receipts) {
|
||||
const result = inspectPrivateReleaseEvidenceReceipt(
|
||||
receipt,
|
||||
options(receipt.evidenceKind),
|
||||
);
|
||||
assert.equal(result.compatible, true);
|
||||
assert.equal(result.receiptDigest, receipt.receiptDigest);
|
||||
assert.equal(
|
||||
result.publicConsumerReplay,
|
||||
'not_possible_without_private_reports',
|
||||
);
|
||||
assert.equal(JSON.stringify(receipt).includes('/run/qinglong3'), false);
|
||||
assert.equal(JSON.stringify(receipt).includes('token'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects source, scope, freshness, digest and static-lock drift', () => {
|
||||
const mutations = [
|
||||
(receipt) => {
|
||||
receipt.release.sourceRevision = 'b'.repeat(40);
|
||||
},
|
||||
(receipt) => {
|
||||
receipt.release.scope = 'all';
|
||||
},
|
||||
(receipt) => {
|
||||
receipt.evidence.observedAt = '2026-08-16T00:00:00.000Z';
|
||||
},
|
||||
(receipt) => {
|
||||
receipt.evidence.reportDigest = `sha256:${'0'.repeat(64)}`;
|
||||
},
|
||||
(receipt) => {
|
||||
receipt.staticAudits.pop();
|
||||
},
|
||||
];
|
||||
for (const mutate of mutations) {
|
||||
const receipt = JSON.parse(
|
||||
JSON.stringify(
|
||||
privateReleaseEvidenceReceipt(
|
||||
RELEASE,
|
||||
'cloudnativepg-disaster-recovery',
|
||||
),
|
||||
),
|
||||
);
|
||||
mutate(receipt);
|
||||
assert.throws(
|
||||
() =>
|
||||
inspectPrivateReleaseEvidenceReceipt(
|
||||
receipt,
|
||||
options('cloudnativepg-disaster-recovery'),
|
||||
),
|
||||
/receipt|freshness/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects publishing private material in an otherwise re-digested receipt', () => {
|
||||
const receipt = JSON.parse(
|
||||
JSON.stringify(privateReleaseEvidenceReceipt(RELEASE, 'worker-management')),
|
||||
);
|
||||
receipt.evidence.privateReport = { credential: 'must-not-publish' };
|
||||
assert.throws(
|
||||
() =>
|
||||
inspectPrivateReleaseEvidenceReceipt(
|
||||
receipt,
|
||||
options('worker-management'),
|
||||
),
|
||||
/receipt shape/,
|
||||
);
|
||||
});
|
||||
|
||||
test('audit CLI accepts a canonical receipt and rejects open argument shapes', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const receiptPath = path.join(directory, 'worker-management.json');
|
||||
const receipt = privateReleaseEvidenceReceipt(RELEASE, 'worker-management');
|
||||
fs.writeFileSync(receiptPath, `${JSON.stringify(receipt)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
const args = [
|
||||
'--mode=audit',
|
||||
`--version=${RELEASE.version}`,
|
||||
`--source-revision=${RELEASE.sourceRevision}`,
|
||||
`--source-ref=${RELEASE.sourceRef}`,
|
||||
`--release-scope=${RELEASE.scope}`,
|
||||
'--evidence-kind=worker-management',
|
||||
`--receipt=${receiptPath}`,
|
||||
];
|
||||
const writes = [];
|
||||
assert.equal(
|
||||
runCli(args, path.resolve(__dirname, '../..'), {
|
||||
write(value) {
|
||||
writes.push(value);
|
||||
},
|
||||
}).compatible,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(writes.join('')).receiptDigest,
|
||||
receipt.receiptDigest,
|
||||
);
|
||||
assert.throws(
|
||||
() => parseArguments([...args, '--extra=true']),
|
||||
/arguments are invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
args.map((entry) =>
|
||||
entry === `--release-scope=${RELEASE.scope}`
|
||||
? '--release-scope=local'
|
||||
: entry,
|
||||
),
|
||||
path.resolve(__dirname, '../..'),
|
||||
{ write() {} },
|
||||
),
|
||||
/Cluster-capable/,
|
||||
);
|
||||
});
|
||||
|
||||
test('create wrappers publish only no-replace digest projections after their source-aware gates', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const reportPath = path.join(directory, 'private-report.json');
|
||||
fs.writeFileSync(
|
||||
reportPath,
|
||||
`${JSON.stringify({ observedAt: '2026-08-18T00:00:00.000Z' })}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const common = {
|
||||
root: path.resolve(__dirname, '../..'),
|
||||
version: RELEASE.version,
|
||||
sourceRevision: RELEASE.sourceRevision,
|
||||
sourceRef: RELEASE.sourceRef,
|
||||
releaseScope: RELEASE.scope,
|
||||
reportFile: reportPath,
|
||||
};
|
||||
const now = () => Date.parse('2026-08-18T00:05:00.000Z');
|
||||
const workerPath = path.join(directory, 'worker.json');
|
||||
const worker = createWorkerReceipt(
|
||||
{
|
||||
...common,
|
||||
ceremonyReportFile: path.join(directory, 'private-ceremony.json'),
|
||||
durableAuditReportFile: path.join(directory, 'private-durable.json'),
|
||||
pkiRotationReportFile: path.join(directory, 'private-pki.json'),
|
||||
caRolloverReportFile: path.join(directory, 'private-ca.json'),
|
||||
outputFile: workerPath,
|
||||
},
|
||||
{
|
||||
now,
|
||||
auditGate(options) {
|
||||
assert.equal(options.sourceCommit, RELEASE.sourceRevision);
|
||||
return {
|
||||
fixture: 'qinglong/worker-credential-management-release-gate@v1',
|
||||
evidenceReportSha256: `sha256:${'5'.repeat(64)}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(worker.staticAudits.length, 0);
|
||||
assert.equal(fs.statSync(workerPath).mode & 0o777, 0o600);
|
||||
assert.equal(JSON.stringify(worker).includes('private-report'), false);
|
||||
|
||||
const drPath = path.join(directory, 'dr.json');
|
||||
const dr = createCloudNativePgReceipt(
|
||||
{ ...common, outputFile: drPath },
|
||||
{
|
||||
now,
|
||||
validateEvidence(_report, options) {
|
||||
assert.equal(options.sourceCommit, RELEASE.sourceRevision);
|
||||
return {
|
||||
fixture: 'qinglong/cloudnativepg-disaster-recovery@v1',
|
||||
compatible: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
dr.staticAudits.map((entry) => entry.name),
|
||||
[
|
||||
'cloudnativepg-backup',
|
||||
'barman-cloud-supply-chain',
|
||||
'cert-manager-selection',
|
||||
],
|
||||
);
|
||||
assert.equal(fs.statSync(drPath).mode & 0o777, 0o600);
|
||||
assert.throws(
|
||||
() =>
|
||||
createCloudNativePgReceipt(
|
||||
{ ...common, outputFile: drPath },
|
||||
{
|
||||
now,
|
||||
validateEvidence() {
|
||||
return {
|
||||
fixture: 'qinglong/cloudnativepg-disaster-recovery@v1',
|
||||
compatible: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
),
|
||||
/output must not already exist/,
|
||||
);
|
||||
fs.chmodSync(reportPath, 0o640);
|
||||
assert.throws(
|
||||
() =>
|
||||
createWorkerReceipt(
|
||||
{
|
||||
...common,
|
||||
ceremonyReportFile: path.join(directory, 'private-ceremony.json'),
|
||||
durableAuditReportFile: path.join(directory, 'private-durable.json'),
|
||||
pkiRotationReportFile: path.join(directory, 'private-pki.json'),
|
||||
caRolloverReportFile: path.join(directory, 'private-ca.json'),
|
||||
outputFile: path.join(directory, 'untrusted-worker.json'),
|
||||
},
|
||||
{
|
||||
now,
|
||||
auditGate() {
|
||||
return {
|
||||
fixture: 'qinglong/worker-credential-management-release-gate@v1',
|
||||
evidenceReportSha256: `sha256:${'5'.repeat(64)}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
),
|
||||
/bounded canonical regular file/,
|
||||
);
|
||||
});
|
||||
@@ -19,6 +19,9 @@ const {
|
||||
const {
|
||||
auditClusterImageCiWorkflow,
|
||||
} = require('../../scripts/ql3-cluster-image-release-audit.cjs');
|
||||
const {
|
||||
privateReleaseEvidenceReceipts,
|
||||
} = require('./ql3ReleaseEvidenceFixture.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const SCRIPT = path.join(
|
||||
@@ -53,6 +56,12 @@ function selectedImages(scope) {
|
||||
|
||||
function releaseSet(scope) {
|
||||
const selected = selectedImages(scope);
|
||||
const release = {
|
||||
version: VERSION,
|
||||
sourceRevision: REVISION,
|
||||
sourceRef: SOURCE_REF,
|
||||
scope,
|
||||
};
|
||||
const images = selected.map(([name, repository], index) => {
|
||||
const digest = `sha256:${(index + 1).toString(16).repeat(64)}`;
|
||||
const reference = `ghcr.io/${OWNER}/${repository}`;
|
||||
@@ -70,13 +79,8 @@ function releaseSet(scope) {
|
||||
const names = images.map((entry) => entry.name);
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/release-set@v1',
|
||||
release: {
|
||||
version: VERSION,
|
||||
sourceRevision: REVISION,
|
||||
sourceRef: SOURCE_REF,
|
||||
scope,
|
||||
},
|
||||
schema: 'qinglong/release-set@v2',
|
||||
release,
|
||||
candidate: {
|
||||
schema: 'qinglong/release-candidate-contract@v1',
|
||||
contractDigest: `sha256:${'b'.repeat(64)}`,
|
||||
@@ -95,6 +99,7 @@ function releaseSet(scope) {
|
||||
images: names.filter((name) => name !== 'local'),
|
||||
},
|
||||
},
|
||||
evidenceReceipts: privateReleaseEvidenceReceipts(release),
|
||||
images,
|
||||
promotion: {
|
||||
authority: 'complete_verified_release_set',
|
||||
@@ -111,6 +116,10 @@ function releaseSet(scope) {
|
||||
'os-vulnerability',
|
||||
'release-candidate-contract',
|
||||
],
|
||||
privateReleaseEvidenceReceipts:
|
||||
scope === 'local'
|
||||
? []
|
||||
: ['worker-management', 'cloudnativepg-disaster-recovery'],
|
||||
releaseSetBuildProvenance: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,6 +28,9 @@ const {
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
const {
|
||||
privateReleaseEvidenceReceipts,
|
||||
} = require('./ql3ReleaseEvidenceFixture.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
@@ -65,6 +68,7 @@ function releaseSet(scope) {
|
||||
root,
|
||||
candidate,
|
||||
records,
|
||||
evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release),
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v1';
|
||||
const FIXTURES = Object.freeze({
|
||||
'worker-management': 'qinglong/worker-credential-management-release-gate@v1',
|
||||
'cloudnativepg-disaster-recovery':
|
||||
'qinglong/cloudnativepg-disaster-recovery@v1',
|
||||
});
|
||||
const STATIC_AUDITS = Object.freeze([
|
||||
'cloudnativepg-backup',
|
||||
'barman-cloud-supply-chain',
|
||||
'cert-manager-selection',
|
||||
]);
|
||||
|
||||
function sha256(value) {
|
||||
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
||||
}
|
||||
|
||||
function privateReleaseEvidenceReceipt(release, evidenceKind) {
|
||||
const staticAudits =
|
||||
evidenceKind === 'cloudnativepg-disaster-recovery'
|
||||
? STATIC_AUDITS.map((name, index) => ({
|
||||
name,
|
||||
auditDigest: `sha256:${String(index + 7).repeat(64)}`,
|
||||
compatible: true,
|
||||
}))
|
||||
: [];
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: RECEIPT_SCHEMA,
|
||||
release: { ...release },
|
||||
evidenceKind,
|
||||
evidence: {
|
||||
fixture: FIXTURES[evidenceKind],
|
||||
observedAt: '2026-08-18T00:00:00.000Z',
|
||||
validatedAt: '2026-08-18T00:05:00.000Z',
|
||||
maximumAgeSeconds: 86_400,
|
||||
reportDigest: `sha256:${
|
||||
evidenceKind === 'worker-management' ? '5' : '6'
|
||||
}`.padEnd(71, evidenceKind === 'worker-management' ? '5' : '6'),
|
||||
sourceReportsUploaded: false,
|
||||
},
|
||||
staticAudits,
|
||||
verification: {
|
||||
sourceAwareAudit: true,
|
||||
privateEvidenceReplayed: true,
|
||||
publicConsumerReplay: 'not_possible_without_private_reports',
|
||||
privateReportContentPublished: false,
|
||||
compatible: true,
|
||||
},
|
||||
};
|
||||
return Object.freeze({
|
||||
...unsigned,
|
||||
receiptDigest: sha256(JSON.stringify(unsigned)),
|
||||
});
|
||||
}
|
||||
|
||||
function privateReleaseEvidenceReceipts(release) {
|
||||
return release.scope === 'local'
|
||||
? []
|
||||
: [
|
||||
privateReleaseEvidenceReceipt(release, 'worker-management'),
|
||||
privateReleaseEvidenceReceipt(
|
||||
release,
|
||||
'cloudnativepg-disaster-recovery',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = Object.freeze({
|
||||
privateReleaseEvidenceReceipt,
|
||||
privateReleaseEvidenceReceipts,
|
||||
});
|
||||
@@ -19,6 +19,9 @@ const {
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
const {
|
||||
privateReleaseEvidenceReceipts,
|
||||
} = require('./ql3ReleaseEvidenceFixture.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
@@ -52,6 +55,10 @@ function recordsFor(releaseCandidate) {
|
||||
);
|
||||
}
|
||||
|
||||
function evidenceFor(releaseCandidate) {
|
||||
return privateReleaseEvidenceReceipts(releaseCandidate.release);
|
||||
}
|
||||
|
||||
function temporaryDirectory(t) {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-set-')),
|
||||
@@ -71,6 +78,7 @@ test('aggregates the independent Local image into one immutable release set', ()
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records,
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
});
|
||||
@@ -88,6 +96,7 @@ test('aggregates the independent Local image into one immutable release set', ()
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records,
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
}).compatible,
|
||||
@@ -101,6 +110,7 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
|
||||
root,
|
||||
candidate: clusterCandidate,
|
||||
records: recordsFor(clusterCandidate).reverse(),
|
||||
evidenceReceipts: evidenceFor(clusterCandidate),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
});
|
||||
@@ -114,12 +124,17 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
|
||||
'admin',
|
||||
'worker',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
clusterSet.evidenceReceipts.map((entry) => entry.evidenceKind),
|
||||
['worker-management', 'cloudnativepg-disaster-recovery'],
|
||||
);
|
||||
|
||||
const allCandidate = candidate('all');
|
||||
const allSet = createReleaseSet({
|
||||
root,
|
||||
candidate: allCandidate,
|
||||
records: recordsFor(allCandidate),
|
||||
evidenceReceipts: evidenceFor(allCandidate),
|
||||
...identity,
|
||||
releaseScope: 'all',
|
||||
});
|
||||
@@ -127,6 +142,51 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
|
||||
assert.deepEqual(allSet.deploymentFamilies.local.images, ['local']);
|
||||
});
|
||||
|
||||
test('requires exact private evidence receipts only for Cluster-capable scopes', () => {
|
||||
const releaseCandidate = candidate('cluster');
|
||||
const records = recordsFor(releaseCandidate);
|
||||
const receipts = evidenceFor(releaseCandidate);
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseSet({
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records,
|
||||
evidenceReceipts: receipts.slice(1),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
/receipt count differs/,
|
||||
);
|
||||
const drifted = JSON.parse(JSON.stringify(receipts));
|
||||
drifted[0].release.sourceRevision = 'c'.repeat(40);
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseSet({
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records,
|
||||
evidenceReceipts: drifted,
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
/receipt shape|release binding/,
|
||||
);
|
||||
const localCandidate = candidate('local');
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseSet({
|
||||
root,
|
||||
candidate: localCandidate,
|
||||
records: recordsFor(localCandidate),
|
||||
evidenceReceipts: [receipts[0]],
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/receipt count differs/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects missing, duplicate and cross-candidate image evidence', () => {
|
||||
const releaseCandidate = candidate('cluster');
|
||||
const records = recordsFor(releaseCandidate);
|
||||
@@ -136,6 +196,7 @@ test('rejects missing, duplicate and cross-candidate image evidence', () => {
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: records.slice(1),
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
@@ -147,6 +208,7 @@ test('rejects missing, duplicate and cross-candidate image evidence', () => {
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: [records[0], records[0], records[2], records[3]],
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
@@ -160,6 +222,7 @@ test('rejects missing, duplicate and cross-candidate image evidence', () => {
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: [mutated, ...records.slice(1)],
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
@@ -199,6 +262,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records,
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
});
|
||||
@@ -210,6 +274,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records,
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
@@ -224,6 +289,7 @@ test('inspects a release set without short-lived candidate or image records', ()
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: recordsFor(releaseCandidate),
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: scope,
|
||||
});
|
||||
@@ -243,6 +309,7 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
|
||||
root,
|
||||
candidate: releaseCandidate,
|
||||
records: recordsFor(releaseCandidate),
|
||||
evidenceReceipts: evidenceFor(releaseCandidate),
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
});
|
||||
@@ -256,6 +323,9 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
|
||||
(value) => {
|
||||
value.releaseSetDigest = `sha256:${'0'.repeat(64)}`;
|
||||
},
|
||||
(value) => {
|
||||
value.evidenceReceipts[0].receiptDigest = `sha256:${'0'.repeat(64)}`;
|
||||
},
|
||||
]) {
|
||||
const drifted = JSON.parse(JSON.stringify(releaseSet));
|
||||
mutate(drifted);
|
||||
@@ -265,7 +335,7 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
}),
|
||||
/standalone release set/,
|
||||
/standalone release set|receipt digest/,
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
@@ -283,7 +353,9 @@ test('standalone inspection rejects image, family and self-digest drift', () =>
|
||||
test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const recordsDirectory = path.join(directory, 'records');
|
||||
const evidenceDirectory = path.join(directory, 'evidence');
|
||||
fs.mkdirSync(recordsDirectory);
|
||||
fs.mkdirSync(evidenceDirectory);
|
||||
const releaseCandidate = candidate('local');
|
||||
const candidatePath = path.join(directory, 'candidate.json');
|
||||
writeCanonical(candidatePath, releaseCandidate);
|
||||
@@ -315,6 +387,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
'--mode=aggregate',
|
||||
...common,
|
||||
`--records=${recordsDirectory}`,
|
||||
`--evidence-receipts=${evidenceDirectory}`,
|
||||
`--output=${setPath}`,
|
||||
],
|
||||
root,
|
||||
@@ -326,6 +399,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
'--mode=audit',
|
||||
...common,
|
||||
`--records=${recordsDirectory}`,
|
||||
`--evidence-receipts=${evidenceDirectory}`,
|
||||
`--report=${setPath}`,
|
||||
],
|
||||
root,
|
||||
@@ -356,6 +430,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
'--mode=aggregate',
|
||||
...common,
|
||||
`--records=${recordsDirectory}`,
|
||||
`--evidence-receipts=${evidenceDirectory}`,
|
||||
`--output=${setPath}`,
|
||||
],
|
||||
root,
|
||||
@@ -368,7 +443,9 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
|
||||
test('CLI rejects extra records, symlinks and open argument shapes', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const recordsDirectory = path.join(directory, 'records');
|
||||
const evidenceDirectory = path.join(directory, 'evidence');
|
||||
fs.mkdirSync(recordsDirectory);
|
||||
fs.mkdirSync(evidenceDirectory);
|
||||
const releaseCandidate = candidate('local');
|
||||
const candidatePath = path.join(directory, 'candidate.json');
|
||||
writeCanonical(candidatePath, releaseCandidate);
|
||||
@@ -392,6 +469,7 @@ test('CLI rejects extra records, symlinks and open argument shapes', (t) => {
|
||||
'--mode=aggregate',
|
||||
...common,
|
||||
`--records=${recordsDirectory}`,
|
||||
`--evidence-receipts=${evidenceDirectory}`,
|
||||
`--output=${path.join(directory, 'set.json')}`,
|
||||
],
|
||||
root,
|
||||
@@ -409,6 +487,7 @@ test('CLI rejects extra records, symlinks and open argument shapes', (t) => {
|
||||
'--mode=aggregate',
|
||||
...common,
|
||||
`--records=${recordsDirectory}`,
|
||||
`--evidence-receipts=${evidenceDirectory}`,
|
||||
`--output=${path.join(directory, 'set.json')}`,
|
||||
],
|
||||
root,
|
||||
|
||||
Reference in New Issue
Block a user