172 lines
7.6 KiB
JavaScript
172 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createHash } from 'node:crypto';
|
|
import { existsSync } from 'node:fs';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const root = process.cwd();
|
|
const args = process.argv.slice(2);
|
|
|
|
function argValue(name) {
|
|
const index = args.indexOf(name);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
}
|
|
|
|
function hasArg(name) {
|
|
return args.includes(name);
|
|
}
|
|
|
|
if (hasArg('--help') || hasArg('-h')) {
|
|
console.log(`Usage: node scripts/build-release-artifact.mjs --release-id <id> [--out-dir dist/releases] [--skip-build] [--check] [--allow-non-linux]`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const releaseId = argValue('--release-id') || process.env.RELEASE_ID;
|
|
if (!releaseId || !/^[A-Za-z0-9._-]+$/.test(releaseId)) {
|
|
throw new Error('Missing or invalid release id. Use --release-id <letters-numbers-dot-underscore-dash>.');
|
|
}
|
|
|
|
const outDir = path.resolve(root, argValue('--out-dir') || 'dist/releases');
|
|
const stagingDir = path.resolve(root, '.release-artifacts', 'staging', releaseId);
|
|
const checkOnly = hasArg('--check');
|
|
const skipBuild = hasArg('--skip-build');
|
|
const allowNonLinux = hasArg('--allow-non-linux');
|
|
|
|
const copyEntries = [
|
|
['package.json', 'package.json', true],
|
|
['pnpm-lock.yaml', 'pnpm-lock.yaml', true],
|
|
['node_modules', 'node_modules', true],
|
|
['prisma/schema.prisma', 'prisma/schema.prisma', true],
|
|
['prisma/migrations', 'prisma/migrations', true],
|
|
['apps/api/package.json', 'apps/api/package.json', true],
|
|
['apps/api/dist', 'apps/api/dist', true],
|
|
['apps/api/node_modules', 'apps/api/node_modules', true],
|
|
['apps/web/package.json', 'apps/web/package.json', true],
|
|
['apps/web/dist', 'public', true],
|
|
['apps/web/node_modules', 'apps/web/node_modules', true],
|
|
['apps/worker-cdr/package.json', 'apps/worker-cdr/package.json', true],
|
|
['apps/worker-cdr/dist', 'apps/worker-cdr/dist', true],
|
|
['apps/worker-cdr/node_modules', 'apps/worker-cdr/node_modules', true],
|
|
['apps/worker-recording/package.json', 'apps/worker-recording/package.json', true],
|
|
['apps/worker-recording/dist', 'apps/worker-recording/dist', true],
|
|
['apps/worker-recording/node_modules', 'apps/worker-recording/node_modules', true],
|
|
['apps/worker-config-publisher/package.json', 'apps/worker-config-publisher/package.json', true],
|
|
['apps/worker-config-publisher/dist', 'apps/worker-config-publisher/dist', true],
|
|
['apps/worker-config-publisher/node_modules', 'apps/worker-config-publisher/node_modules', true],
|
|
['packages/auth/package.json', 'packages/auth/package.json', true],
|
|
['packages/auth/dist', 'packages/auth/dist', true],
|
|
['packages/auth/vendor/argon2id/dist', 'packages/auth/vendor/argon2id/dist', true],
|
|
['packages/contracts/package.json', 'packages/contracts/package.json', true],
|
|
['packages/contracts/dist', 'packages/contracts/dist', true],
|
|
['packages/database/package.json', 'packages/database/package.json', true],
|
|
['packages/database/dist', 'packages/database/dist', true],
|
|
['packages/database/node_modules', 'packages/database/node_modules', true],
|
|
['packages/domain/package.json', 'packages/domain/package.json', true],
|
|
['packages/domain/dist', 'packages/domain/dist', true],
|
|
['packages/observability/package.json', 'packages/observability/package.json', true],
|
|
['packages/observability/dist', 'packages/observability/dist', true],
|
|
['packages/observability/node_modules', 'packages/observability/node_modules', true],
|
|
['packages/redis/package.json', 'packages/redis/package.json', true],
|
|
['packages/redis/dist', 'packages/redis/dist', true],
|
|
['packages/redis/node_modules', 'packages/redis/node_modules', true],
|
|
['infra/server-b/s30', 'infra/server-b/s30', true],
|
|
['infra/server-a/s28/lisglosips_hotpath.lua', 'infra/server-a/s28/lisglosips_hotpath.lua', true],
|
|
['scripts/phase2-gateway-migration.mjs', 'scripts/phase2-gateway-migration.mjs', true]
|
|
];
|
|
|
|
function run(command, commandArgs, options = {}) {
|
|
const result = spawnSync(command, commandArgs, { cwd: root, stdio: options.stdio || 'pipe', encoding: 'utf8' });
|
|
if (result.status !== 0) {
|
|
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
throw new Error(`${command} ${commandArgs.join(' ')} failed\n${output}`);
|
|
}
|
|
return (result.stdout || '').trim();
|
|
}
|
|
|
|
async function sha256(filePath) {
|
|
const hash = createHash('sha256');
|
|
hash.update(await fs.readFile(filePath));
|
|
return hash.digest('hex');
|
|
}
|
|
|
|
async function copyEntry(source, target) {
|
|
const sourcePath = path.resolve(root, source);
|
|
const targetPath = path.resolve(stagingDir, target);
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
await fs.rm(targetPath, { recursive: true, force: true });
|
|
if (process.platform === 'linux') {
|
|
run('cp', ['-a', sourcePath, targetPath]);
|
|
return;
|
|
}
|
|
await fs.cp(sourcePath, targetPath, { recursive: true, verbatimSymlinks: true });
|
|
}
|
|
|
|
async function collectFiles(dir, prefixPath = '') {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
const relative = path.join(prefixPath, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...await collectFiles(dir, relative));
|
|
} else if (entry.isFile()) {
|
|
files.push(relative.replaceAll(path.sep, '/'));
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
if (process.platform !== 'linux' && !allowNonLinux && !checkOnly) {
|
|
throw new Error('Release artifacts should be built on Linux for B-server runtime compatibility. Re-run with --allow-non-linux only for a local inspection artifact.');
|
|
}
|
|
|
|
if (checkOnly) {
|
|
const missing = copyEntries.filter(([source, , required]) => required && !existsSync(path.resolve(root, source)));
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required release paths:\n${missing.map(([source]) => `- ${source}`).join('\n')}`);
|
|
}
|
|
console.log(`Release artifact check passed for ${releaseId}. ${copyEntries.length} required entries are present.`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (!skipBuild) {
|
|
run(process.execPath, ['scripts/pnpm-run.mjs', '-r', 'build'], { stdio: 'inherit' });
|
|
}
|
|
|
|
const missing = copyEntries.filter(([source, , required]) => required && !existsSync(path.resolve(root, source)));
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required release paths:\n${missing.map(([source]) => `- ${source}`).join('\n')}`);
|
|
}
|
|
|
|
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
await fs.mkdir(stagingDir, { recursive: true });
|
|
for (const [source, target] of copyEntries) {
|
|
await copyEntry(source, target);
|
|
}
|
|
|
|
const manifest = {
|
|
releaseId,
|
|
createdAt: new Date().toISOString(),
|
|
gitCommit: run('git', ['rev-parse', 'HEAD']),
|
|
gitStatus: run('git', ['status', '--short', '--', '.', ':(exclude).release-artifacts']).split('\n').filter(Boolean),
|
|
node: process.version,
|
|
pnpm: run(process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm', ['--version']),
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
requiredEntries: copyEntries.map(([source, target]) => ({ source, target }))
|
|
};
|
|
|
|
await fs.writeFile(path.join(stagingDir, 'RELEASE_MANIFEST.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
await fs.mkdir(outDir, { recursive: true });
|
|
const tarPath = path.join(outDir, `${releaseId}.tar.gz`);
|
|
run('tar', ['-czf', tarPath, '-C', stagingDir, '.'], { stdio: 'inherit' });
|
|
const fileCount = process.platform === 'linux'
|
|
? Number(run('bash', ['-lc', `find ${JSON.stringify(stagingDir)} -type f | wc -l`]))
|
|
: (await collectFiles(stagingDir)).length;
|
|
const artifactHash = await sha256(tarPath);
|
|
await fs.writeFile(path.join(outDir, `${releaseId}.sha256`), `${artifactHash} ${path.basename(tarPath)}\n`, 'utf8');
|
|
|
|
console.log(`Release artifact created: ${tarPath}`);
|
|
console.log(`SHA256: ${artifactHash}`);
|
|
console.log(`Files staged: ${fileCount}`);
|