feat(ql3): bind local compose revisions to release catalogs

This commit is contained in:
whyour
2026-08-16 16:02:53 +08:00
parent 2ccdd71851
commit dce72c800f
17 changed files with 973 additions and 130 deletions
@@ -0,0 +1,67 @@
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
function sha256(value) {
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
}
function writeSyntheticLocalReleaseSelection(options) {
const releaseSetDigest = sha256(`release-set:${options.image}`);
const manifestDigest = sha256(`catalog-manifest:${options.image}`);
const consumptionReportDigest = sha256(`catalog-report:${options.image}`);
const unsigned = {
schemaVersion: 1,
schema: 'qinglong/local-compose-release-image@v2',
release: {
version: '3.0.0-alpha.0',
sourceRevision: options.sourceRevision ?? '3'.repeat(40),
sourceRef: 'refs/tags/v3.0.0-alpha.0',
scope: 'local',
},
releaseSetDigest,
catalog: {
schema: 'qinglong/release-catalog-consumption-ceremony@v1',
sourceRepository: 'example/qinglong',
workflowIdentity:
'https://github.com/example/qinglong/.github/workflows/ql3-image-release.yml@refs/tags/v3.0.0-alpha.0',
immutableReference: `ghcr.io/example/qinglong3-release-catalog@${manifestDigest}`,
manifestDigest,
consumptionReportDigest,
releaseSetDigest,
discoveryTagAuthority: 'none',
},
deploymentFamily: 'local',
service: {
kind: 'compose',
image: options.image,
allowRootService: options.allowRootService,
},
verification: {
releaseSet: 'standalone_structure_identity_and_self_digest',
sourceRecordsReplayed: false,
catalogConsumption: 'offline_reconstructed',
externalToolResultsReplayed: false,
networkAccess: false,
deploymentMutation: false,
},
};
const selectionDigest = sha256(JSON.stringify(unsigned));
const filePath = path.join(
options.directory,
`synthetic-local-release-selection-${selectionDigest.slice(7)}.json`,
);
fs.writeFileSync(
filePath,
`${JSON.stringify({ ...unsigned, selectionDigest })}\n`,
{ encoding: 'utf8', mode: 0o600, flag: 'wx' },
);
return Object.freeze({
path: filePath,
expectedSelectionDigest: selectionDigest,
});
}
module.exports = { writeSyntheticLocalReleaseSelection };
@@ -4,6 +4,9 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const {
writeSyntheticLocalReleaseSelection,
} = require('./lib/ql3-local-release-selection-test-fixture.cjs');
const IMAGE_PATTERN =
/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}@sha256:[0-9a-f]{64}$/;
@@ -74,6 +77,12 @@ async function main() {
const deploymentRoot = path.join(temporaryRoot, 'deployment');
const commandPath = path.join(temporaryRoot, 'preflight.json');
const uid = process.getuid();
const releaseSelection = writeSyntheticLocalReleaseSelection({
directory: temporaryRoot,
image: input.image,
allowRootService: uid === 0,
sourceRevision: process.env.GITHUB_SHA,
});
try {
const setup = await prepareLocalDeployment({
@@ -86,7 +95,7 @@ async function main() {
busyTimeoutMs: 100,
service: {
kind: 'compose',
image: input.image,
releaseSelection,
allowRootService: uid === 0,
},
},
@@ -7,6 +7,9 @@ const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const {
writeSyntheticLocalReleaseSelection,
} = require('./lib/ql3-local-release-selection-test-fixture.cjs');
const IMAGE_PATTERN =
/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}@sha256:[0-9a-f]{64}$/;
@@ -178,6 +181,12 @@ async function main() {
const deploymentRoot = path.join(temporaryRoot, 'deployment');
const commandPath = path.join(temporaryRoot, 'rollout.json');
const uid = process.getuid();
const releaseSelection = writeSyntheticLocalReleaseSelection({
directory: temporaryRoot,
image: input.image,
allowRootService: uid === 0,
sourceRevision: process.env.GITHUB_SHA,
});
let composeResourcesPresent = false;
try {
@@ -191,7 +200,7 @@ async function main() {
busyTimeoutMs: 100,
service: {
kind: 'compose',
image: input.image,
releaseSelection,
allowRootService: uid === 0,
},
},
@@ -275,7 +284,7 @@ async function main() {
},
request: {
expectedGeneration: 1,
image: input.image,
releaseSelection,
mutationId: '019f8680-143d-4000-8000-000000000205',
changedAtMs: 1_785_254_600_004,
},
@@ -392,7 +401,7 @@ async function main() {
},
request: {
expectedGeneration: 2,
image: input.image,
releaseSelection,
mutationId: '019f8680-143d-4000-8000-000000000208',
changedAtMs: 1_785_254_600_007,
},
@@ -711,7 +720,7 @@ async function main() {
},
request: {
expectedGeneration: evidenceGeneration,
image: input.image,
releaseSelection,
mutationId: transaction.revisionId,
changedAtMs: 1_785_254_600_012 + index * 3,
},
+19 -27
View File
@@ -6,6 +6,9 @@ const os = require('node:os');
const path = require('node:path');
const { spawn, spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const {
writeSyntheticLocalReleaseSelection,
} = require('./lib/ql3-local-release-selection-test-fixture.cjs');
const MAX_OUTPUT_BYTES = 64 * 1024;
const ACTIVE_TIMEOUT_MS = 45_000;
@@ -15,11 +18,7 @@ function fail(message) {
}
function imageArgument(argv) {
if (
argv.length < 1 ||
argv.length > 2 ||
!argv[0].startsWith('--image=')
) {
if (argv.length < 1 || argv.length > 2 || !argv[0].startsWith('--image=')) {
fail('usage: --image=immutable-local-image [--profile=edge|standalone]');
}
const image = argv[0].slice('--image='.length);
@@ -104,13 +103,7 @@ function parseLines(buffer, events) {
return remaining;
}
async function runApplication(
image,
deploymentRoot,
uid,
gid,
profile,
) {
async function runApplication(image, deploymentRoot, uid, gid, profile) {
const containerName = `ql3-local-image-${process.pid}-${crypto
.randomUUID()
.slice(0, 8)}`;
@@ -173,10 +166,7 @@ async function runApplication(
return;
}
stdout = parseLines(stdout, events);
if (
!stopped &&
events.some(({ event }) => event === 'active')
) {
if (!stopped && events.some(({ event }) => event === 'active')) {
stopped = true;
runDocker(['stop', '--time', '30', containerName]);
}
@@ -254,20 +244,22 @@ async function main() {
const profile = profileArgument(process.argv.slice(2));
const imageIdentity = inspectImage(image);
const root = path.resolve(__dirname, '..');
const {
prepareLocalDeployment,
} = require(path.join(
const { prepareLocalDeployment } = require(path.join(
root,
'packages/ql3-local-owner-cli/dist/deployment/localDeployment.js',
));
const temporaryRoot = fs.realpathSync(
fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-local-image-live-'),
),
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-local-image-live-')),
);
fs.chmodSync(temporaryRoot, 0o700);
const deploymentRoot = path.join(temporaryRoot, 'deployment');
const uid = process.getuid();
const gid = process.getgid();
const releaseSelection = writeSyntheticLocalReleaseSelection({
directory: temporaryRoot,
image: `ghcr.io/example/qinglong3-local-application@${imageIdentity.id}`,
allowRootService: uid === 0,
});
try {
const setup = await prepareLocalDeployment({
@@ -280,7 +272,7 @@ async function main() {
busyTimeoutMs: 100,
service: {
kind: 'compose',
image: `local/qinglong3@${imageIdentity.id}`,
releaseSelection,
allowRootService: uid === 0,
},
},
@@ -309,8 +301,9 @@ async function main() {
);
let integrity;
try {
integrity = database.prepare('PRAGMA integrity_check').get()
.integrity_check;
integrity = database
.prepare('PRAGMA integrity_check')
.get().integrity_check;
} finally {
database.close();
}
@@ -323,8 +316,7 @@ async function main() {
architecture: imageIdentity.architecture,
user: imageIdentity.user,
profile,
memoryBytes:
(profile === 'edge' ? 128 : 256) * 1024 * 1024,
memoryBytes: (profile === 'edge' ? 128 : 256) * 1024 * 1024,
pids: profile === 'edge' ? 64 : 256,
network: 'none',
readOnlyRoot: true,
+14 -3
View File
@@ -13,6 +13,9 @@ const {
mountForPath,
parseMountTable,
} = require('./ql3-physical-edge-evidence.cjs');
const {
writeSyntheticLocalReleaseSelection,
} = require('./lib/ql3-local-release-selection-test-fixture.cjs');
const MIB = 1024 * 1024;
const MAX_INPUT_BYTES = 256 * 1024;
@@ -792,7 +795,15 @@ async function preparePhase(options, manifest) {
);
}
const timestamp = Date.now();
const image = `physical.invalid/qinglong3-local@sha256:${'a'.repeat(64)}`;
const image = `ghcr.io/example/qinglong3-local-application@sha256:${'a'.repeat(
64,
)}`;
fs.mkdirSync(deploymentRoot, { mode: 0o700 });
const releaseSelection = writeSyntheticLocalReleaseSelection({
directory: deploymentRoot,
image,
allowRootService: uid === 0,
});
await products.deployment.prepareLocalDeployment({
schemaVersion: 1,
operation: 'local.deployment.prepare',
@@ -803,7 +814,7 @@ async function preparePhase(options, manifest) {
busyTimeoutMs: 100,
service: {
kind: 'compose',
image,
releaseSelection,
allowRootService: uid === 0,
},
},
@@ -829,7 +840,7 @@ async function preparePhase(options, manifest) {
},
request: {
expectedGeneration: generation - 1,
image,
releaseSelection,
mutationId: crypto.randomUUID(),
changedAtMs: timestamp + generation,
},