Files
awoooi/ops/runner/README.md

376 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 commitproduction 也
`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但主機重開機後
若沒有登入 sessionuser 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 fallbackGitea 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 初查為 nonepre-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