Files
Toju/tools/generate-release-manifest.js
2026-03-10 23:38:57 +01:00

257 lines
6.5 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const rootDir = path.resolve(__dirname, '..');
const defaultManifestPath = path.join(rootDir, 'dist-electron', 'release-manifest.json');
const descriptorCandidates = ['latest.yml', 'latest-mac.yml', 'latest-linux.yml'];
function normalizeVersion(rawValue) {
if (typeof rawValue !== 'string') {
return null;
}
const trimmedValue = rawValue.trim();
if (!trimmedValue) {
return null;
}
return trimmedValue.replace(/^v/i, '').split('+')[0] || null;
}
function parseVersion(rawValue) {
const normalized = normalizeVersion(rawValue);
if (!normalized) {
return null;
}
const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/);
if (!match) {
return null;
}
return {
major: Number.parseInt(match[1], 10),
minor: Number.parseInt(match[2] || '0', 10),
patch: Number.parseInt(match[3] || '0', 10),
prerelease: match[4] ? match[4].split('.') : []
};
}
function comparePrereleaseIdentifiers(left, right) {
const leftNumeric = /^\d+$/.test(left) ? Number.parseInt(left, 10) : null;
const rightNumeric = /^\d+$/.test(right) ? Number.parseInt(right, 10) : null;
if (leftNumeric !== null && rightNumeric !== null) {
return leftNumeric - rightNumeric;
}
if (leftNumeric !== null) {
return -1;
}
if (rightNumeric !== null) {
return 1;
}
return left.localeCompare(right);
}
function compareVersions(leftValue, rightValue) {
const left = parseVersion(leftValue);
const right = parseVersion(rightValue);
if (!left && !right) {
return 0;
}
if (!left) {
return -1;
}
if (!right) {
return 1;
}
if (left.major !== right.major) {
return left.major - right.major;
}
if (left.minor !== right.minor) {
return left.minor - right.minor;
}
if (left.patch !== right.patch) {
return left.patch - right.patch;
}
if (left.prerelease.length === 0 && right.prerelease.length === 0) {
return 0;
}
if (left.prerelease.length === 0) {
return 1;
}
if (right.prerelease.length === 0) {
return -1;
}
const maxLength = Math.max(left.prerelease.length, right.prerelease.length);
for (let index = 0; index < maxLength; index += 1) {
const leftIdentifier = left.prerelease[index];
const rightIdentifier = right.prerelease[index];
if (!leftIdentifier) {
return -1;
}
if (!rightIdentifier) {
return 1;
}
const comparison = comparePrereleaseIdentifiers(leftIdentifier, rightIdentifier);
if (comparison !== 0) {
return comparison;
}
}
return 0;
}
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (!token.startsWith('--')) {
continue;
}
const key = token.slice(2);
const nextToken = argv[index + 1];
if (!nextToken || nextToken.startsWith('--')) {
args[key] = true;
continue;
}
args[key] = nextToken;
index += 1;
}
return args;
}
function ensureHttpUrl(rawValue) {
if (typeof rawValue !== 'string' || rawValue.trim().length === 0) {
throw new Error('A release feed URL is required. Pass it with --feed-url.');
}
const parsedUrl = new URL(rawValue.trim());
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error('The release feed URL must use http:// or https://.');
}
return parsedUrl.toString().replace(/\/$/, '');
}
function readJsonIfExists(filePath) {
if (!fs.existsSync(filePath)) {
return null;
}
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function warnIfNoDescriptors(distPath) {
const availableDescriptors = descriptorCandidates.filter((fileName) => {
return fs.existsSync(path.join(distPath, fileName));
});
if (availableDescriptors.length === 0) {
console.warn('[release-manifest] Warning: no latest*.yml descriptor was found in dist-electron/.');
console.warn('[release-manifest] Upload a feed directory that contains the Electron Builder update descriptors.');
}
}
function buildDefaultManifest(version) {
return {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
minimumServerVersion: version,
pollIntervalMinutes: 30,
versions: []
};
}
function sortManifestVersions(versions) {
return [...versions].sort((left, right) => compareVersions(right.version, left.version));
}
function main() {
const args = parseArgs(process.argv.slice(2));
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
const version = normalizeVersion(args.version || packageJson.version);
if (!version) {
throw new Error('Unable to determine the release version. Pass it with --version.');
}
const feedUrl = ensureHttpUrl(args['feed-url']);
const manifestPath = path.resolve(rootDir, args.manifest || defaultManifestPath);
const existingPath = path.resolve(rootDir, args.existing || manifestPath);
const existingManifest = readJsonIfExists(existingPath) || buildDefaultManifest(version);
const distPath = path.join(rootDir, 'dist-electron');
if (fs.existsSync(distPath)) {
warnIfNoDescriptors(distPath);
}
const nextEntry = {
version,
feedUrl,
publishedAt: typeof args['published-at'] === 'string'
? args['published-at']
: new Date().toISOString(),
...(typeof args.notes === 'string' && args.notes.trim().length > 0
? { notes: args.notes.trim() }
: {})
};
const nextManifest = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
minimumServerVersion: normalizeVersion(args['minimum-server-version'])
|| normalizeVersion(existingManifest.minimumServerVersion)
|| version,
pollIntervalMinutes: Number.isFinite(Number(args['poll-interval-minutes']))
? Math.max(5, Math.round(Number(args['poll-interval-minutes'])))
: Math.max(5, Math.round(Number(existingManifest.pollIntervalMinutes || 30))),
versions: sortManifestVersions(
[...(Array.isArray(existingManifest.versions) ? existingManifest.versions : [])]
.filter((entry) => normalizeVersion(entry && entry.version) !== version)
.concat(nextEntry)
)
};
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
fs.writeFileSync(manifestPath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
console.log(`[release-manifest] Wrote ${manifestPath}`);
}
try {
main();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[release-manifest] ${message}`);
process.exitCode = 1;
}