// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p16-cicd-source-snapshot. // Responsibility: source mirror and publish remote Job/PipelineRun orchestration for web-probe sentinel CI/CD. import { runCommand } from "./command"; import { repoRoot, rootPath } from "./config"; import { readWebProbeSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-config-ref"; import { emitWebProbeSentinelSpan } from "./hwlab-node-web-sentinel-otel"; import { arrayAt, compactCommand, finiteNumberOrNull, monitorWebBuildkitStatePlan, nonEmptyString, numberAt, numberAtNullable, parseJsonObject, record, resolveSentinelChildJson, safeKubernetesSegment, sentinelCliSuffix, sentinelPipelineRunName, sentinelProgressEvent, sentinelSourceSnapshotStageRefPrefix, shellQuote, short, stringAt, stringAtNullable, text, valueAtPath, type SentinelCicdState, type SentinelRemoteJobResult, } from "./hwlab-node-web-sentinel-cicd-shared"; export function runSentinelSourceMirrorSyncJob(state: SentinelCicdState, timeoutSeconds: number): SentinelRemoteJobResult { const prefix = `${stringAt(state.cicd, "builder.jobPrefix")}-source-sync`; const jobName = `${prefix}-${Date.now().toString(36)}`.replace(/[^a-z0-9-]/giu, "-").toLowerCase().slice(0, 63); const manifest = sentinelSourceMirrorSyncJobManifest(state, jobName); const namespace = stringAt(state.cicd, "builder.namespace"); sentinelProgressEvent("sentinel.source-mirror.progress", { phase: "create-job", status: "submitting", jobName, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane }); const created = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", createK8sJobScript(namespace, manifest)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); if (created.exitCode !== 0) { sentinelProgressEvent("sentinel.source-mirror.progress", { phase: "create-job", status: "failed", jobName, node: state.spec.nodeId, lane: state.spec.lane }); return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "create-job", jobName, payload: { ok: false, status: "create-failed", valuesRedacted: true }, create: compactCommand(created), valuesRedacted: true }, "source-mirror"); } const startedAt = Date.now(); const timeoutMs = Math.max(5_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); const warningBudgetMs = Math.max(1, Math.trunc(controlPlaneWaitWarningSeconds(state))) * 1000; let slowWarningSent = false; let polls = 0; let lastProbe: Record = {}; while (Date.now() - startedAt < timeoutMs) { polls += 1; const probeCapture = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", probeK8sJobScript(namespace, jobName)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const probeResolution = resolveSentinelChildJson(probeCapture, "web-probe-sentinel-source-mirror-job-probe"); const probe = probeResolution.parsed ?? {}; lastProbe = { ...probe, stdoutRecovery: probeResolution.diagnostics, capture: compactCommand(probeCapture) }; const payload = sentinelPayloadFromLogs(String(probe.logsTail ?? "")); sentinelProgressEvent("sentinel.source-mirror.progress", { phase: "remote-job", status: probe.succeeded === true ? "succeeded" : probe.failed === true ? "failed" : "running", jobName, polls, elapsedMs: Date.now() - startedAt, pod: probe.pod ?? null, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane, }); if (probe.succeeded === true) { const ok = payload.ok === true; return withSentinelRemoteJobDiagnostics(state, { ok, phase: "job-succeeded", jobName, payload: Object.keys(payload).length === 0 ? { ok: false, status: "result-missing", valuesRedacted: true } : payload, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "source-mirror"); } if (probe.failed === true) { return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "job-failed", jobName, payload: Object.keys(payload).length === 0 ? { ok: false, status: "failed", valuesRedacted: true } : payload, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "source-mirror"); } if (!slowWarningSent && Date.now() - startedAt > warningBudgetMs) { slowWarningSent = true; sentinelProgressEvent("sentinel.source-mirror.warning", { warning: `source mirror sync exceeded configured ${Math.round(warningBudgetMs / 1000)}s timing budget; non-blocking timing alert`, jobName, elapsedMs: Date.now() - startedAt, node: state.spec.nodeId, lane: state.spec.lane }); } runCommand(["sleep", "2"], repoRoot, { timeoutMs: 3_000 }); } return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "job-timeout", jobName, payload: { ok: false, status: "timeout", valuesRedacted: true }, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "source-mirror"); } export function sentinelBlockedRemoteResult(phase: string, reason: string): SentinelRemoteJobResult { return { ok: false, phase, jobName: "-", payload: { ok: false, status: phase, reason, valuesRedacted: true }, valuesRedacted: true, }; } function sentinelSourceMirrorSyncJobManifest(state: SentinelCicdState, jobName: string): Record { const namespace = stringAt(state.cicd, "builder.namespace"); const labels = { "app.kubernetes.io/name": "web-probe-sentinel-source-mirror", "app.kubernetes.io/part-of": "hwlab-web-probe-sentinel", "unidesk.ai/spec-ref": "PJ2026-01060508", "unidesk.ai/node": state.spec.nodeId, "unidesk.ai/lane": state.spec.lane, }; return { apiVersion: "batch/v1", kind: "Job", metadata: { name: jobName, namespace, labels }, spec: { backoffLimit: 0, activeDeadlineSeconds: numberAt(state.cicd, "builder.activeDeadlineSeconds"), ttlSecondsAfterFinished: numberAt(state.cicd, "builder.ttlSecondsAfterFinished"), template: { metadata: { labels }, spec: { restartPolicy: "Never", volumes: [ sentinelGitMirrorCacheVolume(state), { name: "git-ssh", secret: { secretName: stringAt(state.cicd, "builder.gitSshSecretName"), defaultMode: 256 } }, ], containers: [{ name: "sync", image: state.image.baseImage, imagePullPolicy: "IfNotPresent", command: ["/bin/sh", "-ec", sentinelSourceMirrorSyncShell(state, jobName)], volumeMounts: [ { name: "cache", mountPath: "/cache" }, { name: "git-ssh", mountPath: "/git-ssh", readOnly: true }, ], }], }, }, }, }; } function sentinelSourceMirrorSyncShell(state: SentinelCicdState, jobName: string): string { return sentinelSourceMirrorSyncShellFromConfig(state.cicd, state.controlPlaneNode, jobName, state.sourceHead.commit); } export function sentinelSourceMirrorSyncShellFromConfig(cicd: Record, controlPlaneNode: Record, jobName: string, selectedCommit: string | null): string { return [ "set -eu", `job_name=${shellQuote(jobName)}`, `source_repository=${shellQuote(stringAt(cicd, "source.repository"))}`, `source_branch=${shellQuote(stringAt(cicd, "source.branch"))}`, `source_git_url=${shellQuote(stringAt(cicd, "source.gitSshUrl"))}`, `source_commit=${shellQuote(selectedCommit ?? "")}`, `source_stage_ref_prefix=${shellQuote(sentinelSourceSnapshotStageRefPrefix(cicd))}`, "started_ms=$(node -e 'console.log(Date.now())')", "emit_failed() { code=$?; if [ \"$code\" -ne 0 ]; then node - \"$code\" \"$job_name\" <<'NODE'\nconst [code, jobName] = process.argv.slice(2); console.log(JSON.stringify({ ok:false, status:'failed', exitCode:Number(code), jobName, valuesRedacted:true }));\nNODE\nfi; exit \"$code\"; }", "trap emit_failed EXIT", ...sentinelSourceMirrorSshSetupShellLinesForNode(controlPlaneNode), "repo=\"/cache/${source_repository}.git\"", "mkdir -p \"$(dirname \"$repo\")\"", "if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then", " git --git-dir=\"$repo\" remote set-url origin \"$source_git_url\" || git --git-dir=\"$repo\" remote add origin \"$source_git_url\"", "else", " rm -rf \"$repo\"", " git init --bare \"$repo\"", " git --git-dir=\"$repo\" remote add origin \"$source_git_url\"", "fi", "git --git-dir=\"$repo\" config uploadpack.allowReachableSHA1InWant true", "git --git-dir=\"$repo\" config uploadpack.allowAnySHA1InWant true", "git --git-dir=\"$repo\" config http.uploadpack true", "git --git-dir=\"$repo\" config http.receivepack true", "fetch_ok=0", "for attempt in 1 2 3; do", " if timeout 240 git --git-dir=\"$repo\" fetch origin \"+refs/heads/$source_branch:refs/mirror-stage/heads/$source_branch\"; then fetch_ok=1; break; fi", " code=$?", " printf '%s\\n' \"sentinel source-mirror fetch attempt ${attempt}/3 failed exit=${code}; retrying\" >&2", " sleep $((attempt * 5))", "done", "test \"$fetch_ok\" = 1", "mirror_commit=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/$source_branch^{commit}\")", "if [ -z \"$source_commit\" ]; then source_commit=\"$mirror_commit\"; fi", "git --git-dir=\"$repo\" cat-file -e \"$source_commit^{commit}\"", "test \"$mirror_commit\" = \"$source_commit\"", "stage_ref=\"${source_stage_ref_prefix%/}/${source_commit}\"", "git --git-dir=\"$repo\" update-ref \"refs/heads/$source_branch\" \"$mirror_commit\"", "git --git-dir=\"$repo\" update-ref \"$stage_ref\" \"$source_commit\"", "git --git-dir=\"$repo\" update-server-info", "finished_ms=$(node -e 'console.log(Date.now())')", "node - \"$job_name\" \"$source_repository\" \"$source_branch\" \"$source_commit\" \"$mirror_commit\" \"$stage_ref\" \"$started_ms\" \"$finished_ms\" <<'NODE'", "const [jobName, repository, branch, sourceCommit, mirrorCommit, stageRef, startedMs, finishedMs] = process.argv.slice(2);", "console.log(JSON.stringify({ ok:true, status:'succeeded', jobName, repository, branch, sourceCommit, mirrorCommit, stageRef, sourceAuthority:'git-mirror-snapshot', elapsedMs:Number(finishedMs)-Number(startedMs), valuesRedacted:true }));", "NODE", "trap - EXIT", ].join("\n"); } function sentinelSourceMirrorSshSetupShellLines(state: SentinelCicdState): string[] { return sentinelSourceMirrorSshSetupShellLinesForNode(state.controlPlaneNode); } function sentinelSourceMirrorSshSetupShellLinesForNode(controlPlaneNode: Record): string[] { const proxy = record(valueAtPath(controlPlaneNode, "egressProxy")); const serviceName = nonEmptyString(proxy.serviceName); const namespace = nonEmptyString(proxy.namespace); const port = typeof proxy.port === "number" && Number.isFinite(proxy.port) ? proxy.port : null; const hostRouteProxyUrl = nonEmptyString(proxy.proxyUrl); const noProxy = Array.isArray(proxy.noProxy) ? proxy.noProxy.filter((item): item is string => typeof item === "string" && item.length > 0).join(",") : ""; let proxyHost: string | null = null; let proxyPort: number | null = null; let proxyUrl: string | null = null; if (serviceName !== null && namespace !== null && port !== null) { proxyHost = `${serviceName}.${namespace}.svc.cluster.local`; proxyPort = port; proxyUrl = `http://${proxyHost}:${proxyPort}`; } else if (hostRouteProxyUrl !== null) { try { const parsed = new URL(hostRouteProxyUrl); const parsedPort = Number.parseInt(parsed.port || "80", 10); if ((parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.hostname.length > 0 && Number.isInteger(parsedPort)) { proxyHost = parsed.hostname; proxyPort = parsedPort; proxyUrl = hostRouteProxyUrl; } } catch { proxyHost = null; proxyPort = null; proxyUrl = null; } } const useProxy = proxyHost !== null && proxyPort !== null && proxyUrl !== null; if (!useProxy) { return [ "mkdir -p /root/.ssh", "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa", "chmod 0400 /root/.ssh/id_rsa", "printf '%s\\n' 'sentinel source-mirror-egress-proxy mode=direct transport=ssh source=yaml' >&2", "unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy", "export NO_PROXY='*'", "export no_proxy='*'", "cat > /tmp/sentinel-git-ssh-proxy.sh <<'SH_PROXY'", "#!/bin/sh", "exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=30 -o ConnectionAttempts=2 -o ServerAliveInterval=10 -o ServerAliveCountMax=3 \"$@\"", "SH_PROXY", "chmod 0700 /tmp/sentinel-git-ssh-proxy.sh", "export GIT_SSH=/tmp/sentinel-git-ssh-proxy.sh", "unset GIT_SSH_COMMAND", ]; } const proxyCommand = `ProxyCommand=node /tmp/sentinel-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`; return [ "mkdir -p /root/.ssh", "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa", "chmod 0400 /root/.ssh/id_rsa", `printf '%s\\n' ${shellQuote(`sentinel source-mirror-egress-proxy host=${proxyHost} port=${proxyPort} transport=ssh ssh=GIT_SSH-wrapper source=yaml`)} >&2`, `export HTTP_PROXY=${shellQuote(proxyUrl)}`, `export HTTPS_PROXY=${shellQuote(proxyUrl)}`, `export ALL_PROXY=${shellQuote(proxyUrl)}`, `export http_proxy=${shellQuote(proxyUrl)}`, `export https_proxy=${shellQuote(proxyUrl)}`, `export all_proxy=${shellQuote(proxyUrl)}`, `export NO_PROXY=${shellQuote(noProxy)}`, `export no_proxy=${shellQuote(noProxy)}`, "cat > /tmp/sentinel-github-proxy-connect.cjs <<'NODE_PROXY'", "#!/usr/bin/env node", "const net = require('node:net');", "const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);", "const proxyPort = Number.parseInt(proxyPortRaw || '', 10);", "const targetPort = Number.parseInt(targetPortRaw || '', 10);", "if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {", " console.error('sentinel source-mirror proxy-connect: invalid ProxyCommand arguments');", " process.exit(64);", "}", "let settled = false;", "let tunnelEstablished = false;", "function finish(code, message) {", " if (settled) return;", " settled = true;", " if (message) console.error('sentinel source-mirror proxy-connect: ' + message);", " process.exit(code);", "}", "const socket = net.createConnection({ host: proxyHost, port: proxyPort });", "let buffer = Buffer.alloc(0);", "socket.setTimeout(30000, () => { socket.destroy(); finish(65, 'timeout connecting via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); });", "socket.on('connect', () => socket.write('CONNECT ' + targetHost + ':' + targetPort + ' HTTP/1.1\\r\\nHost: ' + targetHost + ':' + targetPort + '\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n'));", "socket.on('error', (error) => finish(tunnelEstablished ? 69 : 66, (tunnelEstablished ? 'tunnel socket error: ' : 'tcp error connecting to proxy: ') + (error && error.message ? error.message : String(error))));", "socket.on('close', () => { if (!tunnelEstablished) finish(68, 'proxy closed before CONNECT completed via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); else finish(0); });", "function onData(chunk) {", " buffer = Buffer.concat([buffer, chunk]);", " const headerEnd = buffer.indexOf('\\r\\n\\r\\n');", " if (headerEnd === -1 && buffer.length < 8192) return;", " if (headerEnd === -1) { socket.destroy(); finish(68, 'proxy response header exceeded 8192 bytes before CONNECT status via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); return; }", " const head = buffer.slice(0, headerEnd + 4).toString('latin1');", " const statusLine = head.split('\\r\\n', 1)[0] || '';", " const statusCode = Number.parseInt(statusLine.split(' ')[1] || '', 10);", " if (!statusLine.startsWith('HTTP/1.') || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {", " const safeStatus = statusLine.replace(/[^\\x20-\\x7e]/g, '?').slice(0, 160);", " socket.destroy();", " finish(67, 'proxy CONNECT failed via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort + ': ' + safeStatus);", " return;", " }", " socket.off('data', onData);", " socket.setTimeout(0);", " tunnelEstablished = true;", " const rest = buffer.slice(headerEnd + 4);", " if (rest.length) process.stdout.write(rest);", " process.stdin.on('error', () => {});", " process.stdout.on('error', () => {});", " process.stdin.pipe(socket);", " socket.pipe(process.stdout);", "}", "socket.on('data', onData);", "NODE_PROXY", "chmod 0700 /tmp/sentinel-github-proxy-connect.cjs", "cat > /tmp/sentinel-git-ssh-proxy.sh <<'SH_PROXY'", "#!/bin/sh", `exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=30 -o ConnectionAttempts=2 -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -o ${shellQuote(proxyCommand)} "$@"`, "SH_PROXY", "chmod 0700 /tmp/sentinel-git-ssh-proxy.sh", "export GIT_SSH=/tmp/sentinel-git-ssh-proxy.sh", "unset GIT_SSH_COMMAND", ]; } export function runSentinelPublishJob(state: SentinelCicdState, publishGitops: boolean, timeoutSeconds: number, rerun: boolean): SentinelRemoteJobResult { const pipelineRunName = sentinelPipelineRunName(state, rerun); const manifest = sentinelPublishPipelineRunManifest(state, pipelineRunName, publishGitops); const namespace = stringAt(state.cicd, "builder.namespace"); sentinelProgressEvent("sentinel.publish.progress", { phase: "create-pipelinerun", status: "submitting", pipelineRun: pipelineRunName, publishGitops, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane }); const created = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", createTektonPipelineRunScript(namespace, manifest)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); if (created.exitCode !== 0) { sentinelProgressEvent("sentinel.publish.progress", { phase: "create-pipelinerun", status: "failed", pipelineRun: pipelineRunName, publishGitops, node: state.spec.nodeId, lane: state.spec.lane }); return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "create-pipelinerun", resourceKind: "PipelineRun", jobName: pipelineRunName, payload: { ok: false, status: "create-failed", valuesRedacted: true }, create: compactCommand(created), valuesRedacted: true }, "publish"); } sentinelProgressEvent("sentinel.publish.progress", { phase: "create-pipelinerun", status: "succeeded", pipelineRun: pipelineRunName, publishGitops, node: state.spec.nodeId, lane: state.spec.lane }); const startedAt = Date.now(); const timeoutMs = Math.max(5_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); const warningBudgetMs = Math.max(1, Math.trunc(controlPlaneWaitWarningSeconds(state))) * 1000; let slowWarningSent = false; let polls = 0; let lastProbe: Record = {}; while (Date.now() - startedAt < timeoutMs) { polls += 1; const probeCapture = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", probeTektonPipelineRunScript(namespace, pipelineRunName)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const probeResolution = resolveSentinelChildJson(probeCapture, "web-probe-sentinel-publish-pipelinerun-probe"); const probe = probeResolution.parsed ?? {}; lastProbe = { ...probe, stdoutRecovery: probeResolution.diagnostics, capture: compactCommand(probeCapture) }; const payload = sentinelPayloadFromLogs(String(probe.logsTail ?? "")); sentinelProgressEvent("sentinel.publish.progress", { phase: "remote-pipelinerun", status: probe.succeeded === true ? "succeeded" : probe.failed === true ? "failed" : "running", pipelineRun: pipelineRunName, publishGitops, polls, elapsedMs: Date.now() - startedAt, taskRun: probe.taskRun ?? null, pod: probe.pod ?? null, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane, }); if (probe.succeeded === true) { const ok = payload.ok === true; return withSentinelRemoteJobDiagnostics(state, { ok, phase: "pipelinerun-succeeded", resourceKind: "PipelineRun", jobName: pipelineRunName, payload: Object.keys(payload).length === 0 ? { ok: false, status: "result-missing", valuesRedacted: true } : payload, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "publish"); } if (probe.failed === true) { return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "pipelinerun-failed", resourceKind: "PipelineRun", jobName: pipelineRunName, payload: Object.keys(payload).length === 0 ? { ok: false, status: "failed", valuesRedacted: true } : payload, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "publish"); } if (!slowWarningSent && Date.now() - startedAt > warningBudgetMs) { slowWarningSent = true; sentinelProgressEvent("sentinel.publish.warning", { warning: `remote publish PipelineRun exceeded configured ${Math.round(warningBudgetMs / 1000)}s timing budget; non-blocking timing alert`, pipelineRun: pipelineRunName, elapsedMs: Date.now() - startedAt, node: state.spec.nodeId, lane: state.spec.lane }); } runCommand(["sleep", "2"], repoRoot, { timeoutMs: 3_000 }); } return withSentinelRemoteJobDiagnostics(state, { ok: false, phase: "pipelinerun-timeout", resourceKind: "PipelineRun", jobName: pipelineRunName, payload: { ok: false, status: "timeout", valuesRedacted: true }, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }, "publish"); } export function sentinelPublishPipelineRunManifest(state: SentinelCicdState, pipelineRunName: string, publishGitops: boolean): Record { const namespace = stringAt(state.cicd, "builder.namespace"); const buildkitImage = requireSentinelBuildkitImage(state); const proxyEnv = sentinelImageBuildProxyEnv(state); const labels = { "app.kubernetes.io/name": "web-probe-sentinel-publish", "app.kubernetes.io/part-of": "hwlab-web-probe-sentinel", "unidesk.ai/spec-ref": "PJ2026-01060508", "unidesk.ai/node": state.spec.nodeId, "unidesk.ai/lane": state.spec.lane, "unidesk.ai/ci-system": "tekton", }; return { apiVersion: "tekton.dev/v1", kind: "PipelineRun", metadata: { name: pipelineRunName, namespace, labels, annotations: { "unidesk.ai/source-commit": state.sourceHead.commit ?? "", "unidesk.ai/source-authority": state.sourceHead.sourceAuthority, "unidesk.ai/source-stage-ref": state.sourceHead.stageRef ?? "", "unidesk.ai/gitops-target-revision": stringAt(state.cicd, "argo.targetRevision"), "unidesk.ai/publish-gitops": publishGitops ? "true" : "false", }, }, spec: { timeouts: { pipeline: `${numberAt(state.cicd, "builder.activeDeadlineSeconds")}s` }, taskRunTemplate: { podTemplate: { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet", securityContext: { fsGroup: 1000 }, }, }, pipelineSpec: { tasks: [{ name: "publish", taskSpec: { volumes: [ sentinelGitMirrorCacheVolume(state), { name: "git-ssh", secret: { secretName: stringAt(state.cicd, "builder.gitSshSecretName"), defaultMode: 256 } }, { name: "workspace", emptyDir: { sizeLimit: "8Gi" } }, sentinelBuildkitStateVolume(state), { name: "tmp", emptyDir: {} }, ], steps: [ { name: "source", image: state.image.baseImage, imagePullPolicy: "IfNotPresent", env: proxyEnv, script: tektonShellScript(sentinelPublishSourceShell(state, pipelineRunName)), volumeMounts: [ { name: "workspace", mountPath: "/workspace" }, { name: "git-ssh", mountPath: "/git-ssh", readOnly: true }, ], }, { name: "prepare-buildkit-state", image: state.image.baseImage, imagePullPolicy: "IfNotPresent", script: tektonShellScript("set -eu\nmkdir -p /home/user/.local/share/buildkit\nchown -R 1000:1000 /home/user/.local/share/buildkit"), securityContext: { runAsUser: 0, runAsGroup: 0 }, volumeMounts: [ { name: "buildkit-state", mountPath: "/home/user/.local/share/buildkit" }, ], }, { name: "image-build", image: buildkitImage, imagePullPolicy: "IfNotPresent", env: [ ...proxyEnv, { name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" }, ], script: tektonShellScript(sentinelPublishImageBuildShell(state, pipelineRunName)), securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 }, volumeMounts: [ { name: "workspace", mountPath: "/workspace" }, { name: "buildkit-state", mountPath: "/home/user/.local/share/buildkit" }, { name: "tmp", mountPath: "/tmp" }, ], }, { name: "publish", image: state.image.baseImage, imagePullPolicy: "IfNotPresent", env: proxyEnv, script: tektonShellScript(sentinelPublishShell(state, pipelineRunName, publishGitops)), volumeMounts: [ { name: "workspace", mountPath: "/workspace" }, { name: "cache", mountPath: "/cache" }, { name: "git-ssh", mountPath: "/git-ssh", readOnly: true }, ], }, ], }, }], }, }, }; } function tektonShellScript(body: string): string { return `#!/bin/sh\n${body}`; } function sentinelGitMirrorCacheVolume(state: SentinelCicdState): Record { return sentinelGitMirrorCacheVolumeFromTarget(state.controlPlaneTarget); } export function sentinelGitMirrorCacheVolumeFromTarget(controlPlaneTarget: Record): Record { const hostPath = nonEmptyString(valueAtPath(controlPlaneTarget, "gitMirror.cacheHostPath")); if (hostPath !== null) return { name: "cache", hostPath: { path: hostPath, type: "DirectoryOrCreate" } }; return { name: "cache", persistentVolumeClaim: { claimName: stringAt(controlPlaneTarget, "gitMirror.cachePvcName") } }; } function sentinelBuildkitStateVolume(state: SentinelCicdState): Record { const buildkitState = monitorWebBuildkitStatePlan(state.cicd); const mode = stringAt(buildkitState, "mode"); if (mode === "hostPath") { return { name: "buildkit-state", hostPath: { path: stringAt(buildkitState, "path"), type: stringAt(buildkitState, "type"), }, }; } if (mode === "persistentVolumeClaim") { return { name: "buildkit-state", persistentVolumeClaim: { claimName: stringAt(buildkitState, "claimName") } }; } if (mode === "emptyDir") { return { name: "buildkit-state", emptyDir: { sizeLimit: stringAt(buildkitState, "sizeLimit") } }; } throw new Error(`monitorWeb.imageBuild.buildkitState.mode must be hostPath, persistentVolumeClaim or emptyDir, got ${mode}`); } function requireSentinelBuildkitImage(state: SentinelCicdState): string { const image = state.spec.buildkit?.sidecarImage; if (typeof image !== "string" || image.length === 0) { throw new Error(`config/hwlab-node-lanes.yaml ${state.spec.nodeId}/${state.spec.lane} buildkit.sidecarImage is required for pure k8s web-probe sentinel image publish`); } return image; } function sentinelImageBuildProxyEnv(state: SentinelCicdState): Array<{ name: string; value: string }> { const proxy = state.spec.networkProfile.imageBuildProxy; const noProxy = proxy.noProxy.join(","); return [ { name: "HTTP_PROXY", value: proxy.http }, { name: "http_proxy", value: proxy.http }, { name: "HTTPS_PROXY", value: proxy.https }, { name: "https_proxy", value: proxy.https }, { name: "ALL_PROXY", value: proxy.all }, { name: "all_proxy", value: proxy.all }, { name: "NO_PROXY", value: noProxy }, { name: "no_proxy", value: noProxy }, ]; } function sentinelPublishSourceShell(state: SentinelCicdState, jobName: string): string { const monitorWeb = record(state.image.monitorWeb); const checkoutPathsB64 = Buffer.from(JSON.stringify(arrayAt(state.cicd, "source.checkoutPaths").map((item) => { if (typeof item !== "string" || item.length === 0 || item.startsWith("/") || item.includes("..")) throw new Error("source.checkoutPaths must contain safe relative paths"); return item; })), "utf8").toString("base64"); const dockerfileB64 = Buffer.from(state.image.dockerfilePreview, "utf8").toString("base64"); const envReuseMode = stringAt(monitorWeb, "envReuseMode"); const envReuseNodeDepsPath = stringAt(monitorWeb, "envReuseNodeDepsPath"); return [ "set -eu", `job_name=${shellQuote(jobName)}`, `source_repository=${shellQuote(stringAt(state.cicd, "source.repository"))}`, `source_branch=${shellQuote(stringAt(state.cicd, "source.branch"))}`, `source_git_url=${shellQuote(stringAt(state.cicd, "source.gitMirrorReadUrl"))}`, `source_commit=${shellQuote(state.sourceHead.commit ?? "")}`, `source_stage_ref=${shellQuote(state.sourceHead.stageRef ?? "")}`, `checkout_paths_b64=${shellQuote(checkoutPathsB64)}`, `dockerfile_b64=${shellQuote(dockerfileB64)}`, `env_reuse_mode=${shellQuote(envReuseMode)}`, `env_reuse_node_deps_path=${shellQuote(envReuseNodeDepsPath)}`, "meta_dir=/workspace/meta", "event_log=/workspace/publish-events.log", "worktree=/workspace/source", "rm -rf \"$worktree\" \"$meta_dir\" /workspace/image-build.log /workspace/build-metadata.json", "mkdir -p \"$meta_dir\"", ": > \"$event_log\"", "now_ms() { seconds=$(date +%s); printf '%s\\n' $((seconds * 1000)); }", "write_meta() { printf '%s' \"$2\" > \"$meta_dir/$1\"; }", "emit_stage() { stage=$1; status=$2; started=$3; finished=$(now_ms); elapsed=$((finished - started)); printf '{\"event\":\"sentinel-publish-stage\",\"stage\":\"%s\",\"status\":\"%s\",\"elapsedMs\":%s,\"valuesRedacted\":true}\\n' \"$stage\" \"$status\" \"$elapsed\" | tee -a \"$event_log\"; }", "emit_failed() { code=$?; if [ \"$code\" -ne 0 ]; then printf '{\"ok\":false,\"status\":\"failed\",\"exitCode\":%s,\"jobName\":\"%s\",\"valuesRedacted\":true}\\n' \"$code\" \"$job_name\" | tee -a \"$event_log\"; fi; exit \"$code\"; }", "trap emit_failed EXIT", "started_ms=$(now_ms)", "write_meta started_ms \"$started_ms\"", "write_meta source_commit \"$source_commit\"", "write_meta source_stage_ref \"$source_stage_ref\"", "test -n \"$source_commit\"", "test -n \"$source_stage_ref\"", "mkdir -p /root/.ssh", "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa", "chmod 0400 /root/.ssh/id_rsa", "export GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1'", "git init \"$worktree\"", "cd \"$worktree\"", "git remote add origin \"$source_git_url\"", "git config core.sparseCheckout true", "git config remote.origin.promisor true", "git config remote.origin.partialclonefilter blob:none", "CHECKOUT_PATHS_B64=\"$checkout_paths_b64\" node <<'NODE'", "const fs = require('node:fs');", "const paths = JSON.parse(Buffer.from(process.env.CHECKOUT_PATHS_B64 || '', 'base64').toString('utf8'));", "fs.mkdirSync('.git/info', { recursive: true });", "fs.writeFileSync('.git/info/sparse-checkout', paths.map((item) => item.endsWith('/') ? item : item + (item.includes('.') ? '' : '/')).join('\\n') + '\\n');", "NODE", "source_fetch_started_ms=$(now_ms)", "write_meta source_fetch_started_ms \"$source_fetch_started_ms\"", "emit_stage source-fetch running \"$source_fetch_started_ms\"", "git fetch --depth=1 --filter=blob:none origin \"+$source_stage_ref:refs/remotes/origin/unidesk-source-snapshot\"", "git checkout --detach \"$source_commit\"", "mirror_commit=$(git rev-parse HEAD)", "test \"$mirror_commit\" = \"$source_commit\"", "write_meta mirror_commit \"$mirror_commit\"", "source_fetch_finished_ms=$(now_ms)", "write_meta source_fetch_finished_ms \"$source_fetch_finished_ms\"", "emit_stage source-fetch succeeded \"$source_fetch_started_ms\"", "env_reuse_node_deps_present=false", "env_reuse_node_deps_entries=0", "if [ -d \"$env_reuse_node_deps_path\" ]; then env_reuse_node_deps_present=true; env_reuse_node_deps_entries=$(find \"$env_reuse_node_deps_path\" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' '); fi", "env_reuse_linked_node_deps=0", "rm -rf node_modules", "if [ \"$env_reuse_node_deps_present\" = true ]; then mkdir -p node_modules; for dep in \"$env_reuse_node_deps_path\"/*; do [ -e \"$dep\" ] || continue; ln -sf \"$dep\" \"node_modules/$(basename \"$dep\")\"; env_reuse_linked_node_deps=$((env_reuse_linked_node_deps + 1)); done; fi", "write_meta env_reuse_mode \"$env_reuse_mode\"", "write_meta env_reuse_node_deps_path \"$env_reuse_node_deps_path\"", "write_meta env_reuse_node_deps_present \"$env_reuse_node_deps_present\"", "write_meta env_reuse_node_deps_entries \"$env_reuse_node_deps_entries\"", "write_meta env_reuse_linked_node_deps \"$env_reuse_linked_node_deps\"", "node - \"$env_reuse_mode\" \"$env_reuse_node_deps_path\" \"$env_reuse_node_deps_present\" \"$env_reuse_node_deps_entries\" \"$env_reuse_linked_node_deps\" <<'NODE'", "const [mode, nodeDepsPath, nodeDepsPresent, nodeDepsEntries, linkedNodeDeps] = process.argv.slice(2); console.log(JSON.stringify({ event:'sentinel-publish-env-reuse', mode, nodeDepsPath, nodeDepsPresent: nodeDepsPresent === 'true', nodeDepsEntries: Number(nodeDepsEntries || 0), linkedNodeDeps: Number(linkedNodeDeps || 0), dependencyReuse: nodeDepsPresent === 'true' ? 'hit' : 'miss', valuesRedacted:true }));", "NODE", "node - \"$env_reuse_mode\" \"$env_reuse_node_deps_path\" \"$env_reuse_node_deps_present\" \"$env_reuse_node_deps_entries\" \"$env_reuse_linked_node_deps\" <<'NODE' >> \"$event_log\"", "const [mode, nodeDepsPath, nodeDepsPresent, nodeDepsEntries, linkedNodeDeps] = process.argv.slice(2); console.log(JSON.stringify({ event:'sentinel-publish-env-reuse', mode, nodeDepsPath, nodeDepsPresent: nodeDepsPresent === 'true', nodeDepsEntries: Number(nodeDepsEntries || 0), linkedNodeDeps: Number(linkedNodeDeps || 0), dependencyReuse: nodeDepsPresent === 'true' ? 'hit' : 'miss', valuesRedacted:true }));", "NODE", "monitor_web_verify_started_ms=$(now_ms)", "write_meta monitor_web_verify_started_ms \"$monitor_web_verify_started_ms\"", "emit_stage monitor-web-verify running \"$monitor_web_verify_started_ms\"", "if ! bun scripts/verify-web-probe-sentinel-monitor-web.ts > /tmp/web-probe-sentinel-monitor-web-verify.log 2>&1; then cat /tmp/web-probe-sentinel-monitor-web-verify.log; emit_stage monitor-web-verify failed \"$monitor_web_verify_started_ms\"; exit 1; fi", "cat /tmp/web-probe-sentinel-monitor-web-verify.log", "monitor_web_verify_finished_ms=$(now_ms)", "write_meta monitor_web_verify_finished_ms \"$monitor_web_verify_finished_ms\"", "emit_stage monitor-web-verify succeeded \"$monitor_web_verify_started_ms\"", "mkdir -p .unidesk-sentinel-bin", "cat > .unidesk-sentinel-bin/trans <<'SH_TRANS'", "#!/bin/sh", "exec bun /app/scripts/ssh-cli.ts \"$@\"", "SH_TRANS", "chmod 0755 .unidesk-sentinel-bin/trans", "DOCKERFILE_B64=\"$dockerfile_b64\" node <<'NODE'", "const fs = require('node:fs');", "fs.writeFileSync('Containerfile.web-probe-sentinel', Buffer.from(process.env.DOCKERFILE_B64 || '', 'base64'));", "NODE", "cat > .dockerignore <<'EOF_DOCKERIGNORE'", ".git", ".git/**", ".state", ".state/**", "logs", "logs/**", "node_modules", "node_modules/**", "**/node_modules", "**/node_modules/**", "**/dist", "**/dist/**", "**/target", "**/target/**", "**/coverage", "**/coverage/**", "npm-debug.log*", ".env", ".env.*", "EOF_DOCKERIGNORE", "context_ignore_entries=$(wc -l < .dockerignore | tr -d ' ')", "write_meta context_ignore_entries \"$context_ignore_entries\"", "chmod -R a+rwX /workspace", "trap - EXIT", ].join("\n"); } function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName: string): string { const monitorWeb = record(state.image.monitorWeb); const imageBuildPackageMode = stringAt(monitorWeb, "imageBuildPackageMode"); const imageBuildNetworkMode = stringAt(monitorWeb, "imageBuildNetworkMode"); const imageBuildProxySource = stringAt(monitorWeb, "imageBuildProxySource"); const imageBuildProxy = state.spec.networkProfile.imageBuildProxy; const imageBuildNoProxy = imageBuildProxy.noProxy.join(","); return [ "set -eu", `job_name=${shellQuote(jobName)}`, `image_ref=${shellQuote(state.image.ref)}`, `image_build_builder=${shellQuote(requireSentinelBuildkitImage(state))}`, `image_build_package_mode=${shellQuote(imageBuildPackageMode)}`, `image_build_network_mode=${shellQuote(imageBuildNetworkMode)}`, `image_build_proxy_source=${shellQuote(imageBuildProxySource)}`, `image_build_http_proxy=${shellQuote(imageBuildProxy.http)}`, `image_build_https_proxy=${shellQuote(imageBuildProxy.https)}`, `image_build_all_proxy=${shellQuote(imageBuildProxy.all)}`, `image_build_no_proxy=${shellQuote(imageBuildNoProxy)}`, "meta_dir=/workspace/meta", "event_log=/workspace/publish-events.log", "build_log=/workspace/image-build.log", "now_ms() { seconds=$(date +%s); printf '%s\\n' $((seconds * 1000)); }", "write_meta() { printf '%s' \"$2\" > \"$meta_dir/$1\"; }", "emit_stage() { stage=$1; status=$2; started=$3; finished=$(now_ms); elapsed=$((finished - started)); printf '{\"event\":\"sentinel-publish-stage\",\"stage\":\"%s\",\"status\":\"%s\",\"elapsedMs\":%s,\"valuesRedacted\":true}\\n' \"$stage\" \"$status\" \"$elapsed\" | tee -a \"$event_log\"; }", "emit_failed() { code=$?; if [ \"$code\" -ne 0 ]; then printf '{\"ok\":false,\"status\":\"failed\",\"exitCode\":%s,\"jobName\":\"%s\",\"valuesRedacted\":true}\\n' \"$code\" \"$job_name\" | tee -a \"$event_log\"; fi; exit \"$code\"; }", "trap emit_failed EXIT", "cd /workspace/source", "image_build_http_proxy_present=false; if [ -n \"$image_build_http_proxy\" ]; then image_build_http_proxy_present=true; fi", "image_build_https_proxy_present=false; if [ -n \"$image_build_https_proxy\" ]; then image_build_https_proxy_present=true; fi", "image_build_all_proxy_present=false; if [ -n \"$image_build_all_proxy\" ]; then image_build_all_proxy_present=true; fi", "image_build_no_proxy_present=false; if [ -n \"$image_build_no_proxy\" ]; then image_build_no_proxy_present=true; fi", "write_meta image_build_builder \"$image_build_builder\"", "write_meta image_build_package_mode \"$image_build_package_mode\"", "write_meta image_build_network_mode \"$image_build_network_mode\"", "write_meta image_build_proxy_source \"$image_build_proxy_source\"", "write_meta image_build_http_proxy_present \"$image_build_http_proxy_present\"", "write_meta image_build_https_proxy_present \"$image_build_https_proxy_present\"", "write_meta image_build_all_proxy_present \"$image_build_all_proxy_present\"", "write_meta image_build_no_proxy_present \"$image_build_no_proxy_present\"", "image_build_started_ms=$(now_ms)", "write_meta image_build_started_ms \"$image_build_started_ms\"", "emit_stage image-build running \"$image_build_started_ms\"", "if ! env HTTP_PROXY=\"$image_build_http_proxy\" HTTPS_PROXY=\"$image_build_https_proxy\" ALL_PROXY=\"$image_build_all_proxy\" NO_PROXY=\"$image_build_no_proxy\" http_proxy=\"$image_build_http_proxy\" https_proxy=\"$image_build_https_proxy\" all_proxy=\"$image_build_all_proxy\" no_proxy=\"$image_build_no_proxy\" buildctl-daemonless.sh build --allow network.host --frontend dockerfile.v0 --local context=/workspace/source --local dockerfile=/workspace/source --opt filename=Containerfile.web-probe-sentinel --opt \"network=$image_build_network_mode\" --opt \"build-arg:HTTP_PROXY=$image_build_http_proxy\" --opt \"build-arg:HTTPS_PROXY=$image_build_https_proxy\" --opt \"build-arg:ALL_PROXY=$image_build_all_proxy\" --opt \"build-arg:NO_PROXY=$image_build_no_proxy\" --opt \"build-arg:http_proxy=$image_build_http_proxy\" --opt \"build-arg:https_proxy=$image_build_https_proxy\" --opt \"build-arg:all_proxy=$image_build_all_proxy\" --opt \"build-arg:no_proxy=$image_build_no_proxy\" --metadata-file /workspace/build-metadata.json --output \"type=image,name=$image_ref,push=true,registry.insecure=true\" > \"$build_log\" 2>&1; then cat \"$build_log\"; emit_stage image-build failed \"$image_build_started_ms\"; exit 1; fi", "cat \"$build_log\"", "image_build_finished_ms=$(now_ms)", "write_meta image_build_finished_ms \"$image_build_finished_ms\"", "metadata_compact=$(tr -d '\\n' < /workspace/build-metadata.json)", "digest=$(printf '%s' \"$metadata_compact\" | sed -n 's/.*\"containerimage.digest\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n 1)", "test -n \"$digest\"", "repo_no_tag=${image_ref%:*}", "digest_ref=\"$repo_no_tag@$digest\"", "write_meta digest \"$digest\"", "write_meta digest_ref \"$digest_ref\"", "emit_stage image-build succeeded \"$image_build_started_ms\"", "trap - EXIT", ].join("\n"); } function sentinelPublishShell(state: SentinelCicdState, jobName: string, publishGitops: boolean): string { const gitopsFiles = publishGitops ? sentinelGitopsFiles(state) : []; const filesB64 = Buffer.from(JSON.stringify(gitopsFiles.map((file) => ({ path: file.path, contentBase64: Buffer.from(file.content, "utf8").toString("base64"), }))), "utf8").toString("base64"); return [ "set -eu", `job_name=${shellQuote(jobName)}`, `image_ref=${shellQuote(state.image.ref)}`, `image_repository=${shellQuote(state.image.repository)}`, `gitops_repository=${shellQuote(stringAt(state.controlPlaneTarget, "source.repository"))}`, `gitops_branch=${shellQuote(stringAt(state.cicd, "argo.targetRevision"))}`, `files_b64=${shellQuote(filesB64)}`, "meta_dir=/workspace/meta", "event_log=/workspace/publish-events.log", "build_log=/workspace/image-build.log", "now_ms() { seconds=$(date +%s); printf '%s\\n' $((seconds * 1000)); }", "read_meta() { cat \"$meta_dir/$1\"; }", "emit_stage() { stage=$1; status=$2; started=$3; finished=$(now_ms); elapsed=$((finished - started)); printf '{\"event\":\"sentinel-publish-stage\",\"stage\":\"%s\",\"status\":\"%s\",\"elapsedMs\":%s,\"valuesRedacted\":true}\\n' \"$stage\" \"$status\" \"$elapsed\"; }", "emit_failed() { code=$?; if [ \"$code\" -ne 0 ]; then printf '{\"ok\":false,\"status\":\"failed\",\"exitCode\":%s,\"jobName\":\"%s\",\"valuesRedacted\":true}\\n' \"$code\" \"$job_name\"; fi; exit \"$code\"; }", "trap emit_failed EXIT", "if [ -f \"$event_log\" ]; then cat \"$event_log\"; fi", "mkdir -p /root/.ssh", "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa", "chmod 0400 /root/.ssh/id_rsa", "export GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1'", "started_ms=$(read_meta started_ms)", "source_commit=$(read_meta source_commit)", "source_stage_ref=$(read_meta source_stage_ref)", "mirror_commit=$(read_meta mirror_commit)", "source_fetch_started_ms=$(read_meta source_fetch_started_ms)", "source_fetch_finished_ms=$(read_meta source_fetch_finished_ms)", "monitor_web_verify_started_ms=$(read_meta monitor_web_verify_started_ms)", "monitor_web_verify_finished_ms=$(read_meta monitor_web_verify_finished_ms)", "image_build_started_ms=$(read_meta image_build_started_ms)", "image_build_finished_ms=$(read_meta image_build_finished_ms)", "digest_ref=$(read_meta digest_ref)", "env_reuse_mode=$(read_meta env_reuse_mode 2>/dev/null || printf 'k8s-buildkit-and-ci-node-deps')", "env_reuse_node_deps_path=$(read_meta env_reuse_node_deps_path 2>/dev/null || printf '-')", "env_reuse_node_deps_present=$(read_meta env_reuse_node_deps_present)", "env_reuse_node_deps_entries=$(read_meta env_reuse_node_deps_entries)", "env_reuse_linked_node_deps=$(read_meta env_reuse_linked_node_deps)", "image_build_builder=$(read_meta image_build_builder)", "image_build_package_mode=$(read_meta image_build_package_mode)", "image_build_network_mode=$(read_meta image_build_network_mode)", "image_build_proxy_source=$(read_meta image_build_proxy_source)", "image_build_http_proxy_present=$(read_meta image_build_http_proxy_present)", "image_build_https_proxy_present=$(read_meta image_build_https_proxy_present)", "image_build_all_proxy_present=$(read_meta image_build_all_proxy_present)", "image_build_no_proxy_present=$(read_meta image_build_no_proxy_present)", "context_ignore_entries=$(read_meta context_ignore_entries)", "image_build_cache_hits=$(grep -Eci '(^|[[:space:]])CACHED([[:space:]]|$)|Using cache|cache hit' \"$build_log\" 2>/dev/null || true)", "image_build_step_lines=$(grep -Eci '^(#|STEP|[[:space:]]*=>)' \"$build_log\" 2>/dev/null || true)", "image_build_log_tail_b64=$(tail -n 30 \"$build_log\" 2>/dev/null | tail -c 4000 | base64 | tr -d '\\n')", "gitops_commit=''", "changed=false", "file_count=0", "gitops_started_ms=$(now_ms)", "if [ \"$files_b64\" != \"W10=\" ]; then", " emit_stage gitops running \"$gitops_started_ms\"", " gitops_cache=\"/cache/${gitops_repository}.git\"", " gitops_worktree=\"/tmp/$job_name/gitops\"", " git clone --no-checkout \"$gitops_cache\" \"$gitops_worktree\"", " cd \"$gitops_worktree\"", " git fetch origin \"$gitops_branch\" || true", " if git rev-parse --verify \"refs/remotes/origin/$gitops_branch^{commit}\" >/dev/null 2>&1; then git checkout -B \"$gitops_branch\" \"refs/remotes/origin/$gitops_branch\"; else git checkout --orphan \"$gitops_branch\"; git rm -rf . >/dev/null 2>&1 || true; fi", " FILES_B64=\"$files_b64\" IMAGE_REF=\"$image_ref\" DIGEST_REF=\"$digest_ref\" node <<'NODE'", "const fs = require('node:fs');", "const path = require('node:path');", "const files = JSON.parse(Buffer.from(process.env.FILES_B64 || '', 'base64').toString('utf8'));", "for (const file of files) {", " const target = path.resolve(process.cwd(), file.path);", " if (!target.startsWith(process.cwd() + path.sep)) throw new Error(`refuse path outside workspace: ${file.path}`);", " fs.mkdirSync(path.dirname(target), { recursive: true });", " const text = Buffer.from(file.contentBase64, 'base64').toString('utf8').split(process.env.IMAGE_REF).join(process.env.DIGEST_REF);", " fs.writeFileSync(target, text);", "}", "console.error(JSON.stringify({event:'web-probe-sentinel-gitops-files', fileCount: files.length, valuesRedacted:true}));", "NODE", " git add .", " file_count=$(git diff --cached --name-only | wc -l | tr -d ' ')", " if git diff --quiet --cached; then changed=false; else changed=true; git -c user.email=web-probe-sentinel@unidesk.local -c user.name='UniDesk Web Probe Sentinel' commit -m \"deploy: render web-probe sentinel ${source_commit}\"; fi", " git push origin \"HEAD:refs/heads/$gitops_branch\"", " gitops_commit=$(git rev-parse HEAD)", " emit_stage gitops succeeded \"$gitops_started_ms\"", "else", " emit_stage gitops skipped \"$gitops_started_ms\"", "fi", "gitops_finished_ms=$(now_ms)", "finished_ms=$(now_ms)", "node - \"$job_name\" \"$source_commit\" \"$source_stage_ref\" \"$mirror_commit\" \"$image_ref\" \"$digest_ref\" \"$gitops_commit\" \"$changed\" \"$file_count\" \"$started_ms\" \"$finished_ms\" \"$source_fetch_started_ms\" \"$source_fetch_finished_ms\" \"$monitor_web_verify_started_ms\" \"$monitor_web_verify_finished_ms\" \"$image_build_started_ms\" \"$image_build_finished_ms\" \"$gitops_started_ms\" \"$gitops_finished_ms\" \"$env_reuse_mode\" \"$env_reuse_node_deps_path\" \"$env_reuse_node_deps_present\" \"$env_reuse_node_deps_entries\" \"$env_reuse_linked_node_deps\" \"$image_build_cache_hits\" \"$image_build_step_lines\" \"$image_build_log_tail_b64\" \"$image_build_builder\" \"$image_build_package_mode\" \"$image_build_network_mode\" \"$image_build_proxy_source\" \"$image_build_http_proxy_present\" \"$image_build_https_proxy_present\" \"$image_build_all_proxy_present\" \"$image_build_no_proxy_present\" \"$context_ignore_entries\" <<'NODE'", "const [jobName, sourceCommit, sourceStageRef, mirrorCommit, imageRef, digestRef, gitopsCommit, changed, fileCount, startedMs, finishedMs, sourceFetchStartedMs, sourceFetchFinishedMs, monitorWebVerifyStartedMs, monitorWebVerifyFinishedMs, imageBuildStartedMs, imageBuildFinishedMs, gitopsStartedMs, gitopsFinishedMs, envReuseMode, envReuseNodeDepsPath, envReuseNodeDepsPresent, envReuseNodeDepsEntries, envReuseLinkedNodeDeps, imageBuildCacheHits, imageBuildStepLines, imageBuildLogTailB64, imageBuildBuilder, imageBuildPackageMode, imageBuildNetworkMode, imageBuildProxySource, imageBuildHttpProxyPresent, imageBuildHttpsProxyPresent, imageBuildAllProxyPresent, imageBuildNoProxyPresent, contextIgnoreEntries] = process.argv.slice(2);", "const elapsed = (start, finish) => Number(finish) - Number(start);", "const cacheHits = Number(imageBuildCacheHits || 0);", "console.log(JSON.stringify({ ok:true, status:'succeeded', jobName, sourceCommit, sourceStageRef, sourceAuthority:'git-mirror-snapshot', mirrorCommit, imageRef, digestRef, gitopsCommit: gitopsCommit || null, changed: changed === 'true', fileCount: Number(fileCount || 0), elapsedMs: elapsed(startedMs, finishedMs), stageTimings: { sourceFetchMs: elapsed(sourceFetchStartedMs, sourceFetchFinishedMs), monitorWebVerifyMs: elapsed(monitorWebVerifyStartedMs, monitorWebVerifyFinishedMs), imageBuildMs: elapsed(imageBuildStartedMs, imageBuildFinishedMs), gitopsMs: elapsed(gitopsStartedMs, gitopsFinishedMs), totalMs: elapsed(startedMs, finishedMs), valuesRedacted:true }, envReuse: { mode: envReuseMode, nodeDepsPath: envReuseNodeDepsPath, nodeDepsPresent: envReuseNodeDepsPresent === 'true', nodeDepsEntries: Number(envReuseNodeDepsEntries || 0), linkedNodeDeps: Number(envReuseLinkedNodeDeps || 0), dependencyReuse: envReuseNodeDepsPresent === 'true' ? 'hit' : 'miss', valuesRedacted:true }, imageBuild: { builder: 'k8s-buildkit-rootless', builderImage: imageBuildBuilder, cacheHitLines: cacheHits, stepLines: Number(imageBuildStepLines || 0), layerCache: cacheHits > 0 ? 'hit' : 'unknown-or-miss', packageMode: imageBuildPackageMode, networkMode: imageBuildNetworkMode, proxySource: imageBuildProxySource, proxy: { httpProxyPresent: imageBuildHttpProxyPresent === 'true', httpsProxyPresent: imageBuildHttpsProxyPresent === 'true', allProxyPresent: imageBuildAllProxyPresent === 'true', noProxyPresent: imageBuildNoProxyPresent === 'true', valuesRedacted:true }, contextIgnoreEntries: Number(contextIgnoreEntries || 0), verifyLocation: 'pre-image-build', logTail: Buffer.from(imageBuildLogTailB64 || '', 'base64').toString('utf8'), valuesRedacted:true }, completedStages: ['source-fetch', 'monitor-web-verify', 'image-build', gitopsCommit ? 'gitops' : 'gitops-skipped'], valuesRedacted:true }));", "NODE", "trap - EXIT", ].join("\n"); } function sentinelGitopsFiles(state: SentinelCicdState): readonly { path: string; content: string }[] { const runtimeManifests = state.manifests.filter((item) => item.kind !== "Application"); return [{ path: `${stringAt(state.cicd, "gitopsPath")}/web-probe-sentinel.yaml`, content: `${runtimeManifests.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`, }]; } export function applySentinelArgoApplication(state: SentinelCicdState, timeoutSeconds: number): Record { const app = state.manifests.find((item) => item.kind === "Application"); if (app === undefined) return { ok: false, reason: "application-manifest-missing", valuesRedacted: true }; const yaml = `${Bun.YAML.stringify(app).trim()}\n`; const namespace = stringAt(state.cicd, "argo.namespace"); const applicationName = stringAt(state.cicd, "argo.applicationName"); const script = [ "set -eu", "tmp=$(mktemp)", `cat >"$tmp" <<'YAML'\n${yaml}YAML`, "kubectl apply -f \"$tmp\"", `kubectl -n ${shellQuote(namespace)} annotate application ${shellQuote(applicationName)} argocd.argoproj.io/refresh=hard --overwrite`, ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); return { ok: result.exitCode === 0, result: compactCommand(result), valuesRedacted: true }; } export function createK8sJobScript(namespace: string, manifest: Record): string { const yaml = `${Bun.YAML.stringify(manifest).trim()}\n`; return [ "set -eu", `kubectl -n ${shellQuote(namespace)} delete job ${shellQuote(stringAt(manifest, "metadata.name"))} --ignore-not-found=true >/dev/null 2>&1 || true`, "tmp=$(mktemp)", `cat >"$tmp" <<'YAML'\n${yaml}YAML`, "kubectl apply -f \"$tmp\"", ].join("\n"); } function createTektonPipelineRunScript(namespace: string, manifest: Record): string { const yaml = `${Bun.YAML.stringify(manifest).trim()}\n`; const pipelineRunName = stringAt(manifest, "metadata.name"); return [ "set -eu", `pipeline_run=${shellQuote(pipelineRunName)}`, `namespace=${shellQuote(namespace)}`, "tmp=$(mktemp)", `cat >"$tmp" <<'YAML'\n${yaml}YAML`, "if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then", " printf '%s\\n' \"sentinel publish PipelineRun already exists; reusing $pipeline_run\"", " exit 0", "fi", "if ! kubectl create -f \"$tmp\"; then", " if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then", " printf '%s\\n' \"sentinel publish PipelineRun appeared concurrently; reusing $pipeline_run\"", " exit 0", " fi", " exit 1", "fi", ].join("\n"); } export function probeK8sJobScript(namespace: string, jobName: string): string { return [ "set +e", `namespace=${shellQuote(namespace)}`, `job=${shellQuote(jobName)}`, "succeeded=$(kubectl -n \"$namespace\" get job \"$job\" -o jsonpath='{.status.succeeded}' 2>/dev/null)", "failed=$(kubectl -n \"$namespace\" get job \"$job\" -o jsonpath='{.status.failed}' 2>/dev/null)", "active=$(kubectl -n \"$namespace\" get job \"$job\" -o jsonpath='{.status.active}' 2>/dev/null)", "pod=$(kubectl -n \"$namespace\" get pod -l job-name=\"$job\" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)", "pod_phase=''", "if [ -n \"$pod\" ]; then pod_phase=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.status.phase}' 2>/dev/null); fi", "logs_tail=''", "if [ -n \"$pod\" ]; then logs_tail=$({ kubectl -n \"$namespace\" logs \"$pod\" --all-containers=true --tail=80 2>/dev/null || true; for container in $(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.spec.initContainers[*].name}' 2>/dev/null); do kubectl -n \"$namespace\" logs \"$pod\" -c \"$container\" --tail=60 2>/dev/null || true; done; } | tail -c 6000 | base64 | tr -d '\\n'); fi", "node - \"$succeeded\" \"$failed\" \"$active\" \"$pod\" \"$pod_phase\" \"$logs_tail\" <<'NODE'", "const [succeeded, failed, active, pod, podPhase, logsB64] = process.argv.slice(2);", "console.log(JSON.stringify({ succeeded: Number(succeeded || 0) > 0, failed: Number(failed || 0) > 0, active: Number(active || 0) > 0, pod: pod || null, podPhase: podPhase || null, logsTail: Buffer.from(logsB64 || '', 'base64').toString('utf8'), valuesRedacted: true }));", "NODE", ].join("\n"); } function probeTektonPipelineRunScript(namespace: string, pipelineRunName: string): string { return [ "set +e", `namespace=${shellQuote(namespace)}`, `pipeline_run=${shellQuote(pipelineRunName)}`, "condition_status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o jsonpath='{.status.conditions[?(@.type==\"Succeeded\")].status}' 2>/dev/null || true)", "condition_reason=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o jsonpath='{.status.conditions[?(@.type==\"Succeeded\")].reason}' 2>/dev/null || true)", "condition_message_b64=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o jsonpath='{.status.conditions[?(@.type==\"Succeeded\")].message}' 2>/dev/null | head -c 1600 | base64 | tr -d '\\n' || true)", "if [ -z \"$condition_status\" ]; then condition_status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o jsonpath='{.status.conditions[0].status}' 2>/dev/null || true); fi", "if [ -z \"$condition_reason\" ]; then condition_reason=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o jsonpath='{.status.conditions[0].reason}' 2>/dev/null || true); fi", "if [ -z \"$condition_message_b64\" ]; then condition_message_b64=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o jsonpath='{.status.conditions[0].message}' 2>/dev/null | head -c 1600 | base64 | tr -d '\\n' || true); fi", "task_run=$(kubectl -n \"$namespace\" get taskrun -l tekton.dev/pipelineRun=\"$pipeline_run\" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)", "pod=$(kubectl -n \"$namespace\" get pod -l tekton.dev/pipelineRun=\"$pipeline_run\" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)", "pod_phase=''", "if [ -n \"$pod\" ]; then pod_phase=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.status.phase}' 2>/dev/null || true); fi", "logs_tail=''", "if [ -n \"$pod\" ]; then logs_tail=$({ kubectl -n \"$namespace\" logs \"$pod\" --all-containers=true --tail=80 2>/dev/null || true; kubectl -n \"$namespace\" logs \"$pod\" -c step-publish --tail=100 2>/dev/null || true; } | tail -c 6000 | base64 | tr -d '\\n'); fi", "node - \"$condition_status\" \"$condition_reason\" \"$condition_message_b64\" \"$task_run\" \"$pod\" \"$pod_phase\" \"$logs_tail\" <<'NODE'", "const [conditionStatus, conditionReason, conditionMessageB64, taskRun, pod, podPhase, logsB64] = process.argv.slice(2);", "const message = Buffer.from(conditionMessageB64 || '', 'base64').toString('utf8');", "const active = conditionStatus === 'Unknown' || (!conditionStatus && (podPhase === 'Pending' || podPhase === 'Running'));", "console.log(JSON.stringify({ succeeded: conditionStatus === 'True', failed: conditionStatus === 'False', active, conditionStatus: conditionStatus || null, conditionReason: conditionReason || null, conditionMessage: message || null, taskRun: taskRun || null, pod: pod || null, podPhase: podPhase || null, logsTail: Buffer.from(logsB64 || '', 'base64').toString('utf8'), valuesRedacted: true }));", "NODE", ].join("\n"); } export function sentinelPayloadFromLogs(logsTail: string): Record { const lines = logsTail.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean); for (let index = lines.length - 1; index >= 0; index -= 1) { const line = lines[index]; if (!line.startsWith("{") || !line.endsWith("}")) continue; const parsed = parseJsonObject(line); if (parsed !== null && (parsed.ok === true || parsed.ok === false)) return parsed; } return {}; } function withSentinelRemoteJobDiagnostics(state: SentinelCicdState, result: SentinelRemoteJobResult, domain: "source-mirror" | "publish"): SentinelRemoteJobResult { return { ...result, diagnostics: sentinelRemoteJobDiagnostics(state, result, domain), valuesRedacted: true }; } function sentinelRemoteJobDiagnostics(state: SentinelCicdState, result: SentinelRemoteJobResult, domain: "source-mirror" | "publish"): Record { const namespace = stringAt(state.cicd, "builder.namespace"); const probe = record(result.probe); const logsTail = typeof probe.logsTail === "string" ? probe.logsTail : ""; const events = sentinelStageEventsFromLogs(logsTail, domain); const envReuse = sentinelEnvReuseFromLogs(logsTail); const completedStages = sentinelCompletedStages(events, record(result.payload)); const stageTimings = sentinelStageTimingSummary(events, record(result.payload), result.elapsedMs); const currentPhase = sentinelCurrentRemotePhase(result, events, domain); const isPipelineRun = result.resourceKind === "PipelineRun"; const commands = { cliStatus: domain === "publish" ? `bun scripts/cli.ts web-probe sentinel control-plane status --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)}` : `bun scripts/cli.ts web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)}`, logs: result.jobName === "-" ? "-" : isPipelineRun ? `trans ${stringAt(state.controlPlaneNode, "kubeRoute")} kubectl -n ${namespace} logs -l tekton.dev/pipelineRun=${result.jobName} --all-containers=true --tail=120` : `trans ${stringAt(state.controlPlaneNode, "kubeRoute")} kubectl -n ${namespace} logs job/${result.jobName} --all-containers=true --tail=120`, describe: result.jobName === "-" ? "-" : isPipelineRun ? `trans ${stringAt(state.controlPlaneNode, "kubeRoute")} kubectl -n ${namespace} describe pipelinerun/${result.jobName}` : `trans ${stringAt(state.controlPlaneNode, "kubeRoute")} kubectl -n ${namespace} describe job/${result.jobName}`, gitMirrorStatus: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, gitMirrorSync: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`, gitMirrorFlush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm --wait`, controlPlaneApply: `bun scripts/cli.ts web-probe sentinel control-plane apply --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --confirm --wait`, publishCurrent: `bun scripts/cli.ts web-probe sentinel publish-current --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)} --confirm --wait`, valuesRedacted: true, }; return { domain, resourceKind: result.resourceKind ?? "Job", pipelineRun: isPipelineRun ? result.jobName : null, taskRun: probe.taskRun ?? null, currentPhase, completedStages, stageTimings, envReuse, pod: probe.pod ?? null, podPhase: probe.podPhase ?? null, active: probe.active ?? null, conditionStatus: probe.conditionStatus ?? null, conditionReason: probe.conditionReason ?? null, recentLogSummary: sentinelRecentLogSummary(logsTail), commands, valuesRedacted: true, }; } function sentinelEnvReuseFromLogs(logsTail: string): Record | null { const lines = logsTail.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean); for (let index = lines.length - 1; index >= 0; index -= 1) { const parsed = parseJsonObject(lines[index]); if (parsed !== null && parsed.event === "sentinel-publish-env-reuse") return { ...parsed, valuesRedacted: true }; } return null; } function sentinelStageEventsFromLogs(logsTail: string, domain: "source-mirror" | "publish"): Record[] { const expectedEvent = domain === "publish" ? "sentinel-publish-stage" : "sentinel-source-mirror-stage"; return logsTail .split(/\r?\n/u) .map((line) => parseJsonObject(line.trim())) .filter((item): item is Record => item !== null && item.event === expectedEvent); } function sentinelCompletedStages(events: readonly Record[], payload: Record): string[] { const completed = events .filter((event) => event.status === "succeeded" || event.status === "skipped") .map((event) => `${text(event.stage)}:${text(event.status)}`); const payloadStages = Array.isArray(payload.completedStages) ? payload.completedStages.map(text) : []; return Array.from(new Set([...completed, ...payloadStages])).filter((item) => item !== "-"); } function sentinelStageTimingSummary(events: readonly Record[], payload: Record, fallbackTotalMs: unknown): Record { const payloadTimings = record(payload.stageTimings); const eventElapsed = (stage: string): number | null => { const event = [...events].reverse().find((item) => item.stage === stage && (item.status === "succeeded" || item.status === "skipped" || item.status === "failed")); return event === undefined ? null : finiteNumberOrNull(event.elapsedMs); }; const sourceFetchMs = finiteNumberOrNull(payloadTimings.sourceFetchMs) ?? eventElapsed("source-fetch") ?? eventElapsed("source-mirror-fetch"); const monitorWebVerifyMs = finiteNumberOrNull(payloadTimings.monitorWebVerifyMs) ?? eventElapsed("monitor-web-verify"); const imageBuildMs = finiteNumberOrNull(payloadTimings.imageBuildMs) ?? eventElapsed("image-build"); const gitopsMs = finiteNumberOrNull(payloadTimings.gitopsMs) ?? eventElapsed("gitops"); const known = [sourceFetchMs, monitorWebVerifyMs, imageBuildMs, gitopsMs].filter((item): item is number => item !== null); const summedTotalMs = known.length === 0 ? null : known.reduce((sum, item) => sum + item, 0); const totalMs = finiteNumberOrNull(payloadTimings.totalMs) ?? finiteNumberOrNull(payload.elapsedMs) ?? finiteNumberOrNull(fallbackTotalMs) ?? summedTotalMs; const result = { sourceFetchMs, monitorWebVerifyMs, imageBuildMs, gitopsMs, totalMs, valuesRedacted: true, }; return Object.values(result).some((item) => item !== null && item !== true) ? result : {}; } function sentinelCurrentRemotePhase(result: SentinelRemoteJobResult, events: readonly Record[], domain: "source-mirror" | "publish"): string { if (result.phase === "job-succeeded" || result.phase === "pipelinerun-succeeded") return "completed"; if (result.phase === "create-job" || result.phase === "create-pipelinerun") return result.phase; const reversed = [...events].reverse(); const failed = reversed.find((event) => event.status === "failed"); if (failed !== undefined) return text(failed.stage); const running = reversed.find((event) => event.status === "running"); if (running !== undefined) return text(running.stage); const completed = new Set(events.filter((event) => event.status === "succeeded" || event.status === "skipped").map((event) => text(event.stage))); const order = domain === "publish" ? ["source-fetch", "monitor-web-verify", "image-build", "gitops"] : ["source-mirror-fetch"]; const next = order.find((stage) => !completed.has(stage)); return next ?? result.phase; } function sentinelRecentLogSummary(logsTail: string): string { const lines = logsTail .split(/\r?\n/u) .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith("{")) .slice(-5) .map((line) => short(line)); return lines.length === 0 ? "-" : lines.join(" | "); } export function sentinelRemoteJobTimeoutWarnings(job: unknown, subject: string): string[] { const remote = record(job); if (remote.phase !== "job-timeout" && remote.phase !== "pipelinerun-timeout") return []; const diagnostics = record(remote.diagnostics); const commands = record(diagnostics.commands); return [`${subject} reached wait budget at phase=${text(diagnostics.currentPhase)} completed=${text(Array.isArray(diagnostics.completedStages) ? diagnostics.completedStages.join(",") : "")}; inspect logs with ${text(commands.logs)} and continue via ${text(commands.cliStatus)}.`]; } export function publishSatisfiedByObservedWarnings(publish: unknown, flush: unknown, observedReady: boolean): string[] { if (!observedReady) return []; const warnings: string[] = []; const publishRecord = record(publish); if (Object.keys(publishRecord).length > 0 && publishRecord.ok !== true) { warnings.push(`sentinel publish did not finish cleanly in the foreground (phase=${text(publishRecord.phase)}), but follow-up control-plane observation proves source, registry, GitOps, Argo and runtime are aligned; treating the publish wait result as visibility warning.`); } const flushRecord = record(flush); if (Object.keys(flushRecord).length > 0 && flushRecord.ok !== true) { warnings.push("sentinel git-mirror flush did not finish cleanly in the foreground, but runtime alignment is already proven; use git-mirror status/flush drill-down for GitHub mirror closeout."); } return warnings; } export function controlPlaneWaitWarningSeconds(state: SentinelCicdState): number { return numberAt(state.cicd, "confirmWait.maxSeconds"); } export function sentinelCicdElapsedWarnings(value: unknown, subject: string, budgetSeconds: number): string[] { const elapsedMs = typeof value === "number" && Number.isFinite(value) ? value : null; const budgetMs = Math.max(1, Math.trunc(budgetSeconds)) * 1000; if (elapsedMs === null || elapsedMs <= budgetMs) return []; return [`${subject} exceeded configured ${Math.round(budgetMs / 1000)}s CI/CD wait budget (${Math.round(elapsedMs / 1000)}s); optimize wait-stage latency before rerunning long confirm-wait operations.`]; } export function sourceMirrorAlreadyReadyWarnings(state: SentinelCicdState, sourceMirrorSync: unknown): string[] { const sync = record(sourceMirrorSync); if (sync.ok === true || state.sourceHead.ok !== true) return []; return [`sentinel source mirror sync did not complete, but internal git mirror already contains ${short(state.sourceHead.commit)}; continuing publish from the YAML-declared read URL and treating the sync failure as a non-blocking egress warning.`]; } export function sentinelSourceMirrorAlreadyPresentResult(state: SentinelCicdState, probe: unknown): Record { return { ok: true, phase: "already-present", jobName: null, probe: record(probe), payload: { ok: true, status: "already-present", sourceCommit: state.sourceHead.commit, mirrorCommit: state.sourceHead.mirrorCommit ?? state.sourceHead.commit, stageRef: state.sourceHead.stageRef, sourceAuthority: state.sourceHead.sourceAuthority, valuesRedacted: true, }, polls: 0, elapsedMs: 0, valuesRedacted: true, }; }