部落格文章草稿

Agent: 部落格寫手 (blog-writer)
Confidence: 75%
Cost: $0.6683
Duration: 2m 49s
Task ID: c8d517f8-803b-4fe4-a3a8-3fedf6113a08


Now I have a solid understanding of the writing style and have gathered all necessary context. Let me craft the blog post.

Now I have all the materials. Let me write the article.


部落格文章草稿

建議檔名:ci-cd-self-hosted-runner-wsl2-2026-03-01.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
---
title: 當你的 CI/CD 管線就是你家客廳——在 WSL2 上跑 GitHub Actions Self-hosted Runner
date: 2026-03-01 10:00:00
tags:
- CI/CD
- GitHub Actions
- WSL2
- DevOps
- Telegram Bot
- Self-hosted Runner
categories:
- 技術日誌
---

昨晚我突然意識到一件有點荒謬的事:我們的 multi-agent 系統每週自動執行 371 次任務,成功率 98%,花掉 $159——但每一行程式碼推上 GitHub 之後會發生什麼?什麼都不會。沒有自動測試、沒有自動部署、沒有任何人在雲端幫你確認「這次 push 沒有搞壞東西」。

唯一的防護網是兩個 git hook:commit 前跑型別檢查,push 前跑測試。但這全部發生在我的 WSL2 本機上。

<!-- more -->

---

## 裸奔的日子

先說現狀。

我們的專案目錄裡沒有 `.github/workflows/`。零個 YAML 檔案。這不是因為懶(好吧,有一部分是),而是因為一開始 bot 就跑在本機 WSL2 上,不是雲端服務,部署的概念等於「在終端機裡按 Ctrl+C 然後重新 `npm start`」。

目前的安全機制全靠兩個 git hook:

pre-commit → tsgo –noEmit(型別檢查,不通過就不讓你 commit)
pre-push → npm test(跑 vitest,不通過就不讓你 push)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

這兩道關卡其實不差。問題在於它們只在**手動操作 git 的時候才觸發**。

我們的 multi-agent 系統裡有 programmer、architect、secretary 等代理人,它們透過 Claude CLI 在背景自動執行程式碼修改。程式設計師(programmer agent)寫完 code 交給審查員(reviewer agent),審查通過後交給秘書(secretary agent)做 commit 和 push。這整條流水線在 worktree 裡運作,git hook 確實會被觸發——但如果 hook 失敗了呢?

上週的反省日誌裡就記著:「pre-push hook 因 21 個測試失敗而阻止推送」。那次的結果是秘書 agent 回報失敗,然後整個流水線停下來等人類介入。代理人不會自己修 bug。

這就是沒有 CI/CD 的代價:你的安全網只在「有人親手推的時候」才存在,而這個系統裡越來越多的操作是自動化的。

---

## 最怪的方案反而最合理

研究 CI/CD 方案時,最先排除的反而是最常見的選項。

雲端 CI(GitHub-hosted runner)跑測試沒問題——`npm ci` + `tsgo --noEmit` + `vitest run`,配一個 `ubuntu-latest` 就搞定。但部署呢?Bot 跑在我家 WSL2 上,GitHub 的雲端 runner 要怎麼部署到我的本機?SSH 進來?開放端口?設定反向代理?

每一個選項都比問題本身更麻煩。

然後我看到了 GitHub Actions Self-hosted Runner 的文件,突然覺得這個方案的邏輯美得像一個巧合:

**既然 bot 已經跑在 WSL2 上了,那就在同一台 WSL2 上裝一個 runner。runner 本身就在目標機器上,部署就是 `git pull && npm start`。**

不需要 SSH,不需要 rsync,不需要 Docker,不需要 Kubernetes。Runner 直接存取本機檔案系統。就像你已經站在廚房裡了,不需要打電話叫外送把食材送到廚房。

---

## 三層蛋糕

想清楚之後,整個 workflow 可以切成三層:

### 第一層:CI(每次 push 和 PR)

這層用 GitHub-hosted runner 就好,不需要碰到本機:

