Files
qinglong/scripts/lib/ql3-release-identity.cjs
T

127 lines
3.7 KiB
JavaScript

'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@v2';
const ARCHITECTURE_SUPPORT = Object.freeze({
tier1: Object.freeze(['amd64', 'arm64']),
candidates: Object.freeze(['ppc64le', 's390x']),
experimentalBlocked: Object.freeze(['arm/v7']),
legacyOnly: Object.freeze(['arm/v6', '386']),
legacyLine: '2.x',
});
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',
'architectureSupport',
'workspacePackageCount',
'legacyRootPackageExcluded',
]) ||
value.schemaVersion !== 2 ||
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' ||
!exactKeys(value.architectureSupport, [
'tier1',
'candidates',
'experimentalBlocked',
'legacyOnly',
'legacyLine',
]) ||
JSON.stringify(value.architectureSupport) !==
JSON.stringify(ARCHITECTURE_SUPPORT) ||
value.workspacePackageCount !== 18 ||
value.legacyRootPackageExcluded !== true
) {
fail('identity shape or value is incompatible');
}
return Object.freeze({
...value,
node: Object.freeze({ ...value.node }),
architectureSupport: Object.freeze({
...value.architectureSupport,
tier1: Object.freeze([...value.architectureSupport.tier1]),
candidates: Object.freeze([...value.architectureSupport.candidates]),
experimentalBlocked: Object.freeze([
...value.architectureSupport.experimentalBlocked,
]),
legacyOnly: Object.freeze([...value.architectureSupport.legacyOnly]),
}),
});
}
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({
ARCHITECTURE_SUPPORT,
MAX_RELEASE_IDENTITY_BYTES,
RELEASE_IDENTITY_PATH,
RELEASE_IDENTITY_SCHEMA,
VERSION_PATTERN,
QingLong3ReleaseIdentityError,
normalizeReleaseIdentity,
readReleaseIdentity,
});