Initial commit
This commit is contained in:
204
scripts/release.js
Normal file
204
scripts/release.js
Normal file
@@ -0,0 +1,204 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user