# 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` | 一鍵部署腳本 |

### Public Gitea Actions queue readback

2026-06-29 起，production deploy blocker 的 public Gitea Actions 佇列證據由
`read-public-gitea-actions-queue.py` 重跑取得：

```bash
python3 ops/runner/read-public-gitea-actions-queue.py --json
```

此 verifier 只讀 public Gitea HTML / unauthenticated API，會把
`No matching online runner with label: ...` 與 CD jobs `total_count` 輸出成
sanitized JSON；不得收集 token、不得 workflow_dispatch、不得註冊或啟動 runner、
不得操作 host / Docker / K8s。若 unauthenticated actions list API 回
`401 token is required`，這是 readback evidence，不是要求 Codex 取得 token。

### 110 controlled CD drain lane registration

2026-07-02 起，110 專用 `awoooi-cd-lane-drain.service` 的 runner registration
必須走 `register-awoooi-110-controlled-cd-lane-drain.sh`。此 helper 與 188
non-110 helper 採同一個安全契約：只允許 interactive TTY hidden prompt，
不接受 `RUNNER_TOKEN` environment，不使用 `--token` argv，不列印 token，不讀
`.runner` 內容。

先在 110 上做非 secret dry-run：

```bash
ops/runner/register-awoooi-110-controlled-cd-lane-drain.sh --check
```

正式註冊時必須先把 helper 放到 110，再用 interactive TTY 執行；不要把 script
透過 stdin pipe 進去跑正式註冊，因為 token entry 需要 TTY：

```bash
ssh -tt wooo@192.168.0.110 'cd /home/wooo/awoooi-cd-lane-drain && /path/to/register-awoooi-110-controlled-cd-lane-drain.sh'
```

若要在註冊成功後同一輪受控啟動 service，必須明確加 `--enable-service`。helper
會先用 `REQUIRE_ACTIVE_SERVICE=0 check-awoooi-110-controlled-cd-lane-readiness.sh`
確認 config、ELF binary、registration metadata、systemd guardrails、
legacy fail-closed、root restore-source 與 host pressure 都通過後，才會
`systemctl enable --now awoooi-cd-lane-drain.service`。

### 監控

```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` | `awoooi-host` | API unit + B5 integration tests，直接跑在 110 host runner |
| `build-and-deploy` | `awoooi-host` | Harbor login、API/Web image build/push、GitOps deploy，直接跑在 110 host |
| `post-deploy-checks` | `awoooi-host` | Alert chain、monitoring coverage、Playwright smoke |

110 只保留 host-level `act_runner` daemon，並在同一份 config 宣告兩類 label：

