archive-org-link-grabber/scripts/release-push.js

195 lines
6.7 KiB
JavaScript

#!/usr/bin/env node
/*
* Uploads the latest signed release artifacts in releases/<version>/ via FTP/FTPS using curl.
* Reads configuration from .env (auto-loaded) or process.env.
* Env vars:
* FTP_PROTOCOL=ftp|ftps (default: ftp)
* FTP_HOST=example.com (required)
* FTP_PORT=21 (optional)
* FTP_USER=username (required)
* FTP_PASS=password (required)
* FTP_REMOTE_DIR=/remote/path (required)
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const root = path.join(__dirname, '..');
// Auto-load .env if present
(() => {
try {
const envPath = path.join(root, '.env');
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, 'utf8');
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const cleaned = line.startsWith('export ')
? line.slice('export '.length).trim()
: line;
const eq = cleaned.indexOf('=');
if (eq === -1) continue;
const key = cleaned.slice(0, eq).trim();
let val = cleaned.slice(eq + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
if (!(key in process.env)) process.env[key] = val;
}
console.log('Loaded environment from .env');
}
} catch (e) {
console.warn('Warning: Failed to load .env:', e.message);
}
})();
function mask(str) {
if (!str) return str;
return '***';
}
function run(cmd, secrets = []) {
let shown = cmd;
for (const s of secrets) {
if (s) shown = shown.split(String(s)).join('***');
}
console.log('> ' + shown);
execSync(cmd, { stdio: 'inherit' });
}
const pkg = require(path.join(root, 'package.json'));
const manifest = require(path.join(root, 'manifest.json'));
const version = pkg.version;
if (!version) {
console.error('package.json is missing version');
process.exit(1);
}
const artifactsDir = path.join(root, 'releases', version);
if (!fs.existsSync(artifactsDir) || !fs.statSync(artifactsDir).isDirectory()) {
console.error(`Artifacts directory not found: ${artifactsDir}. Run npm run release:sign first.`);
process.exit(1);
}
const files = fs.readdirSync(artifactsDir).filter(f => fs.statSync(path.join(artifactsDir, f)).isFile());
const xpis = files.filter(f => f.toLowerCase().endsWith('.xpi'));
if (xpis.length === 0) {
console.error(`No .xpi artifacts found in ${artifactsDir}`);
process.exit(1);
}
const chosenXpi = xpis.sort()[0];
if (files.length === 0) {
console.error(`No files found in ${artifactsDir}`);
process.exit(1);
}
const protocol = (process.env.FTP_PROTOCOL || 'ftp').toLowerCase();
const host = process.env.FTP_HOST;
const port = process.env.FTP_PORT;
const user = process.env.FTP_USER;
const pass = process.env.FTP_PASS;
let remoteDir = process.env.FTP_REMOTE_DIR || '/';
if (!host || !user || !pass || !remoteDir) {
console.error('Missing FTP config. Required: FTP_HOST, FTP_USER, FTP_PASS, FTP_REMOTE_DIR');
process.exit(1);
}
// Normalize remoteDir
if (!remoteDir.startsWith('/')) remoteDir = '/' + remoteDir;
if (remoteDir.endsWith('/')) remoteDir = remoteDir.slice(0, -1);
// Construct base URL for FTP upload target
const baseUrl = `${protocol}://${host}${port ? `:${port}` : ''}${remoteDir}`;
// Prepare or update self-hosted updates.json before upload
function ensureUpdatesJson() {
try {
const updatesPath = path.join(root, 'releases', 'updates.json');
const examplePath = path.join(root, 'releases', 'updates.example.json');
let data;
if (fs.existsSync(updatesPath)) {
data = JSON.parse(fs.readFileSync(updatesPath, 'utf8'));
} else if (fs.existsSync(examplePath)) {
data = JSON.parse(fs.readFileSync(examplePath, 'utf8'));
} else {
data = { addons: {} };
}
const addonId = manifest?.applications?.gecko?.id;
const updateUrl = manifest?.applications?.gecko?.update_url;
if (!addonId || !updateUrl) {
console.warn('Missing add-on id or update_url in manifest; skipping updates.json generation');
return;
}
// Derive public base directory from update_url (strip trailing filename)
let updatesBaseDir = updateUrl.replace(/\/?[^/]*$/, '');
// Build public link to the uploaded XPI
const publicLink = `${updatesBaseDir}/releases/${version}/${encodeURIComponent(chosenXpi)}`;
if (!data.addons) data.addons = {};
if (!data.addons[addonId]) data.addons[addonId] = { updates: [] };
const entry = { version: String(version), update_link: publicLink };
// Try to preserve existing strict_min_version if present in latest entry
const existingUpdates = data.addons[addonId].updates || [];
let strictMin = '91.0';
for (const u of existingUpdates) {
if (u?.applications?.gecko?.strict_min_version) {
strictMin = u.applications.gecko.strict_min_version;
break;
}
}
entry.applications = { gecko: { strict_min_version: strictMin } };
// Replace or append the entry for this version
const idx = existingUpdates.findIndex(u => String(u.version) === String(version));
if (idx >= 0) existingUpdates[idx] = entry; else existingUpdates.push(entry);
data.addons[addonId].updates = existingUpdates;
fs.writeFileSync(updatesPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
console.log(`Prepared releases/updates.json for version ${version}`);
} catch (e) {
console.warn('Warning: Failed to update releases/updates.json:', e.message);
}
}
ensureUpdatesJson();
console.log(`Uploading ${files.length} file(s) from ${artifactsDir} to ${protocol}://${host}${port ? `:${port}` : ''}${remoteDir}/`);
for (const file of files) {
const localPath = path.join(artifactsDir, file);
const url = `${baseUrl}/${encodeURIComponent(file)}`;
// --ftp-create-dirs ensures remote dirs are created; --fail fails on server errors.
const cmd = [
'curl',
'--fail',
'--ftp-create-dirs',
`--user`, `${user}:${pass}`,
'--upload-file', JSON.stringify(localPath),
JSON.stringify(url),
].join(' ');
run(cmd, [user, pass]);
}
// Also upload updates.json (self-hosted updates manifest) alongside artifacts
const updatesPath = path.join(root, 'releases', 'updates.json');
if (fs.existsSync(updatesPath)) {
const updatesUrl = `${baseUrl}/updates.json`;
console.log(`Uploading updates.json to ${updatesUrl}`);
const cmd = [
'curl',
'--fail',
'--ftp-create-dirs',
`--user`, `${user}:${pass}`,
'--upload-file', JSON.stringify(updatesPath),
JSON.stringify(updatesUrl),
].join(' ');
run(cmd, [user, pass]);
} else {
console.warn('Warning: releases/updates.json not found; skipping upload.');
}
console.log('Upload complete.');