# INFRA-F71 — Gitea 自托管发布 CI Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 把 npm 包发布链从 GitHub Actions（受 private 仓库计费额度拦截）迁到自托管 Gitea Actions runner，使发版不再依赖 GitHub 额度；并补上本次漏掉的 `playground-dist/` 重建，让 registry 与设计站版本号一致落到 `1.1.0`。

**Architecture:** 新增 `.gitea/workflows/publish.yml`（`audit-matrix` = vitest + `prepublishOnly` on Node 20/22 → `publish`），认证用 Gitea secret 里的 PAT（`GH_PACKAGES_TOKEN`，scope `write:packages`）——目标 registry 仍是 GitHub Packages，计费只拦 runner、不拦 token publish。**render-gate 不进这条 CI**（runner 无 root，见下方"关键前提修正"），改由 `scripts/release.mjs` 在切 tag 前强制执行。同时把 `.github/workflows/publish.yml` 的 tag trigger 降级为 `workflow_dispatch`-only，消除双 remote 双 CI 重复发布。

**Tech Stack:** Gitea Actions (act_runner)、pnpm 10.28.2、Node 20/22、changesets、GitHub Packages registry (`https://npm.pkg.github.com`)。

---

## ⚠️ 关键前提修正（2026-07-28，本计划初版写错，已改）

**初版把 render-gate 平移进 Gitea workflow，那是错的。** `.gitea/workflows/pr-checks.yml` 尾部早有现成结论：

> INFRA-F40 注：数值 render-verification gate 需 chromium（系统库），这台 Gitea runner **以普通用户跑、无 root 装不了系统库** → 该 gate 改在 GitHub `publish.yml` 的 render-gate job 跑

即：**当初正是因为这台 runner 没 root，才把 render-gate 放到 GitHub 去的**。把发布搬来 Gitea，等于顺手删掉"不发漂移包"那道闸。

**处置（已实施，Task 0b）**：把 render-gate 前移成 `scripts/release.mjs` 切 tag 前的硬闸，无 skip flag。tag 在 owner 本机切，因此这个位置**严格早于**任何 CI 闸；并且正面修掉 v1.1.0 那次"render-gate 只是事后补跑才发现 6 个 Table drift"的洞。

**结论**：Gitea workflow 里没有 render-gate job 不是削闸，是换了个更早的执行点。**不要**在 Gitea workflow 里加 `playwright install --with-deps`，它必挂。

---

## Global Constraints

- 包名 `@nancyzeng0210/tvu-design-system`，registry `https://npm.pkg.github.com`，scope `@nancyzeng0210`。
- pnpm 版本锁 `10.28.2`（与两侧 workflow 一致，不得漂移）。
- **Gitea workflow 的 action 版本一律沿用 `pr-checks.yml` 已实证可用的那套**（`actions/checkout@v4` / `pnpm/action-setup@v3` / `actions/setup-node@v4`），**不要**照抄 GitHub 侧的 `@v5`（在这台 act_runner 上未经验证）。
- **render-gate 不得进 Gitea CI，也不得因此被删**——见上方"关键前提修正"。它的执行点是 `scripts/release.mjs`。
- `audit:status-consistency` 是 strict 闸：`docs/STATUS.md` / `CHANGELOG.md` / `package.json` 三处版本必须自洽。
- 版本号字面值只允许出现在 `docs/STATUS.md` §当前版本（de-mirror 闸 `no-version` mode 强制）。
- `playground-dist/` 与 `react-pilot/dist/` 是**故意 track 进 git 的构建产物**（INFRA-F53 plan B 兜底），不得 un-track；提交需 `VISUAL_COMMIT_APPROVED=1` 且 owner 先肉眼过站点。
- Executor 不得自行 commit/push：跑完只报 diff + 命令实际 stdout，由 plan owner 复审 + owner ack 后才提交。

---

## 前置

**P-1 Gitea runner 活性 — ✅ 已确认（2026-07-28）**
owner 贴出 pr-checks run 截图，trace 里含 `/data/nancy_zeng/.cache/act/eb8672bd…/hostexecutor/`，且跑完 103 个 test file / 843 test → **runner 活着、能装依赖、能跑闸**。
当时 run 是**红的**，但根因与 runner 能力无关（见 Task 0a），已修。

