204 lines
6.1 KiB
JavaScript
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();
|