From 0414afd31947f7597fdf5420835cd303ad56daab Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 09:25:14 +0000 Subject: [PATCH 1/8] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20fusion-api=20?= =?UTF-8?q?=E4=B8=8E=20fusion-ui=20=E5=90=88=E5=B9=B6=E4=B8=BA=20monorepo?= =?UTF-8?q?=20=E7=9A=84=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评估两仓合并可行性并给出分阶段方案。目标为新建 fusion 仓库, 两个应用分别下沉到 apps/api 与 apps/ui。 核心结论:代码层零耦合(505 个 .py 与 522 个 .ts/.tsx 无交集), 成本集中在约 3600 行的发布流水线。deploy.yml 的 2553 行中 deploy-dev 单 job 占 2339 行,其中四个步骤的内联 shell 合计 1757 行,当前不可本地执行也不可单测。 方案分五个 Task:先搬迁(CI 只改路径并加 paths 过滤), 再把 shell 抽到 ops/deploy 使其可测,之后才做参数化合并与 可复用 workflow。搬迁与重写不得在同一次变更内完成, 以保证发布故障可二分定位。同时修正合并后 "回滚到 SHA X" 语义失效的问题,镜像 tag 改为 -。 Co-Authored-By: Claude --- .../plans/2026-08-31-monorepo-merge.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-31-monorepo-merge.md diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md new file mode 100644 index 00000000..945be094 --- /dev/null +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -0,0 +1,130 @@ +# Fusion Monorepo 合并实施计划 + +> Spec: 本文件自带依据章节,无独立 spec +> Base: `fusion-api@c5b0775` / `fusion-ui@master` +> Branch: `feat/monorepo-merge` +> Target: 新建仓库 `HyxiaoGe/fusion` + +**目标:** 把 `fusion-api` 与 `fusion-ui` 合并为单一仓库 `fusion`,保留两边完整 git 历史,并把当前分散在两处、共约 3600 行的发布流水线收敛为一份参数化的可复用 workflow + 一组可本地执行的部署脚本。 + +**本轮范围:** 仓库结构、git 历史、CI/CD、部署平台绑定、agent 与文档配置。不改任何业务代码,不改 Redis Stream 两段式流,不改 API 契约,不借机重构 `app/` 或 `src/` 下的任何模块。 + +**总体架构:** 新仓库采用 `apps/api` + `apps/ui` 两个应用目录,根目录只保留跨应用的编排物(workflow、部署脚本、台账、导航文档)。发布流水线拆成三层:`deploy.yml` 做 paths 过滤与分发,`_deploy-app.yml` 是参数化的 `workflow_call` 可复用 workflow,`ops/deploy/*.sh` 承载实际 shell 逻辑并可脱离 GitHub Actions 本地执行。 + +## 依据 + +本计划的每条判断都来自对两个仓库当前状态的实测,不依赖印象: + +- **代码零耦合。** `fusion-api` 505 个 `.py`,`fusion-ui` 269 个 `.tsx` + 253 个 `.ts`,交集为 0。无共享包、无 workspace 依赖、无交叉 import。目录下沉后各自的相对路径、构建、测试均不变。 +- **历史合并成本低。** 两边 `.git` 分别为 5.5M / 2.8M,常规 `merge --allow-unrelated-histories` 即可完整保留,无需 `filter-repo`。 +- **发布流水线已被手工写了两遍。** 两边步骤名逐字相同的有 14 个(去重后)(`Checkout smoke scripts`、`Configure Docker credential directory`、`Cleanup old images`、`Push CI/CD metrics`、`通知飞书(部署结果)` 等),另有 3 对同职能不同名(`Capture current deployment` ↔ `…for rollback`、`Rollback previous deployment` ↔ `Roll back failed deployment`、`Smoke check candidate` ↔ `Verify health`)。抽象成可复用 workflow 有实证支撑,不是投机。 +- **`deploy.yml` 的体量是内联 shell,不是 YAML 编排。** 全文 2553 行中 `deploy-dev` 单个 job 占 2339 行,其中四个步骤合计 1757 行(占 75%):`Pull and restart fusion-api` 693、`Roll back failed deployment` 476、`Capture current deployment for rollback` 307、`Verify health` 281。全文 28 个 `run: |` 块、436 行 shell 控制流关键字。这套逻辑当前不可本地执行、不可单测,唯一验证方式是向 master 推送。 +- **同职能步骤两边体量差 10 倍。** Capture 307 vs 30 行,Rollback 476 vs 65 行。差额中一部分是 API 的真实约束(alembic 迁移、flyai-adapter 必须与 api 同 SHA、litellm governance worker 与 model management worker 的暂停/恢复),一部分是重复的防御性检查。 +- **双仓成本已有记录。** 50 份 plan 中 7 份横跨两仓;`docs/superpowers/plans/2026-06-28-agent-run-continuation.md` 明确写有"这份 spec 横跨 fusion-api 和 fusion-ui……实施时使用一个工作分支;最终按子仓分别提交,避免把半成品多次推送触发流水线"。 +- **运行时已经耦合。** `fusion-ui/next.config.js` 通过 rewrites 把 `/api/*` 同源代理到 fusion-api,SSE 走此通道;`fusion-ui/docker-compose.yml` 直接引用 `http://fusion-api:8000`;两边共用同一组 self-hosted runner(Windows 构建 / Linux 部署)、同一个阿里云 ACR、同一台 dev server。 + +## Global Constraints + +- **搬迁与重写必须分阶段,不得在同一次变更内完成。** 流水线操作真实 docker 重启与 alembic 迁移,二者混做会使发布故障无法二分定位。 +- Task 1 完成前不得改动任何 workflow 逻辑,只允许改路径与新增 `paths:` 过滤。 +- 每个 Task 结束后两个应用各自完成一次真实发布 + 一次真实回滚,未通过不得进入下一 Task。 +- `paths:` 过滤是强制项。合并后无过滤会导致修改单个文件同时触发两条重型发布流水线(重建镜像、执行 Alembic、重启生产容器)。 +- 抽取 shell 到 `ops/deploy/` 时**逐字搬运**,不得顺手"优化"。行为差异必须为零,抽取与改写分属不同 Task。 +- 镜像 tag 由 `` 改为 `-`,每个应用独立记录 last-deployed-sha。合并后单个 commit SHA 同时对应多个应用镜像,原"回滚到 SHA X"语义失效。 +- 保留 `deploy-dev` 现有的 `cancel-in-progress: false`,并新增跨应用互斥:两条流水线操作同一台 dev server 的 docker,不得并发。 +- API 侧的额外约束(alembic 迁移、flyai-adapter 同 SHA 校验、两个 worker 的暂停/恢复)以可选 hook 形式进入可复用 workflow,不得强加给 UI。 +- 不删除任何现有校验分支。合并后无法确认必要性的检查一律保留,并在 plan 中登记待评估,而非就地删除。 +- 两个原仓库在新仓库通过全部验证前保持可用,仅在 Task 5 归档。 +- 提交信息中文,格式 `: <中文描述>`,包含 `Co-Authored-By`。 + +## 目标结构 + +``` +fusion/ +├── apps/ +│ ├── api/ # 原 fusion-api 全部内容 +│ └── ui/ # 原 fusion-ui 全部内容 +├── ops/ +│ └── deploy/ # 从 YAML 抽出的部署脚本,可本地执行 +│ ├── capture-rollback-target.sh +│ ├── restart-service.sh +│ ├── verify-health.sh +│ ├── rollback.sh +│ └── notify-feishu.sh +├── docs/ +│ ├── EXECUTION_LEDGER.md # 合并两仓台账 +│ └── superpowers/ # 合并两仓 plans / specs +├── .agents/skills/ # api 侧 9 个 + ui 侧 1 个 +├── .github/workflows/ +│ ├── deploy.yml # 薄编排:paths 过滤 → 分发 +│ ├── _deploy-app.yml # workflow_call 可复用 workflow +│ └── pr-ci.yml # paths 分流 +└── CLAUDE.md # 根导航,apps/*/CLAUDE.md 为应用级 +``` + +## Task 1:建仓、历史合并与目录下沉 + +1. 新建 `HyxiaoGe/fusion`,默认分支 `master`。 +2. 分别以 `git remote add` + `merge --allow-unrelated-histories` 引入两仓历史,各自先在原仓库内 `git mv` 到 `apps/api` / `apps/ui` 再合并,避免根目录文件互相覆盖。 +3. 12 个同名根文件按归属下沉:`CLAUDE.md`、`AGENTS.md`、`README.md`、`.gitignore`、`.dockerignore`、`.env.example`、`Dockerfile`、`docker-compose.yml`、`railway.json`、`.agents/`、`docs/`、`scripts/`。 +4. workflow 原样复制两份,仅改动 checkout 后的工作目录、`.github/scripts/*` 路径、Dockerfile build context(`.` → `apps/api` / `apps/ui`),并为每条加 `paths:` 过滤。 +5. `release-safety.yml` 拆为 `apps/api/release-safety.yml` 与 `apps/ui/release-safety.yml`,`release-safety-contract.sh` 改为接受契约文件路径参数。 + +**验证:** 两个应用各自 PR CI 通过;各自向 master 推送一次真实发布并成功;各自执行一次 `workflow_dispatch` 回滚并成功。修改 `apps/api` 下文件不得触发 UI 流水线,反之亦然。 + +## Task 2:抽取 shell 到 ops/deploy + +按体量倒序逐个抽取,每抽一个独立验证一次:`Pull and restart`(693) → `Roll back failed deployment`(476) → `Capture current deployment`(307) → `Verify health`(281) → 其余。 + +1. 每个步骤的 `run: |` 块整体移入 `ops/deploy/.sh`,入参改为显式环境变量或位置参数,YAML 侧只保留调用。 +2. 为每个脚本补 bats 或 shunit2 用例,覆盖至少:正常路径、目标 SHA 非法、回滚锚点缺失三种情况。 +3. PR CI 增加脚本 dry-run 与单测门禁。 + +**验证:** 每抽取一个脚本,两个应用各发布一次并回滚一次,行为与抽取前完全一致。全部抽完后 `deploy.yml` 的 YAML 行数应降至 400 行以内。 + +## Task 3:参数化合并与可复用 workflow + +前置:Task 2 完成,两边 shell 已成为可读的独立文件,重复之处可见。 + +1. 以 UI 侧精简实现为基线,逐条比对 API 侧多出的检查,分类为「真实约束」与「重复防御」。真实约束做成可选 hook,重复防御合并。分类结果登记在本文件,不在本 Task 内删除任何检查。 +2. 抽出 `_deploy-app.yml`,参数:应用名、镜像仓库、健康检查端点、迁移开关、依赖服务列表、回滚锚点校验策略。 +3. `deploy.yml` 收敛为 paths 过滤 + 两次 `workflow_call`。 +4. 镜像 tag 改为 `-`,回滚改为按应用独立追踪 last-deployed-sha。 +5. 新增跨应用 concurrency 互斥。 + +**验证:** 两个应用各发布、各回滚一次;构造一次跨应用并发推送,确认互斥生效;构造一次仅改 UI 的提交,确认 API 容器 SHA 不变且回滚目标仍然唯一可解析。 + +## Task 4:部署平台与外部绑定 + +1. Vercel root directory 改为 `apps/ui`;Railway 两个服务的 root directory 分别指向 `apps/api` / `apps/ui`。 +2. ACR 仓库名保持不变,仅 tag 规则变更,确认存量镜像不受影响。 +3. GitHub secrets / variables 迁移到新仓库;self-hosted runner 重新注册到新仓库。 +4. 分支保护、required checks 按新 workflow 的 job 名重建。 + +**验证:** 逐项在平台控制台确认,并各触发一次真实构建。本 Task 全部改动在仓库之外,必须逐条勾选,不得依赖代码审查发现遗漏。 + +## Task 5:文档、台账与原仓库归档 + +1. 合并两份 `EXECUTION_LEDGER.md`,补一条本次合并记录。 +2. 合并 `docs/superpowers/plans` 与 `specs`,修正 7 份跨仓 plan 中的 `../fusion-ui/` 相对路径。 +3. 根 `CLAUDE.md` 改为导航,`apps/api/CLAUDE.md` 与 `apps/ui/CLAUDE.md` 承载应用级约定;`AGENTS.md` 同理。`.agents/skills/` 中 10 个 skill 合并去重,`fusion-next-step` 两边各有一份需统一。 +4. 两个原仓库置为 archived,README 指向新仓库。 + +**验证:** `fusion-next-step` skill 能正确读取合并后台账;跨仓 plan 中不再存在 `../fusion-ui/` 路径;两个原仓库已归档。 + +## 不做什么 + +- 不改任何业务代码。目录搬迁之外,`app/` 与 `src/` 下不产生 diff。 +- 不引入 npm workspace、pnpm workspace、Nx、Turborepo 等 monorepo 工具。当前两个应用零共享代码,工具链只会增加复杂度。 +- 不在本轮做 OpenAPI 契约同步。`scripts/export_openapi.py` 已存在但 UI 未消费,合并后具备条件,另开 plan。 +- 不合并 `docker-compose.yml`。两边网络拓扑不同(api 侧有 `middleware`、`litellm_net`、`flyai` 三个网络),合并需单独评估。 +- 不调整 self-hosted runner 的机器配置或数量。 + +## 风险与回退 + +| 风险 | 触发点 | 回退方式 | +|---|---|---| +| 路径改错导致发布失败 | Task 1 | 原仓库未归档,直接切回原仓库发布 | +| shell 抽取引入行为差异 | Task 2 | 逐个抽取、逐个验证,单个 revert 即可定位 | +| 回滚锚点在过渡期不可解析 | Task 3 | tag 规则切换前先并行打两种 tag,确认新规则可解析后再停旧规则 | +| 平台绑定遗漏 | Task 4 | 全部在仓库外,以 checklist 逐条勾选,不依赖 code review | From a5ad5b524a8498c7910f4ca23ef133d5164eb0ee Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 09:51:29 +0000 Subject: [PATCH 2/8] =?UTF-8?q?docs:=20=E6=8C=89=20PR=20#83=20=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E6=84=8F=E8=A7=81=E4=BF=AE=E8=AE=A2=20monorepo=20?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基线刷新至 fusion-api@43cee73 / fusion-ui@77b7fc2,全部数字重算。 9 条评审意见全部受理,逐条给出核实结果与处理,见文末修订记录。 主要变更: - 新增 Task 0 承载新仓基建与恢复点,解决原 Task 1 要求真实发布 但 secrets/runner/Environment 到 Task 4 才就绪的顺序矛盾。采用 新增 runner 实例加应用标签,旧仓 runner 保留至 Task 5。 - 移除 event-level paths 过滤,改为始终触发加 changes job 加恒定 required gate,避免被跳过的 workflow 使 required check 永久 Pending,同时规避 changed-files 300 文件上限。 - 跨应用顺序改由顶层 orchestrator DAG 保证,concurrency 只防并发。 - 新增路径与状态清单,A 类为宿主机持久化状态(5 处 cd ~/project/fusion、4 处 storage/files bind mount、cost-sync unit 的 checkout 耦合),独立为 Task 2 受控 cutover;B/C 类为 25 处 API 测试与 12 处 UI 测试对根 .github 的硬引用,其中 UI 侧 filesUnder 会枚举整个 workflows 目录。 - 撤销 - 镜像 tag 方案,原判断有误:两应用已使用不同 ACR repository,同 SHA 不产生歧义。改为新增 per-app 发布台账 解决 last deployed SHA 分叉。 - 真实发布与回滚收敛到 Task 边界,不在每个脚本搬运后执行。 - Task 5 补充平台元数据迁移边界与旧仓退役时点。 Co-Authored-By: Claude --- .../plans/2026-08-31-monorepo-merge.md | 237 +++++++++++------- 1 file changed, 151 insertions(+), 86 deletions(-) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md index 945be094..0bbb4135 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -1,130 +1,195 @@ # Fusion Monorepo 合并实施计划 > Spec: 本文件自带依据章节,无独立 spec -> Base: `fusion-api@c5b0775` / `fusion-ui@master` +> Base: `fusion-api@43cee73` / `fusion-ui@77b7fc2`(2026-08-31 复核基线) > Branch: `feat/monorepo-merge` > Target: 新建仓库 `HyxiaoGe/fusion` +> Review: PR #83 第一轮评审已受理,修订记录见文末 **目标:** 把 `fusion-api` 与 `fusion-ui` 合并为单一仓库 `fusion`,保留两边完整 git 历史,并把当前分散在两处、共约 3600 行的发布流水线收敛为一份参数化的可复用 workflow + 一组可本地执行的部署脚本。 -**本轮范围:** 仓库结构、git 历史、CI/CD、部署平台绑定、agent 与文档配置。不改任何业务代码,不改 Redis Stream 两段式流,不改 API 契约,不借机重构 `app/` 或 `src/` 下的任何模块。 +**本轮范围:** 仓库结构、git 历史、CI/CD、宿主机部署状态、部署平台绑定、agent 与文档配置。不改任何业务代码,不改 Redis Stream 两段式流,不改 API 契约,不借机重构 `app/` 或 `src/` 下的任何模块。 -**总体架构:** 新仓库采用 `apps/api` + `apps/ui` 两个应用目录,根目录只保留跨应用的编排物(workflow、部署脚本、台账、导航文档)。发布流水线拆成三层:`deploy.yml` 做 paths 过滤与分发,`_deploy-app.yml` 是参数化的 `workflow_call` 可复用 workflow,`ops/deploy/*.sh` 承载实际 shell 逻辑并可脱离 GitHub Actions 本地执行。 +**总体架构:** 新仓库采用 `apps/api` + `apps/ui` 两个应用目录,根目录只保留跨应用的编排物。发布流水线拆成三层:顶层 orchestrator 负责变更检测与 `API → UI` 顺序编排,`_deploy-app.yml` 是参数化的 `workflow_call` 可复用 workflow,`ops/deploy/*.sh` 承载实际 shell 逻辑并可脱离 GitHub Actions 本地执行。 ## 依据 -本计划的每条判断都来自对两个仓库当前状态的实测,不依赖印象: +以下数据基于 `fusion-api@43cee73` / `fusion-ui@77b7fc2` 实测,可复核: -- **代码零耦合。** `fusion-api` 505 个 `.py`,`fusion-ui` 269 个 `.tsx` + 253 个 `.ts`,交集为 0。无共享包、无 workspace 依赖、无交叉 import。目录下沉后各自的相对路径、构建、测试均不变。 +- **代码零耦合。** `fusion-api` 516 个 `.py`、699 个文件;`fusion-ui` 524 个 `.ts`/`.tsx`、636 个文件。交叉计数为 0(api 侧 `.ts`/`.tsx` 为 0,ui 侧 `.py` 为 0)。无共享包、无 workspace 依赖、无交叉 import。 - **历史合并成本低。** 两边 `.git` 分别为 5.5M / 2.8M,常规 `merge --allow-unrelated-histories` 即可完整保留,无需 `filter-repo`。 -- **发布流水线已被手工写了两遍。** 两边步骤名逐字相同的有 14 个(去重后)(`Checkout smoke scripts`、`Configure Docker credential directory`、`Cleanup old images`、`Push CI/CD metrics`、`通知飞书(部署结果)` 等),另有 3 对同职能不同名(`Capture current deployment` ↔ `…for rollback`、`Rollback previous deployment` ↔ `Roll back failed deployment`、`Smoke check candidate` ↔ `Verify health`)。抽象成可复用 workflow 有实证支撑,不是投机。 -- **`deploy.yml` 的体量是内联 shell,不是 YAML 编排。** 全文 2553 行中 `deploy-dev` 单个 job 占 2339 行,其中四个步骤合计 1757 行(占 75%):`Pull and restart fusion-api` 693、`Roll back failed deployment` 476、`Capture current deployment for rollback` 307、`Verify health` 281。全文 28 个 `run: |` 块、436 行 shell 控制流关键字。这套逻辑当前不可本地执行、不可单测,唯一验证方式是向 master 推送。 -- **同职能步骤两边体量差 10 倍。** Capture 307 vs 30 行,Rollback 476 vs 65 行。差额中一部分是 API 的真实约束(alembic 迁移、flyai-adapter 必须与 api 同 SHA、litellm governance worker 与 model management worker 的暂停/恢复),一部分是重复的防御性检查。 -- **双仓成本已有记录。** 50 份 plan 中 7 份横跨两仓;`docs/superpowers/plans/2026-06-28-agent-run-continuation.md` 明确写有"这份 spec 横跨 fusion-api 和 fusion-ui……实施时使用一个工作分支;最终按子仓分别提交,避免把半成品多次推送触发流水线"。 -- **运行时已经耦合。** `fusion-ui/next.config.js` 通过 rewrites 把 `/api/*` 同源代理到 fusion-api,SSE 走此通道;`fusion-ui/docker-compose.yml` 直接引用 `http://fusion-api:8000`;两边共用同一组 self-hosted runner(Windows 构建 / Linux 部署)、同一个阿里云 ACR、同一台 dev server。 +- **发布流水线已被手工写了两遍。** 两边步骤名逐字相同的有 14 个(去重后),另有 3 对同职能不同名(`Capture current deployment` ↔ `…for rollback`、`Rollback previous deployment` ↔ `Roll back failed deployment`、`Smoke check candidate` ↔ `Verify health`)。抽象成可复用 workflow 有实证支撑。 +- **`deploy.yml` 的体量是内联 shell,不是 YAML 编排。** 全文 2563 行中 `deploy-dev` 单个 job 占 2339 行(L189–L2527),其中四个步骤合计 1757 行(占该 job 的 75%):`Pull and restart fusion-api` 693、`Roll back failed deployment` 476、`Capture current deployment for rollback` 307、`Verify health` 281。该逻辑当前不可本地执行、不可单测,唯一验证方式是向 master 推送。 +- **同职能步骤两边体量差 10 倍。** Capture 307 vs 30 行,Rollback 476 vs 65 行。差额中一部分是 API 的真实约束(alembic 迁移、flyai-adapter 必须与 api 同 SHA、litellm governance 与 model management 两个 worker 的暂停/恢复),一部分是重复的防御性检查。分类结论见 Task 4,本计划不预判。 +- **双仓成本已有记录。** `docs/superpowers/plans/2026-06-28-agent-run-continuation.md` 明确写有"这份 spec 横跨 fusion-api 和 fusion-ui……实施时使用一个工作分支;最终按子仓分别提交,避免把半成品多次推送触发流水线"。 +- **运行时已经耦合。** `fusion-ui/next.config.js` 通过 rewrites 把 `/api/*` 同源代理到 fusion-api,SSE 走此通道;两边共用同一组 self-hosted runner 类型、同一个阿里云 ACR、同一台 dev server,且**共用同一个宿主机工作目录 `~/project/fusion`**。 ## Global Constraints -- **搬迁与重写必须分阶段,不得在同一次变更内完成。** 流水线操作真实 docker 重启与 alembic 迁移,二者混做会使发布故障无法二分定位。 -- Task 1 完成前不得改动任何 workflow 逻辑,只允许改路径与新增 `paths:` 过滤。 -- 每个 Task 结束后两个应用各自完成一次真实发布 + 一次真实回滚,未通过不得进入下一 Task。 -- `paths:` 过滤是强制项。合并后无过滤会导致修改单个文件同时触发两条重型发布流水线(重建镜像、执行 Alembic、重启生产容器)。 +- **搬迁、切换与流水线重写必须分三阶段,不得合并。** 流水线操作真实 docker 重启、alembic 迁移与用户上传文件目录,混做会使发布故障无法二分定位。 +- **旧仓库在最终验收通过前保持完整发布与回滚能力。** 不得在切换完成前注销旧仓 runner、撤销旧仓 secrets 或归档旧仓。 +- **宿主机持久化状态与代码 checkout 路径必须分开处理。** 二者当前在 `~/project/fusion` 下混放,不得以同一次路径替换处理。 +- `paths:` **不得用作 event-level 过滤**(见 P0-4)。变更检测由 workflow 内首个 `changes` job 承担,且始终提供一个恒定存在的 required gate job。 +- 跨应用顺序由顶层 orchestrator 的 DAG 保证,concurrency 只负责防并发,不承担顺序语义。 - 抽取 shell 到 `ops/deploy/` 时**逐字搬运**,不得顺手"优化"。行为差异必须为零,抽取与改写分属不同 Task。 -- 镜像 tag 由 `` 改为 `-`,每个应用独立记录 last-deployed-sha。合并后单个 commit SHA 同时对应多个应用镜像,原"回滚到 SHA X"语义失效。 -- 保留 `deploy-dev` 现有的 `cancel-in-progress: false`,并新增跨应用互斥:两条流水线操作同一台 dev server 的 docker,不得并发。 -- API 侧的额外约束(alembic 迁移、flyai-adapter 同 SHA 校验、两个 worker 的暂停/恢复)以可选 hook 形式进入可复用 workflow,不得强加给 UI。 -- 不删除任何现有校验分支。合并后无法确认必要性的检查一律保留,并在 plan 中登记待评估,而非就地删除。 -- 两个原仓库在新仓库通过全部验证前保持可用,仅在 Task 5 归档。 +- 不删除任何现有校验分支。合并后无法确认必要性的检查一律保留并登记待评估,不得就地删除。 +- 每个 Task 有独立可执行验收条件;真实发布与回滚只在 Task 边界执行,不在每个脚本搬运后执行(见 P1-8)。 - 提交信息中文,格式 `: <中文描述>`,包含 `Co-Authored-By`。 -## 目标结构 - -``` -fusion/ -├── apps/ -│ ├── api/ # 原 fusion-api 全部内容 -│ └── ui/ # 原 fusion-ui 全部内容 -├── ops/ -│ └── deploy/ # 从 YAML 抽出的部署脚本,可本地执行 -│ ├── capture-rollback-target.sh -│ ├── restart-service.sh -│ ├── verify-health.sh -│ ├── rollback.sh -│ └── notify-feishu.sh -├── docs/ -│ ├── EXECUTION_LEDGER.md # 合并两仓台账 -│ └── superpowers/ # 合并两仓 plans / specs -├── .agents/skills/ # api 侧 9 个 + ui 侧 1 个 -├── .github/workflows/ -│ ├── deploy.yml # 薄编排:paths 过滤 → 分发 -│ ├── _deploy-app.yml # workflow_call 可复用 workflow -│ └── pr-ci.yml # paths 分流 -└── CLAUDE.md # 根导航,apps/*/CLAUDE.md 为应用级 -``` - -## Task 1:建仓、历史合并与目录下沉 - -1. 新建 `HyxiaoGe/fusion`,默认分支 `master`。 -2. 分别以 `git remote add` + `merge --allow-unrelated-histories` 引入两仓历史,各自先在原仓库内 `git mv` 到 `apps/api` / `apps/ui` 再合并,避免根目录文件互相覆盖。 -3. 12 个同名根文件按归属下沉:`CLAUDE.md`、`AGENTS.md`、`README.md`、`.gitignore`、`.dockerignore`、`.env.example`、`Dockerfile`、`docker-compose.yml`、`railway.json`、`.agents/`、`docs/`、`scripts/`。 -4. workflow 原样复制两份,仅改动 checkout 后的工作目录、`.github/scripts/*` 路径、Dockerfile build context(`.` → `apps/api` / `apps/ui`),并为每条加 `paths:` 过滤。 -5. `release-safety.yml` 拆为 `apps/api/release-safety.yml` 与 `apps/ui/release-safety.yml`,`release-safety-contract.sh` 改为接受契约文件路径参数。 - -**验证:** 两个应用各自 PR CI 通过;各自向 master 推送一次真实发布并成功;各自执行一次 `workflow_dispatch` 回滚并成功。修改 `apps/api` 下文件不得触发 UI 流水线,反之亦然。 - -## Task 2:抽取 shell 到 ops/deploy - -按体量倒序逐个抽取,每抽一个独立验证一次:`Pull and restart`(693) → `Roll back failed deployment`(476) → `Capture current deployment`(307) → `Verify health`(281) → 其余。 +## 路径与状态清单(Path Inventory) -1. 每个步骤的 `run: |` 块整体移入 `ops/deploy/.sh`,入参改为显式环境变量或位置参数,YAML 侧只保留调用。 -2. 为每个脚本补 bats 或 shunit2 用例,覆盖至少:正常路径、目标 SHA 非法、回滚锚点缺失三种情况。 -3. PR CI 增加脚本 dry-run 与单测门禁。 +目录下沉**不是**纯路径替换。以下为实测的逐项清单,每项在 Task 1 / Task 2 中必须有对应处理与测试: + +### A 类:宿主机持久化状态(最高风险,Task 2 处理) -**验证:** 每抽取一个脚本,两个应用各发布一次并回滚一次,行为与抽取前完全一致。全部抽完后 `deploy.yml` 的 YAML 行数应降至 400 行以内。 +| 位置 | 内容 | 风险 | +|---|---|---| +| `deploy.yml` L587 / L658 / L1994 | `cd ~/project/fusion` | 宿主机工作目录,与仓库名耦合 | +| `build-and-deploy.yml` L282 / L391 | `cd ~/project/fusion` | 同上,两应用共用同一目录 | +| `deploy.yml` L1036/1042/1077/1206 | `./fusion-api/storage/files` bind mount | **用户上传文件持久化**,路径变更会挂载到空目录 | +| `deploy.yml` L1819 | `${HOME}/project/fusion/.env` | 运行时环境变量来源 | +| `ops/litellm/fusion-litellm-cost-sync.service` | `%h/project/fusion/fusion-api` ×3(L5/L6 `AssertPathExists`、L13 `WorkingDirectory`) | **唯一直接耦合仓库 checkout 的 unit**,路径错则 unit 启动失败 | -## Task 3:参数化合并与可复用 workflow +宿主机当前布局为 `~/project/fusion/fusion-api/…`,目录名直接镜像仓库名。**该布局与新仓库的 `apps/api` 结构不一致,且不能自动跟随。** -前置:Task 2 完成,两边 shell 已成为可读的独立文件,重复之处可见。 +**仓库内已有正确范式可循:** 另外两个 unit(`fusion-litellm-governance.service`、`fusion-litellm-model-management.service`)的 `WorkingDirectory` 指向 `%h/.local/share/fusion/litellm-governance-current` 与 `%h/.local/share/fusion/litellm-model-management-current`,即已与仓库 checkout 解耦的暂存目录。Task 2 应把 `cost-sync` 收敛到同一范式,而不是简单替换其路径字面量。 -1. 以 UI 侧精简实现为基线,逐条比对 API 侧多出的检查,分类为「真实约束」与「重复防御」。真实约束做成可选 hook,重复防御合并。分类结果登记在本文件,不在本 Task 内删除任何检查。 -2. 抽出 `_deploy-app.yml`,参数:应用名、镜像仓库、健康检查端点、迁移开关、依赖服务列表、回滚锚点校验策略。 -3. `deploy.yml` 收敛为 paths 过滤 + 两次 `workflow_call`。 -4. 镜像 tag 改为 `-`,回滚改为按应用独立追踪 last-deployed-sha。 -5. 新增跨应用 concurrency 互斥。 +### B 类:测试硬读根 `.github`(Task 1 处理) + +| 文件 | 引用数 | 失效方式 | +|---|---|---| +| `test/test_model_management_deploy_config.py` | 10 | `ROOT / ".github/..."` | +| `test/test_ci_container_contract.py` | 7 | `ROOT / ".github/..."` | +| `test/test_ci_cd_permission_boundary.py` | 6 | `ROOT / ".github/..."` | +| `test/test_knowledge_deploy_config.py` | 1 | **`Path(".github/...")` 为 CWD 相对**,失效方式与上面三者不同 | +| `test/test_litellm_governance_units.py` | 1 | `ROOT / ".github/..."` | +| `fusion-ui/src/scripts/buildAndDeployWorkflow.test.ts` | 12 | `process.cwd()` 相对 | + +`buildAndDeployWorkflow.test.ts` L168–L169 通过 `filesUnder()` **枚举整个 `.github/workflows` 与 `.github/actions` 目录**并对全部结果做断言。合并到共享根 `.github/` 后,该测试会扫到 API 侧 workflow 并失败。这不是路径替换能解决的,需改为按应用限定扫描范围。 + +### C 类:Action 与构建上下文(Task 1 处理) + +- `fusion-ui/.github/workflows/build-and-deploy.yml` L98:`uses: ./.github/actions/windows-docker-build`(本地 composite action,`defaults.run.working-directory` 对其无效) +- 两边 `.github/release-safety.yml` 与 `.github/scripts/release-safety-contract.sh`(contract script 当前假定仓库根唯一一份) +- 两边 `.github/scripts/*` 的调用路径 +- Dockerfile build context(`.` → `apps/api` / `apps/ui`) +- `actions/setup-node` 的 `cache-dependency-path` +- 7 份跨仓 plan 中的 `../fusion-ui/` 相对路径与文档绝对路径 + +## Task 0:新仓基建与恢复点(新增) + +前置于一切代码搬迁。目的是让 Task 1 的验收在新仓可执行,同时保证旧仓不失能。 + +1. 创建 `HyxiaoGe/fusion`,默认分支 `master`。 +2. 为两个原仓库各打一份 `git bundle` 恢复点,异地留存。 +3. 复制 `dev` Environment 及其 protection rule、branch policy、branch protection;secrets/variables 按 repo 层与 Environment 层分别对应建立。 +4. **新增 runner 实例**(不迁移旧仓 runner),并为全部 runner 增加应用维度标签 `fusion-api` / `fusion-ui`;workflow 的 `runs-on` 同步改为带应用标签的组合。旧仓四个 runner 原样保留至 Task 5。 +5. 产出宿主机状态清单:`~/project/fusion` 下的目录、bind mount 源、`.env`、systemd unit 的实际 `WorkingDirectory`,逐项记录当前值。 + +**验收:** 新仓可跑通一个 hello-world workflow,分别命中 `fusion-api` 与 `fusion-ui` 标签的 runner;旧仓两条流水线仍可正常发布与回滚(各执行一次);bundle 可在临时目录成功还原。 + +## Task 1:历史合并与目录下沉 -**验证:** 两个应用各发布、各回滚一次;构造一次跨应用并发推送,确认互斥生效;构造一次仅改 UI 的提交,确认 API 容器 SHA 不变且回滚目标仍然唯一可解析。 +**本 Task 不改变任何部署行为,只验证 PR CI 与镜像构建。** -## Task 4:部署平台与外部绑定 +1. 两仓各自先在原仓库内 `git mv` 到 `apps/api` / `apps/ui`,再以 `merge --allow-unrelated-histories` 引入,避免根目录文件互相覆盖。 +2. 12 个同名根文件按归属下沉:`CLAUDE.md`、`AGENTS.md`、`README.md`、`.gitignore`、`.dockerignore`、`.env.example`、`Dockerfile`、`docker-compose.yml`、`railway.json`、`.agents/`、`docs/`、`scripts/`。 +3. 逐项处理路径清单 B 类与 C 类: + - 5 个 API 测试文件的 25 处引用改为应用根相对,并统一 `Path()` 与 `ROOT /` 两种写法; + - `buildAndDeployWorkflow.test.ts` 的 `filesUnder()` 扫描范围限定到本应用 workflow; + - `release-safety.yml` 拆为 `apps/api/release-safety.yml` 与 `apps/ui/release-safety.yml`,`release-safety-contract.sh` 改为接受契约文件路径参数; + - 本地 composite action 路径、Dockerfile context、`cache-dependency-path` 逐一修正。 +4. PR CI 改造为:始终触发 → 首个 `changes` job 判定 `api` / `ui` / `shared` → 应用 job 用 `if` 跳过 → 末尾恒定 required gate job。明确 `.github/**`、`ops/**`、根配置、共享文档各触发哪一侧。 +5. 部署 workflow 原样复制两份进新仓但**保持 disabled**,不接管发布。 -1. Vercel root directory 改为 `apps/ui`;Railway 两个服务的 root directory 分别指向 `apps/api` / `apps/ui`。 -2. ACR 仓库名保持不变,仅 tag 规则变更,确认存量镜像不受影响。 -3. GitHub secrets / variables 迁移到新仓库;self-hosted runner 重新注册到新仓库。 -4. 分支保护、required checks 按新 workflow 的 job 名重建。 +**验收:** 新仓 PR CI 全绿;`apps/api` 与 `apps/ui` 的单测与 lint 在新仓通过(含上述被改动的 6 个测试文件);两个应用的镜像可在新仓成功构建(不推送、不部署);只改一侧的 PR 不触发另一侧的应用 job,但 required gate 仍产出成功结论。 + +## Task 2:受控 cutover + +本 Task 是唯一改变生产行为的一步,独立执行、独立回退。 + +1. **宿主机状态迁移**(路径清单 A 类): + - 先备份 `~/project/fusion/fusion-api/storage/files`; + - 确立与仓库布局解耦的稳定数据目录(例如 `~/project/fusion/data/api/storage/files`),bind mount 指向稳定目录; + - 代码 checkout 目录与数据目录分离,必要时以软链接兼容过渡期; + - 把 `cost-sync` unit 的 `WorkingDirectory` 与 `AssertPathExists` 收敛到 `%h/.local/share/fusion/*-current` 范式(与另两个 unit 一致),`systemctl --user daemon-reload` 后逐个验证启动; + - `${HOME}/project/fusion/.env` 的位置与读取方保持一致。 +2. 启用新仓顶层 orchestrator:`detect changes → deploy API → deploy UI`,只改一侧时跳过另一侧。环境级 concurrency 仅防并发。 +3. 首次切换选择低峰时段,切换后旧仓 workflow 置为 disabled 但不删除。 + +**验收:** 新仓完成一次真实 API 发布 + 回滚;一次真实 UI 发布 + 回滚;一次同时修改两边的提交,确认执行顺序为 API → UI;确认上传文件在切换前后可正常读取(切换前上传一个文件,切换后下载校验);三个 systemd unit 均 active(`cost-sync` 需确认已脱离仓库 checkout 路径);**旧仓回退演练一次**(重新 enable 旧 workflow 并成功发布)。任一项失败即回退到旧仓。 + +## Task 3:抽取 shell 到 ops/deploy + +按体量倒序逐个抽取:`Pull and restart`(693) → `Roll back failed deployment`(476) → `Capture current deployment`(307) → `Verify health`(281) → 其余。 + +1. 每个步骤的 `run: |` 块整体移入 `ops/deploy/.sh`,入参改为显式环境变量或位置参数,YAML 侧只保留调用。 +2. 每个脚本配 fixture + dry-run + contract test,覆盖至少:正常路径、目标 SHA 非法、回滚锚点缺失三种情况。PR CI 增加脚本单测门禁。 +3. **不在每个脚本搬运后向 master 发布。** 真实发布与回滚在 Task 边界执行一次。 + +**验收:** 全部脚本单测通过;`deploy.yml` 的 YAML 行数降至 400 行以内;Task 结束时执行一次完整 API → UI 真实发布与回滚验收。 + +## Task 4:参数化 reusable workflow 与发布台账 + +前置:Task 3 完成,两边 shell 已成为可读独立文件,重复之处可见。 + +1. 以 UI 侧精简实现为基线,逐条比对 API 侧多出的检查,分类为「真实约束」与「重复防御」。真实约束做成可选 hook,重复防御合并。**分类结果登记在本文件,本 Task 内不删除任何检查。** +2. 抽出 `_deploy-app.yml`,参数:应用名、镜像仓库、健康检查端点、迁移开关、依赖服务列表、回滚锚点校验策略。 +3. **保留各应用现有 ACR repository 与 `` tag**(`seanfield/fusion-api` 与 `seanfield/fusion-ui` 已天然区分应用,见 P1-6)。新增 per-app 发布台账/manifest,记录每个应用的 last deployed SHA,解决 path-filtered 发布后两应用 SHA 分叉导致的回滚目标定位问题。 +4. Vercel root directory 改为 `apps/ui`;Railway 两个服务的 root directory 分别指向 `apps/api` / `apps/ui`。 +5. 分支保护、required checks 按新 workflow 的 job 名重建(gate job 为 required,应用 job 不是)。 -**验证:** 逐项在平台控制台确认,并各触发一次真实构建。本 Task 全部改动在仓库之外,必须逐条勾选,不得依赖代码审查发现遗漏。 +**验收:** 两应用各发布、各回滚一次;构造仅改 UI 的提交,确认 API 的 last deployed SHA 未变且回滚目标仍可唯一解析;平台绑定逐条在控制台勾选确认并各触发一次真实构建。 -## Task 5:文档、台账与原仓库归档 +## Task 5:文档、台账与旧仓归档 1. 合并两份 `EXECUTION_LEDGER.md`,补一条本次合并记录。 2. 合并 `docs/superpowers/plans` 与 `specs`,修正 7 份跨仓 plan 中的 `../fusion-ui/` 相对路径。 -3. 根 `CLAUDE.md` 改为导航,`apps/api/CLAUDE.md` 与 `apps/ui/CLAUDE.md` 承载应用级约定;`AGENTS.md` 同理。`.agents/skills/` 中 10 个 skill 合并去重,`fusion-next-step` 两边各有一份需统一。 -4. 两个原仓库置为 archived,README 指向新仓库。 +3. 根 `CLAUDE.md` 改为导航,`apps/api/CLAUDE.md` 与 `apps/ui/CLAUDE.md` 承载应用级约定;`AGENTS.md` 同理。`.agents/skills/` 中 10 个 skill 合并去重(`fusion-next-step` 两边各有一份需统一)。 +4. **平台元数据边界**(见 P1-9):git 历史合并不迁移 Issues、PR、Actions runs、releases、webhooks、deploy keys、Environment protection、branch rules。明确: + - 复制:Environment protection、branch rules、secrets/variables、webhooks; + - 仅旧仓保留:Issues、PR、Actions run 历史、releases; + - commit message 中的 `#NN` 在新仓不再解析为原 PR,归档说明中标注旧仓地址供追溯; + - 旧仓 runner 与旧仓 workflow 在本 Task 退役,此前一直保留。 +5. 两个原仓库置为 archived,README 指向新仓库。 -**验证:** `fusion-next-step` skill 能正确读取合并后台账;跨仓 plan 中不再存在 `../fusion-ui/` 路径;两个原仓库已归档。 +**验收:** `fusion-next-step` skill 能正确读取合并后台账;跨仓 plan 中不再存在 `../fusion-ui/` 路径;旧仓 runner 已注销、仓库已归档。 ## 不做什么 - 不改任何业务代码。目录搬迁之外,`app/` 与 `src/` 下不产生 diff。 -- 不引入 npm workspace、pnpm workspace、Nx、Turborepo 等 monorepo 工具。当前两个应用零共享代码,工具链只会增加复杂度。 +- 不引入 npm/pnpm workspace、Nx、Turborepo。当前两应用零共享代码,工具链只增复杂度。 - 不在本轮做 OpenAPI 契约同步。`scripts/export_openapi.py` 已存在但 UI 未消费,合并后具备条件,另开 plan。 -- 不合并 `docker-compose.yml`。两边网络拓扑不同(api 侧有 `middleware`、`litellm_net`、`flyai` 三个网络),合并需单独评估。 -- 不调整 self-hosted runner 的机器配置或数量。 +- 不合并 `docker-compose.yml`。两边网络拓扑不同(api 侧有 `middleware`、`litellm_net`、`flyai` 三个网络),需单独评估。 +- 不把两应用镜像并入同一个 ACR repository。 +- 不迁移旧仓 Issues 与 PR 历史。 ## 风险与回退 | 风险 | 触发点 | 回退方式 | |---|---|---| -| 路径改错导致发布失败 | Task 1 | 原仓库未归档,直接切回原仓库发布 | -| shell 抽取引入行为差异 | Task 2 | 逐个抽取、逐个验证,单个 revert 即可定位 | -| 回滚锚点在过渡期不可解析 | Task 3 | tag 规则切换前先并行打两种 tag,确认新规则可解析后再停旧规则 | -| 平台绑定遗漏 | Task 4 | 全部在仓库外,以 checklist 逐条勾选,不依赖 code review | +| 上传文件目录挂载到空目录 | Task 2 | 切换前全量备份 + 切换后下载校验;失败则恢复备份并回退 bind mount | +| `cost-sync` unit 找不到 WorkingDirectory | Task 2 | unit 文件版本化,`daemon-reload` 前保留旧版本,逐个验证后再提交 | +| 新仓 runner 未就绪导致无法发布 | Task 1/2 | 旧仓 runner 与 workflow 全程保留至 Task 5,可随时 re-enable | +| required check 因 path 跳过而永久 Pending | Task 1 | 恒定 gate job 始终产出结论,不使用 event-level `paths:` | +| 跨应用发布顺序错误导致 SSE 契约不匹配 | Task 2 | orchestrator DAG 强制 API → UI;失败则整条流水线中止 | +| shell 抽取引入行为差异 | Task 3 | 逐个抽取 + 单测,单个 revert 即可定位 | +| 整体方案失败 | 任意 | Task 0 的 git bundle + 未归档的旧仓,可完整回到合并前状态 | + +## 评审修订记录 + +PR #83 第一轮评审共 9 条,全部受理。逐条处理如下: + +| # | 评审意见 | 核实结果 | 处理 | +|---|---|---|---| +| P0-1 | 计划基线过期 | **成立。** master 已从 `c5b0775` 前进 57 个提交至 `43cee73`,`deploy.yml` 2553 → 2563 行 | 已合并 master,全部数字基于 `43cee73` / `77b7fc2` 重算(py 505→516,文件 678→699,ts/tsx 522→524) | +| P0-2 | Task 1 与 Task 4 顺序矛盾 | **成立。** 原计划要求 Task 1 完成真实发布,但 secrets/runner/Environment 到 Task 4 才迁移 | 新增 Task 0 承载新仓基建;Task 1 改为只验证 PR CI 与镜像构建,不做发布;采用新增 runner 实例 + 应用标签,旧仓 runner 保留至 Task 5 | +| P0-3 | concurrency 不保证 API → UI 顺序 | **成立。** concurrency 仅防并发,等待顺序不等于分发顺序 | 改为顶层 orchestrator DAG `detect changes → deploy API → deploy UI`,写入 Global Constraints | +| P0-4 | `paths:` 与 required check 冲突 | **成立。** 被 path filter 跳过的 workflow 不产出结论,required check 会永久 Pending;且 changed-files 过滤有 300 文件上限,首次迁移提交远超此数 | 移除 event-level `paths:`;改为始终触发 + `changes` job + `if` 跳过 + 恒定 required gate | +| P0-5 | 部署脚本含宿主机真实路径与持久化数据 | **成立。** 实测:两边 workflow 共 5 处 `cd ~/project/fusion`;4 处 `./fusion-api/storage/files` bind mount;1 处 `${HOME}/project/fusion/.env`。systemd 侧经逐个核实为 **1 个** unit(`cost-sync`)耦合仓库 checkout 路径,另两个已用 `%h/.local/share/fusion/*-current` 暂存目录解耦 | 新增「路径与状态清单」A 类;宿主机状态迁移独立为 Task 2,含备份、稳定 data 目录、软链接兼容、systemd 更新与上传文件读写校验;`cost-sync` 按仓库内既有的解耦范式收敛 | +| P1-6 | 镜像 tag 歧义判断不成立 | **成立,原判断有误。** 实测 `seanfield/fusion-api` 与 `seanfield/fusion-ui` 已是不同 ACR repository,同 SHA 不产生歧义 | 撤销 `-` 方案,保留各自 `` tag;改为新增 per-app 发布台账解决 last deployed SHA 分叉 | +| P1-7 | "相对路径均不变"过于乐观 | **成立且可量化。** 实测 5 个 API 测试文件 25 处硬读根 `.github`(其中 `test_knowledge_deploy_config.py` 为 CWD 相对,失效方式不同);UI `buildAndDeployWorkflow.test.ts` 12 处,且 L168 枚举整个 workflows 目录做断言,合并后会扫到 API 侧 workflow | 新增「路径与状态清单」B/C 类逐项列出,每项在 Task 1 有对应处理与验收 | +| P1-8 | 每抽一个脚本就发布回滚成本过高 | **成立。** master 是发布通道,不适合作为单步搬运的测试环境 | 改为每脚本 fixture/dry-run/contract test,真实发布与回滚只在 Task 边界执行;Task 3 末尾保留一次完整 API → UI 验收 | +| P1-9 | 缺少平台元数据迁移边界 | **成立。** 原计划未涉及 | Task 5 新增元数据边界章节,明确复制项、仅旧仓保留项、`#NN` 引用语义、旧仓退役时点 | + +**未独立核实的项:** P0-2 中关于 runner 为 repo-scoped、标签仅为通用值、`dev` Environment 仅允许 `master`、secrets 分布于 repo 与 Environment 两层的描述,来自仓库 owner 的评审陈述,本次未通过 API 独立验证。Task 0 第 5 步会在执行时逐项确认实际配置。 From 5263f57890693a57a45dd80a9708d196bac6c3f3 Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 10:04:38 +0000 Subject: [PATCH 3/8] =?UTF-8?q?docs:=20=E6=8C=89=20PR=20#83=20=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E8=BD=AE=E8=AF=84=E5=AE=A1=E8=A1=A5=E5=85=A8=E8=B7=A8?= =?UTF-8?q?=E4=BB=93=20cutover=20=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 条意见全部受理,并按要求写入 Task、Global Constraints、验收与 回退表本身,不只追加评审记录。文档 195 行增至 258 行。 主要变更: - 跨仓互斥:GitHub concurrency 只在单 repository 内协调,无法阻止 三仓同时操作同一台 dev server。新增宿主机 flock 约束,Task 0 部署锁文件与获取脚本。 - Cutover 闭环:Task 2 新增 6 步 runbook,第 6 步回切收尾为强制项, 验镜像 ID、发布台账与部署所有权三项归属新仓,避免验收停在 旧仓重新发布成功而运行态仍在旧仓。 - Secrets:GitHub API 不能读回 secret 值,复制无法按字面执行。改为 名称清单导出加原始凭据源重新注入,不可得则轮换,禁止从日志或 运行环境反向导出,逐项登记来源与验证结果。 - 持久化路径:原方案示例仍位于新仓 checkout 根之下,与自身约束 矛盾。固定为 ~/.local/share/fusion/api/storage/files 与 ~/.config/fusion/runtime.env,验收增加文件数、总字节数、 权限 owner 与校验摘要四项比对。 - 平台绑定判定前移:Task 0 新增平台使用状态清单,活跃且影响运行态 的绑定并入 Task 2 同一次 cutover,Task 4 收窄为不影响运行态的项。 - 恢复点:git bundle 不含未跟踪文件,Task 0 增加 dirty 清单、 未提交资产独立归档与哈希、bundle verify 与还原验证。 - Task 0 移除无改动的真实发布与回滚,改为只读核验,旧仓回退演练 集中到 Task 2 runbook 执行一次。 - 新增本 PR 自身的合并边界章节:deploy.yml 无 paths 过滤,合并将 触发完整 API 发布,应按一次正式发布处理。 Co-Authored-By: Claude --- .../plans/2026-08-31-monorepo-merge.md | 109 ++++++++++++++---- 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md index 0bbb4135..dfe4c10a 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -4,7 +4,7 @@ > Base: `fusion-api@43cee73` / `fusion-ui@77b7fc2`(2026-08-31 复核基线) > Branch: `feat/monorepo-merge` > Target: 新建仓库 `HyxiaoGe/fusion` -> Review: PR #83 第一轮评审已受理,修订记录见文末 +> Review: PR #83 第一轮(9 条)与第二轮(6 条)评审均已受理,修订记录见文末 **目标:** 把 `fusion-api` 与 `fusion-ui` 合并为单一仓库 `fusion`,保留两边完整 git 历史,并把当前分散在两处、共约 3600 行的发布流水线收敛为一份参数化的可复用 workflow + 一组可本地执行的部署脚本。 @@ -28,7 +28,10 @@ - **搬迁、切换与流水线重写必须分三阶段,不得合并。** 流水线操作真实 docker 重启、alembic 迁移与用户上传文件目录,混做会使发布故障无法二分定位。 - **旧仓库在最终验收通过前保持完整发布与回滚能力。** 不得在切换完成前注销旧仓 runner、撤销旧仓 secrets 或归档旧仓。 -- **宿主机持久化状态与代码 checkout 路径必须分开处理。** 二者当前在 `~/project/fusion` 下混放,不得以同一次路径替换处理。 +- **宿主机持久化状态必须迁出仓库 checkout 树,而不只是换一个子目录。** 目标路径不得位于任何仓库 checkout 根之下(含新仓的 `~/project/fusion`)。持久化状态与代码 checkout 当前在 `~/project/fusion` 下混放。 +- **跨仓互斥必须由宿主机侧的锁承担。** GitHub concurrency group 只在单个 repository 内协调,无法阻止旧 `fusion-api`、旧 `fusion-ui` 与新 `fusion` 三方同时操作同一台 dev server。所有部署入口在宿主机获取同一把 `flock` 后才可操作 docker/systemd。 +- **cutover 必须以「运行态归属新仓」收尾。** 任何回退演练之后都要重新切回新仓并复验,验收不得停在"旧仓重新发布成功"这一步。 +- **secrets 不可从旧仓复制。** GitHub API 只能列出名称与元数据,不能读回值;一律从原始凭据源重新注入,找不到原始值的必须轮换,禁止从 workflow 日志或运行环境反向导出。 - `paths:` **不得用作 event-level 过滤**(见 P0-4)。变更检测由 workflow 内首个 `changes` job 承担,且始终提供一个恒定存在的 required gate job。 - 跨应用顺序由顶层 orchestrator 的 DAG 保证,concurrency 只负责防并发,不承担顺序语义。 - 抽取 shell 到 `ops/deploy/` 时**逐字搬运**,不得顺手"优化"。行为差异必须为零,抽取与改写分属不同 Task。 @@ -76,17 +79,31 @@ - `actions/setup-node` 的 `cache-dependency-path` - 7 份跨仓 plan 中的 `../fusion-ui/` 相对路径与文档绝对路径 -## Task 0:新仓基建与恢复点(新增) +## Task 0:新仓基建、恢复点与外部绑定清点 -前置于一切代码搬迁。目的是让 Task 1 的验收在新仓可执行,同时保证旧仓不失能。 +前置于一切代码搬迁。目的是让后续 Task 的验收在新仓可执行,同时保证旧仓不失能。**本 Task 不改动旧仓任何配置,也不执行真实发布。** 1. 创建 `HyxiaoGe/fusion`,默认分支 `master`。 -2. 为两个原仓库各打一份 `git bundle` 恢复点,异地留存。 -3. 复制 `dev` Environment 及其 protection rule、branch policy、branch protection;secrets/variables 按 repo 层与 Environment 层分别对应建立。 -4. **新增 runner 实例**(不迁移旧仓 runner),并为全部 runner 增加应用维度标签 `fusion-api` / `fusion-ui`;workflow 的 `runs-on` 同步改为带应用标签的组合。旧仓四个 runner 原样保留至 Task 5。 -5. 产出宿主机状态清单:`~/project/fusion` 下的目录、bind mount 源、`.env`、systemd unit 的实际 `WorkingDirectory`,逐项记录当前值。 - -**验收:** 新仓可跑通一个 hello-world workflow,分别命中 `fusion-api` 与 `fusion-ui` 标签的 runner;旧仓两条流水线仍可正常发布与回滚(各执行一次);bundle 可在临时目录成功还原。 +2. **恢复点(含未提交资产)**: + - 产出两仓的 dirty / untracked 精确清单(`git status --porcelain --untracked-files=all`); + - 未提交文件另做独立可恢复归档并记录哈希 —— `git bundle` 只包含 refs 与可达 object,**不含未跟踪文件**; + - bundle 以完整 refs 创建(`--all`)并执行 `git bundle verify`; + - bundle 与未提交资产各在临时目录还原一次并校验; + - 迁移分支只从精确 HEAD 创建,不在旧仓 master 上执行目录下沉提交。 +3. 复制 `dev` Environment 及其 custom branch policy、branch protection。 +4. **secrets 重新注入(非复制)**: + - 通过 API 导出旧仓 secret **名称清单**(repo 层与 Environment 层分别导出); + - 从密码管理器、部署主机安全配置或原始凭据源向新仓注入; + - 原始值不可得的 secret 一律轮换; + - webhook secret 与 deploy key 私钥同此处理; + - 新仓侧只验证存在性、权限边界与目标服务连通性,**禁止输出 secret 值**; + - 逐项登记「来源、注入位置、验证结果、是否轮换」。 +5. **新增 runner 实例**(不迁移旧仓 runner),并为全部 runner 增加应用维度标签 `fusion-api` / `fusion-ui`;workflow 的 `runs-on` 同步改为带应用标签的组合。旧仓四个 runner(`dev-server-fusion-api`、`windows-build-api-01`、`dev-server-fusion-ui`、`windows-build-01`)原样保留至 Task 5。 +6. **宿主机状态清单**:`~/project/fusion` 下的目录、bind mount 源、`.env`、三个 systemd unit 的实际 `WorkingDirectory`,逐项记录当前值、文件数、总字节数、权限与 owner。 +7. **外部平台使用状态清单**:逐个 Vercel / Railway 服务记录 —— 是否活跃、跟踪哪个 repo 与 branch、负责 dev 还是 production、是否自动部署。**据此判定每个绑定归属 Task 2 还是 Task 4**:凡仍活跃且影响运行态的,必须并入 Task 2 的同一次受控 cutover;只有不影响当前运行态的纯重构项才留到 Task 4。 +8. 在宿主机部署跨仓 `flock` 锁文件与获取脚本,供三个仓库的部署入口共用。 + +**验收:** 新仓可跑通 hello-world workflow 并分别命中 `fusion-api` / `fusion-ui` 标签的 runner;bundle 与未提交资产归档均通过还原验证;secrets 清单逐项登记完毕且新仓连通性验证通过;平台清单产出且每个绑定已判定 Task 归属。旧仓侧**只做只读核验**:runner 在线、workflow 配置未变、最近一次成功发布记录存在。**不在本 Task 执行真实发布或回滚**(真实旧仓回退演练集中到 Task 2 的 cutover runbook 执行一次)。 ## Task 1:历史合并与目录下沉 @@ -106,18 +123,39 @@ ## Task 2:受控 cutover -本 Task 是唯一改变生产行为的一步,独立执行、独立回退。 +本 Task 是唯一改变生产行为的一步,独立执行、独立回退。**必须按 runbook 顺序执行,不得跳步。** + +### 2.1 宿主机状态迁移(路径清单 A 类) + +持久化状态一律迁出仓库 checkout 树: + +| 内容 | 目标路径 | +|---|---| +| 上传文件 | `~/.local/share/fusion/api/storage/files` | +| 运行时配置 | `~/.config/fusion/runtime.env` | +| systemd release/current | `~/.local/share/fusion/*-current`(沿用既有范式) | + +- 迁移前全量备份,并记录文件数、总字节数、权限与 owner,对文件清单生成校验摘要; +- bind mount 与 `.env` 读取方指向新路径; +- 把 `cost-sync` unit 的 `WorkingDirectory` 与 `AssertPathExists` 收敛到 `%h/.local/share/fusion/*-current` 范式(与另两个 unit 一致),`daemon-reload` 后逐个验证启动; +- 旧路径仅保留**有明确退役时间**的过渡软链接,退役时间写入本文件。 + +### 2.2 Cutover Runbook + +1. 冻结三个仓库(旧 `fusion-api`、旧 `fusion-ui`、新 `fusion`)的 master 合并与手动发布; +2. 在宿主机获取跨仓 `flock` 部署锁; +3. disable 旧仓两条 workflow,再 enable 新仓 orchestrator(`detect changes → deploy API → deploy UI`); +4. 由新仓发布、验证,并记录部署所有权归新仓; +5. **回退演练**:先 disable 新仓,再 enable 旧仓,由旧仓发布并验证成功; +6. **回切收尾(不可省略)**:再次 disable 旧仓、enable 新仓,重新部署新仓当前 SHA,并验证最终镜像 ID、发布台账与部署所有权**全部归新仓**。 + +第 6 步是本 Task 的终点。缺少它,验收会停在"旧仓重新发布成功",而运行态实际停留在旧仓版本。 + +### 2.3 外部平台绑定 -1. **宿主机状态迁移**(路径清单 A 类): - - 先备份 `~/project/fusion/fusion-api/storage/files`; - - 确立与仓库布局解耦的稳定数据目录(例如 `~/project/fusion/data/api/storage/files`),bind mount 指向稳定目录; - - 代码 checkout 目录与数据目录分离,必要时以软链接兼容过渡期; - - 把 `cost-sync` unit 的 `WorkingDirectory` 与 `AssertPathExists` 收敛到 `%h/.local/share/fusion/*-current` 范式(与另两个 unit 一致),`systemctl --user daemon-reload` 后逐个验证启动; - - `${HOME}/project/fusion/.env` 的位置与读取方保持一致。 -2. 启用新仓顶层 orchestrator:`detect changes → deploy API → deploy UI`,只改一侧时跳过另一侧。环境级 concurrency 仅防并发。 -3. 首次切换选择低峰时段,切换后旧仓 workflow 置为 disabled 但不删除。 +Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway 绑定,在本 Task 的同一次 cutover 内完成切换,不得延后到 Task 4。 -**验收:** 新仓完成一次真实 API 发布 + 回滚;一次真实 UI 发布 + 回滚;一次同时修改两边的提交,确认执行顺序为 API → UI;确认上传文件在切换前后可正常读取(切换前上传一个文件,切换后下载校验);三个 systemd unit 均 active(`cost-sync` 需确认已脱离仓库 checkout 路径);**旧仓回退演练一次**(重新 enable 旧 workflow 并成功发布)。任一项失败即回退到旧仓。 +**验收:** 新仓完成一次真实 API 发布 + 回滚;一次真实 UI 发布 + 回滚;一次同时修改两边的提交,确认执行顺序为 API → UI;上传文件迁移前后的**文件数、总字节数、权限/owner、校验摘要四项全部一致**,并另做一次单文件上传下载验证;三个 systemd unit 均 active 且 `cost-sync` 已脱离仓库 checkout 路径;runbook 第 5 步旧仓回退演练成功;runbook 第 6 步回切后镜像 ID、台账、发布所有权均指向新仓。任一项失败即回退到旧仓并停止推进。 ## Task 3:抽取 shell 到 ops/deploy @@ -136,7 +174,7 @@ 1. 以 UI 侧精简实现为基线,逐条比对 API 侧多出的检查,分类为「真实约束」与「重复防御」。真实约束做成可选 hook,重复防御合并。**分类结果登记在本文件,本 Task 内不删除任何检查。** 2. 抽出 `_deploy-app.yml`,参数:应用名、镜像仓库、健康检查端点、迁移开关、依赖服务列表、回滚锚点校验策略。 3. **保留各应用现有 ACR repository 与 `` tag**(`seanfield/fusion-api` 与 `seanfield/fusion-ui` 已天然区分应用,见 P1-6)。新增 per-app 发布台账/manifest,记录每个应用的 last deployed SHA,解决 path-filtered 发布后两应用 SHA 分叉导致的回滚目标定位问题。 -4. Vercel root directory 改为 `apps/ui`;Railway 两个服务的 root directory 分别指向 `apps/api` / `apps/ui`。 +4. 处理 Task 0 判定为「不影响当前运行态」的平台项(例如非活跃服务的 root directory 归位)。**影响运行态的绑定已在 Task 2 切换完毕,本 Task 不得留有此类项。** 5. 分支保护、required checks 按新 workflow 的 job 名重建(gate job 为 required,应用 job 不是)。 **验收:** 两应用各发布、各回滚一次;构造仅改 UI 的提交,确认 API 的 last deployed SHA 未变且回滚目标仍可唯一解析;平台绑定逐条在控制台勾选确认并各触发一次真实构建。 @@ -174,7 +212,18 @@ | required check 因 path 跳过而永久 Pending | Task 1 | 恒定 gate job 始终产出结论,不使用 event-level `paths:` | | 跨应用发布顺序错误导致 SSE 契约不匹配 | Task 2 | orchestrator DAG 强制 API → UI;失败则整条流水线中止 | | shell 抽取引入行为差异 | Task 3 | 逐个抽取 + 单测,单个 revert 即可定位 | -| 整体方案失败 | 任意 | Task 0 的 git bundle + 未归档的旧仓,可完整回到合并前状态 | +| 三仓 workflow 同时操作 dev server | Task 2 | 宿主机 `flock` 跨仓互斥 + cutover 期间冻结三仓发布 | +| 验收停在旧仓、运行态未归新仓 | Task 2 | runbook 第 6 步回切收尾为强制项,验镜像 ID / 台账 / 所有权三项 | +| secret 原始值不可得 | Task 0 | 该项一律轮换,不从日志或运行环境反向导出 | +| 未提交资产在迁移中丢失 | Task 0 | bundle 之外另做未跟踪文件归档 + 哈希 + 还原验证 | +| 平台仍跟踪旧仓导致发布链未真正切换 | Task 2 | Task 0 产出平台使用状态清单,活跃绑定并入 Task 2 同一次 cutover | +| 整体方案失败 | 任意 | Task 0 的 git bundle + 未提交资产归档 + 未归档的旧仓,可完整回到合并前状态 | + +## 关于本 PR 自身的合并边界 + +`.github/workflows/deploy.yml` 的触发条件为 `on.push.branches: [master]`,**无 `paths` 过滤**。因此即便本 PR 只新增文档,合并到 master 仍会触发完整的 API 发布流水线:`publish`(Windows runner 构建并推 ACR)+ `deploy-dev`(alembic 迁移 + 容器重启)。 + +本 PR 的合并**不是操作层面无副作用的文档合并**,应按一次正式发布处理:选择合适时间窗口,合并后单独记录 CI 结论、部署结果与运行态验证。 ## 评审修订记录 @@ -192,4 +241,18 @@ PR #83 第一轮评审共 9 条,全部受理。逐条处理如下: | P1-8 | 每抽一个脚本就发布回滚成本过高 | **成立。** master 是发布通道,不适合作为单步搬运的测试环境 | 改为每脚本 fixture/dry-run/contract test,真实发布与回滚只在 Task 边界执行;Task 3 末尾保留一次完整 API → UI 验收 | | P1-9 | 缺少平台元数据迁移边界 | **成立。** 原计划未涉及 | Task 5 新增元数据边界章节,明确复制项、仅旧仓保留项、`#NN` 引用语义、旧仓退役时点 | -**未独立核实的项:** P0-2 中关于 runner 为 repo-scoped、标签仅为通用值、`dev` Environment 仅允许 `master`、secrets 分布于 repo 与 Environment 两层的描述,来自仓库 owner 的评审陈述,本次未通过 API 独立验证。Task 0 第 5 步会在执行时逐项确认实际配置。 +**第一轮遗留的「未独立核实」项已由第二轮复核结清**,见下节。 + +### 第二轮(PR #83,6 条) + +owner 已通过 GitHub API 独立复核第一轮标注为"未独立核实"的项,结论属实:runner 为 `dev-server-fusion-api` / `windows-build-api-01` / `dev-server-fusion-ui` / `windows-build-01`,均 repo-scoped 且标签仅通用值;两仓 `dev` Environment 均用 custom branch policy 且只允许 `master`;secrets 确实分布于 repo 层与 Environment 层。该标注已从文档中移除。 + +| # | 评审意见 | 核实结果 | 处理 | +|---|---|---|---| +| P0-10 | 跨仓 cutover 缺互斥与最终状态闭环 | **成立。** GitHub concurrency group 只在单 repository 内协调,无法阻止三仓同时操作同一台 dev server | Global Constraints 增加宿主机 `flock` 跨仓互斥与"cutover 必须以运行态归属新仓收尾"两条;Task 2 新增 6 步 cutover runbook,第 6 步回切收尾为强制项;Task 0 增加锁文件部署 | +| P0-11 | secrets 不能从旧仓直接复制 | **成立。** GitHub API 只能列出名称与元数据,不能读回值 | Global Constraints 增加禁止复制与禁止反向导出;Task 0 第 4 步改为名称清单导出 + 原始源重新注入 + 不可得则轮换 + 逐项登记来源/注入位置/验证结果/是否轮换 | +| P0-12 | 持久化状态仍未脱离 checkout | **成立,原方案自相矛盾。** 初稿示例 `~/project/fusion/data/api/storage/files` 仍位于新仓 checkout 根 `~/project/fusion` 之下,与自身 Global Constraint 冲突 | 固定为仓库外路径:上传文件 `~/.local/share/fusion/api/storage/files`、运行时配置 `~/.config/fusion/runtime.env`;Global Constraint 改为"必须迁出 checkout 树,而不只是换子目录";验收增加文件数、总字节数、权限/owner 与校验摘要四项比对 | +| P0-13 | 平台 cutover 边界需前移判定 | **成立。** Task 2 切换 Actions 发布权但平台绑定留到 Task 4,会导致发布链未真正切换 | Task 0 新增第 7 步平台使用状态清单(活跃性、跟踪 repo/branch、dev 或 production、是否自动部署)并据此判定 Task 归属;活跃且影响运行态的并入 Task 2;Task 4 收窄为不影响运行态的项 | +| P0-14 | git bundle 不保护未提交资产 | **原理成立,计数不适用于本仓库当前状态。** `git bundle` 只含 refs 与可达 object,确实不含未跟踪文件。但评审所述"9 个未跟踪 plan/spec 文件"在本会话 checkout(`a5ad5b5`)中不成立 —— `git status --porcelain --untracked-files=all` 返回 0 项,工作区干净。该状态应属 owner 本机工作区;由于迁移将从该工作区执行,防护仍然必要 | Task 0 第 2 步增加 dirty/untracked 精确清单、未提交文件独立归档与哈希、`--all` 创建 bundle 并 `git bundle verify`、两者各做还原验证、迁移分支只从精确 HEAD 创建 | +| P1-15 | Task 0 不必重复真实发布与回滚 | **成立。** Task 0 不修改旧仓 runner/workflow,无改动发布的风险高于收益 | Task 0 验收改为只读核验(runner 在线、workflow 配置未变、最近成功记录存在);真实旧仓回退演练集中到 Task 2 runbook 第 5 步执行一次 | +| — | 本 PR 合并会触发 master 发布 | **成立。** 实测 `deploy.yml` 为 `on.push.branches: [master]` 且无 `paths` 过滤,合并将触发 `publish` + `deploy-dev`(含 alembic 迁移与容器重启) | 新增「关于本 PR 自身的合并边界」章节,明确按一次正式发布处理 | From 3593131c347708ece13983de70f6c07d687cb5a9 Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 10:16:03 +0000 Subject: [PATCH 4/8] =?UTF-8?q?docs:=20=E6=8C=89=20PR=20#83=20=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E8=BD=AE=E8=AF=84=E5=AE=A1=E8=A1=A5=E9=BD=90=20cutove?= =?UTF-8?q?r=20=E5=8F=AF=E6=89=A7=E8=A1=8C=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 条意见全部受理,写入 Task、约束、验收与回退表。文档 258 增至 303 行。 主要变更: - orchestrator 调用路径:实测两边 workflow 均不暴露 workflow_call, 普通 workflow 无法被 job-level uses 加 needs 调用,原顺序下 Task 2 的 DAG 无对象可调。Task 1 新增产出 _deploy-api.yml 与 _deploy-ui.yml 两份逻辑原样、仅暴露 workflow_call 的外壳, 只改调用形式不动部署逻辑,Task 4 再合并为参数化版本。 - 跨仓互斥:实测两个旧仓 workflow 的 flock 调用数均为 0,而计划 声明不改动旧仓,故宿主机放锁文件不产生互斥。约束中锁的范围 收窄为新仓与宿主机操作,跨仓互斥改由 drain 门禁承担。新增 disable 不等于停止一条。 - Task 2 runbook 第 2 步改为显式 drain:查询三仓 queued 与 in-progress run,等待或安全取消至归零并二次确认,每次 enable disable 切换前重跑,三仓不得同时 enabled。锁的两个 方案中选择严格 drain,理由与可推翻性写入正文。 - branch protection 分两步 bootstrap:两旧仓 required check 同名 PR container validation,新仓尚未产生该 check 且 GitHub 要求 近 7 天内成功运行过,直接复制会使 PR 永久 Pending。Task 0 只 复制非 check 类保护,gate 成功运行一次后再设 required。 - changes 与 gate 行为契约具体化:完整 Git diff 不依赖 300-file 过滤,定义五种触发情形的 base head,gate 用 always 并验证应 运行的 job 成功而非 skipped 即放行,UI job 不得因 needs 的 skipped 被级联跳过。验收增加两个反例用例。 - per-app 台账语义表:权威来源为运行容器 immutable image ref 加 image ID,台账仅为可恢复投影,存于 checkout 树外,锁内原子 替换,定义三种恢复路径,禁止把部署时变化的 manifest commit 回 master 以免递归触发发布。 Co-Authored-By: Claude --- .../plans/2026-08-31-monorepo-merge.md | 77 +++++++++++++++---- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md index dfe4c10a..db0dc683 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -4,7 +4,7 @@ > Base: `fusion-api@43cee73` / `fusion-ui@77b7fc2`(2026-08-31 复核基线) > Branch: `feat/monorepo-merge` > Target: 新建仓库 `HyxiaoGe/fusion` -> Review: PR #83 第一轮(9 条)与第二轮(6 条)评审均已受理,修订记录见文末 +> Review: PR #83 三轮评审(9 + 6 + 5 条)均已受理,修订记录见文末 **目标:** 把 `fusion-api` 与 `fusion-ui` 合并为单一仓库 `fusion`,保留两边完整 git 历史,并把当前分散在两处、共约 3600 行的发布流水线收敛为一份参数化的可复用 workflow + 一组可本地执行的部署脚本。 @@ -29,7 +29,8 @@ - **搬迁、切换与流水线重写必须分三阶段,不得合并。** 流水线操作真实 docker 重启、alembic 迁移与用户上传文件目录,混做会使发布故障无法二分定位。 - **旧仓库在最终验收通过前保持完整发布与回滚能力。** 不得在切换完成前注销旧仓 runner、撤销旧仓 secrets 或归档旧仓。 - **宿主机持久化状态必须迁出仓库 checkout 树,而不只是换一个子目录。** 目标路径不得位于任何仓库 checkout 根之下(含新仓的 `~/project/fusion`)。持久化状态与代码 checkout 当前在 `~/project/fusion` 下混放。 -- **跨仓互斥必须由宿主机侧的锁承担。** GitHub concurrency group 只在单个 repository 内协调,无法阻止旧 `fusion-api`、旧 `fusion-ui` 与新 `fusion` 三方同时操作同一台 dev server。所有部署入口在宿主机获取同一把 `flock` 后才可操作 docker/systemd。 +- **跨仓互斥由 drain 门禁承担,`flock` 只覆盖新仓与宿主机侧操作。** GitHub concurrency group 只在单个 repository 内协调。**实测两个旧仓 workflow 均无任何 `flock` 调用**(`grep -c flock` = 0),而本计划不改动旧仓配置,因此仅在宿主机放置锁文件不产生跨仓互斥 —— 旧 workflow 会直接绕过。故:新仓 orchestrator 与宿主机手工操作持同一把 `flock`;cutover 期间的三仓互斥由 Task 2 的 drain 门禁保证(见 2.2)。 +- **`disable` 不等于停止。** GitHub 的 disable 只阻止后续触发,不影响已 queued / in-progress 的 run。任何"冻结"步骤都必须配合对三个仓库在途 run 的查询、等待或取消,直至归零。 - **cutover 必须以「运行态归属新仓」收尾。** 任何回退演练之后都要重新切回新仓并复验,验收不得停在"旧仓重新发布成功"这一步。 - **secrets 不可从旧仓复制。** GitHub API 只能列出名称与元数据,不能读回值;一律从原始凭据源重新注入,找不到原始值的必须轮换,禁止从 workflow 日志或运行环境反向导出。 - `paths:` **不得用作 event-level 过滤**(见 P0-4)。变更检测由 workflow 内首个 `changes` job 承担,且始终提供一个恒定存在的 required gate job。 @@ -90,7 +91,7 @@ - bundle 以完整 refs 创建(`--all`)并执行 `git bundle verify`; - bundle 与未提交资产各在临时目录还原一次并校验; - 迁移分支只从精确 HEAD 创建,不在旧仓 master 上执行目录下沉提交。 -3. 复制 `dev` Environment 及其 custom branch policy、branch protection。 +3. 复制 `dev` Environment 及其 custom branch policy。**branch protection 分两步 bootstrap**(P1-18):本 Task 只复制**非 check 类**保护规则;required status check 暂不设置 —— 两个旧仓当前的 required check 名为 `PR container validation`,而新仓此时尚未产生过该 check,且 GitHub 要求 required check 在目标仓库最近 7 天内成功运行过,直接复制会使新仓 PR 永久 Pending。gate 在新仓成功运行一次后(Task 1)再设为 required。 4. **secrets 重新注入(非复制)**: - 通过 API 导出旧仓 secret **名称清单**(repo 层与 Environment 层分别导出); - 从密码管理器、部署主机安全配置或原始凭据源向新仓注入; @@ -117,9 +118,25 @@ - `release-safety.yml` 拆为 `apps/api/release-safety.yml` 与 `apps/ui/release-safety.yml`,`release-safety-contract.sh` 改为接受契约文件路径参数; - 本地 composite action 路径、Dockerfile context、`cache-dependency-path` 逐一修正。 4. PR CI 改造为:始终触发 → 首个 `changes` job 判定 `api` / `ui` / `shared` → 应用 job 用 `if` 跳过 → 末尾恒定 required gate job。明确 `.github/**`、`ops/**`、根配置、共享文档各触发哪一侧。 -5. 部署 workflow 原样复制两份进新仓但**保持 disabled**,不接管发布。 - -**验收:** 新仓 PR CI 全绿;`apps/api` 与 `apps/ui` 的单测与 lint 在新仓通过(含上述被改动的 6 个测试文件);两个应用的镜像可在新仓成功构建(不推送、不部署);只改一侧的 PR 不触发另一侧的应用 job,但 required gate 仍产出成功结论。 +5. **部署 workflow 改造为 `workflow_call` 外壳**(P0-16)。实测两边现有 workflow 均**不暴露 `workflow_call`**,普通 workflow 无法被 orchestrator 以 job-level `uses` + `needs` 调用,用 `workflow_dispatch` 触发子 workflow 也无法在同一 run 内形成可证明的依赖链 —— 若不在此处理,Task 2 的 orchestrator 无可调用对象。故生成两份**逻辑原样、尚未参数化合并**的 app-specific reusable workflow: + - `.github/workflows/_deploy-api.yml`,仅暴露 `workflow_call`; + - `.github/workflows/_deploy-ui.yml`,仅暴露 `workflow_call`。 + + 这**只改变调用外壳,不合并也不优化部署逻辑**,仍满足"搬迁与重写分离"。两份在本 Task 保持不被任何已启用的 workflow 调用。Task 4 再合并为参数化 `_deploy-app.yml`。 +6. **`changes` 与 required gate 行为契约**(P1-19): + - `changes` 使用 checkout 后的**完整 Git diff**,不依赖 GitHub 的 300-file event 过滤; + - 分别定义 PR、push、首次 push、merge commit、`workflow_dispatch` 五种情形的 base / head; + - required gate 使用 `if: always()`; + - gate 必须验证「按 `changes` 结果**应当运行**的 app job 成功」,**不得**因 app job 为 `skipped` 就无条件通过; + - API 未变化而 UI 变化时,UI job **不得**因 `needs: deploy-api` 处于 `skipped` 而被级联跳过(用 `always()` + 显式结果判定,不用裸 `needs`); + - 恒定 gate 的 display name 在本 Task 确定并记入文档,供 Task 0/4 的 branch protection 引用。 +7. **`workflow_dispatch` 回滚契约**(P0-16): + - 目标应用参数 `api | ui | both`; + - API / UI **各自独立**的 rollback SHA,不假设两应用相同; + - 手动回滚时 `changes` **不得**按路径判定后跳过目标应用; + - 明确单应用与双应用回滚的执行顺序、失败中止行为与最终台账更新规则。 + +**验收:** 新仓 PR CI 全绿;`apps/api` 与 `apps/ui` 的单测与 lint 在新仓通过(含上述被改动的 6 个测试文件);两个应用的镜像可在新仓成功构建(不推送、不部署);只改一侧的 PR 不触发另一侧的应用 job,但 required gate 仍产出成功结论;构造一次「API 未变、UI 变」的 PR,确认 UI job **未**被级联跳过且 gate 正确判定;构造一次「应运行的 app job 失败」,确认 gate **失败**而非放行;`_deploy-api.yml` / `_deploy-ui.yml` 通过 `workflow_call` 语法校验且未被任何已启用 workflow 调用。 ## Task 2:受控 cutover @@ -142,20 +159,22 @@ ### 2.2 Cutover Runbook -1. 冻结三个仓库(旧 `fusion-api`、旧 `fusion-ui`、新 `fusion`)的 master 合并与手动发布; -2. 在宿主机获取跨仓 `flock` 部署锁; -3. disable 旧仓两条 workflow,再 enable 新仓 orchestrator(`detect changes → deploy API → deploy UI`); -4. 由新仓发布、验证,并记录部署所有权归新仓; -5. **回退演练**:先 disable 新仓,再 enable 旧仓,由旧仓发布并验证成功; -6. **回切收尾(不可省略)**:再次 disable 旧仓、enable 新仓,重新部署新仓当前 SHA,并验证最终镜像 ID、发布台账与部署所有权**全部归新仓**。 +1. 冻结三个仓库(旧 `fusion-api`、旧 `fusion-ui`、新 `fusion`)的 master 合并与手动 dispatch; +2. **Drain 门禁(显式,不可跳过)**:查询三个仓库全部 `queued` 与 `in_progress` 的 workflow run,等待其自然结束或安全取消,**直至三仓在途 run 归零**并二次确认。`disable` 只阻止后续触发,不影响已在途的 run,因此本步不能被"冻结"或"disable"替代; +3. disable 旧仓两条 workflow,**再次确认无在途 run**,然后 enable 新仓 orchestrator(`detect changes → deploy API → deploy UI`); +4. 由新仓发布、验证,并记录部署所有权归新仓(新仓 orchestrator 与宿主机手工操作全程持 `flock`); +5. **回退演练**:先 disable 新仓 → drain → enable 旧仓,由旧仓发布并验证成功; +6. **回切收尾(不可省略)**:disable 旧仓 → drain → enable 新仓,重新部署新仓当前 SHA,验证最终镜像 ID、发布台账与部署所有权**全部归新仓**。 + +每次 enable / disable 切换之间都要重跑第 2 步的 drain,三仓不得在任一时刻同时处于 enabled。第 6 步是本 Task 的终点:缺少它,验收会停在"旧仓重新发布成功",而运行态实际停留在旧仓版本。 -第 6 步是本 Task 的终点。缺少它,验收会停在"旧仓重新发布成功",而运行态实际停留在旧仓版本。 +**关于跨仓锁的取舍:** 评审提出两个方案 —— (1) 先发独立 PR 给旧仓加同一把 `flock`;(2) 不宣称锁覆盖三仓,改用严格 drain。**本计划采用方案 2**,理由是方案 1 要在 cutover 前夕修改旧仓的部署 job,而旧仓正是失败时的唯一回退路径,动它会削弱兜底。配合 runbook 的 drain 门禁与"三仓不同时 enabled"纪律,风险窗口已被覆盖。若后续认为需要双保险,方案 1 可作为独立前置 PR 追加 —— 这是可推翻的决策。 ### 2.3 外部平台绑定 Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway 绑定,在本 Task 的同一次 cutover 内完成切换,不得延后到 Task 4。 -**验收:** 新仓完成一次真实 API 发布 + 回滚;一次真实 UI 发布 + 回滚;一次同时修改两边的提交,确认执行顺序为 API → UI;上传文件迁移前后的**文件数、总字节数、权限/owner、校验摘要四项全部一致**,并另做一次单文件上传下载验证;三个 systemd unit 均 active 且 `cost-sync` 已脱离仓库 checkout 路径;runbook 第 5 步旧仓回退演练成功;runbook 第 6 步回切后镜像 ID、台账、发布所有权均指向新仓。任一项失败即回退到旧仓并停止推进。 +**验收:** 新仓完成一次真实 API 发布 + 回滚;一次真实 UI 发布 + 回滚;一次同时修改两边的提交,确认执行顺序为 API → UI;上传文件迁移前后的**文件数、总字节数、权限/owner、校验摘要四项全部一致**,并另做一次单文件上传下载验证;三个 systemd unit 均 active 且 `cost-sync` 已脱离仓库 checkout 路径;runbook 每次切换前的 drain 均确认三仓在途 run 为 0;第 5 步旧仓回退演练成功;第 6 步回切后镜像 ID、台账、发布所有权均指向新仓。任一项失败即回退到旧仓并停止推进。 ## Task 3:抽取 shell 到 ops/deploy @@ -173,9 +192,20 @@ Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway 1. 以 UI 侧精简实现为基线,逐条比对 API 侧多出的检查,分类为「真实约束」与「重复防御」。真实约束做成可选 hook,重复防御合并。**分类结果登记在本文件,本 Task 内不删除任何检查。** 2. 抽出 `_deploy-app.yml`,参数:应用名、镜像仓库、健康检查端点、迁移开关、依赖服务列表、回滚锚点校验策略。 -3. **保留各应用现有 ACR repository 与 `` tag**(`seanfield/fusion-api` 与 `seanfield/fusion-ui` 已天然区分应用,见 P1-6)。新增 per-app 发布台账/manifest,记录每个应用的 last deployed SHA,解决 path-filtered 发布后两应用 SHA 分叉导致的回滚目标定位问题。 +3. **保留各应用现有 ACR repository 与 `` tag**(`seanfield/fusion-api` 与 `seanfield/fusion-ui` 已天然区分应用,见 P1-6)。新增 per-app 发布台账解决两应用 last deployed SHA 分叉导致的回滚目标定位问题,其语义按下表固定(P1-20): + + | 项 | 约定 | + |---|---| + | 权威来源 | **运行容器的 immutable image ref + image ID**(`docker inspect` 可得),不是台账文件 | + | 台账定位 | 权威来源的**可恢复投影**,用于快速查询与审计,丢失可从运行态重建 | + | 存储位置 | 宿主机 checkout 树之外(`~/.local/share/fusion//release-ledger.json`) | + | 更新时机 | 发布验证通过后,在**同一把部署 `flock` 内原子替换**(写临时文件 + `rename`) | + | 失败与自动回滚 | 回滚完成后按回滚后的实际运行镜像重写台账,不保留失败中间态 | + | 手动回滚 | 按 `workflow_dispatch` 的目标应用与 SHA 更新对应条目,另一应用条目不变 | + | 宿主机丢失 | 从运行容器 image ref + ID 重建;容器也不存在则从 ACR tag 与部署记录人工恢复 | + | 禁止项 | **不得把部署时变化的 manifest commit 回 master** —— 会递归触发发布流水线 | 4. 处理 Task 0 判定为「不影响当前运行态」的平台项(例如非活跃服务的 root directory 归位)。**影响运行态的绑定已在 Task 2 切换完毕,本 Task 不得留有此类项。** -5. 分支保护、required checks 按新 workflow 的 job 名重建(gate job 为 required,应用 job 不是)。 +5. **required context 迁移**(P1-18):仅在 gate 的 job display name 确实变化时迁移 required context,迁移后**验证旧 context 已从保护规则中移除**,避免残留一个永不再产生的 required check。应用 job 始终不设为 required。 **验收:** 两应用各发布、各回滚一次;构造仅改 UI 的提交,确认 API 的 last deployed SHA 未变且回滚目标仍可唯一解析;平台绑定逐条在控制台勾选确认并各触发一次真实构建。 @@ -217,6 +247,11 @@ Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway | secret 原始值不可得 | Task 0 | 该项一律轮换,不从日志或运行环境反向导出 | | 未提交资产在迁移中丢失 | Task 0 | bundle 之外另做未跟踪文件归档 + 哈希 + 还原验证 | | 平台仍跟踪旧仓导致发布链未真正切换 | Task 2 | Task 0 产出平台使用状态清单,活跃绑定并入 Task 2 同一次 cutover | +| orchestrator 无可调用的部署单元 | Task 2 | Task 1 先产出 `_deploy-api.yml` / `_deploy-ui.yml` 两份 `workflow_call` 外壳 | +| 在途 run 绕过冻结继续操作 dev server | Task 2 | runbook 第 2 步 drain 门禁,每次 enable/disable 切换前重跑 | +| 新仓 required check 从未运行导致 PR 永久 Pending | Task 0 | branch protection 分两步 bootstrap,gate 成功运行一次后再设 required | +| gate 因 app job skipped 而误放行 | Task 1 | gate 用 `if: always()` 并验证「应运行的 job 成功」,含专门的反例验收 | +| 台账与运行态不一致 | Task 4 | 权威来源为运行容器 image ref + ID,台账仅为可恢复投影,锁内原子替换 | | 整体方案失败 | 任意 | Task 0 的 git bundle + 未提交资产归档 + 未归档的旧仓,可完整回到合并前状态 | ## 关于本 PR 自身的合并边界 @@ -256,3 +291,13 @@ owner 已通过 GitHub API 独立复核第一轮标注为"未独立核实"的项 | P0-14 | git bundle 不保护未提交资产 | **原理成立,计数不适用于本仓库当前状态。** `git bundle` 只含 refs 与可达 object,确实不含未跟踪文件。但评审所述"9 个未跟踪 plan/spec 文件"在本会话 checkout(`a5ad5b5`)中不成立 —— `git status --porcelain --untracked-files=all` 返回 0 项,工作区干净。该状态应属 owner 本机工作区;由于迁移将从该工作区执行,防护仍然必要 | Task 0 第 2 步增加 dirty/untracked 精确清单、未提交文件独立归档与哈希、`--all` 创建 bundle 并 `git bundle verify`、两者各做还原验证、迁移分支只从精确 HEAD 创建 | | P1-15 | Task 0 不必重复真实发布与回滚 | **成立。** Task 0 不修改旧仓 runner/workflow,无改动发布的风险高于收益 | Task 0 验收改为只读核验(runner 在线、workflow 配置未变、最近成功记录存在);真实旧仓回退演练集中到 Task 2 runbook 第 5 步执行一次 | | — | 本 PR 合并会触发 master 发布 | **成立。** 实测 `deploy.yml` 为 `on.push.branches: [master]` 且无 `paths` 过滤,合并将触发 `publish` + `deploy-dev`(含 alembic 迁移与容器重启) | 新增「关于本 PR 自身的合并边界」章节,明确按一次正式发布处理 | + +### 第三轮(PR #83,5 条) + +| # | 评审意见 | 核实结果 | 处理 | +|---|---|---|---| +| P0-16 | Task 2 orchestrator 在 Task 4 前无可调用的部署单元 | **成立。** 实测两边 workflow 均不暴露 `workflow_call`(`grep workflow_call` 无命中)。普通 workflow 无法被 job-level `uses` + `needs` 调用,`workflow_dispatch` 也无法在同一 run 内形成可证明的依赖链 —— 原顺序下 Task 2 的 DAG 无对象可调 | Task 1 新增第 5 步:产出 `_deploy-api.yml` / `_deploy-ui.yml` 两份**逻辑原样、仅暴露 `workflow_call`** 的外壳,只改调用形式不动部署逻辑,仍满足搬迁与重写分离;Task 4 再合并为参数化 `_deploy-app.yml`。同时新增第 7 步 `workflow_dispatch` 回滚契约(目标应用 `api\|ui\|both`、双 SHA 独立、手动回滚不被 `changes` 跳过、顺序与中止规则) | +| P0-17 | `flock` 未覆盖旧仓,disable 前缺 drain | **成立。** 实测两个旧仓 workflow 的 `flock` 调用数均为 **0**,而本计划声明不改动旧仓配置 —— 仅在宿主机放锁文件不产生互斥,旧 workflow 直接绕过。且 GitHub 的 disable 只阻止后续触发,不影响在途 run | Global Constraints 中 `flock` 范围诚实收窄为「新仓 orchestrator + 宿主机手工操作」,并新增「disable 不等于停止」一条;Task 2 runbook 第 2 步改为**显式 drain 门禁**(查询三仓 queued/in-progress → 等待或安全取消 → 归零并二次确认),每次 enable/disable 切换前重跑。**两方案中选择方案 2(严格 drain)**,理由与可推翻性写入 runbook | +| P1-18 | 新仓 branch protection 需要 bootstrap 顺序 | **成立。** 实测两个旧仓的 required check 同名 `PR container validation`;新仓此时未产生过该 check,而 GitHub 要求 required check 近 7 天内在目标仓成功运行过 | Task 0 第 3 步改为只复制非 check 类保护,required check 暂不设置;Task 1 确定恒定 gate 的精确 display name 并在成功运行一次后设为 required;Task 4 仅在 job 名确实变化时迁移 required context 并验证旧 context 已移除 | +| P1-19 | `changes` 与 gate 的行为契约需具体化 | **成立。** 原计划只写了结构,未定义语义 | Task 1 新增第 6 步契约:`changes` 用 checkout 后完整 Git diff 不依赖 300-file 过滤;分别定义五种触发情形的 base/head;gate 用 `if: always()`;gate 须验证「应运行的 app job 成功」而非 skipped 即放行;UI job 不得因 `needs: deploy-api` 的 skipped 被级联跳过。验收增加两个反例用例 | +| P1-20 | per-app 台账缺权威存储与原子更新语义 | **成立。** 原计划只写"新增台账"四字 | Task 4 第 3 步新增语义表:权威来源为运行容器 immutable image ref + image ID,台账仅为可恢复投影;存于 checkout 树外;发布验证通过后在同一把 `flock` 内原子替换;定义失败/自动回滚、手动回滚、宿主机丢失三种恢复路径;明确禁止把部署时变化的 manifest commit 回 master 以免递归触发发布 | From 557006a67a9f919002ea116bb1513e9cfd72a757 Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 10:23:39 +0000 Subject: [PATCH 5/8] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E8=AE=A1=E5=88=92=E4=B8=AD=E8=8C=83=E5=9B=B4=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=E4=B8=8E=E8=B7=AF=E5=BE=84=E6=B8=85=E5=8D=95=E7=9A=84?= =?UTF-8?q?=E7=9F=9B=E7=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不做什么章节声称 app/ 与 src/ 下不产生 diff,但路径清单 B 类要求 修改 fusion-ui/src/scripts/buildAndDeployWorkflow.test.ts,该文件 位于 src/ 之下,两处自相矛盾。 改为限定业务模块不产生 diff,并显式列出该测试文件为唯一例外。 API 侧 5 个同类测试位于 test/ 与 app/ 平级,不涉及此例外。 Co-Authored-By: Claude --- docs/superpowers/plans/2026-08-31-monorepo-merge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md index db0dc683..89d3e202 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -225,7 +225,7 @@ Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway ## 不做什么 -- 不改任何业务代码。目录搬迁之外,`app/` 与 `src/` 下不产生 diff。 +- 不改任何业务代码。目录搬迁之外,`apps/api/app/` 与 `apps/ui/src/` 下的**业务模块**不产生 diff。**唯一例外**:`apps/ui/src/scripts/buildAndDeployWorkflow.test.ts` 属路径清单 B 类(CI 契约测试,需限定 `filesUnder()` 扫描范围),虽位于 `src/` 之下但必须修改。API 侧 5 个同类测试位于 `test/`,与 `app/` 平级,不涉及此例外。 - 不引入 npm/pnpm workspace、Nx、Turborepo。当前两应用零共享代码,工具链只增复杂度。 - 不在本轮做 OpenAPI 契约同步。`scripts/export_openapi.py` 已存在但 UI 未消费,合并后具备条件,另开 plan。 - 不合并 `docker-compose.yml`。两边网络拓扑不同(api 侧有 `middleware`、`litellm_net`、`flyai` 三个网络),需单独评估。 From 56d410d0e8284af392729a51d675b400f78b5918 Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 10:36:12 +0000 Subject: [PATCH 6/8] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=20storage/files?= =?UTF-8?q?=20=E4=B8=A5=E9=87=8D=E7=A8=8B=E5=BA=A6=E7=9A=84=E6=9C=AA?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原文把 storage/files 直接写成用户上传文件持久化并当作事实,但存储 后端有 local minio oss 三种,由宿主机 .env 的 STORAGE_BACKEND 决定, 不在仓库内,本计划无法确认生产实际取值。 改为条件式表述,并补充两侧依据:local 时 local_storage.py 的 upload 直接写原始字节、download 直接读回,是原件唯一副本,且 models.py 有两处 storage_key 非空列与 storage_backend storage_key 唯一约束硬引用,丢文件会留下指向空对象的 DB 行;minio oss 时对象 存于远端,本地目录重要性大幅下降。 同时指出 deploy.yml L1428 对挂载的断言是无条件的,挂载存在不能 证明后端为 local;L1036 至 L1042 存在从容器内 tar 抢救文件的兼容 路径,说明该数据曾只存在于容器内并出过问题。 Task 0 第 6 步增加 STORAGE_BACKEND 实际取值的判定,结果决定 Task 2 中该项迁移的分量;Task 2 的 2.1 相应改为按判定结果执行。 Co-Authored-By: Claude --- docs/superpowers/plans/2026-08-31-monorepo-merge.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md index 89d3e202..6ac48ffc 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -50,10 +50,17 @@ |---|---|---| | `deploy.yml` L587 / L658 / L1994 | `cd ~/project/fusion` | 宿主机工作目录,与仓库名耦合 | | `build-and-deploy.yml` L282 / L391 | `cd ~/project/fusion` | 同上,两应用共用同一目录 | -| `deploy.yml` L1036/1042/1077/1206 | `./fusion-api/storage/files` bind mount | **用户上传文件持久化**,路径变更会挂载到空目录 | +| `deploy.yml` L1036/1042/1077/1206 | `./fusion-api/storage/files` bind mount | 文件对象持久化,路径变更会挂载到空目录。**严重程度取决于生产实际后端**,见下方说明 | | `deploy.yml` L1819 | `${HOME}/project/fusion/.env` | 运行时环境变量来源 | | `ops/litellm/fusion-litellm-cost-sync.service` | `%h/project/fusion/fusion-api` ×3(L5/L6 `AssertPathExists`、L13 `WorkingDirectory`) | **唯一直接耦合仓库 checkout 的 unit**,路径错则 unit 启动失败 | +**关于 `storage/files` 的严重程度(待 Task 0 判定):** 存储后端有三种 —— `local` / `minio` / `oss`(`.env.example` 默认 `local`),由宿主机 `${HOME}/project/fusion/.env` 的 `STORAGE_BACKEND` 决定,**不在仓库内,本计划无法确认生产实际取值**。 + +- 若为 `local`:`app/services/storage/local_storage.py` 的 `upload()` 直接写原始字节、`download()` 直接读回,该目录是**原件的唯一副本**;且 `app/db/models.py` 有两处 `storage_key = Column(String(600), nullable=False)` 与 `UniqueConstraint("storage_backend", "storage_key")` 硬引用,文件丢失会留下指向空对象的 DB 行,属数据不一致而非单纯缺文件。Task 2 的备份、四项校验与回退演练全部必要。 +- 若为 `minio` / `oss`:对象存于远端,本地目录重要性大幅下降,Task 2 可相应减负。 + +注意 `deploy.yml` L1428 对 `/app/storage/files` 挂载的断言是**无条件**的,因此"挂载存在"不能证明后端为 `local`。另 L1036–1042 存在一段从容器内 `tar` 抢救文件的兼容路径,说明该数据历史上曾只存在于容器内并出过问题 —— 是这批数据值得谨慎对待的旁证。 + 宿主机当前布局为 `~/project/fusion/fusion-api/…`,目录名直接镜像仓库名。**该布局与新仓库的 `apps/api` 结构不一致,且不能自动跟随。** **仓库内已有正确范式可循:** 另外两个 unit(`fusion-litellm-governance.service`、`fusion-litellm-model-management.service`)的 `WorkingDirectory` 指向 `%h/.local/share/fusion/litellm-governance-current` 与 `%h/.local/share/fusion/litellm-model-management-current`,即已与仓库 checkout 解耦的暂存目录。Task 2 应把 `cost-sync` 收敛到同一范式,而不是简单替换其路径字面量。 @@ -100,7 +107,7 @@ - 新仓侧只验证存在性、权限边界与目标服务连通性,**禁止输出 secret 值**; - 逐项登记「来源、注入位置、验证结果、是否轮换」。 5. **新增 runner 实例**(不迁移旧仓 runner),并为全部 runner 增加应用维度标签 `fusion-api` / `fusion-ui`;workflow 的 `runs-on` 同步改为带应用标签的组合。旧仓四个 runner(`dev-server-fusion-api`、`windows-build-api-01`、`dev-server-fusion-ui`、`windows-build-01`)原样保留至 Task 5。 -6. **宿主机状态清单**:`~/project/fusion` 下的目录、bind mount 源、`.env`、三个 systemd unit 的实际 `WorkingDirectory`,逐项记录当前值、文件数、总字节数、权限与 owner。 +6. **宿主机状态清单**:`~/project/fusion` 下的目录、bind mount 源、`.env`、三个 systemd unit 的实际 `WorkingDirectory`,逐项记录当前值、文件数、总字节数、权限与 owner。**其中必须先判定 `STORAGE_BACKEND` 的实际取值**(读宿主机 `${HOME}/project/fusion/.env`,取值为 `local` / `minio` / `oss` 之一),该结果决定 Task 2 中 `storage/files` 迁移的分量:`local` 时该目录为原件唯一副本,全套备份与校验必做;`minio` / `oss` 时可减负。判定结果记入本文件。 7. **外部平台使用状态清单**:逐个 Vercel / Railway 服务记录 —— 是否活跃、跟踪哪个 repo 与 branch、负责 dev 还是 production、是否自动部署。**据此判定每个绑定归属 Task 2 还是 Task 4**:凡仍活跃且影响运行态的,必须并入 Task 2 的同一次受控 cutover;只有不影响当前运行态的纯重构项才留到 Task 4。 8. 在宿主机部署跨仓 `flock` 锁文件与获取脚本,供三个仓库的部署入口共用。 @@ -144,7 +151,7 @@ ### 2.1 宿主机状态迁移(路径清单 A 类) -持久化状态一律迁出仓库 checkout 树: +持久化状态一律迁出仓库 checkout 树。**执行前先取 Task 0 第 6 步判定的 `STORAGE_BACKEND` 结果**:非 `local` 时上传文件一项可按实际情况减负,其余不变。 | 内容 | 目标路径 | |---|---| From 90b9796ec51fcbfc3d903614cb30ff0662741a38 Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 10:40:21 +0000 Subject: [PATCH 7/8] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=E8=B7=A8?= =?UTF-8?q?=E4=BB=93=20plan=20=E8=B7=AF=E5=BE=84=E5=AD=98=E9=87=8F?= =?UTF-8?q?=E5=B9=B6=E8=A6=81=E6=B1=82=E8=BF=81=E7=A7=BB=E6=97=B6=E5=81=9A?= =?UTF-8?q?=E5=88=86=E8=AF=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原文称需修正 7 份跨仓 plan 的 ../fusion-ui/ 相对路径,把「提到 fusion-ui 的 plan」与「含 ../fusion-ui/ 相对路径的 plan」混为一谈。 实测含该模式的仅 2 份,其中一份是本计划自身,真实存量为 1 份。 同时补充 Task 5 的分诊要求。实测两套记录体系互不索引:台账按 领域加 commit SHA 追踪,71 处 SHA 引用,而 54 份 plan 中仅 4 份 被台账点名;checkbox 追踪已基本废弃,25 份有 box 但 0 勾选、 10 份无 box、仅 9 份全勾。单看一份 plan 无法判断死活,这正是 AGENTS.md 第 11 条要求先读台账再查 git log 的原因。 Task 5 改为迁移时须标注已完成 进行中 已废弃或按状态分目录, 否则等于把索引缺口原样搬进新仓。 Co-Authored-By: Claude --- docs/superpowers/plans/2026-08-31-monorepo-merge.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/superpowers/plans/2026-08-31-monorepo-merge.md index 6ac48ffc..5922a6b8 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/superpowers/plans/2026-08-31-monorepo-merge.md @@ -85,7 +85,7 @@ - 两边 `.github/scripts/*` 的调用路径 - Dockerfile build context(`.` → `apps/api` / `apps/ui`) - `actions/setup-node` 的 `cache-dependency-path` -- 7 份跨仓 plan 中的 `../fusion-ui/` 相对路径与文档绝对路径 +- 跨仓 plan 中的相对路径与文档绝对路径。**实测存量很小**:`docs/superpowers/` 下含 `../fusion-ui/` 的仅 `2026-06-30-search-read-planner-ledger.md` 一份(另一份是本计划自身);提到 `fusion-ui` 但不含该相对路径模式的另有 6 份,只需按需核对 ## Task 0:新仓基建、恢复点与外部绑定清点 @@ -219,7 +219,11 @@ Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway ## Task 5:文档、台账与旧仓归档 1. 合并两份 `EXECUTION_LEDGER.md`,补一条本次合并记录。 -2. 合并 `docs/superpowers/plans` 与 `specs`,修正 7 份跨仓 plan 中的 `../fusion-ui/` 相对路径。 +2. 合并 `docs/superpowers/plans`(54 份)与 `specs`(23 份),修正 `2026-06-30-search-read-planner-ledger.md` 中的 `../fusion-ui/` 相对路径(实测存量仅此一份)。 + + **同时做一次分诊,不要原样搬运。** 实测当前两套记录体系互不索引:台账 `EXECUTION_LEDGER.md` 按「领域 + commit SHA」追踪(71 处 SHA 引用),54 份 plan 中仅 **4 份**被台账点名;checkbox 追踪已基本废弃(25 份有 box 但 0 勾选、10 份无 box、仅 9 份全勾)。因此**单看一份 plan 无法判断它是活的还是已完成** —— 这正是 `AGENTS.md` 第 11 条要求"必须先读台账再查 git log"的原因。 + + 迁移时至少做到:为每份 plan 标注「已完成 / 进行中 / 已废弃」,或按状态分目录(如 `plans/archive/`)。**否则等于把这个索引缺口原样搬进新仓。** 本步不要求补齐 checkbox,只要求可判定死活。 3. 根 `CLAUDE.md` 改为导航,`apps/api/CLAUDE.md` 与 `apps/ui/CLAUDE.md` 承载应用级约定;`AGENTS.md` 同理。`.agents/skills/` 中 10 个 skill 合并去重(`fusion-next-step` 两边各有一份需统一)。 4. **平台元数据边界**(见 P1-9):git 历史合并不迁移 Issues、PR、Actions runs、releases、webhooks、deploy keys、Environment protection、branch rules。明确: - 复制:Environment protection、branch rules、secrets/variables、webhooks; From 7df9b1c79ebbbe5a1178a83c3c7097d71dab0b1b Mon Sep 17 00:00:00 2001 From: Claude <6h898548vj@privaterelay.appleid.com> Date: Mon, 31 Aug 2026 11:02:40 +0000 Subject: [PATCH 8/8] =?UTF-8?q?docs:=20=E8=BF=81=E5=85=A5=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=97=A0=E5=85=B3=E7=9B=AE=E5=BD=95=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E5=AF=B9=E5=B7=B2=E5=88=A0=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回应 PR #84 评审的第二个 P1:#84 把发现入口收窄后,本计划若仍落在 docs/superpowers/plans 会落在无人扫描的目录,且其对已删文件的完整 路径引用会立即断链。 - 文件迁至 docs/implementation-plans/2026-08-31-monorepo-merge.md, 该目录由 #84 确立为与已弃用插件无关的稳定落位,并已纳入全部发现 入口。本分支不改 gitignore,其白名单随 #84 提供,因此本 PR 需在 #84 之后合并。 - 依据章节:对 2026-06-28-agent-run-continuation.md 的引用改为标注 该文件已由 #84 清理、原文存于 git 历史,并说明结论不受影响。 - 路径清单 C 类:含 ../fusion-ui/ 的唯一存量文件已随 #84 删除,本项 在新树上归零,只需对两个保留目录复查一次。 - Task 5 第 2 步重写:fusion-api 侧文档结构已由 #84 简化,本步改为 迁移 implementation-plans 与 specs,并指出 fusion-ui 侧尚未做过 同一轮清理、需先按同样口径核查再决定删除或迁移;同时要求 specs 脱离 superpowers 这一废弃插件名,且落位与发现入口必须一一对应、 在合并后的最终树上验证。 Co-Authored-By: Claude --- .../2026-08-31-monorepo-merge.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) rename docs/{superpowers/plans => implementation-plans}/2026-08-31-monorepo-merge.md (94%) diff --git a/docs/superpowers/plans/2026-08-31-monorepo-merge.md b/docs/implementation-plans/2026-08-31-monorepo-merge.md similarity index 94% rename from docs/superpowers/plans/2026-08-31-monorepo-merge.md rename to docs/implementation-plans/2026-08-31-monorepo-merge.md index 5922a6b8..fe59c0c0 100644 --- a/docs/superpowers/plans/2026-08-31-monorepo-merge.md +++ b/docs/implementation-plans/2026-08-31-monorepo-merge.md @@ -21,7 +21,7 @@ - **发布流水线已被手工写了两遍。** 两边步骤名逐字相同的有 14 个(去重后),另有 3 对同职能不同名(`Capture current deployment` ↔ `…for rollback`、`Rollback previous deployment` ↔ `Roll back failed deployment`、`Smoke check candidate` ↔ `Verify health`)。抽象成可复用 workflow 有实证支撑。 - **`deploy.yml` 的体量是内联 shell,不是 YAML 编排。** 全文 2563 行中 `deploy-dev` 单个 job 占 2339 行(L189–L2527),其中四个步骤合计 1757 行(占该 job 的 75%):`Pull and restart fusion-api` 693、`Roll back failed deployment` 476、`Capture current deployment for rollback` 307、`Verify health` 281。该逻辑当前不可本地执行、不可单测,唯一验证方式是向 master 推送。 - **同职能步骤两边体量差 10 倍。** Capture 307 vs 30 行,Rollback 476 vs 65 行。差额中一部分是 API 的真实约束(alembic 迁移、flyai-adapter 必须与 api 同 SHA、litellm governance 与 model management 两个 worker 的暂停/恢复),一部分是重复的防御性检查。分类结论见 Task 4,本计划不预判。 -- **双仓成本已有记录。** `docs/superpowers/plans/2026-06-28-agent-run-continuation.md` 明确写有"这份 spec 横跨 fusion-api 和 fusion-ui……实施时使用一个工作分支;最终按子仓分别提交,避免把半成品多次推送触发流水线"。 +- **双仓成本已有记录。** 原 `docs/superpowers/plans/2026-06-28-agent-run-continuation.md` 明确写有"这份 spec 横跨 fusion-api 和 fusion-ui……实施时使用一个工作分支;最终按子仓分别提交,避免把半成品多次推送触发流水线"。该文件已由 PR #84 随已弃用的 superpowers 插件文档一并清理,原文保留在 git 历史中(`git log --diff-filter=D --name-only -- 'docs/superpowers/plans/*'` 可定位)。**结论不受影响:跨仓协同需要一个工作分支、却只能按子仓分别提交,这一约束本身就是双仓成本的直接证据。** - **运行时已经耦合。** `fusion-ui/next.config.js` 通过 rewrites 把 `/api/*` 同源代理到 fusion-api,SSE 走此通道;两边共用同一组 self-hosted runner 类型、同一个阿里云 ACR、同一台 dev server,且**共用同一个宿主机工作目录 `~/project/fusion`**。 ## Global Constraints @@ -85,7 +85,7 @@ - 两边 `.github/scripts/*` 的调用路径 - Dockerfile build context(`.` → `apps/api` / `apps/ui`) - `actions/setup-node` 的 `cache-dependency-path` -- 跨仓 plan 中的相对路径与文档绝对路径。**实测存量很小**:`docs/superpowers/` 下含 `../fusion-ui/` 的仅 `2026-06-30-search-read-planner-ledger.md` 一份(另一份是本计划自身);提到 `fusion-ui` 但不含该相对路径模式的另有 6 份,只需按需核对 +- 文档中的跨仓相对路径与绝对路径。**PR #84 合并后存量归零**:此前唯一含 `../fusion-ui/` 的 `2026-06-30-search-read-planner-ledger.md` 已随 superpowers 遗留文档清理删除。本项在新树上只需对 `docs/implementation-plans/` 与 `docs/superpowers/specs/` 复查一次即可 ## Task 0:新仓基建、恢复点与外部绑定清点 @@ -219,11 +219,14 @@ Task 0 第 7 步判定为「仍活跃且影响运行态」的 Vercel / Railway ## Task 5:文档、台账与旧仓归档 1. 合并两份 `EXECUTION_LEDGER.md`,补一条本次合并记录。 -2. 合并 `docs/superpowers/plans`(54 份)与 `specs`(23 份),修正 `2026-06-30-search-read-planner-ledger.md` 中的 `../fusion-ui/` 相对路径(实测存量仅此一份)。 +2. 合并两仓文档目录。**PR #84 合并后 `fusion-api` 侧已简化**:`docs/superpowers/plans/` 已清空并随已弃用插件一并移除,实施计划的落位改为与插件无关的 `docs/implementation-plans/`(本文件即在此),保留的 `docs/superpowers/specs/` 23 份中三份是被台账与 `TRAJECTORY_DESIGN.md` 引用的承重契约。 - **同时做一次分诊,不要原样搬运。** 实测当前两套记录体系互不索引:台账 `EXECUTION_LEDGER.md` 按「领域 + commit SHA」追踪(71 处 SHA 引用),54 份 plan 中仅 **4 份**被台账点名;checkbox 追踪已基本废弃(25 份有 box 但 0 勾选、10 份无 box、仅 9 份全勾)。因此**单看一份 plan 无法判断它是活的还是已完成** —— 这正是 `AGENTS.md` 第 11 条要求"必须先读台账再查 git log"的原因。 + 因此本步只需:把 `fusion-api` 的 `docs/implementation-plans/` 与 `docs/superpowers/specs/` 迁入新仓,并对 `fusion-ui` 侧的 `docs/superpowers/` 做同一轮判定 —— **UI 侧尚未做过该清理**,需先按同样口径核查入边与插件遗留头,再决定删除或迁移,不要直接搬运。 + + **迁移后 `specs/` 应脱离 `superpowers/` 这个已废弃插件的名字**(建议 `docs/specs/`),并同步更新台账、`TRAJECTORY_DESIGN.md` 与全部发现入口。 + + **落位与发现入口必须闭环。** 教训来自 PR #84 评审:若发现入口(`AGENTS.md` 第 11 条、执行台账开头与检查清单、`fusion-next-step` skill)扫描的目录与实施计划的实际落位不一致,新计划会落在无人扫描的位置。新仓的入口配置必须与实际目录一一对应,并在合并后的**最终树**上跑一次残留引用检查,而不是只查单个分支。 - 迁移时至少做到:为每份 plan 标注「已完成 / 进行中 / 已废弃」,或按状态分目录(如 `plans/archive/`)。**否则等于把这个索引缺口原样搬进新仓。** 本步不要求补齐 checkbox,只要求可判定死活。 3. 根 `CLAUDE.md` 改为导航,`apps/api/CLAUDE.md` 与 `apps/ui/CLAUDE.md` 承载应用级约定;`AGENTS.md` 同理。`.agents/skills/` 中 10 个 skill 合并去重(`fusion-next-step` 两边各有一份需统一)。 4. **平台元数据边界**(见 P1-9):git 历史合并不迁移 Issues、PR、Actions runs、releases、webhooks、deploy keys、Environment protection、branch rules。明确: - 复制:Environment protection、branch rules、secrets/variables、webhooks;