Skip to content

Commit 1775a38

Browse files
committed
GitHub Actions Runner: register several self-hosted runners per repository
The shared helper already creates the container, so this script only installs the runner and keeps those registrations across updates.
1 parent 552c4ed commit 1775a38

3 files changed

Lines changed: 469 additions & 0 deletions

File tree

‎ct/github-actions-runner.sh‎

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env bash
2+
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/DevScripts/main"
3+
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
4+
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
5+
6+
# Copyright (c) 2021-2026 community-scripts ORG
7+
# Author: Baker (Baker)
8+
# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE
9+
# Source: https://github.com/actions/runner
10+
11+
APP="GitHub-Actions-Runner"
12+
var_tags="${var_tags:-ci;github;actions}"
13+
var_cpu="${var_cpu:-4}"
14+
var_ram="${var_ram:-12288}"
15+
var_disk="${var_disk:-40}"
16+
var_os="${var_os:-debian}"
17+
var_version="${var_version:-13}"
18+
var_arm64="${var_arm64:-yes}"
19+
var_unprivileged="${var_unprivileged:-1}"
20+
# Workflow jobs start containers, so the LXC needs nesting and keyctl.
21+
var_nesting="${var_nesting:-1}"
22+
var_keyctl="${var_keyctl:-1}"
23+
24+
header_info "$APP"
25+
variables
26+
color
27+
catch_errors
28+
29+
# A personal GitHub account cannot share one registration across repositories,
30+
# and each runner process takes one job. Ask on the host, before the container
31+
# exists, so a bad token never creates an LXC. Updates run inside the container
32+
# and only refresh binaries, so they skip this.
33+
_cs_platform=""
34+
declare -f detect_lxc_platform >/dev/null 2>&1 && _cs_platform="$(detect_lxc_platform)"
35+
if [[ "${_cs_platform}" != "container" && "${_cs_platform}" != "incus-container" ]]; then
36+
if is_unattended; then
37+
var_runner_count="${var_runner_count:-2}"
38+
var_runner_name="${var_runner_name:-github-runner}"
39+
if [[ -z "${var_gh_owner:-}" || -z "${var_gh_repos:-}" || -z "${var_gh_token:-}" ]]; then
40+
msg_error "Unattended install needs var_gh_owner, var_gh_repos, and var_gh_token"
41+
exit 1
42+
fi
43+
else
44+
if [[ -z "${var_gh_owner:-}" ]]; then
45+
var_gh_owner="$(prompt_input_required "GitHub owner" "UNSET" 180 "var_gh_owner")"
46+
fi
47+
if [[ -z "${var_gh_repos:-}" ]]; then
48+
var_gh_repos="$(prompt_input_required "Repositories, comma-separated" "UNSET" 180 "var_gh_repos")"
49+
fi
50+
if [[ -z "${var_gh_token:-}" ]]; then
51+
var_gh_token="$(prompt_password "GitHub token with Administration on each repository" "" 180 20)"
52+
fi
53+
if [[ -z "${var_runner_count:-}" ]]; then
54+
var_runner_count="$(prompt_input "Runners per repository" "2" 120)"
55+
fi
56+
if [[ -z "${var_runner_name:-}" ]]; then
57+
var_runner_name="$(prompt_input "Runner name prefix" "github-runner" 120)"
58+
fi
59+
fi
60+
61+
var_gh_repos="${var_gh_repos//[[:space:]]/}"
62+
var_runner_count="${var_runner_count:-2}"
63+
var_runner_name="${var_runner_name:-github-runner}"
64+
65+
if [[ "${var_gh_owner}" == "UNSET" || ! "${var_gh_owner}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
66+
msg_error "GitHub owner is invalid"
67+
exit 1
68+
fi
69+
if [[ -z "${var_gh_repos}" || "${var_gh_repos}" == "UNSET" || "${var_gh_repos}" == *,,* || "${var_gh_repos}" == ,* || "${var_gh_repos}" == *, ]]; then
70+
msg_error "At least one repository is required"
71+
exit 1
72+
fi
73+
if [[ -z "${var_gh_token}" ]]; then
74+
msg_error "GitHub token is required"
75+
exit 1
76+
fi
77+
if [[ ! "${var_runner_count}" =~ ^[1-9][0-9]*$ ]]; then
78+
msg_error "Runners per repository must be a positive integer"
79+
exit 1
80+
fi
81+
if [[ ! "${var_runner_name}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
82+
msg_error "Runner name is invalid"
83+
exit 1
84+
fi
85+
86+
command -v curl >/dev/null 2>&1 || {
87+
msg_error "curl is required on the host"
88+
exit 1
89+
}
90+
IFS=',' read -r -a _gh_repos <<< "${var_gh_repos}"
91+
for _gh_repo in "${_gh_repos[@]}"; do
92+
[[ "${_gh_repo}" == */* ]] || _gh_repo="${var_gh_owner}/${_gh_repo}"
93+
if [[ ! "${_gh_repo}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
94+
msg_error "Repository name is invalid"
95+
exit 1
96+
fi
97+
_gh_http="$(curl --connect-timeout 10 --max-time 30 -sS -o /dev/null -w '%{http_code}' -X POST \
98+
-H "Accept: application/vnd.github+json" \
99+
-H "Authorization: Bearer ${var_gh_token}" \
100+
-H "X-GitHub-Api-Version: 2022-11-28" \
101+
"https://api.github.com/repos/${_gh_repo}/actions/runners/registration-token" || true)"
102+
if [[ "${_gh_http}" != "201" ]]; then
103+
msg_error "GitHub refused a registration token for ${_gh_repo} (HTTP ${_gh_http}). The token needs Administration on that repository."
104+
exit 1
105+
fi
106+
done
107+
unset _gh_repos _gh_repo _gh_http
108+
fi
109+
110+
export var_gh_owner="${var_gh_owner:-}"
111+
export var_gh_repos="${var_gh_repos:-}"
112+
export var_gh_token="${var_gh_token:-}"
113+
export var_runner_count="${var_runner_count:-2}"
114+
export var_runner_name="${var_runner_name:-github-runner}"
115+
116+
update_deb_based() {
117+
if [[ ! -x /opt/actions-runner/config.sh ]]; then
118+
msg_error "No ${APP} Installation Found!"
119+
exit 1
120+
fi
121+
122+
local unit dest
123+
if check_for_gh_release "github-actions-runner" "actions/runner"; then
124+
msg_info "Stopping GitHub Actions Runner"
125+
shopt -s nullglob
126+
for unit in /etc/systemd/system/actions-runner-*.service; do
127+
systemctl stop "$(basename "${unit}")"
128+
done
129+
shopt -u nullglob
130+
msg_ok "Stopped GitHub Actions Runner"
131+
132+
# Registrations live in /opt/actions-runners and must survive this wipe.
133+
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "github-actions-runner" "actions/runner" "prebuild" "latest" "/opt/actions-runner" "actions-runner-linux-$(arch_resolve "x64" "arm64")-*.tar.gz"
134+
135+
msg_info "Updating runner copies"
136+
shopt -s nullglob
137+
for dest in /opt/actions-runners/*/; do
138+
cp -a /opt/actions-runner/. "${dest}"
139+
done
140+
shopt -u nullglob
141+
msg_ok "Updated runner copies"
142+
143+
if [[ -x /opt/actions-runner/bin/installdependencies.sh ]]; then
144+
msg_info "Installing runner dependencies"
145+
$STD /opt/actions-runner/bin/installdependencies.sh
146+
msg_ok "Installed runner dependencies"
147+
elif [[ -x /opt/actions-runner/installdependencies.sh ]]; then
148+
msg_info "Installing runner dependencies"
149+
$STD /opt/actions-runner/installdependencies.sh
150+
msg_ok "Installed runner dependencies"
151+
fi
152+
153+
msg_info "Starting GitHub Actions Runner"
154+
shopt -s nullglob
155+
for unit in /etc/systemd/system/actions-runner-*.service; do
156+
systemctl start "$(basename "${unit}")"
157+
done
158+
shopt -u nullglob
159+
msg_ok "Started GitHub Actions Runner"
160+
msg_ok "Updated successfully!"
161+
fi
162+
exit
163+
}
164+
165+
function update_script() {
166+
header_info
167+
check_container_storage
168+
check_container_resources
169+
run_os_update
170+
}
171+
172+
start
173+
build_container
174+
description
175+
176+
msg_ok "Completed Successfully!\n"
177+
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
178+
if [[ -n "${var_gh_repos:-}" ]]; then
179+
echo -e "${INFO}${YW}Runner settings:${CL}"
180+
IFS=',' read -r -a _gh_repos <<< "${var_gh_repos//[[:space:]]/}"
181+
for _gh_repo in "${_gh_repos[@]}"; do
182+
[[ "${_gh_repo}" == */* ]] || _gh_repo="${var_gh_owner}/${_gh_repo}"
183+
echo -e "${TAB}${BGN}https://github.com/${_gh_repo}/settings/actions/runners${CL}"
184+
done
185+
fi
186+
echo -e "${INFO}${YW}Jobs on this container share one network namespace. A workflow that publishes a fixed host port will collide with a second job that publishes the same port.${CL}"
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright (c) 2021-2026 community-scripts ORG
4+
# Author: Baker (Baker)
5+
# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE
6+
# Source: https://github.com/actions/runner
7+
8+
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
9+
color
10+
verb_ip6
11+
catch_errors
12+
setting_up_container
13+
network_check
14+
update_os
15+
16+
setup_deb_based() {
17+
local repos=() repo owner_repo unit i dest name reg_file reg_token http_code dep
18+
19+
var_gh_repos="${var_gh_repos//[[:space:]]/}"
20+
var_runner_count="${var_runner_count:-2}"
21+
var_runner_name="${var_runner_name:-github-runner}"
22+
23+
[[ "${var_gh_owner:-}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
24+
msg_error "GitHub owner is invalid"
25+
exit 1
26+
}
27+
[[ "${var_runner_count}" =~ ^[1-9][0-9]*$ ]] || {
28+
msg_error "Runners per repository must be a positive integer"
29+
exit 1
30+
}
31+
[[ "${var_runner_name}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
32+
msg_error "Runner name is invalid"
33+
exit 1
34+
}
35+
[[ -n "${var_gh_token:-}" ]] || {
36+
msg_error "GitHub token is required"
37+
exit 1
38+
}
39+
[[ -n "${var_gh_repos:-}" ]] || {
40+
msg_error "At least one repository is required"
41+
exit 1
42+
}
43+
44+
IFS=',' read -r -a repos <<< "${var_gh_repos}"
45+
for repo in "${repos[@]}"; do
46+
[[ -n "${repo}" ]] || {
47+
msg_error "Empty repository name"
48+
exit 1
49+
}
50+
if [[ "${repo}" == */* ]]; then
51+
owner_repo="${repo}"
52+
else
53+
owner_repo="${var_gh_owner}/${repo}"
54+
fi
55+
[[ "${owner_repo}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || {
56+
msg_error "Repository '${repo}' is invalid"
57+
exit 1
58+
}
59+
done
60+
61+
msg_info "Installing build tools"
62+
$STD apt install -y git make gcc libc6-dev
63+
msg_ok "Installed build tools"
64+
65+
# Job steps run in containers. The runner binaries stay on the system.
66+
setup_docker
67+
68+
fetch_and_deploy_gh_release "github-actions-runner" "actions/runner" "prebuild" "latest" "/opt/actions-runner" "actions-runner-linux-$(arch_resolve "x64" "arm64")-*.tar.gz"
69+
70+
if [[ -x /opt/actions-runner/bin/installdependencies.sh ]]; then
71+
dep="/opt/actions-runner/bin/installdependencies.sh"
72+
elif [[ -x /opt/actions-runner/installdependencies.sh ]]; then
73+
dep="/opt/actions-runner/installdependencies.sh"
74+
else
75+
msg_error "Runner package has no installdependencies.sh"
76+
exit 1
77+
fi
78+
if [[ -f /opt/actions-runner/config.sh ]]; then
79+
chmod +x /opt/actions-runner/config.sh
80+
fi
81+
if [[ -f /opt/actions-runner/bin/runsvc.sh ]]; then
82+
chmod +x /opt/actions-runner/bin/runsvc.sh
83+
fi
84+
if [[ -f "${dep}" ]]; then
85+
chmod +x "${dep}"
86+
fi
87+
msg_info "Installing runner dependencies"
88+
$STD "${dep}"
89+
msg_ok "Installed runner dependencies"
90+
91+
mkdir -p /opt/actions-runners /opt/runner-tool-cache
92+
93+
msg_info "Registering GitHub Actions runners"
94+
for repo in "${repos[@]}"; do
95+
if [[ "${repo}" == */* ]]; then
96+
owner_repo="${repo}"
97+
else
98+
owner_repo="${var_gh_owner}/${repo}"
99+
fi
100+
unit="${owner_repo//\//-}"
101+
for ((i = 1; i <= var_runner_count; i++)); do
102+
dest="/opt/actions-runners/${unit}-${i}"
103+
name="${var_runner_name}-${i}"
104+
mkdir -p "${dest}"
105+
cp -a /opt/actions-runner/. "${dest}/"
106+
chmod 700 "${dest}"
107+
108+
reg_file="$(mktemp)"
109+
chmod 600 "${reg_file}"
110+
http_code="$(curl --connect-timeout 10 --max-time 30 -sS -o "${reg_file}" -w '%{http_code}' -X POST \
111+
-H "Accept: application/vnd.github+json" \
112+
-H "Authorization: Bearer ${var_gh_token}" \
113+
-H "X-GitHub-Api-Version: 2022-11-28" \
114+
"https://api.github.com/repos/${owner_repo}/actions/runners/registration-token" || true)"
115+
reg_token="$(jq -r '.token // empty' "${reg_file}" 2>/dev/null || true)"
116+
rm -f "${reg_file}"
117+
if [[ "${http_code}" != "201" || -z "${reg_token}" ]]; then
118+
unset reg_token
119+
msg_error "GitHub refused a registration token for ${owner_repo} (HTTP ${http_code})"
120+
exit 1
121+
fi
122+
123+
# config.sh refuses uid 0 unless RUNNER_ALLOW_RUNASROOT is set.
124+
if ! (
125+
cd "${dest}"
126+
RUNNER_ALLOW_RUNASROOT=1 ./config.sh \
127+
--unattended \
128+
--replace \
129+
--url "https://github.com/${owner_repo}" \
130+
--token "${reg_token}" \
131+
--name "${name}" \
132+
--work "_work"
133+
) </dev/null; then
134+
unset reg_token
135+
msg_error "Could not register ${owner_repo} as ${name}"
136+
exit 1
137+
fi
138+
unset reg_token
139+
140+
cat <<EOF >"${dest}/.env"
141+
RUNNER_TOOL_CACHE=/opt/runner-tool-cache
142+
EOF
143+
chmod 600 "${dest}/.env"
144+
if [[ -f "${dest}/.credentials" ]]; then
145+
chmod 600 "${dest}/.credentials"
146+
fi
147+
if [[ -f "${dest}/.runner" ]]; then
148+
chmod 600 "${dest}/.runner"
149+
fi
150+
chmod 700 "${dest}"
151+
152+
cat <<EOF >/etc/systemd/system/actions-runner-${unit}-${i}.service
153+
[Unit]
154+
Description=GitHub Actions runner ${i} for ${owner_repo}
155+
After=network-online.target docker.service
156+
Wants=network-online.target docker.service
157+
158+
[Service]
159+
User=root
160+
Environment=RUNNER_ALLOW_RUNASROOT=1
161+
EnvironmentFile=${dest}/.env
162+
ExecStart=${dest}/bin/runsvc.sh
163+
WorkingDirectory=${dest}
164+
KillMode=process
165+
KillSignal=SIGTERM
166+
TimeoutStopSec=5min
167+
Restart=always
168+
RestartSec=10
169+
170+
[Install]
171+
WantedBy=multi-user.target
172+
EOF
173+
systemctl enable -q --now "actions-runner-${unit}-${i}.service"
174+
done
175+
done
176+
msg_ok "Registered GitHub Actions runners"
177+
unset var_gh_token
178+
}
179+
180+
run_os_setup
181+
182+
motd_ssh
183+
customize
184+
cleanup_lxc

0 commit comments

Comments
 (0)