Files
lisglosips/scripts/build-release-artifact.mjs
T

175 lines
7.8 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', 'packages/auth/vendor/argon2id', 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-b/s56', 'infra/server-b/s56', true],
['infra/server-b/s57', 'infra/server-b/s57', 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],
['scripts/import-number-library.mjs', 'scripts/import-number-library.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}`);