Skip to content

Commit 8b4567c

Browse files
committed
chore: auto registry runner to github
1 parent b1e582b commit 8b4567c

File tree

10 files changed

+610
-81
lines changed

10 files changed

+610
-81
lines changed

Dockerfile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
# 多阶段构建:编译 + 最小运行时
2-
FROM golang:1.26-alpine AS builder
1+
# 多阶段构建:编译 + Ubuntu 运行时(避免 Alpine 导致 GitHub Runner 运行异常)
2+
FROM golang:1.26-bookworm AS builder
33
WORKDIR /app
44
COPY go.mod go.sum ./
55
RUN go mod download
66
COPY . .
77
ARG VERSION=dev
88
RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${VERSION}" -o runner-manager .
99

10-
FROM alpine:3.19
11-
RUN apk --no-cache add ca-certificates curl
10+
FROM ubuntu:24.04
11+
RUN apt-get update && apt-get install -y --no-install-recommends \
12+
ca-certificates \
13+
curl \
14+
&& rm -rf /var/lib/apt/lists/*
1215
WORKDIR /app
1316
COPY --from=builder /app/runner-manager .
1417
COPY --from=builder /app/config.yaml ./config.yaml

docker-compose.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# 基于 docs/docker.md 中「方式二:DinD」编排
2+
# 使用前请确保 config.yaml 中 runners.base_path 为 /app/runners
3+
4+
services:
5+
runner-dind:
6+
image: docker:dind
7+
container_name: runner-dind
8+
privileged: true
9+
environment:
10+
DOCKER_TLS_CERTDIR: ""
11+
# 显式关闭 TLS,仅在内网 runner-net 使用,消除弃用告警
12+
command: ["--tls=false"]
13+
networks:
14+
- runner-net
15+
restart: unless-stopped
16+
17+
runner-manager:
18+
image: ghcr.io/soulteary/runner-fleet:main
19+
container_name: runner-manager
20+
depends_on:
21+
- runner-dind
22+
environment:
23+
DOCKER_HOST: tcp://runner-dind:2375
24+
ports:
25+
- "8080:8080"
26+
volumes:
27+
- ./config.yaml:/app/config.yaml
28+
- ./runners:/app/runners
29+
networks:
30+
- runner-net
31+
restart: unless-stopped
32+
33+
networks:
34+
runner-net:
35+
driver: bridge

docs/adding-runner.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@
1111

1212
- **名称**:唯一,将对应 `runners/<名称>/` 目录。
1313
- **目标类型**:组织(org)或仓库(repo)。
14-
- **目标**:组织名或 `owner/repo`
14+
- **目标**:组织名或 `owner/repo`格式会做校验:org 时不能含 `/`;repo 时必须为 `owner/repo`(恰好一个斜杠,且两端非空)。
1515
- **Token**(可选):粘贴上一步的 Token,提交时可选择自动执行注册。注册成功后会**自动启动** Runner 程序,无需再手动点击「启动」。
1616

1717
若与已有 Runner 同名,会提示不可重复。
1818

19+
### 从 GitHub 命令一键填充
20+
21+
在 GitHub 的「New self-hosted runner」页面会看到类似命令:
22+
23+
```bash
24+
./config.sh --url https://github.com/owner/repo --token YOUR_TOKEN
25+
```
26+
27+
在管理界面「快速添加 Runner」上方的**从 GitHub 复制命令解析**框中粘贴整行命令,点击「解析并填充」,会自动识别并填入**目标类型****目标****注册 Token**,并建议**名称**(仓库名或组织名),无需手动拆分 URL 与 Token。
28+
29+
**说明**:自动注册(填写 Token 并提交)时,服务会向 **GitHub.com** 发起注册;若你使用的是 GitHub Enterprise,请勿在界面使用「自动注册」,需在对应 runner 目录下手动执行 `config.sh --url <你的 Enterprise URL> --token <TOKEN>`
30+
1931
在界面中**编辑**已有 Runner 的配置(目标、标签等)并保存后,若该 Runner 已注册且当前未在运行,也会**自动启动**,无需再手动点击「启动」。
2032

2133
## 3. Runner 程序未安装时
@@ -28,7 +40,12 @@
2840
./config.sh --url https://github.com/owner/repo --token <TOKEN>
2941
```
3042

31-
**使用 Docker 或 DinD 时**:镜像内没有 runner 二进制,添加 Runner 时只会创建空目录。若希望提交表单时自动完成注册,可**在容器内执行安装脚本**(见 [Docker 部署](docker.md) 中「Docker/DinD 下自动注册的前提」)完成解压后再在界面添加;或先在宿主机把 runner 解压到挂载的 `runners/<名称>/` 下,再在界面填写并提交(名称、目标、Token)。若已先提交导致目录为空,请在该目录解压 runner 后,到 GitHub 重新获取 Token,在目录下手动执行上述 `config.sh`
43+
**使用 Docker 或 DinD 时**:若在「快速添加」中填写了 Token 并提交,服务会**先自动执行安装脚本**(下载并解压 runner 到该目录),再执行**向 GitHub 注册**`config.sh`,超时 2 分钟)并启动;无需事先手动执行 `install-runner.sh`。若自动安装失败(如网络问题),可按 [Docker 部署](docker.md) 中「Docker/DinD 下自动注册的前提」手动安装后,重新获取 Token 再在界面提交或在该目录下手动执行 `config.sh`
44+
45+
## 注册结果与定时检查
46+
47+
- **注册结果**:每次在界面使用 Token 执行注册后,结果会写入该 runner 目录下的 `.registration_result.json`(成功或失败原因),并在列表与详情中显示。
48+
- **GitHub 显示检查**:服务内建定时任务每 5 分钟检查各 runner 是否已在 GitHub 的 Actions Runners 列表中显示。需在 `config.yaml` 中配置 `github.token`(需 scope:组织用 `admin:org`,仓库用 `repo`)。检查结果写入各 runner 目录的 `.github_status.json`,并在界面「注册 / GitHub」列与详情中展示。
3249

3350
## 一台机器多 Runner
3451

docs/docker.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Docker 部署
22

3+
- **基础镜像**:运行时使用 **Ubuntu**(非 Alpine),避免 GitHub Runner 在 Alpine 下运行异常。
4+
- **自动拉起 Runner**:服务启动约 15 秒后会自动启动所有「已注册但未在运行」的 Runner;定时任务每 5 分钟也会再次检查并拉起未运行的已注册 Runner,便于 DinD 或管理器重启后恢复。
5+
36
## 使用已发布镜像(推荐)
47

58
从 GitHub Container Registry 拉取并运行,无需本地构建:
@@ -82,39 +85,28 @@ docker run -d --name runner-manager \
8285

8386
- Runner 子进程会继承 `DOCKER_HOST`,Job 中的 `docker` 命令将使用 `runner-dind` 容器内的守护进程。
8487
- DinD 需 `--privileged`,且与宿主机 Docker 隔离,适合希望 Job 与宿主机环境隔离的场景。
88+
- **DinD 或管理器重启后**:runner-manager 启动后会延迟约 15 秒自动启动所有已注册的 Runner;之后每 5 分钟定时任务也会检查并拉起未在运行的已注册 Runner,无需手动点击「启动」。
8589

86-
#### Docker/DinD 下「自动注册」的前提
90+
#### Docker/DinD 下「自动注册」
8791

88-
本镜像**不包含** GitHub Actions runner 二进制。添加 Runner 时若在表单中填写了 Token,服务只会在**已存在 runner 程序的目录**里执行 `config.sh` 完成注册;若该目录为空(刚由服务创建),则只会保存配置并返回提示「请将 runner 解压到某目录后再次提交或手动执行 config.sh
92+
本镜像**不包含** GitHub Actions runner 二进制,但**支持在 Web 界面一次完成安装与注册**:在「快速添加 Runner」中填写名称、目标、Token 并提交后,服务会先自动执行 `/app/scripts/install-runner.sh` 下载并解压 runner,再执行 `config.sh` 向 GitHub 注册并启动 Runner。无需事先手动安装
8993

90-
因此在使用 Docker/DinD 时,若希望**一次提交就完成注册**,请按以下顺序操作
94+
若自动安装失败(例如网络不可达 GitHub releases),可改为手动安装后再在界面提交 Token,或手动执行注册
9195

92-
1. **方式 A:容器内使用安装脚本**(推荐)
93-
在宿主机执行以下命令,会在挂载的 `runners/<名称>/` 下自动下载并解压 runner(默认版本 2.331.0,可选校验哈希):
96+
1. **容器内执行安装脚本**(推荐)
9497
```bash
9598
docker exec runner-manager /app/scripts/install-runner.sh <名称> [版本号]
9699
```
97-
`config.yaml``runners.base_path` 不是 `/app/runners`可设置环境变量后执行,例如
100+
`config.yaml``runners.base_path` 不是 `/app/runners`可设置环境变量
98101
```bash
99102
docker exec -e RUNNERS_BASE_PATH=/app/runners runner-manager /app/scripts/install-runner.sh my-runner
100103
```
104+
2. **或在宿主机**创建 `runners/<名称>/` 并解压 [actions-runner Linux 包](https://github.com/actions/runner/releases),再在界面填写名称、目标、Token 提交。
101105

102-
2. **方式 B:在宿主机**(挂载 `runners` 的那台机器)上,先创建对应子目录并解压 runner:
103-
```bash
104-
mkdir -p runners/<名称>
105-
cd runners/<名称>
106-
# 从 https://github.com/actions/runner/releases 下载 Linux x64 包并解压到当前目录
107-
curl -sL https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz | tar xz
108-
```
109-
3. 再在管理界面「快速添加 Runner」中填写名称(与上面使用的 `<名称>` 一致)、目标、Token 并提交,此时服务会检测到 `config.sh` 并自动执行注册。
110-
111-
若已先在界面添加了 Runner(目录已创建但为空),可:
112-
113-
- 在宿主机进入 `runners/<名称>/`,解压 runner 后,到 GitHub 重新获取一份 Token,在该目录下手动执行:
106+
若已先在界面添加了 Runner(目录已创建但为空),可删除该 Runner 后重新在界面填写 Token 提交(会触发自动安装),或在目录内解压 runner 后到 GitHub 重新获取 Token,手动执行:
114107
```bash
115108
./config.sh --url https://github.com/<目标> --token <新TOKEN>
116109
```
117-
- 或删除该 Runner 配置后,按上面 1/2→3 顺序重新添加(可改用方式 A 在容器内执行安装脚本)。
118110

119111
---
120112

docs/security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## 路径安全
88

9-
- Runner 的 **name****path** 禁止包含 `..``/``\`
9+
- Runner 的 **name****path** 禁止包含 `..``/``\`(添加时校验;查询/启动/停止/更新/删除时也会校验 URL 中的 name 参数)
1010
- 创建目录时强制落在 `runners.base_path` 之下,防止路径穿越。
1111

1212
## 唯一性

internal/githubcheck/check.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package githubcheck
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
"github.com/lab-dev/github-actions-runner-manager/internal/config"
11+
"github.com/lab-dev/github-actions-runner-manager/internal/runner"
12+
)
13+
14+
const (
15+
apiBase = "https://api.github.com"
16+
apiTimeout = 30 * time.Second
17+
apiPerPage = 100 // 单页数量,减少漏判(GitHub 默认 30)
18+
)
19+
20+
// githubRunnersResponse 与 GitHub API 返回结构一致
21+
type githubRunnersResponse struct {
22+
TotalCount int `json:"total_count"`
23+
Runners []struct {
24+
ID int64 `json:"id"`
25+
Name string `json:"name"`
26+
OS string `json:"os"`
27+
Status string `json:"status"`
28+
} `json:"runners"`
29+
}
30+
31+
// Run 根据配置对每个 runner 调用 GitHub API 检查是否已在 GitHub 显示,并写入 .github_status.json
32+
func Run(cfg *config.Config) {
33+
if cfg == nil || cfg.GitHub.Token == "" {
34+
return
35+
}
36+
client := &http.Client{Timeout: apiTimeout}
37+
for _, item := range cfg.Runners.Items {
38+
installDir := item.InstallPath(cfg.Runners.BasePath)
39+
registered := checkOne(client, cfg.GitHub.Token, item.TargetType, item.Target, item.Name)
40+
_ = runner.WriteGitHubStatus(installDir, registered)
41+
}
42+
}
43+
44+
// isValidTargetFormat 与 handler.validateTarget 规则一致,避免对无效 target 发起 API 请求;targetType 会规范为小写
45+
func isValidTargetFormat(targetType, target string) bool {
46+
raw := strings.TrimSpace(target)
47+
if raw == "" {
48+
return false
49+
}
50+
tt := strings.ToLower(strings.TrimSpace(targetType))
51+
switch tt {
52+
case "org":
53+
return !strings.Contains(raw, "/")
54+
case "repo":
55+
parts := strings.SplitN(raw, "/", 2)
56+
if len(parts) != 2 {
57+
return false
58+
}
59+
if strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
60+
return false
61+
}
62+
return !strings.Contains(parts[1], "/")
63+
default:
64+
return false
65+
}
66+
}
67+
68+
func checkOne(client *http.Client, token, targetType, target, runnerName string) bool {
69+
raw := strings.TrimSpace(target)
70+
tt := strings.ToLower(strings.TrimSpace(targetType))
71+
if !isValidTargetFormat(tt, target) {
72+
return false
73+
}
74+
var path string
75+
if tt == "org" {
76+
path = "/orgs/" + raw + "/actions/runners"
77+
} else {
78+
// repo
79+
path = "/repos/" + raw + "/actions/runners"
80+
}
81+
url := apiBase + path + "?per_page=" + strconv.Itoa(apiPerPage)
82+
req, err := http.NewRequest(http.MethodGet, url, nil)
83+
if err != nil {
84+
return false
85+
}
86+
req.Header.Set("Accept", "application/vnd.github+json")
87+
req.Header.Set("Authorization", "Bearer "+token)
88+
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
89+
resp, err := client.Do(req)
90+
if err != nil || resp.StatusCode != http.StatusOK {
91+
if resp != nil {
92+
_ = resp.Body.Close()
93+
}
94+
return false
95+
}
96+
defer func() { _ = resp.Body.Close() }()
97+
var data githubRunnersResponse
98+
if json.NewDecoder(resp.Body).Decode(&data) != nil {
99+
return false
100+
}
101+
for _, r := range data.Runners {
102+
if r.Name == runnerName {
103+
return true
104+
}
105+
}
106+
return false
107+
}

0 commit comments

Comments
 (0)