Communitygithub.com

部署 Afresto Next

一套用于部署 Go 后端、React Web 和 React Native 移动应用的代理技能,包含严格的 Git 提交规则、数据库迁移检查和生产环境部署流程。

部署 Afresto Next 是什么?

Afresto-Deploy 技能专为管理多仓库(Go 后端、React Web 和 React Native 移动端)的生产部署而设计。其核心规则包括:在多个代理会话共用的仓库中,必须选择性暂存文件而非全量 `git add`;所有有意义的工作必须先创建 GitHub Issue 并获得批准,然后才能编写任何代码;代码变更必须通过功能分支和拉取请求进行,严禁直接推送到 `main` 分支。该技能还提供了针对 Go 代码格式化、构建和静态分析的质量关卡,以及数据库迁移编号冲突的检测与处理(通过直接查询文件系统并排序)。部署流程分为三部分:后端通过 PR 合并触发 GHCR 构建和 Watchtower 自动更新;Web 构建需从干净的 Git 工作树进行(避免包含其他会话的半成品);移动端则通过 EAS OTA 从独立的仓库发布。此外,该技能还特别强调在合并后端代码后手动检查 502 错误,以及使用 `--environment` 参数进行 OTA 发布的重要性。支持平台包括 Claude Code 和类似代理,主要适用于 Afresto-Next 组织的项目。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add Afresto-Next/afresto-claude-skills

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

部署 Afresto Next 是做什么的?

技术栈:后端 Go (推送→GHCR→Watchtower 约5分钟) · Web React (Vite→Cloudflare Worker,手动执行 wrangler deploy) · 移动端 RN/Expo 位于 独立仓库 <repo-mobile> (EAS OTA)。生产部署规则:始终先征得用户同意(用户每次操作都需批准)。

🔴 规则 #1 — 此仓库常被多个代理会话同时使用

git status 经常显示的是其他会话修改过的文件。因此:

  • 不要执行 git add -a / git commit -a / git add .——这会将其他会话的半成品一并纳入你的提交。
  • 选择性暂存属于你自己的每个文件,然后验证:git diff --cached --name-only——确保没有混入不属于你的文件(常见误入的文件:web/src/pages/ChatPage.tsx、backend/internal/db/dbgen/models.go,除非这是你重新生成的)。
  • 对于两个会话共享的文件(例如 ExamPage),而你只想暂存自己的部分代码(hunk):不要使用 stash 或 reset(这会从另一个代理脚下抽走文件)。应通过 Git 对象编辑索引:git show :path > tmp → 清除 tmp 中他们的代码块 → SHA=$(git hash-object -w tmp) → git update-index --cacheinfo 100644,$SHA,path。磁盘上的文件不会受到影响。
  • 🚨 cwd 工具的 Bash 可能停留在之前调用的 cd .../web(或 worktree)目录。之后执行 git add backend/... 会失败,出现 pathspec did not match(或者暂存了错误的树)。在选择性暂存之前,先 cd <repo-root> &&(或使用绝对路径)。使用 git diff --cached --name-only 验证是否显示了正确的根相对路径(而不是 ../backend 或 src/...)。
  • 部署后:检查 git status——确保其他会话的工作文件仍然完好。

Go 质量关卡(在提交后端代码前)

类似 Laravel 中 Pint + PHPStan 的 Go 版本。在暂存后端文件之前,请在本地运行:

  • 格式化: cd backend && gofmt -l . → 必须输出为空。有输出 → 执行 gofmt -w .(自动修复),并暂存结果。(相当于 pint --test。)
  • 构建 + vet: go build ./... && go vet ./... 显示绿色(成功)。
  • 静态分析(目标): golangci-lint run ./...(包含 gofmt/govet/staticcheck/errcheck/ineffassign/unused/misspell)。相当于 phpstan analyse。
  • ⚠️ 状态:CI 质量关卡尚未激活。 尚未设置 backend/.golangci.yml 或 .github/workflows/ci.yml,且 staticcheck 版本已过时 → 因此目前手动执行 format+build+vet。计划在 CI 中强制执行并添加完整配置:docs/plan/00-tooling-quality-gate.md。生成的代码目录 internal/db/dbgen 排除在 lint 检查之外。
  • 测试需随变更一起执行 → 参考技能 afresto-testing (go test ./...;若无 TEST_DATABASE_URL 则跳过集成测试)。

🔴 规则 #00 — 有意义的工作必须先生成 GitHub Issue(自 2026 年 9 月 5 日起生效)

