mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
127 lines
3.7 KiB
JavaScript
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,
|
|
});
|