fix(ci): reuse provider egress for backend-core artifacts
This commit is contained in:
+143
-209
@@ -1,6 +1,4 @@
|
||||
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 { readConfig, repoRoot, rootPath } from "./config";
|
||||
@@ -24,7 +22,6 @@ interface ArtifactRegistryOptions {
|
||||
dryRun: boolean;
|
||||
runNow: boolean;
|
||||
commit: string | null;
|
||||
localPullPort: number | null;
|
||||
targetImage: string;
|
||||
}
|
||||
|
||||
@@ -61,7 +58,6 @@ const defaultOptions: ArtifactRegistryOptions = {
|
||||
dryRun: false,
|
||||
runNow: false,
|
||||
commit: null,
|
||||
localPullPort: null,
|
||||
targetImage: "unidesk-backend-core",
|
||||
};
|
||||
|
||||
@@ -81,12 +77,6 @@ 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`);
|
||||
@@ -131,9 +121,6 @@ function parseOptions(args: string[]): ArtifactRegistryOptions {
|
||||
} 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;
|
||||
@@ -154,6 +141,18 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function rootExecPrelude(): string {
|
||||
return [
|
||||
"root_exec() {",
|
||||
" if [ \"$(id -u)\" = \"0\" ]; then \"$@\"; return; fi",
|
||||
" if sudo -n true >/dev/null 2>&1; then sudo -n \"$@\"; return; fi",
|
||||
" if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- \"$@\"; return; fi",
|
||||
" echo 'artifact_registry_root_access=missing' >&2",
|
||||
" return 1",
|
||||
"}",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function file(path: string, content: string, mode = "0644"): RenderedFile {
|
||||
return { path, mode, sha256: sha256(content), content };
|
||||
}
|
||||
@@ -203,15 +202,16 @@ function systemdUnit(options: ArtifactRegistryOptions): string {
|
||||
return `[Unit]
|
||||
Description=UniDesk D601 Artifact Registry (CNCF Distribution)
|
||||
Documentation=https://github.com/pikasTech/unidesk/blob/master/docs/reference/artifact-registry.md
|
||||
Requires=docker.service
|
||||
After=docker.service network-online.target
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
ConditionPathExists=/var/run/docker.sock
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=${options.baseDir}
|
||||
ExecStartPre=/bin/mkdir -p ${options.baseDir} ${options.storageDir}
|
||||
ExecStartPre=/bin/sh -lc 'docker info >/dev/null'
|
||||
ExecStart=/bin/sh -lc 'docker compose -p ${options.composeProject} -f ${options.baseDir}/compose.yml up -d --remove-orphans'
|
||||
ExecStop=/bin/sh -lc 'docker compose -p ${options.composeProject} -f ${options.baseDir}/compose.yml down'
|
||||
TimeoutStartSec=120
|
||||
@@ -490,17 +490,18 @@ function runReadonlyStatus(options: ArtifactRegistryOptions, healthMode: boolean
|
||||
|
||||
function remoteWriteFileCommand(item: RenderedFile): string {
|
||||
const encoded = Buffer.from(item.content, "utf8").toString("base64");
|
||||
const rootOwned = item.path.startsWith("/etc/");
|
||||
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",
|
||||
`if [ ${rootOwned ? "1" : "0"} = 1 ]; then`,
|
||||
" root_exec mkdir -p \"$(dirname \"$target\")\"",
|
||||
` root_exec install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
|
||||
"else",
|
||||
" 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");
|
||||
@@ -513,10 +514,11 @@ function install(options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
"command -v docker >/dev/null",
|
||||
"docker compose version >/dev/null",
|
||||
"command -v systemctl >/dev/null",
|
||||
rootExecPrelude(),
|
||||
`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`,
|
||||
"root_exec systemctl daemon-reload",
|
||||
`root_exec systemctl enable --now ${shellQuote(options.unitName)}`,
|
||||
"sleep 2",
|
||||
`curl -fsS http://${options.host}:${options.port}/v2/ >/dev/null`,
|
||||
].join("\n");
|
||||
@@ -559,91 +561,6 @@ function installDryRun(options: ArtifactRegistryOptions): Record<string, unknown
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -670,6 +587,31 @@ function upsertEnvFileValues(path: string, values: Record<string, string>): void
|
||||
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
function pullBackendCoreArtifactFromD601(options: ArtifactRegistryOptions, sourceImage: string): CommandResult {
|
||||
const remoteScript = [
|
||||
"set -euo pipefail",
|
||||
`image=${shellQuote(sourceImage)}`,
|
||||
"docker pull -q \"$image\" >/dev/null",
|
||||
"docker image inspect \"$image\" --format 'remote_source={{ index .Config.Labels \"unidesk.ai/source-commit\" }} remote_service={{ index .Config.Labels \"unidesk.ai/service-id\" }}' >&2",
|
||||
"docker save \"$image\" | gzip -1",
|
||||
].join("\n");
|
||||
const sshCommand = [
|
||||
process.execPath,
|
||||
rootPath("scripts", "cli.ts"),
|
||||
"ssh",
|
||||
options.providerId,
|
||||
"argv",
|
||||
"bash",
|
||||
"-lc",
|
||||
remoteScript,
|
||||
].map(shellQuote).join(" ");
|
||||
const pipeline = [
|
||||
"set -euo pipefail",
|
||||
`${sshCommand} | gzip -dc | docker load`,
|
||||
].join("\n");
|
||||
return runCommand(["bash", "-lc", pipeline], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 900_000) });
|
||||
}
|
||||
|
||||
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>");
|
||||
@@ -682,11 +624,13 @@ async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<R
|
||||
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\"",
|
||||
`manifest_url=${shellQuote(`http://127.0.0.1:${options.port}/v2/unidesk/backend-core/manifests/${commit}`)}`,
|
||||
"headers=$(mktemp /tmp/unidesk-backend-core-manifest.XXXXXX.headers)",
|
||||
"trap 'rm -f \"$headers\"' EXIT",
|
||||
"curl -fsSI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -D \"$headers\" -o /dev/null \"$manifest_url\"",
|
||||
"manifest_digest=$(awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ {gsub(/\\r/, \"\", $2); print $2; exit}' \"$headers\")",
|
||||
"printf 'registry_image=%s\\nmanifest_url=%s\\nmanifest_digest=%s\\n' \"$registry_image\" \"$manifest_url\" \"$manifest_digest\"",
|
||||
].join("\n");
|
||||
const registryProbe = runRemoteScript(options, registryProbeScript, Math.max(options.timeoutMs, 120_000));
|
||||
if (registryProbe.exitCode !== 0 || registryProbe.timedOut) {
|
||||
@@ -698,110 +642,100 @@ async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<R
|
||||
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)}`,
|
||||
"docker exec \"$cid\" bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\" >/tmp/unidesk-backend-core-health.json",
|
||||
"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);
|
||||
const pull = pullBackendCoreArtifactFromD601(options, sourceImage);
|
||||
if (pull.exitCode !== 0 || pull.timedOut) {
|
||||
return {
|
||||
ok: true,
|
||||
commit,
|
||||
providerId: options.providerId,
|
||||
ok: false,
|
||||
step: "docker-load",
|
||||
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();
|
||||
}
|
||||
const localLoadedImage = sourceImage;
|
||||
const inspectPulled = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{json .Config.Labels}}"], repoRoot);
|
||||
const labelCommit = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot);
|
||||
const labelService = runCommand(["docker", "image", "inspect", localLoadedImage, "--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", localLoadedImage, 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", localLoadedImage, 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)}`,
|
||||
"docker exec \"$cid\" bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\" >/tmp/unidesk-backend-core-health.json",
|
||||
"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),
|
||||
localLoadedImage,
|
||||
targetImage: composeImage,
|
||||
targetCommitImage: commitImage,
|
||||
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`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
@@ -815,7 +749,7 @@ function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions):
|
||||
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.",
|
||||
note: "Backend-core CD continues in the background: D601 registry health, provider-gateway SSH image stream, docker load, retag, no-build recreate, live commit verification.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user