此规则优先于规则 #0(分支和 PR):在触碰任何代码之前执行。

目的并非形式主义。通过聊天提出的请求只有双方知晓——团队无法看到,所有者无法分配任务,模块不会留下任何痕迹记录谁在做什么。

何时必须创建 Issue

新功能 · 学校可见的更改 · 数据库 schema 更改 · 任何需要产品决策的事项 · 跨越多个 PR 的工作。

何时无需创建

明确且范围较小的 Bug · 文本/标签修正 · 文档 · 直接响应 PR 审查意见的后续操作。

如有疑问 → 创建 Issue。成本低,且可以关闭。正是这个门槛维持了规则的生命力: 如果每个琐碎的请求都变成 Issue,人们会开始悄悄绕过它;一旦绕过而没有任何后果,规则就失效了。

代理需要执行的操作

  1. 充分探索代码,以填写“初步发现”并找出阻碍——不要凭空猜测。
  2. 创建 Issue,使用模板 .github/ISSUE_TEMPLATE/pekerjaan.md,添加标签 modul:*(这有助于按模块记录),如果有待定事项,再加上 butuh-keputusan 标签。
  3. 停止。 不要编写代码,不要创建分支,不要自行分配任务。分配是项目所有者的行为;团队也需要空间提供意见或修改想法。
  4. 向项目所有者报告 Issue 编号,以及任何仍待定的决策(如有)。

🔑 项目所有者可以通过说“现在就开始做”来绕过此规则——Issue 仍然作为记录被创建,工作照常进行。此规则规定了默认行为,而不是限制。

在开发过程中发现的阻碍

在继续之前必须告知:在 Issue 中评论(以便团队看到)并且通知项目所有者。如果决策导致工作暂停 → 添加标签 butuh-keputusan。不要默默选择自己的解释然后继续——这会导致所谓“完成”但实际错误的工作。

关闭循环

PR 必须在其正文中包含 Closes #<编号>。这连接了 Issue → PR → 人员,使得 gh issue list --label modul:ujian --state all 成为该模块中谁在做什么的记录。没有这行,痕迹就会中断。

🔴 规则 #0 — 不要直接提交/推送到 main 分支(4 人团队,自 2026 年 7 月 25 日起生效)

所有工作通过分支 + 拉取请求进行;只有审查者才能合并到 main 分支(合并操作会触发部署)。

  1. 开始之前:git checkout main && git pull——如果有新的迁移,先运行迁移。
  2. 在触碰代码之前,创建或切换到一个有主题的分支:
    • 功能 → feat/<名称> · Bug → bugfix/<名称> · 有 Issue 的 Bug → 创建新分支并仔细阅读 Issue。
  3. 选择性提交 → git push -u origin <分支>(不是推送到 main)。
  4. 向 main 分支打开 PR:如果 gh 已登录,使用 gh pr create;否则,向用户提供 URL https://github.com/Afresto-Next/next/compare/main...<分支>?expand=1。
  5. 审查者批准并合并 → 部署。不要自己合并 PR。
  • 下面的“推送后端 / 部署 WEB”部分 = 在 PR 合并到 main 之后执行的机制(通常由负责部署的人执行),而不是允许直接推送到 main。
  • 技能的更改也需通过 PR(有 CODEOWNERS)。

步骤顺序

