Files
Samuel Silva e6ed17b918 nixos/github-runners: GitHub App authentication and multi-org runners
Add three options to `services.github-runners.<name>`:

- `githubApp`: authenticate via a GitHub App installation instead of a
  `tokenFile`. The service derives a short-lived installation token from the
  App's private key on each start, registers the runner, and de-registers it
  on stop. The PEM key is a deployed secret, never copied into the store.

- `count`: fan a single entry out into N identical runner services.
  `github-runner-<name>` is unchanged for `count == 1`, suffixed `-<n>` for
  `count > 1`.

- `orgs`: serve several organisations (or repositories) from one entry,
  fanning out into `github-runner-<name>-<org>-<n>`. The entry-level auth is
  shared across orgs and only the App `login` changes per org; the per-org
  `count` defaults to the entry-level `count`.

This brings the multi-org consumption shape of the soon-to-be-removed srvos
runner role (nix-community/srvos#836, nix-community/srvos#837) into nixpkgs.
An entry without `orgs` and with the default `count = 1` is unchanged, so
existing runners do not re-register.
2026-06-19 15:21:54 -03:00

89 lines
2.7 KiB
Bash

# shellcheck shell=bash
# Request an installation access token for a GitHub App.
#
# Adapted from https://github.com/myoung34/docker-github-actions-runner (MIT,
# Copyright (c) 2020 Marcus Young), see:
# https://github.com/orgs/community/discussions/24743#discussioncomment-3245300
#
# Expects the following environment variables:
# * APP_ID the GitHub App's ID
# * APP_PRIVATE_KEY the GitHub App's PEM-encoded private key (contents)
# * APP_LOGIN the org/user login the App is installed on
# * GITHUB_HOST optional, defaults to github.com (set for GHES)
#
# Prints the installation access token (prefixed with `ghs_`) to stdout.
_GITHUB_HOST=${GITHUB_HOST:="github.com"}
# If the host is not github.com, use the GitHub Enterprise Server API endpoint.
if [[ ${_GITHUB_HOST} == "github.com" ]]; then
URI="https://api.${_GITHUB_HOST}"
else
URI="https://${_GITHUB_HOST}/api/v3"
fi
API_VERSION=v3
API_HEADER="Accept: application/vnd.github.${API_VERSION}+json"
CONTENT_LENGTH_HEADER="Content-Length: 0"
APP_INSTALLATIONS_URI="${URI}/app/installations"
# JWT token issuance and expiration parameters, see:
# https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
JWT_IAT_DRIFT=60
JWT_EXP_DELTA=600
JWT_JOSE_HEADER='{
"alg": "RS256",
"typ": "JWT"
}'
build_jwt_payload() {
now=$(date +%s)
iat=$((now - JWT_IAT_DRIFT))
jq -c \
--arg iat_str "${iat}" \
--arg exp_delta_str "${JWT_EXP_DELTA}" \
--arg app_id_str "${APP_ID}" \
'
($iat_str | tonumber) as $iat
| ($exp_delta_str | tonumber) as $exp_delta
| ($app_id_str | tonumber) as $app_id
| .iat = $iat
| .exp = ($iat + $exp_delta)
| .iss = $app_id
' <<<"{}" | tr -d '\n'
}
base64url() {
base64 | tr '+/' '-_' | tr -d '=\n'
}
rs256_sign() {
openssl dgst -binary -sha256 -sign <(echo "$1")
}
request_access_token() {
jwt_payload=$(build_jwt_payload)
encoded_jwt_parts=$(base64url <<<"${JWT_JOSE_HEADER}").$(base64url <<<"${jwt_payload}")
encoded_mac=$(echo -n "${encoded_jwt_parts}" | rs256_sign "${APP_PRIVATE_KEY}" | base64url)
generated_jwt="${encoded_jwt_parts}.${encoded_mac}"
auth_header="Authorization: Bearer ${generated_jwt}"
app_installations_response=$(
curl -fsSX GET \
-H "${auth_header}" \
-H "${API_HEADER}" \
"${APP_INSTALLATIONS_URI}"
)
access_token_url=$(echo "${app_installations_response}" | jq --raw-output '.[] | select (.account.login == "'"${APP_LOGIN}"'" and .app_id == '"${APP_ID}"') .access_tokens_url')
curl -fsSX POST \
-H "${CONTENT_LENGTH_HEADER}" \
-H "${auth_header}" \
-H "${API_HEADER}" \
"${access_token_url}" |
jq --raw-output .token
}
request_access_token