fix: sync follower sources before native status
This commit is contained in:
@@ -31,6 +31,7 @@ bun scripts/cli.ts cicd branch-follower status
|
||||
|
||||
- CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 部署必须走受控 CLI;不要用裸 `kubectl`、`argo`、`tkn`、`curl` 当正式控制入口。
|
||||
- CI/CD source authority 只能来自 Kubernetes 托管的 git-mirror snapshot:受控命令先同步 GitHub refs 到 k8s git-mirror,再创建/读取不可变 `refs/unidesk/snapshots/.../<commit>` stage ref;build/status/publish 只消费该 snapshot,host worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。
|
||||
- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRef:branch-follower controller 读 `config/cicd-branch-followers.yaml#controller.source.githubSsh`,runtime git-mirror 读 owning lane/control-plane YAML 的 host proxy 和 `githubTransport`;禁止依赖未声明 host env、trans proxy、裸直连 GitHub 或 CLI 输出解析。
|
||||
- `cicd branch-follower` 的自动跟随全过程不得读取或挂载 host worktree、target dev dir、`.worktree/*` 或 local git checkout;controller pod/一次性 reconcile Job 只能用 k8s git-mirror cache、Tekton PipelineRun、Argo Application、runtime workload 和 EmptyDir 执行,状态以 K8s ConfigMap/Lease 承载的 native observation 为准,不得解析下游 CLI 输出。
|
||||
- CI/CD、rollout、publish、image build 和部署链路禁止新引入 Docker 依赖;不得依赖 Docker socket、Docker daemon、host Docker、`docker build`、`docker push` 或等价 Docker-only 路径。
|
||||
- 正式 CI/CD、publish、image build 和 rollout 必须走 Tekton Task/Pipeline/PipelineRun 承担 CI,并通过 GitOps/Argo 承担部署收敛;普通 Kubernetes Job 只允许用于 bounded helper、source sync、diagnostic、cleanup 或 bootstrap,不得作为正式发布、镜像构建或 rollout 入口。
|
||||
|
||||
@@ -21,6 +21,7 @@ bun scripts/cli.ts cicd branch-follower logs --follower <id>
|
||||
|
||||
- Follower decisions must not read host source worktrees, target dev directories, `.worktree/*`, local git state, or direct GitHub branch refs.
|
||||
- Controller pods use EmptyDir plus the YAML-declared k8s git-mirror cache PVC, sync GitHub refs from inside Kubernetes, clone UniDesk controller source from `/cache`, then run the CLI with the mounted registry.
|
||||
- All GitHub/Git egress used by branch-follower source sync, adapter git-mirror sync/flush, PR/merge closeout helpers and controller bootstrap must resolve proxy settings from YAML/sourceRef. Controller GitHub SSH uses `config/cicd-branch-followers.yaml#controller.source.githubSsh`; runtime adapters use their owning lane/control-plane YAML host proxy refs such as `config/hwlab-node-control-plane.yaml#nodes.<NODE>.egressProxy`. Do not rely on undeclared pod env, host shell proxy variables, direct GitHub transport, or trans-side proxy defaults.
|
||||
- Runtime source commits, build contexts, publish inputs and closeout status remain owned by each adapter's k8s git-mirror snapshot and runtime objects.
|
||||
- Trigger adapters communicate through the Kubernetes API with the controller service account. Formal triggering, observation and closeout must not depend on downstream CLI stdout parsing, host worktrees, or operator shell state.
|
||||
- Dirty, stale, or missing-dependency host worktrees are non-authoritative and must not change observed sha, trigger sha, PipelineRun, GitOps, or status output.
|
||||
|
||||
@@ -21,6 +21,8 @@ bun scripts/cli.ts hwlab nodes git-mirror flush --node <node> --lane v03 --confi
|
||||
|
||||
`cicd branch-follower` 已把 `sync` 和 `flush` 作为通用 Kubernetes-native stage 接入 HWLAB/AgentRun 自动跟随:trigger 前先 sync 并创建 immutable source snapshot,GitOps publish 后必须 flush,status/closeout 以 mirror cache ref 和 `pendingFlush=false`/`githubInSync=true` 为准。正常自动跟随不需要 operator 手动清理旧状态;旧 ConfigMap 或历史 PipelineRun 干扰时只用 `cicd branch-follower cleanup-state --follower <id> --confirm` 做显式 cleanup。
|
||||
|
||||
GitHub/GitHub SSH/HTTPS 相关 egress 必须从 YAML-first host proxy 配置进入 git-mirror Job/Pod:node-scoped lanes 读取 `config/hwlab-node-control-plane.yaml#nodes.<NODE>.egressProxy` 和 `targets.<id>.gitMirror.egressProxy/githubTransport`,branch-follower controller 读取 `config/cicd-branch-followers.yaml#controller.source.githubSsh`。状态和日志应披露 `source=yaml`、proxy mode、object/key/sourceRef 摘要和 redacted fingerprint,不得依赖未声明的 host env、trans proxy、裸直连 GitHub 或 CLI 输出解析。
|
||||
|
||||
PipelineRun `gitops-promote` 如果报 git mirror 控制面漂移、refs 不一致或 flush/publish 未完成,优先按当前 `devops-infra/git-mirror.yaml` 收敛:先 `git-mirror apply --confirm`,再 `git-mirror sync --confirm --wait`,然后用 `control-plane cleanup-runs --pipeline-run <failed-run> --confirm` 受控清理失败 PipelineRun 后重试。旧 branch/path allowlist gate 已删除,不要恢复旧 hook、直接 `kubectl delete`、手工 patch pod 内 hook 或绕过 `flush`。
|
||||
|
||||
手动 trigger closeout 不能只看 PipelineRun `Completed`。必须继续查 `control-plane status --pipeline-run <name>` 和 `git-mirror status`;node-scoped `trigger-current --confirm --wait` 会自动做必要的 mirror pre/post flush,但 closeout 仍要确认最终 `pendingFlush=false`、`githubInSync=true`。如果 lower-level 手工路径或旧 job 留下 `pendingFlush=true`,执行 `git-mirror flush --confirm --wait` 到 `pendingFlush=false`。
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower native object bundle reader.
|
||||
// Responsibility: read compact Kubernetes-native source/Tekton/Argo/runtime state through file-backed scripts.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
import type { CommandResult } from "./command";
|
||||
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
|
||||
import type { BranchFollowerRegistry, FollowerSpec, NativeObjectBundle, ParsedOptions } from "./cicd-types";
|
||||
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
|
||||
import { redactText, shQuote } from "./platform-infra-ops-library";
|
||||
|
||||
type KubeScriptRunner = (registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number) => CommandResult;
|
||||
|
||||
const NATIVE_BUNDLE_SCRIPT_NAMES = [
|
||||
"read-native-bundle.sh",
|
||||
"kube-get.mjs",
|
||||
"compact-native-object.mjs",
|
||||
"compact-git-mirror.mjs",
|
||||
"plan-artifacts.mjs",
|
||||
] as const;
|
||||
|
||||
export function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions, timeoutSeconds: number, runKubeScript: KubeScriptRunner): NativeObjectBundle {
|
||||
const native = follower.nativeStatus;
|
||||
const source = native.source;
|
||||
const tekton = native.tekton;
|
||||
const argo = native.argo;
|
||||
const runtime = native.runtime;
|
||||
const gitopsBranch = nativeGitMirrorGitopsBranch(follower);
|
||||
const workloadRefs = (runtime?.workloads ?? []).map((workload, index) => {
|
||||
const resource = workload.kind === "Deployment" ? "deployments" : "statefulsets";
|
||||
return `workload${index}\t/apis/apps/v1/namespaces/${runtime?.namespace ?? follower.target.namespace}/${resource}/${workload.name}`;
|
||||
});
|
||||
const workloadRefsText = workloadRefs.length === 0 ? "" : `${workloadRefs.join("\n")}\n`;
|
||||
const script = [
|
||||
"set +e",
|
||||
"tmpdir=$(mktemp -d)",
|
||||
"cleanup() { rm -rf \"$tmpdir\"; }",
|
||||
"trap cleanup EXIT INT TERM",
|
||||
nativeBundleScriptLoadShell(),
|
||||
`REPO_PATH=${shQuote(source.repoPath)}`,
|
||||
`SOURCE_BRANCH=${shQuote(follower.source.branch)}`,
|
||||
`REPOSITORY=${shQuote(follower.source.repository)}`,
|
||||
`SNAPSHOT_PREFIX=${shQuote(follower.source.snapshotPrefix)}`,
|
||||
`GITOPS_BRANCH=${shQuote(gitopsBranch ?? "")}`,
|
||||
`TEKTON_NAMESPACE=${shQuote(tekton?.namespace ?? "")}`,
|
||||
`PIPELINE_RUN_PREFIX=${shQuote(tekton?.pipelineRunPrefix ?? "")}`,
|
||||
`ARGO_NAMESPACE=${shQuote(argo?.namespace ?? "")}`,
|
||||
`ARGO_APPLICATION=${shQuote(argo?.application ?? "")}`,
|
||||
`WORKLOAD_REFS_B64=${shQuote(Buffer.from(workloadRefsText, "utf8").toString("base64"))}`,
|
||||
"NATIVE_CICD_SCRIPT_DIR=\"$tmpdir\"",
|
||||
"export NATIVE_CICD_SCRIPT_DIR REPO_PATH SOURCE_BRANCH REPOSITORY SNAPSHOT_PREFIX GITOPS_BRANCH TEKTON_NAMESPACE PIPELINE_RUN_PREFIX ARGO_NAMESPACE ARGO_APPLICATION WORKLOAD_REFS_B64",
|
||||
"\"$tmpdir/read-native-bundle.sh\"",
|
||||
].join("\n");
|
||||
const startedAt = Date.now();
|
||||
const result = runKubeScript(registry, options, script, "", timeoutSeconds * 1000);
|
||||
const parsed = parseNativeBundleLines(result.stdout);
|
||||
const sourceRecord = asOptionalRecord(parsed.objects.source);
|
||||
return {
|
||||
ok: result.exitCode === 0 && sourceRecord !== null && parsed.fatalErrors.length === 0,
|
||||
source: sourceRecord,
|
||||
gitMirror: asOptionalRecord(parsed.objects.gitMirror),
|
||||
pipelineRun: asOptionalRecord(parsed.objects.pipelineRun),
|
||||
taskRuns: asOptionalRecord(parsed.objects.taskRuns),
|
||||
planArtifacts: asOptionalRecord(parsed.objects.planArtifacts),
|
||||
argoApplication: asOptionalRecord(parsed.objects.argoApplication),
|
||||
workloads: Object.entries(parsed.objects)
|
||||
.filter(([key]) => /^workload\d+$/u.test(key))
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([, value]) => asOptionalRecord(value))
|
||||
.filter((item): item is Record<string, unknown> => item !== null),
|
||||
errors: [
|
||||
...parsed.errors,
|
||||
...(result.exitCode === 0 ? [] : [`native bundle command failed: exitCode=${result.exitCode}`]),
|
||||
...parsed.fatalErrors,
|
||||
],
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
stdoutTail: redactText(tailText(result.stdout, 1000)),
|
||||
stderrTail: redactText(tailText(result.stderr, 1000)),
|
||||
};
|
||||
}
|
||||
|
||||
function nativeBundleScriptLoadShell(): string {
|
||||
return nativeScriptLoadShell(NATIVE_BUNDLE_SCRIPT_NAMES);
|
||||
}
|
||||
|
||||
function nativeScriptLoadShell(names: readonly string[]): string {
|
||||
return names.map((name) => {
|
||||
const encoded = Buffer.from(readFileSync(rootPath("scripts/native/cicd", name), "utf8"), "utf8").toString("base64");
|
||||
return [
|
||||
`printf '%s' ${shQuote(encoded)} | base64 -d > "$tmpdir/${name}"`,
|
||||
`chmod +x "$tmpdir/${name}"`,
|
||||
].join("\n");
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function parseNativeBundleLines(stdout: string): { objects: Record<string, unknown>; errors: string[]; fatalErrors: string[] } {
|
||||
const objects: Record<string, unknown> = {};
|
||||
const errors: string[] = [];
|
||||
const fatalErrors: string[] = [];
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
if (!line.startsWith("UNIDESK_NATIVE_")) continue;
|
||||
const [kind, key, payload] = line.split("\t");
|
||||
if (kind === undefined || key === undefined || payload === undefined) continue;
|
||||
const decoded = Buffer.from(payload, "base64").toString("utf8").trim();
|
||||
if (kind === "UNIDESK_NATIVE_JSON") {
|
||||
const parsed = parseJsonObject(decoded);
|
||||
if (parsed !== null) objects[key] = parsed;
|
||||
else errors.push(`${key}: invalid native JSON payload`);
|
||||
} else if (kind === "UNIDESK_NATIVE_ERROR") {
|
||||
const message = `${key}: ${redactText(tailText(decoded, 500)) || "not found"}`;
|
||||
errors.push(message);
|
||||
if (key === "source") fatalErrors.push(message);
|
||||
}
|
||||
}
|
||||
return { objects, errors, fatalErrors };
|
||||
}
|
||||
|
||||
function nativeGitMirrorGitopsBranch(follower: FollowerSpec): string | null {
|
||||
if (follower.adapter === "hwlab-node-runtime") {
|
||||
return hwlabRuntimeLaneSpecForNode(follower.target.lane, follower.target.node).gitopsBranch;
|
||||
}
|
||||
if (follower.adapter === "agentrun-yaml-lane") {
|
||||
return resolveAgentRunLaneTarget({ node: follower.target.node, lane: follower.target.lane }).spec.gitops.branch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseJsonObject(text: string): Record<string, unknown> | null {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length === 0) return null;
|
||||
try {
|
||||
return asOptionalRecord(JSON.parse(trimmed));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function tailText(text: string, maxChars: number): string {
|
||||
return text.length <= maxChars ? text : text.slice(text.length - maxChars);
|
||||
}
|
||||
+22
-112
@@ -22,8 +22,9 @@ import { transPath } from "./hwlab-node/runtime-common";
|
||||
import { configRefGraph, resolveConfigRefString } from "./ops/config-refs";
|
||||
import { renderControllerManifests, renderControllerReconcileJob, waitForJobShell } from "./cicd-controller-render";
|
||||
import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
|
||||
import { readNativeObjectBundle } from "./cicd-native-bundle";
|
||||
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
|
||||
import type { AdapterSummary, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeObjectBundle, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types";
|
||||
import type { AdapterSummary, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types";
|
||||
import {
|
||||
arrayField,
|
||||
asRecord,
|
||||
@@ -1472,8 +1473,12 @@ function runNativeArgoRefresh(argo: NonNullable<NativeStatusSpec["argo"]>): Comm
|
||||
|
||||
async function readAdapterStatus(registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions): Promise<AdapterSummary> {
|
||||
const timeoutSeconds = options.timeoutSeconds ?? follower.budgets.statusSeconds;
|
||||
const bundle = readNativeObjectBundle(registry, follower, options, timeoutSeconds);
|
||||
const observedSha = stringOrNull(bundle.source?.commit);
|
||||
const startedAt = Date.now();
|
||||
const sourceSync = runNativeSourceObservationSync(registry, follower, options, Math.min(timeoutSeconds, follower.budgets.sourceSyncSeconds));
|
||||
const sourceSyncDetail = sourceSync === null || sourceSync.result.ok ? null : redactText(tailText(sourceSync.result.conditionMessage ?? sourceSync.result.logsTail ?? "unknown", 800));
|
||||
const sourceSyncError = sourceSyncDetail === null ? null : `native source sync failed: ${sourceSyncDetail}`;
|
||||
const bundle = readNativeObjectBundle(registry, follower, options, remainingSeconds(startedAt, timeoutSeconds), runKubeScript);
|
||||
const observedSha = sourceSyncError === null ? stringOrNull(bundle.source?.commit) : null;
|
||||
const runtimeTargetSha = runtimeTargetShaFromWorkloads(follower.nativeStatus.runtime, bundle.workloads);
|
||||
const pipelineRunName = stringOrNull(asOptionalRecord(bundle.pipelineRun?.metadata)?.name) ?? expectedPipelineRunName(follower, observedSha);
|
||||
const pipelineRunPresent = follower.nativeStatus.tekton === null ? null : bundle.pipelineRun !== null;
|
||||
@@ -1497,8 +1502,9 @@ async function readAdapterStatus(registry: BranchFollowerRegistry, follower: Fol
|
||||
&& argoReady !== false
|
||||
&& runtimeReady !== false;
|
||||
const targetSha = hasRuntimeTarget && runtimeTargetSha === observedSha && aligned ? runtimeTargetSha : runtimeTargetSha;
|
||||
const ok = bundle.ok;
|
||||
const ok = bundle.ok && sourceSyncError === null;
|
||||
const phase = inferPhase(ok, aligned, observedSha, targetSha, bundle.timedOut);
|
||||
const errors = sourceSyncError === null ? bundle.errors : [sourceSyncError, ...bundle.errors];
|
||||
return {
|
||||
ok,
|
||||
command: "native:k8s-git-mirror+tekton+argocd+runtime",
|
||||
@@ -1518,10 +1524,11 @@ async function readAdapterStatus(registry: BranchFollowerRegistry, follower: Fol
|
||||
gitMirrorReady,
|
||||
argoReady,
|
||||
runtimeReady,
|
||||
errors: bundle.errors,
|
||||
errors,
|
||||
}),
|
||||
payload: {
|
||||
source: bundle.source,
|
||||
sourceSync: sourceSync === null ? null : sourceSync.result,
|
||||
gitMirror: nativeGitMirrorSummary(bundle.gitMirror),
|
||||
tekton: nativePipelineRunSummary(bundle.pipelineRun),
|
||||
taskRuns: bundle.taskRuns,
|
||||
@@ -1529,7 +1536,7 @@ async function readAdapterStatus(registry: BranchFollowerRegistry, follower: Fol
|
||||
argo: nativeArgoSummary(bundle.argoApplication),
|
||||
runtime: nativeRuntimeSummary(follower.nativeStatus.runtime, bundle.workloads, observedSha),
|
||||
timings: { statusRead: { elapsedMs: bundle.elapsedMs, budgetSeconds: timeoutSeconds } },
|
||||
errors: bundle.errors,
|
||||
errors,
|
||||
statusAuthority: "k8s-native",
|
||||
parsedDownstreamCliOutput: false,
|
||||
},
|
||||
@@ -1538,6 +1545,13 @@ async function readAdapterStatus(registry: BranchFollowerRegistry, follower: Fol
|
||||
};
|
||||
}
|
||||
|
||||
function runNativeSourceObservationSync(registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions, timeoutSeconds: number): { jobName: string; namespace: string; result: NativeK8sJobResult } | null {
|
||||
if (!options.inCluster) return null;
|
||||
if (follower.adapter !== "hwlab-node-runtime" && follower.adapter !== "agentrun-yaml-lane") return null;
|
||||
const bucket = Math.floor(Date.now() / (registry.controller.loop.intervalSeconds * 1000)).toString(36);
|
||||
return runNativeGitMirrorStage(registry, follower, "source", "sync", timeoutSeconds, `source-${bucket}`);
|
||||
}
|
||||
|
||||
function inferPhase(ok: boolean, aligned: boolean | null, observedSha: string | null, targetSha: string | null, timedOut: boolean): BranchFollowerPhase {
|
||||
if (!ok || timedOut) return "Blocked";
|
||||
if (aligned === true) return "Succeeded";
|
||||
@@ -1562,112 +1576,6 @@ function nativeStatusMessage(ok: boolean, phase: BranchFollowerPhase, observedSh
|
||||
return "k8s-native status did not expose observed source sha";
|
||||
}
|
||||
|
||||
function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions, timeoutSeconds: number): NativeObjectBundle {
|
||||
const native = follower.nativeStatus;
|
||||
const source = native.source;
|
||||
const tekton = native.tekton;
|
||||
const argo = native.argo;
|
||||
const runtime = native.runtime;
|
||||
const gitopsBranch = nativeGitMirrorGitopsBranch(follower);
|
||||
const workloadRefs = (runtime?.workloads ?? []).map((workload, index) => {
|
||||
const resource = workload.kind === "Deployment" ? "deployments" : "statefulsets";
|
||||
return `workload${index}\t/apis/apps/v1/namespaces/${runtime?.namespace ?? follower.target.namespace}/${resource}/${workload.name}`;
|
||||
});
|
||||
const workloadRefsText = workloadRefs.length === 0 ? "" : `${workloadRefs.join("\n")}\n`;
|
||||
const script = [
|
||||
"set +e",
|
||||
"tmpdir=$(mktemp -d)",
|
||||
"cleanup() { rm -rf \"$tmpdir\"; }",
|
||||
"trap cleanup EXIT INT TERM",
|
||||
nativeBundleScriptLoadShell(),
|
||||
`REPO_PATH=${shQuote(source.repoPath)}`,
|
||||
`SOURCE_BRANCH=${shQuote(follower.source.branch)}`,
|
||||
`REPOSITORY=${shQuote(follower.source.repository)}`,
|
||||
`SNAPSHOT_PREFIX=${shQuote(follower.source.snapshotPrefix)}`,
|
||||
`GITOPS_BRANCH=${shQuote(gitopsBranch ?? "")}`,
|
||||
`TEKTON_NAMESPACE=${shQuote(tekton?.namespace ?? "")}`,
|
||||
`PIPELINE_RUN_PREFIX=${shQuote(tekton?.pipelineRunPrefix ?? "")}`,
|
||||
`ARGO_NAMESPACE=${shQuote(argo?.namespace ?? "")}`,
|
||||
`ARGO_APPLICATION=${shQuote(argo?.application ?? "")}`,
|
||||
`WORKLOAD_REFS_B64=${shQuote(Buffer.from(workloadRefsText, "utf8").toString("base64"))}`,
|
||||
"NATIVE_CICD_SCRIPT_DIR=\"$tmpdir\"",
|
||||
"export NATIVE_CICD_SCRIPT_DIR REPO_PATH SOURCE_BRANCH REPOSITORY SNAPSHOT_PREFIX GITOPS_BRANCH TEKTON_NAMESPACE PIPELINE_RUN_PREFIX ARGO_NAMESPACE ARGO_APPLICATION WORKLOAD_REFS_B64",
|
||||
"\"$tmpdir/read-native-bundle.sh\"",
|
||||
].join("\n");
|
||||
const startedAt = Date.now();
|
||||
const result = runKubeScript(registry, options, script, "", timeoutSeconds * 1000);
|
||||
const parsed = parseNativeBundleLines(result.stdout);
|
||||
const sourceRecord = asOptionalRecord(parsed.objects.source);
|
||||
return {
|
||||
ok: result.exitCode === 0 && sourceRecord !== null && parsed.fatalErrors.length === 0,
|
||||
source: sourceRecord,
|
||||
gitMirror: asOptionalRecord(parsed.objects.gitMirror),
|
||||
pipelineRun: asOptionalRecord(parsed.objects.pipelineRun),
|
||||
taskRuns: asOptionalRecord(parsed.objects.taskRuns),
|
||||
planArtifacts: asOptionalRecord(parsed.objects.planArtifacts),
|
||||
argoApplication: asOptionalRecord(parsed.objects.argoApplication),
|
||||
workloads: Object.entries(parsed.objects)
|
||||
.filter(([key]) => /^workload\d+$/u.test(key))
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([, value]) => asOptionalRecord(value))
|
||||
.filter((item): item is Record<string, unknown> => item !== null),
|
||||
errors: [
|
||||
...parsed.errors,
|
||||
...(result.exitCode === 0 ? [] : [`native bundle command failed: exitCode=${result.exitCode}`]),
|
||||
...parsed.fatalErrors,
|
||||
],
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
stdoutTail: redactText(tailText(result.stdout, 1000)),
|
||||
stderrTail: redactText(tailText(result.stderr, 1000)),
|
||||
};
|
||||
}
|
||||
|
||||
const NATIVE_BUNDLE_SCRIPT_NAMES = [
|
||||
"read-native-bundle.sh",
|
||||
"kube-get.mjs",
|
||||
"compact-native-object.mjs",
|
||||
"compact-git-mirror.mjs",
|
||||
"plan-artifacts.mjs",
|
||||
] as const;
|
||||
|
||||
function nativeBundleScriptLoadShell(): string {
|
||||
return nativeScriptLoadShell(NATIVE_BUNDLE_SCRIPT_NAMES);
|
||||
}
|
||||
|
||||
function nativeScriptLoadShell(names: readonly string[]): string {
|
||||
return names.map((name) => {
|
||||
const encoded = Buffer.from(readFileSync(rootPath("scripts/native/cicd", name), "utf8"), "utf8").toString("base64");
|
||||
return [
|
||||
`printf '%s' ${shQuote(encoded)} | base64 -d > "$tmpdir/${name}"`,
|
||||
`chmod +x "$tmpdir/${name}"`,
|
||||
].join("\n");
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function parseNativeBundleLines(stdout: string): { objects: Record<string, unknown>; errors: string[]; fatalErrors: string[] } {
|
||||
const objects: Record<string, unknown> = {};
|
||||
const errors: string[] = [];
|
||||
const fatalErrors: string[] = [];
|
||||
for (const line of stdout.split(/\r?\n/u)) {
|
||||
if (!line.startsWith("UNIDESK_NATIVE_")) continue;
|
||||
const [kind, key, payload] = line.split("\t");
|
||||
if (kind === undefined || key === undefined || payload === undefined) continue;
|
||||
const decoded = Buffer.from(payload, "base64").toString("utf8").trim();
|
||||
if (kind === "UNIDESK_NATIVE_JSON") {
|
||||
const parsed = parseJsonObject(decoded);
|
||||
if (parsed !== null) objects[key] = parsed;
|
||||
else errors.push(`${key}: invalid native JSON payload`);
|
||||
} else if (kind === "UNIDESK_NATIVE_ERROR") {
|
||||
const message = `${key}: ${redactText(tailText(decoded, 500)) || "not found"}`;
|
||||
errors.push(message);
|
||||
if (key === "source") fatalErrors.push(message);
|
||||
}
|
||||
}
|
||||
return { objects, errors, fatalErrors };
|
||||
}
|
||||
|
||||
function expectedPipelineRunName(follower: FollowerSpec, observedSha: string | null): string | null {
|
||||
if (observedSha === null || follower.nativeStatus.tekton === null) return null;
|
||||
return `${follower.nativeStatus.tekton.pipelineRunPrefix}-${shortSha(observedSha)}`;
|
||||
@@ -2340,6 +2248,8 @@ function stageTimingsFromNativePayload(payload: Record<string, unknown> | null):
|
||||
const stages: StageTiming[] = [];
|
||||
const statusRead = asOptionalRecord(asOptionalRecord(payload.timings)?.statusRead);
|
||||
stages.push(stageTiming("status-read", "ok", secondsFromMs(numberOrNull(statusRead?.elapsedMs)), numberOrNull(statusRead?.budgetSeconds), "native-status", null));
|
||||
const sourceSyncStage = k8sJobTiming("git-mirror-sync", asOptionalRecord(payload.sourceSync));
|
||||
if (sourceSyncStage !== null) stages.push(sourceSyncStage);
|
||||
const gitMirror = asOptionalRecord(payload.gitMirror);
|
||||
if (gitMirror !== null) {
|
||||
const status = gitMirror.pendingFlush === true ? "pending-flush" : gitMirror.githubInSync === true && gitMirror.sourceSnapshotReady === true ? "ready" : "not-ready";
|
||||
|
||||
Reference in New Issue
Block a user