From 0cfb5329c78ff0e2ccf2eb21ed26954fe1f4e28d Mon Sep 17 00:00:00 2001 From: pikastech Date: Mon, 20 Jul 2026 17:45:03 +0200 Subject: [PATCH] fix: move Gitea public edge to NC01 --- .agents/skills/unidesk-cicd/SKILL.md | 5 +- .../unidesk-cicd/references/agentrun.md | 6 +- .../unidesk-cicd/references/gitea-pac.md | 14 +- config/platform-infra/gitea.yaml | 42 ++--- config/platform-infra/pipelines-as-code.yaml | 22 +-- config/platform-infra/public-edge.yaml | 3 + docs/reference/cicd-standardization.md | 8 +- docs/reference/platform-infra.md | 16 +- scripts/src/platform-infra-gitea-config.ts | 107 ++++++------ .../src/platform-infra-gitea-payload.test.ts | 3 +- scripts/src/platform-infra-gitea-payload.ts | 2 - scripts/src/platform-infra-gitea-remote.sh | 62 ++----- scripts/src/platform-infra-gitea-render.ts | 10 +- scripts/src/platform-infra-gitea.ts | 163 ++---------------- ...-infra-pipelines-as-code-bootstrap.test.ts | 2 +- scripts/src/platform-infra/entry.ts | 2 +- 16 files changed, 154 insertions(+), 313 deletions(-) diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index cc6efeee..c0f4711e 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -320,7 +320,10 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 同 deliveryId 与同 payload 必须幂等,异 payload 必须返回 `409`;PVC 不可用或容量满必须返回 `503` 并使 readiness=false; - inbox worker 使用 YAML 声明的有界 backoff 自动重试,重启后恢复 `accepted` / `processing`;Caddy 只可在请求尚未持久化时做小于 10 秒的 body-safe 有界重试。 - Durable inbox 只是 webhook delivery 的持久接收与重试 journal,不是业务 source/ref 的第二份真相,也不得被 PaC、Tekton、GitOps、Argo 或 runtime 当作源码/read model。最终 source authority 仍只有 Gitea authority branch 与 immutable snapshot。默认只使用 `platform-infra gitea mirror webhook status` 观察 accepted -> committed 分层。会 POST hook test 的 mutation 测试入口已经删除;连通性只能通过真正只读的 status、GET 与 readiness 观察,不能制造伪 push。平台 bootstrap 必须先落入 Git-backed YAML/controller/source,并由正常 PR merge 的自动事件验收;禁止 Gitea -> GitHub 回写、轮询/read-model fallback、Gitea Actions/act_runner fallback 或第二 source authority。 -- Gitea webhook/FRPC/Caddy 自动链变更的 closeout 必须观察修复 PR 合并后的真实 delivery、rollout、只读 status/GET/readiness 证据;Secret 或 ConfigMap 声明已生成不等于 connector Pod 已加载新 proxy/path,也不得用人工 apply 或 hook test 补齐。 +- Gitea webhook/public-edge 自动链变更的 closeout: + - 必须观察修复 PR 合并后的真实 delivery、rollout 和只读 status/GET/readiness 证据; + - Secret、ConfigMap 或 public-edge 候选已生成不等于 connector Pod 与公网 route 已加载新配置; + - 不得用人工 apply 或 hook test 补齐。 - Sentinel 内部 publish capability 的唯一真相是 `config/platform-infra/pipelines-as-code.yaml#capabilities.sentinelInternalPublish`,当前默认关闭。Pod env、ownerReferences、ServiceAccount 与 metadata 不能证明 creator;#1769 的 admission-owned provenance 落地前不得启用。`.tekton` 保持既有自动 `publish-current` 入口,禁止改成人工 publish。 - PaC PipelineRun 必须区分外层 `outer-pac-event`、内层 `inner-deterministic-publish` 与 `unknown`。只有唯一外层事件可驱动 latest/status/debug-step/closeout;内层只能作为未绑定执行观察,缺少 admission-owned 父子证据时不得按前缀推断。 - `cicd branch-follower` 对当前 PaC consumer 已退役,只保留 `status|events|logs` 与只读 `debug-step` 历史诊断。默认入口、任意 `--config` 和混合/未知 authority 都不能恢复 `apply|run-once|state-write` 等写能力,也不能猜测默认 node。 diff --git a/.agents/skills/unidesk-cicd/references/agentrun.md b/.agents/skills/unidesk-cicd/references/agentrun.md index 4e5807a7..adaea3c7 100644 --- a/.agents/skills/unidesk-cicd/references/agentrun.md +++ b/.agents/skills/unidesk-cicd/references/agentrun.md @@ -88,7 +88,11 @@ AgentRun YAML-only lane closeout 必须同时看当前 k8s git-mirror source sna JD01/NC01 `agentrun--v02` 已迁移为单一路径:GitHub PR merge -> GitHub webhook -> Gitea controlled mirror -> immutable snapshot -> Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime。不要保留 Gitea Actions、act_runner、legacy branch-follower 或第二套 trigger fallback。CLI 必须组合 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml` 精确解析 consumer;未知、歧义或 source identity drift 都必须 `mutation=false` 并 fail-closed。 -Gitea 只作为受控 mirror/source authority 和 PaC webhook 源。公开 Web UI 是 `https://gitea.pikapython.com`;k8s 内部 clone/read 必须走 `http://gitea-http.devops-infra.svc.cluster.local:3000/...`,不要让内部 CI、Argo 或 runtime 通过公网域名回环。需要 CI 匿名内部 read 的 mirror repo 必须在 YAML 中声明 `publicRead: true`,bootstrap/apply 必须修正既有 repo/org visibility,而不只依赖 create-time 默认值。 +- Gitea 只作为受控 mirror/source authority 和 PaC webhook 源: + - 公开 Web UI 是 `https://gitea.hwpod.com`,由 NC01 public-edge 直连 ClusterIP; + - k8s 内部 clone/read 必须走 `http://gitea-http.devops-infra.svc.cluster.local:3000/...`,不得让内部 CI、Argo 或 runtime 通过公网域名回环; + - 需要 CI 匿名内部 read 的 mirror repo 必须在 YAML 中声明 `publicRead: true`; + - bootstrap/apply 必须修正既有 repo/org visibility,不能只依赖 create-time 默认值。 PaC 默认只读状态入口是: diff --git a/.agents/skills/unidesk-cicd/references/gitea-pac.md b/.agents/skills/unidesk-cicd/references/gitea-pac.md index 9f0d5b4e..aa9aee58 100644 --- a/.agents/skills/unidesk-cicd/references/gitea-pac.md +++ b/.agents/skills/unidesk-cicd/references/gitea-pac.md @@ -40,7 +40,12 @@ GitHub 是唯一上游写入权威。目标 source branch 的 GitHub PR merge - 旧人工 mutation 入口只允许在 owning YAML 精确解析为 `legacy-manual` 后从 scoped help 使用;Gitea/PaC migrated consumer 和 `unknown` authority 都不得回退到 legacy 实现。 - 包含 `gitea-actions` 的历史 snapshot prefix 只为既有 ref 保留,不代表 Gitea Actions 仍是有效触发架构。 - node 级状态入口是 `bun scripts/cli.ts cicd status --node `,consumer 级默认下钻入口是 `platform-infra pipelines-as-code status|history --target --consumer `。 -- 共用 GitHub 上游的多 node PaC consumer 不得共用 webhook bridge URL。每个 target node 在 `config/platform-infra/gitea.yaml#targets[].webhookSync` 声明自己的 GitHub webhook 公网路径与 FRP remote port,并由平台自动交付渲染 node-local bridge、FRPC proxy 和 PK01 Caddy 路径;禁止在 source PR 合并后人工 apply 补齐。 +- Gitea 公网入口: + - 公开 Web UI 与 owning webhook path 统一由 `config/platform-infra/gitea.yaml#app.publicExposure` 声明; + - NC01 public-edge 直接连接 ClusterIP Service; + - 不得为 Gitea 保留 FRPC、PK01 Caddy 或公网回环 fallback; + - target 仍通过 `targets[].webhookSync.publicPath` 保持 hook 身份隔离; + - 禁止在 source PR 合并后人工 apply 补齐。 - PaC Repository CR `spec.url` 必须与 Gitea webhook payload 中的 URL 一致。`config/platform-infra/pipelines-as-code.yaml#repositories[].url` 保持公网 Gitea repository URL,k8s 内网 service URL 只能写入 `cloneUrl` 或 `params.git_read_url`;否则 PaC 会记录 `cannot find a repository match` 且不会创建 PipelineRun。 - JD01 与 NC01 共用的 repository 必须传入 target-specific `node` Repository param,每个 `.tekton` PipelineRun 必须用 `pipelinesascode.tekton.dev/on-cel-expression` 过滤该参数。一个 target 的 push 不得在同一 target cluster 创建另一 target 的 PipelineRun。 @@ -211,7 +216,12 @@ GitHub webhook delivery 的持久接收与 refs 提交必须分层: - 只有同一 delivery 的 exact-after immutable snapshot 与 authority branch 经 atomic push 后重新读取 refs 证明一致,才能进入 `committed`; - Caddy 只可对尚未成功落盘的请求做小于 10 秒、body-safe 的有界重试,不能把代理成功当作 refs committed。 -当前 bridge 的权威状态链是 GitHub delivery GUID -> durable inbox record -> post-push refs proof -> live refs。HTTP `202` 后记录仍是 `accepted`、`refsCommitted=false`;只有 worker 完成 exact refs 复核后才成为 committed。旧 durable journal 上线前的记录可以标记 `pre-durable-bootstrap`,但不得标记 committed。`ingressRetry` 在自动 PK01 host reconciler 完成前保持关闭,不能由操作者手工补配置。 +- 当前 bridge 的权威状态链: + - GitHub delivery GUID -> durable inbox record -> post-push refs proof -> live refs; + - HTTP `202` 后记录仍是 `accepted`、`refsCommitted=false`; + - 只有 worker 完成 exact refs 复核后才成为 committed; + - 旧 durable journal 上线前的记录可以标记 `pre-durable-bootstrap`,但不得标记 committed; + - `ingressRetry` 由 owning YAML 与 NC01 public-edge 自动交付共同控制,不能由操作者手工补配置。 Durable inbox 只是 webhook delivery 的持久接收与重试 journal,不是业务 source/ref 的第二份真相,也不得被 PaC、Tekton、GitOps、Argo 或 runtime 当作源码/read model。最终 source authority 仍只有 Gitea authority branch 与 immutable snapshot。`webhook status` 默认摘要必须显示 accepted -> processing -> committed/failed 分层,并将同一 delivery 的 accepted correlation 与 committed refs proof 关联。非 `202`、缺 accepted correlation、accepted 后未 committed 或进入 failed 都是失败/stale;只能修 webhook bridge、inbox worker、受控 mirror、snapshot controller 或 owning YAML,禁止输出或执行人工 `REPAIR`、mirror sync、直接 Gitea push 等补齐命令。 diff --git a/config/platform-infra/gitea.yaml b/config/platform-infra/gitea.yaml index b1fd5a02..e20f681f 100644 --- a/config/platform-infra/gitea.yaml +++ b/config/platform-infra/gitea.yaml @@ -176,9 +176,6 @@ sourceAuthority: email: platform-infra-ci@unidesk.local cas: maxAttempts: 3 - frpc: - proxyName: platform-infra-gitea-jd01-github-webhook - remotePort: 22081 responsibilities: - name: source-read current: legacy-git-mirror @@ -669,9 +666,6 @@ targets: enabled: true webhookSync: publicPath: /_unidesk/github-to-gitea/nc01 - frpc: - proxyName: platform-infra-gitea-nc01-github-webhook - remotePort: 22086 app: name: gitea @@ -687,37 +681,29 @@ app: httpPort: 3000 sshPort: 2222 server: - domain: gitea.pikapython.com - rootUrl: https://gitea.pikapython.com/ + domain: gitea.hwpod.com + rootUrl: https://gitea.hwpod.com/ sshDomain: gitea-http.devops-infra.svc.cluster.local protocol: http startSshServer: true publicExposure: enabled: true - publicBaseUrl: https://gitea.pikapython.com - secretRoot: /root/.unidesk/.state/secrets + publicBaseUrl: https://gitea.hwpod.com + upstream: http://gitea-http.devops-infra.svc.cluster.local:3000 + healthPath: /api/healthz dns: - hostname: gitea.pikapython.com - expectedA: 82.156.23.220 + hostname: gitea.hwpod.com + expectedA: 152.53.229.148 resolvers: [1.1.1.1, 8.8.8.8, 223.5.5.5, 114.114.114.114] - frpc: + routes: + - path: /_unidesk/github-to-gitea/nc01* + upstream: http://gitea-github-sync.devops-infra.svc.cluster.local:8080 + responseHeaderTimeout: 7s + - path: "*" + upstream: http://gitea-http.devops-infra.svc.cluster.local:3000 + legacyCleanup: deploymentName: gitea-frpc secretName: gitea-frpc-secrets - secretKey: frpc.toml - image: fatedier/frpc:v0.68.1 - serverAddr: 82.156.23.220 - serverPort: 22000 - proxyName: platform-infra-gitea-nc01-web - remotePort: 22080 - localIP: gitea-http.devops-infra.svc.cluster.local - localPort: 3000 - tokenSourceRef: platform-infra/pk01-frp.env - tokenSourceKey: FRP_TOKEN - pk01: - route: PK01 - caddyConfigPath: /etc/caddy/Caddyfile - caddyServiceName: caddy - responseHeaderTimeoutSeconds: 600 database: type: sqlite3 path: /var/lib/gitea/gitea.db diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 9ff6fc71..5a499d56 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -146,7 +146,7 @@ repositories: variables: NODE: JD01 LANE: v02 - url: https://gitea.pikapython.com/mirrors/pikasTech-agentrun + url: https://gitea.hwpod.com/mirrors/pikasTech-agentrun - extends: templates.repositories.agentrunV02 variables: NODE: NC01 @@ -156,7 +156,7 @@ repositories: NODE: JD01 LANE: v03 sourceSnapshotPrefix: refs/unidesk/snapshots/gitea-actions/unidesk-master - url: https://gitea.pikapython.com/mirrors/pikasTech-unidesk + url: https://gitea.hwpod.com/mirrors/pikasTech-unidesk - extends: templates.repositories.sentinelV03 variables: NODE: NC01 @@ -177,7 +177,7 @@ repositories: name: sub2rank-nc01 namespace: devops-infra providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikasTech-apistate + url: https://gitea.hwpod.com/mirrors/pikasTech-apistate cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-apistate.git owner: mirrors repo: pikasTech-apistate @@ -209,7 +209,7 @@ repositories: name: sub2rank-development-nc01 namespace: devops-infra providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikasTech-apistate-development + url: https://gitea.hwpod.com/mirrors/pikasTech-apistate-development cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-apistate-development.git owner: mirrors repo: pikasTech-apistate-development @@ -241,7 +241,7 @@ repositories: name: hwpod-home-nc01 namespace: devops-infra providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikainc-hwpod-home + url: https://gitea.hwpod.com/mirrors/pikainc-hwpod-home cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-hwpod-home.git owner: mirrors repo: pikainc-hwpod-home @@ -273,7 +273,7 @@ repositories: variables: NODE: JD01 LANE: v03 - url: https://gitea.pikapython.com/mirrors/pikasTech-HWLAB + url: https://gitea.hwpod.com/mirrors/pikasTech-HWLAB - extends: templates.repositories.hwlabV03 variables: NODE: NC01 @@ -282,7 +282,7 @@ repositories: name: selfmedia-nc01 namespace: selfmedia-ci providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikainc-selfmedia + url: https://gitea.hwpod.com/mirrors/pikainc-selfmedia cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-selfmedia.git owner: mirrors repo: pikainc-selfmedia @@ -317,7 +317,7 @@ repositories: name: pikaoa-dev-nc01 namespace: pikaoa-ci providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikainc-pikaoa + url: https://gitea.hwpod.com/mirrors/pikainc-pikaoa cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git owner: mirrors repo: pikainc-pikaoa @@ -822,7 +822,7 @@ templates: name: "agentrun-${nodeLower}-v02" namespace: agentrun-ci providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikasTech-agentrun + url: https://gitea.hwpod.com/mirrors/pikasTech-agentrun cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git owner: mirrors repo: pikasTech-agentrun @@ -846,7 +846,7 @@ templates: name: "sentinel-${nodeLower}-v03" namespace: devops-infra providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikasTech-unidesk + url: https://gitea.hwpod.com/mirrors/pikasTech-unidesk cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git owner: mirrors repo: pikasTech-unidesk @@ -877,7 +877,7 @@ templates: name: "hwlab-${nodeLower}-v03" namespace: hwlab-ci providerType: gitea - url: https://gitea.pikapython.com/mirrors/pikasTech-HWLAB + url: https://gitea.hwpod.com/mirrors/pikasTech-HWLAB cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git owner: mirrors repo: pikasTech-HWLAB diff --git a/config/platform-infra/public-edge.yaml b/config/platform-infra/public-edge.yaml index 9062275a..7aa70d78 100644 --- a/config/platform-infra/public-edge.yaml +++ b/config/platform-infra/public-edge.yaml @@ -69,6 +69,9 @@ targets: # 共享入口包含长耗时流式 API,首响应头预算统一按 10 分钟配置。 responseHeaderTimeoutSeconds: 600 sites: + - id: gitea + configRef: config/platform-infra/gitea.yaml + path: app.publicExposure - id: artifact-registry configRef: config/platform-infra/k3s-clusters.yaml path: targets.d601.k3s.registryMirror diff --git a/docs/reference/cicd-standardization.md b/docs/reference/cicd-standardization.md index d035a076..e9eb2216 100644 --- a/docs/reference/cicd-standardization.md +++ b/docs/reference/cicd-standardization.md @@ -28,7 +28,13 @@ Network identity is part of the evidence. `127.0.0.1:5000` may mean runner-pod l Manual hotfixes are allowed only to recover and learn the missing contract. The durable close-out is a repo-owned preflight, desired-state check, CLI behavior, or reference update, followed by the same verification from the actual runner environment. A healthy old live service does not prove CD success; success requires the formal job to pass and live commit or digest identity to match the desired state. -For node-scoped HWLAB/Gitea/PaC lanes, CI/CD closeout stops at PipelineRun success, GitOps/Argo sync and health, runtime readiness/provenance, git-mirror alignment, and the formal public HTTPS `/health` probe declared by YAML. Browser automation, `web-probe`, Playwright, DOM assertions and user-path E2E are post-deploy product validation, not CI/CD gates. FRP/Caddy public exposure failures are diagnosed by aligning YAML publicExposure, GitOps-rendered frpc config, Secret key presence, frpc login/proxy logs, PK01/Caddy loopback and target-side HTTPS probe; Secret values are never recorded in docs or issue comments. +- 对于 node-scoped HWLAB/Gitea/PaC lane: + - CI/CD 收口只覆盖 PipelineRun 成功、GitOps/Argo 同步与健康、runtime readiness/provenance、git-mirror 对齐,以及 YAML 声明的正式公网 HTTPS `/health` 探针; + - 浏览器自动化、`web-probe`、Playwright、DOM 断言和用户路径 E2E 属于部署后的产品验收,不是 CI/CD 门禁; + - 公网暴露故障必须按 owning YAML 选中的 managed edge 诊断; + - Gitea 通过 NC01 public-edge 直连 ClusterIP; + - 仍由 YAML 选择 FRP 的服务继续检查各自声明的 FRP/Caddy 路径; + - Secret 值不得写入文档或 issue 评论。 ## Target Shape diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index f6f5194d..d27a1a99 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -180,7 +180,10 @@ ## Gitea 与 Pipelines-as-Code 边界 -- Gitea mirror 与 Pipelines-as-Code 是 UniDesk 运维的 CI source/trigger 服务。`config/platform-infra/gitea.yaml` 和 `config/platform-infra/pipelines-as-code.yaml` 分别拥有 mirror/webhook 与 PaC Repository/consumer 配置;repo URL、snapshot、webhook、public exposure、FRP/Caddy port、token sourceRef 和 PaC params 不得隐藏在代码特例中。 +- Gitea mirror 与 Pipelines-as-Code 是 UniDesk 运维的 CI source/trigger 服务: + - `config/platform-infra/gitea.yaml` 拥有 mirror、webhook 与 NC01 public-edge 暴露; + - `config/platform-infra/pipelines-as-code.yaml` 拥有 PaC Repository 与 consumer 配置; + - repo URL、snapshot、webhook、token sourceRef 和 PaC params 不得隐藏在代码特例中。 - Delivery authority resolver 必须组合上述两份 YAML,并按 consumer id、node、lane、upstream repository、branch 和 Gitea repository 精确关联。不得通过 URL 片段、repo 名称前缀或仓库专属 `if` 推断迁移状态。零匹配、多匹配和配置错误都必须返回 `unknown`、`mutation=false` 并停止生成或执行写操作。 - PaC consumer 的唯一正式触发是目标 source branch 的 GitHub PR merge。完整自动链固定为:GitHub PR merge -> GitHub webhook -> Gitea controlled mirror -> immutable snapshot -> Gitea webhook -> PaC/Pipelines-as-Code -> Tekton -> GitOps/Argo -> runtime 运行面。 - PR 合并后的交付边界: @@ -231,8 +234,15 @@ - cache export 使用原生 `ignore-error=true` 或等价非阻塞语义; - cache export 抖动不得在镜像已发布后制造业务构建失败或人工补跑需求。 - GitHub 始终是 upstream write authority。Bridge 只允许 GitHub -> Gitea 更新 YAML 声明的 branch 与 immutable snapshot,不得反向写 GitHub,也不得增加 polling 或第二 trigger path。 -- 每个 target 必须使用 YAML 独立声明的 webhook path、bridge 与 FRP port;多个节点不得复用 hook URL。Public Gitea Web UI 与 webhook 只能通过明确 Caddy path routing 共享 hostname,k8s 内部 consumer 必须使用 ClusterIP URL。 -- Secret 或 ConfigMap 改变后,Gitea connector 的显式平台配置维护必须等待 workload rollout。成功写 Secret 不能证明 `frpc` 已加载新配置;端到端交付证据仍必须来自之后的新正常 PR merge。 +- Gitea 公网入口: + - 公开 Web UI 与 owning webhook path 由 `app.publicExposure.routes` 明确分流; + - NC01 public-edge 直接连接 ClusterIP Service; + - Gitea 不得依赖 PK01、FRPC、Ingress、NodePort 或公网域名回环; + - k8s 内部 consumer 必须使用 ClusterIP URL。 +- Secret 或 ConfigMap 改变后的收口: + - Gitea connector 的显式平台配置维护必须等待 workload rollout; + - 成功写 Secret 或生成 public-edge 候选不能证明 bridge 已加载新配置; + - 端到端交付证据仍必须来自之后的新正常 PR merge。 - Shared upstream repository 必须通过 Repository CR params 与 `pipelinesascode.tekton.dev/on-cel-expression` 隔离 target-specific `.tekton`。NC01 delivery 不得在 JD01 cluster 创建 PipelineRun,history 也不得把同一 PipelineRun 归属给多个 consumer。退役 branch-follower 的 unknown 或多 node scope 只返回 follower 自身的只读 status/events/logs 和稳定修复引用,禁止猜测 `JD01` 或其他默认 node 的 PaC status/history。 - PaC migrated lane 保持单 trigger path,禁止新增 Gitea Actions、`act_runner`、branch-follower 或脚本 fallback。`config/cicd-gitea-actions-poc.yaml` 只是归档 POC;历史 snapshot 名称不代表该路径仍在运行。 - k8s runtime 从拉取已构建 image 起保持 Docker-free。CI 可以使用 YAML 声明的 build tooling,但 runtime 不得依赖 Docker socket 或 daemon。 diff --git a/scripts/src/platform-infra-gitea-config.ts b/scripts/src/platform-infra-gitea-config.ts index ef4f5104..4ad43749 100644 --- a/scripts/src/platform-infra-gitea-config.ts +++ b/scripts/src/platform-infra-gitea-config.ts @@ -1,7 +1,6 @@ import { dirname } from "node:path"; import { rootPath } from "./config"; import { createYamlFieldReader, readYamlRecord } from "./platform-infra-ops-library"; -import type { PublicServiceExposure } from "./platform-infra-public-service"; import { validateGiteaWebhookTiming } from "./platform-infra-gitea-caddy"; import type { GiteaWebhookIngressRetry } from "./platform-infra-gitea-caddy"; @@ -24,9 +23,26 @@ export interface GiteaTarget { export interface GiteaTargetWebhookSync { publicPath: string; - frpc: { - proxyName: string; - remotePort: number; +} + +export interface GiteaPublicExposure { + enabled: boolean; + publicBaseUrl: string; + upstream: string; + healthPath: string; + dns: { + hostname: string; + expectedA: string; + resolvers: string[]; + }; + routes: Array<{ + path: string; + upstream: string; + responseHeaderTimeout?: string; + }>; + legacyCleanup: { + deploymentName: string; + secretName: string; }; } @@ -92,7 +108,7 @@ export interface GiteaConfig { protocol: "http" | "https"; startSshServer: boolean; }; - publicExposure: PublicServiceExposure & { secretRoot: string }; + publicExposure: GiteaPublicExposure; database: { type: "sqlite3"; path: string; @@ -238,10 +254,6 @@ export interface GiteaWebhookSync { }; }; gitOpsDelivery: GiteaWebhookGitOpsDelivery; - frpc: { - proxyName: string; - remotePort: number; - }; } export interface GiteaWebhookGitOpsDelivery { @@ -490,7 +502,6 @@ function parseWebhookSync(record: Record, path: string): GiteaW const candidateGate = y.objectField(bridge, "candidateGate", `${path}.bridge`); const inbox = y.objectField(bridge, "inbox", `${path}.bridge`); const gitOpsDelivery = y.objectField(record, "gitOpsDelivery", path); - const frpc = y.objectField(record, "frpc", path); return { enabled: y.booleanField(record, "enabled", path), direction: y.enumField(record, "direction", path, ["github-to-gitea"] as const), @@ -560,10 +571,6 @@ function parseWebhookSync(record: Record, path: string): GiteaW retry: parseWebhookRetry(y.objectField(bridge, "retry", `${path}.bridge`), `${path}.bridge.retry`), }, gitOpsDelivery: parseWebhookGitOpsDelivery(gitOpsDelivery, `${path}.gitOpsDelivery`), - frpc: { - proxyName: y.stringField(frpc, "proxyName", `${path}.frpc`), - remotePort: y.portField(frpc, "remotePort", `${path}.frpc`), - }, }; } @@ -612,41 +619,36 @@ function parseWebhookRetry(record: Record, path: string): Gitea }; } -function parsePublicExposure(record: Record, path: string): PublicServiceExposure & { secretRoot: string } { +function parsePublicExposure(record: Record, path: string): GiteaPublicExposure { const dns = y.objectField(record, "dns", path); - const frpc = y.objectField(record, "frpc", path); - const pk01 = y.objectField(record, "pk01", path); + const legacyCleanup = y.objectField(record, "legacyCleanup", path); const publicBaseUrl = httpsUrlField(record, "publicBaseUrl", path); const hostname = y.hostField(dns, "hostname", `${path}.dns`); if (new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${configLabel}.${path}.dns.hostname must match publicBaseUrl`); + const routes = y.arrayOfRecords(record.routes, `${path}.routes`).map((route, index) => { + const routePath = `${path}.routes[${index}]`; + return { + path: y.stringField(route, "path", routePath), + upstream: urlField(route, "upstream", routePath), + ...(route.responseHeaderTimeout === undefined + ? {} + : { responseHeaderTimeout: y.stringField(route, "responseHeaderTimeout", routePath) }), + }; + }); return { enabled: y.booleanField(record, "enabled", path), publicBaseUrl, - secretRoot: y.absolutePathField(record, "secretRoot", path), + upstream: urlField(record, "upstream", path), + healthPath: y.apiPathField(record, "healthPath", path), dns: { hostname, expectedA: y.stringField(dns, "expectedA", `${path}.dns`), resolvers: y.stringArrayField(dns, "resolvers", `${path}.dns`), }, - frpc: { - deploymentName: y.kubernetesNameField(frpc, "deploymentName", `${path}.frpc`), - secretName: y.kubernetesNameField(frpc, "secretName", `${path}.frpc`), - secretKey: y.stringField(frpc, "secretKey", `${path}.frpc`), - image: y.stringField(frpc, "image", `${path}.frpc`), - serverAddr: y.hostField(frpc, "serverAddr", `${path}.frpc`), - serverPort: y.portField(frpc, "serverPort", `${path}.frpc`), - proxyName: y.stringField(frpc, "proxyName", `${path}.frpc`), - remotePort: y.portField(frpc, "remotePort", `${path}.frpc`), - localIP: y.hostField(frpc, "localIP", `${path}.frpc`), - localPort: y.portField(frpc, "localPort", `${path}.frpc`), - tokenSourceRef: secretSourceRefField(frpc, "tokenSourceRef", `${path}.frpc`), - tokenSourceKey: y.envKeyField(frpc, "tokenSourceKey", `${path}.frpc`), - }, - pk01: { - route: y.stringField(pk01, "route", `${path}.pk01`), - caddyConfigPath: y.absolutePathField(pk01, "caddyConfigPath", `${path}.pk01`), - caddyServiceName: y.stringField(pk01, "caddyServiceName", `${path}.pk01`), - responseHeaderTimeoutSeconds: y.integerField(pk01, "responseHeaderTimeoutSeconds", `${path}.pk01`), + routes, + legacyCleanup: { + deploymentName: y.kubernetesNameField(legacyCleanup, "deploymentName", `${path}.legacyCleanup`), + secretName: y.kubernetesNameField(legacyCleanup, "secretName", `${path}.legacyCleanup`), }, }; } @@ -745,13 +747,8 @@ function parseTarget(record: Record, index: number): GiteaTarge } function parseTargetWebhookSync(record: Record, path: string): GiteaTargetWebhookSync { - const frpc = y.objectField(record, "frpc", path); return { publicPath: y.apiPathField(record, "publicPath", path), - frpc: { - proxyName: y.stringField(frpc, "proxyName", `${path}.frpc`), - remotePort: y.portField(frpc, "remotePort", `${path}.frpc`), - }, }; } @@ -811,7 +808,6 @@ function validateConfig(gitea: GiteaConfig): void { if (gitea.sourceAuthority.webhookSync.enabled) { const events = new Set(gitea.sourceAuthority.webhookSync.events); if (!events.has("push")) throw new Error(`${configLabel}.sourceAuthority.webhookSync.events must include push for GitHub -> Gitea source sync`); - if (gitea.sourceAuthority.webhookSync.frpc.remotePort === gitea.app.publicExposure.frpc.remotePort) throw new Error(`${configLabel}.sourceAuthority.webhookSync.frpc.remotePort must differ from app.publicExposure.frpc.remotePort`); const ingressRetry = gitea.sourceAuthority.webhookSync.ingressRetry; const hookReconcile = gitea.sourceAuthority.webhookSync.hookReconcile; const bridge = gitea.sourceAuthority.webhookSync.bridge; @@ -865,14 +861,15 @@ function validateConfig(gitea: GiteaConfig): void { } } const webhookPaths = new Set(); - const webhookPorts = new Set(); - for (const route of webhookCaddyRoutes(gitea)) { - if (webhookPaths.has(route.publicPath)) throw new Error(`${configLabel} webhook publicPath must be unique: ${route.publicPath}`); - if (webhookPorts.has(route.remotePort)) throw new Error(`${configLabel} webhook frpc.remotePort must be unique: ${route.remotePort}`); - if (route.remotePort === gitea.app.publicExposure.frpc.remotePort) throw new Error(`${configLabel} webhook frpc.remotePort must differ from app public exposure port: ${route.remotePort}`); - webhookPaths.add(route.publicPath); - webhookPorts.add(route.remotePort); + for (const publicPath of webhookPublicPaths(gitea)) { + if (webhookPaths.has(publicPath)) throw new Error(`${configLabel} webhook publicPath must be unique: ${publicPath}`); + webhookPaths.add(publicPath); } + const ownerTarget = resolveTarget(gitea, gitea.sourceAuthority.webhookSync.hookReconcile.ownerTargetId); + const ownerSync = targetWebhookSync(gitea, ownerTarget); + const webhookRoute = gitea.app.publicExposure.routes.find((route) => route.path === `${ownerSync.publicPath}*`); + if (webhookRoute === undefined) throw new Error(`${configLabel}.app.publicExposure.routes must expose the owner webhook path ${ownerSync.publicPath}*`); + if (gitea.app.publicExposure.routes.at(-1)?.path !== "*") throw new Error(`${configLabel}.app.publicExposure.routes must end with the Gitea fallback route`); const repoKeys = new Set(); for (const repo of gitea.sourceAuthority.repositories) { if (repoKeys.has(repo.key)) throw new Error(`${configLabel}.sourceAuthority.repositories contains duplicate key ${repo.key}`); @@ -899,21 +896,19 @@ export function targetWebhookSync(gitea: GiteaConfig, target: GiteaTarget): Gite return { ...base, publicPath: target.webhookSync.publicPath, - frpc: target.webhookSync.frpc, }; } -export function webhookCaddyRoutes(gitea: GiteaConfig): Array<{ publicPath: string; remotePort: number }> { +export function webhookPublicPaths(gitea: GiteaConfig): string[] { const sync = gitea.sourceAuthority.webhookSync; if (!sync.enabled) return []; - const routes = new Map(); - routes.set(sync.publicPath, { publicPath: sync.publicPath, remotePort: sync.frpc.remotePort }); + const paths = [sync.publicPath]; for (const target of gitea.targets) { if (!target.enabled || target.webhookSync === null) continue; const targetSync = targetWebhookSync(gitea, target); - routes.set(targetSync.publicPath, { publicPath: targetSync.publicPath, remotePort: targetSync.frpc.remotePort }); + paths.push(targetSync.publicPath); } - return [...routes.values()]; + return paths; } function positiveInteger(obj: Record, key: string, path: string): number { diff --git a/scripts/src/platform-infra-gitea-payload.test.ts b/scripts/src/platform-infra-gitea-payload.test.ts index 361f6309..8907fc02 100644 --- a/scripts/src/platform-infra-gitea-payload.test.ts +++ b/scripts/src/platform-infra-gitea-payload.test.ts @@ -27,7 +27,6 @@ test("stdin file envelope materializes a payload larger than the current Gitea m repositories: JSON.stringify([{ key: "sentinel", branch: "master" }]), hookTopology: JSON.stringify({ version: 1, repositories: [] }), statusEvaluator: "print('ok')\n", - frpcToml: "serverAddr = '127.0.0.1'\n", }; const materializer = renderGiteaRemotePayloadMaterializer(payloads); const result = spawnSync("sh", ["-s"], { @@ -70,6 +69,8 @@ test("Gitea remote runner consumes materialized files instead of base64 environm expect(source).not.toContain("UNIDESK_GITEA_STATUS_EVALUATOR_B64"); expect(source).not.toContain("UNIDESK_GITEA_REPOS_B64"); expect(source).not.toContain("UNIDESK_GITEA_FRPC_TOML_B64"); + expect(source).not.toContain("UNIDESK_GITEA_FRPC_ENABLED"); + expect(source).not.toContain("frpc-rollout"); expect(source).toContain("kubectl apply --server-side --force-conflicts --dry-run=server"); expect(source).toContain("target server dry-run failed; manifest apply skipped"); expect(source.indexOf("kubectl apply --server-side --force-conflicts --dry-run=server")).toBeLessThan( diff --git a/scripts/src/platform-infra-gitea-payload.ts b/scripts/src/platform-infra-gitea-payload.ts index a73bab8f..c4788b6e 100644 --- a/scripts/src/platform-infra-gitea-payload.ts +++ b/scripts/src/platform-infra-gitea-payload.ts @@ -5,7 +5,6 @@ export interface GiteaRemotePayloads { repositories: string; hookTopology: string; statusEvaluator: string; - frpcToml: string; } interface GiteaRemotePayloadFile { @@ -19,7 +18,6 @@ const payloadFiles = (payloads: GiteaRemotePayloads): GiteaRemotePayloadFile[] = { role: "repositories", name: "repos.json", value: payloads.repositories }, { role: "hookTopology", name: "hook-topology.json", value: payloads.hookTopology }, { role: "statusEvaluator", name: "platform_infra_gitea_status_evaluator.py", value: payloads.statusEvaluator }, - { role: "frpcToml", name: "frpc.toml", value: payloads.frpcToml }, ]; function payloadMarker(file: GiteaRemotePayloadFile): string { diff --git a/scripts/src/platform-infra-gitea-remote.sh b/scripts/src/platform-infra-gitea-remote.sh index 340c5450..e5047774 100644 --- a/scripts/src/platform-infra-gitea-remote.sh +++ b/scripts/src/platform-infra-gitea-remote.sh @@ -80,26 +80,12 @@ run_apply() { if [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then kubectl create namespace "$UNIDESK_GITEA_NAMESPACE" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/namespace.out" 2>"$tmp/namespace.err" fi - if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then - kubectl -n "$UNIDESK_GITEA_NAMESPACE" create secret generic "$UNIDESK_GITEA_FRPC_SECRET_NAME" \ - --from-file="$UNIDESK_GITEA_FRPC_SECRET_KEY=$tmp/frpc.toml" \ - --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/frpc-secret.out" 2>"$tmp/frpc-secret.err" - frpc_rc=$? - else - frpc_rc=0 - : >"$tmp/frpc-secret.out" - if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ]; then - printf '%s\n' "dry-run: frpc secret apply skipped" >"$tmp/frpc-secret.err" - else - printf '%s\n' "frpc disabled" >"$tmp/frpc-secret.err" - fi - fi upsert_webhook_sync_secret webhook_secret_rc=$? timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side --force-conflicts --dry-run=server \ --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/server-dry-run.out" 2>"$tmp/server-dry-run.err" server_dry_run_rc=$? - if [ "$frpc_rc" -eq 0 ] && [ "$webhook_secret_rc" -eq 0 ]; then + if [ "$webhook_secret_rc" -eq 0 ]; then if [ "$server_dry_run_rc" -ne 0 ]; then apply_rc=1 : >"$tmp/apply.out" @@ -116,7 +102,7 @@ run_apply() { else apply_rc=1 : >"$tmp/apply.out" - printf '%s\n' "frpc or webhook secret apply failed; manifest apply skipped" >"$tmp/apply.err" + printf '%s\n' "webhook secret apply failed; manifest apply skipped" >"$tmp/apply.err" fi if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "statefulset/$UNIDESK_GITEA_STATEFULSET_NAME" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >"$tmp/rollout.out" 2>"$tmp/rollout.err" @@ -130,25 +116,7 @@ run_apply() { printf '%s\n' "rollout wait not requested" >"$tmp/rollout.err" fi fi - if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then - kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout restart "deployment/$UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME" >"$tmp/frpc-rollout.out" 2>"$tmp/frpc-rollout.err" - frpc_rollout_rc=$? - if [ "$frpc_rollout_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ]; then - timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "deployment/$UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >>"$tmp/frpc-rollout.out" 2>>"$tmp/frpc-rollout.err" - frpc_rollout_rc=$? - fi - else - frpc_rollout_rc=0 - : >"$tmp/frpc-rollout.out" - if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then - printf '%s\n' "dry-run: frpc rollout skipped" >"$tmp/frpc-rollout.err" - elif [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ]; then - printf '%s\n' "manifest apply failed; frpc rollout skipped" >"$tmp/frpc-rollout.err" - else - printf '%s\n' "frpc disabled" >"$tmp/frpc-rollout.err" - fi - fi - if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "0" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then + if [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then kubectl -n "$UNIDESK_GITEA_NAMESPACE" delete deployment "$UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME" --ignore-not-found >"$tmp/frpc-cleanup.out" 2>"$tmp/frpc-cleanup.err" frpc_cleanup_deploy_rc=$? kubectl -n "$UNIDESK_GITEA_NAMESPACE" delete secret "$UNIDESK_GITEA_FRPC_SECRET_NAME" --ignore-not-found >>"$tmp/frpc-cleanup.out" 2>>"$tmp/frpc-cleanup.err" @@ -163,10 +131,6 @@ run_apply() { : >"$tmp/frpc-cleanup.out" if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then printf '%s\n' "dry-run: frpc cleanup skipped" >"$tmp/frpc-cleanup.err" - elif [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ]; then - printf '%s\n' "frpc enabled" >"$tmp/frpc-cleanup.err" - else - printf '%s\n' "frpc cleanup skipped" >"$tmp/frpc-cleanup.err" fi fi if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then @@ -187,9 +151,9 @@ run_apply() { printf '%s\n' "webhook sync disabled" >"$tmp/webhook-rollout.err" fi fi - python3 - "$frpc_rc" "$webhook_secret_rc" "$server_dry_run_rc" "$apply_rc" "$rollout_rc" "$frpc_rollout_rc" "$webhook_rollout_rc" "$frpc_cleanup_rc" "$tmp/frpc-secret.out" "$tmp/frpc-secret.err" "$tmp/frpc-cleanup.out" "$tmp/frpc-cleanup.err" "$tmp/webhook-secret.out" "$tmp/webhook-secret.err" "$tmp/server-dry-run.out" "$tmp/server-dry-run.err" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" "$tmp/frpc-rollout.out" "$tmp/frpc-rollout.err" "$tmp/webhook-rollout.out" "$tmp/webhook-rollout.err" <<'PY' + python3 - "$webhook_secret_rc" "$server_dry_run_rc" "$apply_rc" "$rollout_rc" "$webhook_rollout_rc" "$frpc_cleanup_rc" "$tmp/frpc-cleanup.out" "$tmp/frpc-cleanup.err" "$tmp/webhook-secret.out" "$tmp/webhook-secret.err" "$tmp/server-dry-run.out" "$tmp/server-dry-run.err" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" "$tmp/webhook-rollout.out" "$tmp/webhook-rollout.err" <<'PY' import json, os, sys -frpc_rc, webhook_secret_rc, server_dry_run_rc, apply_rc, rollout_rc, frpc_rollout_rc, webhook_rollout_rc, frpc_cleanup_rc = [int(value) for value in sys.argv[1:9]] +webhook_secret_rc, server_dry_run_rc, apply_rc, rollout_rc, webhook_rollout_rc, frpc_cleanup_rc = [int(value) for value in sys.argv[1:7]] def text(path, limit=3000): try: return open(path, encoding="utf-8", errors="replace").read()[-limit:] @@ -197,7 +161,7 @@ def text(path, limit=3000): return "" dry = os.environ.get("UNIDESK_GITEA_DRY_RUN") == "1" payload = { - "ok": all(value == 0 for value in [frpc_rc, webhook_secret_rc, server_dry_run_rc, apply_rc, rollout_rc, frpc_rollout_rc, webhook_rollout_rc, frpc_cleanup_rc]), + "ok": all(value == 0 for value in [webhook_secret_rc, server_dry_run_rc, apply_rc, rollout_rc, webhook_rollout_rc, frpc_cleanup_rc]), "target": os.environ.get("UNIDESK_GITEA_TARGET_ID"), "route": os.environ.get("UNIDESK_GITEA_ROUTE"), "namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"), @@ -210,14 +174,12 @@ payload = { "networkPolicy": "allow-all", }, "steps": { - "frpcSecret": {"exitCode": frpc_rc, "stdoutTail": text(sys.argv[9]), "stderrTail": text(sys.argv[10])}, - "frpcCleanup": {"exitCode": frpc_cleanup_rc, "stdoutTail": text(sys.argv[11]), "stderrTail": text(sys.argv[12])}, - "webhookSyncSecret": {"exitCode": webhook_secret_rc, "stdoutTail": text(sys.argv[13]), "stderrTail": text(sys.argv[14])}, - "serverDryRun": {"exitCode": server_dry_run_rc, "stdoutTail": text(sys.argv[15]), "stderrTail": text(sys.argv[16])}, - "apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[17]), "stderrTail": text(sys.argv[18])}, - "rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[19]), "stderrTail": text(sys.argv[20])}, - "frpcRollout": {"exitCode": frpc_rollout_rc, "stdoutTail": text(sys.argv[21]), "stderrTail": text(sys.argv[22])}, - "webhookSyncRollout": {"exitCode": webhook_rollout_rc, "stdoutTail": text(sys.argv[23]), "stderrTail": text(sys.argv[24])}, + "legacyFrpcCleanup": {"exitCode": frpc_cleanup_rc, "stdoutTail": text(sys.argv[7]), "stderrTail": text(sys.argv[8])}, + "webhookSyncSecret": {"exitCode": webhook_secret_rc, "stdoutTail": text(sys.argv[9]), "stderrTail": text(sys.argv[10])}, + "serverDryRun": {"exitCode": server_dry_run_rc, "stdoutTail": text(sys.argv[11]), "stderrTail": text(sys.argv[12])}, + "apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[13]), "stderrTail": text(sys.argv[14])}, + "rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[15]), "stderrTail": text(sys.argv[16])}, + "webhookSyncRollout": {"exitCode": webhook_rollout_rc, "stdoutTail": text(sys.argv[17]), "stderrTail": text(sys.argv[18])}, }, "valuesPrinted": False, } diff --git a/scripts/src/platform-infra-gitea-render.ts b/scripts/src/platform-infra-gitea-render.ts index 71d02822..6525415c 100644 --- a/scripts/src/platform-infra-gitea-render.ts +++ b/scripts/src/platform-infra-gitea-render.ts @@ -279,14 +279,11 @@ export function renderApply(result: Record): RenderedCliResult const remote = record(result.remote); const steps = record(remote.steps); const serverDryRunStep = record(steps.serverDryRun); - const frpcStep = record(steps.frpcSecret); - const frpcCleanupStep = record(steps.frpcCleanup); + const legacyFrpcCleanupStep = record(steps.legacyFrpcCleanup); const webhookSecretStep = record(steps.webhookSyncSecret); const applyStep = record(steps.apply); const rolloutStep = record(steps.rollout); - const frpcRolloutStep = record(steps.frpcRollout); const webhookRolloutStep = record(steps.webhookSyncRollout); - const caddy = record(result.pk01Caddy); return rendered(result, "platform-infra gitea apply", [ "PLATFORM-INFRA GITEA APPLY", ...table(["TARGET", "NAMESPACE", "MODE", "OK"], [[stringValue(target.id), stringValue(target.namespace), stringValue(result.mode), boolText(result.ok)]]), @@ -295,14 +292,11 @@ export function renderApply(result: Record): RenderedCliResult "STEPS", ...table(["STEP", "EXIT", "DETAIL"], [ ["serverDryRun", stringValue(serverDryRunStep.exitCode), compactLine(stringValue(serverDryRunStep.stderrTail, stringValue(serverDryRunStep.stdoutTail)))], - ["frpc-secret", stringValue(frpcStep.exitCode), compactLine(stringValue(frpcStep.stderrTail, stringValue(frpcStep.stdoutTail)))], - ["frpc-cleanup", stringValue(frpcCleanupStep.exitCode), compactLine(stringValue(frpcCleanupStep.stderrTail, stringValue(frpcCleanupStep.stdoutTail)))], + ["legacy-frpc-cleanup", stringValue(legacyFrpcCleanupStep.exitCode), compactLine(stringValue(legacyFrpcCleanupStep.stderrTail, stringValue(legacyFrpcCleanupStep.stdoutTail)))], ["webhook-secret", stringValue(webhookSecretStep.exitCode), compactLine(stringValue(webhookSecretStep.stderrTail, stringValue(webhookSecretStep.stdoutTail)))], ["apply", stringValue(applyStep.exitCode), compactLine(stringValue(applyStep.stderrTail, stringValue(applyStep.stdoutTail)))], ["rollout", stringValue(rolloutStep.exitCode), compactLine(stringValue(rolloutStep.stderrTail, stringValue(rolloutStep.stdoutTail)))], - ["frpc-rollout", stringValue(frpcRolloutStep.exitCode), compactLine(stringValue(frpcRolloutStep.stderrTail, stringValue(frpcRolloutStep.stdoutTail)))], ["webhook-rollout", stringValue(webhookRolloutStep.exitCode), compactLine(stringValue(webhookRolloutStep.stderrTail, stringValue(webhookRolloutStep.stdoutTail)))], - ["pk01-caddy", stringValue(caddy.exitCode ?? (caddy.ok === false ? 1 : 0)), compactLine(stringValue(caddy.error, stringValue(caddy.status, stringValue(caddy.reason))))], ]), ...remoteErrorLines(result), "", diff --git a/scripts/src/platform-infra-gitea.ts b/scripts/src/platform-infra-gitea.ts index 0a7ffe78..6486e68a 100644 --- a/scripts/src/platform-infra-gitea.ts +++ b/scripts/src/platform-infra-gitea.ts @@ -7,16 +7,11 @@ import { CliInputError, type RenderedCliResult } from "./output"; import { capture, compactCapture, - fingerprintValues, parseJsonOutput, shQuote, } from "./platform-infra-ops-library"; -import { applyPk01CaddyBlock, escapeTomlString, prepareFrpcSecret, renderFrpcManifest } from "./platform-infra-public-service"; -import type { PublicServiceTarget, FrpcSecretMaterial } from "./platform-infra-public-service"; -import { applyPk01CaddySiteBlock, caddyManagedBlockMarkers } from "./pk01-caddy"; -import { deriveGiteaWebhookAttemptBudgetMs, deriveGiteaWebhookResponseHeaderTimeoutMs, renderGiteaWebhookCaddyHandle } from "./platform-infra-gitea-caddy"; +import { deriveGiteaWebhookAttemptBudgetMs, deriveGiteaWebhookResponseHeaderTimeoutMs } from "./platform-infra-gitea-caddy"; import { renderGiteaRemotePayloadMaterializer, summarizeGiteaRemotePayloads } from "./platform-infra-gitea-payload"; -import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets"; import { PAC_AUTOMATIC_DELIVERY_REFERENCE } from "./cicd-delivery-authority"; import { renderPlatformInfraGiteaDesiredFragments } from "./platform-infra-gitea-desired-fragments"; import { @@ -41,7 +36,6 @@ import { readGiteaConfig, resolveTarget, targetWebhookSync, - webhookCaddyRoutes, type GiteaAdminCredential, type GiteaConfig, type GiteaGithubCredential, @@ -92,7 +86,6 @@ interface MirrorSecrets { interface MirrorRemoteParams { repos?: GiteaMirrorRepository[]; secrets?: MirrorSecrets; - frpcSecret?: FrpcSecretMaterial; } export async function runPlatformInfraGiteaCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { @@ -252,19 +245,15 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise check.ok)) return { ok: false, action: "platform-infra-gitea-apply", mode: "policy-blocked", mutation: false, policy }; - const frpcSecret = prepareGiteaFrpcSecret(gitea, target); const targetRepos = repositoriesForTarget(gitea, target); const secrets = gitea.sourceAuthority.webhookSync.enabled && !options.dryRun ? ensureMirrorSecrets(gitea, target, targetRepos, true, true, true) : undefined; - const remote = remoteScript("apply", gitea, target, manifest, options, { frpcSecret, secrets }); + const remote = remoteScript("apply", gitea, target, manifest, options, { secrets }); const result = await capture(config, target.route, ["sh"], remote.script); const parsed = parseJsonOutput(result.stdout); - const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && caddyExposureNeeded(gitea, target) - ? await applyGiteaCaddyBlock(config, gitea) - : { ok: true, skipped: true, reason: options.dryRun ? "dry-run" : "apply-not-ready" }; return { - ok: result.exitCode === 0 && parsed?.ok === true && record(caddy).ok !== false, + ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-gitea-apply", mode: options.dryRun ? "dry-run" : "confirmed", mutation: !options.dryRun, @@ -273,8 +262,12 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise } { - const frpcExposure = targetFrpcExposure(gitea, target); const sync = targetWebhookSync(gitea, target); const selectedRepositories = params.repos ?? repositoriesForTarget(gitea, target); const selectedCredentials = githubCredentialsForConsumers(gitea, target, selectedRepositories, false); @@ -1210,10 +1201,8 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-prefligh .map((credential) => `${credential.gitFetchCredential.secretRef.key}=${githubTokenEnvName(credential.gitFetchCredential.secretRef.key)}`) .join(","), UNIDESK_GITEA_NO_PROXY: gitea.sourceAuthority.githubProxy.noProxy.join(","), - UNIDESK_GITEA_FRPC_ENABLED: frpcExposure === null ? "0" : "1", - UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: frpcExposure?.frpc.deploymentName ?? gitea.app.publicExposure.frpc.deploymentName, - UNIDESK_GITEA_FRPC_SECRET_NAME: frpcExposure?.frpc.secretName ?? gitea.app.publicExposure.frpc.secretName, - UNIDESK_GITEA_FRPC_SECRET_KEY: frpcExposure?.frpc.secretKey ?? gitea.app.publicExposure.frpc.secretKey, + UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: gitea.app.publicExposure.legacyCleanup.deploymentName, + UNIDESK_GITEA_FRPC_SECRET_NAME: gitea.app.publicExposure.legacyCleanup.secretName, UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED: gitea.sourceAuthority.webhookSync.enabled ? "1" : "0", UNIDESK_GITEA_WEBHOOK_PUBLIC_URL: githubWebhookPublicUrl(gitea, target), UNIDESK_GITEA_WEBHOOK_PATH: sync.publicPath, @@ -1223,10 +1212,6 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-prefligh UNIDESK_GITEA_WEBHOOK_SERVICE_PORT: String(sync.bridge.httpPort), UNIDESK_GITEA_WEBHOOK_SECRET_NAME: sync.bridge.secretName, }; - if (params.frpcSecret !== undefined) { - env.UNIDESK_GITEA_FRPC_SECRET_NAME = params.frpcSecret.secretName; - env.UNIDESK_GITEA_FRPC_SECRET_KEY = params.frpcSecret.secretKey; - } if (params.secrets !== undefined) { env.UNIDESK_GITEA_ADMIN_USERNAME = params.secrets.adminUsername; env.UNIDESK_GITEA_ADMIN_PASSWORD = params.secrets.adminPassword; @@ -1243,7 +1228,6 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-prefligh repositories: JSON.stringify((params.repos ?? []).map((repo) => remoteRepoSpec(repo))), hookTopology: JSON.stringify(githubHookTopology(gitea)), statusEvaluator: readFileSync(statusEvaluatorFile, "utf8"), - frpcToml: params.frpcSecret?.frpcToml ?? "", }; return { script: `${exports}\n${renderGiteaRemotePayloadMaterializer(payloads)}\n${readFileSync(remoteScriptFile, "utf8")}`, @@ -1368,132 +1352,18 @@ function appSummary(gitea: GiteaConfig): Record { }; } -function publicServiceTarget(gitea: GiteaConfig, target: GiteaTarget): PublicServiceTarget { - return { - id: target.id, - route: target.route, - namespace: target.namespace, - replicas: 1, - publicExposure: targetPublicExposure(gitea, target), - }; -} - -function publicExposureEnabled(gitea: GiteaConfig, target: GiteaTarget): boolean { - return gitea.app.publicExposure.enabled && target.publicExposureEnabled; -} - -function targetPublicExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] { - const frpcExposure = targetFrpcExposure(gitea, target); - return frpcExposure === null ? { ...gitea.app.publicExposure, enabled: false } : frpcExposure; -} - -function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial | undefined { - const exposure = targetFrpcExposure(gitea, target); - if (exposure === null) return undefined; - const base = prepareFrpcSecret({ - secretRoot: gitea.app.publicExposure.secretRoot, - exposure, - sourcePathRedactor: redactRepoPath, - parseEnvFile, - requiredEnvValue, - readTextFile, - }); - const sync = targetWebhookSync(gitea, target); - if (!sync.enabled || !publicExposureEnabled(gitea, target)) return base; - const extraProxy = [ - "[[proxies]]", - `name = "${escapeTomlString(sync.frpc.proxyName)}"`, - 'type = "tcp"', - `localIP = "${escapeTomlString(sync.bridge.serviceName)}.${escapeTomlString(target.namespace)}.svc.cluster.local"`, - `localPort = ${sync.bridge.httpPort}`, - `remotePort = ${sync.frpc.remotePort}`, - "", - ].join("\n"); - const frpcToml = `${base.frpcToml.trim()}\n\n${extraProxy}`; - return { - ...base, - frpcToml, - fingerprint: fingerprintValues({ frpcToml }, ["frpcToml"]), - }; -} - -function targetFrpcExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] | null { - if (publicExposureEnabled(gitea, target)) return gitea.app.publicExposure; - if (target.webhookSync === null || !gitea.sourceAuthority.webhookSync.enabled) return null; - const sync = targetWebhookSync(gitea, target); - return { - ...gitea.app.publicExposure, - enabled: true, - frpc: { - ...gitea.app.publicExposure.frpc, - proxyName: sync.frpc.proxyName, - remotePort: sync.frpc.remotePort, - localIP: `${sync.bridge.serviceName}.${target.namespace}.svc.cluster.local`, - localPort: sync.bridge.httpPort, - }, - }; -} - - -function caddyExposureNeeded(gitea: GiteaConfig, target: GiteaTarget): boolean { - return publicExposureEnabled(gitea, target) || (gitea.sourceAuthority.webhookSync.enabled && target.webhookSync !== null); -} - -async function applyGiteaCaddyBlock(config: UniDeskConfig, gitea: GiteaConfig): Promise> { - const exposure = gitea.app.publicExposure; - const ingressRetry = gitea.sourceAuthority.webhookSync.ingressRetry; - const webhookHandles = webhookCaddyRoutes(gitea) - .sort((left, right) => right.publicPath.length - left.publicPath.length) - .map((route) => renderGiteaWebhookCaddyHandle( - route, - ingressRetry, - gitea.sourceAuthority.webhookSync.responseBudgetMs, - exposure.pk01.responseHeaderTimeoutSeconds, - )) - .join("\n\n"); - if (webhookHandles === "") return await applyPk01CaddyBlock(config, "gitea", exposure); - const siteBlock = `${exposure.dns.hostname} { -${webhookHandles} - - handle { - reverse_proxy 127.0.0.1:${exposure.frpc.remotePort} { - transport http { - response_header_timeout ${exposure.pk01.responseHeaderTimeoutSeconds}s - } - } - } -}`; - return await applyPk01CaddySiteBlock(config, "gitea", exposure, siteBlock, caddyManagedBlockMarkers("gitea")); -} - - function publicExposureSummary(gitea: GiteaConfig): Record { const exposure = gitea.app.publicExposure; return { enabled: exposure.enabled, publicBaseUrl: exposure.publicBaseUrl, + upstream: exposure.upstream, + healthPath: exposure.healthPath, internalReadUrlPolicy: "k3s-consumers-use-gitea-http-svc-not-public-url", dns: exposure.dns, - frpc: { - deploymentName: exposure.frpc.deploymentName, - remotePort: exposure.frpc.remotePort, - localIP: exposure.frpc.localIP, - localPort: exposure.frpc.localPort, - tokenSourceRef: exposure.frpc.tokenSourceRef, - valuesPrinted: false, - }, - pk01: exposure.pk01, - }; -} - -function frpcSecretSummary(secret: FrpcSecretMaterial | undefined): Record { - if (secret === undefined) return { enabled: false, skipped: true, reason: "target-public-exposure-disabled", valuesPrinted: false }; - return { - sourceRef: secret.sourceRef, - sourcePath: secret.sourcePath, - secretName: secret.secretName, - secretKey: secret.secretKey, - fingerprint: secret.fingerprint, + routes: exposure.routes, + authority: "nc01-public-edge", + legacyCleanup: exposure.legacyCleanup, valuesPrinted: false, }; } @@ -1698,7 +1568,6 @@ function webhookSyncSummary(gitea: GiteaConfig, target: GiteaTarget): Record; const repositories = config.repositories as Array>; const consumers = config.consumers as Array>; - const selfmediaRepositories = repositories.filter((item) => item.url === "https://gitea.pikapython.com/mirrors/pikainc-selfmedia"); + const selfmediaRepositories = repositories.filter((item) => item.url === "https://gitea.hwpod.com/mirrors/pikainc-selfmedia"); expect(selfmediaRepositories).toHaveLength(1); const development = consumers.find((item) => item.id === "selfmedia-nc01"); const production = consumers.find((item) => item.id === "selfmedia-production-nc01"); diff --git a/scripts/src/platform-infra/entry.ts b/scripts/src/platform-infra/entry.ts index d8c1f529..59b2b648 100644 --- a/scripts/src/platform-infra/entry.ts +++ b/scripts/src/platform-infra/entry.ts @@ -520,7 +520,7 @@ export function platformInfraHelp(): unknown { "bun scripts/cli.ts platform-infra pipelines-as-code help platform-bootstrap", "bun scripts/cli.ts platform-infra pipelines-as-code help compatibility-diagnostics", ], - 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 read-only Gitea + Pipelines-as-Code observation for migrated consumers. Public services use PK01 Caddy/FRP or YAML-declared PK01 Caddy upstreams rather than Kubernetes Ingress, NodePort, or LoadBalancer.", + 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 read-only Gitea + Pipelines-as-Code observation for migrated consumers. Public services use their YAML-declared managed edge rather than Kubernetes Ingress, NodePort, or LoadBalancer.", target, codexPool: { usage: [