feat(ql3): gate local releases through published catalogs

This commit is contained in:
whyour
2026-08-18 00:56:12 +08:00
parent 9cd42ff454
commit fd433c41cd
9 changed files with 794 additions and 19 deletions
+81 -1
View File
@@ -124,6 +124,15 @@ test('accepts the reviewed native CI and digest release contracts', () => {
fencedRetirementReceiptAudited: true,
publicationAuthority: false,
},
localCatalogDeploymentGate: {
scopes: ['local', 'all'],
catalogAuthority: 'immutable_digest_after_public_consumption',
selectionReconstructed: true,
profiles: ['edge', 'standalone'],
rolloutReceiptAudited: true,
gracefulCleanup: true,
publicationAuthority: false,
},
localRolloutPreflight: true,
localRolloutApply: true,
postPublishVerification: [
@@ -136,6 +145,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
'release-set',
'durable-catalog',
'catalog-consumption',
'catalog-bound-local-compose-deployment',
'catalog-bound-k3s-deployment',
'release-tags',
],
@@ -729,7 +739,7 @@ test('rejects an image tag created before digest verification completes', () =>
});
test('rejects any publisher step after the deployment lock is published', () => {
const marker = '\n release-catalog-deployment-live:';
const marker = '\n release-catalog-local-deployment-live:';
assert.equal(releaseSource.includes(marker), true);
const mutated = releaseSource.replace(
marker,
@@ -742,6 +752,76 @@ test('rejects any publisher step after the deployment lock is published', () =>
);
});
test('rejects removal of the downstream Local catalog deployment gate', function rejectsMissingLocalCatalogDeploymentGate() {
const localMarker = '\n release-catalog-local-deployment-live:';
const clusterMarker = '\n release-catalog-deployment-live:';
const localOffset = releaseSource.indexOf(localMarker);
const clusterOffset = releaseSource.indexOf(clusterMarker);
assert.notEqual(localOffset, -1);
assert.equal(clusterOffset > localOffset, true);
const mutated = `${releaseSource.slice(0, localOffset)}${releaseSource.slice(
clusterOffset,
)}`;
assert.throws(
() => auditReleaseWorkflow(mutated),
/keep evidence read-only|Local release must independently consume/,
);
});
test('rejects running the Local catalog gate for Cluster-only releases', function rejectsClusterLocalCatalogDeploymentGate() {
const mutated = releaseSource.replace(
"inputs.release_scope != 'cluster'",
"inputs.release_scope == 'cluster'",
);
assert.throws(
() => auditReleaseWorkflow(mutated),
/Local release must independently consume/,
);
});
test('rejects a Local rollout detached from the audited catalog selection', function rejectsDetachedLocalCatalogSelection() {
const localMarker = '\n release-catalog-local-deployment-live:';
const clusterMarker = '\n release-catalog-deployment-live:';
const localOffset = releaseSource.indexOf(localMarker);
const clusterOffset = releaseSource.indexOf(clusterMarker);
const mutatedLocal = releaseSource
.slice(localOffset, clusterOffset)
.replace(
'--release-selection="${RELEASE_SELECTION}"',
'--release-selection="/tmp/unreviewed.json"',
);
assert.throws(
() =>
auditReleaseWorkflow(
`${releaseSource.slice(
0,
localOffset,
)}${mutatedLocal}${releaseSource.slice(clusterOffset)}`,
),
/Local release must independently consume/,
);
});
test('rejects publication authority in the post-publish Local catalog consumer', function rejectsLocalCatalogConsumerPublicationAuthority() {
const localMarker = '\n release-catalog-local-deployment-live:';
const clusterMarker = '\n release-catalog-deployment-live:';
const localOffset = releaseSource.indexOf(localMarker);
const clusterOffset = releaseSource.indexOf(clusterMarker);
const mutatedLocal = releaseSource
.slice(localOffset, clusterOffset)
.replace('packages: read', 'packages: write');
assert.throws(
() =>
auditReleaseWorkflow(
`${releaseSource.slice(
0,
localOffset,
)}${mutatedLocal}${releaseSource.slice(clusterOffset)}`,
),
/keep evidence read-only|without publication authority/,
);
});
test('rejects removal of the downstream catalog deployment release gate', function rejectsMissingCatalogDeploymentGate() {
const marker = '\n release-catalog-deployment-live:';
const offset = releaseSource.indexOf(marker);