From a111d9195d14c1d51ced849f7cebf3d322b153fe Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 05:15:17 +0000 Subject: [PATCH] fix(cicd): wire hwlab branch-follower health gate --- scripts/native/cicd/branch-follower-gate.mjs | 16 +++++++++++++++- scripts/src/cicd-gates.ts | 14 +++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/scripts/native/cicd/branch-follower-gate.mjs b/scripts/native/cicd/branch-follower-gate.mjs index 2c76c240..f0ebaee2 100644 --- a/scripts/native/cicd/branch-follower-gate.mjs +++ b/scripts/native/cicd/branch-follower-gate.mjs @@ -186,12 +186,26 @@ async function runtimeSummary(expected) { async function healthSummary() { if (!healthUrl) return { ok: null, reason: "health-url-not-configured" }; - const targets = [`${healthUrl.replace(/\/+$/u, "")}/health/readiness`, `${healthUrl.replace(/\/+$/u, "")}/health/live`]; + const targets = healthProbeTargets(healthUrl); const probes = []; for (const url of targets) probes.push(await httpProbe(url)); return { ok: probes.every((probe) => probe.ok), probes }; } +function healthProbeTargets(value) { + const trimmed = value.replace(/\/+$/u, ""); + try { + const parsed = new URL(trimmed); + const pathname = parsed.pathname.replace(/\/+$/u, ""); + if (pathname.endsWith("/health") || pathname.endsWith("/api/health") || pathname.endsWith("/health/readiness") || pathname.endsWith("/health/live")) { + return [trimmed]; + } + } catch { + // Fall back to the historical base-url contract when HEALTH_URL is not a full URL. + } + return [`${trimmed}/health/readiness`, `${trimmed}/health/live`]; +} + function workloadSummary(spec, item, expected) { const status = item?.status || {}; const desired = item?.spec?.replicas ?? 1; diff --git a/scripts/src/cicd-gates.ts b/scripts/src/cicd-gates.ts index 7cb4accb..402c09b3 100644 --- a/scripts/src/cicd-gates.ts +++ b/scripts/src/cicd-gates.ts @@ -5,6 +5,7 @@ import { resolveAgentRunLaneTarget } from "./agentrun-lanes"; import { nativeCicdScriptLoadShell } from "./cicd-native-bundle"; import { waitForJobShell } from "./cicd-controller-render"; import type { BranchFollowerRegistry, FollowerSpec, ParsedOptions } from "./cicd-types"; +import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes"; import { shQuote, redactText } from "./platform-infra-ops-library"; type KubeScriptRunner = (registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number) => CommandResult; @@ -54,7 +55,7 @@ function gateJobManifest(registry: BranchFollowerRegistry, follower: FollowerSpe const labels = { ...registry.controller.labels, "app.kubernetes.io/component": "cicd-gate-job" }; const agentrun = follower.adapter === "agentrun-yaml-lane" ? resolveAgentRunLaneTarget({ node: follower.target.node, lane: follower.target.lane }).spec : null; const gitopsBranch = agentrun?.gitops.branch ?? ""; - const healthUrl = agentrun?.runtime.internalBaseUrl ?? ""; + const healthUrl = gateHealthUrl(follower); const workloads = (follower.nativeStatus.runtime?.workloads ?? []).map((item) => ({ kind: item.kind, name: item.name, sourceCommit: item.sourceCommit })); const gatePolicy = gatePolicyEnv(follower); const gateScript = [ @@ -122,6 +123,17 @@ function gateJobManifest(registry: BranchFollowerRegistry, follower: FollowerSpe }; } +function gateHealthUrl(follower: FollowerSpec): string { + if (follower.adapter === "agentrun-yaml-lane") { + return resolveAgentRunLaneTarget({ node: follower.target.node, lane: follower.target.lane }).spec.runtime.internalBaseUrl; + } + if (follower.adapter === "hwlab-node-runtime") { + const spec = hwlabRuntimeLaneSpecForNode(follower.target.lane, follower.target.node); + return `${spec.publicApiUrl.replace(/\/+$/u, "")}/health`; + } + return ""; +} + function gatePolicyEnv(follower: FollowerSpec): { slowTaskSeconds: number; healthTimeoutMs: number } { if (follower.drillDown === null) { throw new Error(`follower ${follower.id} registry is missing drillDown policy`);