mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): scope deployment-family release candidates
This commit is contained in:
@@ -10,7 +10,9 @@ const verifier = path.join(
|
||||
ROOT,
|
||||
'deploy/console/ql3-cluster-copilot/verify-release.sh',
|
||||
);
|
||||
const image = `ghcr.io/example/qinglong3-cluster-admin@sha256:${'b'.repeat(64)}`;
|
||||
const image = `ghcr.io/example/qinglong3-cluster-admin@sha256:${'b'.repeat(
|
||||
64,
|
||||
)}`;
|
||||
const revision = 'c'.repeat(40);
|
||||
|
||||
function fixture(t) {
|
||||
@@ -46,7 +48,7 @@ function invoke(args, env) {
|
||||
});
|
||||
}
|
||||
|
||||
test('verifies one signature and three digest-bound GitHub attestations', (t) => {
|
||||
test('verifies one signature and four digest-bound GitHub attestations', (t) => {
|
||||
assert.equal(fs.statSync(verifier).mode & 0o777, 0o755);
|
||||
const value = fixture(t);
|
||||
const result = invoke(
|
||||
@@ -61,11 +63,12 @@ test('verifies one signature and three digest-bound GitHub attestations', (t) =>
|
||||
provenance: true,
|
||||
sbom: true,
|
||||
osVulnerabilityEvidence: true,
|
||||
releaseCandidateContract: true,
|
||||
compatible: true,
|
||||
});
|
||||
const calls = fs.readFileSync(value.capture, 'utf8');
|
||||
assert.equal((calls.match(/^cosign$/gmu) ?? []).length, 1);
|
||||
assert.equal((calls.match(/^gh$/gmu) ?? []).length, 3);
|
||||
assert.equal((calls.match(/^gh$/gmu) ?? []).length, 4);
|
||||
for (const required of [
|
||||
'arg=--certificate-identity',
|
||||
'arg=https://github.com/example/qinglong/.github/workflows/ql3-image-release.yml@refs/tags/v3.0.0-alpha.1',
|
||||
@@ -83,17 +86,26 @@ test('verifies one signature and three digest-bound GitHub attestations', (t) =>
|
||||
'arg=refs/tags/v3.0.0-alpha.1',
|
||||
'arg=https://cyclonedx.org/bom',
|
||||
'arg=https://qinglong.dev/attestations/image-os-vulnerability/v1',
|
||||
'arg=https://qinglong.dev/attestations/release-candidate-contract/v1',
|
||||
'arg=--deny-self-hosted-runners',
|
||||
'arg=--bundle-from-oci',
|
||||
]) {
|
||||
assert.match(calls, new RegExp(`^${required.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'mu'));
|
||||
assert.match(
|
||||
calls,
|
||||
new RegExp(`^${required.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'mu'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects mutable or source-unbound inputs before invoking trust tools', (t) => {
|
||||
const value = fixture(t);
|
||||
for (const args of [
|
||||
['ghcr.io/example/qinglong3-cluster-admin:latest', 'example/qinglong', revision, 'refs/tags/v3.0.0'],
|
||||
[
|
||||
'ghcr.io/example/qinglong3-cluster-admin:latest',
|
||||
'example/qinglong',
|
||||
revision,
|
||||
'refs/tags/v3.0.0',
|
||||
],
|
||||
[image, 'other/qinglong', revision, 'refs/tags/v3.0.0'],
|
||||
[image, 'example/qinglong', 'short', 'refs/tags/v3.0.0'],
|
||||
[image, 'example/qinglong', revision, 'refs/heads/next'],
|
||||
|
||||
@@ -118,7 +118,8 @@ test('runs the immutable release ceremony and writes only digest-level evidence'
|
||||
assert.equal(report.release.sourceRevision, revision);
|
||||
assert.equal(report.verification.embeddedEvidenceVerifier, true);
|
||||
assert.equal(report.evidenceVector.classification, 'synthetic_non_sensitive');
|
||||
assert.equal(report.steps.length, 7);
|
||||
assert.equal(report.steps.length, 8);
|
||||
assert.equal(report.verification.releaseCandidateContract, true);
|
||||
assert.equal(report.claims.reportAttestation, 'none');
|
||||
assert.equal(reportText.includes(token), false);
|
||||
assert.equal(fs.statSync(value.output).mode & 0o777, 0o600);
|
||||
@@ -136,11 +137,11 @@ test('runs the immutable release ceremony and writes only digest-level evidence'
|
||||
.map((line) => JSON.parse(line));
|
||||
assert.deepEqual(
|
||||
calls.map(({ name }) => name),
|
||||
['cosign', 'gh', 'gh', 'gh', 'docker', 'docker', 'docker'],
|
||||
['cosign', 'gh', 'gh', 'gh', 'gh', 'docker', 'docker', 'docker'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
calls.map(({ tokenPresent }) => tokenPresent),
|
||||
[false, true, true, true, false, false, false],
|
||||
[false, true, true, true, true, false, false, false],
|
||||
);
|
||||
assert.deepEqual(calls[0].args, [
|
||||
'verify',
|
||||
@@ -150,7 +151,7 @@ test('runs the immutable release ceremony and writes only digest-level evidence'
|
||||
'https://token.actions.githubusercontent.com',
|
||||
image,
|
||||
]);
|
||||
for (const call of calls.slice(1, 4)) {
|
||||
for (const call of calls.slice(1, 5)) {
|
||||
for (const required of [
|
||||
'attestation',
|
||||
'verify',
|
||||
@@ -176,7 +177,12 @@ test('runs the immutable release ceremony and writes only digest-level evidence'
|
||||
'https://qinglong.dev/attestations/image-os-vulnerability/v1',
|
||||
),
|
||||
);
|
||||
const dockerRun = calls[6].args;
|
||||
assert.ok(
|
||||
calls[4].args.includes(
|
||||
'https://qinglong.dev/attestations/release-candidate-contract/v1',
|
||||
),
|
||||
);
|
||||
const dockerRun = calls[7].args;
|
||||
for (const required of [
|
||||
'--read-only',
|
||||
'none',
|
||||
|
||||
@@ -64,6 +64,7 @@ function validReport() {
|
||||
['provenance_attestation', 'gh'],
|
||||
['cyclonedx_sbom_attestation', 'gh'],
|
||||
['os_vulnerability_attestation', 'gh'],
|
||||
['release_candidate_attestation', 'gh'],
|
||||
['immutable_image_pull', 'docker'],
|
||||
['local_digest_inspection', 'docker'],
|
||||
['embedded_evidence_verifier', 'docker'],
|
||||
@@ -89,6 +90,7 @@ function validReport() {
|
||||
provenance: true,
|
||||
cyclonedxSbom: true,
|
||||
osVulnerabilityEvidence: true,
|
||||
releaseCandidateContract: true,
|
||||
imagePulled: true,
|
||||
localRepoDigestBound: true,
|
||||
embeddedEvidenceVerifier: true,
|
||||
@@ -116,9 +118,9 @@ function validReport() {
|
||||
name,
|
||||
tool,
|
||||
executableSha256: toolDigest[tool],
|
||||
argvSha256: `sha256:${'56789ab'[index].repeat(64)}`,
|
||||
stdoutBytes: index === 6 ? 400 : 0,
|
||||
stdoutSha256: index === 6 ? `sha256:${'d'.repeat(64)}` : emptyDigest,
|
||||
argvSha256: `sha256:${'56789abc'[index].repeat(64)}`,
|
||||
stdoutBytes: index === 7 ? 400 : 0,
|
||||
stdoutSha256: index === 7 ? `sha256:${'d'.repeat(64)}` : emptyDigest,
|
||||
stderrBytes: 0,
|
||||
stderrSha256: emptyDigest,
|
||||
exitCode: 0,
|
||||
@@ -166,7 +168,7 @@ test('accepts a canonical digest-bound ceremony report without replay claims', (
|
||||
compatible: true,
|
||||
reportContentDigest: report.contentDigest,
|
||||
releaseImage: image,
|
||||
verificationSteps: 7,
|
||||
verificationSteps: 8,
|
||||
externalResults: 'not_replayed',
|
||||
actionAuthority: 'none',
|
||||
});
|
||||
@@ -187,7 +189,7 @@ test('accepts a canonical digest-bound ceremony report without replay claims', (
|
||||
compatible: true,
|
||||
reportContentDigest: report.contentDigest,
|
||||
releaseImage: image,
|
||||
verificationSteps: 7,
|
||||
verificationSteps: 8,
|
||||
externalResults: 'not_replayed',
|
||||
actionAuthority: 'none',
|
||||
});
|
||||
@@ -205,7 +207,7 @@ test('rejects structural claim widening even after the report is re-digested', (
|
||||
report.isolation.network = 'default';
|
||||
},
|
||||
(report) => {
|
||||
report.steps[6].stderrBytes = 1;
|
||||
report.steps[7].stderrBytes = 1;
|
||||
},
|
||||
(report) => {
|
||||
report.secret = 'must-not-be-accepted';
|
||||
|
||||
@@ -23,7 +23,7 @@ const releaseSource = fs.readFileSync(
|
||||
test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
assert.deepEqual(auditClusterImageRelease(root), {
|
||||
ci: {
|
||||
images: ['control', 'control-ai', 'admin', 'local'],
|
||||
images: ['control', 'control-ai', 'admin', 'local', 'worker'],
|
||||
nativeArchitectures: ['amd64', 'arm64'],
|
||||
runtimeInventory: true,
|
||||
clusterAdminProductFacade: true,
|
||||
@@ -40,6 +40,14 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
},
|
||||
release: {
|
||||
trigger: 'explicit protected v3 tag dispatch',
|
||||
releaseCandidateContract: {
|
||||
scopes: ['local', 'cluster', 'all'],
|
||||
workspacePackages: 18,
|
||||
sourceDerived: true,
|
||||
digestAttested: true,
|
||||
localClusterEvidenceRequired: false,
|
||||
clusterPrivateEvidenceRequired: true,
|
||||
},
|
||||
workerManagementEvidence: {
|
||||
sourceAware: true,
|
||||
privateEphemeralRunner: true,
|
||||
@@ -65,11 +73,16 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
immutableArtifactRetentionDays: 1,
|
||||
attestedToPublishedDigest: true,
|
||||
},
|
||||
images: ['control', 'control-ai', 'admin', 'local'],
|
||||
images: ['control', 'control-ai', 'admin', 'worker', 'local'],
|
||||
platforms: ['linux/amd64', 'linux/arm64'],
|
||||
keylessSignature: true,
|
||||
buildkitAttestations: ['sbom', 'provenance'],
|
||||
githubAttestations: ['provenance', 'sbom', 'os-vulnerability'],
|
||||
githubAttestations: [
|
||||
'provenance',
|
||||
'sbom',
|
||||
'os-vulnerability',
|
||||
'release-candidate',
|
||||
],
|
||||
publication: {
|
||||
copier: 'regctl@0.11.5',
|
||||
copierSha256:
|
||||
@@ -85,6 +98,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
'provenance',
|
||||
'cyclonedx',
|
||||
'os-vulnerability',
|
||||
'release-candidate',
|
||||
'release-tags',
|
||||
],
|
||||
},
|
||||
@@ -175,6 +189,17 @@ test('rejects removal of the native cluster-admin image gate', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the native Worker image gate', () => {
|
||||
const mutated = ciSource.replace(
|
||||
' image: worker\n repository: qinglong3-worker\n runtime_user: 65532:65532',
|
||||
' image: worker-disabled\n repository: qinglong3-worker\n runtime_user: 65532:65532',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/matrices must contain only exact/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects an additional unreviewed CI image authority', () => {
|
||||
const mutated = ciSource.replace(
|
||||
' target: runtime\n steps:',
|
||||
@@ -245,7 +270,7 @@ test('rejects release publication without the private evidence dependency', () =
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/depend on both protected ephemeral private evidence jobs/,
|
||||
/always require candidate and OS gates/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -256,7 +281,7 @@ test('rejects release publication without the OS vulnerability dependency', () =
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/depend on both protected ephemeral private evidence jobs/,
|
||||
/always require candidate and OS gates/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -267,7 +292,7 @@ test('rejects release publication without current disaster-recovery evidence', (
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/depend on both protected ephemeral private evidence jobs/,
|
||||
/always require candidate and OS gates/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -289,12 +314,12 @@ test('rejects disaster-recovery evidence detached from the release source', () =
|
||||
|
||||
test('rejects an incomplete native OS vulnerability architecture matrix', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' - image: local\n runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: arm64\n dockerfile: deploy/containers/ql3-local-application/Dockerfile',
|
||||
' - image: local\n runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: disabled\n dockerfile: deploy/containers/ql3-local-application/Dockerfile',
|
||||
'include: ${{ fromJSON(needs.release-candidate.outputs.os-matrix) }}',
|
||||
'include: []',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/OS vulnerability matrix must scan exact/,
|
||||
/OS vulnerability matrix must come from/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -349,7 +374,7 @@ test('rejects a reusable private evidence runner', () => {
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/protected ephemeral private evidence job/,
|
||||
/always require candidate and OS gates/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -404,34 +429,34 @@ test('rejects a release missing application SBOM attestation', () => {
|
||||
|
||||
test('rejects a release missing the independent admin image', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'- image: admin\n repository: qinglong3-cluster-admin\n runtime_root: deploy/containers/ql3-cluster-admin/runtime-dependencies',
|
||||
'- image: admin-disabled\n repository: qinglong3-cluster-admin\n runtime_root: deploy/containers/ql3-cluster-admin/runtime-dependencies',
|
||||
'include: ${{ fromJSON(needs.release-candidate.outputs.publish-matrix) }}',
|
||||
'include: []',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/matrix must contain only exact/,
|
||||
/matrix must come only from/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a release missing the AI-excluded local image', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'- image: local\n repository: qinglong3-local-application\n runtime_root: deploy/containers/ql3-local-application/runtime-dependencies',
|
||||
'- image: local-disabled\n repository: qinglong3-local-application\n runtime_root: deploy/containers/ql3-local-application/runtime-dependencies',
|
||||
' - local\n - cluster\n - all',
|
||||
' - cluster\n - all',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/matrix must contain only exact/,
|
||||
/closed local, cluster or all/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects an additional repository in the privileged release matrix', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
' runtime_root: deploy/containers/ql3-local-application/runtime-dependencies\n steps:',
|
||||
' runtime_root: deploy/containers/ql3-local-application/runtime-dependencies\n - image: unreviewed\n repository: unreviewed\n dockerfile: Dockerfile\n runtime_root: unreviewed\n steps:',
|
||||
'include: ${{ fromJSON(needs.release-candidate.outputs.publish-matrix) }}',
|
||||
'include:\n - image: unreviewed\n repository: unreviewed\n runtime_root: unreviewed',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/matrix must contain only exact/,
|
||||
/matrix must come only from/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -639,3 +664,36 @@ test('rejects removal of the digest-bound OS vulnerability attestation', () => {
|
||||
/digest-bound OS vulnerability evidence/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the digest-bound release candidate attestation', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'predicate-type: https://qinglong.dev/attestations/release-candidate-contract/v1',
|
||||
'predicate-type: https://example.invalid/not-a-candidate-contract',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/source-derived release candidate contract/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects treating skipped private evidence as cluster success', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
"needs.release-candidate.outputs.cluster-evidence-required != 'true' ||",
|
||||
"needs.release-candidate.outputs.cluster-evidence-required == 'true' ||",
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/requiring private HA evidence only for a cluster family/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a publisher matrix detached from the source-derived contract', () => {
|
||||
const mutated = releaseSource.replace(
|
||||
'include: ${{ fromJSON(needs.release-candidate.outputs.publish-matrix) }}',
|
||||
'include: ${{ fromJSON(inputs.publish_matrix) }}',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseWorkflow(mutated),
|
||||
/matrix must come only from/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -124,6 +124,34 @@ test('generates the AI-excluded local application image closure', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('generates the headless Worker image runtime closure', () => {
|
||||
const document = createClusterImageSbom({ root, image: 'worker' });
|
||||
const report = auditClusterImageSbom(document, {
|
||||
root,
|
||||
image: 'worker',
|
||||
});
|
||||
|
||||
assert.deepEqual(report, {
|
||||
image: 'worker',
|
||||
root: 'pkg:npm/%40qinglong/worker-image-dependencies@3.0.0-alpha.0',
|
||||
components: 27,
|
||||
externalComponents: 24,
|
||||
internalComponents: 3,
|
||||
dependencyNodes: 28,
|
||||
inventoryVerified: false,
|
||||
});
|
||||
assert.equal(
|
||||
document.components.some(
|
||||
(component) => component.name === '@qinglong/worker-runtime',
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
document.components.some((component) => component.name === '@qinglong/ai'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a control SBOM presented as cluster-admin evidence', () => {
|
||||
const document = createClusterImageSbom({ root });
|
||||
assert.throws(
|
||||
|
||||
@@ -25,6 +25,7 @@ function createFixture(t, options = {}) {
|
||||
const isControl = image === 'control' || image === 'control-ai';
|
||||
const isControlAi = image === 'control-ai';
|
||||
const isLocal = image === 'local';
|
||||
const isWorker = image === 'worker';
|
||||
const layoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-oci-layout-'));
|
||||
t.after(() => fs.rmSync(layoutRoot, { recursive: true, force: true }));
|
||||
const blobDirectory = path.join(layoutRoot, 'blobs', 'sha256');
|
||||
@@ -96,6 +97,8 @@ function createFixture(t, options = {}) {
|
||||
User:
|
||||
options.rootArm64 && architecture === 'arm64'
|
||||
? '0:0'
|
||||
: isWorker
|
||||
? '65532:65532'
|
||||
: isLocal
|
||||
? '65532:65532'
|
||||
: '10001:10001',
|
||||
@@ -108,7 +111,9 @@ function createFixture(t, options = {}) {
|
||||
],
|
||||
Entrypoint: [
|
||||
'node',
|
||||
isLocal
|
||||
isWorker
|
||||
? '/opt/qinglong/node_modules/@qinglong/worker-runtime/dist/process/workerProcessCli.js'
|
||||
: isLocal
|
||||
? '/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js'
|
||||
: isControl
|
||||
? isControlAi
|
||||
@@ -118,6 +123,12 @@ function createFixture(t, options = {}) {
|
||||
],
|
||||
WorkingDir: '/opt/qinglong',
|
||||
Labels: {
|
||||
...(isWorker
|
||||
? {
|
||||
'io.qinglong.profile': 'worker',
|
||||
'io.qinglong.worker.capacity-profiles': 'edge,node',
|
||||
}
|
||||
: {}),
|
||||
...(isLocal
|
||||
? {
|
||||
'io.qinglong.ai': 'excluded',
|
||||
@@ -131,6 +142,8 @@ function createFixture(t, options = {}) {
|
||||
: {}),
|
||||
'org.opencontainers.image.description': isLocal
|
||||
? 'QingLong 3.0 AI-excluded Edge and Standalone runtime'
|
||||
: isWorker
|
||||
? 'QingLong 3.0 headless Remote Worker runtime'
|
||||
: isControl
|
||||
? isControlAi
|
||||
? 'Optional QingLong 3.0 AI-enabled cluster control plane'
|
||||
@@ -142,12 +155,14 @@ function createFixture(t, options = {}) {
|
||||
'https://github.com/whyour/qinglong',
|
||||
'org.opencontainers.image.title': isLocal
|
||||
? 'QingLong 3.0 Local Application'
|
||||
: isWorker
|
||||
? 'QingLong 3.0 Worker'
|
||||
: isControl
|
||||
? isControlAi
|
||||
? 'QingLong 3.0 Cluster Control AI'
|
||||
: 'QingLong 3.0 Cluster Control'
|
||||
: 'QingLong 3.0 Cluster Admin',
|
||||
...(isLocal
|
||||
...(isLocal || isWorker || isControl || image === 'admin'
|
||||
? {
|
||||
'org.opencontainers.image.version': '3.0.0-alpha.0',
|
||||
}
|
||||
@@ -349,6 +364,20 @@ test('accepts the AI-excluded local image and attestation closure', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts the headless Worker image and attestation closure', (t) => {
|
||||
const report = auditClusterOciLayout({
|
||||
root,
|
||||
layoutRoot: createFixture(t, { image: 'worker' }),
|
||||
expectedRevision: revision,
|
||||
image: 'worker',
|
||||
});
|
||||
assert.equal(report.image, 'worker');
|
||||
assert.deepEqual(
|
||||
report.platforms.map((platform) => platform.platform),
|
||||
['linux/amd64', 'linux/arm64'],
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects cluster-control config presented as cluster-admin evidence', (t) => {
|
||||
assert.throws(
|
||||
() =>
|
||||
|
||||
@@ -25,7 +25,8 @@ function exception(overrides = {}) {
|
||||
owner: 'security/platform',
|
||||
ticket: 'QLSEC-123',
|
||||
expiresOn: '2026-08-15',
|
||||
rationale: 'Temporary exposure accepted while the fixed base image is qualified.',
|
||||
rationale:
|
||||
'Temporary exposure accepted while the fixed base image is qualified.',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -55,6 +56,7 @@ test('accepts the empty fail-closed production exception policy', () => {
|
||||
control: 0,
|
||||
'control-ai': 0,
|
||||
local: 0,
|
||||
worker: 0,
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
@@ -132,10 +134,7 @@ test('rejects unscoped images and non-OS package purls', () => {
|
||||
test('rejects duplicate, unsorted and extensible exception identities', () => {
|
||||
for (const exceptions of [
|
||||
[exception(), exception()],
|
||||
[
|
||||
exception({ id: 'CVE-2026-99999' }),
|
||||
exception({ id: 'CVE-2026-12345' }),
|
||||
],
|
||||
[exception({ id: 'CVE-2026-99999' }), exception({ id: 'CVE-2026-12345' })],
|
||||
[{ ...exception(), extra: true }],
|
||||
]) {
|
||||
const audit = auditImageOsVulnerabilityPolicy(policy(exceptions), {
|
||||
@@ -144,8 +143,7 @@ test('rejects duplicate, unsorted and extensible exception identities', () => {
|
||||
assert.equal(audit.compatible, false);
|
||||
assert.equal(
|
||||
audit.findings.some(
|
||||
(finding) =>
|
||||
finding.code === 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_ID',
|
||||
(finding) => finding.code === 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_ID',
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
'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 {
|
||||
auditReleaseCandidateContract,
|
||||
createReleaseCandidateContract,
|
||||
parseArguments,
|
||||
runCli,
|
||||
} = require('../../scripts/ql3-release-candidate-contract.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const identity = Object.freeze({
|
||||
version: '3.0.0-alpha.0',
|
||||
sourceRevision: 'a'.repeat(40),
|
||||
sourceRef: 'refs/tags/v3.0.0-alpha.0',
|
||||
});
|
||||
|
||||
test('freezes an independent low-resource local release family', () => {
|
||||
const contract = createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
});
|
||||
assert.deepEqual(
|
||||
contract.images.map((entry) => entry.image),
|
||||
['local'],
|
||||
);
|
||||
assert.equal(contract.releasePlan.clusterEvidenceRequired, false);
|
||||
assert.deepEqual(contract.deploymentFamilies.local.profiles, [
|
||||
'edge',
|
||||
'standalone',
|
||||
]);
|
||||
assert.equal(contract.workspace.packageCount, 18);
|
||||
assert.match(contract.contractDigest, /^sha256:[a-f0-9]{64}$/u);
|
||||
assert.deepEqual(
|
||||
auditReleaseCandidateContract(contract, {
|
||||
root,
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
{
|
||||
compatible: true,
|
||||
contractDigest: contract.contractDigest,
|
||||
releaseScope: 'local',
|
||||
workspacePackageCount: 18,
|
||||
images: ['local'],
|
||||
clusterEvidenceRequired: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('closes the cluster release family with the Worker image', () => {
|
||||
const contract = createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
releaseScope: 'cluster',
|
||||
});
|
||||
assert.deepEqual(
|
||||
contract.images.map((entry) => entry.image),
|
||||
['control', 'control-ai', 'admin', 'worker'],
|
||||
);
|
||||
assert.equal(contract.releasePlan.clusterEvidenceRequired, true);
|
||||
assert.equal(contract.releasePlan.osMatrix.length, 8);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('edge-and-standalone-rollout'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('cloudnativepg-disaster-recovery-evidence'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('combines local and cluster families without weakening either gate', () => {
|
||||
const contract = createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
releaseScope: 'all',
|
||||
});
|
||||
assert.deepEqual(
|
||||
contract.images.map((entry) => entry.image),
|
||||
['control', 'control-ai', 'admin', 'worker', 'local'],
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('edge-and-standalone-rollout'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
contract.requiredGates.includes('worker-management-production-evidence'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects tag, version and source identity drift', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
sourceRef: 'refs/heads/next',
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/exact version tag/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
version: '2.21.0',
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/QingLong 3 SemVer/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
sourceRevision: 'movable',
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/Git SHA-1/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a source-derived report mutated after creation', () => {
|
||||
const contract = createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
});
|
||||
contract.releasePlan.publishMatrix[0].repository = 'unreviewed';
|
||||
assert.throws(
|
||||
() =>
|
||||
auditReleaseCandidateContract(contract, {
|
||||
root,
|
||||
...identity,
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/differs from the source-derived contract/,
|
||||
);
|
||||
});
|
||||
|
||||
test('writes once and independently audits the exact report through the CLI', (t) => {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-candidate-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const report = path.join(directory, 'contract.json');
|
||||
const common = [
|
||||
'--version=3.0.0-alpha.0',
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
'--source-ref=refs/tags/v3.0.0-alpha.0',
|
||||
'--release-scope=local',
|
||||
];
|
||||
const output = { write() {} };
|
||||
runCli(['--mode=create', ...common, `--output=${report}`], root, output);
|
||||
assert.equal(fs.statSync(report).mode & 0o777, 0o600);
|
||||
assert.equal(
|
||||
runCli(['--mode=audit', ...common, `--report=${report}`], root, output)
|
||||
.compatible,
|
||||
true,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
['--mode=create', ...common, `--output=${report}`],
|
||||
root,
|
||||
output,
|
||||
),
|
||||
/output must be unused/,
|
||||
);
|
||||
});
|
||||
|
||||
test('parses only exact closed create and audit modes', () => {
|
||||
const common = [
|
||||
'--version=3.0.0-alpha.0',
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
'--source-ref=refs/tags/v3.0.0-alpha.0',
|
||||
'--release-scope=local',
|
||||
];
|
||||
assert.equal(
|
||||
parseArguments(['--mode=create', ...common, '--output=/tmp/report.json'])
|
||||
.mode,
|
||||
'create',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseArguments([
|
||||
'--mode=create',
|
||||
...common,
|
||||
'--output=/tmp/report.json',
|
||||
'--extra=true',
|
||||
]),
|
||||
/arguments are invalid/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user