Files
Jyotisha/docs/tasks/TASK-release-path-production-20261014.md
T

18 KiB
Raw Blame History

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<H 且纯文档通过;R<H 但中间改了 frontend/** 拒绝;R 不是 H 祖先拒绝;R 无门禁运行拒绝;main≠staging 拒绝;API 失败(exit 2)拒绝
T2 deploy-production 「Validate tested revision」改用 T1;controller 一律取 R 的门禁产物;release 门检查接受 head_sha ∈ {R, H}(且纯文档);require_current_release_heads 改为 main == staging 且 is-docs-only-range R current_head 退出 0;回滚分支逻辑不变 扩展 frontend/tests/staging-backend-workflows.test.ts(或新增同类测试)对 workflow 文本做合同断言:仍检查 push 门禁、release 门、main==staging、回滚确认;不再出现「REQUESTED_SHA != staging_head 即拒绝」的非回滚条件;调用 T1 脚本
T3 create-production-recovery / migrate-production-database / release-quality-gate recovery 与 migrate 的 GITEA_SHA == DEPLOY_SHA、main == staging == DEPLOY_SHA 改为 T1 语义(dispatch 自 main,DEPLOY_SHA = R);release 门不改 checkout,但在 job summary / 输出里写明「H 相对最近门禁 R 是否纯文档」 同 T2 的合同测试;三个工作流对「R 落后 H 且纯文档」给出同一结论
T4 文档 AGENTS.md §2.4–§2.5、§2.7(注明 10-14 授权范围);deploy/README.md 生产发布段落;新增 docs/operations/production-release-runbook-20261014.md:产品逐步点哪个按钮、填什么、看到什么算成功、失败怎么办 runbook 每一步可照做;与新 workflow 文本一致(Claude 逐条对照)
T5 迁移演练脚本与报告模板 新增 deploy/rehearse-production-migrations.sh(只在主机上由产品执行):从最新恢复点解密 → 还原到一次性数据库 rehearse_<时间> → 记录迁移前 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 <staging head> 退出 0。
  2. 产品把 main 快进到 staging head:git push origin <staging head>: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 则停下报产品。