feat: split backend-core artifact ci cd
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { createServer, type AddressInfo, type Server, type Socket } from "node:net";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { runCommand, type CommandResult } from "./command";
|
||||
import { repoRoot } from "./config";
|
||||
import { readConfig, repoRoot, rootPath } from "./config";
|
||||
import { resolveComposeCommand, writeComposeEnv } from "./docker";
|
||||
import { startJob } from "./jobs";
|
||||
|
||||
type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | "install";
|
||||
type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | "install" | "deploy-backend-core";
|
||||
|
||||
interface ArtifactRegistryOptions {
|
||||
providerId: string;
|
||||
@@ -17,6 +22,10 @@ interface ArtifactRegistryOptions {
|
||||
containerName: string;
|
||||
timeoutMs: number;
|
||||
dryRun: boolean;
|
||||
runNow: boolean;
|
||||
commit: string | null;
|
||||
localPullPort: number | null;
|
||||
targetImage: string;
|
||||
}
|
||||
|
||||
interface RenderedFile {
|
||||
@@ -50,6 +59,10 @@ const defaultOptions: ArtifactRegistryOptions = {
|
||||
containerName: "unidesk-artifact-registry",
|
||||
timeoutMs: 30_000,
|
||||
dryRun: false,
|
||||
runNow: false,
|
||||
commit: null,
|
||||
localPullPort: null,
|
||||
targetImage: "unidesk-backend-core",
|
||||
};
|
||||
|
||||
function isHelpArg(value: string | undefined): boolean {
|
||||
@@ -68,18 +81,32 @@ function positiveInt(value: string, option: string): number {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function optionalPort(value: string, option: string): number {
|
||||
const parsed = positiveInt(value, option);
|
||||
if (parsed > 65535) throw new Error(`${option} must be <= 65535`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function absolutePath(value: string, option: string): string {
|
||||
if (!value.startsWith("/")) throw new Error(`${option} must be an absolute path`);
|
||||
if (value.includes("\n") || value.includes("\0")) throw new Error(`${option} must not contain control characters`);
|
||||
return value.replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function commitValue(value: string, option: string): string {
|
||||
const normalized = value.toLowerCase();
|
||||
if (!/^[0-9a-f]{40}$/u.test(normalized)) throw new Error(`${option} must be a full 40-character commit SHA`);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): ArtifactRegistryOptions {
|
||||
const options = { ...defaultOptions };
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--dry-run") {
|
||||
options.dryRun = true;
|
||||
} else if (arg === "--run-now") {
|
||||
options.runNow = true;
|
||||
} else if (arg === "--provider-id") {
|
||||
options.providerId = requireValue(args, index, arg);
|
||||
index += 1;
|
||||
@@ -101,6 +128,15 @@ function parseOptions(args: string[]): ArtifactRegistryOptions {
|
||||
} else if (arg === "--timeout-ms") {
|
||||
options.timeoutMs = positiveInt(requireValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
} else if (arg === "--commit") {
|
||||
options.commit = commitValue(requireValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
} else if (arg === "--local-pull-port") {
|
||||
options.localPullPort = optionalPort(requireValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
} else if (arg === "--target-image") {
|
||||
options.targetImage = requireValue(args, index, arg);
|
||||
index += 1;
|
||||
} else {
|
||||
throw new Error(`unknown artifact-registry option: ${arg}`);
|
||||
}
|
||||
@@ -231,13 +267,14 @@ function plan(options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
"listen only on D601 host loopback 127.0.0.1:5000",
|
||||
"do not expose a public port, NodePort, hostPort, or third-party registry",
|
||||
"do not run inside k3s; keep the registry outside the native k3s failure domain",
|
||||
"first-stage CLI does not mutate D601 runtime; install is dry-run only",
|
||||
"backend-core production deploy behavior is unchanged in this issue",
|
||||
"CI builds and publishes backend-core artifacts on D601",
|
||||
"CD only pulls, retags, recreates backend-core, and verifies live commit",
|
||||
"master server CD must not compile Rust or run docker compose build backend-core",
|
||||
],
|
||||
renderedPaths: bundle.paths,
|
||||
futureFlow: [
|
||||
"D601 CI/dev builds unidesk/backend-core:<commit>",
|
||||
"D601 pushes the image into the loopback registry",
|
||||
backendCoreArtifactFlow: [
|
||||
"D601 CI builds unidesk/backend-core:<commit>",
|
||||
"D601 CI pushes 127.0.0.1:5000/unidesk/backend-core:<commit>",
|
||||
"main server pulls via a controlled short-lived localhost relay",
|
||||
"prod CD retags, recreates backend-core, and verifies live commit metadata",
|
||||
],
|
||||
@@ -352,6 +389,11 @@ function commandTail(result: CommandResult): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs): CommandResult {
|
||||
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
|
||||
return runCommand(command, repoRoot, { timeoutMs });
|
||||
}
|
||||
|
||||
function statusFromValues(options: ArtifactRegistryOptions, values: Record<string, string>, command: CommandResult, healthMode: boolean): Record<string, unknown> {
|
||||
const commandOk = command.exitCode === 0 && !command.timedOut;
|
||||
const checks = {
|
||||
@@ -427,8 +469,7 @@ function statusFromValues(options: ArtifactRegistryOptions, values: Record<strin
|
||||
function runReadonlyStatus(options: ArtifactRegistryOptions, healthMode: boolean): Record<string, unknown> {
|
||||
const bundle = renderBundle(options);
|
||||
const script = statusScript(options, bundle);
|
||||
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
|
||||
const result = runCommand(command, repoRoot, { timeoutMs: options.timeoutMs });
|
||||
const result = runRemoteScript(options, script);
|
||||
if (result.exitCode !== 0 || result.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -447,6 +488,54 @@ function runReadonlyStatus(options: ArtifactRegistryOptions, healthMode: boolean
|
||||
return statusFromValues(options, parseKeyValueOutput(result.stdout), result, healthMode);
|
||||
}
|
||||
|
||||
function remoteWriteFileCommand(item: RenderedFile): string {
|
||||
const encoded = Buffer.from(item.content, "utf8").toString("base64");
|
||||
return [
|
||||
`target=${shellQuote(item.path)}`,
|
||||
"tmp=$(mktemp /tmp/unidesk-artifact-registry.XXXXXX)",
|
||||
"trap 'rm -f \"$tmp\"' EXIT",
|
||||
`printf %s ${shellQuote(encoded)} | base64 -d > "$tmp"`,
|
||||
"if [ \"$(id -u)\" = \"0\" ]; then",
|
||||
" mkdir -p \"$(dirname \"$target\")\"",
|
||||
` install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
|
||||
"else",
|
||||
" sudo mkdir -p \"$(dirname \"$target\")\"",
|
||||
` sudo install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
|
||||
"fi",
|
||||
`echo ${shellQuote(`artifact_registry_file_written path=${item.path} sha256=${item.sha256}`)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function install(options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
const bundle = renderBundle(options);
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
"command -v docker >/dev/null",
|
||||
"docker compose version >/dev/null",
|
||||
"command -v systemctl >/dev/null",
|
||||
`mkdir -p ${shellQuote(bundle.paths.baseDir)} ${shellQuote(bundle.paths.storage)}`,
|
||||
...bundle.files.map(remoteWriteFileCommand),
|
||||
"if [ \"$(id -u)\" = \"0\" ]; then systemctl daemon-reload; else sudo systemctl daemon-reload; fi",
|
||||
`if [ "$(id -u)" = "0" ]; then systemctl enable --now ${shellQuote(options.unitName)}; else sudo systemctl enable --now ${shellQuote(options.unitName)}; fi`,
|
||||
"sleep 2",
|
||||
`curl -fsS http://${options.host}:${options.port}/v2/ >/dev/null`,
|
||||
].join("\n");
|
||||
const command = runRemoteScript(options, script, Math.max(options.timeoutMs, 120_000));
|
||||
const status = runReadonlyStatus(options, true);
|
||||
return {
|
||||
ok: command.exitCode === 0 && !command.timedOut && status.ok === true,
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
providerId: options.providerId,
|
||||
render: {
|
||||
paths: bundle.paths,
|
||||
files: bundle.files.map((item) => ({ path: item.path, mode: item.mode, sha256: item.sha256 })),
|
||||
},
|
||||
installCommand: commandTail(command),
|
||||
health: status,
|
||||
};
|
||||
}
|
||||
|
||||
function installDryRun(options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
const bundle = renderBundle(options);
|
||||
return {
|
||||
@@ -466,32 +555,297 @@ function installDryRun(options: ArtifactRegistryOptions): Record<string, unknown
|
||||
`systemctl enable --now ${options.unitName}`,
|
||||
`curl -fsS http://${options.host}:${options.port}/v2/`,
|
||||
],
|
||||
note: "First-stage artifact-registry install is dry-run only; no D601 runtime files or services were changed.",
|
||||
note: "Dry run only; no D601 runtime files or services were changed.",
|
||||
};
|
||||
}
|
||||
|
||||
async function startRegistryRelay(options: ArtifactRegistryOptions): Promise<{
|
||||
server: Server;
|
||||
port: number;
|
||||
close: () => Promise<void>;
|
||||
}> {
|
||||
const children = new Set<ChildProcessWithoutNullStreams>();
|
||||
const sockets = new Set<Socket>();
|
||||
const remoteProxyScript = String.raw`
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
|
||||
target = socket.create_connection(("127.0.0.1", 5000), timeout=10)
|
||||
target.setblocking(False)
|
||||
sys.stdin.buffer.raw.fileno()
|
||||
sys.stdout.buffer.raw.fileno()
|
||||
stdin_fd = sys.stdin.fileno()
|
||||
stdout_fd = sys.stdout.fileno()
|
||||
target_fd = target.fileno()
|
||||
|
||||
while True:
|
||||
readable, _, _ = select.select([stdin_fd, target_fd], [], [])
|
||||
if stdin_fd in readable:
|
||||
data = sys.stdin.buffer.read1(65536)
|
||||
if not data:
|
||||
try:
|
||||
target.shutdown(socket.SHUT_WR)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
target.sendall(data)
|
||||
if target_fd in readable:
|
||||
try:
|
||||
data = target.recv(65536)
|
||||
except BlockingIOError:
|
||||
data = b""
|
||||
if not data:
|
||||
break
|
||||
sys.stdout.buffer.write(data)
|
||||
sys.stdout.buffer.flush()
|
||||
`;
|
||||
const server = createServer((socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
const child = spawn(process.execPath, [
|
||||
"scripts/cli.ts",
|
||||
"ssh",
|
||||
options.providerId,
|
||||
"argv",
|
||||
"python3",
|
||||
"-u",
|
||||
"-c",
|
||||
remoteProxyScript,
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
children.add(child);
|
||||
child.on("close", () => children.delete(child));
|
||||
child.on("error", () => socket.destroy());
|
||||
socket.pipe(child.stdin);
|
||||
child.stdout.pipe(socket);
|
||||
child.stderr.on("data", (chunk) => process.stderr.write(`[artifact-registry-relay] ${chunk.toString("utf8")}`));
|
||||
socket.on("close", () => child.kill());
|
||||
});
|
||||
const listen = new Promise<number>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(options.localPullPort ?? 0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
const address = server.address() as AddressInfo;
|
||||
resolve(address.port);
|
||||
});
|
||||
});
|
||||
const port = await listen;
|
||||
return {
|
||||
server,
|
||||
port,
|
||||
close: async () => {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
for (const child of children) child.kill();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function composeLockScript(innerScript: string): string {
|
||||
const lockDir = rootPath(".state", "locks");
|
||||
const lockPath = rootPath(".state", "locks", "server-compose.lock");
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
`mkdir -p ${shellQuote(lockDir)}`,
|
||||
`echo ${shellQuote(`compose_lock_wait ${lockPath}`)}`,
|
||||
`flock ${shellQuote(lockPath)} bash -lc ${shellQuote(innerScript)}`,
|
||||
].join("; ");
|
||||
}
|
||||
|
||||
function upsertEnvFileValues(path: string, values: Record<string, string>): void {
|
||||
const existing = readFileSync(path, "utf8");
|
||||
const seen = new Set<string>();
|
||||
const lines = existing.split(/\n/u).filter((line, index, array) => index < array.length - 1 || line.length > 0).map((line) => {
|
||||
const match = /^([A-Za-z0-9_]+)=/u.exec(line);
|
||||
if (match === null || values[match[1]] === undefined) return line;
|
||||
seen.add(match[1]);
|
||||
return `${match[1]}=${values[match[1]]}`;
|
||||
});
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (!seen.has(key)) lines.push(`${key}=${value}`);
|
||||
}
|
||||
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
|
||||
const commit = options.commit;
|
||||
if (commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
|
||||
const health = runReadonlyStatus(options, true);
|
||||
if (health.ok !== true) {
|
||||
return { ok: false, error: "D601 artifact registry is not healthy", health };
|
||||
}
|
||||
const sourceImage = `127.0.0.1:${options.port}/unidesk/backend-core:${commit}`;
|
||||
const composeImage = options.targetImage;
|
||||
const commitImage = `${options.targetImage}:${commit}`;
|
||||
const registryProbeScript = [
|
||||
"set -euo pipefail",
|
||||
`image=${shellQuote(`unidesk/backend-core:${commit}`)}`,
|
||||
`registry_image=${shellQuote(sourceImage)}`,
|
||||
"docker manifest inspect \"$registry_image\" >/tmp/unidesk-backend-core-manifest.json",
|
||||
"manifest_digest=$(docker manifest inspect --verbose \"$registry_image\" 2>/dev/null | awk -F'\"' '/Digest/ {print $4; exit}' || true)",
|
||||
"printf 'registry_image=%s\\nmanifest_digest=%s\\n' \"$registry_image\" \"$manifest_digest\"",
|
||||
].join("\n");
|
||||
const registryProbe = runRemoteScript(options, registryProbeScript, Math.max(options.timeoutMs, 120_000));
|
||||
if (registryProbe.exitCode !== 0 || registryProbe.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
step: "registry-artifact-check",
|
||||
error: "backend-core image artifact is missing from D601 registry; run CI artifact publication first",
|
||||
sourceImage,
|
||||
registryProbe: commandTail(registryProbe),
|
||||
};
|
||||
}
|
||||
const relay = await startRegistryRelay(options);
|
||||
const localSourceImage = `127.0.0.1:${relay.port}/unidesk/backend-core:${commit}`;
|
||||
try {
|
||||
const pull = runCommand(["docker", "pull", localSourceImage], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 900_000) });
|
||||
if (pull.exitCode !== 0 || pull.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
step: "docker-pull",
|
||||
sourceImage,
|
||||
localRelayImage: localSourceImage,
|
||||
registryProbe: commandTail(registryProbe),
|
||||
pull: commandTail(pull),
|
||||
};
|
||||
}
|
||||
const inspectPulled = runCommand(["docker", "image", "inspect", localSourceImage, "--format", "{{json .Config.Labels}}"], repoRoot);
|
||||
const labelCommit = runCommand(["docker", "image", "inspect", localSourceImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot);
|
||||
const labelService = runCommand(["docker", "image", "inspect", localSourceImage, "--format", "{{ index .Config.Labels \"unidesk.ai/service-id\" }}"], repoRoot);
|
||||
if (labelCommit.stdout.trim() !== commit || labelService.stdout.trim() !== "backend-core") {
|
||||
return {
|
||||
ok: false,
|
||||
step: "image-label-verify",
|
||||
expected: { commit, serviceId: "backend-core" },
|
||||
observed: { commit: labelCommit.stdout.trim(), serviceId: labelService.stdout.trim(), labels: inspectPulled.stdout.trim() },
|
||||
registryProbe: commandTail(registryProbe),
|
||||
};
|
||||
}
|
||||
const tag = runCommand(["docker", "tag", localSourceImage, composeImage], repoRoot);
|
||||
if (tag.exitCode !== 0 || tag.timedOut) {
|
||||
return { ok: false, step: "docker-tag", targetImage: composeImage, tag: commandTail(tag), registryProbe: commandTail(registryProbe) };
|
||||
}
|
||||
const tagCommit = runCommand(["docker", "tag", localSourceImage, commitImage], repoRoot);
|
||||
if (tagCommit.exitCode !== 0 || tagCommit.timedOut) {
|
||||
return { ok: false, step: "docker-tag-commit", targetImage: commitImage, tag: commandTail(tagCommit), registryProbe: commandTail(registryProbe) };
|
||||
}
|
||||
const config = readConfig();
|
||||
const runtimeEnv = writeComposeEnv(config, false);
|
||||
upsertEnvFileValues(runtimeEnv.envFile, {
|
||||
UNIDESK_DEPLOY_REF: "deploy.json#environments.prod.services.backend-core",
|
||||
UNIDESK_DEPLOY_SERVICE_ID: "backend-core",
|
||||
UNIDESK_DEPLOY_REPO: "https://github.com/pikasTech/unidesk",
|
||||
UNIDESK_DEPLOY_COMMIT: commit,
|
||||
UNIDESK_DEPLOY_REQUESTED_COMMIT: commit,
|
||||
});
|
||||
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
|
||||
const upScript = [
|
||||
"set -euo pipefail",
|
||||
`echo ${shellQuote(`backend_core_artifact_cd source=${sourceImage} target=${composeImage}`)}`,
|
||||
`${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate backend-core`,
|
||||
"ready=0",
|
||||
"for attempt in $(seq 1 60); do",
|
||||
" cid=$(docker ps -q --filter label=com.docker.compose.project=unidesk --filter label=com.docker.compose.service=backend-core --filter label=com.docker.compose.oneoff=False | head -1 || true)",
|
||||
" if [ -n \"$cid\" ]; then",
|
||||
" health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \"$cid\" 2>/dev/null || true)",
|
||||
" echo \"backend_core_container_probe attempt=$attempt cid=$cid health=$health\"",
|
||||
" if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
|
||||
" else",
|
||||
" echo \"backend_core_container_probe attempt=$attempt cid=missing\"",
|
||||
" fi",
|
||||
" sleep 1",
|
||||
"done",
|
||||
"test \"$ready\" = \"1\"",
|
||||
"cid=$(docker ps -q --filter label=com.docker.compose.project=unidesk --filter label=com.docker.compose.service=backend-core --filter label=com.docker.compose.oneoff=False | head -1)",
|
||||
"image_id=$(docker inspect -f '{{.Image}}' \"$cid\")",
|
||||
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
|
||||
`test "$actual_commit" = ${shellQuote(commit)}`,
|
||||
"if docker exec \"$cid\" sh -lc 'command -v backend-core >/dev/null 2>&1'; then",
|
||||
" docker exec \"$cid\" backend-core --fetch-json http://127.0.0.1:8080/health --require-ok >/tmp/unidesk-backend-core-health.json",
|
||||
"else",
|
||||
" docker exec \"$cid\" bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{console.log(await r.text()); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\" >/tmp/unidesk-backend-core-health.json",
|
||||
"fi",
|
||||
"cat /tmp/unidesk-backend-core-health.json",
|
||||
].join("\n");
|
||||
const deploy = runCommand(["bash", "-lc", composeLockScript(upScript)], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 300_000) });
|
||||
if (deploy.exitCode !== 0 || deploy.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
step: "compose-recreate",
|
||||
sourceImage,
|
||||
targetImage: composeImage,
|
||||
registryProbe: commandTail(registryProbe),
|
||||
deploy: commandTail(deploy),
|
||||
};
|
||||
}
|
||||
const running = runCommand(["docker", "inspect", "unidesk-backend-core", "--format", "image={{.Config.Image}} imageId={{.Image}} status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}"], repoRoot);
|
||||
return {
|
||||
ok: true,
|
||||
commit,
|
||||
providerId: options.providerId,
|
||||
sourceImage,
|
||||
registryProbe: commandTail(registryProbe),
|
||||
localRelayImage: localSourceImage,
|
||||
targetImage: composeImage,
|
||||
targetCommitImage: commitImage,
|
||||
relay: {
|
||||
bind: `127.0.0.1:${relay.port}`,
|
||||
persistent: false,
|
||||
},
|
||||
pull: commandTail(pull),
|
||||
deploy: commandTail(deploy),
|
||||
running: running.stdout.trim(),
|
||||
rollback: {
|
||||
previousImageHint: "Use docker image ls and docker inspect logs for the previous backend-core image id; Compose named volumes were not changed.",
|
||||
commandShape: `${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate backend-core`,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
await relay.close();
|
||||
}
|
||||
}
|
||||
|
||||
function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
|
||||
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
|
||||
const command = [process.execPath, rootPath("scripts", "cli.ts"), "artifact-registry", ...runArgs];
|
||||
const job = startJob("artifact_registry_backend_core_cd", command, `Pull and deploy backend-core artifact ${options.commit} from D601 registry`);
|
||||
return {
|
||||
ok: true,
|
||||
mode: "async-job",
|
||||
job,
|
||||
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
|
||||
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
|
||||
note: "Backend-core CD continues in the background: D601 registry health, short-lived local relay, docker pull, retag, no-build recreate, live commit verification.",
|
||||
};
|
||||
}
|
||||
|
||||
function localHelp(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
command: "artifact-registry plan|render|status|health|install",
|
||||
command: "artifact-registry plan|render|status|health|install|deploy-backend-core",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts artifact-registry plan [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry render [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry status [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry health [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry install --dry-run [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry install [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-backend-core --commit <full-sha> [--run-now] [--provider-id D601]",
|
||||
],
|
||||
firstStage: "install without --dry-run is intentionally disabled",
|
||||
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
|
||||
defaults: defaultOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export function runArtifactRegistryCommand(args: string[]): unknown {
|
||||
export async function runArtifactRegistryCommand(args: string[]): Promise<unknown> {
|
||||
const action = args[0];
|
||||
if (isHelpArg(action)) return localHelp();
|
||||
if (action !== "plan" && action !== "render" && action !== "status" && action !== "health" && action !== "install") {
|
||||
throw new Error("artifact-registry usage: plan|render|status|health|install");
|
||||
if (action !== "plan" && action !== "render" && action !== "status" && action !== "health" && action !== "install" && action !== "deploy-backend-core") {
|
||||
throw new Error("artifact-registry usage: plan|render|status|health|install|deploy-backend-core");
|
||||
}
|
||||
const options = parseOptions(args.slice(1));
|
||||
if (action === "plan") return plan(options);
|
||||
@@ -499,15 +853,11 @@ export function runArtifactRegistryCommand(args: string[]): unknown {
|
||||
if (action === "status") return runReadonlyStatus(options, false);
|
||||
if (action === "health") return runReadonlyStatus(options, true);
|
||||
if (action === "install") {
|
||||
if (!options.dryRun) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "artifact-registry install is first-stage dry-run only; pass --dry-run",
|
||||
firstStageOnly: true,
|
||||
mutation: false,
|
||||
};
|
||||
}
|
||||
return installDryRun(options);
|
||||
return options.dryRun ? installDryRun(options) : install(options);
|
||||
}
|
||||
if (action === "deploy-backend-core") {
|
||||
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
|
||||
return options.runNow ? await deployBackendCoreNow(options) : deployBackendCoreJob(args, options);
|
||||
}
|
||||
throw new Error("unreachable artifact-registry action");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user