diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index daaf7505..cc6efeee 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -166,6 +166,11 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - installed commit 与 candidate 相同则幂等返回; - installed commit 不是 candidate 祖先时,以 `stale-or-diverged-source-commit` 拒绝; - 禁止旧 PipelineRun、分叉提交或人工补跑覆盖较新的运行面。 +- 精确变更判定: + - 任意 `master` 事件可以进入 reconcile,但不能直接等同于共享边缘配置变化; + - 健康运行面的 managed Caddy 与 Compose 指纹均未变化时,只推进 provenance, + 禁止重建 Caddy 或中断既有 HTTPS、流式响应和 WebSocket; + - 只有受管配置指纹变化或运行面漂移时,才允许进入受控 recreate。 - 共享 Caddy 更新必须使用 owner managed block: - public-edge controller只替换 `public-edge-*` blocks; - 非 public-edge managed block 必须保留; diff --git a/scripts/src/platform-infra-public-edge.ts b/scripts/src/platform-infra-public-edge.ts index 3690b178..b4f8b7f5 100644 --- a/scripts/src/platform-infra-public-edge.ts +++ b/scripts/src/platform-infra-public-edge.ts @@ -1052,20 +1052,50 @@ PY )" current_compose_sha="" if [ -f ${shQuote(target.runtime.composePath)} ]; then current_compose_sha="sha256:$(sha256sum ${shQuote(target.runtime.composePath)} | awk '{print $1}')"; fi + +write_candidate_state() { + state_candidate="$tmp/reconcile-state.json" + if ! python3 - "$state_candidate" ${shQuote(provenance.authorityId)} ${shQuote(provenance.sourceCommit)} ${shQuote(desiredCaddyFingerprint)} ${shQuote(desiredComposeFingerprint)} ${sites.length} <<'PY' +import datetime, json, pathlib, sys +path = pathlib.Path(sys.argv[1]) +path.write_text(json.dumps({ + "version": 1, + "authorityId": sys.argv[2], + "sourceCommit": sys.argv[3], + "managedCaddyFingerprint": sys.argv[4], + "composeFingerprint": sys.argv[5], + "siteCount": int(sys.argv[6]), + "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), +}, separators=(",", ":")) + "\\n", encoding="utf-8") +PY + then + return 1 + fi + chmod 0644 "$state_candidate" || return 1 + mv "$state_candidate" ${shQuote(target.runtime.statePath)} +} + if [ "$new_running" -eq 1 ] \ - && [ "$installed_commit" = ${shQuote(provenance.sourceCommit)} ] \ - && [ "$installed_caddy_sha" = ${shQuote(desiredCaddyFingerprint)} ] \ - && [ "$installed_compose_sha" = ${shQuote(desiredComposeFingerprint)} ] \ && [ "$current_managed_sha" = ${shQuote(desiredCaddyFingerprint)} ] \ && [ "$current_compose_sha" = ${shQuote(desiredComposeFingerprint)} ]; then - python3 - ${shQuote(provenance.authorityId)} ${shQuote(provenance.sourceCommit)} "$dns_warnings" <<'PY' + provenance_updated=false + if [ "$installed_commit" != ${shQuote(provenance.sourceCommit)} ] \ + || [ "$installed_caddy_sha" != ${shQuote(desiredCaddyFingerprint)} ] \ + || [ "$installed_compose_sha" != ${shQuote(desiredComposeFingerprint)} ]; then + if ! write_candidate_state; then + printf '%s\\n' '{"ok":false,"mutation":false,"error":"provenance-update-failed","valuesPrinted":false}' + exit 1 + fi + provenance_updated=true + fi + python3 - ${shQuote(provenance.authorityId)} ${shQuote(provenance.sourceCommit)} "$dns_warnings" "$provenance_updated" <<'PY' import json, sys warnings = [] with open(sys.argv[3], encoding="utf-8") as handle: for line in handle: hostname, expected_a = line.rstrip("\\n").split("\\t", 1) warnings.append({"code": "public-edge-dns-mismatch", "level": "warning", "blocking": False, "hostname": hostname, "expectedA": expected_a}) -print(json.dumps({"ok": True, "mutation": False, "mode": "already-current", "authorityId": sys.argv[1], "sourceCommit": sys.argv[2], "warnings": warnings, "valuesPrinted": False})) +print(json.dumps({"ok": True, "mutation": False, "mode": "already-current", "authorityId": sys.argv[1], "sourceCommit": sys.argv[2], "provenanceUpdated": sys.argv[4] == "true", "warnings": warnings, "valuesPrinted": False})) PY exit 0 fi @@ -1089,22 +1119,7 @@ if [ "$recreate_rc" -eq 0 ]; then fi state_rc=1 if [ "$recreate_rc" -eq 0 ] && [ "$ready_rc" -eq 0 ]; then - state_candidate="$tmp/reconcile-state.json" - python3 - "$state_candidate" ${shQuote(provenance.authorityId)} ${shQuote(provenance.sourceCommit)} ${shQuote(desiredCaddyFingerprint)} ${shQuote(desiredComposeFingerprint)} ${sites.length} <<'PY' -import datetime, json, pathlib, sys -path = pathlib.Path(sys.argv[1]) -path.write_text(json.dumps({ - "version": 1, - "authorityId": sys.argv[2], - "sourceCommit": sys.argv[3], - "managedCaddyFingerprint": sys.argv[4], - "composeFingerprint": sys.argv[5], - "siteCount": int(sys.argv[6]), - "updatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), -}, separators=(",", ":")) + "\\n", encoding="utf-8") -PY - chmod 0644 "$state_candidate" - mv "$state_candidate" ${shQuote(target.runtime.statePath)} && state_rc=0 + write_candidate_state && state_rc=0 fi rollback_rc=-1