**P-2 建 PAT + 配 Gitea secret — ✅ DONE（2026-07-29，owner 建 classic PAT）**
GitHub → Settings → Developer settings → PAT (classic)，勾 `write:packages` **+ `read:packages`** → Gitea repo → Settings → Actions → Secrets 新增 `GH_PACKAGES_TOKEN`。有效期建议 90 天并记进日历。
> Gitea 没有 GitHub 那种自动注入的 `GITHUB_TOKEN`，这步无法省略；PAT 过期会导致发版 401 且没有别的症状。

> **⚠️ 必须存在「仓库级」，不是「用户级」**（2026-07-29 实证）：owner 首次存到了**用户设置** → Actions → 密钥，而这个仓库归属 **org `ux-team`**（`GET /api/v1/orgs/ux-team` → `{'id': 3, 'username': 'ux-team'}`），Gitea 按 repo → repo-owner(=org) 查 secret，个人名下那份取不到（官方 secrets 文档只写了 repo 覆盖 org，**完全没提用户级如何参与解析**）。
> **核验法（别只看 UI）**：`GET /api/v1/repos/ux-team/tvu-design-system/actions/secrets` 应返回 `[('GH_PACKAGES_TOKEN', '<created_at>')]`；返回 `[]` 就是不在这层（用户级那层 `/user/actions/secrets` 在 1.22.6 是 404，读不到 = 无法核验，这本身就是不该用它的理由）。

> **不要拿 `gh auth token` 那份顶替（2026-07-28 评估）**：本机 gh CLI 的 token scope 确实已含 `write:packages`，技术上能发。但它是 `gho_` OAuth token —— 重新 `gh auth login` 即轮换，Gitea secret 里那份**静默失效且症状只有 401**（只在下次发版时暴露，那时人正在等发版）；且它带的是登录会话全套 scope，等于把万能钥匙落在 CI 里。classic PAT 的到期至少是可进日历的**可预期**事件，scope 也最小。「先用 gh token 发、之后补 PAT」实际会变成技术债。
> `read:packages` 勾上的额外好处：本机配 `~/.npmrc` 后 Task 3 Step 3 的 `npm view` 一路也能用，registry 核验不只剩 `gh api` 单路。

---

### Task 0a: `prepare` 加 `build:wc` — ✅ SHIPPED `39c75d7b`

**根因**：`tests/demo-renderer-react.test.ts:26` 的 `import '@tvu/wc'` 被 `vite.config.ts:61` alias 到 `./dist-wc/tvu-web-components.js`；`dist-wc/` 是 gitignored、只由 `pnpm build:wc` 产出，而该脚本**既不在 `prepare` 也不在 `test`** 里。任何 fresh checkout（Gitea CI / 新 worktree / 新 clone）都没有它 → 那个 suite 挂；本地靠一份手工建的历史 `dist-wc/` 蒙过。

自 2026-07-14（`91f9583f`，D2 PoC 加的这个 test）起一直红。vitest **只在** Gitea pr-checks 跑（`ci.yml` 没有 vitest job），而 GitHub Actions 7 月中就被计费拦 → 两周无人发现。

**修法选择**：改 `prepare` 而非改 workflow —— `dist-wc/` 与 `dist/`、`dist/tokens`、`dist/icons` 同属 `prepare` 已生成的 gitignored 产物，漏在外面是历史遗漏；只补 pr-checks.yml 会留着"新 worktree 照样踩"的坑。

**实证**：`rm -rf dist-wc` → 复现 CI 完全一致的错误签名；`pnpm prepare` → `dist-wc/` 重建（6.4M）；`pnpm test` → `93 passed | 10 skipped (103)` / 857 tests / 0 failed（CI 原为 `1 failed | 92 passed` / 843 test，差值正是该 suite 的 14 个）。代价 `prepare` 14.28s → 16.39s。

---

### Task 0b: render-gate 前移成切 tag 硬闸 — ✅ SHIPPED `d1ba023c`

`scripts/release.mjs` 在 preflight 之后、`changeset:version` **之前**插入 blocking render gate（失败不会留下半套版本 bump）；无 skip flag（留后门就等于恢复它要替代的那种"靠人记"失败模式）。

该闸会重写两个 **tracked** report（`docs/internal/_generated/render-verification-report.md`、`figma-data/normalized/render-verification.report.json`），已纳入 release commit 的 `git add` —— 随版本发出的报告应当描述该版本；反过来还原会在"drift 仍在 baseline 内但分类构成变了"时静默丢掉真实内容变化（闸只在**超过** baseline 时失败）。

