feat(ql3): govern release version transitions

This commit is contained in:
whyour
2026-08-16 09:32:27 +08:00
parent 7130d77a76
commit bad8399cc3
24 changed files with 1462 additions and 128 deletions
+99
View File
@@ -0,0 +1,99 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const semver = require('semver');
const RELEASE_IDENTITY_PATH = 'ql3-release.json';
const RELEASE_IDENTITY_SCHEMA = 'qinglong/release-identity@v1';
const MAX_RELEASE_IDENTITY_BYTES = 4096;
const VERSION_PATTERN =
/^3\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$/u;
class QingLong3ReleaseIdentityError extends Error {
constructor(message) {
super(`QingLong 3 release identity failed: ${message}`);
this.name = 'QingLong3ReleaseIdentityError';
}
}
function fail(message) {
throw new QingLong3ReleaseIdentityError(message);
}
function exactKeys(value, expected) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value)) === JSON.stringify(expected)
);
}
function normalizeReleaseIdentity(value) {
if (
!exactKeys(value, [
'schemaVersion',
'schema',
'product',
'version',
'node',
'workspacePackageCount',
'legacyRootPackageExcluded',
]) ||
value.schemaVersion !== 1 ||
value.schema !== RELEASE_IDENTITY_SCHEMA ||
value.product !== 'qinglong3' ||
typeof value.version !== 'string' ||
!VERSION_PATTERN.test(value.version) ||
semver.valid(value.version) !== value.version ||
!exactKeys(value.node, ['version', 'engine']) ||
value.node.version !== '24.18.0' ||
value.node.engine !== '>=24.18.0 <25' ||
value.workspacePackageCount !== 18 ||
value.legacyRootPackageExcluded !== true
) {
fail('identity shape or value is incompatible');
}
return Object.freeze({
...value,
node: Object.freeze({ ...value.node }),
});
}
function readReleaseIdentity(root) {
const resolvedRoot = fs.realpathSync(path.resolve(root));
const filePath = path.join(resolvedRoot, RELEASE_IDENTITY_PATH);
const stat = fs.lstatSync(filePath);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 2 ||
stat.size > MAX_RELEASE_IDENTITY_BYTES ||
fs.realpathSync(filePath) !== filePath
) {
fail('identity file must be one bounded canonical regular file');
}
const contents = fs.readFileSync(filePath, 'utf8');
let parsed;
try {
parsed = JSON.parse(contents);
} catch {
fail('identity file must contain valid JSON');
}
const identity = normalizeReleaseIdentity(parsed);
if (`${JSON.stringify(identity, null, 2)}\n` !== contents) {
fail('identity file must use exact canonical JSON encoding');
}
return identity;
}
module.exports = Object.freeze({
MAX_RELEASE_IDENTITY_BYTES,
RELEASE_IDENTITY_PATH,
RELEASE_IDENTITY_SCHEMA,
VERSION_PATTERN,
QingLong3ReleaseIdentityError,
normalizeReleaseIdentity,
readReleaseIdentity,
});
+3 -2
View File
@@ -8,12 +8,13 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const ROOT = path.resolve(__dirname, '..');
const QL3_VERSION = readReleaseIdentity(ROOT).version;
const NAMESPACE = 'qinglong3-system';
const POSTGRES_CLUSTER = 'ql3-postgres';
const APP_IMAGE =
'registry.example.com/qinglong/qinglong3-cluster-control:3.0.0-alpha.0';
const APP_IMAGE = `registry.example.com/qinglong/qinglong3-cluster-control:${QL3_VERSION}`;
const APP_IMAGE_PLACEHOLDER = `registry.example.com/qinglong/qinglong3-cluster-control@sha256:${'0'.repeat(
64,
)}`;
@@ -2,6 +2,9 @@
const { execFileSync, spawnSync } = require('node:child_process');
const { resolve } = require('node:path');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const QL3_VERSION = readReleaseIdentity(resolve(__dirname, '..')).version;
const IMAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,255}$/u;
const ENTRYPOINT = [
@@ -608,7 +611,7 @@ function main() {
}
}
const version = runImage(image, ['--version']).trim();
if (version !== '3.0.0-alpha.0') fail('product version contract drifted');
if (version !== QL3_VERSION) fail('product version contract drifted');
runOperatorContextContract(image);
runConsoleContract(image);
runEvidenceVerifierContract(image);
+65 -42
View File
@@ -4,6 +4,9 @@ const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');
const yaml = require('js-yaml');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const QL3_VERSION = readReleaseIdentity(path.resolve(__dirname, '..')).version;
const EXPECTED_EXTERNAL_DEPENDENCIES = Object.freeze({
'@aws-sdk/client-s3': '3.1093.0',
@@ -97,7 +100,7 @@ function assertClusterAdminImageCommands(readFile, root, findings) {
const podSpec = podSpecFor(document);
for (const section of ['initContainers', 'containers']) {
for (const container of podSpec?.[section] ?? []) {
if (container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0') {
if (container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}`) {
continue;
}
references += 1;
@@ -457,8 +460,7 @@ function assertExactExternalClosure(readFile, root, findings) {
);
if (
adminManifest.bin?.['ql3-cluster-admin'] !== 'dist/product-cli/cli.js' ||
adminManifest.bin?.['ql3-copilot-mcp'] !==
'dist/copilot-mcp/cli.js' ||
adminManifest.bin?.['ql3-copilot-mcp'] !== 'dist/copilot-mcp/cli.js' ||
adminManifest.exports?.['./copilot-mcp']?.require !==
'./dist/copilot-mcp/server.js' ||
adminManifest.bin?.['ql3-plugin-package-recover'] !==
@@ -1174,7 +1176,7 @@ function assertKubernetes(readFile, root, findings) {
);
}
if (
recoveryContainer?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
recoveryContainer?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
JSON.stringify(recoveryContainer?.command) !==
JSON.stringify([
'node',
@@ -1420,13 +1422,22 @@ function assertClusterAiComponent(readFile, root, findings) {
'deploy/kubernetes/ql3-cluster/components/cluster-ai-copilot',
);
const copilotComponent = yaml.load(
readFile(path.join(copilotComponentDirectory, 'kustomization.yaml'), 'utf8'),
readFile(
path.join(copilotComponentDirectory, 'kustomization.yaml'),
'utf8',
),
);
const copilotPatch = yaml.load(
readFile(path.join(copilotComponentDirectory, 'deployment-patch.yaml'), 'utf8'),
readFile(
path.join(copilotComponentDirectory, 'deployment-patch.yaml'),
'utf8',
),
);
const copilotConfig = yaml.load(
readFile(path.join(copilotComponentDirectory, 'copilot-configmap.yaml'), 'utf8'),
readFile(
path.join(copilotComponentDirectory, 'copilot-configmap.yaml'),
'utf8',
),
);
const copilotOverlay = yaml.load(
readFile(
@@ -1496,8 +1507,8 @@ function assertClusterAiComponent(readFile, root, findings) {
return (
mount?.mountPath !== mountPath ||
mount?.readOnly !== true ||
projection?.name !== authorityName &&
projection?.secretName !== authorityName ||
(projection?.name !== authorityName &&
projection?.secretName !== authorityName) ||
projection?.defaultMode !== 0o440 ||
projection?.optional === true ||
JSON.stringify(projection?.items) !==
@@ -1721,7 +1732,7 @@ function assertClusterAiComponent(readFile, root, findings) {
if (
patch?.kind !== 'Deployment' ||
patch?.metadata?.name !== 'ql3-cluster-control' ||
patchContainer?.image !== 'qinglong3-cluster-control-ai:3.0.0-alpha.0' ||
patchContainer?.image !== `qinglong3-cluster-control-ai:${QL3_VERSION}` ||
patchPod?.serviceAccountName !== undefined ||
patchPod?.automountServiceAccountToken !== undefined ||
authorityMount?.mountPath !== '/var/run/qinglong3/ai/provider-authority' ||
@@ -1830,7 +1841,7 @@ function assertClusterAiComponent(readFile, root, findings) {
const baseContainer = namedEntry(basePod?.containers, 'cluster-control');
const baseEnv = environmentByName(baseContainer);
if (
baseContainer?.image !== 'qinglong3-cluster-control:3.0.0-alpha.0' ||
baseContainer?.image !== `qinglong3-cluster-control:${QL3_VERSION}` ||
[...baseEnv.keys()].some((name) => name.startsWith('QL3_CLUSTER_AI_')) ||
namedEntry(baseContainer?.volumeMounts, 'cluster-ai-provider-authority') ||
namedEntry(baseContainer?.volumeMounts, 'cluster-ai-provider-secrets') ||
@@ -1841,9 +1852,10 @@ function assertClusterAiComponent(readFile, root, findings) {
namedEntry(basePod?.volumes, 'cluster-ai-provider-authority') ||
namedEntry(basePod?.volumes, 'cluster-ai-provider-secrets') ||
namedEntry(basePod?.volumes, 'cluster-ai-prompt-output-keyring') ||
[...copilotProjections].some(([name]) =>
namedEntry(baseContainer?.volumeMounts, name) ||
namedEntry(basePod?.volumes, name),
[...copilotProjections].some(
([name]) =>
namedEntry(baseContainer?.volumeMounts, name) ||
namedEntry(basePod?.volumes, name),
)
) {
findings.push(
@@ -1942,7 +1954,7 @@ function assertPluginPackageManagementDeployment(readFile, root, findings) {
);
}
if (
container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
JSON.stringify(container?.command) !==
JSON.stringify([
'node',
@@ -2384,7 +2396,7 @@ function assertWorkerCredentialManagementDeployment(readFile, root, findings) {
);
}
if (
container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
JSON.stringify(container?.command) !==
JSON.stringify([
'node',
@@ -2777,7 +2789,7 @@ function assertWorkerCredentialManagementClientOperation(
const readinessScript = String(init?.args?.[0] ?? '');
const clientScript = String(container?.args?.[0] ?? '');
if (
init?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
init?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
init?.imagePullPolicy !== 'IfNotPresent' ||
JSON.stringify(init?.command) !== JSON.stringify(['node', '-e']) ||
!readinessScript.includes(
@@ -2790,7 +2802,7 @@ function assertWorkerCredentialManagementClientOperation(
!readinessScript.includes('cert,') ||
!readinessScript.includes('key,') ||
!readinessScript.includes('attempt <= 30') ||
container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
container?.imagePullPolicy !== 'IfNotPresent' ||
JSON.stringify(container?.command) !== JSON.stringify(['/bin/sh', '-c']) ||
!clientScript.includes('set -eu') ||
@@ -3168,7 +3180,7 @@ function assertWorkerCredentialExecutorDeployment(readFile, root, findings) {
);
}
if (
container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
JSON.stringify(container?.command) !==
JSON.stringify([
'node',
@@ -3488,7 +3500,11 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
'ServiceAccount',
actionName,
);
const admissionConfig = namedResource(resources, 'ConfigMap', actionName + '-admission');
const admissionConfig = namedResource(
resources,
'ConfigMap',
actionName + '-admission',
);
const role = namedResource(resources, 'Role', name);
const roleBinding = namedResource(resources, 'RoleBinding', name);
const admissionPolicy = namedResource(
@@ -3572,14 +3588,18 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
admissionPolicy?.spec?.failurePolicy !== 'Fail' ||
admissionPolicy?.spec?.paramKind?.apiVersion !== 'v1' ||
admissionPolicy?.spec?.paramKind?.kind !== 'ConfigMap' ||
admissionPolicy?.spec?.matchConstraints?.resourceRules?.[0]?.operations?.[0] !==
'CREATE' ||
admissionPolicy?.spec?.matchConstraints?.resourceRules?.[0]?.resources?.[0] !==
'jobs' ||
admissionPolicy?.spec?.matchConstraints?.resourceRules?.[0]
?.operations?.[0] !== 'CREATE' ||
admissionPolicy?.spec?.matchConstraints?.resourceRules?.[0]
?.resources?.[0] !== 'jobs' ||
admissionPolicy?.spec?.matchConditions?.[0]?.expression !==
"request.userInfo.username == 'system:serviceaccount:qinglong3-system:ql3-plugin-package-executor'" ||
!admissionExpressions.includes("variables.executor.image == params.data.image") ||
!admissionExpressions.includes('variables.pod.automountServiceAccountToken == false') ||
!admissionExpressions.includes(
'variables.executor.image == params.data.image',
) ||
!admissionExpressions.includes(
'variables.pod.automountServiceAccountToken == false',
) ||
!admissionExpressions.includes('variables.values.secret.items.all') ||
admissionBinding?.spec?.policyName !== actionName ||
admissionBinding?.spec?.paramRef?.name !== actionName + '-admission' ||
@@ -3602,7 +3622,7 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
);
}
if (
container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
JSON.stringify(container?.command) !==
JSON.stringify([
'node',
@@ -3660,7 +3680,10 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_SECRET',
'postgresUrlSecretName',
],
['QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY', 'postgresUrlSecretKey'],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_URL_KEY',
'postgresUrlSecretKey',
],
[
'QL3_PLUGIN_PACKAGE_SECRET_ACTION_POSTGRES_AUTH_SECRET',
'postgresAuthSecretName',
@@ -3773,22 +3796,22 @@ function assertPluginPackageExecutorDeployment(readFile, root, findings) {
);
if (
JSON.stringify(apiServerEgressExample) !==
JSON.stringify([
{
op: 'add',
path: '/spec/egress/-',
value: {
to: [
{
ipBlock: {
cidr: 'REPLACE_WITH_API_SERVER_CIDR',
},
JSON.stringify([
{
op: 'add',
path: '/spec/egress/-',
value: {
to: [
{
ipBlock: {
cidr: 'REPLACE_WITH_API_SERVER_CIDR',
},
],
ports: [{ protocol: 'TCP', port: 443 }],
},
},
],
ports: [{ protocol: 'TCP', port: 443 }],
},
])
},
])
) {
findings.push(
finding(
@@ -282,6 +282,16 @@ function auditClusterImageCiWorkflow(
/pnpm audit:image-release:ql3/,
'image CI must audit the shared release workflow contract',
);
requirePattern(
source,
/test\/back\/ql3VersionTransition\.test\.cjs/,
'supply-chain CI must run release version transition negative tests',
);
requirePattern(
source,
/pnpm audit:release-version:ql3/,
'supply-chain CI must audit the source-derived release version identity',
);
requirePattern(
source,
/docker build[\s\S]*--file \$\{\{ matrix\.dockerfile \}\}[\s\S]*--target \$\{\{ matrix\.target \}\}/,
@@ -375,6 +385,7 @@ function auditClusterImageCiWorkflow(
clusterAdminOperatorContext: true,
clusterAdminContextPreflight: true,
clusterAdminContextReadiness: true,
releaseVersionAudit: true,
ociAttestations: true,
osVulnerabilityScan: {
scanner: 'trivy@0.70.0',
+5 -3
View File
@@ -9,8 +9,10 @@ const {
createClusterImageSbom,
resolveImageProfile,
} = require('./ql3-cluster-image-sbom.cjs');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const QL3_VERSION = readReleaseIdentity(DEFAULT_ROOT).version;
const OCI_INDEX_MEDIA_TYPE = 'application/vnd.oci.image.index.v1+json';
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
const OCI_CONFIG_MEDIA_TYPE = 'application/vnd.oci.image.config.v1+json';
@@ -196,7 +198,7 @@ function expectedImageConfig(architecture, revision, image) {
'org.opencontainers.image.source':
'https://github.com/whyour/qinglong',
'org.opencontainers.image.title': 'QingLong 3.0 Worker',
'org.opencontainers.image.version': '3.0.0-alpha.0',
'org.opencontainers.image.version': QL3_VERSION,
},
},
};
@@ -233,7 +235,7 @@ function expectedImageConfig(architecture, revision, image) {
'org.opencontainers.image.source':
'https://github.com/whyour/qinglong',
'org.opencontainers.image.title': 'QingLong 3.0 Local Application',
'org.opencontainers.image.version': '3.0.0-alpha.0',
'org.opencontainers.image.version': QL3_VERSION,
},
},
};
@@ -281,7 +283,7 @@ function expectedImageConfig(architecture, revision, image) {
? 'QingLong 3.0 Cluster Control AI'
: 'QingLong 3.0 Cluster Control'
: 'QingLong 3.0 Cluster Admin',
'org.opencontainers.image.version': '3.0.0-alpha.0',
'org.opencontainers.image.version': QL3_VERSION,
},
},
};
+3 -1
View File
@@ -2,8 +2,10 @@
const fs = require('node:fs');
const path = require('node:path');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const IMAGE_DIRECTORY = 'deploy/containers/ql3-local-application';
const QL3_VERSION = readReleaseIdentity(path.resolve(__dirname, '..')).version;
const NODE_IMAGE =
'node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d';
const BUILD_DEPENDENCIES = Object.freeze({
@@ -56,7 +58,7 @@ function auditManifest(manifest, runtime, findings) {
const expectedName = '@qinglong/local-application-image';
if (
manifest.name !== expectedName ||
manifest.version !== '3.0.0-alpha.0' ||
manifest.version !== QL3_VERSION ||
manifest.private !== true ||
manifest.license !== 'Apache-2.0' ||
manifest.engines?.node !== '>=24.18.0 <25'
@@ -16,8 +16,10 @@ const {
parseProcStat,
parseProcStatus,
} = require('./ql3-physical-edge-idle-sampler.cjs');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const MIB = 1024 * 1024;
const QL3_VERSION = readReleaseIdentity(path.resolve(__dirname, '..')).version;
const MAX_INPUT_BYTES = 256 * 1024;
const MAX_OUTPUT_BYTES = 64 * 1024;
const MAX_ARTIFACT_FILES = 768;
@@ -569,7 +571,7 @@ function collectArtifactIdentity(artifactRootInput) {
const applicationEntrypoint = path.join(packageRoot, 'dist', 'cli.js');
if (
packageManifest.name !== '@qinglong/local-application' ||
packageManifest.version !== '3.0.0-alpha.0' ||
packageManifest.version !== QL3_VERSION ||
packageManifest.bin?.['ql3-local-application'] !== 'dist/cli.js' ||
packageManifest.engines?.node !== '>=24.18.0 <25' ||
fs.realpathSync(applicationEntrypoint) !== applicationEntrypoint
@@ -1,6 +1,9 @@
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('js-yaml');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const QL3_VERSION = readReleaseIdentity(path.resolve(__dirname, '..')).version;
const OPERATION = path.join(
'deploy',
@@ -130,7 +133,7 @@ function auditPromptOutputExternalRecoveryDeployment(options = {}) {
!Array.isArray(containers) ||
containers.length !== 1 ||
container?.name !== 'verifier' ||
container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0' ||
container?.image !== `qinglong3-cluster-admin:${QL3_VERSION}` ||
JSON.stringify(container?.command) !==
JSON.stringify([
'node',
+26 -13
View File
@@ -6,6 +6,10 @@ const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { auditPackageBoundaries } = require('./ql3-package-boundary-audit.cjs');
const {
VERSION_PATTERN,
readReleaseIdentity,
} = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/release-candidate-contract@v1';
@@ -13,8 +17,6 @@ const PREDICATE_TYPE =
'https://qinglong.dev/attestations/release-candidate-contract/v1';
const MAX_REPORT_BYTES = 1024 * 1024;
const RELEASE_SCOPES = Object.freeze(['all', 'cluster', 'local']);
const NODE_ENGINE = '>=24.18.0 <25';
const NODE_VERSION = '24.18.0';
const LOCAL_IMAGES = Object.freeze([
Object.freeze({
image: 'local',
@@ -93,9 +95,7 @@ function selectedImages(scope) {
function validateIdentity(options) {
if (
typeof options.version !== 'string' ||
!/^3\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$/u.test(
options.version,
)
!VERSION_PATTERN.test(options.version)
) {
fail('version must be an exact QingLong 3 SemVer');
}
@@ -113,11 +113,17 @@ function validateIdentity(options) {
function createReleaseCandidateContract(options) {
const root = path.resolve(options.root || DEFAULT_ROOT);
validateIdentity(options);
const releaseIdentity = readReleaseIdentity(root);
if (options.version !== releaseIdentity.version) {
fail('requested version differs from the repository release identity');
}
const boundaries = auditPackageBoundaries(root);
if (
!boundaries.compatible ||
boundaries.workspacePackageCount !== 18 ||
boundaries.workspacePackageHardCap !== 18 ||
boundaries.workspacePackageCount !==
releaseIdentity.workspacePackageCount ||
boundaries.workspacePackageHardCap !==
releaseIdentity.workspacePackageCount ||
boundaries.singleSourcePackages.length !== 0 ||
boundaries.shallowSourcePackages.length !== 0
) {
@@ -129,7 +135,7 @@ function createReleaseCandidateContract(options) {
if (
manifest.name !== entry.name ||
manifest.version !== options.version ||
manifest.engines?.node !== NODE_ENGINE
manifest.engines?.node !== releaseIdentity.node.engine
) {
fail(`workspace release identity differs: ${entry.path}`);
}
@@ -147,7 +153,7 @@ function createReleaseCandidateContract(options) {
);
if (
manifest.version !== options.version ||
manifest.engines?.node !== NODE_ENGINE
manifest.engines?.node !== releaseIdentity.node.engine
) {
fail(`image release identity differs: ${image.runtime_root}`);
}
@@ -156,7 +162,9 @@ function createReleaseCandidateContract(options) {
'utf8',
);
if (
!dockerfile.includes(`node:${NODE_VERSION}-bookworm-slim@sha256:`) ||
!dockerfile.includes(
`node:${releaseIdentity.node.version}-bookworm-slim@sha256:`,
) ||
!dockerfile.includes(
`org.opencontainers.image.version=\"${options.version}\"`,
)
@@ -204,9 +212,14 @@ function createReleaseCandidateContract(options) {
compatibility: {
legacyRootPackageVersion: readJson(path.join(root, 'package.json'))
.version,
legacyRootExcludedFromReleaseIdentity: true,
nodeVersion: NODE_VERSION,
nodeEngine: NODE_ENGINE,
legacyRootExcludedFromReleaseIdentity:
releaseIdentity.legacyRootPackageExcluded,
releaseIdentitySchema: releaseIdentity.schema,
releaseIdentityDigest: sha256(
Buffer.from(JSON.stringify(releaseIdentity)),
),
nodeVersion: releaseIdentity.node.version,
nodeEngine: releaseIdentity.node.engine,
platforms: ['linux/amd64', 'linux/arm64'],
},
workspace: {
+717
View File
@@ -0,0 +1,717 @@
#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const semver = require('semver');
const {
RELEASE_IDENTITY_PATH,
RELEASE_IDENTITY_SCHEMA,
VERSION_PATTERN,
readReleaseIdentity,
} = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const PLAN_SCHEMA = 'qinglong/version-transition-plan@v1';
const REPORT_SCHEMA = 'qinglong/version-transition-report@v1';
const MAX_FILE_BYTES = 4 * 1024 * 1024;
const MAX_PLAN_BYTES = 2 * 1024 * 1024;
const MAX_GOVERNED_FILES = 512;
const CONTAINER_ROOTS = Object.freeze([
'deploy/containers/ql3-cluster-control',
'deploy/containers/ql3-cluster-admin',
'deploy/containers/ql3-local-application',
'deploy/containers/ql3-worker',
]);
const DEPLOYMENT_ROOTS = Object.freeze([
'deploy/kubernetes/ql3-cluster',
'deploy/kubernetes/ql3-worker',
]);
const DEPLOYMENT_FILES = Object.freeze([
'deploy/console/ql3-cluster-copilot/README.md',
]);
const CONTAINER_FILES = Object.freeze([
'Dockerfile',
'package.json',
'package-lock.json',
'runtime-dependencies/package.json',
'runtime-dependencies/package-lock.json',
]);
const TEXT_EXTENSIONS = new Set(['.json', '.md', '.yaml', '.yml']);
const IMAGE_TAG_PATTERN =
/qinglong3-(?:cluster-control-ai|cluster-control|cluster-admin|local-application|worker):([0-9A-Za-z.-]+)/gu;
const SOURCE_TAG_PATTERN = /refs\/tags\/v(3\.[0-9A-Za-z.-]+)/gu;
class QingLong3VersionTransitionError extends Error {
constructor(message) {
super(`QingLong 3 version transition failed: ${message}`);
this.name = 'QingLong3VersionTransitionError';
}
}
function fail(message) {
throw new QingLong3VersionTransitionError(message);
}
function sha256(contents) {
return `sha256:${crypto.createHash('sha256').update(contents).digest('hex')}`;
}
function exactKeys(value, expected) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value)) === JSON.stringify(expected)
);
}
function resolveRoot(root) {
return fs.realpathSync(path.resolve(root || DEFAULT_ROOT));
}
function resolveGovernedPath(root, relativePath) {
if (
typeof relativePath !== 'string' ||
relativePath.length < 1 ||
relativePath.length > 512 ||
path.isAbsolute(relativePath) ||
path.posix.normalize(relativePath) !== relativePath ||
relativePath.includes('\\') ||
relativePath.split('/').includes('..')
) {
fail('governed path must be one canonical repository-relative path');
}
const resolved = path.resolve(root, relativePath);
if (!resolved.startsWith(`${root}${path.sep}`)) {
fail('governed path escapes the repository');
}
return resolved;
}
function readRegularFile(root, relativePath, maximumBytes = MAX_FILE_BYTES) {
const filePath = resolveGovernedPath(root, relativePath);
const stat = fs.lstatSync(filePath);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 1 ||
stat.size > maximumBytes ||
fs.realpathSync(filePath) !== filePath ||
fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath)
) {
fail(`invalid governed regular file: ${relativePath}`);
}
return Object.freeze({
filePath,
contents: fs.readFileSync(filePath, 'utf8'),
mode: stat.mode & 0o777,
});
}
function readJson(root, relativePath) {
const file = readRegularFile(root, relativePath);
try {
return Object.freeze({ ...file, value: JSON.parse(file.contents) });
} catch {
fail(`governed JSON is invalid: ${relativePath}`);
}
}
function walkTextFiles(root, relativeDirectory, output = []) {
const directory = resolveGovernedPath(root, relativeDirectory);
const stat = fs.lstatSync(directory);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
fs.realpathSync(directory) !== directory
) {
fail(`invalid governed directory: ${relativeDirectory}`);
}
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const relativePath = path.posix.join(relativeDirectory, entry.name);
if (entry.isSymbolicLink())
fail(`symbolic link in governed tree: ${relativePath}`);
if (entry.isDirectory()) {
walkTextFiles(root, relativePath, output);
continue;
}
if (!entry.isFile())
fail(`unsupported entry in governed tree: ${relativePath}`);
if (TEXT_EXTENSIONS.has(path.extname(entry.name)))
output.push(relativePath);
if (output.length > MAX_GOVERNED_FILES)
fail('governed file ceiling exceeded');
}
return output;
}
function workspaceManifestPaths(root) {
const packagesRoot = resolveGovernedPath(root, 'packages');
const entries = fs
.readdirSync(packagesRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith('ql3-'))
.map((entry) => `packages/${entry.name}/package.json`)
.sort();
return Object.freeze(entries);
}
function fixedContainerPaths() {
return Object.freeze(
CONTAINER_ROOTS.flatMap((root) =>
CONTAINER_FILES.map((file) => `${root}/${file}`),
).sort(),
);
}
function deploymentTextPaths(root) {
return Object.freeze(
[
...DEPLOYMENT_ROOTS.flatMap((directory) =>
walkTextFiles(root, directory),
),
...DEPLOYMENT_FILES,
].sort(),
);
}
function versionOccurrences(contents, version) {
return contents.split(version).length - 1;
}
function taggedVersions(contents) {
return Object.freeze([
...[...contents.matchAll(IMAGE_TAG_PATTERN)].map((match) => match[1]),
...[...contents.matchAll(SOURCE_TAG_PATTERN)].map((match) => match[1]),
]);
}
function auditReleaseVersionContract(rootInput = DEFAULT_ROOT) {
const root = resolveRoot(rootInput);
const identity = readReleaseIdentity(root);
const legacyRoot = readJson(root, 'package.json').value;
if (
typeof legacyRoot.version !== 'string' ||
!/^2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$/u.test(
legacyRoot.version,
) ||
legacyRoot.version === identity.version
) {
fail('legacy root package must remain outside the 3.0 release identity');
}
const workspacePaths = workspaceManifestPaths(root);
if (workspacePaths.length !== identity.workspacePackageCount) {
fail('workspace package count differs from the release identity');
}
for (const relativePath of workspacePaths) {
const manifest = readJson(root, relativePath).value;
if (
typeof manifest.name !== 'string' ||
!manifest.name.startsWith('@qinglong/') ||
manifest.version !== identity.version ||
manifest.engines?.node !== identity.node.engine
) {
fail(`workspace package release identity drifted: ${relativePath}`);
}
}
for (const containerRoot of CONTAINER_ROOTS) {
const buildManifest = readJson(root, `${containerRoot}/package.json`).value;
const buildLock = readJson(
root,
`${containerRoot}/package-lock.json`,
).value;
const runtimeManifest = readJson(
root,
`${containerRoot}/runtime-dependencies/package.json`,
).value;
const runtimeLock = readJson(
root,
`${containerRoot}/runtime-dependencies/package-lock.json`,
).value;
if (
buildManifest.version !== identity.version ||
runtimeManifest.version !== identity.version ||
buildManifest.engines?.node !== identity.node.engine ||
runtimeManifest.engines?.node !== identity.node.engine ||
buildLock.version !== identity.version ||
buildLock.packages?.['']?.version !== identity.version ||
runtimeLock.version !== identity.version ||
runtimeLock.packages?.['']?.version !== identity.version
) {
fail(
`container manifest or lock release identity drifted: ${containerRoot}`,
);
}
const dockerfile = readRegularFile(
root,
`${containerRoot}/Dockerfile`,
).contents;
if (
!dockerfile.includes(
`node:${identity.node.version}-bookworm-slim@sha256:`,
) ||
versionOccurrences(
dockerfile,
`org.opencontainers.image.version=\"${identity.version}\"`,
) !== 1
) {
fail(`container Dockerfile release identity drifted: ${containerRoot}`);
}
}
let deploymentVersionOccurrences = 0;
let deploymentImageReferences = 0;
const deploymentFiles = deploymentTextPaths(root);
for (const relativePath of deploymentFiles) {
const contents = readRegularFile(root, relativePath).contents;
const versions = taggedVersions(contents);
if (versions.some((version) => version !== identity.version)) {
fail(`deployment release identity drifted: ${relativePath}`);
}
deploymentImageReferences += [...contents.matchAll(IMAGE_TAG_PATTERN)]
.length;
deploymentVersionOccurrences += versionOccurrences(
contents,
identity.version,
);
}
if (deploymentImageReferences < 4 || deploymentVersionOccurrences < 4) {
fail('deployment release identity coverage is incomplete');
}
return Object.freeze({
schemaVersion: 1,
schema: RELEASE_IDENTITY_SCHEMA,
version: identity.version,
nodeVersion: identity.node.version,
nodeEngine: identity.node.engine,
legacyRootPackageVersion: legacyRoot.version,
legacyRootExcluded: true,
workspacePackageCount: workspacePaths.length,
containerRootCount: CONTAINER_ROOTS.length,
deploymentFileCount: deploymentFiles.length,
deploymentImageReferences,
deploymentVersionOccurrences,
compatible: true,
});
}
function governedTransitionPaths(root, version) {
const deploymentPaths = deploymentTextPaths(root).filter((relativePath) =>
readRegularFile(root, relativePath).contents.includes(version),
);
return Object.freeze(
[
RELEASE_IDENTITY_PATH,
...workspaceManifestPaths(root),
...fixedContainerPaths(),
...deploymentPaths,
].sort(),
);
}
function validateVersionTransition(sourceVersion, targetVersion) {
if (
typeof sourceVersion !== 'string' ||
typeof targetVersion !== 'string' ||
!VERSION_PATTERN.test(sourceVersion) ||
!VERSION_PATTERN.test(targetVersion) ||
semver.valid(sourceVersion) !== sourceVersion ||
semver.valid(targetVersion) !== targetVersion ||
!semver.gt(targetVersion, sourceVersion)
) {
fail('target must be one exact monotonically newer QingLong 3 SemVer');
}
}
function createVersionTransitionPlan(options = {}) {
const root = resolveRoot(options.root || DEFAULT_ROOT);
validateVersionTransition(options.sourceVersion, options.targetVersion);
const audit = auditReleaseVersionContract(root);
if (audit.version !== options.sourceVersion) {
fail('source version differs from the current release identity');
}
const paths = governedTransitionPaths(root, options.sourceVersion);
const entries = paths.map((relativePath) => {
const file = readRegularFile(root, relativePath);
const replacementCount = versionOccurrences(
file.contents,
options.sourceVersion,
);
if (replacementCount < 1) {
fail(`governed transition target has no source version: ${relativePath}`);
}
const next = file.contents
.split(options.sourceVersion)
.join(options.targetVersion);
return Object.freeze({
path: relativePath,
mode: file.mode,
replacementCount,
beforeBytes: Buffer.byteLength(file.contents),
afterBytes: Buffer.byteLength(next),
beforeDigest: sha256(file.contents),
afterDigest: sha256(next),
});
});
const unsigned = {
schemaVersion: 1,
schema: PLAN_SCHEMA,
sourceVersion: options.sourceVersion,
targetVersion: options.targetVersion,
legacyRootPackageVersion: audit.legacyRootPackageVersion,
legacyRootExcluded: true,
fileCount: entries.length,
replacementCount: entries.reduce(
(total, entry) => total + entry.replacementCount,
0,
),
entries,
};
return Object.freeze({
...unsigned,
planDigest: sha256(JSON.stringify(unsigned)),
});
}
function validatePlan(plan) {
if (
!exactKeys(plan, [
'schemaVersion',
'schema',
'sourceVersion',
'targetVersion',
'legacyRootPackageVersion',
'legacyRootExcluded',
'fileCount',
'replacementCount',
'entries',
'planDigest',
]) ||
plan.schemaVersion !== 1 ||
plan.schema !== PLAN_SCHEMA ||
plan.legacyRootExcluded !== true ||
!Array.isArray(plan.entries) ||
plan.entries.length < 1 ||
plan.entries.length > MAX_GOVERNED_FILES ||
plan.fileCount !== plan.entries.length
) {
fail('version transition plan shape is invalid');
}
validateVersionTransition(plan.sourceVersion, plan.targetVersion);
let previousPath = '';
let replacements = 0;
for (const entry of plan.entries) {
if (
!exactKeys(entry, [
'path',
'mode',
'replacementCount',
'beforeBytes',
'afterBytes',
'beforeDigest',
'afterDigest',
]) ||
typeof entry.path !== 'string' ||
entry.path <= previousPath ||
!Number.isSafeInteger(entry.mode) ||
entry.mode < 0o400 ||
entry.mode > 0o777 ||
!Number.isSafeInteger(entry.replacementCount) ||
entry.replacementCount < 1 ||
!Number.isSafeInteger(entry.beforeBytes) ||
entry.beforeBytes < 1 ||
entry.beforeBytes > MAX_FILE_BYTES ||
!Number.isSafeInteger(entry.afterBytes) ||
entry.afterBytes < 1 ||
entry.afterBytes > MAX_FILE_BYTES ||
!/^sha256:[a-f0-9]{64}$/u.test(entry.beforeDigest || '') ||
!/^sha256:[a-f0-9]{64}$/u.test(entry.afterDigest || '') ||
entry.beforeDigest === entry.afterDigest
) {
fail('version transition plan entry is invalid');
}
previousPath = entry.path;
replacements += entry.replacementCount;
}
if (plan.replacementCount !== replacements) {
fail('version transition replacement count drifted');
}
const { planDigest, ...unsigned } = plan;
if (planDigest !== sha256(JSON.stringify(unsigned))) {
fail('version transition plan digest drifted');
}
return plan;
}
function readPlan(filePath) {
const resolved = path.resolve(filePath || '');
if (!path.isAbsolute(filePath || '')) fail('plan path must be absolute');
const stat = fs.lstatSync(resolved);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 2 ||
stat.size > MAX_PLAN_BYTES ||
fs.realpathSync(resolved) !== resolved
) {
fail('plan must be one bounded canonical regular file');
}
let plan;
try {
plan = JSON.parse(fs.readFileSync(resolved, 'utf8'));
} catch {
fail('plan must contain valid JSON');
}
return validatePlan(plan);
}
function outputPathReady(filePath) {
const resolved = path.resolve(filePath || '');
if (
!path.isAbsolute(filePath || '') ||
fs.existsSync(resolved) ||
fs.realpathSync(path.dirname(resolved)) !== path.dirname(resolved)
) {
fail('output must be unused in one canonical directory');
}
return resolved;
}
function writeNoReplace(filePath, value) {
const resolved = outputPathReady(filePath);
fs.writeFileSync(resolved, `${JSON.stringify(value)}\n`, {
encoding: 'utf8',
mode: 0o600,
flag: 'wx',
});
}
function validatePlanCoverage(root, plan) {
const legacyVersion = readJson(root, 'package.json').value.version;
if (legacyVersion !== plan.legacyRootPackageVersion) {
fail('legacy root package changed after plan creation');
}
const expectedPaths = new Set([
RELEASE_IDENTITY_PATH,
...workspaceManifestPaths(root),
...fixedContainerPaths(),
]);
for (const relativePath of deploymentTextPaths(root)) {
const contents = readRegularFile(root, relativePath).contents;
const versions = taggedVersions(contents);
if (
versions.some(
(version) =>
version !== plan.sourceVersion && version !== plan.targetVersion,
)
) {
fail(`deployment version is outside the transition: ${relativePath}`);
}
if (
contents.includes(plan.sourceVersion) ||
contents.includes(plan.targetVersion)
) {
expectedPaths.add(relativePath);
}
}
const actualPaths = plan.entries.map((entry) => entry.path);
if (
JSON.stringify([...expectedPaths].sort()) !== JSON.stringify(actualPaths)
) {
fail('version transition plan does not cover the exact governed file set');
}
}
function temporaryPath(filePath, planDigest) {
return path.join(
path.dirname(filePath),
`.${path.basename(filePath)}.ql3-version-${planDigest.slice(7, 23)}.tmp`,
);
}
function materializeTarget(file, entry, plan) {
const next = file.contents.split(plan.sourceVersion).join(plan.targetVersion);
if (
versionOccurrences(file.contents, plan.sourceVersion) !==
entry.replacementCount ||
Buffer.byteLength(next) !== entry.afterBytes ||
sha256(next) !== entry.afterDigest
) {
fail(`source content no longer derives the planned target: ${entry.path}`);
}
return next;
}
function applyVersionTransitionPlan(planInput, options = {}) {
const root = resolveRoot(options.root || DEFAULT_ROOT);
const plan = validatePlan(planInput);
outputPathReady(options.report);
validatePlanCoverage(root, plan);
const states = plan.entries.map((entry) => {
const file = readRegularFile(root, entry.path);
const digest = sha256(file.contents);
const state =
digest === entry.beforeDigest
? 'source'
: digest === entry.afterDigest
? 'target'
: null;
if (!state || file.mode !== entry.mode) {
fail(`governed file drifted after plan creation: ${entry.path}`);
}
const next =
state === 'source' ? materializeTarget(file, entry, plan) : null;
const tempPath = temporaryPath(file.filePath, plan.planDigest);
if (fs.existsSync(tempPath)) {
const temp = fs.lstatSync(tempPath);
const tempContents =
temp.isFile() && !temp.isSymbolicLink()
? fs.readFileSync(tempPath, 'utf8')
: '';
if (
fs.realpathSync(tempPath) !== tempPath ||
(temp.mode & 0o777) !== entry.mode ||
sha256(tempContents) !== entry.afterDigest
) {
fail(`deterministic recovery file drifted: ${entry.path}`);
}
}
return Object.freeze({ entry, file, state, next, tempPath });
});
let changedFiles = 0;
let alreadyCurrentFiles = 0;
for (const state of states) {
if (state.state === 'target') {
alreadyCurrentFiles += 1;
if (fs.existsSync(state.tempPath)) fs.unlinkSync(state.tempPath);
continue;
}
if (!fs.existsSync(state.tempPath)) {
const descriptor = fs.openSync(
state.tempPath,
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY,
state.entry.mode,
);
try {
fs.writeFileSync(descriptor, state.next, 'utf8');
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
fs.renameSync(state.tempPath, state.file.filePath);
changedFiles += 1;
}
const audit = auditReleaseVersionContract(root);
if (audit.version !== plan.targetVersion) {
fail('post-transition release identity is incompatible');
}
const reportUnsigned = {
schemaVersion: 1,
schema: REPORT_SCHEMA,
planDigest: plan.planDigest,
sourceVersion: plan.sourceVersion,
targetVersion: plan.targetVersion,
fileCount: plan.fileCount,
changedFiles,
alreadyCurrentFiles,
exactReplay: alreadyCurrentFiles === plan.fileCount,
legacyRootPackageVersion: audit.legacyRootPackageVersion,
legacyRootExcluded: true,
compatible: true,
};
const report = Object.freeze({
...reportUnsigned,
reportDigest: sha256(JSON.stringify(reportUnsigned)),
});
writeNoReplace(options.report, report);
return report;
}
function parseArguments(argv) {
const values = {};
for (const argument of argv) {
const match = /^--([a-z-]+)=(.+)$/u.exec(argument);
if (!match || Object.hasOwn(values, match[1]))
fail('arguments are invalid');
values[match[1]] = match[2];
}
const expected =
values.mode === 'audit'
? ['mode']
: values.mode === 'plan'
? ['from', 'mode', 'output', 'to']
: values.mode === 'apply'
? ['mode', 'plan', 'report']
: [];
if (
expected.length === 0 ||
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)
) {
fail('arguments are invalid');
}
return Object.freeze(values);
}
function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) {
const options = parseArguments(argv);
if (options.mode === 'audit') {
const audit = auditReleaseVersionContract(root);
output.write(`${JSON.stringify(audit)}\n`);
return audit;
}
if (options.mode === 'plan') {
const plan = createVersionTransitionPlan({
root,
sourceVersion: options.from,
targetVersion: options.to,
});
writeNoReplace(options.output, plan);
output.write(`${JSON.stringify(plan)}\n`);
return plan;
}
const plan = readPlan(options.plan);
const report = applyVersionTransitionPlan(plan, {
root,
report: options.report,
});
output.write(`${JSON.stringify(report)}\n`);
return report;
}
if (require.main === module) {
try {
runCli(process.argv.slice(2));
} catch (error) {
process.stderr.write(
`${
error instanceof Error ? error.message : 'version transition failed'
}\n`,
);
process.exitCode = 1;
}
}
module.exports = Object.freeze({
CONTAINER_ROOTS,
DEPLOYMENT_FILES,
DEPLOYMENT_ROOTS,
PLAN_SCHEMA,
REPORT_SCHEMA,
QingLong3VersionTransitionError,
applyVersionTransitionPlan,
auditReleaseVersionContract,
createVersionTransitionPlan,
parseArguments,
readPlan,
runCli,
validatePlan,
});