fix(cicd): run refresh gate as native job
This commit is contained in:
+13
-76
@@ -2,7 +2,7 @@
|
||||
// Responsibility: submit bounded target-side gate Jobs and return compact evidence.
|
||||
import type { CommandResult } from "./command";
|
||||
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
|
||||
import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
|
||||
import { nativeHwlabControlPlaneRefreshJobManifest, runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
|
||||
import { nativeCicdScriptLoadShell } from "./cicd-native-bundle";
|
||||
import { waitForJobShell } from "./cicd-controller-render";
|
||||
import type { BranchFollowerRegistry, FollowerSpec, ParsedOptions } from "./cicd-types";
|
||||
@@ -60,9 +60,10 @@ function runTargetControlPlaneRefreshGateJob(registry: BranchFollowerRegistry, f
|
||||
if (follower.adapter !== "hwlab-node-runtime" || options.sourceCommit === null || !options.confirm) {
|
||||
return runControlPlaneRefreshGate(registry, follower, options);
|
||||
}
|
||||
const spec = hwlabRuntimeLaneSpecForNode(follower.target.lane, follower.target.node);
|
||||
const timeoutSeconds = options.timeoutSeconds ?? follower.budgets.controlPlaneRefreshSeconds;
|
||||
const jobName = `bf-gate-${safeName(follower.id)}-control-refresh-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const manifest = controllerGateJobManifest(registry, follower, options, jobName, timeoutSeconds);
|
||||
const jobName = nativeCapabilityJobName(follower.id, "control-plane-refresh", options.sourceCommit);
|
||||
const manifest = nativeHwlabControlPlaneRefreshJobManifest(registry, follower, spec, options.sourceCommit, jobName, timeoutSeconds);
|
||||
const manifestYaml = `${Bun.YAML.stringify(manifest).trim()}\n`;
|
||||
const script = [
|
||||
"set -eu",
|
||||
@@ -76,14 +77,14 @@ function runTargetControlPlaneRefreshGateJob(registry: BranchFollowerRegistry, f
|
||||
].join("\n");
|
||||
const startedAt = Date.now();
|
||||
const command = runKubeScript(registry, options, script, "", (timeoutSeconds + registry.controller.budgets.reconcileTransportGraceSeconds) * 1000);
|
||||
const parsed = command.exitCode === 0 ? parseFirstJsonObject(command.stdout) : null;
|
||||
const ok = command.exitCode === 0 && parsed !== null && parsed.ok !== false;
|
||||
const parsed = command.exitCode === 0 ? parseLastJsonObject(command.stdout, isControlPlaneRefreshResult) : null;
|
||||
const ok = command.exitCode === 0 && parsed !== null && parsed.ok === true;
|
||||
return {
|
||||
ok,
|
||||
action: "gate",
|
||||
gate: options.gate,
|
||||
follower: follower.id,
|
||||
target: { name: jobName, namespace: registry.controller.namespace, execution: "k8s-native-gate-job" },
|
||||
target: { name: jobName, namespace: registry.controller.namespace, execution: "k8s-native-control-plane-refresh" },
|
||||
result: parsed,
|
||||
command: {
|
||||
exitCode: command.exitCode,
|
||||
@@ -159,74 +160,6 @@ function runControlPlaneRefreshGate(registry: BranchFollowerRegistry, follower:
|
||||
};
|
||||
}
|
||||
|
||||
function controllerGateJobManifest(registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions, jobName: string, timeoutSeconds: number): Record<string, unknown> {
|
||||
const labels = { ...registry.controller.labels, "app.kubernetes.io/component": "cicd-gate-job" };
|
||||
const commandArgs = [
|
||||
"bun",
|
||||
"scripts/cli.ts",
|
||||
"cicd",
|
||||
"branch-follower",
|
||||
"gate",
|
||||
"--follower",
|
||||
follower.id,
|
||||
"--gate",
|
||||
"control-plane-refresh",
|
||||
"--source-commit",
|
||||
options.sourceCommit ?? "",
|
||||
"--confirm",
|
||||
"--in-cluster",
|
||||
"--config",
|
||||
"config/cicd-branch-followers.yaml",
|
||||
"--timeout-seconds",
|
||||
String(timeoutSeconds),
|
||||
"--json",
|
||||
];
|
||||
return {
|
||||
apiVersion: "batch/v1",
|
||||
kind: "Job",
|
||||
metadata: { name: jobName, namespace: registry.controller.namespace, labels },
|
||||
spec: {
|
||||
backoffLimit: registry.controller.budgets.reconcileJobBackoffLimit,
|
||||
ttlSecondsAfterFinished: registry.controller.budgets.reconcileJobTtlSeconds,
|
||||
activeDeadlineSeconds: timeoutSeconds + registry.controller.budgets.reconcileJobDeadlineGraceSeconds,
|
||||
template: {
|
||||
metadata: { labels },
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
serviceAccountName: registry.controller.serviceAccountName,
|
||||
volumes: [
|
||||
{ name: "registry", configMap: { name: registry.controller.configMapName, defaultMode: 0o755 } },
|
||||
{ name: "git-mirror-cache", persistentVolumeClaim: { claimName: registry.controller.source.gitMirrorCachePvcName } },
|
||||
{ name: "git-ssh", secret: { secretName: registry.controller.source.githubSsh.secretName, defaultMode: 0o400 } },
|
||||
{ name: "work", emptyDir: {} },
|
||||
],
|
||||
containers: [{
|
||||
name: "gate",
|
||||
image: registry.controller.image,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["/bin/sh", "/etc/unidesk-cicd-branch-follower/controller-one-shot.sh"],
|
||||
args: commandArgs,
|
||||
env: [
|
||||
{ name: "UNIDESK_CONTROLLER_SOURCE_BRANCH", value: registry.controller.source.branch },
|
||||
{ name: "UNIDESK_CONTROLLER_SOURCE_REPOSITORY", value: registry.controller.source.repository },
|
||||
{ name: "UNIDESK_CONTROLLER_SOURCE_SNAPSHOT_PREFIX", value: registry.controller.source.sourceSnapshot.stageRefPrefix.replaceAll("{branch}", registry.controller.source.branch) },
|
||||
{ name: "UNIDESK_CONTROLLER_GITHUB_SSH_PRIVATE_KEY", value: `/git-ssh/${registry.controller.source.githubSsh.privateKeySecretKey}` },
|
||||
{ name: "UNIDESK_CONTROLLER_GITHUB_PROXY_HOST", value: registry.controller.source.githubSsh.proxyHost },
|
||||
{ name: "UNIDESK_CONTROLLER_GITHUB_PROXY_PORT", value: String(registry.controller.source.githubSsh.proxyPort) },
|
||||
],
|
||||
volumeMounts: [
|
||||
{ name: "registry", mountPath: "/etc/unidesk-cicd-branch-follower", readOnly: true },
|
||||
{ name: "git-mirror-cache", mountPath: "/cache" },
|
||||
{ name: "git-ssh", mountPath: "/git-ssh", readOnly: true },
|
||||
{ name: "work", mountPath: "/work" },
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function gateJobManifest(registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions, jobName: string, timeoutSeconds: number): Record<string, unknown> {
|
||||
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;
|
||||
@@ -353,7 +286,7 @@ function parseFirstJsonObject(text: string): Record<string, unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseLastJsonObject(text: string): Record<string, unknown> | null {
|
||||
function parseLastJsonObject(text: string, predicate: (value: Record<string, unknown>) => boolean = () => true): Record<string, unknown> | null {
|
||||
let last: Record<string, unknown> | null = null;
|
||||
let offset = 0;
|
||||
while (offset < text.length) {
|
||||
@@ -374,7 +307,7 @@ function parseLastJsonObject(text: string): Record<string, unknown> | null {
|
||||
else if (char === "}" && --depth === 0) {
|
||||
try {
|
||||
const parsed = JSON.parse(text.slice(start, index + 1)) as unknown;
|
||||
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) last = parsed as Record<string, unknown>;
|
||||
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && predicate(parsed as Record<string, unknown>)) last = parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
// Keep scanning because controller bootstrap may print non-contract JSON-like fragments.
|
||||
}
|
||||
@@ -388,6 +321,10 @@ function parseLastJsonObject(text: string): Record<string, unknown> | null {
|
||||
return last;
|
||||
}
|
||||
|
||||
function isControlPlaneRefreshResult(value: Record<string, unknown>): boolean {
|
||||
return value.ok === true && value.status === "applied" && value.sourceAuthority === "k8s-git-mirror-snapshot";
|
||||
}
|
||||
|
||||
function safeName(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/-+/gu, "-").replace(/^-|-$/gu, "").slice(0, 32);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function runNativeHwlabControlPlaneRefresh(
|
||||
return { jobName, namespace, result };
|
||||
}
|
||||
|
||||
function nativeHwlabControlPlaneRefreshJobManifest(
|
||||
export function nativeHwlabControlPlaneRefreshJobManifest(
|
||||
registry: BranchFollowerRegistry,
|
||||
follower: FollowerSpec,
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
|
||||
Reference in New Issue
Block a user