**实证**：`pnpm test:render-verification` → 1 passed (1.5m)，manifest 930 entries；`pnpm audit:render-drift-gate` → PASS，`A_TRUE_DRIFT_CANDIDATE=0 ≤ baseline 0`，full-tree node mismatches=0。

---

### Task 1: `.gitea/workflows/publish.yml` — ✅ DONE（含两次修正）

**Files:** Create `.gitea/workflows/publish.yml`

结构：`audit-matrix`（Node 20+22 矩阵：`pnpm test` + `pnpm run prepublishOnly`）→ `publish`（needs audit-matrix）。触发：tag push `v*.*.*` + `workflow_dispatch`（布尔输入 `confirm_publish`，缺省 `false` = 只跑 gates 干跑）。

**刻意的两处偏离 GitHub 版**：
1. **无 render-gate job** —— 见"关键前提修正"。
2. **加了 `pnpm test`** —— 发布路径此前从不跑单元测试（`prepublishOnly` 全是 audit/lint，`ci.yml` 无 vitest job），等于"发版"是唯一不验单测的动作。这台 runner 已实证跑得动，就补上。

- [x] **Step 1: 写 workflow 文件**（已完成）
- [x] **Step 2: YAML 结构校验**

Run:
```bash
JS=node_modules/.pnpm/js-yaml@4.1.1/node_modules/js-yaml
node -e "const y=require('$PWD/'+'$JS'),f=require('fs');const d=y.load(f.readFileSync('.gitea/workflows/publish.yml','utf8'));const on=d.on??d[true];console.log(Object.keys(on),Object.keys(d.jobs),d.jobs.publish.needs)"
```
Expected（**已过时** —— `workflow_dispatch` 后来因 1.22.6 的连坐 bug 删除，见 Task 3 偏离说明）。**当前实测值**：`triggers: [ 'push' ] | push keys: [ 'tags' ] | tags: [ 'v*.*.*' ] | jobs: [ 'audit-matrix', 'publish' ] | needs: [ 'audit-matrix' ] | if: github.event_name == 'push'`，且 `Unit tests (vitest)` step 带 `if: matrix.node-version == '22'`。
> 注意：`require('js-yaml')` 在仓库根直接 require 会 MODULE_NOT_FOUND（pnpm 硬链接布局），必须走 `.pnpm` 实路径；`python3 -c "import yaml"` 本机也没有。

- [x] **Step 3: ~~owner dispatch 干跑~~ — ❌ 不可执行（Gitea 1.22.6 没有 dispatch，见 Task 3 偏离说明）。本 host 上不存在干跑环节。**

> **旁证（2026-07-28，非本 workflow 但同 runner 同 gates）**：Gitea `pr-checks` **#623（`39c75d7b`）/ #624（`e4c7f047`）均绿**，修复前的 **#622 红** —— Task 0a 的 `dist-wc` 根因确认闭环，这台 runner 跑得动 vitest + prepublishOnly。干跑若仍红，先怀疑 publish.yml 自身（Node 20 差异 / secret 未配），不必再回头查 dist-wc。

- [x] **Step 4: 判读结果 — 命中了本节预判的第三条**

- ~~全绿 → 进 Task 3。~~
- ~~`Unit tests (vitest)` 失败 …~~
- **`audit-matrix` 在 Node 20 失败但 22 绿 → 差异来自 Node 版本** —— ✅ **正是实际发生的**（run #627）。按"照报错定位、不盲改闸"执行：定位到 3 个脚本的 Node≥22 守卫 → vitest 限 Node 22、`prepublishOnly` 保持双 Node，**没有**改 `engines`、**没有**砍 Node 20 矩阵。

---

### Task 2: 降级 GitHub tag trigger + 文档 owner-of-record — ✅ 代码+文档已改

