feat(ql3): close public local release pair

This commit is contained in:
whyour
2026-08-27 14:51:33 +08:00
parent 238df17fdf
commit 78c261b556
31 changed files with 558 additions and 91 deletions
+17 -3
View File
@@ -23,7 +23,14 @@ 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', 'worker'],
images: [
'control',
'control-ai',
'admin',
'local',
'local-operator',
'worker',
],
nativeArchitectures: ['amd64', 'arm64'],
runtimeInventory: true,
clusterAdminProductFacade: true,
@@ -85,7 +92,14 @@ test('accepts the reviewed native CI and digest release contracts', () => {
immutableArtifactRetentionDays: 1,
attestedToPublishedDigest: true,
},
images: ['control', 'control-ai', 'admin', 'worker', 'local'],
images: [
'control',
'control-ai',
'admin',
'worker',
'local',
'local-operator',
],
platforms: ['linux/amd64', 'linux/arm64'],
keylessSignature: true,
buildkitAttestations: ['sbom', 'provenance'],
@@ -123,7 +137,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
},
durableCatalog: {
repository: 'qinglong3-release-catalog',
artifactType: 'application/vnd.qinglong.release-set.v3+json',
artifactType: 'application/vnd.qinglong.release-set.v4+json',
planSchema: 'qinglong/release-catalog-plan@v2',
receiptSchema: 'qinglong/release-catalog-receipt@v2',
tagInventoryDecisionSchema:
+35 -2
View File
@@ -29,6 +29,7 @@ function createFixture(t, options = {}) {
const isControl = image === 'control' || image === 'control-ai';
const isControlAi = image === 'control-ai';
const isLocal = image === 'local';
const isLocalOperator = image === 'local-operator';
const isWorker = image === 'worker';
const layoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-oci-layout-'));
t.after(() => fs.rmSync(layoutRoot, { recursive: true, force: true }));
@@ -103,7 +104,7 @@ function createFixture(t, options = {}) {
? '0:0'
: isWorker
? '65532:65532'
: isLocal
: isLocal || isLocalOperator
? '65532:65532'
: '10001:10001',
...(isControl ? { ExposedPorts: { '5800/tcp': {} } } : {}),
@@ -119,6 +120,8 @@ function createFixture(t, options = {}) {
? '/opt/qinglong/node_modules/@qinglong/worker-runtime/dist/process/workerProcessCli.js'
: isLocal
? '/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js'
: isLocalOperator
? '/opt/qinglong/node_modules/@qinglong/local-owner-cli/dist/product-cli/cli.js'
: isControl
? isControlAi
? '/opt/qinglong/node_modules/@qinglong/cluster-control/dist/aiCli.js'
@@ -144,8 +147,17 @@ function createFixture(t, options = {}) {
'io.qinglong.profile': 'edge,standalone',
}
: {}),
...(isLocalOperator
? {
'io.qinglong.authority': 'local-owner-management',
'io.qinglong.lifecycle': 'short-lived',
'io.qinglong.network': 'none-by-default',
}
: {}),
'org.opencontainers.image.description': isLocal
? 'QingLong 3.0 AI-excluded Edge and Standalone runtime'
: isLocalOperator
? 'QingLong 3.0 short-lived Local management authority'
: isWorker
? 'QingLong 3.0 headless Remote Worker runtime'
: isControl
@@ -159,6 +171,8 @@ function createFixture(t, options = {}) {
'https://github.com/whyour/qinglong',
'org.opencontainers.image.title': isLocal
? 'QingLong 3.0 Local Application'
: isLocalOperator
? 'QingLong 3.0 Local Operator'
: isWorker
? 'QingLong 3.0 Worker'
: isControl
@@ -166,7 +180,11 @@ function createFixture(t, options = {}) {
? 'QingLong 3.0 Cluster Control AI'
: 'QingLong 3.0 Cluster Control'
: 'QingLong 3.0 Cluster Admin',
...(isLocal || isWorker || isControl || image === 'admin'
...(isLocal ||
isLocalOperator ||
isWorker ||
isControl ||
image === 'admin'
? {
'org.opencontainers.image.version': version,
}
@@ -379,6 +397,21 @@ test('accepts the AI-excluded local image and attestation closure', (t) => {
);
});
test('accepts the short-lived Local operator image and attestation closure', (t) => {
const report = auditClusterOciLayout({
root,
layoutRoot: createFixture(t, { image: 'local-operator' }),
expectedRevision: revision,
image: 'local-operator',
});
assert.equal(report.image, 'local-operator');
assert.equal(report.maximumPlatformBytes, 128 * 1024 * 1024);
assert.deepEqual(
report.platforms.map((entry) => entry.spdxApplicationPackages),
[9, 9],
);
});
test('accepts the headless Worker image and attestation closure', (t) => {
const report = auditClusterOciLayout({
root,
+24 -3
View File
@@ -93,6 +93,12 @@ function releaseSet(scope) {
...identity,
releaseScope: scope,
image: entry.image,
localRoleVerification:
entry.image === 'local'
? 'application_rollout_verified'
: entry.image === 'local-operator'
? 'operator_entrypoint_verified'
: 'not_applicable',
digest: `sha256:${String(index + 1).repeat(64)}`,
}),
);
@@ -429,7 +435,7 @@ test('selects catalog-backed live artifacts only for one complete environment',
assert.equal(artifacts.report.requiredImages.length, 4);
});
test('selects one immutable Local Compose image without adding device work', () => {
test('selects immutable Local runtime and short-lived operator images without adding steady-state device work', () => {
for (const scope of ['local', 'all']) {
const set = releaseSet(scope);
const selection = createLocalSelection(
@@ -437,7 +443,7 @@ test('selects one immutable Local Compose image without adding device work', ()
options(set, { allowRootService: false }),
);
assert.equal(selection.deploymentFamily, 'local');
assert.equal(selection.schema, 'qinglong/local-compose-release-image@v2');
assert.equal(selection.schema, 'qinglong/local-compose-release-image@v3');
assert.equal(selection.releaseSetDigest, set.releaseSetDigest);
assert.equal(
selection.catalog.releaseSetDigest,
@@ -451,6 +457,9 @@ test('selects one immutable Local Compose image without adding device work', ()
assert.equal(selection.service.kind, 'compose');
assert.equal(selection.service.image, references(set).local);
assert.equal(selection.service.allowRootService, false);
assert.equal(selection.operator.kind, 'short-lived');
assert.equal(selection.operator.image, references(set)['local-operator']);
assert.equal(selection.operator.network, 'none-by-default');
assert.equal(selection.verification.networkAccess, false);
assert.equal(selection.verification.deploymentMutation, false);
assert.equal(
@@ -500,6 +509,18 @@ test('Local selection rejects cluster scope, implicit root policy and drift', ()
),
/differs from the verified release set/,
);
const operatorDrifted = JSON.parse(JSON.stringify(selection));
operatorDrifted.operator.image =
'ghcr.io/example/qinglong3-local-operator:latest';
assert.throws(
() =>
auditLocalSelection(
operatorDrifted,
localSet,
options(localSet, { allowRootService: true }),
),
/differs from the verified release set/,
);
});
test('deployment materialization rejects missing or mismatched catalog authority', () => {
@@ -947,7 +968,7 @@ test('deployment-lock CLI cannot regress to a loose release-set input', () => {
assert.match(source, /'consumption-bundle'/u);
assert.match(source, /'source-repository'/u);
assert.doesNotMatch(source, /['"]release-set['"]/u);
assert.match(source, /qinglong\/local-compose-release-image@v2/u);
assert.match(source, /qinglong\/local-compose-release-image@v3/u);
assert.match(source, /qinglong\/kubernetes-deployment-lock@v2/u);
});
@@ -56,6 +56,7 @@ test('accepts the empty fail-closed production exception policy', () => {
control: 0,
'control-ai': 0,
local: 0,
'local-operator': 0,
worker: 0,
},
});
@@ -31,7 +31,7 @@ test('freezes an independent low-resource local release family', () => {
});
assert.deepEqual(
contract.images.map((entry) => entry.image),
['local'],
['local', 'local-operator'],
);
assert.equal(contract.releasePlan.clusterEvidenceRequired, false);
assert.deepEqual(contract.deploymentFamilies.local.profiles, [
@@ -78,7 +78,7 @@ test('freezes an independent low-resource local release family', () => {
contractDigest: contract.contractDigest,
releaseScope: 'local',
workspacePackageCount: 18,
images: ['local'],
images: ['local', 'local-operator'],
clusterEvidenceRequired: false,
},
);
@@ -141,12 +141,16 @@ test('combines local and cluster families without weakening either gate', () =>
});
assert.deepEqual(
contract.images.map((entry) => entry.image),
['control', 'control-ai', 'admin', 'worker', 'local'],
['control', 'control-ai', 'admin', 'worker', 'local', 'local-operator'],
);
assert.equal(
contract.requiredGates.includes('edge-and-standalone-rollout'),
true,
);
assert.equal(
contract.requiredGates.includes('local-operator-entrypoint'),
true,
);
assert.equal(
contract.requiredGates.includes('cross-image-release-set'),
true,
@@ -46,7 +46,10 @@ function selectedImages(scope) {
['admin', 'qinglong3-cluster-admin'],
['worker', 'qinglong3-worker'],
];
const local = [['local', 'qinglong3-local-application']];
const local = [
['local', 'qinglong3-local-application'],
['local-operator', 'qinglong3-local-operator'],
];
return scope === 'local'
? local
: scope === 'cluster'
@@ -79,7 +82,7 @@ function releaseSet(scope) {
const names = images.map((entry) => entry.name);
const unsigned = {
schemaVersion: 1,
schema: 'qinglong/release-set@v3',
schema: 'qinglong/release-set@v4',
release,
candidate: {
schema: 'qinglong/release-candidate-contract@v1',
@@ -91,12 +94,16 @@ function releaseSet(scope) {
local: {
selected: ['local', 'all'].includes(scope),
profiles: ['edge', 'standalone'],
images: names.filter((name) => name === 'local'),
images: names.filter((name) =>
['local', 'local-operator'].includes(name),
),
},
cluster: {
selected: ['cluster', 'all'].includes(scope),
profiles: ['cluster', 'worker-edge', 'worker-node'],
images: names.filter((name) => name !== 'local'),
images: names.filter((name) =>
['control', 'control-ai', 'admin', 'worker'].includes(name),
),
},
},
evidenceReceipts: privateReleaseEvidenceReceipts(release),
@@ -392,8 +399,9 @@ test('supports the complete all-scope image family without runtime coupling', (t
'admin',
'worker',
'local',
'local-operator',
]);
assert.equal(report.releaseSet.imageCount, 5);
assert.equal(report.releaseSet.imageCount, 6);
assert.equal(report.claims.deploymentMutation, false);
});
@@ -67,6 +67,12 @@ function releaseSet(scope) {
...identity,
releaseScope: scope,
image: entry.image,
localRoleVerification:
entry.image === 'local'
? 'application_rollout_verified'
: entry.image === 'local-operator'
? 'operator_entrypoint_verified'
: 'not_applicable',
digest: `sha256:${String(index + 1).repeat(64)}`,
}),
);
@@ -71,6 +71,12 @@ function releaseSet(scope) {
...identity,
releaseScope: scope,
image: entry.image,
localRoleVerification:
entry.image === 'local'
? 'application_rollout_verified'
: entry.image === 'local-operator'
? 'operator_entrypoint_verified'
: 'not_applicable',
digest: `sha256:${String(index + 1).repeat(64)}`,
}),
);
+30 -4
View File
@@ -51,6 +51,12 @@ function recordsFor(releaseCandidate) {
...identity,
releaseScope: releaseCandidate.release.scope,
image: entry.image,
localRoleVerification:
entry.image === 'local'
? 'application_rollout_verified'
: entry.image === 'local-operator'
? 'operator_entrypoint_verified'
: 'not_applicable',
digest: `sha256:${String(index + 1).repeat(64)}`,
}),
);
@@ -72,7 +78,7 @@ function writeCanonical(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
}
test('aggregates the independent Local image into one immutable release set', () => {
test('aggregates both independent Local images into one immutable release set', () => {
const releaseCandidate = candidate('local');
const records = recordsFor(releaseCandidate);
const { validationClockMs: _unused, ...localIdentity } = identity;
@@ -86,7 +92,7 @@ test('aggregates the independent Local image into one immutable release set', ()
});
assert.deepEqual(
releaseSet.images.map((entry) => entry.name),
['local'],
['local', 'local-operator'],
);
assert.equal(releaseSet.deploymentFamilies.local.selected, true);
assert.equal(releaseSet.deploymentFamilies.cluster.selected, false);
@@ -140,8 +146,11 @@ test('closes cluster and all scopes over the exact candidate image order', () =>
...identity,
releaseScope: 'all',
});
assert.equal(allSet.images.length, 5);
assert.deepEqual(allSet.deploymentFamilies.local.images, ['local']);
assert.equal(allSet.images.length, 6);
assert.deepEqual(allSet.deploymentFamilies.local.images, [
'local',
'local-operator',
]);
});
test('requires exact private evidence receipts only for Cluster-capable scopes', () => {
@@ -303,6 +312,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
repositoryOwner: 'UPPERCASE',
releaseScope: 'local',
image: 'local',
localRoleVerification: 'application_rollout_verified',
digest: `sha256:${'1'.repeat(64)}`,
}),
/lowercase GitHub owner/,
@@ -315,6 +325,7 @@ test('rejects mutable identity, malformed owner and post-aggregate drift', () =>
...identity,
releaseScope: 'local',
image: 'local',
localRoleVerification: 'application_rollout_verified',
digest: 'latest',
}),
/exact SHA-256 digest/,
@@ -436,6 +447,7 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
'--mode=record-image',
...common,
'--image=local',
'--local-role-verification=application_rollout_verified',
`--digest=sha256:${'1'.repeat(64)}`,
`--output=${recordPath}`,
],
@@ -443,6 +455,20 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => {
output,
);
assert.equal(fs.statSync(recordPath).mode & 0o777, 0o600);
const operatorRecordPath = path.join(recordsDirectory, 'local-operator.json');
runCli(
[
'--mode=record-image',
...common,
'--image=local-operator',
'--local-role-verification=operator_entrypoint_verified',
`--digest=sha256:${'2'.repeat(64)}`,
`--output=${operatorRecordPath}`,
],
root,
output,
);
assert.equal(fs.statSync(operatorRecordPath).mode & 0o777, 0o600);
const setPath = path.join(directory, 'release-set.json');
runCli(
[
+29 -6
View File
@@ -32,7 +32,10 @@ function publicationPlan() {
const version = '3.0.0-alpha.0';
const sourceRevision = 'a'.repeat(40);
const repository = 'ghcr.io/qinglong-release/qinglong3-local-application';
const operatorRepository =
'ghcr.io/qinglong-release/qinglong3-local-operator';
const imageDigest = `sha256:${'1'.repeat(64)}`;
const operatorDigest = `sha256:${'9'.repeat(64)}`;
const manifestDigest = `sha256:${'2'.repeat(64)}`;
const unsigned = {
schemaVersion: 1,
@@ -90,6 +93,22 @@ function publicationPlan() {
},
],
},
{
name: 'local-operator',
registryRepository: operatorRepository,
immutableReference: `${operatorRepository}@${operatorDigest}`,
digest: operatorDigest,
tags: [
{
kind: 'version',
reference: `${operatorRepository}:${version}`,
},
{
kind: 'source',
reference: `${operatorRepository}:sha-${sourceRevision}`,
},
],
},
],
};
return Object.freeze({
@@ -174,10 +193,14 @@ test('finalizes every exact tag and audits the live terminal state', () => {
const plan = publicationPlan();
const registry = new FakeRegistry(plan);
const observation = finalizeReleaseTags(plan, registry);
assert.equal(registry.copyCalls().length, 2);
assert.equal(observation.tags.length, 2);
assert.equal(registry.copyCalls().length, 4);
assert.equal(observation.tags.length, 4);
assert.equal(
observation.tags.every((tag) => tag.digest === plan.images[0].digest),
observation.tags.every(
(tag) =>
tag.digest ===
plan.images.find((image) => image.name === tag.image)?.digest,
),
true,
);
const copiesBeforeAudit = registry.copyCalls().length;
@@ -185,7 +208,7 @@ test('finalizes every exact tag and audits the live terminal state', () => {
schemaVersion: 1,
planDigest: plan.planDigest,
observationDigest: observation.observationDigest,
tagCount: 2,
tagCount: 4,
allTagsExactDigest: true,
registryMutation: false,
compatible: true,
@@ -228,8 +251,8 @@ test('recovers copy response loss by reusing exact tags and only filling absence
);
assert.equal(registry.copyCalls().length, 1);
const observation = finalizeReleaseTags(plan, registry);
assert.equal(registry.copyCalls().length, 2);
assert.equal(observation.tags.length, 2);
assert.equal(registry.copyCalls().length, 4);
assert.equal(observation.tags.length, 4);
assert.equal(
registry.resolveDigest(plan.images[0].tags[0].reference),
plan.images[0].digest,