diff --git a/.agents/skills/unidesk-cicd/references/control-plane.md b/.agents/skills/unidesk-cicd/references/control-plane.md index 8807ec7b..8061764b 100644 --- a/.agents/skills/unidesk-cicd/references/control-plane.md +++ b/.agents/skills/unidesk-cicd/references/control-plane.md @@ -80,3 +80,5 @@ runtime base image 走 `config/hwlab-node-lanes.yaml`:`baseImageSource` 是公 PipelineRun 失败或长时间未完成时,先按定点 `control-plane status --pipeline-run ` 和 bounded 只读诊断定位失败 TaskRun/Pod/container。env-reuse service build 常见失败点是 `build-` 的 `step-publish` 日志;先用 `platform-infra sub2api status|validate` 区分共享 proxy 整体故障和单个上游 transient。proxy 健康但单个依赖下载 transient 时,可以受控 `trigger-current --rerun`;重复失败应修 `artifact-publish`/envRecipe 的有限 retry 后重新合并发布。 小范围 PR 触发 120s 时必须看 plan artifacts 的 `affectedServices/buildServices/reusedServices`:如果 source diff 很小却出现所有 envreuse 服务都在 `buildServices` 且 `reusedServices=[]`,优先怀疑 current GitOps artifact catalog 没有 hydrate 到 source plan 阶段,而不是继续盲目重跑 PipelineRun。 + +Web 哨兵 control-plane 出现 registry half-state、GitOps 超预算或异步 git-mirror flush 时,先看默认 `DRILLDOWN` 表。PipelineRun/TaskRun 细节走 `platform-infra pipelines-as-code history --id`,registry tag 走 `web-probe sentinel image status`,GitOps flush 走受控 `hwlab nodes git-mirror status|flush` 或 async job status;不要回退到裸 `kubectl/tkn/argo` 或解析原生命令输出。 diff --git a/.agents/skills/unidesk-cicd/references/gitea-pac.md b/.agents/skills/unidesk-cicd/references/gitea-pac.md index fe69e88b..32b61a21 100644 --- a/.agents/skills/unidesk-cicd/references/gitea-pac.md +++ b/.agents/skills/unidesk-cicd/references/gitea-pac.md @@ -34,6 +34,8 @@ Use `bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 - Do not use `cicd branch-follower status` to decide whether the three migrated JD01 consumers are current. It is historical/migration-only and may contain stale state from before PaC cutover. +For migrated sentinel CI/CD half-state triage, the PaC `status` command owns the source/registry/GitOps/Argo/runtime diagnosis. It must classify registry-missing, GitOps-missing, Argo-pending and runtime-mismatch states from target-side short probes, using YAML-declared probe endpoints such as `registry_probe_base`; do not reintroduce old control-plane status as the primary architecture or use registry manifest `HEAD` as the readiness check. + ## Reuse Interpretation - AgentRun `IMAGE_STATUS=reused` means the image build was skipped or registry artifact was reused for the same env identity. diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 9595a695..40dfc3d5 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -94,6 +94,10 @@ repositories: node: JD01 lane: v03 sentinel_id: jd01-web-probe-sentinel + image_repository: 127.0.0.1:5000/hwlab/web-probe-sentinel-jd01 + registry_probe_base: http://127.0.0.1:5000 + gitops_branch: v0.3-gitops + gitops_manifest_path: deploy/gitops/node/jd01/web-probe-sentinel/web-probe-sentinel.yaml pipeline_name: hwlab-web-probe-sentinel-jd01-pac pipeline_run_prefix: hwlab-web-probe-sentinel-jd01 - id: hwlab-jd01-v03 diff --git a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts index 654b3b6a..2594d997 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts @@ -482,6 +482,7 @@ export function renderPublishCurrentResult(result: Record): str const blocker = record(result.blocker); const recoveryNext = record(controlPlane.recoveryNext); const next = record(result.next); + const drillDown = record(result.drillDown); const warnings = Array.isArray(result.warnings) ? result.warnings : []; const slowStages = Array.isArray(result.slowStages) ? result.slowStages.map(record) : []; const lines = [ @@ -575,6 +576,18 @@ export function renderPublishCurrentResult(result: Record): str "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : ["BLOCKER", table(["CODE", "REASON"], [[blocker.code, blocker.reason]])].join("\n"), "", + Object.keys(drillDown).length === 0 ? "DRILLDOWN\n-" : [ + "DRILLDOWN", + table(["PIPELINERUN", "ASYNC_FLUSH_JOB"], [[drillDown.pipelineRun ?? "-", drillDown.asyncFlushJob ?? "-"]]), + ` pipeline-run: ${drillDown.pipelineRunStatus ?? "-"}`, + ` pipeline-run-full: ${drillDown.pipelineRunFull ?? "-"}`, + ` pac-status: ${drillDown.consumerStatus ?? "-"}`, + ` image-status: ${drillDown.imageStatus ?? "-"}`, + ` git-mirror: ${drillDown.gitMirrorStatus ?? "-"}`, + ` flush: ${drillDown.gitMirrorFlush ?? "-"}`, + ` async-flush-status: ${drillDown.asyncFlushStatus ?? "-"}`, + ].join("\n"), + "", Object.keys(recoveryNext).length === 0 ? "RECOVERY_NEXT\n-" : [ "RECOVERY_NEXT", table(["REASON", "PIPELINERUN", "DIGEST", "GITOPS"], [[recoveryNext.reason, recoveryNext.pipelineRun ?? "-", short(recoveryNext.digestRef), short(recoveryNext.gitopsCommit)]]), @@ -665,6 +678,7 @@ export function renderControlPlaneResult(result: Record): strin const argoApply = record(result.argoApply); const blocker = record(result.blocker); const statusDiagnosis = record(result.statusDiagnosis); + const drillDown = record(result.drillDown); const targetValidation = record(result.targetValidation); const targetValidationBusiness = record(targetValidation.businessStatus); const recoveryNext = record(result.recoveryNext); @@ -700,6 +714,18 @@ export function renderControlPlaneResult(result: Record): strin ]]), ].join("\n"), "", + Object.keys(drillDown).length === 0 ? "DRILLDOWN\n-" : [ + "DRILLDOWN", + table(["PIPELINERUN", "ASYNC_FLUSH_JOB"], [[drillDown.pipelineRun ?? "-", drillDown.asyncFlushJob ?? "-"]]), + ` pipeline-run: ${drillDown.pipelineRunStatus ?? "-"}`, + ` pipeline-run-full: ${drillDown.pipelineRunFull ?? "-"}`, + ` pac-status: ${drillDown.consumerStatus ?? "-"}`, + ` image-status: ${drillDown.imageStatus ?? "-"}`, + ` git-mirror: ${drillDown.gitMirrorStatus ?? "-"}`, + ` flush: ${drillDown.gitMirrorFlush ?? "-"}`, + ` async-flush-status: ${drillDown.asyncFlushStatus ?? "-"}`, + ].join("\n"), + "", Object.keys(sourceMirrorSync).length === 0 ? "SOURCE_MIRROR_SYNC\n-" : table(["OK", "PHASE", "JOB", "COMMIT", "STAGE_REF", "ELAPSED"], [[sourceMirrorSync.ok, sourceMirrorSync.phase, sourceMirrorSync.jobName, short(record(sourceMirrorSync.payload).mirrorCommit), short(record(sourceMirrorSync.payload).stageRef), sourceMirrorSync.elapsedMs ?? "-"]]), "", Object.keys(targetValidation).length === 0 ? "TARGET_VALIDATION\n-" : table(["OK", "STATUS", "BUSINESS", "ERROR_TITLE", "SCENARIO", "RUN", "OBSERVER", "REPORT", "FINDINGS", "ARTIFACTS"], [[ diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index f46e8998..3a3e1082 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -280,6 +280,7 @@ function runSentinelControlPlane(state: SentinelCicdState, options: Extract { + const pipelineRunName = nonEmptyString(pipelineRun); + const next = controlPlaneNext(state, "status"); + const pacBase = "bun scripts/cli.ts platform-infra pipelines-as-code"; + const flushRecord = record(flush); + const flushNext = record(flushRecord.next); + const flushJob = record(flushRecord.job); + return { + pipelineRun: pipelineRunName, + pipelineRunStatus: pipelineRunName === null + ? null + : `${pacBase} history --target ${state.spec.nodeId} --id ${pipelineRunName}`, + pipelineRunFull: pipelineRunName === null + ? null + : `${pacBase} history --target ${state.spec.nodeId} --id ${pipelineRunName} --full`, + consumerStatus: `${pacBase} status --target ${state.spec.nodeId} --consumer sentinel-${state.spec.nodeId.toLowerCase()}-${state.spec.lane}`, + imageStatus: `bun scripts/cli.ts web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)}`, + gitMirrorStatus: next.gitMirrorStatus, + gitMirrorFlush: next.gitMirrorFlush, + asyncFlushJob: nonEmptyString(flushJob.id), + asyncFlushStatus: nonEmptyString(flushNext.status), + valuesRedacted: true, + }; +} + function applySentinelRuntimeSecrets(state: SentinelCicdState, timeoutSeconds: number): Record { const sourcesByPurpose = new Map>(); for (const source of arrayAt(state.secrets, "sources").map(record)) { diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 198564b5..e07f0724 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -27,6 +27,23 @@ duration_seconds() { echo null } +json_normalize() { + UNIDESK_JSON_NORMALIZE_INPUT="${1:-}" node <<'NODE' +const input = String(process.env.UNIDESK_JSON_NORMALIZE_INPUT || '').trim(); +if (!input) { + process.stdout.write('{}'); + process.exit(0); +} +for (let end = input.length; end > 0; end -= 1) { + try { + process.stdout.write(JSON.stringify(JSON.parse(input.slice(0, end)))); + process.exit(0); + } catch {} +} +process.stdout.write('{}'); +NODE +} + api_url() { prepare_gitea_api_base printf '%s/api/v1/%s' "$UNIDESK_PAC_GITEA_API_BASE_URL" "$1" @@ -632,29 +649,35 @@ function envReuseOf(item) { const env = item.envReuse || {}; return env.dependencyReuse || env.mode || item.envReuseStatus || null; } -process.stdout.write(JSON.stringify(image ? { - imageStatus: image.imageStatus || image.status || (image.digestRef ? 'built' : null), - envIdentity: image.envIdentity || null, - envReuse: envReuseOf(image), - nodeDepsReuse: null, - buildCache: null, - digest: digestOf(image), - gitopsCommit: image.gitopsCommit || null, - sourceCommit: image.sourceCommit || null, - valuesPrinted: false, -} : humanEnv ? { - imageStatus: 'published', - envIdentity: null, - envReuse: humanEnv.envReuse, - nodeDepsReuse: humanEnv.nodeDeps, - buildCache: humanEnv.cache, - buildPackage: humanEnv.buildPackage, - buildNetwork: humanEnv.buildNetwork, - digest: null, - gitopsCommit: null, - sourceCommit: null, - valuesPrinted: false, -} : { valuesPrinted: false })); +let out = { valuesPrinted: false }; +if (image) { + out = { + imageStatus: image.imageStatus || image.status || (image.digestRef ? 'built' : null), + envIdentity: image.envIdentity || null, + envReuse: envReuseOf(image), + nodeDepsReuse: null, + buildCache: null, + digest: digestOf(image), + gitopsCommit: image.gitopsCommit || null, + sourceCommit: image.sourceCommit || null, + valuesPrinted: false, + }; +} else if (humanEnv) { + out = { + imageStatus: 'published', + envIdentity: null, + envReuse: humanEnv.envReuse, + nodeDepsReuse: humanEnv.nodeDeps, + buildCache: humanEnv.cache, + buildPackage: humanEnv.buildPackage, + buildNetwork: humanEnv.buildNetwork, + digest: null, + gitopsCommit: null, + sourceCommit: null, + valuesPrinted: false, + }; +} +process.stdout.write(JSON.stringify(out)); NODE rm -f "$log_file" } @@ -701,6 +724,149 @@ NODE rm -f "$deploy_file" } +cicd_diagnostics() { + params_file=$(mktemp) + pipelines_file=$(mktemp) + artifact_file=$(mktemp) + argo_file=$(mktemp) + runtime_file=$(mktemp) + printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file" + printf '%s' "${1:-[]}" >"$pipelines_file" + printf '%s' "${2:-{}}" >"$artifact_file" + printf '%s' "${3:-{}}" >"$argo_file" + printf '%s' "${4:-{}}" >"$runtime_file" + probe_env=$(node - "$params_file" "$pipelines_file" <<'NODE' +const fs = require('node:fs'); +const params = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{}'); +const pipelines = JSON.parse(fs.readFileSync(process.argv[3], 'utf8') || '[]'); +const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {}; +const sourceCommit = latest.sourceCommit || null; +const tag = sourceCommit ? String(sourceCommit).slice(0, 12) : ''; +const imageRepository = typeof params.image_repository === 'string' ? params.image_repository : ''; +const probeBase = typeof params.registry_probe_base === 'string' ? params.registry_probe_base.replace(/\/+$/u, '') : ''; +let registryUrl = ''; +if (imageRepository && tag) { + const firstSlash = imageRepository.indexOf('/'); + const registry = firstSlash >= 0 ? imageRepository.slice(0, firstSlash) : imageRepository; + const path = firstSlash >= 0 ? imageRepository.slice(firstSlash + 1) : ''; + const base = probeBase || (registry ? `http://${registry}` : ''); + registryUrl = base && path ? `${base}/v2/${path}/manifests/${tag}` : ''; +} +function line(key, value) { + process.stdout.write(`${key}=${Buffer.from(String(value || ''), 'utf8').toString('base64')}\n`); +} +line('imageRepository', imageRepository); +line('registryProbeBase', probeBase); +line('sourceCommit', sourceCommit || ''); +line('tag', tag); +line('registryUrl', registryUrl); +line('sentinelId', params.sentinel_id || ''); +line('gitopsBranch', params.gitops_branch || ''); +line('gitopsManifestPath', params.gitops_manifest_path || ''); +NODE +) + image_repository=$(printf '%s\n' "$probe_env" | sed -n 's/^imageRepository=//p' | base64 -d 2>/dev/null || true) + registry_probe_base=$(printf '%s\n' "$probe_env" | sed -n 's/^registryProbeBase=//p' | base64 -d 2>/dev/null || true) + source_commit=$(printf '%s\n' "$probe_env" | sed -n 's/^sourceCommit=//p' | base64 -d 2>/dev/null || true) + image_tag=$(printf '%s\n' "$probe_env" | sed -n 's/^tag=//p' | base64 -d 2>/dev/null || true) + registry_url=$(printf '%s\n' "$probe_env" | sed -n 's/^registryUrl=//p' | base64 -d 2>/dev/null || true) + sentinel_id=$(printf '%s\n' "$probe_env" | sed -n 's/^sentinelId=//p' | base64 -d 2>/dev/null || true) + gitops_branch=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsBranch=//p' | base64 -d 2>/dev/null || true) + gitops_manifest_path=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsManifestPath=//p' | base64 -d 2>/dev/null || true) + registry_present=false + registry_digest="" + if [ -n "$registry_url" ]; then + header_file=$(mktemp) + if curl -fsS -D "$header_file" -o /dev/null -H 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' "$registry_url" >/dev/null 2>&1; then + registry_present=true + fi + headers=$(cat "$header_file" 2>/dev/null || true) + rm -f "$header_file" + if printf '%s' "$headers" | grep -qi '^HTTP/.* 200'; then registry_present=true; fi + registry_digest=$(printf '%s\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ { sub(/\r$/,"",$2); print $2; exit }') + fi + registry_present_json=false + if [ "$registry_present" = true ]; then registry_present_json=true; fi + export UNIDESK_PAC_DIAG_IMAGE_REPOSITORY="$image_repository" + export UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE="$registry_probe_base" + export UNIDESK_PAC_DIAG_SOURCE_COMMIT="$source_commit" + export UNIDESK_PAC_DIAG_IMAGE_TAG="$image_tag" + export UNIDESK_PAC_DIAG_REGISTRY_URL="$registry_url" + export UNIDESK_PAC_DIAG_REGISTRY_PRESENT="$registry_present_json" + export UNIDESK_PAC_DIAG_REGISTRY_DIGEST="$registry_digest" + export UNIDESK_PAC_DIAG_SENTINEL_ID="$sentinel_id" + export UNIDESK_PAC_DIAG_GITOPS_BRANCH="$gitops_branch" + export UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH="$gitops_manifest_path" + node - "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file" <<'NODE' +const fs = require('node:fs'); +function parseLoose(path, fallback) { + const input = fs.readFileSync(path, 'utf8').trim(); + if (!input) return fallback; + for (let end = input.length; end > 0; end -= 1) { + try { return JSON.parse(input.slice(0, end)); } catch {} + } + return fallback; +} +const params = parseLoose(process.argv[2], {}); +const pipelines = parseLoose(process.argv[3], []); +const artifact = parseLoose(process.argv[4], {}); +const argo = parseLoose(process.argv[5], {}); +const runtime = parseLoose(process.argv[6], {}); +const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {}; +const registryPresent = process.env.UNIDESK_PAC_DIAG_REGISTRY_PRESENT === 'true'; +const sourceCommit = process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null; +const registryDigest = process.env.UNIDESK_PAC_DIAG_REGISTRY_DIGEST || null; +const expectedDigest = registryDigest || artifact.digest || null; +const runtimeImage = runtime.image || null; +const runtimeMatches = !expectedDigest || (typeof runtimeImage === 'string' && runtimeImage.includes(expectedDigest)); +const gitopsReady = Boolean(artifact.gitopsCommit || argo.revision); +let code = 'pac-diagnostics-not-applicable'; +let phase = 'not-applicable'; +let ok = true; +let hint = 'consumer has no sentinel image_repository diagnostic config'; +if (params.image_repository) { + if (!sourceCommit) { + ok = false; code = 'sentinel-pac-source-unknown'; phase = 'source-unknown'; hint = 'latest PaC PipelineRun did not expose a source commit'; + } else if (!registryPresent) { + ok = false; code = 'sentinel-pac-registry-missing'; phase = 'source-ready-registry-missing'; hint = 'PaC source commit is known but the configured registry tag is missing'; + } else if (!gitopsReady) { + ok = false; code = 'sentinel-pac-gitops-missing'; phase = 'registry-ready-gitops-missing'; hint = 'registry tag exists but GitOps revision is not visible in PaC artifact or Argo status'; + } else if (argo.sync !== 'Synced' || argo.health !== 'Healthy') { + ok = false; code = 'sentinel-pac-argo-not-ready'; phase = 'gitops-ready-argo-pending'; hint = 'GitOps exists but Argo is not Synced/Healthy'; + } else if (!runtimeMatches) { + ok = false; code = 'sentinel-pac-runtime-not-aligned'; phase = 'argo-ready-runtime-mismatch'; hint = 'runtime image does not match the registry digest observed for the source commit'; + } else { + code = 'sentinel-pac-ready'; phase = 'ready'; hint = 'source, registry tag, GitOps, Argo and runtime are aligned'; + } +} +process.stdout.write(JSON.stringify({ + ok, + code, + phase, + hint, + sourceCommit, + imageRepository: process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY || null, + registryProbeBase: process.env.UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE || null, + imageTag: process.env.UNIDESK_PAC_DIAG_IMAGE_TAG || null, + registry: { + present: registryPresent, + digest: registryDigest, + url: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null, + }, + gitops: { + branch: process.env.UNIDESK_PAC_DIAG_GITOPS_BRANCH || null, + manifestPath: process.env.UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH || null, + commit: artifact.gitopsCommit || argo.revision || null, + }, + argo: { sync: argo.sync || null, health: argo.health || null, revision: argo.revision || null }, + runtime: { image: runtimeImage, digest: runtime.digest || null, readyReplicas: runtime.readyReplicas ?? null, replicas: runtime.replicas ?? null }, + pipelineRun: latest.name || null, + valuesPrinted: false, +})); +NODE + rm -f "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file" +} + hook_summary() { hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" 2>/dev/null || echo '[]') HOOKS_JSON="$hooks" node <<'NODE' @@ -719,16 +885,18 @@ status_action() { latest=$(printf '%s' "$pipelines" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(a[0]?.name||"")') export UNIDESK_PAC_TARGET_PIPELINERUN="$latest" tasks=$(task_rows) - artifact=$(artifact_summary) + artifact=$(json_normalize "$(artifact_summary)") hooks=$(hook_summary) argo=$(kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json 2>/dev/null | node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null}))' || echo '{}') - runtime=$(runtime_summary) - printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"valuesPrinted":false}\n' \ + argo=$(json_normalize "$argo") + runtime=$(json_normalize "$(runtime_summary)") + diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime") + printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \ "$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \ "$( [ -n "$crd" ] && echo true || echo false )" \ "$(json_string "$controller_ready")" \ "$(json_string "$repository_condition")" \ - "$hooks" "$pipelines" "$tasks" "$artifact" "$argo" "$runtime" + "$hooks" "$pipelines" "$tasks" "$artifact" "$argo" "$runtime" "$diagnostics" } history_action() { diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index f04c633e..8ecb078d 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -615,6 +615,7 @@ function statusSummary(payload: Record): Record): Record): Record): RenderedCliResult { const taskRuns = arrayRecords(summary.taskRuns); const artifact = record(summary.artifact); const argo = record(summary.argo); + const diagnostics = record(summary.diagnostics); const lines = [ "PLATFORM-INFRA PIPELINES-AS-CODE STATUS", ...(coverage.length === 0 ? [] : [ @@ -802,6 +805,19 @@ function renderStatus(result: Record): RenderedCliResult { "ARGO", ...table(["SYNC", "HEALTH", "REVISION"], [[stringValue(argo.sync), stringValue(argo.health), short(stringValue(argo.revision))]]), "", + "CICD DIAGNOSIS", + ...table(["OK", "CODE", "PHASE", "SOURCE", "REGISTRY", "GITOPS", "RUNTIME"], [[ + boolText(diagnostics.ok !== false), + stringValue(diagnostics.code), + stringValue(diagnostics.phase), + short(stringValue(diagnostics.sourceCommit)), + registryText(record(diagnostics.registry)), + short(stringValue(record(diagnostics.gitops).commit)), + runtimeText(record(diagnostics.runtime)), + ]]), + ` hint: ${compactLine(stringValue(diagnostics.hint))}`, + ` pipeline-run: ${latest.name === undefined ? "-" : `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${stringValue(record(result.target).id)} --id ${stringValue(latest.name)}`}`, + "", "NEXT", ` full: ${stringValue(record(result.next).status)} --full`, ` all-history: ${stringValue(record(result.next).history).replace(/ --consumer [^ ]+/u, "")} --limit 10`, @@ -1042,6 +1058,20 @@ function envReuseText(envReuse: Record): string { return parts.length === 0 ? "-" : parts.join(","); } +function registryText(registry: Record): string { + if (Object.keys(registry).length === 0) return "-"; + const present = registry.present === true ? "present" : "missing"; + const digest = short(stringValue(registry.digest), 18); + return digest === "-" ? present : `${present}:${digest}`; +} + +function runtimeText(runtime: Record): string { + if (Object.keys(runtime).length === 0) return "-"; + const ready = `${stringValue(runtime.readyReplicas)}/${stringValue(runtime.replicas)}`; + const digest = short(stringValue(runtime.digest), 18); + return digest === "-" ? ready : `${ready}:${digest}`; +} + function renderHistoryDetail(row: Record): string[] { const envReuse = record(row.envReuse); const taskRuns = record(row.taskRuns);