CI/CD 實戰指南(二):GitHub Actions 與 Self-hosted Runner 使用與建置大全
帶您從頭了解 GitHub Actions/Self-hosted Runner 運作方式與手把手使用教學。
ℹ️ℹ️ℹ️ Click here to view the English version of this article, translated by OpenAI.
CI/CD 實戰指南(二):GitHub Actions 與 Self-hosted Runner 使用與建置大全
帶您從頭了解 GitHub Actions/Self-hosted Runner 運作方式與手把手使用教學。
Photo by Dan Taylor
前言
前篇「 CI/CD 實戰指南(一):CI/CD 是什麼?如何透過 CI/CD 打造穩定高效的開發團隊?工具選擇? 」我們介紹了 CI/CD 是什麼?能帶來哪些效益與工具的選擇, 這篇將著重在 GitHub Actions, Self-hosted Runner 的架構與使用介紹 ,並手把手一起建立幾個有趣的自動化工作流程,帶您慢慢上手。
GitHub Actions 架構流程圖
在開始之前我們先來確定一下 GitHub Actions 的運作架構流程關係與職責。
GitHub Repo
- 在 GitHub Actions 的世界裡,所有 Actions (Workflow YAML 檔案) 都要存放在某個 Git Repo 之中 (
REPO/.github/workflows/
)
GitHub Repo —Actions Secrets
Repo → Settings → Secrets and variables → Actions → Secrets。
- 存放 Actions 步驟中會用到的 Secret Key, Token e.g. Slack Bot Token、Apple Store Connect API .p8 Key
- Secrets 內容無法在 Action Log 中查看,會自動用 * * * * 隱蔽
- Secrets 內容無法查看、編輯,只能覆蓋
- Secrets 目前只支援純文字內容,無法上傳檔案 - 如果是二進制的金要請參考 官方步驟使用 Base64 編碼轉換後儲存 。 - iOS 開發憑證儲存方式可參考官方教學: Installing an Apple certificate on macOS runners for Xcode development
- 可以儲存組織層級的 Secrets,跨 Repo 共享
GitHub Repo — Actions Variables
Repo → Settings → Secrets and variables → Actions → Variables。
- 存放 Actions 步驟中常用到的變數 e.g. 模擬器 iOS 版本、工作目錄
- Variables 內容可以查看、編輯
- Variables 內容可以輸出在 Action Log 中
- Variables 只支援純文字,也可存放 json 字串然後自己解析使用
- 可以儲存組織層級的 Variables,跨 Repo 共享
GitHub Actions — Trigger
- Github Action 中最重要的起始點 — 觸發事件(條件)
- 符合觸發事件的 GitHub Actions 才會觸發執行
- 完整事件列表可 參考官方文件
- 基本上涵蓋了所有 CI/CD、自動化會遇到的事件場景。 但 如果有特殊場景沒有事件,那就只能用其他事件+在 Job 中判斷組合或是用 Schedule 排程手動檢查了 。 e.g. 例如沒有 PR Merged 事件,就只能用
pull_request: closed
+ Jobif: github.event.pull_request.merged == true
達成
常用事件:
schedule
(cron):排程定時執行(同 crontab) 可以用來做自動化:定時檢查 PR、定時打包、定時執行自動化腳本 統一都在 main / develop (Default Branch) 執行。pull_request:
:PR 相關事件 當 PR 開啟時、PR Assign 時、加 Label 時、有新 Push Commit 時…等等issues
、issue_comment
:Issue 相關事件 當 Issue 開啟時、有新留言時…等等workflow_dispatch
:手動觸發;可以設定需要提供的欄位,GitHub Actions 提供簡易的表單讓使用者可以填寫資訊。 e.g.:
workflow_call
:觸發另一個 Action(Workflow) 執行任務。workflow_run
:當別的 Action(Workflow) 執行任務,觸發執行此任務。
更多事件類型、設定細節請 參考官方文件 。
GitHub Actions — Workflow
- a.k.a Action
- 使用 YAML 撰寫 .yaml 檔案,檔案統一放置在
REPO/.github/workflows/
之下 - 以主分支內的 Workflow YAML 檔案為主 如果在其他分支看不到正在開發的 Action 或執行上有誤,可以先 Merge 回主分支看看。
- GitHub Actions 中的最基礎單位,每個 Workflow 就代表一項 CI/CD 或自動化操作
- Workflow 可以呼叫別的 Workflow 執行任務 (可以利用這個特性拆出核心 Workflow 和呼叫的 Workflow)
- 當中會定義任務名稱、執行策略、觸發事件、任務工作…等等所有 Action 相關設定
- 目前檔案結構不支援子目錄
- Action 完全免費 (Public and Private Repo)
GitHub Actions — Workflow — Job
- GitHub Actions 中的執行單位
- 定義 Workflow 中的任務工作有哪些
- 每個 Workflow 可以有多個 Jobs
- 每個 Job 需要指定使用哪個 Runner Label,執行的時候會使用對應的 Runner 機器來執行任務
- 多個 Jobs 是並發執行 (如有順序可以用
needs
約束) - 每個 Job 應該視為獨立執行個體(每個都要當成是 Sandbox) ,Job 結束後如果有產出資源檔案要給後續其他 Job/Workflow 使用,需要 Upload Artifacts 或在 self-hosted 移動到共用產出目錄。
- Job 做完可以 Output 字串給其他 Job 參考使用。 (例如執行結果 true or false)
- 沒特別設流程條件的話,多個 Job 如果其中有 Job 發生錯誤, 其他 Job 依然會繼續執行
GitHub Actions — Reuse Workflow/Job
- Workflow 定義
on: workflow_call
就能封裝給其他 Workflow 當成 Job 複用 - 同個組織下可以跨 Repo 分享使用
- 因此可以把跨 Repo 共用的 CI/CD 工作放到共享的 Repo 一起使用
GitHub Actions — Workflow — Job — Step
- GitHub Actions 中的最小執行項目
- Job 中實際執行任務的程式
- 每個 Job 可以有多個 Steps
- 多個 Steps 是照順序執行
- Step 做完可以 Output 字串給後續 Steps 參考使用。
- Step 可以直接撰寫 shell script 程式 可以引用 gh cli 、當前環境變數(例如取得 PR 編號),直接做想做的事
- 沒特別設流程條件的話,Step 如果發生錯誤, 會直接中斷 ,後續的 Steps 不會執行
GitHub Actions — Workflow — Job — Reuse Action Step
- 可以直接復用 Marketplace 上各路大神包好的現有工作步驟。 例如: Comment 內容到 PR 。
- 也可以將自己一系列的工作任務 Step 打包成一個 Action GitHub Repo 讓其他工作直接復用
- Public Repo 的 Action 可以上架到 Marketplace
打包 Action 支援使用:
- Docker Action — 代表 GitHub Actions 會把環境變數傳到 Docker 容器中,再看你要怎麼處理,可以是 shell script、Java、PHP…etc.
- JavaScript/TypeScript Action — 直接使用 node.js 撰寫 GitHub Actions 處理邏輯,同樣的會把環境變數都傳給你參考使用。 e.g. pozil/auto-assign-issue
- Composite (YAML) — 純 YAML 描述任務步驟 (同 GitHub Actions — Workflow — Job — Step) 可以宣告有哪寫步驟要做或直接在上面寫 shell script。 e.g. ZhgChgLi/ZReviewTender
礙於篇幅,本篇文章不會介紹如何打包 Github Actions Action,有興趣可以參考官方文件: tutorials/creating-a-composite-action 。
GitHub Runner
- GitHub 會根據 Runner Label 派發對應的 Job 給 Runner 執行
- Runner 只做為監聽者,輪詢監聽 GitHub 派發任務
- 只關心 Job 不關心是哪個 Action(Workflow) 因此會出現 Action A 的 Job-1 執行完,下一個換 Action B 的 Job-1,而不是 Action A 的 Jobs 都執行完才換 Action B。
- Runner 可以使用 GitHub Hosted Runner 或 Self-hosted Runner。
GitHub Hosted Runner
- GitHub 提供的 Runner,可參考官方 Repo 列表:
- Runner 預先安裝了什麼可以點進去查看: e.g. macos-14-arm64
- iOS 開發優先使用 -arm64 (M系列) 處理器的 Runner,跑起來比較快
- 只要在 Job
run-on
貼上表格上的 YAML Label,就能使用該 Runner 執行任務 - Public Repo 收費方式: 完全免費無限使用
- ️ Private Repo 免費額度: 免費額度(依照帳號不同額度不同,以 GitHub Free 為例): 用量:每月免費 2,000 分鐘 儲存:500 MB
- ⚠️️Private Repo 計費方式: 超過免費額度之後開始用用量計費(可設上限跟通知),依照 Runner 所屬的機器作業系統、核心不同,價格也不同:
about-billing-for-github-actions
可以看到 macOS 的價格因為設備成本很高所以貴。
- 最多並發任務數限制:
usage-limits-billing-and-administration
這邊扯太多了,我們的重點是 Self-hosted Runner。
Self-hosted Runner on In-house Server
- 將自己的機器作為 Runner
- 一台實體機器可以起多個 Runner 並發接任務來做
- 免費無限量無限制使用 只有機器購買成本,花一次使用到飽! 以 32G RAM M4 Mini (=NT$40,900) 計算,如果用 GitHub Hosted Runner 一個月要花 500 USD; 買一台架設好用超過三個月就回本 了!
- 支援 Windows, macOS, Linux (x64/ARM/ARM64)
- 同個組織可以跨 Repo 共享 Runner
- ⚠️目前:actions/cache, actions/upload-artifact, actions/download-artifact 都只支援 GitHub 雲端服務,代表這些內容還是會上傳到 GitHub 伺服器並計算儲存量收費。 可以在自己的機器上開共用目錄取代。
- Self-hosted Runner 也支援 Docker, k8s 只是我沒研究。
架設 Self-hosted Runner 只需幾步 (10 分鐘內設定好) 就能上線開始接任務執行(本文稍後會介紹)。
GitHub Workflow x Job x Step x Runner 流程關係圖
這邊用一張圖總結工作流程與關係,假設我們有兩個 Workflow、兩個 Runner:
- CI —假設 R有 3 個 Jobs,每個 Job 各有幾個 Steps,Runner Label(run-on) —
self-hosted-app
- CD — 有 4 個 Jobs,每個 Job 各有幾個 Steps,Runner Label(run-on) —
self-hosted-app
- Runner — 有 2 個,Runner Label 也都是—
self-hosted-app
同前述,Runner 跟 Workflow 是依照 Runnler Label 來派發、接收任務來做,這個案例都是用同個 self-hosted-app
Runner Label,Workflow 的 Jobs 預設是並發執行、Steps 是每個 Job 的實際執行內容,串行照順序執行,Steps 都執行完等於 Job 執行完畢。
因此 Workflow 實際執行的時間軸會是在兩個 Runner 之間穿梭執行 Job,也會並發執行、當前 Job 執行完下一個也不一定是接著同個 Worflow 的下一個 Job。
⚠️ 所以才說確保每個 Job 是獨立的很重要 (尤其在 Self-hosted Runner 環境之下,Job 結束之後可能不會清除的太乾淨,我們可能會下意識覺得這個 Job 的產出,下一個 Job 可以接著做事),不應該這樣用。
如果 Job 有產出要給後續的 Job 使用:
- Job Output String : 輸出純文字到變數給其他 Job 引用
- Artifact-upload/download: Job 的 Step 最後一步將執行結果上傳到 GitHub Artifact,另一個 Job 的第一個 Step 下載回來繼續處理使用。 e.g. 例如 Job — 打包 → 將打包結果 .ipa 傳到 Artifact → Job — 部署 → 下載回來 .ipa → 上傳 App Store 部署 要注意: 目前就算是 Self-hosted 也是會上 GitHub Artifact 雲儲存。
- AWS/GCP…雲端儲存 : 同上,只是是用自己的雲端儲存服務。
- [Self-hosted Only] 共用硬碟、目錄: Self-hosted Runner 如果都有掛載一個共用目錄,則可用這個目錄依照 UUID 建立資料夾存放產出結果,然後後續 Job 去讀取前一個 Job Output UUID 去找到對應的儲存再拿回來用。 要注意,不同主機的 Runner 也都要掛載同個共用目錄。
- 在同個 Job 的 Steps 把所有事情做完。
做中學 GitHub Actions — 案例實作
「坐而言,不如起而行」以上名詞解釋跟流程架構介紹相信大家也是看得懵懵懂懂,接下來會直接舉三個功能例子,帶大家實際動手做,並一邊解釋碰到的東西,從做當中學習,以了解 GitHub Actions 到底是什麼。
案例 — 1
建立 Pull Request 後自動標記 File Changes Size Label 讓 Reviewer 方便安排 Review 工作。
成果圖
運作流程
- 使用者開 PR、重開 PR、Push 新 Commit 到 PR
- 觸發 GitHub Actions Workflow
- shell script 取的 file changes 數量
- 判斷數量對 PR 標記上 Label
- 完成
動手做
Repo → Actions → New workflow → set up a workflow yourself。
檔案名稱: Automation-PullRequest.yml
Action Workflow 可以每個任務獨立一個檔案,也可以依照觸發事件、目的,同個目的聚合在同個檔案,反正多個 Job 是並發執行的,另外 因為 GitHub Actions 暫時不支援目錄結構,所以檔案少一點、使用階層命名檔案會比較好管理 。
這邊把 PR 相關事件的 Actions 都放在同個 Workflow。
Automation-PullRequest.yml
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# Workflow(Action) 名稱
name: Pull Reqeust Automation
# Actions Log 的標題名稱
run-name: "[Pull Reqeust Automation] ${{ github.event.pull_request.title || github.ref }}"
# 觸發事件
on:
# PR 事件
pull_request:
# PR - 開啟、重開、有新 Push Commit 時
types: [opened, synchronize, reopened]
# 同個 Concurrency Group 如果有新的 Job 會取消正在跑的
# 例如 Push Commit 觸發的任務還沒執行就又 Push Commit 時,會取消前一個任務
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
# Job 工作項目
# Job 會並發執行
jobs:
# Job ID
label-pr-by-file-count:
# Job 名稱 (可省略,有設定在 Log 顯示比較好讀)
name: Label PR by changes file count
# 如果這個 Job 失敗,不影響整個 Workflow,繼續其他 Job
continue-on-error: true
# 設定最長 Timeout 時間,防止異常情況發生時無止盡的等待
timeout-minutes: 10
# Runner Label - 使用 GitHub Hosted Runner ubuntu-latest 來執行工作
# 如果是 Private Repo 會計算用量,超過可能會產生費用
runs-on: ubuntu-latest
# 工作步驟
# 工作步驟會照順序執行
steps:
# 步驟名稱
- name: Get changed file count and apply label
# 步驟 ID (可省略,後續若沒有 Step 要引用 Output 輸出則不需設定)
id: get-changed-files-count-by-gh
# 注入外部環境參數到執行階段
env:
# secrets.GITHUB_TOKEN 是 GitHub Actions 執行時自動產生的 Token,不需自行在 Secrets 設定,擁有一些 GitHub Repo API Scopes 權限
# https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/use-github_token-in-workflows
# gh(GitHub) cli 需要注入 GH_TOKEN 到 ENV,gh 才有權限操作
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Shell script
# GitHub Hosted Runner 內建都有安裝好 gh cli,不需要安裝 Job 就能直接使用
run: |
# ${{ github.xxx }} 是 GitHub Actions Context 表達式
# 不是 Shell 變數,而是 YAML 解析階段由 GitHub Actions 替換成對應值
# 其他參數:https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
# 取得 PR 編號:
PR_NUMBER=${{ github.event.pull_request.number }}
# 取得 Repo:
REPO=${{ github.repository }}
# 使用 GitHub API (gh cli) 取得 File changed 數量
FILE_COUNT=$(gh pr view $PR_NUMBER --repo $REPO --json files --jq '.files | length')
# Print Log
echo "Changed file count: $FILE_COUNT"
# Label 邏輯
if [ "$FILE_COUNT" -lt 5 ]; then
LABEL="XS"
elif [ "$FILE_COUNT" -lt 10 ]; then
LABEL="S"
elif [ "$FILE_COUNT" -lt 30 ]; then
LABEL="M"
elif [ "$FILE_COUNT" -lt 80 ]; then
LABEL="L"
elif [ "$FILE_COUNT" -lt 200 ]; then
LABEL="XL"
else
LABEL="XXL"
fi
# 使用 GitHub API (gh cli) 移除目前的 Size Label
EXISTING_LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
for EXISTING in $EXISTING_LABELS; do
case "$EXISTING" in
XS|S|M|L|XL|XXL)
echo "🧹 Removing existing label: $EXISTING"
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$EXISTING"
;;
esac
done
# (可選)如果 Label 不存在則建立
if ! gh label list --repo "$REPO" | grep -q "^$LABEL"; then
echo "🆕 Creating missing label: $LABEL"
gh label create "$LABEL" --repo "$REPO" --description "Size label: $LABEL" --color "ededed"
else
echo "✅ Label '$LABEL' already exists"
fi
# 使用 GitHub API (gh cli) 標記上 Label
gh pr edit $PR_NUMBER --repo $REPO --add-label "$LABEL"
Commit 檔案到 Repo 主分支之後,我們再開新 PR 就會自動觸發 GitHub Actions:
Action 執行狀態顯示 Queued 代表任務正在等待 Runner 接任務回去做。
執行結果
執行完畢並且成功後 PR 上就會自動標記好對應的 Label 了!紀錄會顯示由 github-actions
標記。
完整程式碼: Automation-PullRequest.yml
直接使用別人包好的 Action 步驟: pascalgn/size-label-action
前面有說到可以直接使用別人封裝好的 Action,標記 PR Size Label 這任務已經有現成的輪子可以使用,上述只是為了教學目的,實際上不需要自己重造輪子。
只需要在 Action Workflow Job Step 中直接使用就能完成任務:
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
49
50
# Workflow(Action) 名稱
name: Pull Reqeust Automation
# 觸發事件
on:
# PR 事件
pull_request:
# PR - 開啟、重開、有新 Push Commit 時
types: [opened, synchronize, reopened]
# 同個 Concurrency Group 如果有新的 Job 會取消正在跑的
# 例如 Push Commit 觸發的任務還沒執行就又 Push Commit 時,會取消前一個任務
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
# Job 工作項目
# Job 會並發執行
jobs:
# Job ID
label-pr-by-file-count:
# Job 名稱 (可省略,有設定在 Log 顯示比較好讀)
name: Label PR by changes file count
# 如果這個 Job 失敗,不影響整個 Workflow,繼續其他 Job
continue-on-error: true
# 設定最長 Timeout 時間,防止異常情況發生時無止盡的等待
timeout-minutes: 10
# Runner Label - 使用 GitHub Hosted Runner ubuntu-latest 來執行工作
# 如果是 Private Repo 會計算用量,超過可能會產生費用
runs-on: ubuntu-latest
# 工作步驟
# 工作步驟會照順序執行
steps:
# 步驟名稱
- name: Get changed file count and apply label
# 步驟 ID (可省略,後續若沒有 Step 要引用 Output 輸出則不需設定)
id: get-changed-files-count-by-gh
# 直接使用別人封裝好的程式
uses: "pascalgn/size-label-action@v0.5.5"
# 注入外部環境參數到執行階段
# 參數命名、可用參數要參考說明:https://github.com/pascalgn/size-label-action/tree/main
env:
# secrets.GITHUB_TOKEN 是 GitHub Actions 執行時自動產生的 Token (github-actions 身份),不需自行在 Secrets 設定,擁有一些 GitHub Repo API Scopes 權限
# https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/use-github_token-in-workflows
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
這個包好的 Action 是 JavaScript Action,實際執行程式碼可以參考以下檔案: dist/index.js 。
name, run-name 的補充:
- name: Action Workflow 的名稱
- run-name: 執行紀錄的標題名稱 (可帶入 PR Title or Branch or Author…etc) 如果是 on:pull_request 事件預設是 PR Title。
案例 — 2
建立 Pull Request 後如果沒有 Assignee 則自動 Assign 作者自己並且 Comment 提示。(只有在初次建立時才會執行)
成果圖
運作流程
- 使用者開 PR
- 觸發 GitHub Actions Workflow
- github script 取得 assignee
- 如果沒有 asignee 則 assign 開 PR 的作者 & Comment 訊息
- 完成
動手做
Repo → Actions → New workflow → set up a workflow yourself。
檔案名稱: Automation-PullRequest.yml
(同上)
Automation-PullRequest.yml
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# Workflow(Action) 名稱
name: Pull Reqeust Automation
# Actions Log 的標題名稱
run-name: "Pull Reqeust Automation - Daily Checker"
# 觸發事件
on:
# PR 事件
pull_request:
# PR - 開啟、重開、有新 Push Commit 時
types: [opened, synchronize, reopened]
# 同個 Concurrency Group 如果有新的 Job 會取消正在跑的
# 例如 Push Commit 觸發的任務還沒執行就又 Push Commit 時,會取消前一個任務
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
# Job 工作項目
# Job 會並發執行
jobs:
# Job ID
label-pr-by-file-count:
# 請參考前文,略....
# ---------
assign-self-if-no-assignee:
name: Automatically assign to self if no assignee is specified
# 因為是共用觸發事件,所以在 Job 上自己判斷,當是 Pull Request Opened(首次建立) 時才執行 Job 否則會 Skipped
if: github.event_name == 'pull_request' && github.event.action == 'opened'
# 如果這個 Job 失敗,不影響整個 Workflow,繼續其他 Job
continue-on-error: true
# 設定最長 Timeout 時間,防止異常情況發生時無止盡的等待
timeout-minutes: 10
# Runner Label - 使用 GitHub Hosted Runner ubuntu-latest 來執行工作
# 如果是 Private Repo 會計算用量,超過可能會產生費用
runs-on: ubuntu-latest
steps:
- name: Assign self if No Assignee
# 使用 GitHub Script (JavaScript) 撰寫腳本 (Node.js 環境)
# 相較上面直接用 Shell Script 寫起來更方便漂亮
# 也不需要自行注入環境變數、GITHUB_TOKEN
uses: actions/github-script@v7
with:
script: |
// github-script 中會自動注入到 context 變數供 javascript 直接引用
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
const issue = context.payload.pull_request; // 如果要連 Issue 一起支援可寫成 context.payload.issue || context.payload.pull_request
const assignees = issue.assignees || [];
const me = context.actor;
if (assignees.length === 0) {
// Assignee 設成自己
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: [me]
});
// 留言通知
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `🔧 No assignee was set, so I have assigned this to myself (@${me}).`
});
}
這次我們示範改用 GitHub Script (JavaScript) 撰寫腳本,程式碼語法上更彈性更好撰寫。
當然如果你想照我之前說的每個任務一個檔案,就可以拔掉 Job If. . 直接在 Action Workflow 觸發條件設定:
Automation-PullRequest-Auto-Assign.yml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Workflow(Action) 名稱
name: Pull Reqeust Automation - Auto Assignee Self
# 觸發事件
on:
# PR 事件
pull_request:
# PR - 開啟時
types: [opened]
jobs:
assign-self-if-no-assignee:
name: Automatically assign to self if no assignee is specified
runs-on: ubuntu-latest
steps:
# 請參考前文,略....
Commit 檔案到 Repo 主分支之後,我們再開新 PR 就會自動觸發 GitHub Actions:
現在有兩個 Job 要執行了!
執行結果
執行完畢並且成功後 PR 如果沒有 Asignees 會自動 Assign PR 作者並且 Comment 訊息。(都是用 github-actions
身份操作)
完整程式碼: Automation-PullRequest.yml
測試重開(Reopened) PR
可看到只會執行 Size Label Job,Auto Assignee Job 被 Skipped 了。
這個任務也有人包好 Action 可以直接復用,可參考: pozil/auto-assign-issue 。
案例 — 3
每日早上 9 點自動統計當前 PR 數量及已開啟多久時間發送通知訊息到 Slack 工作尋組、自動關閉已開啟超過 3 個月的 PR。
成果圖
- Slack 工作群組每天早上自動收到報告
- 自動關閉超過 90 天的 PR
運作流程
- GitHub Actions 每天早上 9 點自動觸發
- 觸發 GitHub Actions Workflow
- github script 取得 開啟中的 PR 列表、統計開啟了幾天
- 傳送統計報告訊息到 Slack
- 關閉超過 90 天的 PR
- 完成
動手做
Repo → Actions → New workflow → set up a workflow yourself。
檔案名稱: Automation-PullRequest-Daily.yml
Automation-PullRequest-Daily.yml
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# Workflow(Action) 名稱
name: Pull Reqeust Automation - Daily Checker
# 觸發事件
on:
# 排程定時自動執行
# https://crontab.guru/
# UTC 時間
schedule:
# UTC 的 01:00 = 每天 UTC+8 的 09:00
- cron: '0 1 * * *'
# 手動觸發
workflow_dispatch:
# Job 工作項目
# Job 會並發執行
jobs:
# Job ID
caculate-pr-status:
# Job 名稱 (可省略,有設定在 Log 顯示比較好讀)
name: Caculate PR Status
# Runner Label - 使用 GitHub Hosted Runner ubuntu-latest 來執行工作
# 如果是 Private Repo 會計算用量,超過可能會產生費用
runs-on: ubuntu-latest
# Job Output
outputs:
pr_list: ${{ steps.pr-info.outputs.pr_list }}
# 工作步驟
# 工作步驟會照順序執行
steps:
# 步驟名稱
- name: Fetch open PRs and caculate
# Step 外部要引用 Output 輸出,需設定
id: pr-info
uses: actions/github-script@v7
with:
script: |
const now = new Date();
const per_page = 100;
let page = 1;
let allPRs = [];
while (true) {
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page,
page,
});
if (prs.length === 0) break;
allPRs = allPRs.concat(prs);
if (prs.length < per_page) break;
page++;
}
const result = allPRs.map(pr => {
const created = new Date(pr.created_at);
const daysOpen = Math.floor((now - created) / (1000 * 60 * 60 * 24));
return {
pr: pr.number.toString(),
title: pr.title,
idle: daysOpen
};
});
// 設定回 Output,只接受 String
core.setOutput('pr_list', JSON.stringify(result));
# ----
send-pr-summary-message-to-slack:
name: Send PR Summary Messag to Slack
# Job 預設是並發,使用 needs 可以迫使當前 Job 等到 need Job 完成時才會執行
needs: [caculate-pr-status]
runs-on: ubuntu-latest
steps:
- name: Generate Message
# Step 外部要引用 Output 輸出,需設定
id: gen-msg
uses: actions/github-script@v7
with:
script: |
const prList = JSON.parse(`${{ needs.caculate-pr-status.outputs.pr_list }}`);
const blocks = [];
// 標題
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `📬 *Open PR Report*\nTotal: *${prList.length}* PR(s)`
}
});
// 每個 PR 一行
for (const pr of prList) {
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `• <https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${pr.pr}|PR #${pr.pr}> *${pr.title}* - 🕒 ${pr.idle} day(s)`
}
});
}
// 設定回 Output,只接受 String
core.setOutput('blocks', JSON.stringify(blocks));
# 使用 Slack 官方封裝好的 Slack API Github Actions
# https://tools.slack.dev/slack-github-action/sending-techniques/sending-data-slack-api-method/
# 發送訊息
- name: Post text to a Slack channel
uses: slackapi/slack-github-action@v2.1.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ vars.SLACK_TEAM_CHANNEL_ID }}
blocks: ${{ steps.gen-msg.outputs.blocks }}
# ----
auto-close-old-prs:
name: Auto Close Old PRs
needs: [caculate-pr-status]
runs-on: ubuntu-latest
steps:
- name: Auto close PRs opened more than 90 days
uses: actions/github-script@v7
with:
script: |
const prList = JSON.parse(`${{ needs.caculate-pr-status.outputs.pr_list }}`);
const oldPRs = prList.filter(pr => pr.idle > 90);
for (const pr of oldPRs) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(pr.pr),
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(pr.pr),
body: `⚠️ This pull request has been automatically closed because it has been open for more than 90 days. Please reopen if needed.`
});
}
console.log(`Closed ${oldPRs.length} PR(s)`);
在這個範例中我們使用:
- on: schedule Crontab 排程自動觸發跟 workflow_dispatch 支援手動觸發
- Job output/Step output (都只能是字串)
- 多個 Jobs 預設是並發但可以用
needs
時間依賴等待關係 - 從 Repo Secrets/Variables 取得設定
- 串接 Slack API
Repo Secrets — 新增 SLACK_BOT_TOKEN
- Slack App 建立、發訊息權限設定可參考我 之前的文章
Repo Variables — 新增 SLACK_TEAM_CHANNEL_ID
Commit 檔案到 Repo 主分支之後,回到 Actions 手動觸發看看:
日後會每日自動觸發。
Actions → Pull Request Automation — Daily Checker → Run workflow → Branch: main → Run workflow。
執行後可以點擊進入查看執行狀況:
因為有 needs
的限制,Jobs 流程就會是 Cacluate PR Status
先完成後才會並發執行 Auto Close Old PRs
和 Send PR Summary Message to Slack
。
執行結果
任務都成功執行後可以查看 Slack 訊息:
成功 🚀🚀🚀
完整程式碼: Automation-PullRequest-Daily.yml
小結
希望上面三個案例能讓您對 GitHub Actions 有個初步的了解也希望有激起你的自動化工作創意,可以自己發想工作流程(請務必先參考 觸發事件 ),然後撰寫腳本執行;也記得先去 Marketplace 找找現有的步驟可以站在巨人的肩膀直接使用。
本篇只是入門基礎(甚至沒 Checkout Code),下一篇「 CI/CD 實戰指南(三):使用 GitHub Actions 實作 App 專案的 CI 與 CD 工作流程 」會在介紹更複雜深入的 GitHub Actions Workflow。
GitHub 自動化延伸議題
GitHub 可以與 Slack 整合,訂閱 Repo 的 PR 更新通知、Push Default Branch 通知…等等。
問題 1. GitHub 訊息無法標記到人,只標記了 GitHub 帳號,Slack 帳號收不到通知:
Slack App or Apps 搜尋 GitHub → 打開訊息視窗 →完成 Connect GitHub Account 步驟,這樣 GitHub 才知道你對應的 Slack UID 是什麼,才能標記你。
問題 2. Pull Request Reminder
我記得這本來是第三方開發的功能,後來直接整合在 GitHub 裡面了,不要傻傻的自己用 GitHub Actions 寫腳本做!
GitHub 內建的是 by Team 設置,你需要先為組織增加 Team、Repo 加入 Team 然後才能設置 Pull Request Reminder。
- 輸入完 Slack Channel 跟規則儲存之後每天就會自動傳送提示訊息跟標記 Reviewer 了,設置可 參考 ,結果如下圖:
https://michaelheap.com/github-scheduled-reminders/
問題 4. GitHub PR 訊息,作者收不到通知:
這是一個萬年的問題,就是 PR 的 Slack 訊息 其實作者並不知道有人 Reply Threads 討論 ,GitHub 發的這則 Slack 訊息只會標記 Reviewers,不會標記 Assignee or PR 作者。
這時候 GitHub Actions 做自動化小工具就派上用場了,我們可以加一個 Job 在 PR Description 最後 Append 標記作者,這樣訊息傳到 Slack 也會一並標記到作者的 Slack:
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
# Workflow(Action) 名稱
name: Pull Reqeust Automation
# 請參考前文,略....
# ---------
append-author-to-pr-description:
name: Append author to PR description
# 因為是共用觸發事件,所以在 Job 上自己判斷,當是 Pull Request Opened(首次建立) 時才執行 Job 否則會 Skipped
if: github.event_name == 'pull_request' && github.event.action == 'opened'
# 如果這個 Job 失敗,不影響整個 Workflow,繼續其他 Job
continue-on-error: true
# 設定最長 Timeout 時間,防止異常情況發生時無止盡的等待
timeout-minutes: 10
# Runner Label - 使用 GitHub Hosted Runner ubuntu-latest 來執行工作
# 如果是 Private Repo 會計算用量,超過可能會產生費用
# 但這種自動化小工作不太容易用過量
runs-on: ubuntu-latest
steps:
- name: Append author to PR description
env:
# secrets.GITHUB_TOKEN 是 GitHub Actions 執行時自動產生的 Token,不需自行在 Secrets 設定,擁有一些 GitHub Repo API Scopes 權限
# gh(GitHub) cli 需要注入 GH_TOKEN 到 ENV,gh 才有權限操作
# https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/use-github_token-in-workflows
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ${{ github.xxx }} 是 GitHub Actions Context 表達式
# 不是 Shell 變數,而是 YAML 解析階段由 GitHub Actions 替換成對應值
# 其他參數:https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
PR_NUMBER: ${{ github.event.pull_request.number }}
AUTHOR_TAG: '@${{ github.event.pull_request.user.login }}'
run: |
PR_BODY=$(gh pr view $PR_NUMBER --repo ${{ github.repository }} --json body -q ".body")
NEW_BODY=$(printf "%s\n\nCreated by %s" "$PR_BODY" "$AUTHOR_TAG")
gh pr edit $PR_NUMBER --repo ${{ github.repository }} --body "$NEW_BODY"
完整程式碼: Automation-PullRequest.yml
開 PR 時會自動在描述後加上作者資訊、Slack 訊息也會正確的標記作者:
問題 5. GitHub 瘋狂寄通知信到信箱很干擾:
GitHub Repo 通知跟 Slack 都有設定好之後,統一都在 Slack 收通知了,可以去 GitHub 個人設定把 Email 通知關掉:
其他
Actions 目前沒有目錄結構,但可以在 Actions 頁,置頂(Pin) 五個 Actions;也可以使用 Disable 暫停某個 Action。
可以在 Insights 查看 GitHub Actions 用量跟執行成效:
Reuse Workflow 補充
下一篇文章 有用到同個 Repo 拆 Reuse Workflow,這邊補充一下不同 Repo 拆 Reuse Workflow 的範例。
先定義一個 Reuse Workflow: Automation-label-pr-base-branch.yml: 在 ZhgChgLi/github-actions-ci-cd-demo-share-actions Repo 。
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
name: Automation-label-pr-base-branch
run-name: "[Automation-label-pr-base-branch] ${{ github.event.inputs.PR_NUMBER || github.ref }}"
concurrency:
group: ${{ github.workflow }}-${{ github.event.inputs.PR_NUMBER || github.ref }}
cancel-in-progress: true
# 觸發事件
on:
# 其他 Workflow 呼叫此 Workflow 觸發
workflow_call:
# 資料輸入
inputs:
# PR Number
PR_NUMBER:
required: true
type: string
# 密鑰輸入
secrets:
GH_TOKEN:
description: "GitHub token for API access"
required: true
jobs:
label-pr-base-branch:
name: Label PR Base Branch
continue-on-error: true
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Label PR by Base Branch
id: label-pr-base-branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ inputs.PR_NUMBER }}"
REPO="${{ github.repository }}"
echo "📦 Processing PR #$PR_NUMBER in repo $REPO"
# 取得 PR 的 base branch
BASE_BRANCH=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json baseRefName --jq '.baseRefName')
echo "🔖 PR Base Branch: $BASE_BRANCH"
# 允許的 base branch labels
BRANCH_LABELS=("develop" "main" "master")
# Label 是否在允許列表
if [[ " ${BRANCH_LABELS[@]} " =~ " ${BASE_BRANCH} " ]]; then
LABEL="$BASE_BRANCH"
else
echo "⚠️ Base branch '$BASE_BRANCH' not in allowed list, skipping label."
exit 0
fi
# 移除現有的 base branch labels
EXISTING_LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
for EXISTING in $EXISTING_LABELS; do
if [[ " ${BRANCH_LABELS[@]} " =~ " ${EXISTING} " ]]; then
echo "🧹 Removing existing base branch label: $EXISTING"
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$EXISTING"
fi
done
# 如果 Label 不存在則建立
if ! gh label list --repo "$REPO" | grep -q "^$LABEL$"; then
echo "🆕 Creating missing label: $LABEL"
gh label create "$LABEL" --repo "$REPO" --description "PR targeting $LABEL branch" --color "ededed"
else
echo "✅ Label '$LABEL' already exists"
fi
# 加上 base branch label
echo "🏷️ Adding label '$LABEL' to PR #$PR_NUMBER"
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "$LABEL"
回到我們的 主 ZhgChgLi/github-actions-ci-cd-demo Repo, 新增一個主 Workflow Demo-Reuse-Action-From-Another-Repo.yml :
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
name: Automation-label-pr-base-branch
on:
pull_request:
types: [opened]
jobs:
call-label-pr-workflow:
name: Call Base Branch Label Workflow
# ✅ 如果被呼叫的 workflow 在同一個 repo:
# uses: ./.github/workflows/Automation-label-pr-base-branch.yml
# 注意:這種寫法無法指定分支(固定使用 caller workflow 的 branch)
# 參考範例:
# - CD-Deploy-Form.yml 呼叫本 repo workflow
# - CD-Deploy.yml 呼叫跨 repo workflow
#
# ✅ 如果被呼叫的 workflow 在其他 repo:
# uses: {owner}/{repo}/.github/workflows/{file}.yml@{branch_or_tag}
# 可以指定 branch 或 tag
# ref: https://github.com/ZhgChgLi/github-actions-ci-cd-demo-share-actions/blob/main/.github/workflows/Automation-label-pr-base-branch.yml
uses: ZhgChgLi/github-actions-ci-cd-demo-share-actions/.github/workflows/Automation-label-pr-base-branch.yml@main
with:
PR_NUMBER: ${{ github.event.number }}
# 如果所有 secrets 都要繼承 caller workflow,直接用 `inherit`
# secrets: inherit
#
# 如果只要傳特定 secrets,個別指定
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Commit 檔案後開 PR 試試看。
工作流程:主 Repo 的 Demo-Reuse-Action-From-Another-Repo.yml → 呼叫並把參數傳到另一個 Repo 的 Automation-label-pr-base-branch.yml 執行任務 → 回到主 Repo 回報結果。
成功!
Private Repo 存取問題
這邊要注意一個安全性問題,Public Repo 完全沒問題,但如果 Reuse Workflow 在 Private Repo 當中,只有在 同個組織下的其他 Repo + Repo 同樣也是 Private + 設定允許 =才能存取使用 Private Repo Reuse Workflow。
Public — ZhgChgLi/ReuseRepo
+Private or Public-ZhgChgli or harry/myRepo
= ✅Private — ZhgChgLi/ReuseRepo
+Private or Public— harry/myRepo
= ❌Private — ZhgChgLi/ReuseRepo
+Public — ZhgChgLi/anotherRepo
= ❌Private — ZhgChgLi/ReuseRepo
+Private — ZhgChgLi/anotherRepo
= ✅
設定允許的方式,要去 Reuse Workflow Repo → Settings → Actions → General → Acces:
改勾選:
1
2
Accessible from repositories in the 'ZhgChgLi' organization
Workflows in other repositories that are part of the 'ZhgChgLi' organization can access the actions and reusable workflows in this repository. Access is allowed only from private repositories.
點擊「Save」儲存,完成。
如果沒權限存取會出現以下錯誤訊息:
1
2
3
4
5
Invalid workflow file: .github/workflows/Demo-reuse-action-from-another-repo.yml#L21
error parsing called workflow
".github/workflows/Demo-reuse-action-from-another-repo.yml"
-> "ZhgChgLi/github-actions-ci-cd-demo-share-actions/.github/workflows/Automation-label-pr-base-branch.yml@main"
: workflow was not found.
GitHub Actions Script 語法補充
補充一下大家可能會感到疑惑的 ${{ XXX }}
or ${XXX}
or $XXX
變數語法。
Demo-Env-Vars- .yml:
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
name: Demo-Env-Vars
on:
workflow_dispatch:
jobs:
print-env-vars:
runs-on: ubuntu-latest
steps:
- name: print-env
run: env
- name: context-vars-vs-vars
env:
SAY_MY_NAME: "Heisenberg"
# GitHub Actions Context 表達式,同下方解釋
FROM_REF: "${{ github.ref }}"
run: |
# Sehll Script:
# 引用自訂注入的 ENV 變數內容
# ${SAY_MY_NAME} or $SAY_MY_NAME 都可以
# ${XX} 遇到字串拼接比較好用:
echo "HI: ${SAY_MY_NAME}"
# 💡 GitHub Actions Context 表達式
# 這不是 shell 變數,而是 YAML 解析階段由 GitHub Actions 替換成對應值
# ⚠ 在本機或非 GitHub Actions 環境執行會失敗
# 🔗 https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
BRANCH_NAME_FROM_CONTEXT="${{ github.ref }}"
# 💡 GitHub Actions 執行階段的環境變數
# 這些變數在 runner 執行 shell 時由 GitHub Actions 自動注入
# ✅ 在其他環境可用 export 或 ENV 預先定義相同變數
# 🔗 https://docs.github.com/en/actions/learn-github-actions/environment-variables
BRANCH_NAME_FROM_ENV_VARS="${GITHUB_REF}"
echo "FROM_REF: ${FROM_REF}"
echo "BRANCH_NAME_FROM_CONTEXT: ${BRANCH_NAME_FROM_CONTEXT}"
echo "BRANCH_NAME_FROM_ENV_VARS: ${BRANCH_NAME_FROM_ENV_VARS}"
- name: print-github-script-env
uses: actions/github-script@v7
env:
SAY_MY_NAME: "Heisenberg"
# GitHub Actions Context 表達式,同上方解釋
FROM_REF: "${{ github.ref }}"
with:
script: |
// GitHub Script: (JavaScript (Node.js)):
// 從 process.env 取 ENV 值
console.log(`HI: ${process.env.SAY_MY_NAME}`);
console.log(`FROM_REF: ${process.env.FROM_REF}`);
// github-script 中會自動注入到 context 變數供 javascript 直接引用
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
const branch_name_from_context_vars = context.ref;
console.log(`branch_name_from_context_vars: ${branch_name_from_context_vars}`);
// 同樣也能用 GitHub Actions Context 表達式(只是意義不大):
const branch_name_from_context = "${{ github.ref }}";
console.log(`branch_name_from_context: ${branch_name_from_context}`);
for (const [key, value] of Object.entries(process.env)) {
console.log(`${key}=${value}`);
}
// github-script 中的 github 物件是 Octokit REST API 實例
// 用來操作 github api 使用
// 例如:
// await github.rest.pulls.list({
// owner: context.repo.owner,
// repo: context.repo.repo,
// state: "open"
// });
# gh CLI does NOT use GITHUB_TOKEN by default; requires GH_TOKEN
- name: gh CLI without GH_TOKEN (expected to fail)
continue-on-error: true
run: |
PR_COUNT=$(gh pr list --repo $GITHUB_REPOSITORY --json number --jq 'length')
echo "Found $PR_COUNT open pull requests"
- name: gh CLI with GH_TOKEN (expected to succeed)
env:
# Assign GH_TOKEN so gh CLI can authenticate
# https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-guides/use-github_token-in-workflows
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_COUNT=$(gh pr list --repo $GITHUB_REPOSITORY --json number --jq 'length')
echo "Found $PR_COUNT open pull requests"
- name: github-script auto-authentication (no GH_TOKEN needed)
uses: actions/github-script@v7
with:
script: |
// github = Octokit REST client (auto-authenticated with GITHUB_TOKEN)
const pulls = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: "open"
});
console.log(`Found ${pulls.data.length} open pull requests`);
${ { XXX } } GitHub Actions Context 表達式
${{ XXX }}
是 GitHub Actions Context 表達式, 在YAML 解析階段由 GitHub Actions 替換成對應值 (如上圖BRANCH_NAME_FROM_CONTEXT
)- 完整可用表達式可 參考官方文件
${XXX}
or$XXX
是真實環境變數,${XXX}
在字串拼接比較好用- GitHub Actions 預設會注入一些環境變數,完整可 參考官方文件
actions/github-script@v7
- 在
github-script
中同樣可以用${{ XXX }}
是 GitHub Actions Context 表達式,但是意義不大;因為 在 github-scirpt 環境中預設會注入 context 變數可以直接在 javascript 中引用 - 在
github-script
中github
物件是 Octokit REST API 實例 github-script
也能從env:
注入變數,但是要用${process.env.xxx}
存取- 同樣預設會注入一些環境變數,完整可 參考官方文件
${process.env.xxx}
存取;但是已經有context
變數了,從這存取意義也不大
github-script
會自動帶入github-token
,無需指定
gh cli GH_TOKEN
- 在 GitHub Actions Shell Script 使用
gh cli
需指定GH_TOKEN
參數 - 可以直接帶入預設 Token —
secrets.GITHUB_TOKEN
(GitHub Actions 執行時自動產生) - 預設 Token 有一些基本權限,如果要操作的指令權限不夠就要改用 Personal Access Token ,團隊使用建議用共用帳號建立 PAT。
完整程式碼: Demo-Env-Vars- .yml
GitHub Actions 已經開發好了,下一步我們可以抽換 GitHub Hosted Runner 成自己的 Self-hosted Runner。
GitHub Hosted Runner 有免費額度 2,000 分鐘一個月(起),跑這種小的自動化任務花不了多少時間,而且是跑在 linux 機器花費很低,可能還用不到免費上限; 不一定要改成 Self-hosted Runner ,改 Runner 還要確保 Runner 環境正確(例如 GitHub Hosted Runner 自帶 gh cli,self-hosted Runner 就記得要自己安裝好), 本文純粹是因為教學才會這樣抽換 。
如果是用在執行 CI/CD 任務才必須要用 Self-hosted Runner。
新增 Self-hosted Runner
本文以 macOS M1 為例。
- Settings → Actions → Runners → New self-hosted runner。
- Runner image: macOS
- Architecture : M1 記得選 ARM64 執行比較快
在實體電腦上開一個 Termnial。
按照 Download 步驟在本地電腦完成:
1
2
3
4
5
6
7
8
9
10
# 在你想要的路徑,建立 Runner 目錄
mkdir actions-runner && cd actions-runner
# 下載 Runner image
curl -o actions-runner-osx-x64-2.325.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.325.0/actions-runner-osx-x64-2.325.0.tar.gz
# Optional: Validate the hash
echo "0562bd934b27ca0c6d8a357df00809fbc7b4d5524d4aeb6ec152e14fd520a4c3 actions-runner-osx-x64-2.325.0.tar.gz" | shasum -a 256 -c
# 解壓縮
tar xzf ./actions-runner-osx-x64-2.325.0.tar.gz
以上只是參考;建議照你設定頁裡的步驟來做,才會是最新版本的 Runner image。
設定 Configure:
1
2
# 請參考設定頁指令,Token 會隨時間改變
./config.sh --url https://github.com/ORG/REPO --token XXX
會依序要你輸入:
- Enter the name of the runner group to add this runner to: [press Enter for Default] 直接 Enter *只有註冊到 Organization 組織層級的 Runner 才有 Group 分組功能
- Enter the name of runner: [press Enter for ZhgChgLideMacBook-Pro] 可以輸入想設定的 Runner 名稱 e.g.
app-runner-1
或直接 Enter - This runner will have the following labels: ‘self-hosted’, ‘macOS’, ‘X64’ Enter any additional labels (ex. label-1,label-2): [press Enter to skip] 輸入想要設定的 Runner label,可以多輸入自訂 Label 方便之後使用 同前述 GitHub Acitons/Runner 是依照對應 Label 找任務來做, 如果只用 default label,Runner 可能會撿到組織內的其他 Runner 來執行工作,自訂一個最保險 。 這邊我自己亂定了一個 label
self-hosted-zhgchgli
- Enter name of work folder: [press Enter for _work] 直接 Enter
出現 √ Settings Saved. 代表設定完成了。
啟動 Runner:
1
./run.sh
出現 √ Connected to GitHub、Listening for Jobs 就代表已經在監聽 Actions 任務了:
這個 Terminal 視窗不關閉就會持續接收任務來做。
🚀🚀🚀同一台電腦開多個 Terminal 在不同目錄就能起多個 Runner。
回到 Repo 設定頁也能看到 Runner 正在等待任務:
Status:
- Idle: 閑置,在等待任務
- Active: 有任務正在執行
- Offline: Runner 不在線上
Workflow(GitHub Actions) Runner 改用 Self-hosted Runner
以 Automation-PullRequest.yml
為例:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 請參考前文,略....
jobs:
label-pr-by-file-count:
# 請參考前文,略....
runs-on: [self-hosted-zhgchgli]
# 請參考前文,略....
# ---------
assign-self-if-no-assignee:
# 請參考前文,略....
runs-on: [self-hosted-zhgchgli]
steps:
# 請參考前文,略....
Commit 檔案到 Repo 主分支之後,重新開 PR 觸發驗證一下 Actions。
回到 Runner Terminal 就能看到有新的任務進來了,正在執行跟執行結果:
失敗了,因為我本機電腦沒有安裝 gh cli 環境:
使用 brew install gh
在實體電腦上安裝完 gh 後再次觸發執行:
成功!現在這個任務就完全是使用我們自己的電腦在執行,不使用 GitHub Hosted Runner、不計費使用。
我們可以點進去 Action Log 查看任務執行在的 Runner、機器是哪一個:
runs-on: [ Runner Label] 設定
這裡是 AND 不是 OR,GitHub Runner 暫不支援 OR 挑選 Runner 執行。
例如: [self-hosted, macOS, app]
→ 代表 Runner 要 同時有 self-hosted, macOS, app
這 3 個 Labels 才會匹配接任務來執行。
如果一個 Job 想同時測試不同 Runner 環境下的結果可以使用 matrix
參數:
1
2
3
4
5
6
7
8
9
10
11
jobs:
test:
runs-on: ${{ matrix.runner }}
strategy:
matrix:
runner:
- [self-hosted, linux, high-memory]
- [self-hosted, macos, xcode-15]
steps:
- run: echo "Running on ${{ matrix.runner }}"
這樣這個 Job 會在下列兩個 Runner Labels Runner 中並行各執行一次:
- self-hosted, linux, high-memory
- self-hosted, macos, xcode-15
Runner 暫不支援:
- OR 挑選 Runner
- Runner 權重設定
註冊 Runner 成 Service
可以參考官方文件「 Configuring the self-hosted runner application as a service 」將 Runner 直接註冊成系統 Service,這樣就能在背景執行(不用開 Terminal 在前景)、開機後也會自動啟動。
有多個 Runner 記得調整「 Customizing the self-hosted runner service 」設定註冊不同名稱。
iOS 這邊我有一個待研究排除的問題, 就是我改用背景 Service 之後在 Archive 會遇到錯誤(疑似是跟 keychain 權限有關) ,當時沒時間解決,就先用起前景 Terminal Runner 了。
如果是傳統前景要做到開機自動啟動就要去 ~/Library/LaunchAgents
新增一個自動啟動設定檔案:
actions.runner.REPO.RUNNER_NAME.plist
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>actions.runner.REPO.RUNNER_NAME</string>
<!-- 指定 Terminal.app 來啟動 -->
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>-a</string>
<string>Terminal</string>
<string>/Users/zhgchgli/Documents/actions-runner/run.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>/Users/zhgchgli/Documents/actions-runner</string>
</dict>
</plist>
有興趣深入研究 DevOps 的朋友可參考官方 k8s Runner 文件。
完整專案 Repo
官方文件
更細節的設置方式請務必參考官方手冊。
AI Can Help!
實測給 ChatGPT 完整的步驟跟時機點就能幫你寫好 GitHub Actions 直接使用!
總結
現在你應該已經對 GitHub Actions + Self-hosted Runner 有了一定程度的了解,下一篇我將開始以 App (iOS) CI/CD 為案例,手把手建置整套流程。
系列文章:
- CI/CD 實戰指南(一):CI/CD 是什麼?如何透過 CI/CD 打造穩定高效的開發團隊?工具選擇?
- CI/CD 實戰指南(二):GitHub Actions 與 self-hosted Runner 使用與建置大全
- CI/CD 實戰指南(三):使用 GitHub Actions 實作 App 專案的 CI 與 CD 工作流程
- CI/CD 實戰指南(四):使用 Google Apps Script Web App 串接 GitHub Actions 建置免費易用的打包工具平台
Buy me a coffee
本系列文章花費了大量的時間精力撰寫,如果內容對您有幫助、對您的團隊有實質提升工作效率與產品品質;歡迎請我喝杯咖啡,感謝支持!
有任何問題及指教歡迎 與我聯絡 。
本文首次發表於 Medium ➡️ 前往查看
由 ZMediumToMarkdown 與 Medium-to-jekyll-starter 提供自動轉換與同步技術。