Sync latest TrueGrowth updates
This commit is contained in:
417
scripts/desktop-signing.mjs
Normal file
417
scripts/desktop-signing.mjs
Normal file
@@ -0,0 +1,417 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = path.resolve(__dirname, '..');
|
||||
const defaultEnvPath = path.join(workspaceRoot, '.env.desktop-signing.local');
|
||||
const secretKeys = new Set([
|
||||
'APPLE_APP_SPECIFIC_PASSWORD',
|
||||
'CSC_KEY_PASSWORD',
|
||||
'WIN_CSC_KEY_PASSWORD',
|
||||
]);
|
||||
|
||||
function expandHome(filePath) {
|
||||
if (!filePath) return filePath;
|
||||
const cleanPath = filePath.startsWith('file://')
|
||||
? filePath.slice('file://'.length)
|
||||
: filePath;
|
||||
if (cleanPath === '~') {
|
||||
return process.env.HOME || cleanPath;
|
||||
}
|
||||
if (cleanPath.startsWith('~/')) {
|
||||
return path.join(process.env.HOME || '', cleanPath.slice(2));
|
||||
}
|
||||
return cleanPath;
|
||||
}
|
||||
|
||||
function parseEnvValue(rawValue) {
|
||||
let value = rawValue.trim();
|
||||
const quote = value[0];
|
||||
if (
|
||||
value.length >= 2 &&
|
||||
(quote === '"' || quote === "'") &&
|
||||
value[value.length - 1] === quote
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
if (quote === '"') {
|
||||
value = value.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { loaded: false, values: {} };
|
||||
}
|
||||
const values = {};
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid env line in ${path.relative(workspaceRoot, filePath)}:${index + 1}`
|
||||
);
|
||||
}
|
||||
values[match[1]] = parseEnvValue(match[2]);
|
||||
}
|
||||
return { loaded: true, values };
|
||||
}
|
||||
|
||||
function mergeEnv(envFilePath) {
|
||||
const { loaded, values } = loadEnvFile(envFilePath);
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (process.env[key] == null || process.env[key] === '') {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
function readArgValue(args, name, fallback) {
|
||||
const prefix = `${name}=`;
|
||||
const match = args.find((arg) => arg.startsWith(prefix));
|
||||
return match ? match.slice(prefix.length) : fallback;
|
||||
}
|
||||
|
||||
function flag(args, name) {
|
||||
return args.includes(name);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: workspaceRoot,
|
||||
encoding: 'utf8',
|
||||
env: process.env,
|
||||
stdio: options.stdio || 'pipe',
|
||||
timeout: options.timeout || 0,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runRequired(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: workspaceRoot,
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function maskEnvValue(key, value) {
|
||||
if (!value) return '<empty>';
|
||||
return secretKeys.has(key) ? '<set>' : value;
|
||||
}
|
||||
|
||||
function certificateSubjectsFromP12(p12Path) {
|
||||
const result = run('openssl', [
|
||||
'pkcs12',
|
||||
'-in',
|
||||
p12Path,
|
||||
'-nokeys',
|
||||
'-clcerts',
|
||||
'-passin',
|
||||
'env:CSC_KEY_PASSWORD',
|
||||
'-info',
|
||||
]);
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
output: [result.stdout, result.stderr].filter(Boolean).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function codesigningIdentities() {
|
||||
const result = run('security', ['find-identity', '-v', '-p', 'codesigning']);
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
output: [result.stdout, result.stderr].filter(Boolean).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function checkTool(command, args) {
|
||||
try {
|
||||
const result = run(command, args, { timeout: 5000 });
|
||||
return result.status === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function findXcodeDeveloperDir() {
|
||||
try {
|
||||
const result = run('xcode-select', ['-p'], { timeout: 5000 });
|
||||
if (result.status === 0) {
|
||||
const developerDir = result.stdout.trim();
|
||||
return developerDir || '';
|
||||
}
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function hasXcodeTool(toolName) {
|
||||
const developerDir = findXcodeDeveloperDir();
|
||||
if (developerDir) {
|
||||
const directTool = path.join(developerDir, 'usr/bin', toolName);
|
||||
if (fs.existsSync(directTool)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return checkTool('xcrun', ['--find', toolName]);
|
||||
}
|
||||
|
||||
function hasCompleteApiKeyLogin() {
|
||||
return Boolean(
|
||||
process.env.APPLE_API_KEY &&
|
||||
process.env.APPLE_API_KEY_ID &&
|
||||
process.env.APPLE_API_ISSUER
|
||||
);
|
||||
}
|
||||
|
||||
function hasPartialApiKeyLogin() {
|
||||
return Boolean(
|
||||
process.env.APPLE_API_KEY ||
|
||||
process.env.APPLE_API_KEY_ID ||
|
||||
process.env.APPLE_API_ISSUER
|
||||
);
|
||||
}
|
||||
|
||||
function hasCompleteAppleIdLogin() {
|
||||
return Boolean(
|
||||
process.env.APPLE_ID &&
|
||||
process.env.APPLE_APP_SPECIFIC_PASSWORD &&
|
||||
process.env.APPLE_TEAM_ID
|
||||
);
|
||||
}
|
||||
|
||||
function hasPartialAppleIdLogin() {
|
||||
return Boolean(
|
||||
process.env.APPLE_ID ||
|
||||
process.env.APPLE_APP_SPECIFIC_PASSWORD ||
|
||||
process.env.APPLE_TEAM_ID
|
||||
);
|
||||
}
|
||||
|
||||
function checkSigning({ requireNotarization = true, quiet = false } = {}) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const details = [];
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
errors.push('macOS signed builds must run on macOS.');
|
||||
}
|
||||
|
||||
const cscLink = process.env.CSC_LINK ? expandHome(process.env.CSC_LINK) : '';
|
||||
const cscName = process.env.CSC_NAME || '';
|
||||
if (cscLink) {
|
||||
process.env.CSC_LINK = cscLink;
|
||||
if (!fs.existsSync(cscLink)) {
|
||||
errors.push(`CSC_LINK does not exist: ${cscLink}`);
|
||||
} else {
|
||||
details.push(`CSC_LINK=${cscLink}`);
|
||||
if (!process.env.CSC_KEY_PASSWORD) {
|
||||
errors.push('CSC_KEY_PASSWORD is required when CSC_LINK points to a .p12 file.');
|
||||
} else {
|
||||
const certInfo = certificateSubjectsFromP12(cscLink);
|
||||
if (!certInfo.ok) {
|
||||
errors.push('Unable to read CSC_LINK .p12. Check the certificate password.');
|
||||
} else if (!/Developer ID Application:/i.test(certInfo.output)) {
|
||||
errors.push(
|
||||
'CSC_LINK is not a "Developer ID Application" certificate. It cannot be used for direct customer distribution outside the Mac App Store.'
|
||||
);
|
||||
} else {
|
||||
const subject =
|
||||
certInfo.output.match(/subject=.*Developer ID Application:[^\n]+/i)?.[0] ||
|
||||
'Developer ID Application certificate found in CSC_LINK';
|
||||
details.push(subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const identities = codesigningIdentities();
|
||||
if (!identities.ok) {
|
||||
errors.push('Unable to inspect macOS code signing identities with security.');
|
||||
} else if (!/Developer ID Application:/i.test(identities.output)) {
|
||||
errors.push(
|
||||
'No valid "Developer ID Application" identity is available. Set CSC_LINK to a Developer ID .p12 or import one into Keychain.'
|
||||
);
|
||||
} else {
|
||||
details.push('Developer ID Application identity found in Keychain.');
|
||||
}
|
||||
if (cscName) {
|
||||
details.push(`CSC_NAME=${cscName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const appleApiKey = process.env.APPLE_API_KEY
|
||||
? expandHome(process.env.APPLE_API_KEY)
|
||||
: '';
|
||||
if (appleApiKey) {
|
||||
process.env.APPLE_API_KEY = appleApiKey;
|
||||
}
|
||||
if (appleApiKey && !fs.existsSync(appleApiKey)) {
|
||||
errors.push(`APPLE_API_KEY does not exist: ${appleApiKey}`);
|
||||
}
|
||||
|
||||
if (requireNotarization) {
|
||||
if (hasCompleteApiKeyLogin()) {
|
||||
details.push(`APPLE_API_KEY=${appleApiKey}`);
|
||||
details.push(`APPLE_API_KEY_ID=${process.env.APPLE_API_KEY_ID}`);
|
||||
if (process.env.APPLE_TEAM_ID) {
|
||||
details.push(`APPLE_TEAM_ID=${process.env.APPLE_TEAM_ID}`);
|
||||
}
|
||||
} else if (hasCompleteAppleIdLogin()) {
|
||||
warnings.push(
|
||||
'Apple ID notarization is configured. The App Store Connect API key method is recommended for repeatable production builds.'
|
||||
);
|
||||
details.push(`APPLE_ID=${process.env.APPLE_ID}`);
|
||||
details.push(`APPLE_TEAM_ID=${process.env.APPLE_TEAM_ID}`);
|
||||
} else {
|
||||
if (hasPartialApiKeyLogin()) {
|
||||
errors.push(
|
||||
'Notarization API key config is incomplete. Set APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER.'
|
||||
);
|
||||
} else if (hasPartialAppleIdLogin()) {
|
||||
errors.push(
|
||||
'Apple ID notarization config is incomplete. Set APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID.'
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
'Notarization credentials are missing. Use APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER for production macOS builds.'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!hasXcodeTool('notarytool')) {
|
||||
errors.push('xcrun notarytool is unavailable. Install or select Xcode command line tools.');
|
||||
}
|
||||
if (!hasXcodeTool('stapler')) {
|
||||
warnings.push('xcrun stapler is unavailable. Notarization may succeed, but ticket validation/stapling checks will be limited.');
|
||||
}
|
||||
} else {
|
||||
warnings.push('TRUEGROWTH_NOTARIZE=false: this build can be signed, but it is not a full commercial macOS release.');
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
console.log('[desktop:signing] macOS signing inputs');
|
||||
for (const key of [
|
||||
'CSC_LINK',
|
||||
'CSC_NAME',
|
||||
'APPLE_API_KEY',
|
||||
'APPLE_API_KEY_ID',
|
||||
'APPLE_API_ISSUER',
|
||||
'APPLE_ID',
|
||||
'APPLE_TEAM_ID',
|
||||
'TRUEGROWTH_MAC_TARGETS',
|
||||
'TRUEGROWTH_NOTARIZE',
|
||||
]) {
|
||||
if (process.env[key]) {
|
||||
console.log(` ${key}=${maskEnvValue(key, process.env[key])}`);
|
||||
}
|
||||
}
|
||||
for (const detail of details) {
|
||||
console.log(` ok: ${detail}`);
|
||||
}
|
||||
for (const warning of warnings) {
|
||||
console.warn(` warn: ${warning}`);
|
||||
}
|
||||
for (const error of errors) {
|
||||
console.error(` error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: errors.length === 0, errors, warnings, details };
|
||||
}
|
||||
|
||||
function resolveMacTargets(args) {
|
||||
const fromArg = readArgValue(args, '--targets', '');
|
||||
const fromEnv = process.env.TRUEGROWTH_MAC_TARGETS || '';
|
||||
const raw = fromArg || fromEnv || 'zip';
|
||||
return raw
|
||||
.split(/[,\s]+/)
|
||||
.map((target) => target.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildMac(args) {
|
||||
const envFile = readArgValue(args, '--env', defaultEnvPath);
|
||||
const envLoaded = mergeEnv(envFile);
|
||||
const requireNotarization = process.env.TRUEGROWTH_NOTARIZE !== 'false';
|
||||
const check = checkSigning({ requireNotarization });
|
||||
if (!check.ok) {
|
||||
throw new Error('macOS signing preflight failed; fix the errors above before building a customer release.');
|
||||
}
|
||||
const targets = resolveMacTargets(args);
|
||||
const builderArgs = [
|
||||
'exec',
|
||||
'electron-builder',
|
||||
'--config',
|
||||
'electron-builder.yml',
|
||||
'--mac',
|
||||
...targets,
|
||||
'--arm64',
|
||||
'-c.mac.forceCodeSigning=true',
|
||||
];
|
||||
if (process.env.APPLE_TEAM_ID) {
|
||||
builderArgs.push(`-c.mac.notarize.teamId=${process.env.APPLE_TEAM_ID}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[desktop:signing] Loaded ${envLoaded ? envFile : 'process environment'}`
|
||||
);
|
||||
console.log(`[desktop:signing] Building macOS targets: ${targets.join(', ')}`);
|
||||
runRequired('pnpm', ['build:web']);
|
||||
runRequired('pnpm', ['desktop:prepare']);
|
||||
runRequired('pnpm', builderArgs);
|
||||
runRequired('pnpm', ['desktop:release-manifest', '--', '--platform=mac']);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage:
|
||||
node scripts/desktop-signing.mjs check [--env=.env.desktop-signing.local] [--no-notarize]
|
||||
node scripts/desktop-signing.mjs build-mac [--env=.env.desktop-signing.local] [--targets=zip|dmg,zip]
|
||||
`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [command = 'help', ...args] = process.argv.slice(2);
|
||||
const envFile = readArgValue(args, '--env', defaultEnvPath);
|
||||
if (command === 'help' || flag(args, '--help')) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
if (command === 'check') {
|
||||
const loaded = mergeEnv(envFile);
|
||||
const requireNotarization =
|
||||
!flag(args, '--no-notarize') && process.env.TRUEGROWTH_NOTARIZE !== 'false';
|
||||
if (!loaded && !process.env.CSC_LINK && !process.env.CSC_NAME) {
|
||||
console.warn(
|
||||
`[desktop:signing] ${path.relative(workspaceRoot, envFile)} was not found; checking process environment only.`
|
||||
);
|
||||
}
|
||||
const result = checkSigning({ requireNotarization });
|
||||
if (!result.ok) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (command === 'build-mac') {
|
||||
buildMac(args);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -17,7 +17,7 @@ const sourceIcon = path.join(
|
||||
const requiredWebFiles = [
|
||||
path.join(workspaceRoot, 'dist/apps/web/index.html'),
|
||||
];
|
||||
const runtimeDependencies = ['croner', 'p-queue', 'xstate'];
|
||||
const runtimeDependencies = ['croner', 'electron-updater', 'p-queue', 'xstate'];
|
||||
|
||||
function copyIfExists(from, to, options = {}) {
|
||||
if (!fs.existsSync(from)) {
|
||||
|
||||
203
scripts/prepare-desktop-update-manifest.mjs
Normal file
203
scripts/prepare-desktop-update-manifest.mjs
Normal file
@@ -0,0 +1,203 @@
|
||||
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();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,7 @@ function publishingWorkflowFixture(id) {
|
||||
inputs: [{ id: 'video', label: '视频', type: 'video' }],
|
||||
outputs: [{ id: 'video', label: '成片', type: 'video' }],
|
||||
config: {
|
||||
providerId: 'funclip-local',
|
||||
providerId: 'videoagent-local',
|
||||
operations: ['asr', 'highlight-cut', 'subtitle', 'ffmpeg-export'],
|
||||
},
|
||||
},
|
||||
@@ -278,7 +278,7 @@ async function main() {
|
||||
capability: 'clip.compose',
|
||||
inputs: [],
|
||||
outputs: [{ id: 'video', label: '成片', type: 'video' }],
|
||||
config: { providerId: 'funclip-local', prompt: '生成字幕成片' },
|
||||
config: { providerId: 'videoagent-local', prompt: '生成字幕成片' },
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
|
||||
Reference in New Issue
Block a user