125 lines
7.1 KiB
JavaScript
Executable File
125 lines
7.1 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
function option(name) {
|
|
const index = process.argv.indexOf(name);
|
|
if (index === -1) return null;
|
|
const value = process.argv[index + 1];
|
|
if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
return value;
|
|
}
|
|
|
|
function record(value, path) {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
|
return value;
|
|
}
|
|
|
|
function required(value, path) {
|
|
if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${path} must be a non-empty single-line string`);
|
|
return value;
|
|
}
|
|
|
|
function safeRelativePath(value, path) {
|
|
const result = required(value, path);
|
|
if (result.startsWith("/") || result.split("/").includes("..")) throw new Error(`${path} must be a safe relative path`);
|
|
return result;
|
|
}
|
|
|
|
function stringArray(value, path) {
|
|
if (!Array.isArray(value) || value.length === 0) throw new Error(`${path} must be a non-empty string array`);
|
|
return value.map((item, index) => safeRelativePath(item, `${path}[${index}]`));
|
|
}
|
|
|
|
function run(command, args, cwd, allowFailure = false) {
|
|
const result = spawnSync(command, args, { cwd, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
|
|
if (result.error !== undefined) throw result.error;
|
|
if (result.status !== 0 && !allowFailure) {
|
|
const detail = `${result.stderr || result.stdout || "command failed"}`.trim().slice(-2000);
|
|
throw new Error(`${command} ${args.join(" ")} failed (${result.status}): ${detail}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function valueAt(root, reference) {
|
|
return reference.split(".").reduce((value, key) => record(value, reference)[key], root);
|
|
}
|
|
|
|
function writeValue(outputDir, name, value) {
|
|
writeFileSync(resolve(outputDir, name), `${value}\n`, { encoding: "utf8", mode: 0o644 });
|
|
}
|
|
|
|
function main() {
|
|
const configPath = option("--config") ?? "config/unidesk-host-k8s.yaml";
|
|
const sourceRoot = resolve(option("--source-root") ?? process.cwd());
|
|
const sourceCommit = required(option("--source-commit"), "--source-commit");
|
|
const sourceSnapshotPrefix = required(option("--source-snapshot-prefix"), "--source-snapshot-prefix");
|
|
const outputDir = resolve(required(option("--output-dir"), "--output-dir"));
|
|
if (!/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error("--source-commit must be a full Git commit SHA");
|
|
if (run("git", ["check-ref-format", sourceSnapshotPrefix], sourceRoot, true).status !== 0) throw new Error("--source-snapshot-prefix must be a valid Git ref prefix");
|
|
const config = record(Bun.YAML.parse(readFileSync(configPath, "utf8")), configPath);
|
|
const delivery = record(config.delivery, "delivery");
|
|
const serviceRef = required(delivery.serviceRef, "delivery.serviceRef");
|
|
const service = record(valueAt(config, serviceRef), serviceRef);
|
|
const changeDetection = record(delivery.changeDetection, "delivery.changeDetection");
|
|
const runtimePaths = stringArray(changeDetection.runtimePaths, "delivery.changeDetection.runtimePaths");
|
|
const build = record(delivery.build, "delivery.build");
|
|
const proxy = record(build.proxy, "delivery.build.proxy");
|
|
const image = record(delivery.image, "delivery.image");
|
|
const gitops = record(delivery.gitops, "delivery.gitops");
|
|
const readUrl = required(gitops.readUrl, "delivery.gitops.readUrl");
|
|
const branch = required(gitops.branch, "delivery.gitops.branch");
|
|
const releaseStatePath = safeRelativePath(gitops.releaseStatePath, "delivery.gitops.releaseStatePath");
|
|
const enabled = delivery.enabled === true;
|
|
if (delivery.enabled !== true && delivery.enabled !== false) throw new Error("delivery.enabled must be boolean");
|
|
const noProxy = proxy.noProxy;
|
|
if (!Array.isArray(noProxy) || noProxy.some((value) => typeof value !== "string" || value.length === 0)) throw new Error("delivery.build.proxy.noProxy must be a non-empty string array");
|
|
let action = enabled ? "build" : "disabled";
|
|
let reason = enabled ? "gitops-baseline-unavailable" : "delivery-disabled";
|
|
let baselineSourceCommit = null;
|
|
let changedPaths = [];
|
|
if (enabled) {
|
|
const gitopsRef = "refs/unidesk/release-baseline/unidesk-host";
|
|
const gitopsFetch = run("git", ["fetch", "--depth=1", readUrl, `+refs/heads/${branch}:${gitopsRef}`], sourceRoot, true);
|
|
if (gitopsFetch.status === 0) {
|
|
const stateResult = run("git", ["show", `${gitopsRef}:${releaseStatePath}`], sourceRoot, true);
|
|
if (stateResult.status === 0) {
|
|
const state = record(JSON.parse(stateResult.stdout), releaseStatePath);
|
|
if (state.kind !== "UniDeskHostReleaseState") throw new Error(`${releaseStatePath}.kind must be UniDeskHostReleaseState`);
|
|
if (state.serviceRef !== serviceRef) throw new Error(`${releaseStatePath}.serviceRef must match delivery.serviceRef`);
|
|
baselineSourceCommit = required(state.sourceCommit, `${releaseStatePath}.sourceCommit`);
|
|
if (!/^[0-9a-f]{40}$/u.test(baselineSourceCommit)) throw new Error(`${releaseStatePath}.sourceCommit must be a full Git commit SHA`);
|
|
const sourceRef = "refs/unidesk/release-baseline/unidesk-host-source";
|
|
const sourceFetch = run("git", ["fetch", "--depth=1", "--filter=blob:none", "origin", `+${sourceSnapshotPrefix}/${baselineSourceCommit}:${sourceRef}`], sourceRoot, true);
|
|
if (sourceFetch.status === 0) {
|
|
const diff = run("git", ["diff", "--name-only", "--no-renames", sourceRef, sourceCommit, "--", ...runtimePaths], sourceRoot);
|
|
changedPaths = diff.stdout.split(/\r?\n/u).filter(Boolean);
|
|
action = changedPaths.length === 0 ? "skip" : "build";
|
|
reason = changedPaths.length === 0 ? "runtime-inputs-unchanged" : "runtime-inputs-changed";
|
|
} else {
|
|
reason = "source-baseline-unavailable";
|
|
}
|
|
} else {
|
|
reason = "release-state-missing";
|
|
}
|
|
}
|
|
}
|
|
mkdirSync(outputDir, { recursive: true });
|
|
writeValue(outputDir, "enabled", enabled ? "true" : "false");
|
|
writeValue(outputDir, "action", action);
|
|
writeValue(outputDir, "reason", reason);
|
|
writeValue(outputDir, "baseline-source-commit", baselineSourceCommit ?? "");
|
|
writeFileSync(resolve(outputDir, "changed-paths.json"), `${JSON.stringify(changedPaths)}\n`, { encoding: "utf8", mode: 0o644 });
|
|
writeValue(outputDir, "dockerfile", required(record(service.repository, `${serviceRef}.repository`).dockerfile, `${serviceRef}.repository.dockerfile`));
|
|
writeValue(outputDir, "image-repository", required(image.repository, "delivery.image.repository"));
|
|
writeValue(outputDir, "network-mode", required(build.networkMode, "delivery.build.networkMode"));
|
|
writeValue(outputDir, "http-proxy", required(proxy.http, "delivery.build.proxy.http"));
|
|
writeValue(outputDir, "https-proxy", required(proxy.https, "delivery.build.proxy.https"));
|
|
writeValue(outputDir, "all-proxy", required(proxy.all, "delivery.build.proxy.all"));
|
|
writeValue(outputDir, "no-proxy", noProxy.join(","));
|
|
process.stdout.write(`${JSON.stringify({ ok: true, phase: "release-prepare", action, reason, enabled, serviceRef, sourceCommit, baselineSourceCommit, changedPaths, valuesPrinted: false })}\n`);
|
|
}
|
|
|
|
if (!process.execArgv.includes("--check")) main();
|