- [x] `.github/workflows/publish.yml` 的 `on:` 降为 `workflow_dispatch` only，顶部写清"⚠️ 别恢复 tag trigger"及其原因（双 remote 双发布 → 409）。`publish` job 的 `if: github.event_name == 'push'` 保持不动 —— 去掉 tag trigger 后该条件恒 false，dispatch 只跑闸、永不发包，正是 fallback 语义。实测 `triggers: [ 'workflow_dispatch' ]`。
- [x] `docs/DEPLOY.md` 交付链表格 npm 行改指 `.gitea/workflows/publish.yml` + Owner-of-record 改 `CI (Gitea, self-hosted)`，并补一段说明两条链现在同在内网 host 但仍相互独立。
- [x] `docs/RELEASING.md` 四处：pre-release checklist 的 render-gate 项改成"`pnpm release` 自己强制、无需手跑"；Step 3 的 "CI auto-publishes" 注明是 Gitea；§Audit gate policy 重写 render-gate 段落 + 说明 vitest 已进发布路径；§Troubleshooting 替换 401 行（`GH_PACKAGES_TOKEN`）并新增 runner 离线 / `pnpm release` 撞 render-gate 两行。
- [x] `pnpm run prepublishOnly` → EXIT=0（含 `audit:doc-sync` / `audit:stale-anchors`，证明新增跨文件链接有效）。

---

### Task 3: 用 Gitea 链实际发布 1.1.0 — ✅ **DONE 2026-07-29T01:48:27Z**

> **⚠️ 本节原设计（dispatch 干跑 → dispatch 实发 → 不动 tag）在这台 host 上不可执行，实际走的是 tag-push。**
> 三条与原计划的偏离及其实证根因，全部登记在 backlog INFRA-F71「首次实战踩到的三坑」：
> ① Gitea **1.22.6** 无 `workflow_dispatch`（1.23.0 才有）**且它在 `on:` 里会连坐压死 `push` 触发**（go-gitea#32142 / forgejo#4789）→ 删 dispatch 块（`acedd2ec`）；本 host 永久无干跑能力。
> ② `pnpm test` 在 Node 20 必红（3 个 audit 脚本需 Node≥22 原生 TS 类型剥离）→ vitest 限 Node 22（`0f7fd8aa`）。
> ③ **tag 必须前移**：`v1.1.0` 原指 `f0ef9174`，那个 commit 里没有 `publish.yml`（`e4c7f047` 才加），重推旧 tag 永远不会触发 —— 原计划「不动 tag」的前提被证伪。前移的语义安全性已由 `git diff f0ef9174 HEAD` 证明（只有 CI/docs/release 脚本 + `prepare` 加 `build:wc`，`src/` 零改动、版本号不变）。最终 tag → `0f7fd8aa`。
> **触发方式**：Gitea 靠 **tag create 事件**，force-update 未必产生 → 用「删远端 tag + 重新 push」，输出必须是 `* [new tag]`。

- [x] **Step 1: 确认待发版本与 tag 就位 — ✅ 已核验 2026-07-28**

Run:
```bash
node -p "require('./package.json').version"
git ls-remote --tags origin | grep v1.1.0
find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md'
```
Expected: `1.1.0`；tag 指向 `f0ef9174…`；changeset 列表**为空**（否则 publish job 的 consumed check 会 fail fast）。

**实测**：`1.1.0` · `f0ef917474673307b09d8e1dfb29ab653c817dd2	refs/tags/v1.1.0` · changeset 输出为空 —— 三项全就位。发布前 registry 基线见 Step 3 命令的实测行。

- [x] **Step 2: ~~owner dispatch~~ → 改为 tag-push（见上方偏离说明）**

> 故意不用「删 tag 再 push」触发：`v1.1.0` 已因 render-gate 修复 force-push 过一次，再做 tag 手术纯属增加风险。

- [x] **Step 3: 核验 registry 实物 — ✅ API 路已验，人眼路待 owner**

Run（**本机用这条**——`npm view --registry=https://npm.pkg.github.com` 在本机恒 401，因为**没有 `~/.npmrc`**；401 ≠ "没发成功"）:
```bash
gh api "/user/packages/npm/tvu-design-system/versions" --jq '.[] | "\(.name) \(.created_at)"'
```
Expected: 列表含 `1.1.0`。

**发布前基线实测（2026-07-28）**：`1.0.0 2026-07-20T10:33:19Z` 为最新，14 个版本里**无 `1.1.0`** —— 确认发布尚未发生，publish 后此列表新增 `1.1.0` 即为真实变化证据（非"本来就有"）。

