#!/usr/bin/env bash # entrypoint.sh — Container startup script for the GitHub Actions runner. # # Lifecycle: # 1. Validate required environment variables # 2. Generate a short-lived registration token from GITHUB_PAT # 3. Configure the runner in ephemeral mode (one job, then exit) # 4. Trap SIGTERM/SIGINT for graceful deregistration # 5. Start the runner (run.sh) # # Docker's restart policy (restart: unless-stopped) brings the container # back after each job completes, repeating this cycle. set -euo pipefail # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- RUNNER_DIR="/home/runner/actions-runner" RUNNER_LABELS="${RUNNER_LABELS:-self-hosted,Linux,X64}" RUNNER_GROUP="${RUNNER_GROUP:-default}" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log() { printf '[entrypoint] %s\n' "$*" } die() { printf '[entrypoint] ERROR: %s\n' "$*" >&2 exit 1 } # --------------------------------------------------------------------------- # Environment validation — fail fast with clear errors # --------------------------------------------------------------------------- validate_env() { local missing=() [[ -z "${GITHUB_PAT:-}" ]] && missing+=("GITHUB_PAT") [[ -z "${REPO_URL:-}" ]] && missing+=("REPO_URL") [[ -z "${RUNNER_NAME:-}" ]] && missing+=("RUNNER_NAME") if [[ ${#missing[@]} -gt 0 ]]; then die "Missing required environment variables: ${missing[*]}. Check your .env and envs/*.env files." fi } # --------------------------------------------------------------------------- # Token generation — PAT → short-lived registration token # --------------------------------------------------------------------------- generate_token() { # Extract OWNER/REPO from the full URL. # Supports: https://github.com/OWNER/REPO or https://github.com/OWNER/REPO.git local repo_slug repo_slug="$(printf '%s' "$REPO_URL" \ | sed -E 's#^https?://github\.com/##' \ | sed -E 's/\.git$//')" if [[ -z "$repo_slug" ]] || ! printf '%s' "$repo_slug" | grep -qE '^[^/]+/[^/]+$'; then die "Could not parse OWNER/REPO from REPO_URL: $REPO_URL" fi log "Generating registration token for ${repo_slug}..." local response response="$(curl -fsSL \ -X POST \ -H "Authorization: token ${GITHUB_PAT}" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repo_slug}/actions/runners/registration-token")" REG_TOKEN="$(printf '%s' "$response" | jq -r '.token // empty')" if [[ -z "$REG_TOKEN" ]]; then die "Failed to generate registration token. Check that GITHUB_PAT has 'repo' scope and is valid." fi log "Registration token obtained (expires in 1 hour)." } # --------------------------------------------------------------------------- # Cleanup — deregister runner on container stop # --------------------------------------------------------------------------- cleanup() { log "Caught signal, removing runner registration..." # Generate a removal token (different from registration token) local repo_slug repo_slug="$(printf '%s' "$REPO_URL" \ | sed -E 's#^https?://github\.com/##' \ | sed -E 's/\.git$//')" local remove_token remove_token="$(curl -fsSL \ -X POST \ -H "Authorization: token ${GITHUB_PAT}" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repo_slug}/actions/runners/remove-token" \ | jq -r '.token // empty' || true)" if [[ -n "$remove_token" ]]; then "${RUNNER_DIR}/config.sh" remove --token "$remove_token" 2>/dev/null || true log "Runner deregistered." else log "WARNING: Could not obtain removal token. Runner may appear stale in GitHub until it expires." fi exit 0 } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { validate_env generate_token # Trap signals for graceful shutdown trap cleanup SIGTERM SIGINT # Remove stale configuration from previous run. # On container restart (vs recreate), the runner's writable layer persists # and config.sh refuses to re-configure if .runner already exists. # The --replace flag only handles server-side name conflicts, not this local check. if [[ -f "${RUNNER_DIR}/.runner" ]]; then log "Removing stale runner configuration from previous run..." rm -f "${RUNNER_DIR}/.runner" "${RUNNER_DIR}/.credentials" "${RUNNER_DIR}/.credentials_rsaparams" fi log "Configuring runner '${RUNNER_NAME}' for ${REPO_URL}..." log "Labels: ${RUNNER_LABELS}" log "Group: ${RUNNER_GROUP}" "${RUNNER_DIR}/config.sh" \ --url "${REPO_URL}" \ --token "${REG_TOKEN}" \ --name "${RUNNER_NAME}" \ --labels "${RUNNER_LABELS}" \ --runnergroup "${RUNNER_GROUP}" \ --work "/home/runner/_work" \ --ephemeral \ --unattended \ --replace log "Runner configured. Starting..." # exec replaces the shell with the runner process. # The runner picks up one job, executes it, and exits. # Docker's restart policy restarts the container for the next job. exec "${RUNNER_DIR}/run.sh" } main "$@"