前言
最近有了在住處自架一個簡易 Homelab 的想法。 首先想到的是可以把這個 Devblog 從 Github Page 移動到自己的主機上, 同時在工作上也遇到一些可以利用 Github Actions 達到方便快速地更新服務的情境。 然而在筆者實際摸索時,發現整套 Github Actions 的系統相當有彈性,可以應付小型大型的測試 / 部署需求,但也因此我認為這不是一個容易入門的系統。 故我在此以我的 Devblog 自動化部署為例,一步步地說明設置的過程,提供一個學習的出發點。
設定 Self-hosted Runner
在你的 Github Repo 上,開啟 Setting > Actions > Runners,然後選擇 Add new self-hosted runner。
裡面會指示你在自己的 Runner 上下載 runner package,然後使用 ./config.sh 設定。
設定時需要提供 repo url 跟驗證用的 token,讓 Github 允許這個 Runner 加入到 repo 中。
設定時,config.sh 會詢問是否要為該 runner 加入標籤,標籤可以讓 workflow 在執行時能夠篩選可以分配工作的 runner,
在這裡,我新增了一個 development 的標籤,用來表示這是部署測試環境用的主機。

完成以上步驟後,就可以在 runners 列表看到自己的 runner。

最後需要啟動 runner。Github 雖然指示你直接執行 ./run.sh,但這種方式會佔據你的 shell process,因此更好的方式是把它設定成一個 systemd service。
runner package 提供 ./svc.sh 可以快速地幫助你安裝 service 然後啟動。
$ sudo ./svc.sh install
$ sudo ./svc.sh start
$ sudo ./svc.sh status
可以回到 Runners 頁面查看自己的 runner status 是否顯示上線。
撰寫 Workflow
在 .github/workflows 目錄中,每一個 .yml / .yaml 檔案就代表了一個 workflow。以下我們先釐清 workflow 中的層級概念:
- 一個 workflow 代表一套想要完整執行的目標,Github Actions 執行時就是以一個 workflow 為單位去完成。
- 一個 workflow 由一個或多個 jobs 組成。Github Actions 會以 job 為單位,派發到 runner 上,因此一個 workflow 中的 jobs 可能是由不同的 runners 協力完成的。jobs 之間可以定義依賴關係(哪個 job 必須在哪個 job 完成之後才能開始。)
- 一個 jobs 由多個 runs / actions 構成。run 是一個想要機器執行的 shell command process,actions 本質上也是一個 process,只是由 Github 上的其他 action 專案包裝好一個特定的行為。runner 會按照順序執行指定的 runs / actions。
接下來我們以上面部署 devblog 為例子:
name: Deploy site to self-hosted development server
on:
push:
branches:
- dev
workflow_dispatch:
permissions:
contents: read
concurrency:
group: development
cancel-in-progress: false
jobs:
build:
runs-on: [self-hosted, development]
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- run: |
mkdir -p /srv/devblog/release/${{ github.sha }}/devblog
rsync -a --delete public/ /srv/devblog/release/${{ github.sha }}/devblog
rm -f /srv/devblog/current
ln -s /srv/devblog/release/${{ github.sha }} /srv/devblog/current
on: workflow 的觸發條件。例如可以設定在某個 branch 有 push 時、某個 branch 的 PR 被創建時、符合特定 Regex 的 Tag 被建立時,或是允許管理者手動觸發。permissions: 允許 runner 什麼樣的權限。有些 workflow 只是做 testing 時,通常只需要 read 權限就夠了,如果是想要達成自動 commitment 的情境,可能就需要允許對 Repo 進行寫入。jobs: 在這裡面定義你的每一個 job。上面的範例中,只有一個 job-id 為build的 job。jobs.<job_id>.runs-on: 這個欄位可以指定 job 要分配給什麼樣的 runner。上面使用了self-hosted代表只運行在自架的 runner 上,然後 runner 還必須有development這個標籤。這個標籤是我用來代表開發伺服器的標籤。假設我的專案今天想要分別有測試環境跟正式部署環境,那麼我就可以有兩台不同的 runner,並且給他們不同的標籤來區分。
接下來,我們可以看到 steps 裡面有五個 uses / run 子項目,runner 會依序執行這五個 action / shell command。讀者可以從字面意思了解執行內容,白話來說就是以下的步驟:
- 用
actions/checkout@v7clone 出 repo 中的內容。 - 用
actions/setup-node@v4建立 nodejs 環境。 - 安裝專案的 node packages。
- 執行專案的 build 流程。
- 把生出來的靜態檔案整包丟到
/srv/devblog/release裡面交給 nginx 處理。
瞭解目錄結構
我在撰寫 workflow 時遇到的一個小難題就是搞不懂 runner 如何管理目錄結構,因此在這邊簡單說明整理。
假設 runner 的路徑為 ~/my-runner,repo 名稱為 my-repo,那麼你的每一個 step 預設的 working directory 都是 ~/my-runner/_work/my-repo/my-repo/
讀者可能會疑惑「為什麼路徑裡面會有兩層 my-repo 目錄?」第一層存在的目的是因為一個 runner 可能可以接受來自不同 repo 的 job。例如今天 repo-A 跟 repo-B 都登錄了這個 runner,那麼 ~/my-runner/_work/repo-A/ 就會用來放置執行 repo-A 的 job 產生的檔案,而 ~/my-runner/_work/repo-B/ 亦同。第二層則是因為 job 可能可以 checkout 多個 repo,例如 ~/my-runner/_work/my-repo/my-repo/ 跟 ~/my-runner/_work/my-repo/another-repo/。
如果想要修改 working directory,可以透過 jobs.<job_id>.steps[*].working-directory 來設定該特定步驟的 working directory,或是透過 jobs.<job_id>.defaults.run.working-directory 來為整個 job 修改預設的 working directory。
環境變數與 context
Github 有兩種方式可以傳遞資訊給 runner。
首先先介紹環境變數 (environment variable)。這裡的環境變數指的就是 runner process 會看到的作業系統環境變數。我們可以給一個 step、一個 job 或是整個 workflow 指定環境變數。看一下以下的例子:
name: Greeting on variable day
on:
workflow_dispatch
env:
DAY_OF_WEEK: Monday
jobs:
greeting_job:
runs-on: ubuntu-latest
env:
Greeting: Hello
steps:
- name: "Say Hello Mona it's Monday"
run: echo "$Greeting $First_Name. Today is $DAY_OF_WEEK!"
env:
First_Name: Mona
這個 workflow 在三個不同的層級都設定了一些環境變數,這些變數會傳遞給 runner,並套用在 runner 執行 step 啟動的 process 上,因此該 echo 指令將會輸出:
Hello Mona. Today is Monday!
另一方面,上下文變數 (context variable) 則是 Github Action 在 workflow 被執行時,Github 自動帶入的數值。舉例來說,我上面 devblog 的 workflow 有這麼一行:
mkdir -p /srv/devblog/release/${{ github.sha }}/devblog
其中的 ${{ github.sha }} 就是 github 這個 context 的 sha 屬性,代表觸發 workflow 的 commit 之雜湊值。Context variables 可以取得 workflow 相關的各種資訊,還有 repository variable 等等。可以參考 Github Context reference 了解有哪些 context 可以使用。
Context variable 跟 environment variable 不同之處在於,context variable 在 workflow 開始時會由 Github 決定好數值並填入,runner 並不知道 context variable 的存在。以上面的指令來說,runner 的 shell 實際上收到的指令是 mkdir -p /srv/devblog/release/abc123.../devblog,因此 runner 無法取得有關 context 的資訊。
Reference
https://www.reddit.com/r/devops/comments/1favu4a/why_is_github_actions_doubling_my_working/ https://docs.github.com/en/actions/reference/workflows-and-actions/variables https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context