使用 GitHub App 註冊 Self-Hosted Runner
本文說明如何建立 GitHub App,並以 App 憑證(而非 Personal Access Token)向 GitHub API 取得 Runner Registration Token,進而完成 self-hosted runner 的自動化註冊。
:::info 官方參考文件
- GitHub Docs — Creating a GitHub App
- GitHub Docs — Authenticating as a GitHub App installation
- GitHub Docs — About self-hosted runners
- GitHub Docs — Adding self-hosted runners :::
為什麼用 GitHub App 而非 PAT?
| 比較項目 | Personal Access Token (PAT) | GitHub App |
|---|---|---|
| 授權範圍 | 綁定個人帳號 | 綁定 App,每次安裝獨立授權 |
| 多 Org 部署 | 須個別建立 PAT | 同一個 App 可被多個 Org 安裝(需設為 Public) |
| Token 有效期 | 最長 1 年(fine-grained) | Installation Token 1 小時,可自動刷新 |
| 稽核追蹤 | 以個人身份記錄 | 以 App 名稱記錄,責任歸屬清晰 |
| 適合 CI/CD | 較不建議用於自動化 | 官方推薦 |
:::note 關於跨 Org 安裝 建立 App 時的 Where can this GitHub App be installed? 設定決定可安裝範圍:
- Only on this account(Private):僅 App owner 帳號可安裝
- Any account(Public):任何 user / org 皆可安裝,每次安裝會產生獨立的 Installation ID
若僅內部使用,建議選 Private 以降低風險。 :::
步驟一:建立 GitHub App
-
前往 GitHub → Settings → Developer settings → GitHub Apps → New GitHub App (或 Organization 層級:Org Settings → Developer settings → GitHub Apps)
-
填寫基本資訊:
- GitHub App name:任意,例如
my-runner-app - Homepage URL:任意有效 URL,例如公司網站
- Webhook:取消勾選 Active(runner 不需要 webhook)
- GitHub App name:任意,例如
-
設定 Permissions:
分類 權限 值 Repository Actions Read & write Organization Self-hosted runners Read & write 若 runner 僅用於單一 repository,也可選擇 Repository 層級的 Self-hosted runners 權限即可。
-
Where can this GitHub App be installed? 選擇
Only on this account或Any account,視需求而定。 -
點擊 Create GitHub App。
步驟二:認識 App 設定頁面的三個識別碼
App 建立後,設定頁面頂端會顯示三個識別碼,用途各不同:
| 欄位 | 格式 | 用途 |
|---|---|---|
| App ID | 純數字,例如 1234567 | 簽署 JWT 的 iss 欄位(傳統方式) |
| Client ID | 字串,例如 Iv1.abc123def456 | 簽署 JWT 的 iss 欄位(新方式,可替代 App ID) |
| Client Secret | 隨機字串 | 僅用於 OAuth 使用者授權流程,取得 Installation Token 不需要此值 |
:::tip App ID vs Client ID GitHub 目前在設定頁面會提示:
"Using your App ID to get installation tokens? You can now use your Client ID instead."
兩者皆可作為 JWT 的 iss 值,Client ID 是較新的推薦做法。
本文範例使用 APP_ID 變數,實際填入 App ID 或 Client ID 均可。
:::
步驟三:產生 Private Key
在 App 設定頁面:
- 往下捲動至 Private keys
- 點擊 Generate a private key
- 下載
.pem檔案並妥善保管(此檔案只會顯示一次)
步驟四:安裝 App 到 Organization 或 Repository
- 在 App 設定頁面點擊 Install App
- 選擇目標 Organization 或 Account
- 選擇要授權的 Repositories(All repositories 或指定 repo)
- 確認安裝後,從瀏覽器 URL 取得 Installation ID:
https://github.com/organizations/<org>/settings/installations/<installation_id>
步驟五:取得 Installation Access Token
GitHub App 的認證流程分兩步:
App Private Key → 簽署 JWT → 呼叫 API 取得 Installation Token
所需資訊
| 變數 | 來源 | 備註 |
|---|---|---|
APP_ID | App ID(數字)或 Client ID(字串)均可 | 填入其中一個即可 |
INSTALLATION_ID | 安裝後 URL 中的數字 | 見步驟四 |
PRIVATE_KEY | 下載的 .pem 檔案內容 | 機密,必須存入 Secret |
:::warning 敏感資訊處理
APP_ID/Client ID和INSTALLATION_ID本身非機密- Private Key(.pem)絕對不可洩漏,請存入 GitHub Actions Secret 或 Kubernetes Secret,切勿提交進 repo
- Client Secret 與此流程無關,請勿混淆 :::
Shell 腳本範例(使用 curl + openssl)
#!/usr/bin/env bash
set -euo pipefail
APP_ID="${GITHUB_APP_ID}"
INSTALLATION_ID="${GITHUB_APP_INSTALLATION_ID}"
PRIVATE_KEY="${GITHUB_APP_PRIVATE_KEY}" # PEM 內容(含 header/footer)
# 1. 產生 JWT(有效期 10 分鐘)
NOW=$(date +%s)
IAT=$((NOW - 60))
EXP=$((NOW + 600))
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
PAYLOAD=$(echo -n "{\"iat\":${IAT},\"exp\":${EXP},\"iss\":\"${APP_ID}\"}" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
UNSIGNED="${HEADER}.${PAYLOAD}"
SIG=$(echo -n "${UNSIGNED}" | openssl dgst -sha256 -sign <(echo "${PRIVATE_KEY}") | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
JWT="${UNSIGNED}.${SIG}"
# 2. 用 JWT 取得 Installation Access Token
INSTALLATION_TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer ${JWT}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens" \
| jq -r '.token')
echo "Installation Token: ${INSTALLATION_TOKEN}"
Python 範例(使用 PyJWT + requests)
import time, jwt, requests
APP_ID = "YOUR_APP_ID"
INSTALLATION_ID = "YOUR_INSTALLATION_ID"
PRIVATE_KEY = open("my-app.private-key.pem").read() # 實務上從環境變數或 Secret 讀取
# 1. 產生 JWT
payload = {
"iat": int(time.time()) - 60,
"exp": int(time.time()) + 600,
"iss": APP_ID,
}
token = jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
# 2. 取得 Installation Access Token
resp = requests.post(
f"https://api.github.com/app/installations/{INSTALLATION_ID}/access_tokens",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
},
)
resp.raise_for_status()
installation_token = resp.json()["token"]
print(f"Token: {installation_token}")
步驟六:取得 Runner Registration Token 並啟動 Runner
取得 Installation Access Token 後,再呼叫 Runner API:
# 取得 Repository 層級的 Registration Token
REG_TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer ${INSTALLATION_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/<owner>/<repo>/actions/runners/registration-token" \
| jq -r '.token')
# 或 Organization 層級
REG_TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer ${INSTALLATION_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/orgs/<org>/actions/runners/registration-token" \
| jq -r '.token')
# 執行 Runner 設定腳本
./config.sh \
--url https://github.com/<owner>/<repo> \
--token "${REG_TOKEN}" \
--name "my-runner" \
--labels "self-hosted,linux,x64" \
--unattended \
--replace
# 啟動 Runner
./run.sh
在 GitHub Actions Workflow 中使用機密
將以下三個值存入 Repository / Organization Secrets:
| Secret 名稱 | 對應值 |
|---|---|
GH_APP_ID | App 數字 ID |
GH_APP_INSTALLATION_ID | Installation 數字 ID |
GH_APP_PRIVATE_KEY | .pem 檔案的完整內容 |
Workflow 範例:
name: Register Runner
on:
workflow_dispatch:
jobs:
register:
runs-on: ubuntu-latest
steps:
- name: Get Runner Registration Token
env:
GITHUB_APP_ID: ${{ secrets.GH_APP_ID }}
GITHUB_APP_INSTALLATION_ID: ${{ secrets.GH_APP_INSTALLATION_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
run: |
# 此處執行上方 Shell 腳本取得 INSTALLATION_TOKEN
# 再呼叫 registration-token API
echo "Registration token obtained"
Kubernetes 環境下的整合範例
若 runner 跑在 K8s,可將三個值存入 Secret:
kubectl create secret generic github-app-credentials \
--from-literal=app-id='YOUR_APP_ID' \
--from-literal=installation-id='YOUR_INSTALLATION_ID' \
--from-file=private-key=my-app.private-key.pem \
-n your-namespace
Pod 中透過環境變數讀取:
env:
- name: GITHUB_APP_ID
valueFrom:
secretKeyRef:
name: github-app-credentials
key: app-id
- name: GITHUB_APP_INSTALLATION_ID
valueFrom:
secretKeyRef:
name: github-app-credentials
key: installation-id
- name: GITHUB_APP_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: github-app-credentials
key: private-key
常見問題
JWT 簽署錯誤
確認 Private Key 的格式正確,-----BEGIN RSA PRIVATE KEY----- 開頭與結尾需完整,換行不可遺失。
Installation Token 取得 401
- 確認 App 已安裝到正確的 Organization 或 Repository
- 確認
INSTALLATION_ID是安裝 ID,不是 App ID
Runner 啟動後顯示 Offline
Runner 成功啟動後需要保持 run.sh 行程持續運行,若作為 Daemon 使用請以 ./svc.sh install && ./svc.sh start 安裝成系統服務。