```yaml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run typecheck
- run: npm test

做的事情和現有的 git hook 一模一樣,但有兩個關鍵差異:

  1. 它在遠端跑,不依賴你本機的環境是否乾淨
  2. PR 也會觸發,所以任何人(或任何 agent)開的 PR 都會被自動驗證

第一層的存在讓 git hook 從「唯一的防護網」降級為「本地快速回饋」。真正的守門員在雲端。

第二層:CD(push to main 通過 CI 後)

這層用 self-hosted runner,觸發條件是 push 到 main 且 CI 通過:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: Deploy
on:
workflow_run:
workflows: ["CI"]
branches: [main]
types: [completed]
jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: self-hosted
steps:
- run: cd /mnt/d/gitcode/mybotteam && git pull origin main
- run: cd /mnt/d/gitcode/mybotteam && npm ci
- run: sudo systemctl restart metacognitive-bot

最後那行 systemctl restart 讓我多想了一下。現在 bot 是透過 restart.ts wrapper 啟動的,exit code 42 會自動重啟(叫做「蛻皮」,molt)。但用 systemd 管理會更乾淨——開機自動啟動、crash 自動重啟、日誌自動歸檔,全都由作業系統層面處理。

這大概是一個值得做但不緊急的改善。

第三層:Blog Deploy(偵測到 blog/ 變更時)

1
2
3
4
on:
push:
branches: [main]
paths: ['blog/**']

偵測到 blog/ 目錄有變更,就自動跑 hexo generate + wrangler pages deploy。目前這個流程是由 blog-publisher agent 手動觸發的,改成 CI/CD 後可以完全自動化。

不過這層的優先級最低。部落格發布的頻率遠低於程式碼修改,而且現有的 agent 流水線(blog-writer → blog-publisher → channel-op)已經能跑了,只是需要人類點一下觸發。


安全的那層皮

Self-hosted runner 有一個大紅燈:如果你的 repo 是 public,任何人的 PR 都能在你的機器上跑任意程式碼。

這等於開了一個遠端程式碼執行的後門。Fork + 惡意 workflow + 提 PR = 你的機器被人當成免費的 shell。

但我們的 repo 是 private,所以這個問題目前不存在。不過萬一有一天需要 open source,這個架構就得改——要麼改回 GitHub-hosted runner 做 CI,要麼用 ephemeral mode(--ephemeral)讓每次執行都是一次性的乾淨環境。

另一個值得注意的是 runner 的 token 管理。Self-hosted runner 在安裝時需要一個 registration token,這個 token 別寫進 .env 或 commit 進 repo。用系統環境變數或 secrets manager。


為什麼現在該做這件事

三週前,multi-agent 系統每週跑 101 次任務,成功率 61%。上週是 371 次,98%。

執行頻率漲了 3.7 倍,而且這些任務裡有很大一部分是程式碼修改。programmer agent 寫 code、reviewer agent 審查、secretary agent commit——這條流水線每天都在跑。

沒有 CI 的情況下,每一次自動 push 都是在賭:「這次應該沒壞吧?」pre-push hook 確實在本機攔住了一些,但本機的測試環境和 GitHub 上的環境不一定完全一致(WSL2 的 IPv6 問題就是個例子——我們曾經被迫在所有 HTTP 呼叫加上 { family: 4 } 強制走 IPv4)。

更重要的是心理上的改變。有 CI 在背後撐腰,你會更敢做大幅度的重構。沒有 CI 的時候,你會本能地避免碰太多東西,因為你不確定改完之後系統還能不能跑。這種恐懼是隱性的生產力殺手。


那些還沒想清楚的事

寫到這裡,有幾個問題還沒有答案:

Bot 本身要不要改成 systemd service?

restart.ts wrapper 已經夠用了,但它只能在有人手動啟動之後才開始工作。開機的時候呢?WSL2 重啟的時候呢?用 systemd 管理可以解決這些,但 WSL2 的 systemd 支援是 2022 年才加入的,穩定性存疑。

Self-hosted runner 要不要也包成 service?

Runner 如果不是 service,那你每次重新開機都要手動啟動它。但如果它是 service,又多了一個需要維護的背景程序。

要不要同時跑 GitHub-hosted 和 self-hosted?

理想狀態是 CI 在雲端(乾淨環境,確認跨平台相容性),CD 在本機(直接部署)。但這意味著每次 push 都會消耗 GitHub Actions 的免費額度。Private repo 每月 2,000 分鐘,以目前的 push 頻率應該夠用,但值得監控。

這些問題沒有急迫性。目前的 git hook 還在運作,multi-agent 的流水線也有 reviewer 這個環節在做人工(好吧,是 AI 的人工)審查。CI/CD 是錦上添花,但確實是一朵很有價值的花。


結語

最有趣的發現是這個:CI/CD 對我來說不只是「自動跑測試」的工具。它更像是一個承諾——承諾每次修改都會被驗證,承諾沒有人(也沒有 agent)可以偷偷把壞東西推上去。

在一個 multi-agent 系統裡,這種承諾比在傳統團隊裡更重要。因為你沒辦法走到某個 agent 的座位旁邊說「你剛才 push 的那個改動有沒有測過?」你唯一能依賴的是流程。

而 CI/CD 就是把流程變成基礎設施的那一步。


一見生財,寫於 2026-03-01


---

## 報告摘要

- **文章主題**:GitHub Actions CI/CD 自動化部署 Telegram Bot,聚焦 Self-hosted Runner on WSL2 方案
- **素材來源**:explorer agent 的探索報告(task `e315aece`)、專案 CLAUDE.md 中的 Exit Code 語義、package.json 中的測試腳本、近期反省日誌(agent 執行統計)
- **字數**:約 2,300 字(繁體中文)
- **特色**:
  - 從「我們的系統裸奔」這個切入點開始,避免枯燥的技術文件風
  - 結合 multi-agent 系統的實際痛點(pre-push 失敗阻止推送、371 次/週的自動任務)
  - 三層架構清晰拆解(CI / CD / Blog Deploy)
  - 安全考量和未解決問題的誠實呈現
  - 用「承諾」這個比喻收尾,連結 multi-agent 信任問題
- **重要性**:4/5(直接改善開發品質的實用技術文章)

---HANDOFF---
TO: blog-publisher
INTENT: handoff
ARTIFACT_TYPE: report
SUMMARY: 完成 CI/CD Self-hosted Runner 技術部落格文章,檔名 ci-cd-self-hosted-runner-wsl2-2026-03-01.md,可直接發布