**发布后实测（2026-07-29 09:48:45 CST 首次检出）**：`1.1.0 2026-07-29T01:48:27Z` 出现在列表首位 → 前后差分成立。版本页 `https://github.com/users/NancyZeng0210/packages/npm/tvu-design-system/1076662762`。**Packages 页人眼核验 = owner 待办**（硬纪律未免除）。
> PAT 勾了 `read:packages` 并配进 `~/.npmrc` 后，`npm view` 那一路可恢复，届时改回两路命令交叉。

外加**必须**人工看一眼 `https://github.com/NancyZeng0210/TVU-Design-System/packages` 确认 `1.1.0` 实物存在。
> 硬纪律：`tag pushed + CI green ≠ publish 成功`。v0.1.0 / v0.1.1 曾 silent fail 1-2 周无人发现。

- [x] **Step 4: 未触发（无 409）**

说明该版本号已存在于 registry。**不要 bump 版本号绕过**——先用 Step 3 核实 registry 真实状态；若确实已发，那是"已发布、只是 STATUS 记录错了"，直接走 Task 5 改记录。

---

### Task 4: 重建 `playground-dist/` 让设计站显示 1.1.0 — ✅ **DONE**

**这是设计站版本号落后的唯一原因，与 CI / 计费全都无关。** 站点的版本菜单 / changelog 是 build time 经 `?raw` 把 `package.json` + `CHANGELOG.md` 烤进 bundle 的；host 侧自建必失败并回落到 committed 产物（INFRA-F53 plan B），所以**只有 commit 进去的 `playground-dist/` 决定线上内容**。v1.1.0 release 漏了这步（[`RELEASING.md`](../../RELEASING.md) Step 4）。

- [x] **Step 1: 重建** — `pnpm build` — ✅ exit 0（2026-07-28）
- [x] **Step 2: 验证新 bundle 真烤进了新版本 — ✅ 已实证**

```bash
MAIN=$(grep -oE 'assets/index-[^"]+\.js' playground-dist/index.html | head -1)
echo "bundle=$MAIN"; grep -c "1\.1\.0" "playground-dist/$MAIN"
```
Expected: bundle hash **变了**（旧值 `index-BW03GW5S.js`）；`grep -c` ≥ 1。

**实测**：`bundle=assets/index-DcOrz5Uw.js`（hash 已变）· `grep -c` = 1。两处实体确认：
- 版本号 → bundle 内 `gp="1.1.0",I8={version:gp}`
- changelog → bundle 内 `bp='# Changelog\n\n## 1.1.0\n\n### Minor Changes\n\n- 7edcff6: Add \`Form\` validation engine (APID-02)…`

`react-pilot/dist` 同批重建（新 `assets/index-DEcsKu6J.js`）。工作树共 290 个文件待提交。
> **别拿 `ChangelogPage-*.js` chunk 验**：changelog 是 `playground/docs/docs-meta.ts` 用 `?raw` import 的，会被打进**主 chunk**，那个 page chunk 里 grep `1.1.0` = 0 属正常，不是漏烤。

- [x] **Step 3: owner 肉眼过站 — ✅ 已过（2026-07-28，本地 4290 静态站，版本菜单 1.1.0 确认）**

```bash
cd playground-dist && python3 -m http.server 4290
```
开 `http://127.0.0.1:4290/#/changelog` 确认版本菜单 + changelog 顶条正确、无破图。

- [x] **Step 4: 已提交 `63b451df`**（`VISUAL_COMMIT_APPROVED=1`；⚠️ 踩坑：`git commit <pathspec>` **不收 untracked 文件** —— 首次提交只进了删除/修改，新 bundle 全漏，靠 `git add` + `--amend` 修正，并用 `git cat-file -e HEAD:<bundle>` 核验 index.html 引用的产物真在 tree 里）

```bash
VISUAL_COMMIT_APPROVED=1 git commit -m "chore(docs): rebuild playground-dist for v1.1.0 (RELEASING Step 4, missed at release time)" playground-dist react-pilot/dist
git push origin master
```

- [x] **Step 5: 核验线上 — 一开始不符，查出独立 bug（backlog INFRA-F72）后已修复**

```bash
curl -sI https://product-demo.tvustream.com/tvu-design-system/playground-dist/index.html | grep -i last-modified
curl -s  https://product-demo.tvustream.com/tvu-design-system/playground-dist/index.html | grep -oE 'assets/index-[^"]+\.js'
```
Expected: `Last-Modified` 是今天（不再是 `Fri, 24 Jul 2026 09:22:10 GMT`）；bundle 名 == Step 2 的新 hash。
> 若 Last-Modified 仍冻结 → 是 stale **deploy** 而非 stale artifact，按 [`DEPLOY.md`](../../DEPLOY.md) §Troubleshooting 步骤 3 处理（需 host 访问权）。

