部署 Afresto Next
堆疊:後端 Go(推送→GHCR→Watchtower 約 5 分鐘)· 網頁 React(Vite→Cloudflare Worker,
手動 wrangler deploy)· 行動版 RN/Expo 位於獨立儲存庫 <repo-mobile>
(EAS OTA)。生產部署 = 務必先取得用戶許可(每次皆須用戶同意)。
🔴 規則 #1 — 此儲存庫常同時被多個代理分頁使用
git status 常顯示其他工作階段修改的檔案。因此:
- 禁止
git add -a/git commit -a/git add .— 這會將其他階段未完成的工作一併納入你的 Commit。 - 選擇性地暫存你的檔案,然後驗證:
git diff --cached --name-only— 確保沒有混入不屬於你的檔案(常見誤加檔案:web/src/pages/ChatPage.tsx、backend/internal/db/dbgen/models.go,除非是你重新產生的結果)。 - 若檔案同時被兩個階段使用(例如 ExamPage)且你只想暫存自己的區塊:不要使用
stash/reset(這會從其他代理腳下拉走檔案)。請透過 Git 物件編輯索引:git show :path > tmp→ 從tmp移除他們的區塊 →SHA=$(git hash-object -w tmp)→git update-index --cacheinfo 100644,$SHA,path。磁碟上的檔案不會被改動。 - 🚨 Bash 工具的當前目錄可能因先前的
cd .../web(或 worktree)呼叫而固定。接著執行git add backend/...會失敗(pathspec did not match)或暫存到錯誤的樹。選擇性暫存前,請先**cd <儲存庫根目錄> &&**(或使用絕對路徑)。驗證git diff --cached --name-only顯示的是正確的根目錄相對路徑(而非../backend、src/...)。 - 部署後:檢查
git status— 確認其他工作階段的檔案仍完整無缺。
Go 品質閘門(送出後端 Commit 前)
類似 Laravel 的 Pint + PHPStan,專為 Go 設計。在暫存後端檔案前於本地執行:
- 格式化:
cd backend && gofmt -l .→ 必須為空。若有內容 →gofmt -w .(自動修正),暫存結果。(相當於pint --test。) - 建構 + 靜態檢查:
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
新功能 · 學校會看到的變更 · 資料庫結構變更 · 任何需要產品決策的事項 · 需要超過一個 PR 的工作。
何時不需要
明確且範圍小的錯誤 · 文字/標籤修正 · 文件 · PR 審查評論的直接後續處理。
有疑慮 → 建立 Issue。成本低,且可關閉。正是這個門檻維持了規則的生命力: 如果 每個小請求都變成 Issue,人們會開始悄悄繞過它,而一旦繞過 卻無後果,規則就失效了。
代理應執行的事項
- 適度瀏覽程式碼以填寫「初步發現」並找出阻礙因素——不要猜測。
- 建立 Issue,使用範本
.github/ISSUE_TEMPLATE/pekerjaan.md,加上modul:*標籤 (這會形成每個模組的記錄),若有事項懸而未決則加上butuh-keputusan。 - 停止。 不要撰寫程式碼,不要建立分支,不要自行指派任務。 任務指派是擁有者的行為;團隊也需要空間提供意見/修正想法。
- 將 Issue 編號回報給擁有者,並附上仍待決策的事項(若有的話)。
🔑 擁有者可跳過此規則,只需說「現在就做」— Issue 仍會建立作為 記錄,工作繼續進行。此規則設定的是預設行為,並非限制。
在執行過程中才發現的阻礙因素
在繼續之前務必告知:在 Issue 中留言(讓團隊也能看到)並通知擁有者。
若決策導致工作停止 → 加上 butuh-keputusan 標籤。
不要默默選擇自己的解讀然後繼續執行——那會產出「完成」但錯誤的工作。
閉環
PR 必須在正文中包含 Closes #<編號>。這會連結 Issue → PR → 人員,
使得 gh issue list --label modul:ujian --state all 成為該模組中誰在做什麼的
記錄。沒有這一行,軌跡就會中斷。
🔴 規則 #0 — 禁止直接 Commit/Push 到 main(4 人團隊,自 2026 年 7 月 25 日起)
所有工作均透過分支 + Pull Request;僅審查人員能合併到 main(合併 = 觸發部署)。
- 開始前:
git checkout main && git pull— 若有新的遷移,先執行遷移。 - 接觸程式碼前先建立/切換到主題分支:
- 功能 →
feat/<名稱>· 錯誤 →bugfix/<名稱>· 有 Issue 的錯誤 → 新分支 + 了解該 Issue。
- 功能 →
- 選擇性 Commit →
git push -u origin <分支>(而非 main)。 - 開 PR 到
main:若已登入gh則使用gh pr create;若尚未登入,提供用戶 URLhttps://github.com/Afresto-Next/next/compare/main...<分支>?expand=1。 - 審查人員核准並合併 → 部署。禁止自行合併 PR。
- 下方的「推送後端 / 部署 WEB」部分 = 在 PR 合併到 main 之後 執行的機械性操作 (通常由負責部署的人執行),並非允許直接推送至 main。
- 技能的變更也需透過 PR(CODEOWNERS)。
步驟順序
1. Commit(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」= 正常,忽略即可。
- Commit 到你的任務分支(
feat/*·bugfix/*),非main(參見規則 #0)。用戶要求在時才 Commit。
🔴 禁止 git add -A / git add <目錄>/ — 已三次誤加入用戶檔案
此儲存庫包含許多用戶未追蹤的工作檔案: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/到/檔案1 path/到/檔案2 # 永遠一個一個加入
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. 資料庫遷移 — 必須在頂端 Commit 中
.github/workflows/migrate.yml 透過 git diff --name-only HEAD~1 HEAD(僅最後一個 Commit)偵測遷移,在「建構並推送 Docker 映像檔」成功後執行(workflow_run)。
- 若推送包含遷移 → 遷移必須位於頂端 Commit(一個 Commit 包含遷移+程式碼 = 安全)。
- 遷移會在自託管 Runner VM 上於建構後自動執行。
run --rm migrate進行同步 → 若發生錯誤,Job 會直接失敗(非無聲失敗)。 - 驗證遷移:檢查該 Commit 的 Actions →「Migrate DB」顯示綠色(
ghCLI 已可用:gh run list --workflow="Migrate DB (self-hosted)" --limit 1— 代理可自行檢查,無需請用戶操作)。Watchtower 在下一次輪詢時(約 5 分鐘)交換api映像檔,幾乎總是在遷移完成後。 - 推送遷移前,透過本地資料庫的 psql
BEGIN; … ROLLBACK;(v102 = 正式環境)驗證 SQL。請參閱技能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}
" --max-time 15 https://next.afresto.co/api/v1/auth/me
# 401 = 正常(需要驗證)· 502 = 中斷
若為 502 → Actions →「Ops — 恢復 API」→ restart-docker(無需 SSH,1 分鐘內恢復)。
不要分析剛部署的功能:502 + 容器 Up = 需要重新啟動 Docker 守護行程。
詳情:docs/ops-auto-pulih.md。
4. 部署 WEB — 從乾淨的工作樹進行(因為有多個代理分頁)
npm run build 會讀取整個工作目錄 → 其他階段未完成的工作也會被發佈。因此請從你 Commit 的工作樹進行建構和部署:
rm -rf /c/wtd 2>/dev/null; git worktree add --detach /c/wtd <你的 Commit SHA>
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 <專案根目錄> && git worktree remove --force /c/wtd && git worktree prune
- 🔥 工作樹路徑必須簡短(
/c/wtd)— 暫存區內含長檔名 PDF(如 DKI 校曆) → Windows 會在 checkout 時報錯 「檔名過長」。 - 必要時驗證套件是否正確:
grep -rl "<你的功能標記>" /c/wtd/web/dist/assets/*.js— 但請注意誤判(常見標記可能來自其他功能)。 - Cloudflare 快取資產約 20 秒 → 使用 Ctrl+F5 測試。(200 但大小約 397 位元組 = 備援方案,非實際資產。)
5. 行動版 OTA — 獨立儲存庫,--environment 必須指定
cd "<行動版儲存庫>"
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 發送的是工作目錄,而非 Commit
與步驟 4 的 npm run build 完全相同 — 整個工作目錄都會被捆綁,
包括其他階段未完成的工作。發佈結果中 Commit <雜湊值>* 行上的 * 記號 = 目錄不乾淨。
發佈前務必執行 — 輸出結果,然後與發佈結果中的 Commit 行比對:
echo "從以下位置發佈:$(git branch --show-current) @ $(git log --oneline -1 --format=%h)"
git log --oneline HEAD..origin/main # 為空 = 沒有遺漏其他人的工作
- 從
main發佈,而非功能分支。從分支發佈時,已合併的其他人工作會從頻道中消失 — OTA 會完整取代整個套件,而非修補。 - 正式環境:先暫時擱置其他階段未 Commit 的變更
(
git stash push -- <檔案>…git stash pop)。正式環境不應包含未經審查的程式碼。
🚨 分支可能在工作中變更(多個代理分頁,同一個複本)
2026 年 8 月 20 日事件:預覽版明顯是從 main @ d0938fa 發佈;下一條指令
發現 HEAD 已位於其他分支 @ 8126a50 — 另一個階段在同一個複本中切換了分支。
正式環境曾短暫執行比預期多 3 個 PR 的程式碼,且沒有任何錯誤。
➡️ **不要假設分支在指令間保持不變。**每次重新輸出(上述指令)。 ➡️ 真正的預防措施:從工作樹進行 OTA,與步驟 4 的 Web 部署相同 — 其他階段無法移動其分支:
rm -rf /c/wtm 2>/dev/null; git worktree add --detach /c/wtm <你的 Commit SHA>
cd /c/wtm && git status --short # 必須為空
npm ci && npx eas update --branch <頻道> --environment <環境> --message "…" --non-interactive
cd <行動版儲存庫> && git worktree remove --force /c/wtm && git worktree prune
🪤 工作樹路徑簡短(/c/wtm)— 原因與步驟 4 相同。
🪤 工作樹中的 npm ci 需要時間;對於小型 OTA,可留在主複本執行,前提是必須執行上述分支+Commit 驗證,且結果相符。
🏢 GHCR 與 GitHub 組織 — 儲存庫現已移至組織 Afresto-Next
儲存庫(next、afresto-next-mobile、afresto-claude-skills)已從 ristology/* 移至組織 Afresto-Next(2026 年 7 月 20 日)。完整轉移操作手冊:docs/runbook-transfer-next-org.md。
- 🚨 將儲存庫移至組織 = GHCR 路徑的擁有者變更 → Watchtower 會無聲凍結**。** CI 標籤映像檔為
ghcr.io/${GITHUB_REPOSITORY_OWNER}/afresto-next-{api,web,migrate}— 擁有者跟隨儲存庫擁有者。若