GitHub Actions|Self-hosted Runner Setup and Usage Guide for Efficient CI/CD
Discover step-by-step instructions to implement GitHub Actions and Self-hosted Runner, resolving CI/CD deployment challenges and accelerating your automation workflows for reliable and scalable software delivery.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
CI/CD Practical Guide (Part 2): Comprehensive Use and Setup of GitHub Actions and Self-hosted Runner
Take you through the basics of GitHub Actions/Self-hosted Runner operation and a step-by-step tutorial.
Photo by Dan Taylor
Preface
The previous article “CI/CD Practical Guide (Part 1): What is CI/CD? How to Build a Stable and Efficient Development Team with CI/CD? Tool Selection?” introduced what CI/CD is, the benefits it brings, and tool selection. This article will focus on the architecture and usage of GitHub Actions and Self-hosted Runners, and guide you step-by-step to create several interesting automated workflows to help you get started.
GitHub Actions Architecture Flowchart
Before we begin, let’s first clarify the operational framework, workflow relationships, and responsibilities of GitHub Actions.
GitHub Repo
- In the world of GitHub Actions, all Actions (Workflow YAML files) must be stored within a Git repo (
REPO/.github/workflows/
).
GitHub Repo — Actions Secrets
Repo → Settings → Secrets and variables → Actions → Secrets.
Store the Secret Key and Token used in the Actions steps
e.g. Slack Bot Token, Apple Store Connect API .p8 Key*Secrets cannot be viewed in the Action Log and will be automatically masked with * * * **
Secrets content cannot be viewed or edited, only overwritten
- Secrets currently only support plain text content and cannot upload files
- For binary data, please refer to the official steps to store using Base64 encoding.- For storing iOS development certificates, refer to the official guide: Installing an Apple certificate on macOS runners for Xcode development
- Can store organization-level Secrets, shared across Repos
GitHub Repo — Actions Variables
Repo → Settings → Secrets and variables → Actions → Variables.
Store commonly used variables in Actions steps
e.g. Simulator iOS version, working directoryVariables content can be viewed and edited
Variables content can be output in the Action Log
Variables only support plain text, but you can store JSON strings and parse them yourself for use.
You can save organization-level Variables and share them across Repos.
GitHub Actions — Trigger
The Most Important Starting Point in Github Actions — Trigger Events (Conditions)
Only GitHub Actions triggered by the specified event will run.
The complete list of events is available in the official documentation
It basically covers all event scenarios in CI/CD and automation.
However, if there are special cases without direct events, you can only use other events combined with conditional checks in the Job or use Schedule to manually check.
For example, if there is no PR Merged event, you can usepull_request: closed
+ Jobif: github.event.pull_request.merged == true
to achieve this.
Common Events:
schedule
(cron): Scheduled execution (same as crontab)
Can be used for automation: scheduled PR checks, scheduled builds, scheduled execution of automation scripts
All run uniformly on main / develop (Default Branch).pull_request:
: PR-related events
When a PR is opened, assigned, labeled, new commits are pushed, etc.issues
andissue_comment
: Issue-related events
When an Issue is opened, when there is a new comment, and so on.workflow_dispatch
: manual trigger; you can set required input fields, and GitHub Actions provides a simple form for users to fill in information.
e.g.:
workflow_call
: Triggers another Action (Workflow) to execute a task.workflow_run
: Triggers this task when another Action (Workflow) runs a job.
For more event types and configuration details, please refer to the official documentation.
GitHub Actions — Workflow
a.k.a Action
Use YAML to write .yaml files, all stored under the
REPO/.github/workflows/
directory.Use the Workflow YAML file in the main branch as the source of truth
If you can’t see the Action being developed or encounter errors in other branches, try merging back into the main branch first.The most basic unit in GitHub Actions, each Workflow represents a CI/CD or automation process.
Workflow can call other Workflows to execute tasks
(This feature can be used to separate core Workflows and called Workflows)This defines the task name, execution strategy, trigger events, task jobs, and all other action-related settings.
The current file structure does not support subdirectories.
Action Completely Free (Public and Private Repo)
GitHub Actions — Workflow — Job
Execution Units in GitHub Actions
Definition of Tasks in a Workflow
Each Workflow can have multiple Jobs
Each Job needs to specify which Runner Label to use. During execution, the corresponding Runner machine will be used to run the task.
Multiple Jobs Run Concurrently (use
needs
to enforce order if needed)Each Job should be treated as an independent execution unit (each as a Sandbox). If a Job produces resource files that need to be used by subsequent Jobs or Workflows after completion, you must Upload Artifacts or move them to a shared output directory on self-hosted runners.
Job can output strings for other Jobs to reference.
(For example, execution results like true or false)If no specific workflow conditions are set, when multiple Jobs are running and one Job encounters an error, the other Jobs will still continue to execute.
GitHub Actions — Reuse Workflow/Job
Defining
on: workflow_call
in a workflow allows it to be encapsulated and reused as a Job by other workflows.Sharing across repositories is possible within the same organization.
Therefore, CI/CD tasks shared across repositories can be placed in a shared repository for common use.
GitHub Actions — Workflow — Job — Step
Minimal Workflow in GitHub Actions
The program that actually executes the tasks in the Job
Each Job can have multiple Steps
Multiple steps are executed in order
Step can output strings for subsequent Steps to reference and use.
Steps can directly write shell script programs
You can reference gh cli and current environment variables (e.g., get PR number) to directly do what you want.If no specific flow conditions are set, the Step will stop immediately if an error occurs, and subsequent Steps will not execute.
GitHub Actions — Workflow — Job — Reuse Action Step
You can directly reuse existing workflows created by experts on Marketplace.
For example: Comment content on PR.You can also package a series of your work task steps into an Action GitHub Repo for others to reuse directly.
Public Repo Actions can be published to the Marketplace
Packaging Action supports use:
Docker Action — means GitHub Actions will pass environment variables into the Docker container, and then you can handle them however you want, such as with shell scripts, Java, PHP, etc.
JavaScript/TypeScript Action — Write GitHub Actions logic directly using node.js, with environment variables provided for your reference.
e.g. pozil/auto-assign-issueComposite (YAML) — Pure YAML describing task steps (similar to GitHub Actions — Workflow — Job — Step). You can declare which steps to perform or write shell scripts directly within it.
e.g. ZhgChgLi/ZReviewTender
Due to length constraints, this article will not cover how to package a Github Actions Action. For those interested, please refer to the official documentation: tutorials/creating-a-composite-action.
GitHub Runner
GitHub assigns jobs to Runners based on the Runner Label for execution
Runner acts only as a listener, polling and monitoring tasks dispatched by GitHub.
Only cares about the Job, not which Action (Workflow) it belongs to.
Therefore, after Action A’s Job-1 finishes, the next one will be Action B’s Job-1, instead of completing all Jobs of Action A before moving to Action B.Runner can use GitHub Hosted Runner or Self-hosted Runner.
GitHub Hosted Runner
- GitHub provides Runners, refer to the official Repo list:
- What is pre-installed on the Runner can be viewed by clicking:
e.g. macos-14-arm64
iOS development prioritizes using the -arm64 (M-series) processor Runner for faster performance
Simply paste the YAML Label from the table into the Job
run-on
to use that Runner for the task.Public Repo Pricing: Completely Free with Unlimited Use
️ Private Repo Free Tier:
Free tier (varies by account type, GitHub Free as an example):
Usage: 2,000 minutes free per month
Storage: 500 MB⚠️️Private Repo Billing Method:
After exceeding the free quota, usage-based billing starts (with adjustable limits and notifications). Prices vary depending on the operating system and cores of the machine running the Runner:
about-billing-for-github-actions
You can see that macOS is expensive because the device costs are very high.
- Maximum Concurrent Task Limit:
usage-limits-billing-and-administration
This part is too off-topic; our focus is on Self-hosted Runner.
Self-hosted Runner on In-house Server
Using Your Own Machine as a Runner
A physical machine can run multiple Runners to handle tasks concurrently
Free Unlimited Usage Without Restrictions
Only the machine purchase cost, pay once and use as much as you want!
Based on a 32G RAM M4 Mini (=NT$40,900), if you use GitHub Hosted Runner, it costs 500 USD per month; buying a machine and setting it up pays off in just over three months!Supports Windows, macOS, Linux (x64/ARM/ARM64)
The same organization can share Runners across Repos
⚠️Currently: actions/cache, actions/upload-artifact, and actions/download-artifact only support GitHub cloud services, meaning these contents are still uploaded to GitHub servers and storage usage is charged.
You can set up a shared directory on your own machine as an alternative.Self-hosted Runner also supports Docker, k8s, but I haven’t explored it.
Setting up a Self-hosted Runner takes just a few steps (ready within 10 minutes) to go online and start handling tasks (explained later in this article).
GitHub Workflow x Job x Step x Runner Process Relationship Diagram
Here is a diagram summarizing the workflow and relationships, assuming we have two Workflows and two Runners:
CI — Suppose R has 3 Jobs, each Job contains several Steps, Runner Label (run-on) —
self-hosted-app
CD — There are 4 Jobs, each with several Steps. Runner Label (run-on) —
self-hosted-app
Runner — There are 2, and both Runner Labels are —
self-hosted-app
As mentioned earlier, the Runner and Workflow dispatch and receive tasks based on the Runner Label. In this case, the same self-hosted-app
Runner Label is used. Jobs in the Workflow run concurrently by default, while Steps are the actual execution content of each Job, running sequentially in order. When all Steps are completed, the Job is considered finished.
Therefore, the actual execution timeline of the Workflow will alternate between two Runners to run Jobs, can run concurrently, and the next Job after the current one finishes is not necessarily the next Job in the same Workflow.
⚠️ This is why ensuring each Job is independent is crucial (especially in a Self-hosted Runner environment, where cleanup after a Job may not be thorough. We might subconsciously assume the output of one Job can be used by the next Job), but it should not be used this way.
If a Job produces output to be used by subsequent Jobs:
Job Output String : Output plain text to a variable for other jobs to reference
Artifact-upload/download: The last step of a Job uploads the execution results to GitHub Artifact, and the first step of another Job downloads it for further processing.
e.g. For example, Job — Packaging → upload the packaged .ipa to Artifact → Job — Deployment → download the .ipa → upload to App Store for deployment
Note: Currently, even for Self-hosted runners, the artifacts are stored on GitHub Artifact cloud storage.AWS/GCP… Cloud Storage: Same as above, but using your own cloud storage service.
[Self-hosted Only] Shared Drive, Directory: If all Self-hosted Runners mount a shared directory, you can create folders based on UUIDs in this directory to store output results. Subsequent jobs can read the previous job’s output UUID to locate and retrieve the corresponding stored data.
Note that Runners on different machines must also mount the same shared directory.Complete all tasks within the same Job’s Steps.
Learning by Doing GitHub Actions — Case Study Implementation
“Saying is not as good as doing.” The above explanation of terms and process structure might have been confusing. Next, we will directly show three functional examples, guiding you to practice hands-on while explaining encountered concepts, learning by doing to understand what GitHub Actions really is.
Case — 1
Automatically add File Changes Size Label after creating a Pull Request to help Reviewers manage their review tasks easily.
Results Image
Operation Process
User Opens PR, Reopens PR, Pushes New Commit to PR
Trigger GitHub Actions Workflow
shell script to get the number of file changes
Determine Quantity to Label PRs
Completed
Hands-on Practice
Repo → Actions → New workflow → set up a workflow yourself.
File Name: Automation-PullRequest.yml
Action Workflows can have each task in a separate file, or group them by trigger event or purpose in the same file. Multiple Jobs run concurrently. Additionally, because GitHub Actions currently does not support directory structures, having fewer files and using hierarchical file naming is easier to manage.
Here, all Actions related to PR events are placed in the same 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
# Workflow(Action) name
name: Pull Request Automation
# Actions Log title
run-name: "[Pull Request Automation] ${{ github.event.pull_request.title \|\| github.ref }}"
# Trigger events
on:
# PR event
pull_request:
# PR - opened, reopened, or new Push Commit
types: [opened, synchronize, reopened]
# Cancel running jobs in the same concurrency group if a new job starts
# For example, if a new Push Commit triggers a job before the previous one runs, the previous job is canceled
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
# Job tasks
# Jobs run concurrently
jobs:
# Job ID
label-pr-by-file-count:
# Job name (optional, better for logs)
name: Label PR by changes file count
# If this job fails, it won't affect the whole workflow; other jobs continue
continue-on-error: true
# Set max timeout to avoid endless waiting in abnormal cases
timeout-minutes: 10
# Runner Label - use GitHub Hosted Runner ubuntu-latest to run the job
# For Private Repos, usage is counted and may incur costs
runs-on: ubuntu-latest
# Job steps
# Steps run sequentially
steps:
# Step name
- name: Get changed file count and apply label
# Step ID (optional, only needed if outputs are referenced later)
id: get-changed-files-count-by-gh
# Inject external environment variables into runtime
env:
# secrets.GITHUB_TOKEN is automatically generated by GitHub Actions, no need to set manually, has some 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) requires GH_TOKEN in ENV for permission to operate
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Shell script
# GitHub Hosted Runner has gh CLI preinstalled, no need to install before use
run: \|
# ${{ github.xxx }} is a GitHub Actions Context expression
# Not a shell variable, replaced by GitHub Actions during YAML parsing
# More info: https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
# Get PR number:
PR_NUMBER=${{ github.event.pull_request.number }}
# Get Repo:
REPO=${{ github.repository }}
# Use GitHub API (gh CLI) to get number of changed files
FILE_COUNT=$(gh pr view $PR_NUMBER --repo $REPO --json files --jq '.files \| length')
# Print log
echo "Changed file count: $FILE_COUNT"
# Label logic
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
# Use GitHub API (gh CLI) to remove current Size labels
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
# (Optional) Create label if it doesn't exist
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
# Use GitHub API (gh CLI) to add the label
gh pr edit $PR_NUMBER --repo $REPO --add-label "$LABEL"
After committing files to the repo’s main branch, opening a new PR will automatically trigger GitHub Actions:
Action execution status showing Queued means the task is waiting for the Runner to pick it up and execute.
Execution Result
After execution is complete and successful, the PR will automatically be labeled accordingly! The record will show it was labeled by github-actions
.
Full code: Automation-PullRequest.yml
Use Prebuilt Action Steps: pascalgn/size-label-action
Earlier, it was mentioned that you can directly use pre-built Actions created by others. The task of marking the PR Size Label already has ready-made solutions available. The above example is only for teaching purposes; in practice, you don’t need to reinvent the wheel.
You can complete the task directly by using it in the 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
name: Pull Request Automation
# Trigger Event
on:
# PR event
pull_request:
# PR - opened, reopened, or new push commit
types: [opened, synchronize, reopened]
# Cancel running jobs in the same Concurrency Group if a new job starts
# For example, if a push commit triggers a job and another push commit happens before it runs, the previous job will be canceled
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
# Job tasks
# Jobs run concurrently
jobs:
# Job ID
label-pr-by-file-count:
# Job name (optional, better readability in logs)
name: Label PR by changes file count
# If this job fails, do not affect the entire workflow, continue with other jobs
continue-on-error: true
# Set maximum timeout to prevent endless waiting in case of issues
timeout-minutes: 10
# Runner Label - use GitHub Hosted Runner ubuntu-latest to run the job
# For private repos, usage counts and may incur costs
runs-on: ubuntu-latest
# Job steps
# Steps run sequentially
steps:
# Step name
- name: Get changed file count and apply label
# Step ID (optional, only needed if outputs are referenced later)
id: get-changed-files-count-by-gh
# Use a pre-built action from others
uses: "pascalgn/size-label-action@v0.5.5"
# Inject external environment variables into runtime
# For parameter names and usage, see: https://github.com/pascalgn/size-label-action/tree/main
env:
# secrets.GITHUB_TOKEN is automatically generated by GitHub Actions (github-actions identity), no need to set manually in Secrets, has some 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 }}"
This packaged Action is a JavaScript Action. You can refer to the actual execution code in the following file: dist/index.js.
Supplement on name and run-name:
name: Name of the Action Workflow
run-name: Title of the execution record (can include PR Title, Branch, Author, etc.)
If the event is on:pull_request, the default is the PR Title.
Case — 2
Automatically assign the author and add a comment prompt if there is no Assignee after creating a Pull Request. (This only runs on the initial creation)
Result Image
Workflow
User opens a PR
Trigger GitHub Actions Workflow
GitHub script to get assignee
If there is no assignee, assign the PR author & comment a message
Completed
Hands-On Practice
Repo → Actions → New workflow → set up a workflow yourself.
File Name: Automation-PullRequest.yml
(same as above)
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
name: Pull Request Automation
# Actions Log Title
run-name: "Pull Request Automation - Daily Checker"
# Trigger Events
on:
# PR Event
pull_request:
# PR - opened, reopened, or new push commit
types: [opened, synchronize, reopened]
# Cancel running jobs in the same concurrency group if a new job starts
# For example, if a push commit triggers a job that hasn't started yet and a new push commit happens, the previous job is canceled
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
# Job Tasks
# Jobs run concurrently
jobs:
# Job ID
label-pr-by-file-count:
# Please refer to previous text, omitted....
# ---------
assign-self-if-no-assignee:
name: Automatically assign to self if no assignee is specified
# Since the trigger event is shared, this job checks if the PR is opened (first creation) before running; otherwise, it will be skipped
if: github.event_name == 'pull_request' && github.event.action == 'opened'
# If this job fails, it won't affect the whole workflow; other jobs continue
continue-on-error: true
# Set maximum timeout to prevent endless waiting in abnormal situations
timeout-minutes: 10
# Runner Label - use GitHub Hosted Runner ubuntu-latest to run the job
# For private repos, usage is counted and may incur charges
runs-on: ubuntu-latest
steps:
- name: Assign self if No Assignee
# Use GitHub Script (JavaScript) to write the script (Node.js environment)
# Compared to shell script above, this is easier and cleaner
# No need to manually inject environment variables or GITHUB_TOKEN
uses: actions/github-script@v7
with:
script: \|
// github-script automatically injects the context variable for direct JavaScript use
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
const issue = context.payload.pull_request; // To support issues too, use context.payload.issue \|\| context.payload.pull_request
const assignees = issue.assignees \|\| [];
const me = context.actor;
if (assignees.length === 0) {
// Assign self as assignee
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: [me]
});
// Leave comment notification
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}).`
});
}
This time, we demonstrate using GitHub Script (JavaScript) to write the script, offering more flexibility and easier coding.
Of course, if you want to follow what I said before about one file per task, you can remove the Job If. . and directly set the trigger conditions in the 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
name: Pull Request Automation - Auto Assignee Self
# Trigger Event
on:
# PR Event
pull_request:
# PR - When opened
types: [opened]
jobs:
assign-self-if-no-assignee:
name: Automatically assign to self if no assignee is specified
runs-on: ubuntu-latest
steps:
# Please refer to the previous text, omitted....
After committing files to the repo’s main branch, opening a new PR will automatically trigger GitHub Actions:
There are now two jobs to be executed!
Execution Results
After execution and success, if the PR has no Assignees, it will automatically assign the PR author and comment a message. (All actions are performed using the github-actions
identity)
Full Code: Automation-PullRequest.yml
Testing Reopened PR
You can see that only the Size Label Job was executed, while the Auto Assignee Job was skipped.
This task also has a ready-made Action that can be reused, see: pozil/auto-assign-issue.
Case — 3
Automatically count the current number of PRs and how long they have been open every day at 9 AM, then send a notification message to the Slack workgroup. Automatically close PRs that have been open for more than 3 months.
Result Image
Slack workgroup automatically receives reports every morning
Automatically close PRs older than 90 days
Workflow
GitHub Actions Automatically Triggered Every Day at 9 AM
Trigger GitHub Actions Workflow
GitHub script to get the list of open PRs and count how many days they have been open
Send Statistical Report Messages to Slack
Close PRs Older Than 90 Days
Completed
Hands-On Practice
Repo → Actions → New workflow → set up a workflow yourself.
File Name: 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
name: Pull Request Automation - Daily Checker
# Trigger Events
on:
# Scheduled automatic execution
# https://crontab.guru/
# UTC time
schedule:
# 01:00 UTC = 09:00 UTC+8 daily
- cron: '0 1 * * *'
# Manual trigger
workflow_dispatch:
# Job Items
# Jobs run concurrently
jobs:
# Job ID
caculate-pr-status:
# Job Name (optional, improves log readability)
name: Calculate PR Status
# Runner Label - Use GitHub Hosted Runner ubuntu-latest to run the job
# For private repos, usage counts and may incur costs
runs-on: ubuntu-latest
# Job Output
outputs:
pr_list: ${{ steps.pr-info.outputs.pr_list }}
# Job Steps
# Steps run sequentially
steps:
# Step Name
- name: Fetch open PRs and calculate
# To expose Step Output, set id
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
};
});
// Set output, only accepts String
core.setOutput('pr_list', JSON.stringify(result));
# ----
send-pr-summary-message-to-slack:
name: Send PR Summary Message to Slack
# Jobs run concurrently by default; using needs forces this job to wait for the specified job to complete
needs: [caculate-pr-status]
runs-on: ubuntu-latest
steps:
- name: Generate Message
# To expose Step Output, set id
id: gen-msg
uses: actions/github-script@v7
with:
script: \|
const prList = JSON.parse(`${{ needs.caculate-pr-status.outputs.pr_list }}`);
const blocks = [];
// Title
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `📬 *Open PR Report*\nTotal: *${prList.length}* PR(s)`
}
});
// One line per 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)`
}
});
}
// Set output, only accepts String
core.setOutput('blocks', JSON.stringify(blocks));
# Use Slack's official Slack API GitHub Actions
# https://tools.slack.dev/slack-github-action/sending-techniques/sending-data-slack-api-method/
# Send message
- 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)`);
In this example, we use:
on: schedule Crontab schedule for automatic trigger and workflow_dispatch for manual trigger support
Job output/Step output (must be strings only)
Multiple jobs run concurrently by default but can use
needs
to set time-dependent dependencies.Get Settings from Repo Secrets/Variables
Connecting to Slack API
Repo Secrets — Add SLACK_BOT_TOKEN
- For setting up a Slack App and configuring message permissions, you can refer to my previous article.
Repo Variables — Add SLACK_TEAM_CHANNEL_ID
After committing files to the Repo main branch, go back to Actions and manually trigger it to check:
It will be automatically triggered daily in the future.
Actions → Pull Request Automation — Daily Checker → Run workflow → Branch: main → Run workflow。
After execution, you can click to view the execution status:
Due to the needs
constraint, the Jobs workflow will have Calculate PR Status
complete first, then Auto Close Old PRs
and Send PR Summary Message to Slack
will run concurrently.
Execution Result
After all tasks are successfully executed, you can check the Slack messages:
Success 🚀🚀🚀
Full code: Automation-PullRequest-Daily.yml
Summary
I hope the above three examples give you a basic understanding of GitHub Actions and inspire your creativity in automation. You can design your own workflows (be sure to first check the trigger events) and then write scripts to execute them. Also, remember to visit the Marketplace to find existing actions you can use to stand on the shoulders of giants.
This article is just an introduction (without even checking out the code). The next article, CI/CD Practical Guide (Part 3): Implementing CI and CD Workflows for App Projects Using GitHub Actions, will cover more complex and advanced GitHub Actions workflows.
GitHub Automation Extended Topics
GitHub can integrate with Slack to subscribe to Repo PR update notifications, Push Default Branch notifications, and more.
Issue 1. GitHub messages cannot tag people, only GitHub accounts are tagged, Slack accounts do not receive notifications:
Slack App or search for GitHub in Apps → open the message window → complete the Connect GitHub Account step. This lets GitHub know your corresponding Slack UID so it can mention you.
Question 2. Pull Request Reminder
I remember this was originally a third-party feature, but later it was directly integrated into GitHub. Don’t foolishly write scripts yourself using GitHub Actions!
- After joining the Slack Channel and saving the rules, daily reminder messages will be automatically sent with the Reviewer tagged. Setup instructions can be found here. The result is shown in the image below:
https://michaelheap.com/github-scheduled-reminders/
Issue 4. GitHub PR message, author does not receive notification:
This is a perennial issue: the PR Slack message does not actually notify the author when someone replies in Threads. The Slack message sent by GitHub only mentions Reviewers, not the Assignee or PR author.
At this point, GitHub Actions as an automation tool comes in handy. We can add a Job to append the author tag at the end of the PR Description, so the message sent to Slack will also tag the author’s 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
name: Pull Request Automation
# Please refer to the previous text, omitted....
# ---------
append-author-to-pr-description:
name: Append author to PR description
# Since this is a shared trigger event, the Job itself checks and runs only when the Pull Request is opened (initial creation); otherwise, it will be skipped
if: github.event_name == 'pull_request' && github.event.action == 'opened'
# If this Job fails, it won't affect the entire Workflow; other Jobs will continue
continue-on-error: true
# Set maximum timeout to prevent endless waiting in abnormal situations
timeout-minutes: 10
# Runner Label - use GitHub Hosted Runner ubuntu-latest to run the job
# For Private Repos, usage counts and may incur costs if exceeded
# But such small automation jobs rarely exceed limits
runs-on: ubuntu-latest
steps:
- name: Append author to PR description
env:
# secrets.GITHUB_TOKEN is automatically generated by GitHub Actions during execution; no need to set manually in Secrets, it has some GitHub Repo API scopes
# gh (GitHub) CLI requires GH_TOKEN injected into ENV to have permission to operate
# 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 }} is a GitHub Actions Context expression
# Not a shell variable, but replaced by GitHub Actions during YAML parsing with corresponding values
# Other parameters: 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"
Full Code: Automation-PullRequest.yml
When opening a PR, the author information is automatically added after the description, and Slack messages correctly tag the author:
Issue 5. GitHub sending excessive notification emails is very disruptive:
After setting up notifications for both the GitHub Repo and Slack, all notifications are received in Slack. You can go to your GitHub personal settings to turn off email notifications:
Others
Actions currently do not have a directory structure, but you can pin up to five Actions on the Actions page. You can also disable an Action to pause it.
You can view GitHub Actions usage and performance in Insights:
Reuse Workflow Supplement
Next article uses the same repo to split Reuse Workflow. Here is an example of splitting Reuse Workflow across different repos.
First, define a Reuse Workflow: Automation-label-pr-base-branch.yml: in the 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
# Trigger event
on:
# Triggered by other workflows calling this workflow
workflow_call:
# Data input
inputs:
# PR Number
PR_NUMBER:
required: true
type: string
# Secret input
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"
# Get PR's base branch
BASE_BRANCH=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json baseRefName --jq '.baseRefName')
echo "🔖 PR Base Branch: $BASE_BRANCH"
# Allowed base branch labels
BRANCH_LABELS=("develop" "main" "master")
# Check if label is in allowed list
if [[ " ${BRANCH_LABELS[@]} " =~ " ${BASE_BRANCH} " ]]; then
LABEL="$BASE_BRANCH"
else
echo "⚠️ Base branch '$BASE_BRANCH' not in allowed list, skipping label."
exit 0
fi
# Remove existing 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
# Create label if it doesn't exist
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
# Add base branch label
echo "🏷️ Adding label '$LABEL' to PR #$PR_NUMBER"
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "$LABEL"
Back to our main ZhgChgLi/github-actions-ci-cd-demo repo, add a main 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
# ✅ If the called workflow is in the same repo:
# uses: ./.github/workflows/Automation-label-pr-base-branch.yml
# Note: This method cannot specify the branch (always uses the caller workflow's branch)
# Reference examples:
# - CD-Deploy-Form.yml calls workflow in this repo
# - CD-Deploy.yml calls workflow across repos
#
# ✅ If the called workflow is in another repo:
# uses: {owner}/{repo}/.github/workflows/{file}.yml@{branch_or_tag}
# You can specify branch or 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 }}
# If all secrets should be inherited from the caller workflow, use `inherit`
# secrets: inherit
#
# If only specific secrets are needed, specify individually
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Try opening a PR after committing the file.
Workflow: The main repo’s Demo-Reuse-Action-From-Another-Repo.yml → calls and passes parameters to another repo’s Automation-label-pr-base-branch.yml to execute tasks → returns to the main repo to report results.
Success!
Private Repo Access Issues
Here is a security issue to note: Public Repos are completely fine, but if Reuse Workflow is in a Private Repo, it can only be accessed and used if other Repos are in the same organization + those Repos are also Private + permissions are granted.
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
= ✅
Set allowed methods by going to Reuse Workflow Repo → Settings → Actions → General → Access:
Change selection:
1
2
Accessible from repositories in the 'ZhgChgLi' organization
Workflows in other repositories within the 'ZhgChgLi' organization can access the actions and reusable workflows in this repository. Access is allowed only from private repositories.
Click “Save” to save and complete.
If you don’t have access permission, the following error message will appear:
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 Syntax Supplement
Adding some clarification on the ${{ XXX }}
or ${XXX}
or $XXX
variable syntax that everyone might find confusing.
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 expression, explained below
FROM_REF: "${{ github.ref }}"
run: \|
# Shell Script:
# Referencing custom injected ENV variable values
# ${SAY_MY_NAME} or $SAY_MY_NAME both work
# ${XX} is better for string concatenation:
echo "HI: ${SAY_MY_NAME}"
# 💡 GitHub Actions Context expression
# This is not a shell variable, but replaced by GitHub Actions during YAML parsing
# ⚠ Will fail if run locally or outside GitHub Actions environment
# 🔗 https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
BRANCH_NAME_FROM_CONTEXT="${{ github.ref }}"
# 💡 GitHub Actions runtime environment variables
# These variables are automatically injected by GitHub Actions when the runner executes the shell
# ✅ Can be pre-defined in other environments with export or 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 expression, same as above
FROM_REF: "${{ github.ref }}"
with:
script: \|
// GitHub Script: (JavaScript (Node.js)):
// Get ENV values from process.env
console.log(`HI: ${process.env.SAY_MY_NAME}`);
console.log(`FROM_REF: ${process.env.FROM_REF}`);
// github-script automatically injects context variables for direct JavaScript access
// 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}`);
// Can also use GitHub Actions Context expression (though less meaningful):
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}`);
}
// The github object in github-script is an Octokit REST API instance
// Used for interacting with the GitHub API
// For example:
// 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 Expression
${{ XXX }}
is a GitHub Actions Context expression, replaced by GitHub Actions with the corresponding value during the YAML parsing phase (as shown above withBRANCH_NAME_FROM_CONTEXT
)The complete usable expressions can be referenced in the official documentation
${XXX}
or$XXX
are real environment variables.${XXX}
is more convenient for string concatenation.GitHub Actions injects some default environment variables. For the full list, please refer to the official documentation
actions/github-script@v7
In
github-script
, you can also use${{ XXX }}
, which is a GitHub Actions Context expression, but it is not very meaningful; because in the github-script environment, context variables are injected by default and can be directly referenced in JavaScript.In
github-script
, thegithub
object is an instance of the Octokit REST API.github-script
can also inject variables fromenv:
, but you need to access them using${process.env.xxx}
.The same environment variables are injected by default and can be fully referenced in the official documentation. Access them via
${process.env.xxx}
; however, since there is already acontext
variable, accessing them this way is not very meaningful.
github-script
automatically includes thegithub-token
, no need to specify it.
gh cli GH_TOKEN
Using
gh cli
in GitHub Actions Shell Script requires specifying theGH_TOKEN
parameterYou can directly use the default token —
secrets.GITHUB_TOKEN
(automatically generated during GitHub Actions execution)The default token has basic permissions. If higher permissions are needed, use a Personal Access Token. For team use, it is recommended to create a PAT with a shared account.
Full code: Demo-Env-Vars- .yml
GitHub Actions has been developed. The next step is to replace the GitHub Hosted Runner with your own Self-hosted Runner.
GitHub Hosted Runner offers a free quota of 2,000 minutes per month (starting point). Running these small automation tasks takes little time, and since it runs on Linux machines, the cost is very low, often not even reaching the free limit; it’s not necessary to switch to a Self-hosted Runner. Changing the Runner also requires ensuring the Runner environment is set up correctly (for example, GitHub Hosted Runner comes with the gh CLI pre-installed, but with a self-hosted Runner, you need to install it yourself). This article only switches Runners for teaching purposes.
Self-hosted Runner is only required when running CI/CD tasks.
Add Self-hosted Runner
This article uses macOS M1 as an example.
Settings → Actions → Runners → New self-hosted runner.
Runner image: macOS
Architecture : For M1, remember to select ARM64 for faster execution
Open a Terminal on a physical computer.
Complete on your local computer following the Download steps:
1
2
3
4
5
6
7
8
9
10
# Create the Runner directory in your desired path
mkdir actions-runner && cd actions-runner
# Download the 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
# Extract the archive
tar xzf ./actions-runner-osx-x64-2.325.0.tar.gz
The above is for reference only; it is recommended to follow the steps on your setup page to get the latest version of the Runner image.
設定 Configure:
1
2
# Please refer to the settings page command, the Token changes over time
./config.sh --url https://github.com/ORG/REPO --token XXX
You will be asked to enter in sequence:
Enter the name of the runner group to add this runner to: [press Enter for Default] Press Enter directly
Only runners registered at the Organization level have the Group grouping featureEnter the name of runner: [press Enter for ZhgChgLideMacBook-Pro] You can enter the desired Runner name e.g.
app-runner-1
or just press EnterThis runner will have the following labels: ‘self-hosted’, ‘macOS’, ‘X64’
Enter any additional labels (ex. label-1,label-2): [press Enter to skip]
Enter the Runner labels you want to set. You can add multiple custom labels for easier use later.
As mentioned before, GitHub Actions/Runner finds tasks based on the corresponding labels. If you only use the default labels, the runner might pick up jobs intended for other runners in the organization. Creating a custom label is the safest option.
Here, I randomly set a labelself-hosted-zhgchgli
Enter name of work folder: [press Enter for _work] Press Enter directly
When √ Settings Saved. appears, it means the settings are complete.
Start Runner:
1
./run.sh
When you see √ Connected to GitHub and Listening for Jobs, it means the Actions tasks are being monitored:
This Terminal window will keep receiving tasks as long as it remains open.
🚀🚀🚀You can start multiple Runners by opening multiple Terminals in different directories on the same computer.
Back on the Repo settings page, you can also see the Runner waiting for tasks:
Status:
Idle: inactive, waiting for tasks
Active: A task is currently running
Offline: Runner is not online
Workflow (GitHub Actions) Runner Switch to Self-hosted Runner
Taking Automation-PullRequest.yml
as an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Please refer to the previous text, omitted....
jobs:
label-pr-by-file-count:
# Please refer to the previous text, omitted....
runs-on: [self-hosted-zhgchgli]
# Please refer to the previous text, omitted....
# ---------
assign-self-if-no-assignee:
# Please refer to the previous text, omitted....
runs-on: [self-hosted-zhgchgli]
steps:
# Please refer to the previous text, omitted....
After committing files to the Repo’s main branch, reopen a PR to trigger and verify the Actions.
Back in the Runner Terminal, you can see new tasks coming in, along with their execution and results:
Failed because my local computer does not have the gh cli environment installed:
After installing gh on a physical computer using brew install gh
, run it again:
Success! Now this task runs entirely on our own computer, without using GitHub Hosted Runner or incurring any charges.
We can click into the Action Log to see which Runner and machine the task is running on:
runs-on: [ Runner Label] Setting
This is AND, not OR. GitHub Runner currently does not support OR when selecting Runners to execute.
For example: [self-hosted, macOS, app]
→ means the Runner must have all three labels self-hosted, macOS, app
to match and execute the task.
If a job wants to test results across different runner environments simultaneously, you can use the matrix
parameter:
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 }}"
This Job will run concurrently once in each of the following two Runner Labels Runners:
self-hosted, linux, high-memory
self-hosted, macos, xcode-15
Runner is currently not supported:
- OR Select Runner
- Runner Weight Settings
Register Runner as a Service
You can refer to the official document “Configuring the self-hosted runner application as a service” to register the Runner directly as a system service. This allows it to run in the background (no need to keep the Terminal open in the foreground) and start automatically after boot.
If you have multiple runners, remember to adjust the “Customizing the self-hosted runner service” settings to register different names.
iOS side, I have an issue to investigate and exclude: after switching to background Service, I encounter errors during Archive (suspected to be related to keychain permissions). I didn’t have time to fix it then, so I temporarily used the foreground Terminal Runner.
If you want a traditional app to start automatically on boot, you need to add a launch agent file in ~/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>
<!-- Specify Terminal.app to launch -->
<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>
For those interested in diving deeper into DevOps, please refer to the official k8s Runner documentation.
Complete Project Repo
Official Documentation
For more detailed setup instructions, please refer to the official manual.
AI Can Help!
Tested: Provide ChatGPT with complete steps and timing, and it can write GitHub Actions for you to use directly!
Summary
You should now have a basic understanding of GitHub Actions + Self-hosted Runner. In the next article, I will start using an App (iOS) CI/CD as a case study to guide you through building the entire process step by step.
Series Articles:
Buy me a coffee
If you have any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.