const fs = require('fs/promises'); const path = require('path'); const { spawn } = require('child_process'); const rootDir = path.resolve(__dirname, '..'); const artifactsDir = path.join(rootDir, 'artifacts', 'releases'); const registry = process.env.IMAGE_REGISTRY || 'registry.internal.budgethost.io'; const namespace = process.env.IMAGE_NAMESPACE || 'skipper'; const channel = process.env.RELEASE_CHANNEL || 'stable'; function getAction() { const action = process.argv[2] || 'print'; if (!['print', 'plan', 'build', 'publish'].includes(action)) { throw new Error(`Unsupported action: ${action}`); } return action; } function timestampUtc() { return new Date().toISOString(); } function compactTimestamp(value) { return value.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z'); } function sanitizeTagPart(value) { return String(value || '') .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-') .replace(/^-+|-+$/g, '') || 'unknown'; } function spawnCapture(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: rootDir, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', reject); child.on('close', (code) => { if (code === 0) { resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); return; } reject(new Error(stderr.trim() || `${command} ${args.join(' ')} failed with exit code ${code}`)); }); }); } function spawnInherited(command, args, env) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: { ...process.env, ...env, }, }); child.on('error', reject); child.on('close', (code) => { if (code === 0) { resolve(); return; } reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}`)); }); }); } async function detectGitSha() { try { const result = await spawnCapture('git', ['rev-parse', '--short', 'HEAD']); return result.stdout || null; } catch (error) { return null; } } async function buildReleaseMetadata() { const createdAt = timestampUtc(); const gitSha = sanitizeTagPart(process.env.RELEASE_GIT_SHA || (await detectGitSha()) || 'nogit'); const versionTag = sanitizeTagPart( process.env.RELEASE_VERSION_TAG || `${channel}-${gitSha}-${compactTimestamp(createdAt)}` ); const images = [ { id: 'skipper-api', repository: 'skipper-api', image: `${registry}/${namespace}/skipper-api:${versionTag}`, latest_image: `${registry}/${namespace}/skipper-api:latest`, }, { id: 'skippy-agent', repository: 'skippy-agent', image: `${registry}/${namespace}/skippy-agent:${versionTag}`, latest_image: `${registry}/${namespace}/skippy-agent:latest`, }, ]; return { schema_version: 'v1', created_at: createdAt, registry, namespace, channel, git_sha: gitSha, version_tag: versionTag, images, compose: { files: ['docker-compose.yml', 'docker-compose.registry.yml'], image_tag: versionTag, }, }; } async function writeManifest(metadata) { await fs.mkdir(artifactsDir, { recursive: true }); const versionPath = path.join(artifactsDir, `${metadata.version_tag}.json`); const latestPath = path.join(artifactsDir, 'latest.json'); const payload = JSON.stringify(metadata, null, 2); await fs.writeFile(versionPath, payload); await fs.writeFile(latestPath, payload); return { versionPath, latestPath, }; } async function runImagesRelease(mode, metadata) { const scriptName = mode === 'build' ? 'images:build' : 'images:release'; await spawnInherited('npm', ['run', scriptName], { IMAGE_REGISTRY: metadata.registry, IMAGE_NAMESPACE: metadata.namespace, IMAGE_TAG: metadata.version_tag, }); } function printMetadata(metadata, manifestPaths) { console.log(`channel: ${metadata.channel}`); console.log(`git_sha: ${metadata.git_sha}`); console.log(`version_tag: ${metadata.version_tag}`); for (const image of metadata.images) { console.log(`${image.id}: ${image.image}`); console.log(`${image.id}: ${image.latest_image}`); } if (manifestPaths) { console.log(`manifest: ${manifestPaths.versionPath}`); console.log(`manifest_latest: ${manifestPaths.latestPath}`); } } async function main() { const action = getAction(); const metadata = await buildReleaseMetadata(); if (action === 'print') { printMetadata(metadata); return; } const manifestPaths = await writeManifest(metadata); if (action === 'plan') { printMetadata(metadata, manifestPaths); return; } if (process.env.RUN_SMOKE_TEST === '1') { await spawnInherited('npm', ['run', 'smoke:test']); } await runImagesRelease(action, metadata); printMetadata(metadata, manifestPaths); } main().catch((error) => { console.error(error.message); process.exit(1); });