mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): govern release version transitions
This commit is contained in:
@@ -12,9 +12,13 @@ const {
|
||||
reviewedOperatorManifest,
|
||||
verifyImageIds,
|
||||
} = require('../../scripts/ql3-cloudnativepg-live-contract.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const INDEX = `sha256:${'a'.repeat(64)}`;
|
||||
const PLATFORM = `sha256:${'b'.repeat(64)}`;
|
||||
const VERSION = readReleaseIdentity(path.resolve(__dirname, '../..')).version;
|
||||
|
||||
function pods(...imageIds) {
|
||||
return imageIds.map((imageID) => ({
|
||||
@@ -23,10 +27,7 @@ function pods(...imageIds) {
|
||||
}
|
||||
|
||||
test('extracts only one exact digest-pinned image reference', () => {
|
||||
assert.equal(
|
||||
imageDigest(`registry.example/operand:18.4@${INDEX}`),
|
||||
INDEX,
|
||||
);
|
||||
assert.equal(imageDigest(`registry.example/operand:18.4@${INDEX}`), INDEX);
|
||||
assert.throws(() => imageDigest('registry.example/operand:18.4'));
|
||||
assert.throws(() => imageDigest(`registry.example/operand@${INDEX}:tag`));
|
||||
});
|
||||
@@ -41,16 +42,16 @@ test('derives a normal tagged preload reference from a reviewed image', () => {
|
||||
});
|
||||
|
||||
test('replaces exactly one fail-closed application image only in live rendering', () => {
|
||||
const placeholder = `registry.example.com/qinglong/qinglong3-cluster-control@sha256:${'0'.repeat(64)}`;
|
||||
const placeholder = `registry.example.com/qinglong/qinglong3-cluster-control@sha256:${'0'.repeat(
|
||||
64,
|
||||
)}`;
|
||||
const rendered = `kind: Deployment\nspec:\n image: ${placeholder}\n`;
|
||||
assert.equal(
|
||||
localApplicationManifest(rendered),
|
||||
'kind: Deployment\nspec:\n image: registry.example.com/qinglong/qinglong3-cluster-control:3.0.0-alpha.0\n',
|
||||
`kind: Deployment\nspec:\n image: registry.example.com/qinglong/qinglong3-cluster-control:${VERSION}\n`,
|
||||
);
|
||||
assert.throws(() => localApplicationManifest('kind: Deployment\n'));
|
||||
assert.throws(() =>
|
||||
localApplicationManifest(`${rendered}---\n${rendered}`),
|
||||
);
|
||||
assert.throws(() => localApplicationManifest(`${rendered}---\n${rendered}`));
|
||||
});
|
||||
|
||||
test('accepts uniform runtime reporting of the reviewed index or platform digest', () => {
|
||||
@@ -74,7 +75,8 @@ test('accepts uniform runtime reporting of the reviewed index or platform digest
|
||||
|
||||
test('rejects tags, unknown digests, missing status and widened reviewed sets', () => {
|
||||
for (const invoke of [
|
||||
() => verifyImageIds(pods('registry.example/operand:18.4'), [INDEX], 'operand'),
|
||||
() =>
|
||||
verifyImageIds(pods('registry.example/operand:18.4'), [INDEX], 'operand'),
|
||||
() =>
|
||||
verifyImageIds(
|
||||
pods(`registry.example/operand@sha256:${'c'.repeat(64)}`),
|
||||
@@ -82,7 +84,12 @@ test('rejects tags, unknown digests, missing status and widened reviewed sets',
|
||||
'operand',
|
||||
),
|
||||
() => verifyImageIds([{ status: {} }], [INDEX], 'operand'),
|
||||
() => verifyImageIds(pods(`registry.example/operand@${INDEX}`), ['*'], 'operand'),
|
||||
() =>
|
||||
verifyImageIds(
|
||||
pods(`registry.example/operand@${INDEX}`),
|
||||
['*'],
|
||||
'operand',
|
||||
),
|
||||
]) {
|
||||
assert.throws(invoke);
|
||||
}
|
||||
@@ -90,7 +97,10 @@ test('rejects tags, unknown digests, missing status and widened reviewed sets',
|
||||
|
||||
test('creates the namespaced control identity before the migration Job', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../scripts/ql3-cloudnativepg-live-contract.cjs',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const namespace = source.indexOf(
|
||||
@@ -105,10 +115,7 @@ test('creates the namespaced control identity before the migration Job', () => {
|
||||
assert.ok(namespace >= 0);
|
||||
assert.ok(serviceAccount > namespace);
|
||||
assert.ok(migration > serviceAccount);
|
||||
assert.match(
|
||||
source.slice(namespace, serviceAccount),
|
||||
/kubectl\(\[/,
|
||||
);
|
||||
assert.match(source.slice(namespace, serviceAccount), /kubectl\(\[/);
|
||||
assert.match(
|
||||
source.slice(namespace, migration),
|
||||
/'-n',\s*NAMESPACE,\s*'apply',\s*'-f',\s*'deploy\/kubernetes\/ql3-cluster\/base\/service-account\.yaml'/,
|
||||
@@ -117,7 +124,10 @@ test('creates the namespaced control identity before the migration Job', () => {
|
||||
|
||||
test('provisions the fail-closed worker ingress identity and derives all role evidence from one set', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../scripts/ql3-cloudnativepg-live-contract.cjs',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /const roleList = ROLE_NAMES\.map/);
|
||||
@@ -144,7 +154,10 @@ test('provisions the fail-closed worker ingress identity and derives all role ev
|
||||
|
||||
test('preloads both lock-owned images before applying the operator manifest', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../scripts/ql3-cloudnativepg-live-contract.cjs',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const preload = source.indexOf(
|
||||
@@ -158,14 +171,8 @@ test('preloads both lock-owned images before applying the operator manifest', ()
|
||||
const contract = source.slice(preload, manifest);
|
||||
assert.match(contract, /docker\(\['pull', reviewedImage\]\)/);
|
||||
assert.match(contract, /imageDigest\(reviewedImage\)/);
|
||||
assert.match(
|
||||
contract,
|
||||
/const preloadTag = imageTag\(reviewedImage\)/,
|
||||
);
|
||||
assert.match(
|
||||
contract,
|
||||
/docker\(\['tag', reviewedImage, preloadTag\]\)/,
|
||||
);
|
||||
assert.match(contract, /const preloadTag = imageTag\(reviewedImage\)/);
|
||||
assert.match(contract, /docker\(\['tag', reviewedImage, preloadTag\]\)/);
|
||||
assert.match(
|
||||
contract,
|
||||
/kind\(\['load', 'docker-image', preloadTag, '--name', clusterName\]\)/,
|
||||
@@ -195,12 +202,18 @@ test('rejects a canonical but checksum-unreviewed operator manifest', () => {
|
||||
|
||||
test('bounds remote manifest retries and removes disposable temporary state', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../scripts/ql3-cloudnativepg-live-contract.cjs'),
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../scripts/ql3-cloudnativepg-live-contract.cjs',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /'--http1\.1'/);
|
||||
assert.match(source, /'--retry-max-time',\s*'300'/);
|
||||
assert.match(source, /reviewedOperatorManifest\(downloadedOperatorManifest\)/);
|
||||
assert.match(
|
||||
source,
|
||||
/reviewedOperatorManifest\(downloadedOperatorManifest\)/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/fs\.rmSync\(tempDirectory, \{ recursive: true, force: true \}\)/,
|
||||
|
||||
@@ -5,8 +5,12 @@ const { test } = require('node:test');
|
||||
const {
|
||||
auditClusterDeployment,
|
||||
} = require('../../scripts/ql3-cluster-deployment-audit.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const VERSION = readReleaseIdentity(ROOT).version;
|
||||
|
||||
function intercept(relativePath, transform) {
|
||||
const target = path.join(ROOT, relativePath);
|
||||
@@ -233,8 +237,8 @@ test('keeps Cluster AI optional with projected authority and an independent dige
|
||||
'deploy/kubernetes/ql3-cluster/base/deployment.yaml',
|
||||
(source) =>
|
||||
source.replace(
|
||||
'image: qinglong3-cluster-control:3.0.0-alpha.0',
|
||||
'image: qinglong3-cluster-control-ai:3.0.0-alpha.0',
|
||||
`image: qinglong3-cluster-control:${VERSION}`,
|
||||
`image: qinglong3-cluster-control-ai:${VERSION}`,
|
||||
),
|
||||
),
|
||||
});
|
||||
@@ -263,8 +267,7 @@ test('keeps Cluster AI optional with projected authority and an independent dige
|
||||
root: ROOT,
|
||||
readFile: intercept(
|
||||
'deploy/kubernetes/ql3-cluster/overlays/cluster-ai-example/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
),
|
||||
});
|
||||
assert.equal(tagBased.compatible, false);
|
||||
@@ -600,8 +603,7 @@ test('rejects widened lifecycle, authority or public inputs in the Approval clie
|
||||
],
|
||||
[
|
||||
'deploy/kubernetes/ql3-cluster/operations/approval-management-client/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
'QL3_CLUSTER_APPROVAL_MANAGEMENT_CLIENT_OPT_IN',
|
||||
],
|
||||
[
|
||||
@@ -654,8 +656,7 @@ test('rejects widened lifecycle, authority or public inputs in the automation cl
|
||||
],
|
||||
[
|
||||
'deploy/kubernetes/ql3-cluster/operations/automation-management-client/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
'QL3_CLUSTER_AUTOMATION_MANAGEMENT_CLIENT_RELEASE_DIGEST_PIN',
|
||||
],
|
||||
[
|
||||
@@ -726,8 +727,7 @@ test('rejects widened authority or lifecycle in the Worker management client', (
|
||||
],
|
||||
[
|
||||
'deploy/kubernetes/ql3-cluster/operations/worker-credential-management-client/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
'QL3_CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_CLIENT_RELEASE_DIGEST_PIN',
|
||||
],
|
||||
[
|
||||
@@ -840,8 +840,7 @@ test('rejects recurring, default-enabled or public-egress Worker execution', ()
|
||||
],
|
||||
[
|
||||
'deploy/kubernetes/ql3-cluster/operations/worker-credential-executor/cloudnative-pg/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
'QL3_CLUSTER_WORKER_CREDENTIAL_EXECUTOR_CLOUDNATIVE_PG_AUTHORITY',
|
||||
],
|
||||
]) {
|
||||
@@ -917,8 +916,7 @@ test('rejects default or tag-based Worker management rollout', () => {
|
||||
root: ROOT,
|
||||
readFile: intercept(
|
||||
'deploy/kubernetes/ql3-cluster/operations/worker-credential-management/cloudnative-pg/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
),
|
||||
});
|
||||
assert.equal(tagBased.compatible, false);
|
||||
@@ -1114,8 +1112,7 @@ test('rejects default enablement or tag-based Package management rollout', () =>
|
||||
root: ROOT,
|
||||
readFile: intercept(
|
||||
'deploy/kubernetes/ql3-cluster/operations/plugin-package-management/cloudnative-pg/kustomization.yaml',
|
||||
(source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
(source) => source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
),
|
||||
});
|
||||
assert.equal(tagBased.compatible, false);
|
||||
@@ -1424,7 +1421,7 @@ test('rejects tag-based production overlays for either cluster image', () => {
|
||||
const report = auditClusterDeployment({
|
||||
root: ROOT,
|
||||
readFile: intercept(relativePath, (source) =>
|
||||
source.replace(/digest: sha256:0{64}/, 'newTag: 3.0.0-alpha.0'),
|
||||
source.replace(/digest: sha256:0{64}/, `newTag: ${VERSION}`),
|
||||
),
|
||||
});
|
||||
assert.equal(report.compatible, false);
|
||||
|
||||
@@ -30,6 +30,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
clusterAdminOperatorContext: true,
|
||||
clusterAdminContextPreflight: true,
|
||||
clusterAdminContextReadiness: true,
|
||||
releaseVersionAudit: true,
|
||||
ociAttestations: true,
|
||||
osVulnerabilityScan: {
|
||||
scanner: 'trivy@0.70.0',
|
||||
@@ -105,6 +106,17 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects removal of the source-derived release version audit', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'pnpm audit:release-version:ql3',
|
||||
'echo release-version-audit-removed',
|
||||
);
|
||||
assert.throws(
|
||||
() => auditClusterImageCiWorkflow(mutated),
|
||||
/source-derived release version identity/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects removal of the native arm64 image gate', () => {
|
||||
const mutated = ciSource.replace(
|
||||
'runner: ubuntu-24.04-arm\n node_arch: arm64\n image_arch: arm64\n image: control',
|
||||
|
||||
@@ -10,8 +10,12 @@ const {
|
||||
componentRef,
|
||||
createClusterImageSbom,
|
||||
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
@@ -23,7 +27,7 @@ test('generates the exact reviewed cluster image runtime closure', () => {
|
||||
|
||||
assert.deepEqual(report, {
|
||||
image: 'control',
|
||||
root: 'pkg:npm/%40qinglong/cluster-control-image-dependencies@3.0.0-alpha.0',
|
||||
root: `pkg:npm/%40qinglong/cluster-control-image-dependencies@${version}`,
|
||||
components: 46,
|
||||
externalComponents: 43,
|
||||
internalComponents: 3,
|
||||
@@ -49,7 +53,7 @@ test('generates the optional Cluster AI image runtime closure', () => {
|
||||
|
||||
assert.deepEqual(report, {
|
||||
image: 'control-ai',
|
||||
root: 'pkg:npm/%40qinglong/cluster-control-image-dependencies@3.0.0-alpha.0',
|
||||
root: `pkg:npm/%40qinglong/cluster-control-image-dependencies@${version}`,
|
||||
components: 47,
|
||||
externalComponents: 43,
|
||||
internalComponents: 4,
|
||||
@@ -71,7 +75,7 @@ test('generates the independent reviewed cluster-admin image closure', () => {
|
||||
|
||||
assert.deepEqual(report, {
|
||||
image: 'admin',
|
||||
root: 'pkg:npm/%40qinglong/cluster-admin-image-dependencies@3.0.0-alpha.0',
|
||||
root: `pkg:npm/%40qinglong/cluster-admin-image-dependencies@${version}`,
|
||||
components: 91,
|
||||
externalComponents: 87,
|
||||
internalComponents: 4,
|
||||
@@ -107,7 +111,7 @@ test('generates the AI-excluded local application image closure', () => {
|
||||
|
||||
assert.deepEqual(report, {
|
||||
image: 'local',
|
||||
root: 'pkg:npm/%40qinglong/local-application-image@3.0.0-alpha.0',
|
||||
root: `pkg:npm/%40qinglong/local-application-image@${version}`,
|
||||
components: 10,
|
||||
externalComponents: 2,
|
||||
internalComponents: 8,
|
||||
@@ -133,7 +137,7 @@ test('generates the headless Worker image runtime closure', () => {
|
||||
|
||||
assert.deepEqual(report, {
|
||||
image: 'worker',
|
||||
root: 'pkg:npm/%40qinglong/worker-image-dependencies@3.0.0-alpha.0',
|
||||
root: `pkg:npm/%40qinglong/worker-image-dependencies@${version}`,
|
||||
components: 27,
|
||||
externalComponents: 24,
|
||||
internalComponents: 3,
|
||||
@@ -182,7 +186,7 @@ test('rejects widened metadata and root component drift', () => {
|
||||
|
||||
test('rejects a missing internal dependency edge', () => {
|
||||
const document = createClusterImageSbom({ root });
|
||||
const controlRef = componentRef('@qinglong/cluster-control', '3.0.0-alpha.0');
|
||||
const controlRef = componentRef('@qinglong/cluster-control', version);
|
||||
const edge = document.dependencies.find((entry) => entry.ref === controlRef);
|
||||
edge.dependsOn = edge.dependsOn.slice(1);
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@ const {
|
||||
const {
|
||||
createClusterImageSbom,
|
||||
} = require('../../scripts/ql3-cluster-image-sbom.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
const revision = 'fixture-revision';
|
||||
|
||||
function createFixture(t, options = {}) {
|
||||
@@ -164,7 +168,7 @@ function createFixture(t, options = {}) {
|
||||
: 'QingLong 3.0 Cluster Admin',
|
||||
...(isLocal || isWorker || isControl || image === 'admin'
|
||||
? {
|
||||
'org.opencontainers.image.version': '3.0.0-alpha.0',
|
||||
'org.opencontainers.image.version': version,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
@@ -20,6 +20,11 @@ const {
|
||||
const {
|
||||
canonicalDigest,
|
||||
} = require('../../scripts/ql3-physical-edge-evidence.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const version = readReleaseIdentity(path.resolve(__dirname, '../..')).version;
|
||||
|
||||
const packages = [
|
||||
'@qinglong/local-admin',
|
||||
@@ -55,7 +60,7 @@ function artifactFixture(t) {
|
||||
packageName === '@qinglong/local-application'
|
||||
? {
|
||||
name: packageName,
|
||||
version: '3.0.0-alpha.0',
|
||||
version,
|
||||
engines: { node: '>=24.18.0 <25' },
|
||||
bin: { 'ql3-local-application': 'dist/cli.js' },
|
||||
}
|
||||
|
||||
@@ -11,12 +11,16 @@ const {
|
||||
parseArguments,
|
||||
runCli,
|
||||
} = require('../../scripts/ql3-release-candidate-contract.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const version = readReleaseIdentity(root).version;
|
||||
const identity = Object.freeze({
|
||||
version: '3.0.0-alpha.0',
|
||||
version,
|
||||
sourceRevision: 'a'.repeat(40),
|
||||
sourceRef: 'refs/tags/v3.0.0-alpha.0',
|
||||
sourceRef: `refs/tags/v${version}`,
|
||||
});
|
||||
|
||||
test('freezes an independent low-resource local release family', () => {
|
||||
@@ -35,6 +39,14 @@ test('freezes an independent low-resource local release family', () => {
|
||||
'standalone',
|
||||
]);
|
||||
assert.equal(contract.workspace.packageCount, 18);
|
||||
assert.equal(
|
||||
contract.compatibility.releaseIdentitySchema,
|
||||
'qinglong/release-identity@v1',
|
||||
);
|
||||
assert.match(
|
||||
contract.compatibility.releaseIdentityDigest,
|
||||
/^sha256:[a-f0-9]{64}$/u,
|
||||
);
|
||||
assert.match(contract.contractDigest, /^sha256:[a-f0-9]{64}$/u);
|
||||
assert.deepEqual(
|
||||
auditReleaseCandidateContract(contract, {
|
||||
@@ -126,6 +138,18 @@ test('rejects tag, version and source identity drift', () => {
|
||||
}),
|
||||
/Git SHA-1/,
|
||||
);
|
||||
const nextVersion = `${version.slice(0, version.lastIndexOf('.') + 1)}1`;
|
||||
assert.throws(
|
||||
() =>
|
||||
createReleaseCandidateContract({
|
||||
root,
|
||||
...identity,
|
||||
version: nextVersion,
|
||||
sourceRef: `refs/tags/v${nextVersion}`,
|
||||
releaseScope: 'local',
|
||||
}),
|
||||
/repository release identity/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a source-derived report mutated after creation', () => {
|
||||
@@ -153,9 +177,9 @@ test('writes once and independently audits the exact report through the CLI', (t
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const report = path.join(directory, 'contract.json');
|
||||
const common = [
|
||||
'--version=3.0.0-alpha.0',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
'--source-ref=refs/tags/v3.0.0-alpha.0',
|
||||
`--source-ref=refs/tags/v${version}`,
|
||||
'--release-scope=local',
|
||||
];
|
||||
const output = { write() {} };
|
||||
@@ -168,20 +192,16 @@ test('writes once and independently audits the exact report through the CLI', (t
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
['--mode=create', ...common, `--output=${report}`],
|
||||
root,
|
||||
output,
|
||||
),
|
||||
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',
|
||||
`--version=${version}`,
|
||||
`--source-revision=${identity.sourceRevision}`,
|
||||
'--source-ref=refs/tags/v3.0.0-alpha.0',
|
||||
`--source-ref=refs/tags/v${version}`,
|
||||
'--release-scope=local',
|
||||
];
|
||||
assert.equal(
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
'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 semver = require('semver');
|
||||
const {
|
||||
CONTAINER_ROOTS,
|
||||
DEPLOYMENT_FILES,
|
||||
DEPLOYMENT_ROOTS,
|
||||
applyVersionTransitionPlan,
|
||||
auditReleaseVersionContract,
|
||||
createVersionTransitionPlan,
|
||||
parseArguments,
|
||||
runCli,
|
||||
validatePlan,
|
||||
} = require('../../scripts/ql3-version-transition.cjs');
|
||||
const {
|
||||
readReleaseIdentity,
|
||||
} = require('../../scripts/lib/ql3-release-identity.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '../..');
|
||||
const SOURCE_VERSION = readReleaseIdentity(root).version;
|
||||
const TARGET_VERSION = semver.inc(SOURCE_VERSION, 'prerelease');
|
||||
const LEGACY_VERSION = JSON.parse(
|
||||
fs.readFileSync(path.join(root, 'package.json')),
|
||||
).version;
|
||||
|
||||
function copyFile(sourceRoot, targetRoot, relativePath) {
|
||||
const source = path.join(sourceRoot, relativePath);
|
||||
const target = path.join(targetRoot, relativePath);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.copyFileSync(source, target);
|
||||
fs.chmodSync(target, fs.statSync(source).mode & 0o777);
|
||||
}
|
||||
|
||||
function createFixture(t) {
|
||||
const fixture = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-version-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(fixture, { recursive: true, force: true }));
|
||||
copyFile(root, fixture, 'package.json');
|
||||
copyFile(root, fixture, 'ql3-release.json');
|
||||
for (const entry of fs.readdirSync(path.join(root, 'packages'), {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('ql3-')) {
|
||||
copyFile(root, fixture, `packages/${entry.name}/package.json`);
|
||||
}
|
||||
}
|
||||
for (const containerRoot of CONTAINER_ROOTS) {
|
||||
for (const relativePath of [
|
||||
'Dockerfile',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'runtime-dependencies/package.json',
|
||||
'runtime-dependencies/package-lock.json',
|
||||
]) {
|
||||
copyFile(root, fixture, `${containerRoot}/${relativePath}`);
|
||||
}
|
||||
}
|
||||
for (const deploymentRoot of DEPLOYMENT_ROOTS) {
|
||||
fs.cpSync(
|
||||
path.join(root, deploymentRoot),
|
||||
path.join(fixture, deploymentRoot),
|
||||
{ recursive: true, dereference: false },
|
||||
);
|
||||
}
|
||||
for (const relativePath of DEPLOYMENT_FILES) {
|
||||
copyFile(root, fixture, relativePath);
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
function replaceVersion(
|
||||
filePath,
|
||||
source = SOURCE_VERSION,
|
||||
target = TARGET_VERSION,
|
||||
) {
|
||||
const contents = fs.readFileSync(filePath, 'utf8');
|
||||
assert.equal(contents.includes(source), true);
|
||||
fs.writeFileSync(filePath, contents.split(source).join(target));
|
||||
}
|
||||
|
||||
test('audits one source-derived QingLong 3 release identity', () => {
|
||||
assert.deepEqual(auditReleaseVersionContract(root), {
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/release-identity@v1',
|
||||
version: SOURCE_VERSION,
|
||||
nodeVersion: '24.18.0',
|
||||
nodeEngine: '>=24.18.0 <25',
|
||||
legacyRootPackageVersion: LEGACY_VERSION,
|
||||
legacyRootExcluded: true,
|
||||
workspacePackageCount: 18,
|
||||
containerRootCount: 4,
|
||||
deploymentFileCount: 242,
|
||||
deploymentImageReferences: 32,
|
||||
deploymentVersionOccurrences: 36,
|
||||
compatible: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('plans the exact governed version surface without touching legacy 2.x', () => {
|
||||
const plan = createVersionTransitionPlan({
|
||||
root,
|
||||
sourceVersion: SOURCE_VERSION,
|
||||
targetVersion: TARGET_VERSION,
|
||||
});
|
||||
assert.equal(plan.fileCount, 65);
|
||||
assert.equal(plan.replacementCount, 83);
|
||||
assert.equal(plan.legacyRootPackageVersion, LEGACY_VERSION);
|
||||
assert.equal(plan.legacyRootExcluded, true);
|
||||
assert.equal(
|
||||
plan.entries.some((entry) => entry.path === 'package.json'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
plan.entries.filter((entry) => entry.path.startsWith('packages/')).length,
|
||||
18,
|
||||
);
|
||||
assert.match(plan.planDigest, /^sha256:[a-f0-9]{64}$/u);
|
||||
assert.equal(validatePlan(plan), plan);
|
||||
});
|
||||
|
||||
test('applies and exactly replays a complete version transition', (t) => {
|
||||
const fixture = createFixture(t);
|
||||
const plan = createVersionTransitionPlan({
|
||||
root: fixture,
|
||||
sourceVersion: SOURCE_VERSION,
|
||||
targetVersion: TARGET_VERSION,
|
||||
});
|
||||
const report = applyVersionTransitionPlan(plan, {
|
||||
root: fixture,
|
||||
report: path.join(fixture, 'first-report.json'),
|
||||
});
|
||||
assert.equal(report.changedFiles, plan.fileCount);
|
||||
assert.equal(report.alreadyCurrentFiles, 0);
|
||||
assert.equal(report.exactReplay, false);
|
||||
assert.equal(auditReleaseVersionContract(fixture).version, TARGET_VERSION);
|
||||
assert.equal(
|
||||
JSON.parse(fs.readFileSync(path.join(fixture, 'package.json'))).version,
|
||||
LEGACY_VERSION,
|
||||
);
|
||||
|
||||
const replay = applyVersionTransitionPlan(plan, {
|
||||
root: fixture,
|
||||
report: path.join(fixture, 'replay-report.json'),
|
||||
});
|
||||
assert.equal(replay.changedFiles, 0);
|
||||
assert.equal(replay.alreadyCurrentFiles, plan.fileCount);
|
||||
assert.equal(replay.exactReplay, true);
|
||||
});
|
||||
|
||||
test('resumes a partial transition using before and after digests', (t) => {
|
||||
const fixture = createFixture(t);
|
||||
const plan = createVersionTransitionPlan({
|
||||
root: fixture,
|
||||
sourceVersion: SOURCE_VERSION,
|
||||
targetVersion: TARGET_VERSION,
|
||||
});
|
||||
replaceVersion(path.join(fixture, 'ql3-release.json'));
|
||||
const report = applyVersionTransitionPlan(plan, {
|
||||
root: fixture,
|
||||
report: path.join(fixture, 'recovered-report.json'),
|
||||
});
|
||||
assert.equal(report.changedFiles, plan.fileCount - 1);
|
||||
assert.equal(report.alreadyCurrentFiles, 1);
|
||||
assert.equal(auditReleaseVersionContract(fixture).version, TARGET_VERSION);
|
||||
});
|
||||
|
||||
test('preflights every governed file before making a partial mutation', (t) => {
|
||||
const fixture = createFixture(t);
|
||||
const plan = createVersionTransitionPlan({
|
||||
root: fixture,
|
||||
sourceVersion: SOURCE_VERSION,
|
||||
targetVersion: TARGET_VERSION,
|
||||
});
|
||||
const drifted = path.join(fixture, plan.entries.at(-1).path);
|
||||
fs.appendFileSync(drifted, '\n');
|
||||
assert.throws(
|
||||
() =>
|
||||
applyVersionTransitionPlan(plan, {
|
||||
root: fixture,
|
||||
report: path.join(fixture, 'must-not-exist.json'),
|
||||
}),
|
||||
/drifted after plan creation/,
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(fs.readFileSync(path.join(fixture, 'ql3-release.json'))).version,
|
||||
SOURCE_VERSION,
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(fixture, 'must-not-exist.json')), false);
|
||||
});
|
||||
|
||||
test('rejects invalid SemVer, downgrade, plan mutation and a symbolic-link identity', (t) => {
|
||||
assert.throws(
|
||||
() =>
|
||||
createVersionTransitionPlan({
|
||||
root,
|
||||
sourceVersion: SOURCE_VERSION,
|
||||
targetVersion: '3.0.0-alpha',
|
||||
}),
|
||||
/monotonically newer/,
|
||||
);
|
||||
const plan = createVersionTransitionPlan({
|
||||
root,
|
||||
sourceVersion: SOURCE_VERSION,
|
||||
targetVersion: TARGET_VERSION,
|
||||
});
|
||||
const mutatedPlan = JSON.parse(JSON.stringify(plan));
|
||||
mutatedPlan.entries[0].path = 'package.json';
|
||||
assert.throws(() => validatePlan(mutatedPlan), /plan entry is invalid/);
|
||||
|
||||
const fixture = createFixture(t);
|
||||
const invalidIdentityPath = path.join(fixture, 'ql3-release.json');
|
||||
const invalidIdentity = JSON.parse(fs.readFileSync(invalidIdentityPath));
|
||||
invalidIdentity.version = '3.0.0-alpha.01';
|
||||
fs.writeFileSync(
|
||||
invalidIdentityPath,
|
||||
`${JSON.stringify(invalidIdentity, null, 2)}\n`,
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseVersionContract(fixture),
|
||||
/identity shape or value is incompatible/,
|
||||
);
|
||||
copyFile(root, fixture, 'ql3-release.json');
|
||||
fs.renameSync(
|
||||
path.join(fixture, 'ql3-release.json'),
|
||||
path.join(fixture, 'identity-target.json'),
|
||||
);
|
||||
fs.symlinkSync(
|
||||
'identity-target.json',
|
||||
path.join(fixture, 'ql3-release.json'),
|
||||
);
|
||||
assert.throws(
|
||||
() => auditReleaseVersionContract(fixture),
|
||||
/canonical regular file/,
|
||||
);
|
||||
});
|
||||
|
||||
test('CLI writes no-replace plans and accepts only closed modes', (t) => {
|
||||
const directory = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-version-cli-')),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const planPath = path.join(directory, 'plan.json');
|
||||
const output = { write() {} };
|
||||
runCli(
|
||||
[
|
||||
'--mode=plan',
|
||||
`--from=${SOURCE_VERSION}`,
|
||||
`--to=${TARGET_VERSION}`,
|
||||
`--output=${planPath}`,
|
||||
],
|
||||
root,
|
||||
output,
|
||||
);
|
||||
assert.equal(fs.statSync(planPath).mode & 0o777, 0o600);
|
||||
assert.throws(
|
||||
() =>
|
||||
runCli(
|
||||
[
|
||||
'--mode=plan',
|
||||
`--from=${SOURCE_VERSION}`,
|
||||
`--to=${TARGET_VERSION}`,
|
||||
`--output=${planPath}`,
|
||||
],
|
||||
root,
|
||||
output,
|
||||
),
|
||||
/output must be unused/,
|
||||
);
|
||||
assert.deepEqual(parseArguments(['--mode=audit']), { mode: 'audit' });
|
||||
assert.throws(
|
||||
() => parseArguments(['--mode=audit', '--extra=true']),
|
||||
/arguments are invalid/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user