```yaml
runner:
  capacity: 1
  shutdown_timeout: 1h
  labels:
    - "awoooi-ubuntu: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 / smoke / CI 壓力，
  hard fail，避免繼續把新的 browser smoke 疊到 production host。
- 只有明確設定 `HOST_WEB_BUILD_PRESSURE_WARN_ONLY=1` 才 warning 放行；這只能
  用在已確認壓力來源可接受的受控補跑。

長期方向仍是 runner 隔離或 build offload；此 gate 是在 shared runner 尚未
拆分前，降低重型前端 build 互相踩踏的保守保護層。

### 第四層補充: startup 只自動拉起受控 drain lane

2026-06-27 110 CPU 事故止血後，`gitea-act-runner-host.service` 維持 inactive 是
刻意降壓狀態。`scripts/reboot-recovery/awoooi-startup-110.sh` 仍可修正 runner
`shutdown_timeout` 與 labels，也會停用 legacy Docker runner，但不會啟動 legacy
host runner。2026-06-30 全主機重啟後，`awoooi-host` repair lane 卡在 no matching
runner，確認 post-boot 自動恢復必須讓受控 drain lane 走 guardrails 後自動拉起。
因此 startup 預設會嘗試開啟 `awoooi-cd-lane-drain.service`，但只在
`capacity=1`、labels 僅 `awoooi-ubuntu` / `awoooi-host`、無泛用重型 label、
binary / config / `.runner` path 可用、root restore-source left `0` 且 systemd
limit verifier 成立時放行；不成立就維持 fail-closed。

```bash
AWOOOI_START_CONTROLLED_CD_LANE=0 /usr/local/bin/awoooi-startup-110.sh
```

上方只用於明確關閉受控 drain lane；不得用它恢復 legacy runner。未完成 runner
限流 / 搬遷前，仍不得加入 `AWOOOI_START_GITEA_RUNNER_ON_BOOT=1` 或解除 legacy
runner mask。

### 第五層修復: 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。

2026-06-27 live update：110 的 `gitea-act-runner-host.service` 已刻意停在
`inactive`；`/home/wooo/act-runner/config.yaml` labels 已收斂為
`awoooi-ubuntu` 與 `awoooi-host`，capacity 仍為 `1`。這是降壓與 label isolation
狀態；AWOOI workflows 也應只使用 `awoooi-ubuntu` 或 `awoooi-host`，不可再使用
`ubuntu-latest` / `self-hosted` 這類泛用 label。這不代表 runner 搬遷完成，也不代表可以直接重開 runner。

2026-06-28 live update：110 runner 壓力事故確認有直呼
`/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard` 的孤兒
daemon，且曾透過 transient `awoooi-direct-runner-open.service` 繞過既有
Gitea service 名稱。四條 live runner 入口已改為 immutable fail-closed stub，
原 ELF 僅 quarantine 不讀內容；相關 systemd units 維持 inactive / masked：

- `/home/wooo/act-runner/act_runner`
- `/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard`
- `/home/wooo/act-runner-controlled/act_runner`
- `/home/wooo/awoooi-controlled-runner/awoooi_controlled_runner`

必須一併維持 fail-closed 的 legacy unit 名稱；Gitea / direct runner 維持 masked：

- `awoooi-direct-runner-open.service`
- `awoooi-direct-runner.service`
- `gitea-act-runner-host.service`
- `gitea-act-runner-awoooi-controlled.service`
- `gitea-awoooi-controlled-runner.service`
- `gitea-act-runner-awoooi-open.service`

`awoooi-cd-lane.service` 是專用 controlled lane，不屬於 legacy runner mask 清單；
只有在 `/run/awoooi-cd-lane-enabled` 或 `AWOOOI_START_CONTROLLED_CD_LANE=1`
存在、`capacity=1`、label 僅限 `awoooi-ubuntu` / `awoooi-host`、沒有
`ubuntu-latest` / StockPlatform / headless / Playwright 類泛用重型 label，且
systemd CPU / memory / tasks 限流、root restore-source left `0` 與
post-apply verifier 可讀回 `CD_LANE_CONTROLLED ok=1` 時，才可受控恢復。
未滿足條件時 cd-lane 應回到 static `/bin/false` unit 與 shell stub。

未完成 runner 搬遷、限流、smoke 排程前，不得解除 legacy mask、恢復泛用 runner label，
或把 host pressure gate 預設改成 warn-only。

2026-06-28 controlled update：舊的 manual-only / freeze guard 已改為分流判讀。
legacy runner 仍維持 masked / fail-closed；專用 `awoooi-cd-lane.service` 與
`awoooi-cd-lane-drain.service` 只要通過 capacity、label、binary、process 與
systemd limit、root restore-source left `0`、post-apply verifier，可作為
AWOOOI 專用受控部署 lane。

若 verifier 失敗，rollback 回 inactive / masked / fail-closed stub；若 verifier
通過，不得再用 generic runner fail-closed 規則殺掉 controlled lane。legacy / generic
runner 仍不得解除 mask 或恢復泛用 label；所有會命中 `awoooi-ubuntu` / `awoooi-host`
的 `.gitea/workflows` 都不得保留 `push`、`pull_request` 或 `pull_request_target`
自動事件。

### 第九層修復: workflow pressure source guard

2026-06-28 補上 source guard：

```bash
python3 ops/runner/guard-gitea-runner-pressure.py --root .
```

此 guard 只讀 repo 內 `.gitea/workflows/*.yml` / `.yaml`，禁止兩類回歸：

1. `push` / `pull_request` / `pull_request_target` 自動事件命中 `awoooi-ubuntu` 或
   `awoooi-host`。
2. Gitea workflow 恢復 `ubuntu-latest`、`ubuntu-*` 或 `self-hosted` 泛用 label。

`scripts/ops/ansible-validate.sh` 會執行同一 guard。若要恢復自動事件，必須先有
runner 搬遷或非 110 硬限流的 source-of-truth diff、rollback 與 post-apply verifier。
`cd.yaml` / `code-review.yaml` 不得因非事故級 guard 長期停在 `workflow_dispatch` only；
恢復自動事件前必須先通過 runner 搬遷或非 110 硬限流 verifier。

### 第十層修復: non-110 runner readiness verifier

2026-06-28 補上 AWOOOI non-110 / hard-limited runner readiness gate：

```bash
ssh 192.168.0.188 'TARGET_HOST_IP=192.168.0.188 bash -s' \
  < ops/runner/check-awoooi-non110-runner-readiness.sh
```

此 verifier 只讀 metadata，不讀 `.runner`、runner token、secret 或 raw config value；
只輸出 host IP、runner `capacity`、labels、systemd limit、rollback unit、active
Actions container、heavy build/smoke process 與 load/core。恢復 `push` /
`pull_request` 自動事件前，目標 host 必須讀回：

```text
AWOOOI_NON110_RUNNER_READY=1
```

source-of-truth 範本：

- `ops/runner/awoooi-non110-runner.service.example`
- `ops/runner/awoooi-non110-runner-rollback.service.example`
- `ops/runner/awoooi-non110-runner.user.service.example`
- `ops/runner/awoooi-non110-runner-rollback.user.service.example`
- `ops/runner/install-awoooi-non110-runner-user-service.sh`
- `ops/runner/check-awoooi-non110-runner-readiness.sh`

188 user-level 安裝只允許 non-secret apply，預設不註冊、不啟動：

```bash
ssh ollama@192.168.0.188 'bash -s -- --apply' \
  < ops/runner/install-awoooi-non110-runner-user-service.sh
ssh ollama@192.168.0.188 'bash -s' \
  < ops/runner/check-awoooi-non110-runner-readiness.sh
```

runner registration 只能由 operator 在互動 TTY 執行；Codex 不得接觸 token。
註冊 helper 的 check-mode 不需要 token，也不得接受 `RUNNER_TOKEN` environment：

```bash
ssh ollama@192.168.0.188 'bash -s -- --check' \
  < ops/runner/register-awoooi-non110-runner.sh
```

正式註冊時，helper 會用 silent TTY 讀取 runner token，透過 stdin 餵給
`act_runner register`，不使用 `--token` argv、不列印 token、不讀 `.runner`
內容；若 `.runner` 已存在，預設只回報下一步 enable / verifier，不覆寫。正式註冊
不能用 `ssh -t ... 'bash -s' < ops/runner/register-awoooi-non110-runner.sh`，
因為 stdin 會被 script 佔用而不是互動 TTY；請先把 helper 放在 188 上，再用：

```bash
ssh -tt ollama@192.168.0.188 \
  'cd /home/ollama/act-runner-awoooi && ./register-awoooi-non110-runner.sh'
```

若 Gitea 回 `runner registration token not found`，代表貼入的不是
`http://192.168.0.110:3001` 當前 scope 的 runner registration token、token 已過期，
或 scope 錯誤；重新從 `wooo/awoooi` repo / `wooo` org / instance runner 頁產生
registration token，不要使用 Personal Access Token 或 API token，也不要把 token
貼到 Codex / ChatGPT / Slack / ticket / log；只貼到 helper 的 hidden TTY prompt。

註冊與 autostart 後，CD lane 不能只靠「runner 已註冊」宣稱恢復；必須用
sanitized evidence 彙整 verifier：

```bash
python3 ops/runner/verify-awoooi-non110-cd-closure.py \
  --production-deploy-snapshot-json-file docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json \
  --readiness-file /path/to/sanitized-non110-readiness.txt \
  --queue-json-file /path/to/public-actions-queue.json \
  --production-workbench-json-file /path/to/delivery-closure-workbench.json \
  --json
```

此 verifier 只接受 `check-awoooi-non110-runner-readiness.sh` 的非 secret 輸出、
public queue JSON 與 production Delivery Workbench JSON/API；它不讀 `.runner`
內容、不讀 token、不 workflow_dispatch、不操作 host / Docker / K8s / runner service。
若沒有傳入 fresh readiness file，verifier 只會退回 committed production deploy
snapshot 內的 sanitized non-110 readiness 欄位，並輸出
`non110_runner_readiness_source=committed_production_deploy_snapshot`；這只代表
source-level fail-closed evidence，不是 live host proof。
只有 `AWOOOI_NON110_RUNNER_READY=1`、public queue 不再顯示
`No matching online runner`、production image tag 已跟 main 對齊且 governance fields
已出現時，才會輸出 `closure_verified`。
verifier 也會輸出 `ordered_steps` 與 `progress`：順序固定為 registration metadata、
active service、public queue match、production Workbench readback、production image tag、
governance fields。若 live readback 仍缺 `.runner` metadata，`next_blocked_step_id`
必須維持 `non110_runner_registration_metadata`，不得跳到 dispatch、image tag 或
governance fields。

`--enable` 只允許在 `AWOOOI_NON110_ENABLE=1`、`act_runner` executable、
`config.yaml` present、`.runner` present 且 service 已由 verifier 證明 target / limits
正確後使用。installer 不會讀 `.runner` 內容，也不會寫 runner token；rollback 只會
stop / disable user service 並移除 `.awoooi-non110-runner-enabled` sentinel。
若 host binary / config 被外部流程移到 disabled 路徑或刪掉，installer 可在 `--apply`
時從同機 disabled `act_runner` metadata path 恢復 binary，並生成不含 token 的
`capacity=1` / `awoooi-non110-*` labels config；它仍不會複製 `.runner` 或讀取 registration
內容。

2026-06-28 20:32 live readback：188 host 可達、`ollama` user `Linger=yes`、
`/home/ollama/act-runner-awoooi/act_runner` executable、`config.yaml` present、
action containers `0`、heavy process `0`、load/core 約 `0.143333`。舊 user service
已被 verifier 收緊為 target mismatch，因其 `ExecStart` 仍指向 Docker
`gitea/act_runner:latest` 與 `/home/ollama/awoooi-non110-runner`，不是已驗證的
`/home/ollama/act-runner-awoooi`。本輪應用 installer 後仍必須維持
`AWOOOI_NON110_RUNNER_READY=0`，直到 `.runner` registration metadata 安全補齊並明確
enable。

---
版本: v2.0 | 更新: 2026-03-29 | 作者: Claude Code
變更: v1.0→v2.0 序列建構取代 Job Concurrency Groups
