205 lines
5.1 KiB
JavaScript
205 lines
5.1 KiB
JavaScript
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);
|
|
});
|