**实际发生的是第三种情况（DEPLOY.md 原二分法没覆盖，已补）**：`Last-Modified` 是新的，但 `index.html` 指向**旧** bundle，而**新** bundle 已在主机上且与本地字节级一致 → 主机 deploy checkout 被自己的 untracked 构建残留卡死 `git pull`，HEAD 停在 `e4c7f047`，兜底 `git checkout --` 忠实恢复了那个 commit 里的旧产物。全过程 + 修复（owner 授权走 **Gitea runner 跳板**，因 runner 是 hostexecutor = 主机 shell）记在 backlog **INFRA-F72**。
**最终实测**：线上 bundle = `assets/index-DcOrz5Uw.js`、`gp="1.1.0"`、与本地 sha256 前 24 位同为 `de4d53b9c9c347e5249f7c3b`；react-pilot demo = `assets/index-DEcsKu6J.js`；`Last-Modified: Wed, 29 Jul 2026 01:50:36 GMT`。主机侧 round-3 复核：`HEAD 0f7fd8aa` / 落后 0 / 产物目录外仅 2 个无害 `??`。

---

### Task 5: 记录收口 — ✅ **DONE（本 commit）**

- [x] `docs/STATUS.md` §当前版本（已改；Packages 页人眼核验仍列 owner 待办，未代签）：`⛔ NOT PUBLISHED` → `✅ published & verified`，写清 ① 发布经**自托管 Gitea CI**（INFRA-F71）非 GitHub CI ② registry 实证时间 + Packages 页已亲验 ③ `playground-dist/` 已重建、设计站与 registry 一致。保持 STATUS / CHANGELOG / package.json 三角自洽。
- [x] `docs/internal/backlog.md` 新增 **INFRA-F71**（+ 顺带新增 **INFRA-F72** deploy 链回滚；STATUS Active 计数 10→12）（ID 已 grep `docs/` + `.claude/` 全部 `INFRA-F\d+` 确认最大为 70）。要点：根因=发布链单点依赖 GitHub Actions 计费额度；解=自托管 publish workflow + PAT secret + render-gate 前移；遗留=**PAT 到期需轮换**、Gitea 成为发布单点（GitHub dispatch-only 为 fallback）。字段须符合 schema（有 pre-commit validator）。
- [x] `pnpm run prepublishOnly` 全绿后提交，`git ls-remote` 双 remote 核验落地。

**顺带纳入本次收口 commit 的 4 个 audit report**（上个 session 跑 prepublishOnly 的残留，非本轮产物、非并行 session）：`docs/internal/published-vs-code-audit.md` · `figma-data/normalized/published-vs-code.audit.json` · `figma-data/normalized/tokenized-diff-report.json` · `figma-data/normalized/translation-completeness.audit.json`。内容是**对的**、该提交而非还原——生成时间更到 `2026-07-28`，新增 `⚠️ Code-only: Form / Icon`（= APID-02 Form + Icon 升 public canonical 的真实结果，非回归；canonical 导出 31→33），另补一条 `canonical-exempt.json → Icon` 的 divergence verifyHint 注解。

---

## 已识别风险

| 风险 | 概率 | 后果 | 处置 |
|---|---|---|---|
| PAT 过期导致某次发版 401 | 中（时间维度必然） | 发版中断且无其他症状 | RELEASING §Troubleshooting 已加行；backlog entry 记轮换；建议 PAT 有效期进日历 |
| Gitea 成为发布单点（host 挂了发不了版） | 低 | 发版中断 | GitHub workflow 保留 dispatch-only fallback（额度恢复后可跑闸；真要发包需临时恢复 tag trigger 或本地发） |
| render-gate 因不在 CI 而被绕过 | 低 | 发漂移包 | `release.mjs` 无 skip flag；唯一绕法是手工 `git tag`，属明知故犯 |
| act_runner 离线 | 低 | job 不启动 | RELEASING §Troubleshooting 已加行（需 host 访问权重启） |
| 双 CI 重复发布 409 | 已消除 | — | Task 2 |
