fix(cicd): 收敛 PaC 状态读取预算

This commit is contained in:
Codex
2026-07-16 03:27:42 +02:00
parent 2cedbf46f7
commit 29bc103c84
5 changed files with 61 additions and 30 deletions
+2 -1
View File
@@ -171,7 +171,8 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
- PaC `status|closeout|history``--json` 是有界 machine contract`--full` 展开单条详情,`--raw` 才展开目标侧原始 payload。`history --id --full` 不得重复嵌套同一 rows/consumer payload或依赖 `/tmp/unidesk-cli-output` 才可读。
- PaC `status` 必须在 `observability.readOnlyCapture.timeoutMs` 内完成:
- 目标侧通过 `pac-status-progress` 披露最后阶段,CLI 将其投影为 `observation.remoteStage`
- PipelineRun 与 TaskRun 原始对象各只读取一次并复用,禁止 status 为同一 run 串行读取每个 Pod 日志;
- PipelineRun 按 YAML 选中的 Repository CR label 做服务端 selectorTaskRun 与 Argo 原始对象各只读取一次并复用,禁止 status 为同一 run 串行读取每个 Pod 日志或重复读取 Argo
- 单次 Kubernetes/Gitea/registry/runtime 读取上限必须从 YAML 总 capture 预算派生;慢调用返回 unknown/false 和阶段证据,不得吞完整体预算;
- status 的 TaskRun 日志证据模式固定为 `taskrun-status-only`,需要日志时使用 id-specific 下钻;
- 不得通过提高 YAML timeout 掩盖重复采集、同步日志或无界远端查询。
- PaC `debug-step --id <pipelinerun>` 必须读取目标侧真实 PipelineRun、TaskRun condition/result 与合同 Task 日志:
@@ -134,6 +134,12 @@
- history collector 收敛为一次 selector 聚合,保留失败日志下钻;
- 目标侧输出无敏感值的 `pac-status-progress`CLI 在 timeout observation 中显示 `remoteStage`
- PipelineRun 成功、Argo `Synced/Healthy` 且 runtime ready 时,缺少同步日志证据降级为 `pac-artifact-log-evidence-deferred` 非阻断告警。
- 首次合并后的补充恢复:
- 首个修复 PR 合并后,canary 又在 `remoteStage=argo` 间歇超过 `25000ms`
- 给 PipelineRun 查询直接设置过短的单次上限会返回空列表并制造 `pipeline-missing` false negative,不能以“命令不再超时”冒充恢复;
- 最终按 Repository CR label 在服务端筛选 PipelineRun,同时保留 consumer classifier 区分共享 repository lanes
- Kubernetes、Gitea、registry 和 runtime 的单次读取上限从 YAML 总预算派生;Argo 原始 JSON 只读一次并由 Argo 摘要与 runtime 共同复用;
- 阶段事件同时输出 elapsed 与 step timeout,连续两轮 canary 分别在 `16.3s` 与 `17.9s` 完成且保持 `ok=true`。
- 验收结论:
- 原 `status --target NC01 --consumer selfmedia-nc01 --json` 在 `14.5s` 内返回合法 JSON
- 输出 `ok=true`、`remoteStage=complete`、PipelineRun `Succeeded`、Argo `Synced/Healthy`、runtime `1/1`
@@ -220,6 +220,10 @@ test("PaC apply checks the Gitea repository before dry-run return and cluster mu
const bootstrapReadyAssignment = statusAction.split("\n").find((line) => line.includes("bootstrap_ready=$("));
expect(bootstrapReadyAssignment).toBeDefined();
expect(remote).toContain("status_progress() {");
expect(remote).toContain('stepTimeoutSeconds');
expect(remote).toContain('pac_read kubectl');
expect(remote).toContain('export UNIDESK_PAC_ARGO_FILE="$argo_file"');
expect(remote).toContain('get pipelinerun -l "pipelinesascode.tekton.dev/repository=$UNIDESK_PAC_REPOSITORY_NAME"');
expect(statusAction).toContain("status_progress artifact-evidence");
expect(statusAction).toContain("status_progress diagnostics");
expect(statusAction).toContain("status_progress complete");
@@ -5,6 +5,12 @@ 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
UNIDESK_PAC_HTTP_TIMEOUT_SECONDS="$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS"
if [ "$UNIDESK_PAC_ACTION" = status ]; then UNIDESK_PAC_HTTP_TIMEOUT_SECONDS="$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS"; fi
pac_read() {
if [ "$UNIDESK_PAC_ACTION" = status ]; then timeout "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" "$@"; else "$@"; fi
}
json_escape() {
sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' '
@@ -67,7 +73,7 @@ prepare_gitea_api_base() {
service=${host%%.*}
rest=${host#*.}
namespace=${rest%%.*}
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
cluster_ip=$(pac_read kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
UNIDESK_PAC_GITEA_API_BASE_URL="http://$cluster_ip:$port"
else
UNIDESK_PAC_GITEA_API_BASE_URL="$base"
@@ -81,13 +87,13 @@ gitea_api() {
body="${3:-}"
tmp=$(mktemp)
if [ -n "$body" ]; then
code=$(curl -sS -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
code=$(curl -sS --max-time "$UNIDESK_PAC_HTTP_TIMEOUT_SECONDS" -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
-H 'Content-Type: application/json' \
-X "$method" \
--data "$body" \
"$(api_url "$path")" || true)
else
code=$(curl -sS -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
code=$(curl -sS --max-time "$UNIDESK_PAC_HTTP_TIMEOUT_SECONDS" -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
-X "$method" \
"$(api_url "$path")" || true)
fi
@@ -103,7 +109,7 @@ gitea_api() {
gitea_api_status() {
method="$1"
path="$2"
status=$(curl -sS -o /dev/null -w '%{http_code}' \
status=$(curl -sS --max-time "$UNIDESK_PAC_HTTP_TIMEOUT_SECONDS" -o /dev/null -w '%{http_code}' \
-u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
-X "$method" \
"$(api_url "$path")" 2>/dev/null) || status=000
@@ -378,7 +384,7 @@ apply_action() {
}
condition_status() {
kubectl -n "$1" get "$2" "$3" -o jsonpath='{range .status.conditions[*]}{.type}={.status}:{.reason}{";"}{end}' 2>/dev/null || true
pac_read kubectl -n "$1" get "$2" "$3" -o jsonpath='{range .status.conditions[*]}{.type}={.status}:{.reason}{";"}{end}' 2>/dev/null || true
}
admission_provenance_state() {
@@ -390,14 +396,14 @@ admission_provenance_state() {
binding_file=$(mktemp)
role_file=$(mktemp)
role_binding_file=$(mktemp)
kubectl get validatingadmissionpolicy "$UNIDESK_PAC_ADMISSION_POLICY_NAME" -o json >"$policy_file" 2>/dev/null || printf '{}' >"$policy_file"
kubectl get validatingadmissionpolicybinding "$UNIDESK_PAC_ADMISSION_BINDING_NAME" -o json >"$binding_file" 2>/dev/null || printf '{}' >"$binding_file"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get role unidesk-pac-consumer-runner -o json >"$role_file" 2>/dev/null || printf '{}' >"$role_file"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding unidesk-pac-consumer-runner-default -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
pac_read kubectl get validatingadmissionpolicy "$UNIDESK_PAC_ADMISSION_POLICY_NAME" -o json >"$policy_file" 2>/dev/null || printf '{}' >"$policy_file"
pac_read kubectl get validatingadmissionpolicybinding "$UNIDESK_PAC_ADMISSION_BINDING_NAME" -o json >"$binding_file" 2>/dev/null || printf '{}' >"$binding_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get role unidesk-pac-consumer-runner -o json >"$role_file" 2>/dev/null || printf '{}' >"$role_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding unidesk-pac-consumer-runner-default -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
default_can_mutate=false
for verb in create patch update delete deletecollection; do
for resource in pipelineruns.tekton.dev taskruns.tekton.dev; do
if answer=$(timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" kubectl auth can-i --as="system:serviceaccount:$UNIDESK_PAC_TARGET_NAMESPACE:default" "$verb" "$resource" -n "$UNIDESK_PAC_TARGET_NAMESPACE" 2>/dev/null); then :; else :; fi
if answer=$(pac_read kubectl auth can-i --as="system:serviceaccount:$UNIDESK_PAC_TARGET_NAMESPACE:default" "$verb" "$resource" -n "$UNIDESK_PAC_TARGET_NAMESPACE" 2>/dev/null); then :; else :; fi
case "$answer" in
yes) default_can_mutate=true ;;
no) ;;
@@ -440,7 +446,7 @@ prepare_admission_provenance_state() {
pipeline_rows() {
payload_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -l "pipelinesascode.tekton.dev/repository=$UNIDESK_PAC_REPOSITORY_NAME" -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
node - "$payload_file" <<'NODE'
const fs = require('node:fs');
const { classifyPacPipelineRun, pipelineRunMatchesConsumer } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
@@ -1072,7 +1078,7 @@ task_rows() {
if [ -z "$payload_file" ]; then
payload_file=$(mktemp)
remove_payload=true
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
fi
node - "$payload_file" <<'NODE'
const fs = require('node:fs');
@@ -1127,7 +1133,7 @@ artifact_summary() {
if [ -z "$task_file" ]; then
task_file=$(mktemp)
remove_task_file=true
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
fi
node - "$task_file" <<'NODE'
const fs = require('node:fs');
@@ -1198,9 +1204,14 @@ NODE
}
runtime_summary() {
app_file=$(mktemp)
app_file="${UNIDESK_PAC_ARGO_FILE:-}"
remove_app_file=false
if [ -z "$app_file" ]; then
app_file=$(mktemp)
remove_app_file=true
pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$app_file" 2>/dev/null || printf '{}' >"$app_file"
fi
params_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$app_file" 2>/dev/null || printf '{}' >"$app_file"
printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file"
target=$(node - "$app_file" "$params_file" <<'NODE'
const fs = require('node:fs');
@@ -1216,7 +1227,8 @@ const resource = (app.status?.resources || []).find((item) => item.kind === 'Dep
if (resource) process.stdout.write(`${resource.namespace}\t${resource.name}`);
NODE
)
rm -f "$app_file" "$params_file"
if [ "$remove_app_file" = true ]; then rm -f "$app_file"; fi
rm -f "$params_file"
if [ -z "$target" ]; then
printf '{}'
return
@@ -1224,7 +1236,7 @@ NODE
namespace=$(printf '%s' "$target" | cut -f1)
name=$(printf '%s' "$target" | cut -f2)
deploy_file=$(mktemp)
kubectl -n "$namespace" get deploy "$name" -o json >"$deploy_file" 2>/dev/null || printf '{}' >"$deploy_file"
pac_read kubectl -n "$namespace" get deploy "$name" -o json >"$deploy_file" 2>/dev/null || printf '{}' >"$deploy_file"
node - "$deploy_file" <<'NODE' || printf '{}'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8').trim();
@@ -1261,7 +1273,7 @@ NODE
service=${host%%.*}
rest=${host#*.}
namespace=${rest%%.*}
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
cluster_ip=$(pac_read kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
if [ -z "$cluster_ip" ]; then
return 1
fi
@@ -1468,7 +1480,7 @@ NODE
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
if curl -fsS --max-time "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" -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)
@@ -1482,7 +1494,7 @@ NODE
if [ -n "$health_url" ]; then
if [ -n "$runtime_namespace" ] && [ -n "$runtime_service" ] && [ -n "$runtime_service_port" ] && [ -n "$health_path" ]; then
health_file=$(mktemp)
if kubectl get --raw "/api/v1/namespaces/$runtime_namespace/services/$runtime_service:$runtime_service_port/proxy$health_path" >"$health_file" 2>/dev/null \
if pac_read kubectl get --raw "/api/v1/namespaces/$runtime_namespace/services/$runtime_service:$runtime_service_port/proxy$health_path" >"$health_file" 2>/dev/null \
&& node - "$health_file" <<'NODE'
const fs = require('node:fs');
const body = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
@@ -1495,7 +1507,7 @@ NODE
fi
rm -f "$health_file"
else
health_status=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 8 "$health_url" 2>/dev/null || true)
health_status=$(curl -sS -o /dev/null -w '%{http_code}' --max-time "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" "$health_url" 2>/dev/null || true)
fi
fi
export UNIDESK_PAC_DIAG_IMAGE_REPOSITORY="$image_repository"
@@ -1570,14 +1582,14 @@ consumer_bootstrap_state() {
role_binding_file=$(mktemp)
argo_secret_file=$(mktemp)
if [ -n "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME:-}" ]; then
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get serviceaccount "$UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME" -o json >"$sa_file" 2>/dev/null || printf '{}' >"$sa_file"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding "$UNIDESK_PAC_RUNNER_ROLE_BINDING_NAME" -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get serviceaccount "$UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME" -o json >"$sa_file" 2>/dev/null || printf '{}' >"$sa_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding "$UNIDESK_PAC_RUNNER_ROLE_BINDING_NAME" -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
else
printf '{}' >"$sa_file"
printf '{}' >"$role_binding_file"
fi
if [ -n "${UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME:-}" ]; then
kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get secret "$UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME" -o json >"$argo_secret_file" 2>/dev/null || printf '{}' >"$argo_secret_file"
pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get secret "$UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME" -o json >"$argo_secret_file" 2>/dev/null || printf '{}' >"$argo_secret_file"
else
printf '{}' >"$argo_secret_file"
fi
@@ -1673,13 +1685,15 @@ NODE
}
status_progress() {
printf '{"event":"pac-status-progress","stage":"%s","valuesPrinted":false}\n' "$1" >&2
elapsed_seconds=$(( $(now_epoch) - UNIDESK_PAC_STATUS_STARTED_EPOCH ))
printf '{"event":"pac-status-progress","stage":"%s","elapsedSeconds":%s,"stepTimeoutSeconds":%s,"valuesPrinted":false}\n' "$1" "$elapsed_seconds" "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" >&2
}
status_action() {
UNIDESK_PAC_STATUS_STARTED_EPOCH=$(now_epoch)
status_progress control-plane
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
crd=$(pac_read kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
controller_ready=$(pac_read kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
status_progress admission-provenance
prepare_admission_provenance_state
@@ -1693,7 +1707,7 @@ status_action() {
status_progress task-runs
task_file=$(mktemp)
if [ -n "$latest" ]; then
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$latest" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$latest" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
else
printf '{"items":[]}' >"$task_file"
fi
@@ -1706,7 +1720,10 @@ status_action() {
status_progress webhooks
hooks=$(hook_summary)
status_progress argo
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); const source=a.spec?.source || a.spec?.sources?.[0] || {}; let repoURL=source.repoURL||null; try { const url=new URL(repoURL); url.username=""; url.password=""; repoURL=url.toString(); } catch {} process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null, repoURL, targetRevision:source.targetRevision||null}))' || echo '{}')
argo_file=$(mktemp)
pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$argo_file" 2>/dev/null || printf '{}' >"$argo_file"
export UNIDESK_PAC_ARGO_FILE="$argo_file"
argo=$(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); const source=a.spec?.source || a.spec?.sources?.[0] || {}; let repoURL=source.repoURL||null; try { const url=new URL(repoURL); url.username=""; url.password=""; repoURL=url.toString(); } catch {} process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null, repoURL, targetRevision:source.targetRevision||null}))' <"$argo_file" || echo '{}')
argo=$(json_normalize "$argo")
status_progress gitops-revision
revision_relation=$(gitops_revision_relation "$artifact" "$argo")
@@ -1724,6 +1741,8 @@ NODE
)
status_progress runtime
runtime=$(json_normalize "$(runtime_summary)")
unset UNIDESK_PAC_ARGO_FILE
rm -f "$argo_file"
status_progress diagnostics
diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
diagnostics=$(UNIDESK_PAC_DIAGNOSTICS="$diagnostics" UNIDESK_PAC_STATE="$UNIDESK_PAC_ADMISSION_STATE_JSON" UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" UNIDESK_PAC_PIPELINES_JSON="$pipelines" UNIDESK_PAC_ARTIFACT_JSON="$artifact" UNIDESK_PAC_ARGO_JSON="$argo" UNIDESK_PAC_RUNTIME_JSON="$runtime" node <<'NODE'
@@ -2059,6 +2059,7 @@ function remoteScript(action: "apply" | "status" | "history" | "diagnose-regress
UNIDESK_PAC_RELEASE_MANIFEST_B64: Buffer.from(releaseManifest, "utf8").toString("base64"),
UNIDESK_PAC_CONTROLLER_SERVICE_NAME: pac.release.controllerServiceName,
UNIDESK_PAC_WAIT_TIMEOUT_SECONDS: String(pac.release.waitTimeoutSeconds),
UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS: String(Math.max(1, Math.floor(pac.observability.readOnlyCapture.timeoutMs / 8_000))),
UNIDESK_PAC_DRY_RUN: "dryRun" in options && options.dryRun ? "1" : "0",
UNIDESK_PAC_GITEA_BASE_URL: pac.gitea.internalBaseUrl.replace(/\/+$/u, ""),
UNIDESK_PAC_GITEA_ADMIN_USERNAME: secrets.adminUsername,