376 lines
14 KiB
Markdown
376 lines
14 KiB
Markdown
# GitHub Actions Runner 穩定性修復
|
||
|
||
## 問題: `_diag/pages` 檔案衝突
|
||
|
||
```
|
||
Error: The file '/home/wooo/actions-runner-awoooi/_diag/pages/xxx.log' already exists.
|
||
```
|
||
|
||
### 根因分析 (2026-03-29 完整診斷)
|
||
|
||
1. **發生時機**: "Set up job" 階段 (在任何自定義步驟之前)
|
||
2. **原因**: GitHub Actions Runner 內部 bug
|
||
- Runner 在 Job 初始化時寫入 `_diag/pages/*.log`
|
||
- 並行執行的多個 Job 可能寫入相同的 UUID 檔案
|
||
- 這發生在我們的清理步驟執行**之前**
|
||
3. **次要問題**: `RUNNER_TEMP` 共享
|
||
- `_work/_temp/_runner_file_commands` 在所有 Jobs 之間共享
|
||
- 清理此目錄會導致 "Missing file at path" 錯誤
|
||
|
||
### 解決方案 (v4 - 最終版 2026-03-29)
|
||
|
||
#### 1. 序列建構 (核心修復)
|
||
```yaml
|
||
# build-api 必須等 build-web 完成
|
||
build-api:
|
||
needs: [detect-changes, build-web] # 關鍵: 依賴 build-web
|
||
```
|
||
|
||
**根因**: Job 並行時,"Set up job" 階段會同時寫入 `_runner_file_commands`,導致衝突
|
||
**解法**: 改為序列執行,確保同一時間只有一個 Job 在 Runner 上
|
||
|
||
#### 2. Workflow Concurrency (輔助)
|
||
```yaml
|
||
concurrency:
|
||
group: cd-${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
```
|
||
|
||
確保同一時間只有一個 workflow 在執行
|
||
|
||
#### 3. Job 層清理 (防禦性)
|
||
每個 Job 開始時清理 `_diag/pages`:
|
||
|
||
```yaml
|
||
- name: "Clean Runner Diagnostics"
|
||
run: |
|
||
RUNNER_ROOT=$(dirname "$(dirname "$RUNNER_TEMP")")
|
||
rm -rf "$RUNNER_ROOT/_diag/pages" .claude/worktrees 2>/dev/null || true
|
||
mkdir -p "$RUNNER_ROOT/_diag/pages" 2>/dev/null || true
|
||
```
|
||
|
||
**警告**: 絕對不要清理 `$RUNNER_TEMP/*`,會破壞 `_runner_file_commands`
|
||
|
||
#### 2. Systemd Timer (背景清理)
|
||
每 5 分鐘自動清理過期的診斷檔案:
|
||
|
||
```bash
|
||
# 部署
|
||
ssh wooo@192.168.0.110
|
||
cd /path/to/awoooi/ops/runner
|
||
bash deploy-runner-cleanup.sh
|
||
```
|
||
|
||
### 檔案說明
|
||
|
||
| 檔案 | 用途 |
|
||
|------|------|
|
||
| `cleanup-runner-diag.sh` | 清理腳本 (安裝到 Runner 目錄) |
|
||
| `runner-diag-cleanup.service` | Systemd service 定義 |
|
||
| `runner-diag-cleanup.timer` | Systemd timer (每 5 分鐘) |
|
||
| `deploy-runner-cleanup.sh` | 一鍵部署腳本 |
|
||
|
||
### 監控
|
||
|
||
```bash
|
||
# 查看 timer 狀態
|
||
sudo systemctl status runner-diag-cleanup.timer
|
||
|
||
# 查看清理日誌
|
||
journalctl -u runner-diag-cleanup.service -f
|
||
|
||
# 手動觸發清理
|
||
sudo systemctl start runner-diag-cleanup.service
|
||
```
|
||
|
||
### 相關文件
|
||
- Memory: `feedback_runner_zombie_process.md`
|
||
- ADR: 待建立 (如果問題持續)
|
||
|
||
## 問題: Gitea act-runner 並行 Docker Build 讓 Job Container 消失
|
||
|
||
### 症狀
|
||
|
||
```
|
||
Error response from daemon: RWLayer of container <id> is unexpectedly nil
|
||
Error response from daemon: No such container: <id>
|
||
```
|
||
|
||
### 根因分析 (2026-04-30)
|
||
|
||
1. AWOOOI CD 在 `Build and Push Web` 仍執行 Next.js production build 時,110 的 `gitea-runner` 又接了另一個 repo 的 Actions task。
|
||
2. 兩個 task 共用同一個 Docker daemon 與同一個 act-runner 容器;act-runner `capacity: 2` 允許跨 repo 並行。
|
||
3. 第二個 task 啟動後,第一個 AWOOOI job container 被 Docker/act 清掉,BuildKit 後續只看到 `RWLayer ... unexpectedly nil`。
|
||
4. Web image 在 110 host 直接 `docker build` 可成功,證明不是 Web 程式 build error。
|
||
|
||
### 第一層修復
|
||
|
||
1. 110 act-runner 必須單工:
|
||
|
||
```yaml
|
||
# /home/wooo/act-runner/config.yaml
|
||
runner:
|
||
capacity: 1
|
||
```
|
||
|
||
2. AWOOOI CD workflow 需要 Docker daemon 全域 lock:
|
||
|
||
```yaml
|
||
- name: Acquire Docker Build Lock
|
||
run: docker network create awoooi-cd-docker-build-lock
|
||
```
|
||
|
||
實作使用 Docker network 當 host-global lock,因為 `/tmp/flock` 只存在 transient job container 內,無法跨 repo/跨 container 生效。
|
||
|
||
3. 若 job 非正常中止留下 lock,下一次 CD 會在 lock 超過 2 小時後移除 stale network。
|
||
|
||
### 第二層修復: host label build/deploy
|
||
|
||
`capacity: 1` 與 Docker network lock 可避免跨 repo 並行,但長時間
|
||
`docker build` 仍可能讓 transient act job container 在 build 收尾時消失。
|
||
2026-04-30 起,AWOOOI CD 拆成三段:
|
||
|
||
| Job | runner label | 用途 |
|
||
|-----|--------------|------|
|
||
| `tests` | `ubuntu-latest` | API unit + B5 integration tests,仍跑在 ci-runner container |
|
||
| `build-and-deploy` | `awoooi-host` | Harbor login、API/Web image build/push、GitOps deploy,直接跑在 110 host |
|
||
| `post-deploy-checks` | `ubuntu-latest` | Alert chain、monitoring coverage、Playwright smoke |
|
||
|
||
110 只保留 host-level `act_runner` daemon,並在同一份 config 宣告兩類 label:
|
||
|
||
```yaml
|
||
runner:
|
||
capacity: 1
|
||
shutdown_timeout: 1h
|
||
labels:
|
||
- "ubuntu-latest:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
|
||
- "ubuntu-22.04:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
|
||
- "ubuntu-24.04:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
|
||
- "awoooi-host:host"
|
||
```
|
||
|
||
Docker-wrapped `gitea-runner` container 必須停用,避免它用同一份 config
|
||
搶走 `awoooi-host` job,導致 host job 其實跑在 runner container 裡。
|
||
`scripts/ops/docker-health-monitor.sh` 預設也必須排除 `gitea-runner`,
|
||
否則每 5 分鐘的 Docker 自動修復會把已停用的 runner container 拉起來。
|
||
|
||
### 第三層修復: graceful shutdown service
|
||
|
||
2026-05-01 發現 build/deploy 已推 GitOps deploy commit,production 也
|
||
`Synced Healthy`,但 Gitea commit status 仍顯示 `build-and-deploy` failure。
|
||
根因是 host-level `act_runner` 收到停止訊號時使用預設
|
||
`runner.shutdown_timeout: 0s`,log 會出現:
|
||
|
||
```text
|
||
runner: wooo-runner shutdown initiated, waiting 0s for running jobs to complete
|
||
```
|
||
|
||
因此 daemon 重啟會直接取消仍在收尾的 job,造成「實際已部署、狀態回寫失敗」。
|
||
110 必須安裝 systemd host runner service,並把 shutdown timeout 固定為 1h:
|
||
|
||
```bash
|
||
cd /path/to/awoooi
|
||
RESTART_NOW=1 bash ops/runner/install-gitea-host-runner-service.sh
|
||
```
|
||
|
||
此 script 會:
|
||
|
||
- 更新 `/home/wooo/act-runner/config.yaml` 的 `shutdown_timeout: 1h`
|
||
- 有 passwordless sudo 時安裝 `/etc/systemd/system/gitea-act-runner-host.service`
|
||
- 沒有 sudo 時 fallback 到 `~/.config/systemd/user/gitea-act-runner-host.service`
|
||
- 停用 Docker-wrapped `gitea-runner` container 的 restart policy
|
||
- 拒絕在 `GITEA-ACTIONS-TASK-*` container 正在跑時重啟 runner
|
||
|
||
若 fallback 到 user-level service,請檢查:
|
||
|
||
```bash
|
||
loginctl show-user wooo -p Linger
|
||
```
|
||
|
||
`Linger=no` 代表 service 已能在目前 user manager 內維持 runner,但主機重開機後,
|
||
若沒有登入 session,user service 不一定會自動啟動。需要 root 執行
|
||
`loginctl enable-linger wooo`,或改安裝 system-level service。
|
||
|
||
### 第四層修復: host Web build pressure gate
|
||
|
||
2026-05-21 追加一層 CD preflight:`.gitea/workflows/cd.yaml` 在 Harbor login
|
||
之後、Docker build lock 之前呼叫 `scripts/ci/wait-host-web-build-pressure.sh`。
|
||
|
||
背景是 AWOOI workflow concurrency 與 Docker network lock 只能保護 AWOOI 自己
|
||
與 Docker build/push;其他 repo 仍可能在同一台 110 host runner 直接執行
|
||
`next build` / `turbo build` / `vite build`。這類 host-side build 不會拿
|
||
AWOOI 的 Docker lock,會和 AWOOI Web image 內的 Next production build 疊加,
|
||
造成 110 load、Gitea API timeout、Actions `context canceled` 或 post-deploy
|
||
觀測失真。
|
||
|
||
此 gate 的行為:
|
||
|
||
- 只讀取 `ps`,不 kill / renice / reset 任何外部 process。
|
||
- 排除 AWOOI 自身 checkout、local worktree 與 Web Docker build 內的
|
||
`/app/apps/web` process,避免誤判自己的部署。
|
||
- 預設最多等待 60 次、每次 10 秒;若仍有外部 build,先以 warning 放行,
|
||
避免 CD 永久卡住。
|
||
- 可用 `HOST_WEB_BUILD_PRESSURE_WARN_ONLY=0` 改成 hard fail,但必須先確認
|
||
runner 隔離與其他 repo build 排程已收斂,避免把 shared runner 壓力轉成
|
||
部署中斷。
|
||
|
||
長期方向仍是 runner 隔離或 build offload;此 gate 是在 shared runner 尚未
|
||
拆分前,降低重型前端 build 互相踩踏的保守保護層。
|
||
|
||
### 第五層修復: legacy Docker runner drain
|
||
|
||
2026-05-21 再次確認 110 同時存在兩個 runner:
|
||
|
||
- host-level `gitea-act-runner-host.service`
|
||
- Docker-wrapped `gitea-runner`
|
||
|
||
兩者使用同一份 labels/config,會同時接 `awoooi`、`stockplatform-v2`、
|
||
`ewoooc` 等 repo 的 job。這會讓 AWOOI CD 的 runner ownership 失真,也會
|
||
讓 shared Docker daemon 壓力無法預測。
|
||
|
||
正確處理不是在 task container 正在跑時直接 `docker stop gitea-runner`。
|
||
`ops/runner/install-gitea-host-runner-service.sh` 現在採用 drain 流程:
|
||
|
||
1. `docker update --restart=no gitea-runner`
|
||
2. 若沒有 `GITEA-ACTIONS-TASK-*`,用長 timeout 停止 container
|
||
3. 若仍有 `GITEA-ACTIONS-TASK-*`,送 `SIGINT` 給 `gitea-runner`
|
||
4. act-runner 依 `shutdown_timeout: 1h` 停止接新 job,等待手上的 job 收尾
|
||
|
||
現場判讀:
|
||
|
||
```bash
|
||
docker inspect gitea-runner --format 'Restart={{.HostConfig.RestartPolicy.Name}} Status={{.State.Status}}'
|
||
docker ps --format '{{.Names}}' | grep '^GITEA-ACTIONS-TASK-' || true
|
||
docker logs --since 10m gitea-runner
|
||
```
|
||
|
||
目標狀態:
|
||
|
||
```text
|
||
Restart=no Status=exited
|
||
```
|
||
|
||
### 第六層修復: shared runner label inventory
|
||
|
||
2026-05-21 T139 已把 CI/CD stage transition 寫回 AwoooP,但也暴露下一個基礎設施問題:
|
||
同一個 110 user-level `gitea-act-runner-host.service` 同時宣告 AWOOI 與其他 repo
|
||
label。即使 Docker-wrapped `gitea-runner` 已停用,`capacity: 1` 的 host runner 仍會在
|
||
`awoooi`、`ewoooc`、`stockplatform-v2` 等 repo 之間排隊,讓 AWOOI `post-deploy-checks`
|
||
看起來像部署卡住。
|
||
|
||
本層先做只讀盤點,不直接改 live label:
|
||
|
||
```bash
|
||
# 在 110 本機執行
|
||
bash ops/runner/audit-runner-pool.sh
|
||
|
||
# 或從工作站透過 SSH 執行
|
||
ssh 192.168.0.110 'TASK_LOG_LINES=20 bash -s' < ops/runner/audit-runner-pool.sh
|
||
```
|
||
|
||
腳本會輸出:
|
||
|
||
- `gitea-act-runner-host.service` 的 active/substate/main PID/restart 次數。
|
||
- `/home/wooo/act-runner/config.yaml` 的 `capacity`、`timeout`、`shutdown_timeout`、labels。
|
||
- 非 AWOOI / shared CI labels,例如 `ewoooc-host`,列為 `foreign_or_cross_repo`。
|
||
- Docker-wrapped `gitea-runner` 是否仍為 `Restart=no Status=exited`。
|
||
- 是否存在 active `GITEA-ACTIONS-TASK-*` containers。
|
||
- 近 2 小時 runner journal 內各 repo task 次數,作為 label 隔離前的 live evidence。
|
||
|
||
T140 live evidence 摘要(2026-05-24 09:45 台北):
|
||
|
||
```text
|
||
service=gitea-act-runner-host.service active/running, NRestarts=0
|
||
runner.capacity=1
|
||
runner.shutdown_timeout=1h
|
||
docker gitea-runner: Restart=no Status=exited Running=false
|
||
active_action_containers=none
|
||
foreign_labels=ewoooc-host:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04
|
||
recent 2h repo counts: none
|
||
```
|
||
|
||
判讀:
|
||
|
||
- 不應用 `capacity: 2` 當修復,因為先前 `RWLayer unexpectedly nil` / `context canceled`
|
||
就是跨 repo 並行與 Docker daemon 壓力造成。
|
||
- 下一步應先讀各 repo workflow 實際使用的 labels,再規劃 repo label isolation 或獨立 runner
|
||
registration;不可在沒有替代 runner 前直接移除 live `ewoooc-host`。
|
||
|
||
### 第七層修復: workflow label matrix
|
||
|
||
Runner config 只能看到「這台 runner 願意接什麼 label」,不能回答「哪些 repo 實際在使用」。
|
||
T141 新增 workflow label 盤點工具:
|
||
|
||
```bash
|
||
ops/runner/audit-workflow-labels.py \
|
||
--local-repo wooo/stockplatform-v2=/Users/ogt/stockplatform-v2
|
||
```
|
||
|
||
工具會透過 Gitea API 讀 `.gitea/workflows/*.yml` / `.yaml` 的 `runs-on`,Gitea 不可讀時可指定
|
||
local fallback;Gitea token 只從 env 或目前 repo `gitea` remote 解析,永不輸出。
|
||
|
||
T141 evidence 摘要(2026-05-24 台北):
|
||
|
||
```text
|
||
wooo/awoooi:
|
||
awoooi-host: cd.yaml tests / build-and-deploy / post-deploy-checks
|
||
ubuntu-latest: code-review, e2e-health, deploy-alerts, cd-dev, ansible-lint, type-sync, run-migration
|
||
|
||
wooo/ewoooc:
|
||
ewoooc-host: cd.yaml deploy
|
||
|
||
wooo/stockplatform-v2:
|
||
ubuntu-latest: ci.yaml hygiene / frontend
|
||
```
|
||
|
||
風險判讀:
|
||
|
||
- `awoooi-host` 已經是 AWOOI CD 專用 label,但同一個 runner service 仍同時宣告
|
||
`ewoooc-host` 與 `ubuntu-latest`,所以 runner queue 仍共享。
|
||
- `ubuntu-latest` 是最主要共享入口;AWOOI code-review / e2e-health 與 stockplatform-v2 CI
|
||
仍可能互相排隊。
|
||
- 下一步若要真正隔離,必須做新的 runner registration / service split,或把非 AWOOI repo 移到
|
||
另一台 runner。不可只在同一個 runner config 加更多 label,因為 `capacity: 1` 仍是同一條隊列。
|
||
|
||
### 第八層修復: runner isolation readiness
|
||
|
||
T142 補一個 live readiness gate,用來判斷「現在能不能安全拆 runner」:
|
||
|
||
```bash
|
||
ssh 192.168.0.110 'bash -s' < ops/runner/check-runner-isolation-readiness.sh
|
||
```
|
||
|
||
T142 live evidence 摘要(2026-05-24 09:54 台北):
|
||
|
||
```text
|
||
primary_service=gitea-act-runner-host.service scope=user LoadState=loaded ActiveState=active SubState=running
|
||
primary_runner_dir=/home/wooo/act-runner
|
||
primary_registration_file=present
|
||
primary labels:
|
||
ubuntu-latest / ubuntu-22.04 / ubuntu-24.04 -> shared_queue
|
||
awoooi-host -> awoooi_dedicated
|
||
ewoooc-host -> foreign_dedicated
|
||
mixed_owner_classes=1
|
||
split_dir=/home/wooo/act-runner-awoooi status=missing
|
||
split_dir=/home/wooo/act-runner-shared status=missing
|
||
split_dir=/home/wooo/act-runner-ewoooc status=missing
|
||
installed_split_services=0/3
|
||
active_action_containers=時間敏感欄位;09:54 初查為 none,pre-commit recheck 曾看到 GITEA-ACTIONS-TASK-3435_WORKFLOW-ci_JOB-frontend
|
||
isolation_ready=false
|
||
blocker=single_registered_runner_with_mixed_owner_labels
|
||
safe_next_step=register_additional_runner_dirs_before_removing_live_labels
|
||
```
|
||
|
||
這代表目前**不能**直接刪掉 `ewoooc-host` 或 `ubuntu-latest`。正確的下一步是先準備新的
|
||
runner registration / service:
|
||
|
||
1. `act-runner-awoooi`:承接 `awoooi-host`,優先保護 production CD。
|
||
2. `act-runner-shared`:承接 `ubuntu-latest`,給 code-review / health / stockplatform-v2 CI。
|
||
3. `act-runner-ewoooc`:承接 `ewoooc-host`,讓 EwoooC CD 不再卡 AWOOI。
|
||
|
||
三個 split runner smoke 都通過後,才 drain primary runner 並移除混合 labels。
|
||
|
||
---
|
||
版本: v2.0 | 更新: 2026-03-29 | 作者: Claude Code
|
||
變更: v1.0→v2.0 序列建構取代 Job Concurrency Groups
|