merge: code queue claim move race fix

# Conflicts:
#	AGENTS.md
#	TEST.md
#	config.json
#	deploy.json
#	docs/reference/cli.md
#	docs/reference/deploy.md
#	docs/reference/microservices.md
#	scripts/cli.ts
#	scripts/src/check.ts
#	scripts/src/e2e.ts
#	scripts/src/remote.ts
#	src/components/microservices/code-queue/src/index.ts
#	src/components/microservices/code-queue/src/queue-api.ts
#	src/components/microservices/decision-center/Dockerfile
#	src/components/microservices/decision-center/src/index.ts
This commit is contained in:
Codex
2026-05-17 12:29:03 +00:00
23 changed files with 1913 additions and 54 deletions
+354
View File
@@ -0,0 +1,354 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { runCommand } from "./command";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { startJob } from "./jobs";
const k3sctlContainerName = "k3sctl-adapter";
const k3sctlSshKey = "/run/host-ssh/id_ed25519";
const d601SshTarget = "ubuntu@host.docker.internal";
const d601Kubeconfig = "/etc/rancher/k3s/k3s.yaml";
const tektonPipelineVersion = "v1.12.0";
const tektonTriggersVersion = "v0.34.0";
const tektonPipelineReleaseUrl = `https://infra.tekton.dev/tekton-releases/pipeline/previous/${tektonPipelineVersion}/release.yaml`;
const tektonTriggersReleaseUrl = `https://infra.tekton.dev/tekton-releases/triggers/previous/${tektonTriggersVersion}/release.yaml`;
const tektonTriggersInterceptorsUrl = `https://infra.tekton.dev/tekton-releases/triggers/previous/${tektonTriggersVersion}/interceptors.yaml`;
const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789";
const ciRuntimeImages = [
"rancher/mirrored-pause:3.6",
"rancher/mirrored-library-busybox:1.36.1",
"cgr.dev/chainguard/busybox@sha256:19f02276bf8dbdd62f069b922f10c65262cc34b710eea26ff928129a736be791",
"ghcr.io/tektoncd/pipeline/entrypoint-bff0a22da108bc2f16c818c97641a296:v1.12.0",
"ghcr.io/tektoncd/pipeline/workingdirinit-0c558922ec6a1b739e550e349f2d5fc1:v1.12.0",
"ghcr.io/tektoncd/pipeline/nop-8eac7c133edad5df719dc37b36b62482:v1.12.0",
"ghcr.io/tektoncd/pipeline/events-a9042f7efb0cbade2a868a1ee5ddd52c:v1.12.0",
"ghcr.io/tektoncd/triggers/eventlistenersink-7ad1faa98cddbcb0c24990303b220bb8:v0.34.0",
"oven/bun:1-debian",
"alpine/git:2.45.2",
"unidesk-code-queue:d601",
];
interface CiOptions {
repoUrl: string;
revision: string;
waitMs: number;
}
function stringOption(args: string[], name: string): string | null {
const index = args.indexOf(name);
if (index === -1) return null;
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
return value;
}
function numberOption(args: string[], name: string, fallback: number): number {
const raw = stringOption(args, name);
if (raw === null) return fallback;
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
return value;
}
function requireRevision(value: string | null): string {
if (value === null || value.length === 0) throw new Error("ci run requires --revision <commit-or-ref>");
if (!/^[A-Za-z0-9._/@:-]{1,160}$/u.test(value)) throw new Error("ci --revision contains unsupported characters");
return value;
}
function shellQuote(value: string): string {
return `'${value.replace(/'/gu, "'\\''")}'`;
}
function dockerExecK3sctl(args: string[]) {
return runCommand(["docker", "exec", k3sctlContainerName, ...args], repoRoot);
}
function dockerExecK3sctlWithInput(args: string[], input: string) {
const command = ["docker", "exec", "-i", k3sctlContainerName, ...args];
const result = spawnSync(command[0], command.slice(1), {
cwd: repoRoot,
encoding: "utf8",
input,
maxBuffer: 1024 * 1024 * 8,
});
return {
command,
cwd: repoRoot,
exitCode: result.status,
stdout: result.stdout ?? "",
stderr: result.stderr ?? result.error?.message ?? "",
};
}
function remoteKubectlCommand(script: string): string[] {
return [
"sh",
"-lc",
[
"ssh",
"-i",
shellQuote(k3sctlSshKey),
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/tmp/unidesk-ci-known-hosts",
"-o",
"ConnectTimeout=10",
shellQuote(d601SshTarget),
shellQuote(`KUBECONFIG=${d601Kubeconfig} bash -lc ${shellQuote(script)}`),
].join(" "),
];
}
function runRemoteKubectl(script: string) {
const result = dockerExecK3sctl(remoteKubectlCommand(script));
if (result.exitCode !== 0) {
throw new Error(`D601 kubectl command failed: ${result.stderr || result.stdout}`);
}
return result;
}
function remoteApplyManifest(path: string): void {
const absolute = rootPath(path);
if (!existsSync(absolute)) throw new Error(`manifest not found: ${path}`);
const result = dockerExecK3sctlWithInput([
"sh",
"-lc",
[
"ssh",
"-i",
shellQuote(k3sctlSshKey),
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/tmp/unidesk-ci-known-hosts",
"-o",
"ConnectTimeout=10",
shellQuote(d601SshTarget),
shellQuote(`KUBECONFIG=${d601Kubeconfig} kubectl apply -f -`),
].join(" "),
], readFileSync(absolute, "utf8"));
if (result.exitCode !== 0) {
throw new Error(`kubectl apply failed for ${path}: ${result.stderr || result.stdout}`);
}
}
function prewarmCiRuntimeImages(): void {
const images = ciRuntimeImages.map(shellQuote).join(" ");
runRemoteKubectl([
"set -euo pipefail",
"export DOCKER_CONFIG=/tmp/unidesk-ci-docker-config",
"mkdir -p \"$DOCKER_CONFIG\"",
"printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
`images=(${images})`,
"for image in \"${images[@]}\"; do",
" if ! docker image inspect \"$image\" >/dev/null 2>&1; then",
" echo ci_runtime_image_pull=$image",
` HTTP_PROXY=${shellQuote(providerGatewayWsEgressProxyUrl)} HTTPS_PROXY=${shellQuote(providerGatewayWsEgressProxyUrl)} ALL_PROXY=${shellQuote(providerGatewayWsEgressProxyUrl)} NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal docker pull --platform linux/amd64 "$image"`,
" else",
" echo ci_runtime_image_cached=$image",
" fi",
"done",
"pause_entrypoint=$(docker image inspect rancher/mirrored-pause:3.6 --format '{{json .Config.Entrypoint}}' 2>/dev/null || true)",
"if ! printf '%s' \"$pause_entrypoint\" | grep -q '\"/pause\"'; then echo native_k3s_pause_image_invalid_entrypoint=$pause_entrypoint >&2; exit 1; fi",
"rm -f /tmp/unidesk-ci-runtime-images.tar",
"docker save \"${images[@]}\" -o /tmp/unidesk-ci-runtime-images.tar",
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import --digests --all-platforms /tmp/unidesk-ci-runtime-images.tar >/tmp/unidesk-ci-runtime-images-import.log",
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/rancher/mirrored-pause:3.6' >/dev/null",
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/oven/bun:1-debian' >/dev/null",
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/alpine/git:2.45.2' >/dev/null",
"/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/library/unidesk-code-queue:d601' >/dev/null",
].join("\n"));
}
function status(): Record<string, unknown> {
const summary = runRemoteKubectl([
"set -euo pipefail",
"printf 'tekton_pipelines='",
"kubectl get deploy -n tekton-pipelines -o name 2>/dev/null | tr '\\n' ' ' || true",
"printf '\\ntekton_triggers='",
"kubectl get deploy -n tekton-pipelines-resolvers -o name 2>/dev/null | tr '\\n' ' ' || true",
"printf '\\nunidesk_ci='",
"kubectl get pipeline,task,pipelinerun,eventlistener,svc -n unidesk-ci -o name 2>/dev/null | tr '\\n' ' ' || true",
"printf '\\n'",
].join("\n"));
return {
ok: true,
providerId: "D601",
orchestrator: "native-k3s",
tekton: {
pipelineVersion: tektonPipelineVersion,
triggersVersion: tektonTriggersVersion,
},
summary: summary.stdout.trim(),
};
}
function install(): Record<string, unknown> {
if (!existsSync(rootPath("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"))) {
throw new Error("CI manifests are missing");
}
prewarmCiRuntimeImages();
runRemoteKubectl([
"set -euo pipefail",
`kubectl apply -f ${shellQuote(tektonPipelineReleaseUrl)}`,
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s",
`kubectl apply -f ${shellQuote(tektonTriggersReleaseUrl)}`,
`kubectl apply -f ${shellQuote(tektonTriggersInterceptorsUrl)}`,
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s",
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines-resolvers --timeout=900s",
].join("\n"));
remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/tekton-install.yaml");
remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml");
remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.triggers.yaml");
return status();
}
function pipelineRunManifest(options: CiOptions): string {
const safeSuffix = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14).toLowerCase();
return `apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: unidesk-ci-${safeSuffix}-
namespace: unidesk-ci
labels:
app.kubernetes.io/name: unidesk-ci
app.kubernetes.io/part-of: unidesk
unidesk.ai/revision: ${JSON.stringify(options.revision)}
spec:
pipelineRef:
name: unidesk-ci
taskRunTemplate:
serviceAccountName: unidesk-ci-runner
params:
- name: repo-url
value: ${JSON.stringify(options.repoUrl)}
- name: revision
value: ${JSON.stringify(options.revision)}
workspaces:
- name: shared-workspace
persistentVolumeClaim:
claimName: unidesk-ci-cache
`;
}
function remoteCreatePipelineRun(manifest: string): string {
const result = dockerExecK3sctlWithInput([
"sh",
"-lc",
[
"ssh",
"-i",
shellQuote(k3sctlSshKey),
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/tmp/unidesk-ci-known-hosts",
shellQuote(d601SshTarget),
shellQuote(`KUBECONFIG=${d601Kubeconfig} kubectl create -f - -o jsonpath='{.metadata.name}'`),
].join(" "),
], manifest);
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout);
return result.stdout.trim();
}
function run(options: CiOptions): Record<string, unknown> {
const name = remoteCreatePipelineRun(pipelineRunManifest(options));
const wait = options.waitMs > 0 ? dockerExecK3sctl(remoteKubectlCommand([
"set -euo pipefail",
`deadline=$((SECONDS + ${Math.ceil(options.waitMs / 1000)}))`,
"while [ \"$SECONDS\" -lt \"$deadline\" ]; do",
` condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`,
" case \"$condition\" in",
" True*)",
" echo \"$condition\"",
` kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
" exit 0",
" ;;",
" False*)",
" echo \"$condition\"",
` kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
" exit 1",
" ;;",
" esac",
" sleep 2",
"done",
`echo "Timed out waiting for pipelinerun/${name}" >&2`,
`kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
"exit 124",
].join("\n"))) : null;
const waitSucceeded = wait === null || wait.exitCode === 0 || wait.stdout.trimStart().startsWith("True\tSucceeded\t");
return {
ok: waitSucceeded,
pipelineRun: name,
namespace: "unidesk-ci",
repoUrl: options.repoUrl,
revision: options.revision,
wait: wait === null ? null : {
stdoutTail: wait.stdout.slice(-6000),
stderrTail: wait.stderr.slice(-6000),
},
next: [
`bun scripts/cli.ts ci logs ${name}`,
"bun scripts/cli.ts ci status",
],
};
}
function logs(name: string): Record<string, unknown> {
if (name.length === 0) throw new Error("ci logs requires PipelineRun name");
const result = runRemoteKubectl([
"set -euo pipefail",
`kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o wide`,
`kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o wide`,
`for pod in $(kubectl get pods -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o name); do echo "===== $pod"; kubectl logs -n unidesk-ci "$pod" --all-containers=true --tail=160; done`,
].join("\n"));
return {
ok: true,
pipelineRun: name,
output: result.stdout,
stderr: result.stderr,
};
}
function help(): Record<string, unknown> {
return {
command: "ci install|status|run|logs",
description: "Manage the D601 k3s Tekton CI gate. This intentionally does not deploy CD.",
examples: [
"bun scripts/cli.ts ci install",
"bun scripts/cli.ts ci run --revision <commit>",
"bun scripts/cli.ts ci logs <pipelineRun>",
],
tekton: {
pipelineVersion: tektonPipelineVersion,
triggersVersion: tektonTriggersVersion,
sources: {
pipeline: tektonPipelineReleaseUrl,
triggers: tektonTriggersReleaseUrl,
interceptors: tektonTriggersInterceptorsUrl,
},
},
};
}
export function runCiCommand(_config: UniDeskConfig, args: string[]): Record<string, unknown> {
const [action = "status", nameArg] = args;
if (action === "help" || action === "--help" || action === "-h") return help();
if (action === "install") return install();
if (action === "status") return status();
if (action === "run") {
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
const revision = requireRevision(stringOption(args, "--revision") ?? stringOption(args, "--commit"));
const waitMs = numberOption(args, "--wait-ms", 0);
return run({ repoUrl, revision, waitMs });
}
if (action === "logs") return logs(nameArg ?? "");
throw new Error("ci command must be one of: install, status, run, logs");
}
export function startCiInstallJob(): Record<string, unknown> {
const job = startJob("ci_install", ["bun", "scripts/cli.ts", "ci", "install"], "Install/refresh Tekton CI on D601 k3s");
return { ok: true, job };
}
+9 -1
View File
@@ -125,11 +125,19 @@ export async function runMicroserviceCommand(_config: UniDeskConfig, args: strin
const id = requireId(idArg, "microservice health");
return coreInternalFetch(`/api/microservices/${encodeId(id)}/health`);
}
if (action === "diagnostics") {
const id = requireId(idArg, "microservice diagnostics");
return coreInternalFetch(`/api/microservices/${encodeId(id)}/diagnostics`);
}
if (action === "tunnel-self-test") {
const id = requireId(idArg, "microservice tunnel-self-test");
return coreInternalFetch(`/api/microservices/${encodeId(id)}/tunnel-self-test`);
}
if (action === "proxy") {
const id = requireId(idArg, "microservice proxy");
const path = requireProxyPath(pathArg);
const body = requestBodyOption(args);
return summarizeMicroserviceProxyResponse(coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`, { method: methodOption(args, body !== undefined), body }), args);
}
throw new Error("microservice command must be one of: list, status, health, proxy");
throw new Error("microservice command must be one of: list, status, health, diagnostics, tunnel-self-test, proxy");
}
+3 -3
View File
@@ -455,7 +455,7 @@ async function remoteMicroservice(session: FrontendSession, args: string[]): Pro
if (action === "list") {
return { transport: "frontend", response: await frontendJson(session, "/api/microservices", undefined, 12_000) };
}
if ((action === "status" || action === "health") && id !== undefined) {
if ((action === "status" || action === "health" || action === "diagnostics" || action === "tunnel-self-test") && id !== undefined) {
return {
transport: "frontend",
response: await frontendJson(session, `/api/microservices/${encodeURIComponent(id)}/${action}`, undefined, 18_000),
@@ -468,7 +468,7 @@ async function remoteMicroservice(session: FrontendSession, args: string[]): Pro
response: summarizeMicroserviceProxyResponse(response, args),
};
}
throw new Error("remote microservice command must be: microservice list | status <id> | health <id> | proxy <id> <path>");
throw new Error("remote microservice command must be: microservice list | status <id> | health <id> | diagnostics <id> | tunnel-self-test <id> | proxy <id> <path>");
}
async function remoteCodeQueue(session: FrontendSession, args: string[]): Promise<unknown> {
@@ -559,7 +559,7 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
emitRemoteJson(name, {
transport: "frontend",
baseUrl: session.baseUrl,
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex judge <taskId> --attempt N", "network perf"],
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex judge <taskId> --attempt N", "network perf"],
});
return 0;
}