fix: recognize retained PaC source observations
This commit is contained in:
@@ -25,6 +25,7 @@ bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 --consu
|
||||
bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --consumer <consumer> --source-commit <sha> --wait
|
||||
bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --limit 10
|
||||
bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>
|
||||
bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer>
|
||||
```
|
||||
|
||||
节点级只读状态必须优先用 `cicd status --node <NODE>`。它从 `config/platform-infra/pipelines-as-code.yaml` 找到该 node 的所有当前 PaC consumer,一次性汇总 PipelineRun、Argo/GitOps、runtime readiness 和诊断;不要再靠阅读源码或手动拼三条 consumer 命令来回答 “NC01 的 CI/CD 流水线情况”。`platform-infra pipelines-as-code status --target <NODE> --consumer <id>` 只作为单 consumer drill-down。
|
||||
@@ -46,6 +47,13 @@ bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <
|
||||
- CI/CD source authority 只能来自 YAML 声明的 Kubernetes 托管 source authority:legacy lane 使用 k8s git-mirror snapshot,Gitea/PaC migrated lane 使用 GitHub PR merge -> GitHub webhook bridge -> Gitea controlled mirror + immutable snapshot ref -> Pipelines-as-Code。受控命令先在 k8s 内同步/创建不可变 `refs/unidesk/snapshots/.../<commit>` stage ref;build/status/publish 只消费该 snapshot,host worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。
|
||||
- JD01/NC01 `agentrun-<node>-v02`、`sentinel-<node>-v03`、`hwlab-<node>-v03` 的正式 CI/CD closeout 入口是 `cicd status --node <NODE>` 和 `platform-infra pipelines-as-code closeout|status|history --target <NODE> --consumer <id>`。`closeout` 是通用 consumer 引导入口,只读取/等待 PaC、Tekton、GitOps、Argo 和 runtime 对齐,不触发旧手动发布。`cicd branch-follower` 和 `cicd gitea-actions-poc` 对这些 consumer 只保留历史/迁移只读用途,不得作为当前交付判断入口。
|
||||
- PaC `.tekton` 文件必须用 Repository CR 的 target/node 参数隔离 JD01/NC01,避免同一个 Gitea push 在一个 target cluster 内额外创建另一个 target 的 PipelineRun;history/detail 必须按实际 PipelineRun prefix/pipeline 归属 consumer。
|
||||
- PaC source-only closeout 必须使用目标侧结构化证据:
|
||||
- `g14-ci-plan` 的 source commit、affected/build/rollout 计数必须完整;
|
||||
- `collect-artifacts` 与 `gitops-promote` 必须同时明确 `no-build-no-rollout-plan`;
|
||||
- 三者一致时才允许返回 `no-runtime-change` 和 `retained` provenance;
|
||||
- 仍要求 PipelineRun 成功、原 Argo `Synced/Healthy`、runtime ready、sha256 digest pin 与 health ready;
|
||||
- 证据缺失、冲突或 source commit 不匹配必须 fail-closed,不得等价为 delivery disabled,也不得伪造新 artifact、GitOps revision 或 runtime source commit。
|
||||
- PaC `status|closeout|history` 的 `--json` 是有界 machine contract;`--full` 展开单条详情,`--raw` 才展开目标侧原始 payload。`history --id --full` 不得重复嵌套同一 rows/consumer payload或依赖 `/tmp/unidesk-cli-output` 才可读。
|
||||
- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRef:branch-follower controller 读 `config/cicd-branch-followers.yaml#controller.source.githubSsh`,runtime legacy git-mirror 读 owning lane/control-plane YAML 的 host proxy 和 `githubTransport`,Gitea/PaC 迁移 lane 读 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml`;禁止依赖未声明 host env、trans proxy、裸直连 GitHub 或 CLI 输出解析。
|
||||
- Gitea/PaC lane 的 GitHub -> Gitea 自动同步必须用 `platform-infra gitea mirror webhook apply|status|test`。该路径是单向同步,只更新 Gitea branch 和 immutable snapshot ref;不得引入 Gitea -> GitHub 回写、轮询 fallback、Gitea Actions/act_runner fallback 或第二套状态存储。
|
||||
- Gitea webhook/FRPC/Caddy 变更的 closeout 必须看受控 CLI 的 rollout/status/test 证据;Secret 或 ConfigMap apply 成功不等于 connector Pod 已加载新 proxy/path。
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
"use strict";
|
||||
|
||||
function record(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
||||
}
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function stringArray(value) {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
||||
}
|
||||
|
||||
function digestFromImage(value) {
|
||||
if (typeof value !== "string" || !value.includes("@")) return null;
|
||||
return value.split("@").slice(1).join("@") || null;
|
||||
}
|
||||
|
||||
function exactSkipMarker(records, event) {
|
||||
return [...records].reverse().find((item) => item.event === event
|
||||
&& item.status === "skipped"
|
||||
&& item.reason === "no-build-no-rollout-plan") || null;
|
||||
}
|
||||
|
||||
function extractPacSourceObservation(recordsInput) {
|
||||
const records = Array.isArray(recordsInput) ? recordsInput.map(record) : [];
|
||||
const plan = [...records].reverse().find((item) => item.event === "g14-ci-plan") || null;
|
||||
const collectMarker = exactSkipMarker(records, "collect-artifacts");
|
||||
const promoteMarker = exactSkipMarker(records, "gitops-promote");
|
||||
if (plan === null && collectMarker === null && promoteMarker === null) return null;
|
||||
|
||||
const affected = stringArray(plan?.affectedServices);
|
||||
const rollout = stringArray(plan?.rolloutServices);
|
||||
const build = stringArray(plan?.buildServices);
|
||||
const reused = stringArray(plan?.reusedServices);
|
||||
const sourceCommit = stringOrNull(plan?.sourceCommitId);
|
||||
const planShapeValid = affected !== null && rollout !== null && build !== null;
|
||||
const sourceCommitValid = sourceCommit !== null && /^[0-9a-f]{40}$/iu.test(sourceCommit);
|
||||
const noRuntimeChangePlan = planShapeValid
|
||||
&& affected.length === 0
|
||||
&& rollout.length === 0
|
||||
&& build.length === 0;
|
||||
const noRuntimeChangeMarkers = collectMarker !== null && promoteMarker !== null;
|
||||
const skipMarkerSeen = collectMarker !== null || promoteMarker !== null;
|
||||
const skipMarkerConflict = skipMarkerSeen && !noRuntimeChangePlan;
|
||||
|
||||
let mode = "inconsistent";
|
||||
let valid = false;
|
||||
let reason = "structured-plan-or-terminal-marker-is-incomplete";
|
||||
if (planShapeValid && sourceCommitValid && noRuntimeChangePlan && noRuntimeChangeMarkers) {
|
||||
mode = "no-runtime-change";
|
||||
valid = true;
|
||||
reason = "no-build-no-rollout-plan";
|
||||
} else if (planShapeValid && sourceCommitValid && !noRuntimeChangePlan && !skipMarkerConflict) {
|
||||
mode = "delivery";
|
||||
valid = true;
|
||||
reason = "artifact-or-runtime-delivery-required";
|
||||
} else if (!sourceCommitValid) {
|
||||
reason = "source-commit-invalid-or-missing";
|
||||
} else if (!planShapeValid) {
|
||||
reason = "delivery-plan-shape-invalid-or-missing";
|
||||
} else if (skipMarkerConflict) {
|
||||
reason = "no-runtime-change-marker-conflicts-with-delivery-plan";
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: "v1",
|
||||
contract: "hwlab-g14-ci-plan",
|
||||
mode,
|
||||
valid,
|
||||
reason,
|
||||
sourceCommit,
|
||||
plan: {
|
||||
affectedServiceCount: affected?.length ?? null,
|
||||
rolloutServiceCount: rollout?.length ?? null,
|
||||
buildServiceCount: build?.length ?? null,
|
||||
reusedServiceCount: reused?.length ?? null,
|
||||
},
|
||||
terminal: {
|
||||
collectArtifacts: collectMarker === null ? "missing" : "no-build-no-rollout-plan",
|
||||
gitopsPromote: promoteMarker === null ? "missing" : "no-build-no-rollout-plan",
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function evaluatePacStatus(inputValue) {
|
||||
const input = record(inputValue);
|
||||
const artifact = record(input.artifact);
|
||||
const argo = record(input.argo);
|
||||
const runtime = record(input.runtime);
|
||||
const sourceObservation = record(artifact.sourceObservation);
|
||||
const revisionRelation = record(argo.revisionRelation);
|
||||
const gitopsRelation = stringOrNull(revisionRelation.relation) || "unknown";
|
||||
const sourceCommit = stringOrNull(input.sourceCommit);
|
||||
const registryDigest = stringOrNull(input.registryDigest);
|
||||
const artifactDigest = stringOrNull(artifact.digest);
|
||||
const expectedDigest = registryDigest || artifactDigest;
|
||||
const runtimeImage = stringOrNull(runtime.image);
|
||||
const runtimeDigest = stringOrNull(runtime.digest) || digestFromImage(runtimeImage);
|
||||
const runtimeMatches = expectedDigest === null || runtimeDigest === expectedDigest;
|
||||
const runtimeReady = Number.isInteger(runtime.replicas)
|
||||
&& runtime.replicas > 0
|
||||
&& runtime.readyReplicas === runtime.replicas;
|
||||
const selectedArtifactDigests = [...new Set([
|
||||
artifactDigest,
|
||||
...(Array.isArray(artifact.digests) ? artifact.digests : []),
|
||||
].filter((value) => typeof value === "string" && value.length > 0))];
|
||||
const selectedArtifactMatchesRuntime = runtimeDigest !== null && selectedArtifactDigests.includes(runtimeDigest);
|
||||
const registryMatchesArtifact = registryDigest === null || artifactDigest === null || registryDigest === artifactDigest;
|
||||
const healthUrl = stringOrNull(input.healthUrl);
|
||||
const healthStatus = stringOrNull(input.healthStatus);
|
||||
const healthReady = healthUrl === null || healthStatus === "200";
|
||||
const gitopsReady = stringOrNull(artifact.gitopsCommit) !== null && stringOrNull(argo.revision) !== null;
|
||||
const gitopsRevisionAligned = gitopsRelation === "exact" || gitopsRelation === "descendant";
|
||||
const deliveryDisabled = artifact.imageStatus === "disabled";
|
||||
const deliverySkipped = artifact.imageStatus === "skipped";
|
||||
const sourceObservationMode = stringOrNull(sourceObservation.mode);
|
||||
const noRuntimeChange = sourceObservationMode === "no-runtime-change";
|
||||
const sourceObservationInconsistent = sourceObservationMode === "inconsistent"
|
||||
|| (sourceObservationMode !== null && sourceObservation.valid !== true);
|
||||
const observationSourceCommit = stringOrNull(sourceObservation.sourceCommit);
|
||||
const observationSourceMatches = sourceCommit !== null && observationSourceCommit === sourceCommit;
|
||||
const retainedRevision = stringOrNull(argo.revision);
|
||||
const retainedDigestReady = runtimeDigest !== null && /^sha256:[0-9a-f]{64}$/iu.test(runtimeDigest);
|
||||
const retainedOutputConflict = noRuntimeChange
|
||||
&& (stringOrNull(artifact.gitopsCommit) !== null
|
||||
|| artifactDigest !== null
|
||||
|| selectedArtifactDigests.length > 0);
|
||||
|
||||
let code = "pac-diagnostics-not-applicable";
|
||||
let phase = "not-applicable";
|
||||
let ok = true;
|
||||
let hint = "consumer did not expose artifact or runtime alignment evidence";
|
||||
if (deliveryDisabled) {
|
||||
code = "pac-delivery-disabled";
|
||||
phase = "disabled";
|
||||
hint = "delivery is disabled by YAML; registry, GitOps and runtime artifacts are intentionally absent";
|
||||
} else if (!sourceCommit) {
|
||||
ok = false;
|
||||
code = "pac-source-unknown";
|
||||
phase = "source-unknown";
|
||||
hint = "latest PaC PipelineRun did not expose a source commit";
|
||||
} else if (sourceObservationInconsistent) {
|
||||
ok = false;
|
||||
code = "pac-source-observation-inconsistent";
|
||||
phase = "source-observation-inconsistent";
|
||||
hint = "structured delivery plan and no-runtime-change terminal markers are incomplete or contradictory";
|
||||
} else if (sourceObservationMode !== null && !observationSourceMatches) {
|
||||
ok = false;
|
||||
code = "pac-source-observation-mismatch";
|
||||
phase = "source-observation-mismatch";
|
||||
hint = "the structured no-runtime-change observation does not belong to the selected PipelineRun source commit";
|
||||
} else if (noRuntimeChange && retainedOutputConflict) {
|
||||
ok = false;
|
||||
code = "pac-retained-output-conflict";
|
||||
phase = "source-only-output-conflict";
|
||||
hint = "a no-runtime-change PipelineRun unexpectedly reported a new artifact or GitOps output";
|
||||
} else if (noRuntimeChange && retainedRevision === null) {
|
||||
ok = false;
|
||||
code = "pac-retained-gitops-missing";
|
||||
phase = "retained-gitops-missing";
|
||||
hint = "the no-runtime-change observation is valid but the retained Argo GitOps revision is missing";
|
||||
} else if (noRuntimeChange && (argo.sync !== "Synced" || argo.health !== "Healthy")) {
|
||||
ok = false;
|
||||
code = "pac-retained-argo-not-ready";
|
||||
phase = "retained-argo-not-ready";
|
||||
hint = "the no-runtime-change observation is valid but retained Argo is not Synced/Healthy";
|
||||
} else if (noRuntimeChange && gitopsRelation !== "retained") {
|
||||
ok = false;
|
||||
code = "pac-retained-revision-unproven";
|
||||
phase = "retained-revision-unproven";
|
||||
hint = "the retained GitOps revision was not linked to the structured no-runtime-change observation";
|
||||
} else if (noRuntimeChange && !runtimeReady) {
|
||||
ok = false;
|
||||
code = "pac-retained-runtime-not-ready";
|
||||
phase = "retained-runtime-not-ready";
|
||||
hint = "the retained runtime ready replicas do not match the desired replica count";
|
||||
} else if (noRuntimeChange && !retainedDigestReady) {
|
||||
ok = false;
|
||||
code = "pac-retained-runtime-digest-missing";
|
||||
phase = "retained-runtime-digest-missing";
|
||||
hint = "the retained runtime is not pinned to a visible sha256 digest";
|
||||
} else if (noRuntimeChange && !healthReady) {
|
||||
ok = false;
|
||||
code = "pac-retained-health-not-ready";
|
||||
phase = "retained-health-not-ready";
|
||||
hint = "the retained runtime health endpoint is not ready";
|
||||
} else if (noRuntimeChange) {
|
||||
code = "pac-ready-no-runtime-change";
|
||||
phase = "ready-no-runtime-change";
|
||||
hint = "the successful source observation requires no artifact, GitOps or runtime change; retained Argo and runtime provenance remain healthy";
|
||||
} else if (stringOrNull(input.imageRepository) !== null && input.registryPresent !== true) {
|
||||
ok = false;
|
||||
code = "pac-registry-missing";
|
||||
phase = "source-ready-registry-missing";
|
||||
hint = "PaC source commit is known but the configured registry tag is missing";
|
||||
} else if (!registryMatchesArtifact) {
|
||||
ok = false;
|
||||
code = "pac-artifact-registry-mismatch";
|
||||
phase = "artifact-registry-mismatch";
|
||||
hint = "PipelineRun artifact digest does not match the configured registry digest";
|
||||
} else if (!gitopsReady) {
|
||||
ok = false;
|
||||
code = "pac-gitops-missing";
|
||||
phase = "artifact-ready-gitops-missing";
|
||||
hint = "PipelineRun completed but its GitOps commit is not visible";
|
||||
} else if (argo.sync !== "Synced" || argo.health !== "Healthy") {
|
||||
ok = false;
|
||||
code = "pac-argo-not-ready";
|
||||
phase = "gitops-ready-argo-pending";
|
||||
hint = "GitOps exists but Argo is not Synced/Healthy";
|
||||
} else if (!gitopsRevisionAligned) {
|
||||
ok = false;
|
||||
code = `pac-argo-revision-${gitopsRelation}`;
|
||||
phase = `gitops-ready-argo-revision-${gitopsRelation}`;
|
||||
hint = `Argo revision relation is ${gitopsRelation}; only exact or commit-graph-proven descendant is deployable`;
|
||||
} else if (gitopsRelation === "descendant" && !selectedArtifactMatchesRuntime) {
|
||||
ok = false;
|
||||
code = "pac-descendant-artifact-runtime-mismatch";
|
||||
phase = "argo-descendant-runtime-mismatch";
|
||||
hint = "a GitOps descendant is ready only when the selected PipelineRun artifact digest exactly matches the runtime digest";
|
||||
} else if (!runtimeMatches) {
|
||||
ok = false;
|
||||
code = "pac-runtime-not-aligned";
|
||||
phase = "argo-ready-runtime-mismatch";
|
||||
hint = "runtime image digest does not match the artifact digest produced by the selected PipelineRun";
|
||||
} else if (!runtimeReady) {
|
||||
ok = false;
|
||||
code = "pac-runtime-not-ready";
|
||||
phase = "runtime-replicas-not-ready";
|
||||
hint = "runtime ready replicas do not match the desired replica count";
|
||||
} else if (!healthReady) {
|
||||
ok = false;
|
||||
code = "pac-health-not-ready";
|
||||
phase = "runtime-ready-health-pending";
|
||||
hint = "runtime image is aligned but the configured health endpoint is not ready";
|
||||
} else if (artifactDigest || selectedArtifactDigests.length > 0 || stringOrNull(input.imageRepository) !== null) {
|
||||
code = deliverySkipped
|
||||
? "pac-ready-runtime-unchanged"
|
||||
: gitopsRelation === "descendant"
|
||||
? "pac-ready-gitops-descendant"
|
||||
: "pac-ready-gitops-exact";
|
||||
phase = "ready";
|
||||
hint = deliverySkipped
|
||||
? "YAML-declared runtime inputs are unchanged; the existing image, GitOps manifest, runtime and health remain aligned"
|
||||
: gitopsRelation === "descendant"
|
||||
? "Argo is on a commit-graph-proven GitOps descendant and the selected artifact, runtime, Argo and health are aligned"
|
||||
: "Argo is on the exact selected GitOps commit and the artifact, runtime, Argo and health are aligned";
|
||||
}
|
||||
|
||||
return {
|
||||
ok,
|
||||
code,
|
||||
phase,
|
||||
hint,
|
||||
sourceCommit,
|
||||
runtimeSourceCommit: stringOrNull(input.runtimeSourceCommit),
|
||||
imageRepository: stringOrNull(input.imageRepository),
|
||||
registryProbeBase: stringOrNull(input.registryProbeBase),
|
||||
imageTag: stringOrNull(input.imageTag),
|
||||
registry: {
|
||||
present: input.registryPresent === true,
|
||||
digest: registryDigest,
|
||||
url: stringOrNull(input.registryUrl),
|
||||
},
|
||||
selectedArtifactDigests,
|
||||
gitops: {
|
||||
branch: stringOrNull(input.gitopsBranch),
|
||||
manifestPath: stringOrNull(input.gitopsManifestPath),
|
||||
commit: noRuntimeChange ? retainedRevision : stringOrNull(artifact.gitopsCommit) || retainedRevision,
|
||||
revisionRelation,
|
||||
},
|
||||
argo: {
|
||||
sync: stringOrNull(argo.sync),
|
||||
health: stringOrNull(argo.health),
|
||||
revision: retainedRevision,
|
||||
repoURL: stringOrNull(argo.repoURL),
|
||||
targetRevision: stringOrNull(argo.targetRevision),
|
||||
revisionRelation,
|
||||
},
|
||||
runtime: {
|
||||
image: runtimeImage,
|
||||
digest: runtimeDigest,
|
||||
readyReplicas: runtime.readyReplicas ?? null,
|
||||
replicas: runtime.replicas ?? null,
|
||||
},
|
||||
health: { url: healthUrl, status: healthStatus, ready: healthReady },
|
||||
pipelineRun: stringOrNull(input.pipelineRun),
|
||||
deliveryDisabled,
|
||||
deliverySkipped,
|
||||
deliveryMode: noRuntimeChange ? "no-runtime-change" : sourceObservationMode || "delivery",
|
||||
sourceObservation: Object.keys(sourceObservation).length > 0 ? sourceObservation : null,
|
||||
provenance: noRuntimeChange ? {
|
||||
relation: "retained",
|
||||
sourceCommit,
|
||||
gitopsRevision: retainedRevision,
|
||||
runtimeImage,
|
||||
runtimeDigest,
|
||||
newArtifact: false,
|
||||
newGitopsRevision: false,
|
||||
runtimeChanged: false,
|
||||
valuesPrinted: false,
|
||||
} : {
|
||||
relation: gitopsRelation,
|
||||
sourceCommit,
|
||||
gitopsRevision: retainedRevision,
|
||||
runtimeImage,
|
||||
runtimeDigest,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function fixtureInput(overrides = {}) {
|
||||
const digest = `sha256:${"a".repeat(64)}`;
|
||||
const revision = "b".repeat(40);
|
||||
return {
|
||||
sourceCommit: "c".repeat(40),
|
||||
artifact: {},
|
||||
argo: {
|
||||
sync: "Synced",
|
||||
health: "Healthy",
|
||||
revision,
|
||||
revisionRelation: { relation: "retained", observedRevision: revision },
|
||||
},
|
||||
runtime: { image: `registry/service@${digest}`, digest, replicas: 1, readyReplicas: 1 },
|
||||
registryPresent: false,
|
||||
healthUrl: null,
|
||||
healthStatus: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function noRuntimeChangeObservation(sourceCommit = "c".repeat(40)) {
|
||||
return extractPacSourceObservation([
|
||||
{
|
||||
event: "g14-ci-plan",
|
||||
sourceCommitId: sourceCommit,
|
||||
affectedServices: [],
|
||||
rolloutServices: [],
|
||||
buildServices: [],
|
||||
reusedServices: ["service"],
|
||||
},
|
||||
{ event: "collect-artifacts", status: "skipped", reason: "no-build-no-rollout-plan" },
|
||||
{ event: "gitops-promote", status: "skipped", reason: "no-build-no-rollout-plan" },
|
||||
]);
|
||||
}
|
||||
|
||||
function deliveryObservation(sourceCommit = "c".repeat(40)) {
|
||||
return extractPacSourceObservation([{
|
||||
event: "g14-ci-plan",
|
||||
sourceCommitId: sourceCommit,
|
||||
affectedServices: ["service"],
|
||||
rolloutServices: ["service"],
|
||||
buildServices: ["service"],
|
||||
reusedServices: [],
|
||||
}]);
|
||||
}
|
||||
|
||||
function runPacStatusFixtureChecks() {
|
||||
const digestA = `sha256:${"a".repeat(64)}`;
|
||||
const digestD = `sha256:${"d".repeat(64)}`;
|
||||
const revision = "b".repeat(40);
|
||||
const retained = noRuntimeChangeObservation();
|
||||
const delivery = deliveryObservation();
|
||||
const exactArgo = { sync: "Synced", health: "Healthy", revision, revisionRelation: { relation: "exact", expectedRevision: revision, observedRevision: revision } };
|
||||
const cases = [
|
||||
{
|
||||
id: "retained-ready",
|
||||
expectedOk: true,
|
||||
expectedCode: "pac-ready-no-runtime-change",
|
||||
input: fixtureInput({ artifact: { imageStatus: "retained", sourceObservation: retained } }),
|
||||
},
|
||||
{
|
||||
id: "retained-gitops-missing",
|
||||
expectedOk: false,
|
||||
expectedCode: "pac-retained-gitops-missing",
|
||||
input: fixtureInput({ artifact: { imageStatus: "retained", sourceObservation: retained }, argo: { sync: "Synced", health: "Healthy", revision: null, revisionRelation: { relation: "unknown" } } }),
|
||||
},
|
||||
{
|
||||
id: "delivery-ready-exact",
|
||||
expectedOk: true,
|
||||
expectedCode: "pac-ready-gitops-exact",
|
||||
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
|
||||
},
|
||||
{
|
||||
id: "delivery-gitops-missing",
|
||||
expectedOk: false,
|
||||
expectedCode: "pac-gitops-missing",
|
||||
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, sourceObservation: delivery }, argo: { sync: "Synced", health: "Healthy", revision: null, revisionRelation: { relation: "unknown" } } }),
|
||||
},
|
||||
{
|
||||
id: "delivery-runtime-mismatch",
|
||||
expectedOk: false,
|
||||
expectedCode: "pac-runtime-not-aligned",
|
||||
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, argo: exactArgo, runtime: { image: `registry/service@${digestD}`, digest: digestD, replicas: 1, readyReplicas: 1 } }),
|
||||
},
|
||||
{
|
||||
id: "incomplete-no-runtime-change-markers",
|
||||
expectedOk: false,
|
||||
expectedCode: "pac-source-observation-inconsistent",
|
||||
input: fixtureInput({ artifact: { sourceObservation: extractPacSourceObservation([{ event: "g14-ci-plan", sourceCommitId: "c".repeat(40), affectedServices: [], rolloutServices: [], buildServices: [], reusedServices: [] }]) } }),
|
||||
},
|
||||
{
|
||||
id: "retained-source-mismatch",
|
||||
expectedOk: false,
|
||||
expectedCode: "pac-source-observation-mismatch",
|
||||
input: fixtureInput({ sourceCommit: "e".repeat(40), artifact: { imageStatus: "retained", sourceObservation: retained } }),
|
||||
},
|
||||
{
|
||||
id: "delivery-one-sided-skip-conflict",
|
||||
expectedOk: false,
|
||||
expectedCode: "pac-source-observation-inconsistent",
|
||||
input: fixtureInput({ artifact: { sourceObservation: extractPacSourceObservation([
|
||||
{ event: "g14-ci-plan", sourceCommitId: "c".repeat(40), affectedServices: ["service"], rolloutServices: ["service"], buildServices: ["service"], reusedServices: [] },
|
||||
{ event: "gitops-promote", status: "skipped", reason: "no-build-no-rollout-plan" },
|
||||
]) } }),
|
||||
},
|
||||
];
|
||||
const checks = cases.map((item) => {
|
||||
const result = evaluatePacStatus(item.input);
|
||||
return {
|
||||
id: item.id,
|
||||
ok: result.ok === item.expectedOk && result.code === item.expectedCode,
|
||||
expectedOk: item.expectedOk,
|
||||
actualOk: result.ok,
|
||||
expectedCode: item.expectedCode,
|
||||
actualCode: result.code,
|
||||
};
|
||||
});
|
||||
return { ok: checks.every((item) => item.ok), checks, valuesPrinted: false };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
evaluatePacStatus,
|
||||
extractPacSourceObservation,
|
||||
runPacStatusFixtureChecks,
|
||||
};
|
||||
@@ -1,6 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
UNIDESK_PAC_EVALUATOR_PATH=$(mktemp)
|
||||
printf '%s' "$UNIDESK_PAC_EVALUATOR_B64" | base64 -d >"$UNIDESK_PAC_EVALUATOR_PATH"
|
||||
export UNIDESK_PAC_EVALUATOR_PATH
|
||||
trap 'rm -f "$UNIDESK_PAC_EVALUATOR_PATH"' 0 1 2 15
|
||||
|
||||
json_escape() {
|
||||
sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' '
|
||||
}
|
||||
@@ -339,6 +344,7 @@ history_rows() {
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const cp = require('node:child_process');
|
||||
const { extractPacSourceObservation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
|
||||
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
|
||||
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
|
||||
@@ -492,7 +498,7 @@ function recursiveEnvReuse(value, state) {
|
||||
}
|
||||
|
||||
function envReuseForPipelineRun(namespace, name) {
|
||||
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'logs' };
|
||||
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'logs', sourceObservation: null };
|
||||
let logs = '';
|
||||
try {
|
||||
logs = cp.execFileSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers', '--tail=320'], {
|
||||
@@ -513,13 +519,17 @@ function envReuseForPipelineRun(namespace, name) {
|
||||
state.buildCache = row[4];
|
||||
}
|
||||
}
|
||||
const records = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
||||
try {
|
||||
recursiveEnvReuse(JSON.parse(trimmed), state);
|
||||
const parsed = JSON.parse(trimmed);
|
||||
records.push(parsed);
|
||||
recursiveEnvReuse(parsed, state);
|
||||
} catch {}
|
||||
}
|
||||
state.sourceObservation = extractPacSourceObservation(records);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -572,6 +582,12 @@ function rowFor(consumer, item, taskRuns) {
|
||||
const tasks = taskSummary(taskRuns, item.metadata.name, consumer.namespace);
|
||||
const labels = item.metadata?.labels || {};
|
||||
const annotations = item.metadata?.annotations || {};
|
||||
const sourceCommit = extractCommit(item, params);
|
||||
const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name);
|
||||
const sourceObservation = evidence.sourceObservation === null ? null : {
|
||||
...evidence.sourceObservation,
|
||||
sourceMatched: sourceCommit !== null && evidence.sourceObservation.sourceCommit === sourceCommit,
|
||||
};
|
||||
return {
|
||||
id: item.metadata.name,
|
||||
consumer: consumer.id,
|
||||
@@ -590,11 +606,18 @@ function rowFor(consumer, item, taskRuns) {
|
||||
durationSeconds: durationSeconds(item.status?.startTime || item.metadata?.creationTimestamp, item.status?.completionTime),
|
||||
status: c.status || null,
|
||||
reason: c.reason || null,
|
||||
commit: extractCommit(item, params),
|
||||
commit: sourceCommit,
|
||||
branch: extractBranch(item, params),
|
||||
eventType: firstString(labels['pipelinesascode.tekton.dev/event-type'], annotations['pipelinesascode.tekton.dev/event-type']),
|
||||
sender: firstString(labels['pipelinesascode.tekton.dev/sender'], annotations['pipelinesascode.tekton.dev/sender']),
|
||||
envReuse: envReuseForPipelineRun(consumer.namespace, item.metadata.name),
|
||||
envReuse: {
|
||||
status: evidence.status,
|
||||
buildSkippedCount: evidence.buildSkippedCount,
|
||||
serviceReusedCount: evidence.serviceReusedCount,
|
||||
buildCache: evidence.buildCache,
|
||||
source: evidence.source,
|
||||
},
|
||||
sourceObservation,
|
||||
taskRuns: tasks,
|
||||
source: 'gitea-pac-tekton-live',
|
||||
valuesPrinted: false,
|
||||
@@ -684,6 +707,7 @@ artifact_summary() {
|
||||
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" logs -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" --all-containers --tail=240 >"$log_file" 2>/dev/null || true
|
||||
node - "$log_file" <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const { extractPacSourceObservation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const lines = fs.readFileSync(process.argv[2], 'utf8').split(/\r?\n/);
|
||||
const records = [];
|
||||
for (const line of lines) {
|
||||
@@ -691,6 +715,7 @@ for (const line of lines) {
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
||||
try { records.push(JSON.parse(trimmed)); } catch {}
|
||||
}
|
||||
const sourceObservation = extractPacSourceObservation(records);
|
||||
const publish = [...records].reverse().find((item) => item.phase === 'gitops-publish' || item.gitopsCommit);
|
||||
const image = publish || [...records].reverse().find((item) => item.imageStatus || item.status === 'reused' || item.status === 'built');
|
||||
const gitopsCommit = image?.gitopsCommit
|
||||
@@ -731,6 +756,24 @@ if (image) {
|
||||
baselineSourceCommit: image.baselineSourceCommit || null,
|
||||
action: image.action || null,
|
||||
reason: image.reason || null,
|
||||
sourceObservation,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
} else if (sourceObservation) {
|
||||
out = {
|
||||
imageStatus: sourceObservation.mode === 'no-runtime-change' && sourceObservation.valid === true ? 'retained' : null,
|
||||
envIdentity: null,
|
||||
envReuse: null,
|
||||
nodeDepsReuse: null,
|
||||
buildCache: null,
|
||||
digest: null,
|
||||
digests: [],
|
||||
gitopsCommit: null,
|
||||
sourceCommit: sourceObservation.sourceCommit || null,
|
||||
runtimeSourceCommit: null,
|
||||
action: sourceObservation.mode,
|
||||
reason: sourceObservation.reason,
|
||||
sourceObservation,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
} else if (humanEnv || gitopsCommit || loggedDigests.length > 0) {
|
||||
@@ -854,17 +897,23 @@ line('expected', artifact.gitopsCommit);
|
||||
line('observed', argo.revision);
|
||||
line('repoUrl', argo.repoURL);
|
||||
line('targetRevision', argo.targetRevision);
|
||||
line('retained', artifact.sourceObservation?.mode === 'no-runtime-change' && artifact.sourceObservation?.valid === true ? 'true' : 'false');
|
||||
NODE
|
||||
)
|
||||
expected=$(printf '%s\n' "$fields" | sed -n 's/^expected=//p' | base64 -d 2>/dev/null || true)
|
||||
observed=$(printf '%s\n' "$fields" | sed -n 's/^observed=//p' | base64 -d 2>/dev/null || true)
|
||||
repo_url=$(printf '%s\n' "$fields" | sed -n 's/^repoUrl=//p' | base64 -d 2>/dev/null || true)
|
||||
target_revision=$(printf '%s\n' "$fields" | sed -n 's/^targetRevision=//p' | base64 -d 2>/dev/null || true)
|
||||
retained=$(printf '%s\n' "$fields" | sed -n 's/^retained=//p' | base64 -d 2>/dev/null || true)
|
||||
relation=unknown
|
||||
proof=unavailable
|
||||
reason=missing-revision-context
|
||||
fetch_ok=false
|
||||
if printf '%s\n%s\n' "$expected" "$observed" | grep -Eqv '^[0-9a-fA-F]{7,64}$'; then
|
||||
if [ "$retained" = true ] && printf '%s' "$observed" | grep -Eq '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$'; then
|
||||
relation=retained
|
||||
proof=structured-no-runtime-change-plan
|
||||
reason=source-observation-retains-current-revision
|
||||
elif printf '%s\n%s\n' "$expected" "$observed" | grep -Eqv '^[0-9a-fA-F]{7,64}$'; then
|
||||
reason=invalid-or-missing-revision
|
||||
elif [ -z "$repo_url" ] \
|
||||
|| ! git check-ref-format "refs/heads/$target_revision" >/dev/null 2>&1; then
|
||||
@@ -949,8 +998,9 @@ const pipelines = parseLoose(process.argv[3], []);
|
||||
const artifact = parseLoose(process.argv[4], {});
|
||||
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
|
||||
const sourceCommit = latest.sourceCommit || null;
|
||||
const runtimeSourceCommit = artifact.runtimeSourceCommit || sourceCommit;
|
||||
const tag = runtimeSourceCommit ? String(runtimeSourceCommit).slice(0, 12) : '';
|
||||
const noRuntimeChange = artifact.sourceObservation?.mode === 'no-runtime-change' && artifact.sourceObservation?.valid === true;
|
||||
const runtimeSourceCommit = artifact.runtimeSourceCommit || (noRuntimeChange ? null : sourceCommit);
|
||||
const tag = !noRuntimeChange && runtimeSourceCommit ? String(runtimeSourceCommit).slice(0, 12) : '';
|
||||
const prefix = process.env.UNIDESK_PAC_CONSUMER_ID === 'unidesk-host' ? 'unidesk_host_' : '';
|
||||
const param = (name) => params[`${prefix}${name}`] ?? params[name];
|
||||
const imageRepository = typeof param('image_repository') === 'string' ? param('image_repository') : '';
|
||||
@@ -1045,6 +1095,7 @@ NODE
|
||||
export UNIDESK_PAC_DIAG_HEALTH_STATUS="$health_status"
|
||||
node - "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file" <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const { evaluatePacStatus } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
function parseLoose(path, fallback) {
|
||||
const input = fs.readFileSync(path, 'utf8').trim();
|
||||
if (!input) return fallback;
|
||||
@@ -1053,105 +1104,30 @@ function parseLoose(path, fallback) {
|
||||
}
|
||||
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 artifactDigest = artifact.digest || null;
|
||||
const expectedDigest = registryDigest || artifactDigest;
|
||||
const runtimeImage = runtime.image || null;
|
||||
const runtimeDigest = runtime.digest || (typeof runtimeImage === 'string' && runtimeImage.includes('@') ? runtimeImage.split('@').slice(1).join('@') : null);
|
||||
const runtimeMatches = !expectedDigest || runtimeDigest === expectedDigest;
|
||||
const runtimeReady = Number.isInteger(runtime.replicas) && runtime.replicas > 0 && runtime.readyReplicas === runtime.replicas;
|
||||
const selectedArtifactDigests = [...new Set([artifactDigest, ...(Array.isArray(artifact.digests) ? artifact.digests : [])].filter(Boolean))];
|
||||
const selectedArtifactMatchesRuntime = Boolean(runtimeDigest && selectedArtifactDigests.includes(runtimeDigest));
|
||||
const registryMatchesArtifact = !registryDigest || !artifactDigest || registryDigest === artifactDigest;
|
||||
const healthUrl = process.env.UNIDESK_PAC_DIAG_HEALTH_URL || null;
|
||||
const healthStatus = process.env.UNIDESK_PAC_DIAG_HEALTH_STATUS || null;
|
||||
const healthReady = !healthUrl || healthStatus === '200';
|
||||
const revisionRelation = argo.revisionRelation || { relation: 'unknown' };
|
||||
const gitopsRelation = revisionRelation.relation || 'unknown';
|
||||
const gitopsReady = Boolean(artifact.gitopsCommit && argo.revision);
|
||||
const gitopsRevisionAligned = gitopsRelation === 'exact' || gitopsRelation === 'descendant';
|
||||
const deliveryDisabled = artifact.imageStatus === 'disabled';
|
||||
const deliverySkipped = artifact.imageStatus === 'skipped';
|
||||
let code = 'pac-diagnostics-not-applicable';
|
||||
let phase = 'not-applicable';
|
||||
let ok = true;
|
||||
let hint = 'consumer did not expose artifact or runtime alignment evidence';
|
||||
if (deliveryDisabled) {
|
||||
code = 'pac-delivery-disabled'; phase = 'disabled'; hint = 'delivery is disabled by YAML; registry, GitOps and runtime artifacts are intentionally absent';
|
||||
} else if (!sourceCommit) {
|
||||
ok = false; code = 'pac-source-unknown'; phase = 'source-unknown'; hint = 'latest PaC PipelineRun did not expose a source commit';
|
||||
} else if (process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY && !registryPresent) {
|
||||
ok = false; code = 'pac-registry-missing'; phase = 'source-ready-registry-missing'; hint = 'PaC source commit is known but the configured registry tag is missing';
|
||||
} else if (!registryMatchesArtifact) {
|
||||
ok = false; code = 'pac-artifact-registry-mismatch'; phase = 'artifact-registry-mismatch'; hint = 'PipelineRun artifact digest does not match the configured registry digest';
|
||||
} else if (!gitopsReady) {
|
||||
ok = false; code = 'pac-gitops-missing'; phase = 'artifact-ready-gitops-missing'; hint = 'PipelineRun completed but its GitOps commit is not visible';
|
||||
} else if (argo.sync !== 'Synced' || argo.health !== 'Healthy') {
|
||||
ok = false; code = 'pac-argo-not-ready'; phase = 'gitops-ready-argo-pending'; hint = 'GitOps exists but Argo is not Synced/Healthy';
|
||||
} else if (!gitopsRevisionAligned) {
|
||||
ok = false; code = `pac-argo-revision-${gitopsRelation}`; phase = `gitops-ready-argo-revision-${gitopsRelation}`; hint = `Argo revision relation is ${gitopsRelation}; only exact or commit-graph-proven descendant is deployable`;
|
||||
} else if (gitopsRelation === 'descendant' && !selectedArtifactMatchesRuntime) {
|
||||
ok = false; code = 'pac-descendant-artifact-runtime-mismatch'; phase = 'argo-descendant-runtime-mismatch'; hint = 'a GitOps descendant is ready only when the selected PipelineRun artifact digest exactly matches the runtime digest';
|
||||
} else if (!runtimeMatches) {
|
||||
ok = false; code = 'pac-runtime-not-aligned'; phase = 'argo-ready-runtime-mismatch'; hint = 'runtime image digest does not match the artifact digest produced by the selected PipelineRun';
|
||||
} else if (!runtimeReady) {
|
||||
ok = false; code = 'pac-runtime-not-ready'; phase = 'runtime-replicas-not-ready'; hint = 'runtime ready replicas do not match the desired replica count';
|
||||
} else if (!healthReady) {
|
||||
ok = false; code = 'pac-health-not-ready'; phase = 'runtime-ready-health-pending'; hint = 'runtime image is aligned but the configured health endpoint is not ready';
|
||||
} else if (artifactDigest || selectedArtifactDigests.length > 0 || process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY) {
|
||||
code = deliverySkipped ? 'pac-ready-runtime-unchanged' : gitopsRelation === 'descendant' ? 'pac-ready-gitops-descendant' : 'pac-ready-gitops-exact';
|
||||
phase = 'ready';
|
||||
hint = deliverySkipped
|
||||
? 'YAML-declared runtime inputs are unchanged; the existing image, GitOps manifest, runtime and health remain aligned'
|
||||
: gitopsRelation === 'descendant'
|
||||
? 'Argo is on a commit-graph-proven GitOps descendant and the selected artifact, runtime, Argo and health are aligned'
|
||||
: 'Argo is on the exact selected GitOps commit and the artifact, runtime and health are aligned';
|
||||
}
|
||||
process.stdout.write(JSON.stringify({
|
||||
ok,
|
||||
code,
|
||||
phase,
|
||||
hint,
|
||||
sourceCommit,
|
||||
const evaluated = evaluatePacStatus({
|
||||
sourceCommit: process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null,
|
||||
runtimeSourceCommit: process.env.UNIDESK_PAC_DIAG_RUNTIME_SOURCE_COMMIT || artifact.runtimeSourceCommit || null,
|
||||
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,
|
||||
},
|
||||
selectedArtifactDigests,
|
||||
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,
|
||||
revisionRelation,
|
||||
},
|
||||
argo: {
|
||||
sync: argo.sync || null,
|
||||
health: argo.health || null,
|
||||
revision: argo.revision || null,
|
||||
repoURL: argo.repoURL || null,
|
||||
targetRevision: argo.targetRevision || null,
|
||||
revisionRelation,
|
||||
},
|
||||
runtime: { image: runtimeImage, digest: runtimeDigest, readyReplicas: runtime.readyReplicas ?? null, replicas: runtime.replicas ?? null },
|
||||
health: { url: healthUrl, status: healthStatus, ready: healthReady },
|
||||
registryUrl: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null,
|
||||
registryPresent: process.env.UNIDESK_PAC_DIAG_REGISTRY_PRESENT === 'true',
|
||||
registryDigest: process.env.UNIDESK_PAC_DIAG_REGISTRY_DIGEST || null,
|
||||
gitopsBranch: process.env.UNIDESK_PAC_DIAG_GITOPS_BRANCH || null,
|
||||
gitopsManifestPath: process.env.UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH || null,
|
||||
healthUrl: process.env.UNIDESK_PAC_DIAG_HEALTH_URL || null,
|
||||
healthStatus: process.env.UNIDESK_PAC_DIAG_HEALTH_STATUS || null,
|
||||
pipelineRun: latest.name || null,
|
||||
deliveryDisabled,
|
||||
deliverySkipped,
|
||||
valuesPrinted: false,
|
||||
}));
|
||||
artifact,
|
||||
argo,
|
||||
runtime,
|
||||
});
|
||||
process.stdout.write(JSON.stringify(evaluated));
|
||||
NODE
|
||||
rm -f "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file"
|
||||
}
|
||||
@@ -1229,6 +1205,15 @@ history_action() {
|
||||
"$consumers" "$rows" "$errors" "$hooks"
|
||||
}
|
||||
|
||||
debug_step_action() {
|
||||
node <<'NODE'
|
||||
const { runPacStatusFixtureChecks } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const result = runPacStatusFixtureChecks();
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
NODE
|
||||
}
|
||||
|
||||
webhook_test_action() {
|
||||
hooks=$(hook_summary)
|
||||
hook_id=$(printf '%s' "$hooks" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(String(a[0]?.id||""))')
|
||||
@@ -1241,6 +1226,7 @@ case "$UNIDESK_PAC_ACTION" in
|
||||
apply) apply_action ;;
|
||||
status) status_action ;;
|
||||
history) history_action ;;
|
||||
debug-step) debug_step_action ;;
|
||||
webhook-test) webhook_test_action ;;
|
||||
*) printf '{"ok":false,"error":"unsupported-action","valuesPrinted":false}\n'; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -21,6 +21,7 @@ import { materializeYamlComposition } from "./yaml-composition";
|
||||
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
|
||||
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
|
||||
const remoteScriptFile = rootPath("scripts", "src", "platform-infra-pipelines-as-code-remote.sh");
|
||||
const evaluatorFile = rootPath("scripts", "native", "cicd", "pac-status-evaluator.cjs");
|
||||
const fieldManager = "unidesk-platform-infra-pipelines-as-code";
|
||||
const y = createYamlFieldReader(configLabel);
|
||||
|
||||
@@ -138,6 +139,7 @@ interface CommonOptions {
|
||||
consumerId: string | null;
|
||||
full: boolean;
|
||||
raw: boolean;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
export interface PipelinesAsCodeNodeStatusOptions {
|
||||
@@ -182,48 +184,54 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
||||
if (action === "plan") {
|
||||
const options = parseCommonOptions(args.slice(1));
|
||||
const result = plan(options);
|
||||
return options.full || options.raw ? result : renderPlan(result);
|
||||
return options.json || options.full || options.raw ? result : renderPlan(result);
|
||||
}
|
||||
if (action === "apply") {
|
||||
const options = parseApplyOptions(args.slice(1));
|
||||
const result = await apply(config, options);
|
||||
return options.full || options.raw ? result : renderApply(result);
|
||||
return options.json || options.full || options.raw ? result : renderApply(result);
|
||||
}
|
||||
if (action === "status") {
|
||||
const options = parseCommonOptions(args.slice(1));
|
||||
const result = await status(config, options);
|
||||
return options.full || options.raw ? result : renderStatus(result);
|
||||
return options.full || options.raw ? result : options.json ? compactStatusJson(result) : renderStatus(result);
|
||||
}
|
||||
if (action === "closeout") {
|
||||
const options = parseCloseoutOptions(args.slice(1));
|
||||
const result = await closeout(config, options);
|
||||
return options.full || options.raw ? result : renderCloseout(result);
|
||||
return options.full || options.raw ? result : options.json ? compactCloseoutJson(result) : renderCloseout(result);
|
||||
}
|
||||
if (action === "history" || action === "runs") {
|
||||
const options = parseHistoryOptions(args.slice(1));
|
||||
const result = await history(config, options);
|
||||
return options.full || options.raw ? result : renderHistory(result);
|
||||
return options.full || options.raw ? result : options.json ? compactHistoryJson(result) : renderHistory(result);
|
||||
}
|
||||
if (action === "debug-step") {
|
||||
const options = parseCommonOptions(args.slice(1));
|
||||
const result = await debugStep(config, options);
|
||||
return options.full || options.raw ? result : options.json ? result : renderDebugStep(result);
|
||||
}
|
||||
if (action === "webhook-test") {
|
||||
const options = parseWebhookTestOptions(args.slice(1));
|
||||
const result = await webhookTest(config, options);
|
||||
return options.full || options.raw ? result : renderWebhookTest(result);
|
||||
return options.json || options.full || options.raw ? result : renderWebhookTest(result);
|
||||
}
|
||||
return { ok: false, error: "unsupported-platform-infra-pipelines-as-code-command", args, help: help() };
|
||||
}
|
||||
|
||||
function help(): Record<string, unknown> {
|
||||
return {
|
||||
command: "platform-infra pipelines-as-code plan|apply|status|closeout|history",
|
||||
command: "platform-infra pipelines-as-code plan|apply|status|closeout|history|debug-step",
|
||||
configTruth: configLabel,
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --dry-run",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --confirm",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --consumer <consumer> --source-commit <sha> --wait",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--json]",
|
||||
],
|
||||
diagnostics: "webhook-test exists only for bounded connectivity diagnosis and must not be used as delivery evidence.",
|
||||
boundary: "Sole CI trigger path for GH-1552/GH-1607: GitHub PR merge -> GitHub webhook bridge -> Gitea mirror/snapshot -> Pipelines-as-Code -> Tekton -> GitOps/Argo/k8s runtime.",
|
||||
@@ -469,7 +477,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
consumer,
|
||||
coverage: consumerCoverage(pac, target.id),
|
||||
summary,
|
||||
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw || options.full ? parsed : summary,
|
||||
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw ? parsed : undefined,
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
@@ -486,7 +494,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
|
||||
const statuses = await Promise.all(consumers.map(async (consumer) => {
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
try {
|
||||
const current = await status(config, { targetId: target.id, consumerId: consumer.id, full: false, raw: false });
|
||||
const current = await status(config, { targetId: target.id, consumerId: consumer.id, full: false, raw: false, json: false });
|
||||
return nodeStatusRow(target.id, consumer, repository, current);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -564,7 +572,7 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
|
||||
let observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
|
||||
let sourceMatched = options.sourceCommit === null || observedSourceCommit === options.sourceCommit;
|
||||
const ciReady = closeoutCiReady(current, options.sourceCommit);
|
||||
let gitOpsMirrorFlush = await runCloseoutGitOpsMirrorFlush(config, pac, consumer, latest, ciReady, observedSourceCommit);
|
||||
let gitOpsMirrorFlush = await runCloseoutGitOpsMirrorFlush(config, pac, consumer, summary, latest, ciReady, observedSourceCommit);
|
||||
const gitOpsMirrorFlushReady = record(gitOpsMirrorFlush).ok !== false;
|
||||
const runtimeStartedAt = Date.now();
|
||||
if (ciReady && gitOpsMirrorFlushReady) {
|
||||
@@ -621,8 +629,12 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
|
||||
async function history(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const targetConsumers = pac.consumers.filter((consumer) => consumer.node.toLowerCase() === target.id.toLowerCase());
|
||||
const inferredDetailConsumers = options.detailId === null
|
||||
? []
|
||||
: targetConsumers.filter((consumer) => options.detailId?.startsWith(consumer.pipelineRunPrefix));
|
||||
const selectedConsumers = options.consumerId === null
|
||||
? pac.consumers
|
||||
? inferredDetailConsumers.length === 1 ? inferredDetailConsumers : targetConsumers
|
||||
: [resolveConsumer(pac, options.consumerId)];
|
||||
const firstConsumer = selectedConsumers[0];
|
||||
if (firstConsumer === undefined) throw new Error("no Pipelines-as-Code consumers are configured");
|
||||
@@ -648,11 +660,41 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
||||
detailId: options.detailId,
|
||||
rows: arrayRecords(record(remote).rows),
|
||||
historyErrors,
|
||||
remote,
|
||||
controlPlane: parsed === null ? undefined : {
|
||||
crdPresent: parsed.crdPresent === true,
|
||||
controllerReady: parsed.controllerReady,
|
||||
repositoryCondition: parsed.repositoryCondition,
|
||||
repository: record(parsed.repository),
|
||||
consumerRows: arrayRecords(parsed.consumerRows),
|
||||
webhookCount: arrayRecords(parsed.webhooks).length,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
remote: parsed === null || options.raw ? remote : undefined,
|
||||
next: nextCommands(target.id, options.consumerId ?? pac.defaults.consumerId, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
|
||||
async function debugStep(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("debug-step", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, emptySecretMaterial(), ""));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed?.ok === true,
|
||||
action: "platform-infra-pipelines-as-code-debug-step",
|
||||
mutation: false,
|
||||
target: targetSummary(target),
|
||||
consumer: consumer.id,
|
||||
evaluator: "scripts/native/cicd/pac-status-evaluator.cjs",
|
||||
checks: parsed === null ? [] : arrayRecords(parsed.checks),
|
||||
remote: parsed === null || options.raw ? parsed ?? compactCapture(result, { full: true }) : undefined,
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function webhookTest(config: UniDeskConfig, options: WebhookTestOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
@@ -681,10 +723,11 @@ async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
|
||||
return text;
|
||||
}
|
||||
|
||||
function remoteScript(action: "apply" | "status" | "history" | "webhook-test", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | WebhookTestOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
|
||||
function remoteScript(action: "apply" | "status" | "history" | "debug-step" | "webhook-test", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | WebhookTestOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
|
||||
const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`;
|
||||
const env: Record<string, string> = {
|
||||
UNIDESK_PAC_ACTION: action,
|
||||
UNIDESK_PAC_EVALUATOR_B64: Buffer.from(readFileSync(evaluatorFile, "utf8"), "utf8").toString("base64"),
|
||||
UNIDESK_PAC_FIELD_MANAGER: fieldManager,
|
||||
UNIDESK_PAC_TARGET_ID: target.id,
|
||||
UNIDESK_PAC_TARGET_NAMESPACE: consumer.namespace,
|
||||
@@ -791,6 +834,17 @@ function parseLinePair(path: string, usernameLine: number, passwordLine: number)
|
||||
return { username, password };
|
||||
}
|
||||
|
||||
function emptySecretMaterial(): SecretMaterial {
|
||||
return {
|
||||
adminUsername: "",
|
||||
adminPassword: "",
|
||||
adminFingerprint: "",
|
||||
webhookSecret: "",
|
||||
webhookFingerprint: "",
|
||||
webhookPath: "",
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSecrets(pac: PacConfig, createWebhookSecret: boolean, allowMissingWebhookSecret = false): SecretMaterial {
|
||||
const adminPath = credentialPath(pac.gitea.admin.sourceRoot, pac.gitea.admin.sourceRef);
|
||||
if (!existsSync(adminPath)) throw new Error(`${pac.gitea.admin.sourceRef} is missing`);
|
||||
@@ -858,11 +912,18 @@ function statusSummary(payload: Record<string, unknown>): Record<string, unknown
|
||||
const runtime = record(payload.runtime);
|
||||
const diagnostics = record(payload.diagnostics);
|
||||
const webhooks = arrayRecords(payload.webhooks);
|
||||
const rawArtifact = record(payload.artifact);
|
||||
const sourceObservation = record(rawArtifact.sourceObservation);
|
||||
const provenance = record(diagnostics.provenance);
|
||||
const noRuntimeChange = stringValue(sourceObservation.mode) === "no-runtime-change"
|
||||
&& sourceObservation.valid === true
|
||||
&& stringValue(diagnostics.code) === "pac-ready-no-runtime-change";
|
||||
const artifact = {
|
||||
...record(payload.artifact),
|
||||
imageStatus: record(payload.artifact).imageStatus ?? (runtime.image === undefined ? undefined : "runtime"),
|
||||
digest: record(payload.artifact).digest ?? runtime.digest,
|
||||
gitopsCommit: record(payload.artifact).gitopsCommit ?? argo.revision,
|
||||
...rawArtifact,
|
||||
imageStatus: noRuntimeChange ? "retained" : rawArtifact.imageStatus ?? (runtime.image === undefined ? undefined : "runtime"),
|
||||
digest: noRuntimeChange ? provenance.runtimeDigest ?? runtime.digest : rawArtifact.digest ?? runtime.digest,
|
||||
gitopsCommit: noRuntimeChange ? provenance.gitopsRevision ?? argo.revision : rawArtifact.gitopsCommit ?? argo.revision,
|
||||
provenanceRelation: noRuntimeChange ? "retained" : record(argo.revisionRelation).relation,
|
||||
};
|
||||
const pipelineGate = pipelineRunGate(latest);
|
||||
return {
|
||||
@@ -877,6 +938,8 @@ function statusSummary(payload: Record<string, unknown>): Record<string, unknown
|
||||
taskRuns,
|
||||
pipelineRunGate: pipelineGate,
|
||||
artifact,
|
||||
sourceObservation: Object.keys(sourceObservation).length === 0 ? null : sourceObservation,
|
||||
provenance: Object.keys(provenance).length === 0 ? null : provenance,
|
||||
argo,
|
||||
runtime,
|
||||
diagnostics,
|
||||
@@ -1028,7 +1091,7 @@ function repositorySummary(repository: PacRepository): Record<string, unknown> {
|
||||
}
|
||||
|
||||
function consumerCoverage(pac: PacConfig, targetId: string): Record<string, unknown>[] {
|
||||
return pac.consumers.map((consumer) => {
|
||||
return pac.consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase()).map((consumer) => {
|
||||
const repository = resolveRepository(pac, consumer.repositoryRef);
|
||||
const suffix = consumerSuffix(consumer.id, pac.defaults.consumerId);
|
||||
return {
|
||||
@@ -1057,6 +1120,167 @@ function nextCommands(targetId: string, consumerId: string, defaultConsumerId: s
|
||||
};
|
||||
}
|
||||
|
||||
function compactStatusJson(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const consumer = record(result.consumer);
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
action: result.action,
|
||||
mutation: false,
|
||||
target: result.target,
|
||||
consumer: {
|
||||
id: consumer.id,
|
||||
node: consumer.node,
|
||||
lane: consumer.lane,
|
||||
namespace: consumer.namespace,
|
||||
pipeline: consumer.pipeline,
|
||||
argoApplication: consumer.argoApplication,
|
||||
},
|
||||
summary: compactStatusSummary(record(result.summary)),
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactStatusSummary(summary: Record<string, unknown>): Record<string, unknown> {
|
||||
const latest = record(summary.latestPipelineRun);
|
||||
const artifact = record(summary.artifact);
|
||||
const argo = record(summary.argo);
|
||||
const runtime = record(summary.runtime);
|
||||
const diagnostics = record(summary.diagnostics);
|
||||
const taskRuns = arrayRecords(summary.taskRuns);
|
||||
const longest = longestTaskRun(taskRuns);
|
||||
return {
|
||||
ready: summary.ready === true,
|
||||
crdPresent: summary.crdPresent === true,
|
||||
controllerReady: summary.controllerReady,
|
||||
repositoryCondition: summary.repositoryCondition,
|
||||
webhookCount: summary.webhookCount,
|
||||
latestPipelineRun: {
|
||||
name: latest.name,
|
||||
status: latest.status,
|
||||
reason: latest.reason,
|
||||
durationSeconds: latest.durationSeconds,
|
||||
sourceCommit: latest.sourceCommit,
|
||||
},
|
||||
taskRuns: {
|
||||
count: taskRuns.length,
|
||||
longest: longest === null ? null : {
|
||||
name: longest.name,
|
||||
status: longest.status,
|
||||
reason: longest.reason,
|
||||
durationSeconds: longest.durationSeconds,
|
||||
},
|
||||
},
|
||||
pipelineRunGate: summary.pipelineRunGate,
|
||||
sourceObservation: summary.sourceObservation,
|
||||
artifact: {
|
||||
imageStatus: artifact.imageStatus,
|
||||
envReuse: artifact.envReuse,
|
||||
envIdentity: artifact.envIdentity,
|
||||
digest: artifact.digest,
|
||||
gitopsCommit: artifact.gitopsCommit,
|
||||
provenanceRelation: artifact.provenanceRelation,
|
||||
},
|
||||
argo: {
|
||||
sync: argo.sync,
|
||||
health: argo.health,
|
||||
revision: argo.revision,
|
||||
repoURL: argo.repoURL,
|
||||
targetRevision: argo.targetRevision,
|
||||
revisionRelation: argo.revisionRelation,
|
||||
},
|
||||
runtime: {
|
||||
deployment: runtime.deployment,
|
||||
readyReplicas: runtime.readyReplicas,
|
||||
replicas: runtime.replicas,
|
||||
image: runtime.image,
|
||||
digest: runtime.digest,
|
||||
},
|
||||
diagnostics: {
|
||||
ok: diagnostics.ok !== false,
|
||||
code: diagnostics.code,
|
||||
phase: diagnostics.phase,
|
||||
hint: diagnostics.hint,
|
||||
deliveryMode: diagnostics.deliveryMode,
|
||||
registry: diagnostics.registry,
|
||||
},
|
||||
provenance: summary.provenance,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactCloseoutJson(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const consumer = record(result.consumer);
|
||||
const flush = record(result.gitOpsMirrorFlush);
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
action: result.action,
|
||||
mutation: result.mutation === true,
|
||||
target: result.target,
|
||||
consumer: { id: consumer.id, node: consumer.node, lane: consumer.lane },
|
||||
sourceCommit: result.sourceCommit,
|
||||
observedSourceCommit: result.observedSourceCommit,
|
||||
sourceMatched: result.sourceMatched === true,
|
||||
ready: result.ready === true,
|
||||
wait: result.wait,
|
||||
summary: compactStatusSummary(record(result.summary)),
|
||||
gitOpsMirrorFlush: {
|
||||
ok: flush.ok !== false,
|
||||
enabled: flush.enabled,
|
||||
required: flush.required,
|
||||
applicable: flush.applicable,
|
||||
mode: flush.mode,
|
||||
executed: flush.executed === true,
|
||||
reason: flush.reason,
|
||||
},
|
||||
blocker: result.blocker,
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactHistoryJson(result: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
action: result.action,
|
||||
mutation: false,
|
||||
target: result.target,
|
||||
config: result.config,
|
||||
consumers: result.consumers,
|
||||
detailId: result.detailId,
|
||||
rows: arrayRecords(result.rows).map((row) => {
|
||||
const taskRuns = record(row.taskRuns);
|
||||
return {
|
||||
id: row.id ?? row.pipelineRun,
|
||||
consumer: row.consumer,
|
||||
repo: row.repo,
|
||||
pipeline: row.pipeline,
|
||||
triggeredAt: row.triggeredAt,
|
||||
startTime: row.startTime,
|
||||
completionTime: row.completionTime,
|
||||
displayTime: row.displayTime,
|
||||
displayTimeZone: row.displayTimeZone,
|
||||
durationSeconds: row.durationSeconds,
|
||||
status: row.status,
|
||||
reason: row.reason,
|
||||
commit: row.commit,
|
||||
branch: row.branch,
|
||||
envReuse: row.envReuse,
|
||||
sourceObservation: row.sourceObservation,
|
||||
taskRuns: {
|
||||
count: taskRuns.count,
|
||||
longest: taskRuns.longest,
|
||||
failed: taskRuns.failed,
|
||||
logsCommand: taskRuns.logsCommand,
|
||||
},
|
||||
};
|
||||
}),
|
||||
historyErrors: result.historyErrors,
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
||||
const target = record(result.target);
|
||||
const config = record(result.config);
|
||||
@@ -1116,6 +1340,8 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
|
||||
const artifact = record(summary.artifact);
|
||||
const argo = record(summary.argo);
|
||||
const diagnostics = record(summary.diagnostics);
|
||||
const sourceObservation = record(summary.sourceObservation);
|
||||
const deliveryPlan = record(sourceObservation.plan);
|
||||
const repository = record(summary.repository);
|
||||
const webhooks = arrayRecords(summary.webhooks);
|
||||
const lines = [
|
||||
@@ -1134,6 +1360,18 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
|
||||
"LATEST PIPELINERUN",
|
||||
...table(["NAME", "STATUS", "REASON", "DURATION_S", "SOURCE"], [[stringValue(latest.name), stringValue(latest.status), stringValue(latest.reason), stringValue(latest.durationSeconds), short(stringValue(latest.sourceCommit))]]),
|
||||
"",
|
||||
"DELIVERY OBSERVATION",
|
||||
...table(["MODE", "VALID", "SOURCE_MATCHED", "AFFECTED", "ROLLOUT", "BUILD", "REUSED", "REASON"], [[
|
||||
stringValue(sourceObservation.mode),
|
||||
boolText(sourceObservation.valid),
|
||||
boolText(sourceObservation.sourceMatched ?? (stringValue(sourceObservation.sourceCommit) === stringValue(latest.sourceCommit))),
|
||||
stringValue(deliveryPlan.affectedServiceCount),
|
||||
stringValue(deliveryPlan.rolloutServiceCount),
|
||||
stringValue(deliveryPlan.buildServiceCount),
|
||||
stringValue(deliveryPlan.reusedServiceCount),
|
||||
stringValue(sourceObservation.reason),
|
||||
]]),
|
||||
"",
|
||||
"TASKRUN DURATIONS",
|
||||
...(taskRuns.length === 0 ? ["-"] : table(["TASKRUN", "STATUS", "REASON", "FAILED_STEP", "EXIT", "DURATION_S"], taskRuns.map((item) => {
|
||||
const failedStep = record(item.failedStep);
|
||||
@@ -1184,6 +1422,7 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
|
||||
const argo = record(summary.argo);
|
||||
const runtime = record(summary.runtime);
|
||||
const diagnostics = record(summary.diagnostics);
|
||||
const sourceObservation = record(summary.sourceObservation);
|
||||
const blocker = record(result.blocker);
|
||||
const gitOpsMirrorFlush = record(result.gitOpsMirrorFlush);
|
||||
const lines = [
|
||||
@@ -1203,6 +1442,7 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
|
||||
"",
|
||||
"SOURCE / PIPELINERUN",
|
||||
...table(["EXPECTED", "OBSERVED", "PIPELINERUN", "STATUS", "DURATION_S"], [[short(stringValue(result.sourceCommit), 16), short(stringValue(result.observedSourceCommit), 16), stringValue(latest.name), stringValue(latest.reason ?? latest.status), stringValue(latest.durationSeconds)]]),
|
||||
` delivery: ${stringValue(sourceObservation.mode)} (${stringValue(sourceObservation.reason)})`,
|
||||
"",
|
||||
"RUNTIME",
|
||||
...table(["IMAGE_STATUS", "ENV_REUSE", "DIGEST", "GITOPS", "ARGO", "RELATION", "RUNTIME"], [[
|
||||
@@ -1289,6 +1529,32 @@ function renderWebhookTest(result: Record<string, unknown>): RenderedCliResult {
|
||||
return rendered(result, "platform-infra pipelines-as-code webhook-test", lines);
|
||||
}
|
||||
|
||||
function renderDebugStep(result: Record<string, unknown>): RenderedCliResult {
|
||||
const checks = arrayRecords(result.checks);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE DEBUG STEP",
|
||||
...table(["TARGET", "CONSUMER", "OK", "CHECKS", "MUTATION"], [[
|
||||
stringValue(record(result.target).id),
|
||||
stringValue(result.consumer),
|
||||
boolText(result.ok),
|
||||
stringValue(checks.length),
|
||||
boolText(result.mutation),
|
||||
]]),
|
||||
"",
|
||||
"STATUS EVALUATOR FIXTURES",
|
||||
...(checks.length === 0 ? ["-"] : table(["CASE", "OK", "EXPECTED", "ACTUAL"], checks.map((item) => [
|
||||
stringValue(item.id),
|
||||
boolText(item.ok),
|
||||
`${stringValue(item.expectedOk)}/${stringValue(item.expectedCode)}`,
|
||||
`${stringValue(item.actualOk)}/${stringValue(item.actualCode)}`,
|
||||
]))),
|
||||
"",
|
||||
"NEXT",
|
||||
` status: ${stringValue(record(result.next).status)}`,
|
||||
];
|
||||
return rendered(result, "platform-infra pipelines-as-code debug-step", lines);
|
||||
}
|
||||
|
||||
function parseApplyOptions(args: string[]): ApplyOptions {
|
||||
const commonArgs: string[] = [];
|
||||
let confirm = false;
|
||||
@@ -1388,6 +1654,7 @@ function parseCommonOptions(args: string[]): CommonOptions {
|
||||
let consumerId: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
let json = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
|
||||
@@ -1402,11 +1669,13 @@ function parseCommonOptions(args: string[]): CommonOptions {
|
||||
} else if (arg === "--raw") {
|
||||
raw = true;
|
||||
full = true;
|
||||
} else if (arg === "--json") {
|
||||
json = true;
|
||||
} else {
|
||||
throw new Error(`unsupported pipelines-as-code option: ${arg}`);
|
||||
}
|
||||
}
|
||||
return { targetId, consumerId, full, raw };
|
||||
return { targetId, consumerId, full, raw, json };
|
||||
}
|
||||
|
||||
function closeoutReady(value: Record<string, unknown>, sourceCommit: string | null): boolean {
|
||||
@@ -1427,6 +1696,7 @@ function closeoutCiReady(value: Record<string, unknown>, sourceCommit: string |
|
||||
if (pipelineGate.ok !== true) return false;
|
||||
const observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
|
||||
if (sourceCommit !== null && observedSourceCommit !== sourceCommit) return false;
|
||||
if (stringValue(diagnostics.code) === "pac-ready-no-runtime-change") return true;
|
||||
const artifact = record(summary.artifact);
|
||||
if (stringValue(artifact.imageStatus) === "disabled") return true;
|
||||
return stringValue(artifact.gitopsCommit) !== "-";
|
||||
@@ -1479,10 +1749,23 @@ function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function runCloseoutGitOpsMirrorFlush(config: UniDeskConfig, pac: PacConfig, consumer: PacConsumer, latest: Record<string, unknown>, baseReady: boolean, observedSourceCommit: string): Promise<Record<string, unknown>> {
|
||||
async function runCloseoutGitOpsMirrorFlush(config: UniDeskConfig, pac: PacConfig, consumer: PacConsumer, summary: Record<string, unknown>, latest: Record<string, unknown>, baseReady: boolean, observedSourceCommit: string): Promise<Record<string, unknown>> {
|
||||
const cfg = pac.closeout.gitOpsMirrorFlush;
|
||||
const configSource = `${configLabel}.closeout.gitOpsMirrorFlush`;
|
||||
const required = cfg.enabled && consumer.closeoutGitOpsMirrorFlush;
|
||||
if (baseReady && stringValue(record(summary.diagnostics).code) === "pac-ready-no-runtime-change") {
|
||||
return {
|
||||
ok: true,
|
||||
enabled: cfg.enabled,
|
||||
required,
|
||||
applicable: false,
|
||||
mode: "retained-no-runtime-change",
|
||||
executed: false,
|
||||
reason: "the successful source observation produced no GitOps change, so there is no new revision to flush",
|
||||
configSource,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
if (!required) {
|
||||
return {
|
||||
ok: true,
|
||||
@@ -1766,6 +2049,8 @@ function runtimeText(runtime: Record<string, unknown>): string {
|
||||
|
||||
function renderHistoryDetail(row: Record<string, unknown>): string[] {
|
||||
const envReuse = record(row.envReuse);
|
||||
const sourceObservation = record(row.sourceObservation);
|
||||
const deliveryPlan = record(sourceObservation.plan);
|
||||
const taskRuns = record(row.taskRuns);
|
||||
const longest = record(taskRuns.longest);
|
||||
const failed = record(taskRuns.failed);
|
||||
@@ -1789,6 +2074,14 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
|
||||
["failedStepTermination", stringValue(failedStep.terminationReason ?? failedStep.reason)],
|
||||
["failure", compactLine(stringValue(failed.message))],
|
||||
["envReuseSource", stringValue(envReuse.source)],
|
||||
["deliveryMode", stringValue(sourceObservation.mode)],
|
||||
["deliveryValid", stringValue(sourceObservation.valid)],
|
||||
["deliverySourceMatched", stringValue(sourceObservation.sourceMatched)],
|
||||
["deliveryReason", stringValue(sourceObservation.reason)],
|
||||
["affectedServices", stringValue(deliveryPlan.affectedServiceCount)],
|
||||
["rolloutServices", stringValue(deliveryPlan.rolloutServiceCount)],
|
||||
["buildServices", stringValue(deliveryPlan.buildServiceCount)],
|
||||
["reusedServices", stringValue(deliveryPlan.reusedServiceCount)],
|
||||
]),
|
||||
"",
|
||||
"LOGS",
|
||||
|
||||
@@ -468,8 +468,10 @@ export function platformInfraHelp(): unknown {
|
||||
"bun scripts/cli.ts platform-infra gitea mirror sync --target JD01 --confirm",
|
||||
"bun scripts/cli.ts platform-infra gitea mirror status --target JD01",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --consumer <consumer> --source-commit <sha> --wait",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 --consumer <consumer>",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --consumer <consumer> --limit 10",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 --consumer <consumer> [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --consumer <consumer> --limit 10 [--json]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun> [--json|--full]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer> [--json]",
|
||||
],
|
||||
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, Web Terminal, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, the target-scoped Kafka event bus, internal Gitea source authority, and Gitea + Pipelines-as-Code closeout for JD01 migrated consumers. Public services use PK01 Caddy/FRP or YAML-declared PK01 Caddy upstreams rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
|
||||
target,
|
||||
|
||||
Reference in New Issue
Block a user