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);
@@ -0,0 +1,180 @@
'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 {
argumentsValue,
resolveReleaseSelection,
} = require('../../scripts/ql3-local-compose-rollout-live-contract.cjs');
const {
writeSyntheticLocalReleaseSelection,
} = require('../../scripts/lib/ql3-local-release-selection-test-fixture.cjs');
const IMAGE = `ghcr.io/example/qinglong3-local-application@sha256:${'a'.repeat(
64,
)}`;
function privateDirectory(t) {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-catalog-selection-')),
);
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return directory;
}
function catalogSelection(t) {
const directory = privateDirectory(t);
return {
directory,
releaseSelection: writeSyntheticLocalReleaseSelection({
directory,
image: IMAGE,
allowRootService: process.getuid() === 0,
sourceRevision: 'b'.repeat(40),
}),
};
}
test('preserves the existing synthetic rollout arguments', (t) => {
const directory = privateDirectory(t);
const socket = path.join(directory, 'docker.sock');
fs.writeFileSync(socket, 'fixture', { mode: 0o600 });
assert.deepEqual(
argumentsValue([
`--image=${IMAGE}`,
`--docker-executable=${fs.realpathSync(process.execPath)}`,
`--docker-socket=${fs.realpathSync(socket)}`,
'--profile=edge',
]),
{
image: IMAGE,
profile: 'edge',
dockerExecutable: fs.realpathSync(process.execPath),
dockerSocket: fs.realpathSync(socket),
},
);
});
test('accepts only a complete catalog selection handoff', (t) => {
const { directory, releaseSelection } = catalogSelection(t);
const socket = path.join(directory, 'docker.sock');
fs.writeFileSync(socket, 'fixture', { mode: 0o600 });
const common = [
`--image=${IMAGE}`,
`--docker-executable=${fs.realpathSync(process.execPath)}`,
`--docker-socket=${fs.realpathSync(socket)}`,
'--profile=standalone',
];
assert.deepEqual(
argumentsValue([
...common,
`--release-selection=${releaseSelection.path}`,
`--expected-selection-digest=${releaseSelection.expectedSelectionDigest}`,
]).releaseSelection,
releaseSelection,
);
assert.throws(
() =>
argumentsValue([
...common,
`--release-selection=${releaseSelection.path}`,
]),
/usage/,
);
assert.throws(
() =>
argumentsValue([
...common,
`--expected-selection-digest=${releaseSelection.expectedSelectionDigest}`,
]),
/usage/,
);
});
test('projects a content-free verified catalog authority from the exact selection', (t) => {
const { directory, releaseSelection } = catalogSelection(t);
const result = resolveReleaseSelection(
{ image: IMAGE, releaseSelection },
directory,
process.getuid(),
);
assert.equal(result.releaseSelection, releaseSelection);
assert.deepEqual(result.authority, {
mode: 'verified_release_catalog',
sourceRevision: 'b'.repeat(40),
sourceRef: 'refs/tags/v3.0.0-alpha.0',
scope: 'local',
releaseSetDigest: JSON.parse(fs.readFileSync(releaseSelection.path, 'utf8'))
.releaseSetDigest,
catalogManifestDigest: JSON.parse(
fs.readFileSync(releaseSelection.path, 'utf8'),
).catalog.manifestDigest,
catalogConsumptionDigest: JSON.parse(
fs.readFileSync(releaseSelection.path, 'utf8'),
).catalog.consumptionReportDigest,
selectionDigest: releaseSelection.expectedSelectionDigest,
});
});
test('rejects selection drift, image substitution and wider permissions', (t) => {
const first = catalogSelection(t);
assert.throws(
() =>
resolveReleaseSelection(
{
image: IMAGE.replace('qinglong3-local-application', 'other'),
releaseSelection: first.releaseSelection,
},
first.directory,
process.getuid(),
),
/incompatible/,
);
const second = catalogSelection(t);
fs.chmodSync(second.releaseSelection.path, 0o640);
assert.throws(
() =>
resolveReleaseSelection(
{ image: IMAGE, releaseSelection: second.releaseSelection },
second.directory,
process.getuid(),
),
/incompatible/,
);
const third = catalogSelection(t);
const selection = JSON.parse(
fs.readFileSync(third.releaseSelection.path, 'utf8'),
);
selection.release.sourceRevision = 'c'.repeat(40);
fs.writeFileSync(
third.releaseSelection.path,
`${JSON.stringify(selection)}\n`,
{ mode: 0o600 },
);
assert.throws(
() =>
resolveReleaseSelection(
{ image: IMAGE, releaseSelection: third.releaseSelection },
third.directory,
process.getuid(),
),
/incompatible/,
);
});
test('marks an ordinary PR rollout as a synthetic fixture', (t) => {
const directory = privateDirectory(t);
const result = resolveReleaseSelection(
{ image: IMAGE },
directory,
process.getuid(),
);
assert.deepEqual(result.authority, { mode: 'synthetic_live_fixture' });
assert.equal(fs.existsSync(result.releaseSelection.path), true);
});