Files
TrueGrowth/scripts/prepare-desktop-update-manifest.mjs
jiam 5119ac0ef8
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Sync latest TrueGrowth updates
2026-07-08 02:03:18 +08:00

204 lines
6.1 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import yaml from 'js-yaml';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const workspaceRoot = path.resolve(__dirname, '..');
const packageJson = JSON.parse(
fs.readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8')
);
const releaseDirArg = process.argv
.find((arg) => arg.startsWith('--release-dir='))
?.slice('--release-dir='.length);
const releaseDir = releaseDirArg
? path.resolve(workspaceRoot, releaseDirArg)
: path.join(workspaceRoot, 'dist/desktop/release');
const channelUrl =
process.env.TRUEGROWTH_UPDATE_FEED_URL ||
'https://truegrowth.benchu.cloud/desktop/stable/';
const channelHost = new URL(channelUrl).host;
const platformArg =
process.argv
.find((arg) => arg.startsWith('--platform='))
?.slice('--platform='.length) || 'all';
function readFileIfExists(filePath) {
return fs.existsSync(filePath) ? fs.readFileSync(filePath) : null;
}
function sha256(filePath) {
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(filePath));
return hash.digest('hex');
}
function statArtifact(filePath) {
const stats = fs.statSync(filePath);
return {
name: path.basename(filePath),
size: stats.size,
sha256: sha256(filePath),
url: new URL(path.basename(filePath), channelUrl).toString(),
};
}
function readYamlMetadata(fileName) {
const filePath = path.join(releaseDir, fileName);
const content = readFileIfExists(filePath);
if (!content) {
return null;
}
return yaml.load(content.toString('utf8'));
}
function assertMetadataUrl(metadata, fileName) {
if (!metadata) {
throw new Error(`Missing required update metadata: ${fileName}`);
}
const files = Array.isArray(metadata.files) ? metadata.files : [];
const urls = [metadata.path, ...files.map((file) => file?.url)].filter(
Boolean
);
if (!urls.length) {
throw new Error(`${fileName} does not contain any update file URLs.`);
}
for (const url of urls) {
const parsed = new URL(String(url), channelUrl);
if (parsed.host !== channelHost) {
throw new Error(
`${fileName} points to ${parsed.host}; expected ${channelHost}.`
);
}
}
}
function listReleaseArtifacts() {
if (!fs.existsSync(releaseDir)) {
throw new Error(
`Desktop release directory is missing: ${releaseDir}\nRun pnpm desktop:build first.`
);
}
const escapedVersion = packageJson.version.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
const macArtifactPattern = new RegExp(
`^TrueGrowth-${escapedVersion}-(arm64|x64)(-mac\\.zip|\\.dmg)(\\.blockmap)?$`,
'i'
);
const winArtifactPattern = new RegExp(
`^TrueGrowth-${escapedVersion}-x64-(Setup|Portable)\\.exe(\\.blockmap)?$`,
'i'
);
return fs
.readdirSync(releaseDir)
.filter((name) => {
const isMacMetadata = name === 'latest-mac.yml';
const isWinMetadata = name === 'latest.yml';
const isMacArtifact = macArtifactPattern.test(name);
const isWinArtifact = winArtifactPattern.test(name);
if (platformArg === 'mac') {
return isMacMetadata || isMacArtifact;
}
if (platformArg === 'win') {
return isWinMetadata || isWinArtifact;
}
return isMacMetadata || isWinMetadata || isMacArtifact || isWinArtifact;
})
.sort();
}
function classifyArtifact(name) {
if (name === 'latest.yml') {
return 'windows-update-metadata';
}
if (name === 'latest-mac.yml') {
return 'mac-update-metadata';
}
if (/\.dmg$/i.test(name)) {
return 'mac-installer';
}
if (/-mac\.zip$/i.test(name)) {
return 'mac-auto-update';
}
if (/-Setup\.exe$/i.test(name)) {
return 'windows-installer';
}
if (/-Portable\.exe$/i.test(name)) {
return 'windows-portable-manual';
}
if (/\.blockmap$/i.test(name)) {
return 'differential-update';
}
return 'release-file';
}
function writeReleaseManifest(artifacts) {
const generatedAt = new Date().toISOString();
const releaseFiles = artifacts.map((name) => {
const filePath = path.join(releaseDir, name);
const base = statArtifact(filePath);
return {
...base,
role: classifyArtifact(name),
};
});
const manifest = {
app: 'TrueGrowth',
version: packageJson.version,
channel: 'stable',
channelUrl,
generatedAt,
files: releaseFiles,
cachePolicy: {
metadata: ['latest.yml', 'latest-mac.yml', 'releases.json'],
metadataHeader: 'Cache-Control: no-store, no-cache, must-revalidate',
artifactsHeader:
'Cache-Control: public, max-age=31536000, immutable',
},
onePanelUploadRoot: '/desktop/stable/',
signingGate: {
macos: 'Developer ID signed and notarized before customer release',
windows: 'Authenticode signed before customer release',
unsignedBuilds: 'internal testing only',
},
};
const outputPath = path.join(releaseDir, 'releases.json');
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
return manifest;
}
function printUploadGuide(manifest) {
console.log('\n[desktop:update] Upload these files to 1Panel static root:');
for (const file of manifest.files) {
console.log(`- ${file.name} -> ${manifest.onePanelUploadRoot}${file.name}`);
}
if (!manifest.files.some((file) => file.name === 'releases.json')) {
console.log('- releases.json -> /desktop/stable/releases.json');
}
console.log('\n[desktop:update] Nginx cache guidance:');
console.log(
'- latest.yml, latest-mac.yml, releases.json: Cache-Control no-store'
);
console.log('- installers, zip, blockmap: long cache with immutable');
console.log(`\n[desktop:update] Channel: ${manifest.channelUrl}`);
}
function main() {
const artifacts = listReleaseArtifacts();
const latest = readYamlMetadata('latest.yml');
const latestMac = readYamlMetadata('latest-mac.yml');
if (platformArg === 'all' || platformArg === 'win') {
assertMetadataUrl(latest, 'latest.yml');
}
if (platformArg === 'all' || platformArg === 'mac') {
assertMetadataUrl(latestMac, 'latest-mac.yml');
}
const manifest = writeReleaseManifest(artifacts);
printUploadGuide(manifest);
}
main();