Post

CI/CD 實戰指南(二):GitHub Actions 與 Self-hosted Runner 使用與建置大全

帶您從頭了解 GitHub Actions/Self-hosted Runner 運作方式與手把手使用教學。

CI/CD 實戰指南(二):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](https://unsplash.com/@theoneandonlydantaylor?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash){:target="_blank"}

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/ )
  • 同個組織可以跨 Repo 分享 Actions

GitHub Repo —Actions Secrets

Repo → Settings → Secrets and variables → Actions → Secrets。

GitHub Repo — Actions Variables

Repo → Settings → Secrets and variables → Actions → Variables。

  • 存放 Actions 步驟中常用到的變數 e.g. 模擬器 iOS 版本、工作目錄
  • Variables 內容可以查看、編輯
  • Variables 內容可以輸出在 Action Log 中
  • Variables 只支援純文字,也可存放 json 字串然後自己解析使用

GitHub Actions — Trigger

  • Github Action 中最重要的起始點 — 觸發事件(條件)
  • 符合觸發事件的 GitHub Actions 才會觸發執行
  • 完整事件列表可 參考官方文件
  • 基本上涵蓋了所有 CI/CD、自動化會遇到的事件場景。 但 如果有特殊場景沒有事件,那就只能用其他事件+在 Job 中判斷組合或是用 Schedule 排程手動檢查了 。 e.g. 例如沒有 PR Merged 事件,就只能用 pull_request: closed + Job if: github.event.pull_request.merged == true 達成

常用事件:

  • schedule (cron):排程定時執行(同 crontab) 可以用來做自動化:定時檢查 PR、定時打包、定時執行自動化腳本
  • pull_request: :PR 相關事件 當 PR 開啟時、PR Assign 時、加 Label 時、有新 Push Commit 時…等等
  • issuesissue_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 檔案為主
  • 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)

GitHub Actions — Workflow — Job — Step

  • GitHub Actions 中的最小執行項目
  • Job 中實際執行任務的程式
  • 每個 Job 可以有多個 Steps
  • 多個 Steps 是照順序執行
  • Step 做完可以 Output 字串給後續 Steps 參考使用。
  • Step 可以直接撰寫 shell script 程式 可以引用 gh cli 、當前環境變數(例如取得 PR 編號),直接做想做的事

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 列表:

[2025/06 的 Images 列表](https://github.com/actions/runner-images){:target="_blank"}

2025/06 的 Images 列表

  • Runner 預先安裝了什麼可以點進去查看: e.g. macos-14-arm64

[macos-14-arm64](https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md){:target="_blank"}

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](https://docs.github.com/en/billing/managing-billing-for-your-products/about-billing-for-github-actions){:target="_blank"}

about-billing-for-github-actions

可以看到 macOS 的價格因為設備成本很高所以貴。

  • 最多並發任務數限制:

[usage-limits-billing-and-administration](https://docs.github.com/en/actions/concepts/overview/usage-limits-billing-and-administration#usage-limits){:target="_blank"}

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; 買一台架設好用超過三個月就回本 了!
  • 架設 Runner 只需 5 步驟 (10 分鐘內) 就能上線開始接任務執行
  • 支援 Windows, macOS, Linux (x64/ARM/ARM64)
  • ⚠️目前:actions/cache, actions/upload-artifact, actions/download-artifact 都只支援 GitHub 雲端服務,代表這些內容還是會上傳到 GitHub 伺服器並計算儲存量收費。 可以在自己的機器上開共用目錄取代。

做中學 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
# 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
    # 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 (github-actions 身份),不需自行在 Secrets 設定,擁有一些 GitHub Repo API Scopes 權限
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        # Shell script
        # GitHub Hosted Runner 內建都有安裝好 gh cli,不需要安裝 Job 就能直接使用
        run: |
          # ${{ github }} 是 GitHub Actions 執行時會自動注入的變數參考,可以從中取得執行階段資訊
          
          # 取得 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
# 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
    # 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 權限
          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

這個包好的 Action 是 JavaScript Action,實際執行程式碼可以參考以下檔案: dist/index.js

案例 — 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
# 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:
    # 請參考前文,略....
  # ---------
  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'
    
    # 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: |
            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

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 PRsSend PR Summary Message to Slack

執行結果

任務都成功執行後可以查看 Slack 訊息:

成功 🚀🚀🚀

完整程式碼: Automation-PullRequest-Daily.yml

小結

希望上面三個案例能讓您對 GitHub Actions 有個初步的了解也希望有激起你的自動化工作創意,可以自己發想工作流程(請務必先參考 觸發事件 ),然後撰寫腳本執行;也記得先去 Marketplace 找找現有的步驟可以站在巨人的肩膀直接使用。

其他

Actions 目前沒有目錄結構,但可以在 Actions 頁,置頂(Pin) 五個 Actions;也可以使用 Disable 暫停某個 Action。

可以在 Insights 查看 GitHub Actions 用量跟執行成效:

Self-hosted Runner 建置與改用

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 自動擴展

註冊 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>

總結

現在你應該已經對 GitHub Actions + Self-hosted Runner 有了一定程度的了解,下一篇我將開始以 App (iOS) CI/CD 為案例,手把手建置整套流程。

下一篇:

[撰寫中,敬請期待] CI/CD 實戰指南(三):使用 GitHub Actions 實作 App 專案的 CI 與 CD 工作流程

有任何問題及指教歡迎 與我聯絡


本文首次發表於 Medium ➡️ 前往查看

ZMediumToMarkdownMedium-to-jekyll-starter 提供自動轉換與同步技術。

Improve this page on Github.

Buy me a beer

This post is licensed under CC BY 4.0 by the author.