fix: move branch follower targets to jd01

This commit is contained in:
Codex
2026-07-03 04:57:40 +00:00
parent cb93348c29
commit c866f8318d
4 changed files with 69 additions and 28 deletions
+47 -6
View File
@@ -1,7 +1,7 @@
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
// Responsibility: YAML-first K8s branch-follower controller, status, and adapter orchestration.
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { isAbsolute } from "node:path";
import { repoRoot, rootPath, type UniDeskConfig } from "./config";
import { runCommand, type CommandResult } from "./command";
@@ -579,11 +579,14 @@ function buildPlan(registry: BranchFollowerRegistry, options: ParsedOptions): Re
async function applyController(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
const manifests = renderControllerManifests(registry);
const manifestYaml = `${manifests.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`;
const manifestBase64 = Buffer.from(manifestYaml, "utf8").toString("base64");
const waitSeconds = options.timeoutSeconds ?? registry.controller.budgets.applyWaitSeconds;
const script = [
"set -eu",
"tmp=$(mktemp)",
"cat >\"$tmp\"",
"base64 -d >\"$tmp\" <<'UNIDESK_CICD_BRANCH_FOLLOWER_MANIFEST_B64'",
manifestBase64,
"UNIDESK_CICD_BRANCH_FOLLOWER_MANIFEST_B64",
options.dryRun
? `kubectl apply --dry-run=server --field-manager=${shQuote(registry.controller.fieldManager)} -f "$tmp"`
: `kubectl apply --server-side --force-conflicts --field-manager=${shQuote(registry.controller.fieldManager)} -f "$tmp"`,
@@ -592,7 +595,7 @@ async function applyController(registry: BranchFollowerRegistry, options: Parsed
: "true",
`kubectl -n ${shQuote(registry.controller.namespace)} get deploy/${shQuote(registry.controller.deploymentName)} cm/${shQuote(registry.controller.configMapName)} cm/${shQuote(registry.controller.stateConfigMapName)} lease/${shQuote(registry.controller.leaseName)} -o wide 2>/dev/null || true`,
].join("\n");
const result = runKubeScript(registry, options, script, manifestYaml, (waitSeconds + 15) * 1000);
const result = runKubeScript(registry, options, script, "", (waitSeconds + 15) * 1000);
return {
ok: result.exitCode === 0,
action: "apply",
@@ -849,7 +852,8 @@ async function readAdapterStatus(follower: FollowerSpec, options: ParsedOptions)
const spec = follower.commands.status;
const timeoutSeconds = options.timeoutSeconds ?? spec.timeoutSeconds;
const result = runCommand(spec.argv, repoRoot, { timeoutMs: timeoutSeconds * 1000 });
const payload = parseJsonObject(result.stdout) ?? parseJsonObject(result.stderr);
const rawPayload = parseJsonObject(result.stdout) ?? parseJsonObject(result.stderr);
const payload = recoverDumpPayload(rawPayload) ?? rawPayload;
const body = payload === null ? null : unwrapEnvelope(payload);
const observedSha = firstStringPath(body, [
"summary.sourceCommit",
@@ -864,6 +868,7 @@ async function readAdapterStatus(follower: FollowerSpec, options: ParsedOptions)
"selectedCommit",
"sourceCommit",
"observedSha",
"alignment.sourceCommit",
], ["sourceCommit", "observedSha", "observedCommit", "selectedCommit", "selectedSourceCommit"]);
const targetSha = firstStringPath(body, [
"summary.targetCommit",
@@ -878,6 +883,10 @@ async function readAdapterStatus(follower: FollowerSpec, options: ParsedOptions)
"deployedCommit",
"currentCommit",
"targetSha",
"summary.runtimeAlignment.managerSourceCommit",
"alignment.runtimeAlignment.managerSourceCommit",
"runtime.manager.sourceCommit",
"manager.sourceCommit",
], ["targetCommit", "targetSha", "runtimeCommit", "gitopsCommit", "deployedCommit", "currentCommit"]);
const lastTriggeredSha = firstStringPath(body, [
"summary.lastTriggeredSha",
@@ -894,10 +903,12 @@ async function readAdapterStatus(follower: FollowerSpec, options: ParsedOptions)
], ["lastSucceededSha", "lastSucceededCommit", "succeededCommit"]);
const pipelineRun = firstStringPath(body, [
"summary.pipelineRun",
"summary.expectedPipelineRun",
"alignment.expectedPipelineRun",
"pipelineRun.name",
"pipelineRun",
"latestPipelineRun.name",
], ["pipelineRun", "pipelineRunName"]);
], ["pipelineRun", "pipelineRunName", "expectedPipelineRun"]);
const inFlightJob = firstStringPath(body, [
"summary.inFlightJob",
"job.id",
@@ -1001,7 +1012,7 @@ function readK8sState(registry: BranchFollowerRegistry, options: ParsedOptions):
const deploymentResult = kubeJson(registry, options, `kubectl -n ${shQuote(registry.controller.namespace)} get deploy ${shQuote(registry.controller.deploymentName)} -o json`, 10_000);
const leaseResult = kubeJson(registry, options, `kubectl -n ${shQuote(registry.controller.namespace)} get lease ${shQuote(registry.controller.leaseName)} -o json`, 10_000);
const podSelector = labelSelector(registry.controller.labels);
const podsResult = kubeJson(registry, options, `kubectl -n ${shQuote(registry.controller.namespace)} get pods -l ${shQuote(podSelector)} -o json`, 10_000);
const podsResult = kubePodList(registry, options, podSelector);
if (!stateResult.ok && !isNotFoundText(stateResult.error)) errors.push(`state configmap: ${stateResult.error}`);
if (!deploymentResult.ok && !isNotFoundText(deploymentResult.error)) errors.push(`deployment: ${deploymentResult.error}`);
if (!leaseResult.ok && !isNotFoundText(leaseResult.error)) errors.push(`lease: ${leaseResult.error}`);
@@ -1037,6 +1048,21 @@ function kubeJson(registry: BranchFollowerRegistry, options: ParsedOptions, comm
};
}
function kubePodList(registry: BranchFollowerRegistry, options: ParsedOptions, selector: string): { ok: boolean; value: Record<string, unknown> | null; error: string } {
const command = `kubectl -n ${shQuote(registry.controller.namespace)} get pods -l ${shQuote(selector)} -o name`;
const result = runKubeScript(registry, options, `set -eu\n${command}`, "", 10_000);
const names = result.stdout
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => line.replace(/^pod\//u, ""));
return {
ok: result.exitCode === 0,
value: result.exitCode === 0 ? { items: names.map((name) => ({ metadata: { name } })) } : null,
error: redactText(tailText(result.stderr || result.stdout, 800)),
};
}
function runKubeScript(registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number): CommandResult {
if (options.controller) {
return runCommand(["sh", "-lc", script], repoRoot, { input, timeoutMs });
@@ -1166,12 +1192,14 @@ function controllerLoopScript(): string {
"while true; do",
" started_at=$(date -Iseconds)",
" echo \"branch-follower loop started ${started_at}\"",
" cd /work",
" rm -rf /work/unidesk",
" git clone --depth=1 --branch \"${UNIDESK_CONTROLLER_SOURCE_BRANCH}\" \"${UNIDESK_CONTROLLER_GIT_MIRROR_READ_URL}\" /work/unidesk",
" cp /etc/unidesk-cicd-branch-follower/cicd-branch-followers.yaml /work/unidesk/config/cicd-branch-followers.yaml",
" cd /work/unidesk",
" bun scripts/cli.ts cicd branch-follower run-once --all --confirm --wait --controller --config config/cicd-branch-followers.yaml --timeout-seconds \"${timeout}\" --json || true",
" echo \"branch-follower loop finished $(date -Iseconds)\"",
" cd /work",
" sleep \"${interval}\"",
"done",
].join("\n");
@@ -1278,6 +1306,19 @@ function unwrapEnvelope(payload: Record<string, unknown>): Record<string, unknow
return data ?? payload;
}
function recoverDumpPayload(payload: Record<string, unknown> | null): Record<string, unknown> | null {
if (payload === null) return null;
const data = asOptionalRecord(payload.data);
const dump = asOptionalRecord(data?.dump) ?? asOptionalRecord(payload.dump);
const path = stringOrNull(dump?.path);
if (path === null || !existsSync(path)) return payload;
try {
return parseJsonObject(readFileSync(path, "utf8")) ?? payload;
} catch {
return payload;
}
}
function firstStringPath(root: Record<string, unknown> | null, paths: string[], fallbackKeys: string[] = []): string | null {
if (root === null) return null;
for (const path of paths) {