1. 提交(Bash 工具 = Git Bash / sh,而不是 PowerShell)

  • 多行消息:heredoc git commit -q -F - <<'EOF' … EOF。不要使用 @'…'@(这是 PowerShell 的 here-string → 标题会被污染为 @ ...)。
  • 在消息末尾添加:Co-Authored-By: Claude Opus 4.8 <[email protected]>。
  • 警告“CRLF will be replaced by LF” = 正常,可以忽略。
  • 提交到你的任务分支(feat/* · bugfix/*),不是 main(参见规则 #0)。用户在请求时提交。

🔴 不要执行 git add -A 或 git add <dir>/ —— 已经三次误加了用户文件

此仓库包含许多用户未跟踪的工作文件:docs/bskap/(图片和 PDF)、docs/analisis butir soal/、elibrary/、cloudflare/**/.wrangler/(账户缓存)。一旦使用 -A,所有文件都会被添加。

历史事件:git add docs/ 误加了 BSKAP 的 PDF/xls/图片(7 月 25 日和 8 月 2 日);git add -A 误加了 446 个文件,包括 wrangler-account.json(8 月 9 日)。

git add path/ke/berkas1 path/ke/berkas2        # 逐个添加,始终保持
git commit -q -F - <<'EOF'
...
EOF
git show --name-only --format="" HEAD | wc -l   # 检查数量是否符合预期?

如果数量不符:git reset --soft HEAD~1 && git reset,然后重新正确暂存。 如果已经推送到自己的分支,使用 git push --force-with-lease 是安全的。

2. 数据库迁移 — 必须位于最顶层的提交

.github/workflows/migrate.yml 通过 git diff --name-only HEAD~1 HEAD 检测迁移(仅检查最新的提交),在“构建并推送 Docker 镜像”步骤成功之后运行(workflow_run 事件)。

  • 如果推送包含迁移 → 迁移文件必须位于最顶层的提交(一个提交同时包含迁移和代码是安全的)。
  • 迁移会自动在自托管运行器 VM 上构建后执行。run --rm migrate 同步命令 → 如果出错,任务会硬失败(而不是静默失败)。
  • 验证迁移:检查 Actions → “Migrate DB (self-hosted)” 标签页是否为绿色(针对该提交)(gh CLI 可用:gh run list --workflow="Migrate DB (self-hosted)" --limit 1 —— 代理可以自行检查,无需麻烦用户)。Watchtower 会在下一次轮询(约 5 分钟)时替换 api 镜像,通常会在迁移完成后进行。
  • 在推送迁移之前,通过本地数据库上的 psql BEGIN; … ROLLBACK; 验证 SQL(v102 等同于生产环境)。请参考技能 afresto-db-change/堵漏工程 §4。

🔴 迁移编号:在合并前重新检查,而不是在创建分支时

在你分支打开期间,团队成员可能合并了另一系列迁移。重复的编号会导致 goose 在 sortAndConnectMigrations 处崩溃——这不是 SQL 错误,因此容易被误读。 ✅ 数据无损坏:崩溃发生在收集迁移列表时,在任何一个迁移被执行之前。

历史事件(2026 年 8 月 9 日):创建分支时选择了 00138;合并时,团队已经使用了 00138–00140。第一次修复将其重命名为 00140,但仍然冲突——因为又是猜测的结果。 计算,不要猜测:

git pull                                        # 必须先执行,以查看他人的迁移
MAX=$(ls backend/migrations/ | sed 's/_.*//' | sort -n | tail -1)
NEXT=$(printf "%05d" $((10#$MAX + 1)))
ls backend/migrations/ | sed 's/_.*//' | sort | uniq -d   # 必须为空

如果已经冲突:使用 git mv 重命名为新编号,创建一个小的 PR,合并,然后从 Actions 标签页重新运行 “Migrate DB (self-hosted)” 工作流。

3. 推送后端(在 PR 合并到 main 之后 — 参见规则 #0)

合并 PR → main 更新 → GHCR 构建 → Watchtower 约 5 分钟更新镜像。如果没有后端更改,则无需等待 Watchtower。(开发期间:使用 git push -u origin <分支> 然后创建 PR — 不要推送到 main。)

⚠️ 合并后端后:检查 502 错误(已复发 6 次)

重新创建 api/worker 容器经常导致完全中断——所有容器状态为 Up,但所有路由返回 502。实际上,每次后端合并都像是一场赌博。

curl -s -o /dev/null -w "%{http_code}\n" --max-time 15 https://next.afresto.co/api/v1/auth/me
# 401 = 健康(需要身份验证)· 502 = 中断

如果返回 502 → Actions → “Ops — pulihkan API” workflow → restart-docker(无需 SSH,1 分钟内恢复)。 不要分析刚部署的功能的细节:如果 502 且容器状态为 Up,则意味着需要重启 Docker 守护进程。 详细信息:docs/ops-auto-pulih.md。

4. 部署 WEB — 从干净的工作树进行(因为存在多个代理会话)

npm run build 会读取整个工作树 → 其他会话的半成品也会被包含在构建中。因此,请从你提交版本的独立工作树进行构建和部署:

rm -rf /c/wtd 2>/dev/null; git worktree add --detach /c/wtd <SHA-commit-mu>
cd /c/wtd && git status --short   # 必须为空(干净)
cd /c/wtd/web && npm ci && npm run build
cd /c/wtd/cloudflare/web-worker && npx wrangler deploy
cd <repo-projectTwo> && git worktree remove --force /c/wtd && git worktree prune
  • 🔥 工作树路径必须短 (/c/wtd) —— 过长的暂存区路径加 DKI 日历长文件名会导致 Windows 错误 “Filename too long” 检出失败。
  • 如有必要,验证构建包是否正确:grep -rl "<penanda fiturmu>" /c/wtd/web/dist/assets/*.js —— 但要小心误报(公共标识可能来自其他功能)。
  • Cloudflare 缓存资产约 20 秒 → 使用 Ctrl+F5 进行测试。(返回 200 但大小约为 397 字节 = 后备页面,而非实际资产。)

5. 移动端 OTA — 位于独立仓库,必须指定 --environment

cd "<repo-mobile>"
npx eas update --branch preview --environment preview --message "…" --non-interactive
  • 🔥 --environment 必须指定(preview 或 production)—— 不指定会导致更新到错误的环境或失败。
  • 移动端仓库有远程 GitHub 仓库 (Afresto-Next/afresto-next-mobile) → 因此规则 #0 适用:分支 → PR → 合并,不要直接推送到 main。
  • OTA 仅更新 JS/资产。原生模块的更改 → 通过 OTA 将导致崩溃 → 必须重新构建原生包。

🔥 eas update 发送的是工作树,而不是提交

这与步骤 4 中的 npm run build 完全一样——整个工作目录都被打包,包括其他会话的半成品。发布结果中 Commit <hash>* 行的 * 符号 = 工作树不干净。

必须在发布前立即执行——打印结果,然后与发布输出的 Commit 行进行核对:

echo "publish dari: $(git branch --show-current) @ $(git log --oneline -1 --format=%h)"
git log --oneline HEAD..origin/main   # 为空 = 没有其他人未合并的工作
  • 从 main 分支发布,而不是功能分支。从功能分支发布,其他已合并人员的工作将丢失于渠道中——OTA 会替换整个包,而不是增量更新。
  • 生产环境:先暂存其他会话的未提交更改(git stash push -- <berkas> … git stash pop)。生产环境不应包含未经审查的代码。

🚨 分支可能在开发过程中变动(多个代理会话,单个克隆仓库)

2026 年 8 月 20 日事件:预览版发布明确来自 main @ d0938fa;但下一个命令发现 HEAD 已经指向另一个分支 @ 8126a50——另一个会话在同一个克隆仓库中切换了分支。生产环境因此运行了超前 3 个 PR 的代码,且没有任何错误。

➡️ 不要假设分支在命令之间保持不变。 每次都重新打印(使用上面的命令)。 ➡️ 真正的预防措施:从 WORKTREE 发布 OTA,类似于步骤 4 中的 Web 部署——其他会话无法移动它的分支:

rm -rf /c/wtm 2>/dev/null; git worktree add --detach /c/wtm <SHA-commit-mu>
cd /c/wtm && git status --short          # 必须为空
npm ci && npx eas update --branch <ch> --environment <env> --message "…" --non-interactive
cd <repo-mobile> && git worktree remove --force /c/wtm && git worktree prune

🪤 工作树路径要短 (/c/wtm) —— 原因与步骤 4 相同。 🪤 在工作树中运行 npm ci 需要时间;对于小的 OTA,如果执行了上述分支+提交验证并且结果匹配,可以保留在主克隆仓库中。

🏢 GHCR 和 GitHub 组织 — 仓库现在位于组织 Afresto-Next

仓库 (next、afresto-next-mobile、afresto-claude-skills) 已从 ristology/* 移至组织 Afresto-Next(2026 年 7 月 20 日)。完整的迁移运行手册:docs/runbook-transfer-next-org.md。

  • 🚨 仓库迁移到组织后,GHCR 路径的 owner 字段随之改变 → Watchtower 会静默冻结。 CI 为镜像添加标签 ghcr.io/${GITHUB_REPOSITORY_OWNER}/afresto-next-{api,web,migrate} — owner 字段跟随仓库所有者。自托管运行器的 image: 字段是硬编码的,因此如果 CI 标签推送到新路径,但运行器仍在拉取旧路径,Watchtower 会停留在旧镜像上,并且 Docker Compose 启动时会回退到本地缓存(如果未拉取新标签,则可能使用旧缓存)。解决方案:在所有镜像标签稳定后的下一次 PR 中,更新 docker-compose.yml 中的 image: 字段。
  • 对于非 next 仓库(技能、移动端):仅影响 image: 标签的含义。Docker Compose 未使用这些,因此无需更改。
  • 组织级机密(例如 TEST_DATABASE_URL、DOPPLER_TOKEN)在 Afresto-Next 组织中的行为应与之前相同。

相关技能