# TASK · 生产发布通路改规则 + 内测首发方案(2026-10-14) - 日期:2026-10-14 - 来源:产品 10-14 内测体检后拍板:**内测跑生产**;发布规则**授权修改**(含 `.gitea/workflows/**`)。体检证据见本单 §2。 - 执行方:**Grok(Windows)**,分支 `codex/release-path-production-20261014`,只推这个分支。Grok 碰不到生产主机、不能 dispatch 生产工作流:本单产出的是 workflow / 脚本 / 测试 / runbook,生产上的每一步由产品在 Gitea 上点按钮执行。 - 验收:Claude(Linux)。合入 staging 由 Claude 做;**提升 main 与生产 dispatch 只由产品做**。 - BUG 编号:开工时核对最大号顺延(写作时 BUG-1317)。注意:其他已推任务书在 `docs/tasks/` 里已预占到 BUG-1346,开工时取 `docs/BUG_HISTORY.md` 与 `docs/tasks/` 已引用编号两者的最大号顺延。 - 串行:与 `TASK-beta-safety-net-20261014` 都可能改 `deploy/README.md`、`AGENTS.md`;**本单先做**,安全网单在本单合入后开工。与 issue #57(校正重算)、#58(Node 进程资源)不改同一文件;若 #58 改了 `deploy/docker-compose.server.yml`,本单不碰该文件。 ## 1. 基线 - 基线 commit:`origin/staging` `48a2fadd`(`0be4cb29` 之后只多文档提交);最近一次含门禁改动且已部署 staging 的提交以开工时 `/api/health` 为准。 - 生产:`https://jyotisha.chat/api/health` `deployment.gitCommit = 7b620c7a`(08-16);`origin/main = 7b620c7a`;main..staging 约 1674 个提交。 - 生产相关工作流(全部 `workflow_dispatch`,由产品手动触发):`release-quality-gate.yml`、`create-production-recovery.yml`、`migrate-production-database.yml`、`deploy-production.yml`。Gitea Actions 记录:四者最后一次运行都在 **08-16**(release 1872/1876/1880、recovery 1881、migrate 1882、deploy 1883)。 ## 2. 事故实证(按符号定位) ### 2.1 纯文档 head 卡死发布 `deploy-production.yml` 步骤「Validate tested revision and gate run」: 1. 要求 `REQUESTED_SHA` 自己有 `backend-quality-gate.yml` 的 `push / staging / success` 运行。纯文档提交不在 `deploy/gated-paths.txt` 内,**不触发门禁,永远没有这条记录**。 2. `controller_sha`(= main head)`== staging_head`,否则拒绝。 3. 非回滚时 `REQUESTED_SHA == staging_head`,否则 `stale production revision refused`。 4. `controller_sha != REQUESTED_SHA` 时再找 controller 的门禁运行(纯文档 controller 同样找不到)。 5. 要求 `REQUESTED_SHA` 有 `release-quality-gate.yml` 的 dispatch 成功运行;该工作流 checkout 的是 `gitea.sha`(被 dispatch 的分支 head),所以 head_sha 只能等于分支 head。 6. 部署前后 `require_current_release_heads`:`current_main == DEPLOY_SHA && current_staging == DEPLOY_SHA`。 `create-production-recovery.yml`(`GITEA_SHA == DEPLOY_SHA`,`main_head == staging_head == DEPLOY_SHA`,release 运行 head_sha == DEPLOY_SHA)与 `migrate-production-database.yml`(同样三条 + push 门禁运行)有相同约束。 结果:staging 只要在最后一次代码提交之后再推过任何文档(agent 循环每天都会推),四个生产工作流全部无法通过;唯一出路是冻结 staging。产品选择改规则。 ### 2.2 待上线迁移:63 个,全部是新增文件 `git diff --name-status origin/main origin/staging -- frontend/supabase/migrations frontend/db/migrations`:63 个 `A`,`frontend/db/migrations` 0 个。按顶层语句(去掉函数体后)分级: | 级别 | 数量 | 迁移 | 说明 | | --- | --- | --- | --- | | **A 破坏性 / 改写用户数据** | 2 | `20260925020000_chart_subject_typed_columns.sql`:先 backfill 到类型化列,再顶层 `delete from public.chart_profiles where role = 'self'`(本人资料改由 `profiles` 承载)。`20260921010000_retire_chat_session_archive.sql`:`update public.chat_sessions set archived_at = null where archived_at is not null`(所有已归档会话回到列表) | 不可逆;必须先在恢复点副本上演练 | | **B 改写数据(范围有限)** | 5 | `20260816010000_accept_exact_family_birth_times`(`profiles` 家人精确时间直接 accepted)、`20260822010000_declared_birth_window_clocks`(按时段回填申报窗口)、`20260823010000_rectification_occupation_dateless`(职业/外貌/胎记证据置 confirmed)、`20260906030000_rectification_block_scan_stage`(minute 阶段未采用 case 改 block_scan)、`20260925030000_chart_subject_trust_and_limit`(**他人档案的 accepted / confirmed 时间全部退回 reported、清空 active_birth_***) | 演练时逐条记行数;`20260925030000` 会让他人档案失去已采用时间,需产品知情 | | **C 权限收紧** | 约 45 | 绝大多数文件含 `revoke`(函数 / 表 EXECUTE、SELECT 收回) | 上线后 web / worker 角色可能失权(同类问题见 BUG-1063:07-18 迁移收回了 worker 对人物资料表的权限);演练后必须跑主路径冒烟 | | **D 纯新增** | 其余 | 加表 / 列 / 函数 / 索引 | 低风险 | 注:A+B 合计 7 条是按「顶层 `delete` / `update`」机械筛出;函数体内的 `delete`(注销、清理)不计。执行方需复核每条分级并补全 C/D 清单。 另:重复时间戳仍有 3 组(`20260806010000`、`20260816010000`、`20260920010000`),`frontend/db/migrations` 另有 `20260807010000` 一组。迁移器按文件名记账,功能无害,本单只记录不改名(改名会让已迁移的 staging 重复执行)。 ### 2.3 生产可以误开测试验证码 `frontend/src/modules/identity/config.ts::readTestOtp`:只要设置了 `IDENTITY_TEST_OTP`(6 位数字)就接受固定验证码,不区分环境。`deploy/validate-production-env.sh` 不检查这个变量。生产登录页当前负载 `testOtp: null`(只读 GET 核对),但没有任何防误配机制;内测跑生产后,误配一次即任何人可用固定码登录任意邮箱。 ## 3. 根因 - 生产工作流在 08-16 前设计,当时没有「纯文档提交不触发门禁」(`deploy/gated-paths.txt` 与 `deploy/is-docs-only-range.sh` 是之后才加的),四个工作流都假设「每个 staging head 都有门禁运行」。staging 侧已用 `is-docs-only-range.sh` 解决(AGENTS §2.4),生产侧没跟上。 - 生产停在 08-16 两个月,63 个迁移在 staging 逐个上线、逐个验证,但从未作为一批在生产数据上演练过。 - 测试验证码只在 staging 配置文档里出现,生产校验脚本是白名单思路,没把它列为禁止项。 ## 4. 决策记录(产品 2026-10-14 授权) 1. **授权修改 `.gitea/workflows/` 下的 `deploy-production.yml`、`create-production-recovery.yml`、`migrate-production-database.yml`、`release-quality-gate.yml`**(只为本单的发布规则;不改 staging 工作流的触发与部署逻辑)。这推翻 AGENTS.md §2.7「不改 `.gitea/workflows/**`」在本单范围内的限制,§2.7 其余(不自行提升 main、不动 DNS)不变。 2. **新发布规则**(取代「请求 SHA == staging head」): - 记 `H` = 当前 staging head,`main` 必须快进到 `H`(继续满足 AGENTS §2.5「main 与 staging 同一 SHA」,不得 merge)。 - 发布 SHA `R` = **最近一次 `backend-quality-gate` push 成功的 staging 提交**,并且 `deploy/is-docs-only-range.sh R H` 退出 0(R 是 H 的祖先,R..H 之间只改了门禁清单之外的路径)。 - 由于 R..H 不改门禁路径(含 `deploy/**`、`.gitea/**`、`frontend/**`、`scripts/**`),R 与 H 的部署脚本、镜像输入完全相同;controller 一律取 R 的门禁产物。 - `release-quality-gate` 仍从 main dispatch(跑在 H 上),记录的 head_sha = H;生产三个工作流接受「release 运行的 head_sha ∈ {R, 当前 main},且该 SHA 与 R 之间纯文档」。 - staging 公网 `/api/health` 的 `deployment.gitCommit` 必须等于 R(与 AGENTS §2.4 一致)。 - `allow_rollback=true` 的回滚语义不变(回滚目标仍须在 main 历史里、有门禁运行)。 3. 同步改 AGENTS.md §2.4–§2.5 与 `deploy/README.md` 的生产发布段落,文字与新规则一致。 4. 生产首发的执行顺序固定为:**release 门 → 恢复点(含还原验证)→ 迁移演练报告确认 → 生产迁移 → 部署 → 核对**。每一步由产品在 Gitea 点按钮;Grok 不 dispatch 任何生产工作流。 5. 生产运行时拒绝测试验证码(双保险:env 校验脚本 + 运行时)。 6. 不在本单做:备份定时化、监控告警、日志轮转(见 `TASK-beta-safety-net-20261014`);按用户限流与并发压测(另单);备案 / 主体 / 律师(产品后续自理)。 ## 5. 硬红线 1. 不得放宽任何安全检查:仍要求 R 有 push 门禁成功运行、有 release 门成功运行、有 ≤24h 且 `restore_verified=true` 的恢复点(迁移)、`main == staging`、生产 `previous_sha` 到 R 的单向前进校验、回滚显式确认。新规则只把「等于 staging head」换成「是 staging head 且之后纯文档」,`is-docs-only-range.sh` 返回 1 或 2 一律拒绝。 2. `is-docs-only-range.sh` 不改语义;若需在工作流里调用,用已 checkout 的 R 或 H 版本,API 模式用现有 `GITEA_TOKEN`,不新增密钥。 3. 不改 staging 部署工作流、不改 `deploy/gated-paths.txt`(若发现必须改,停下回报)。 4. 不在任何日志 / 回报里打印密钥、数据库口令、`.env.production*` 内容、恢复点加密密钥。 5. 迁移文件**不改**(已在 staging 执行过,改内容会造成两库不一致);需要补救就新增迁移,并在回报里说明。 6. Grok 不 dispatch 任何生产工作流、不 push main、不推 staging。 7. 不写真实用户资料。演练报告只写行数和表名,不写任何行内容。 ## 6. 任务分解 | 项 | 内容 | 验收标准 | | --- | --- | --- | | **T1 发布 SHA 解析脚本** | 新增 `deploy/resolve-release-revision.sh`(名字可调):输入 `REQUESTED_SHA`、读 main / staging head(Gitea API),校验 `main == staging == H`、`REQUESTED_SHA` 有 push 门禁成功运行、`is-docs-only-range.sh REQUESTED_SHA H` 退出 0;输出 `release_sha`、`gate_run_id`、`head_sha` 供后续步骤使用。四个生产工作流共用这一个脚本,不各写一份 jq。 | Python 测试(mock Gitea API 响应:本地 HTTP 桩或把 curl 换成可注入的读文件函数)覆盖:R==H 通过;R` → 记录迁移前 7 条 A/B 迁移涉及表的行数 → 按迁移器顺序执行 63 个迁移 → 记录迁移后行数与每个迁移的 `RAISE NOTICE` → 用 web 运行角色做只读冒烟(能读本人 profile、能读会话列表、worker 角色能读人物资料表——对应 BUG-1063)→ 删除一次性库 → 输出不含任何行内容的报告。复用 `run-production-recovery.sh` 的加密 / 还原 / 锁 / 磁盘检查写法,不另造。另写 `docs/testing/production-migration-rehearsal-20261014.md` 报告模板与通过判据 | 在**本地 PostgreSQL**(Docker 或 Claude 机器的替身)上用 staging 迁移前的 schema 快照跑通脚本(演示数据只能是虚构数据);判据写清:A 类两条的行数变化与预期一致、C 类冒烟全过、无迁移报错 | | **T6 迁移清单** | `docs/operations/production-migrations-20261014.md`:63 个迁移逐条:文件名、级别(A/B/C/D)、一句话作用、对生产用户的影响、演练要看的指标。§2.2 是初筛,执行方逐条复核 | 63 条齐全;A/B 每条有「产品需知」一句 | | **T7 禁止生产测试验证码** | `deploy/validate-production-env.sh`:出现 `IDENTITY_TEST_OTP=`(任何值,含空)即失败。`readTestOtp` 或其调用处:当 `AUTH_USER_ORIGIN` 为生产域名(`https://jyotisha.chat`)时,设置了测试码直接抛错拒绝启动(fail closed);staging 行为不变 | Shell 测试:生产 env 含该变量失败、不含通过;TS 测试:生产 origin + 测试码抛错、staging origin + 测试码返回码、未设置返回 null | | **T8 首发后核对清单** | `docs/testing/production-first-release-20261014.md`:`/api/health` 200 且 `deployment.gitCommit == R`、`apiGitCommit == R`、`swisseph_available == true`(内部 health)、`database.requiredMigrationsPresent == true` 且 `latestMigration` 等于 staging 同字段;`/login` 200 且负载 `testOtp` 为 null;未登录 `/api/account` 401;用受控邮箱走一遍登录 → 建盘 → 一次对话 → 打开报告页;后台 `/admin` 只有管理员能进 | 清单可照做,每条有预期值 | ## 7. 让步顺序 T1、T2、T3、T7 不可让(没有它们发不了版或有安全缺口)。T4 不可让(文档与规则不一致会让下一个人照旧文档操作)。T5 可先交脚本 + 本地验证,主机演练由产品执行后回填。T6、T8 可与 T5 一起交。 ## 8. 验收(执行方自测 + Claude 复验) - 前端:`tsc --noEmit` 0 错;`npm run lint` 0 error;`npm test` 失败名单与开工基线逐名一致(Windows 已知失败按 `tools/agent-loop/windows-known-failures.txt`)。 - Python:新增测试 + 快速门 `scripts/run_quality_gate.py --profile quick`。 - Shell:`bash -n` 全部新脚本;`shellcheck`(有则跑)。 - 工作流:本地无法运行 Gitea Actions——合同测试断言 workflow 文本;Claude 合入 staging 后,`backend-quality-gate` 会跑在含新 workflow 的提交上(`.gitea/**` 是门禁路径)。生产工作流的实跑由产品首发时完成,结果回填 T8 清单。 - 文档:`docs/BUG_HISTORY.md`(发布卡死、测试验证码各一条)、`CHANGELOG.md`(一句:生产可以在 staging 之后有文档提交时照常发布)、`docs/tasks/PROGRESS-release-path-production-20261014.md`、`docs/tasks/README.md` 状态。 ## 9. 开工前置 ``` git fetch origin --prune git status -sb git worktree add -b codex/release-path-production-20261014 .worktrees/release-path-production-20261014 origin/staging ``` 开工时记录 `origin/staging` SHA 与 staging `/api/health` 的 `deployment.gitCommit`,写进进度记录。 ## 9a. 生产首发步骤(写进 T4 runbook,由产品执行,本单合入 staging 并部署 staging 之后) 1. 确认 staging `/api/health` 的 gitCommit = R,且 `deploy/is-docs-only-range.sh R ` 退出 0。 2. 产品把 main 快进到 staging head:`git push origin :main`(或 Gitea 界面快进),不得 merge。 3. Gitea → Release Quality Gate,Use workflow from: `main`,等成功。 4. Gitea → Create Production Recovery Point,`deploy_sha = R`,记下 `recovery_reference`、`recovery_created_at`。 5. 主机上按 T5 脚本演练迁移(或由 Claude 远程指导),报告填进 `docs/testing/production-migration-rehearsal-20261014.md`;A 类两条行数、C 类冒烟全部符合判据才继续。 6. Gitea → Migrate Production Database,填 R、恢复点引用、时间、`restore_verified=true`(24 小时内)。 7. Gitea → Deploy production,`deploy_sha = R`,`allow_rollback=false`,`verification_mode=public`。 8. 按 T8 清单核对。失败:应用层用 Deploy production `allow_rollback=true` 回到 `7b620c7a`(注意:迁移已执行后旧代码可能读不了新 schema——回滚以「恢复点还原 + 回滚应用」为准,runbook 里写清由谁、怎么还原)。 ## 10. 需产品提供 / 拍板 1. **首发时间窗**:建议选晚上低峰,整个流程约 1–2 小时;首发前一天冻结 agent 循环向 staging 推代码(文档可以推)。 2. **`20260925030000` 他人档案已采用时间退回 reported**:内测前接受,还是另立补救迁移(例如只退回无校正记录的)——演练报告出来后拍板。 3. **`20260921010000` 已归档会话全部回到列表**:确认这是想要的(该迁移对应「取消归档功能」的产品决定)。 4. **迁移演练在哪执行**:主机上由产品执行脚本,或给 Claude 一次受控的主机只读 + 一次性库操作权限(默认前者)。 5. **回滚口径**:首发失败时是否接受「恢复点整库还原」(会丢失首发后写入的数据);内测开放前失败则无影响。 ## 11. 产品答复(2026-10-14,覆盖上文对应建议值) 1. 首发时间窗:首发前一天冻结代码推送(只许纯文档);首发失败**接受整库还原**到首发前恢复点。 2. 迁移演练由**产品本人**在 Gitea 上点按钮执行(Grok 只交脚本与 runbook);runbook 写成产品可照做的步骤,每步写清看哪个输出判断通过。 3. `20260925030000`(他人档案时间退回 reported):演练时先数生产受影响行数(预期 0,生产停在 08-16、人物档案 09 月下旬才上线),非 0 则停下报产品。