diff --git a/.github/workflows/runner-image.yml b/.github/workflows/runner-image.yml new file mode 100644 index 0000000..426cf05 --- /dev/null +++ b/.github/workflows/runner-image.yml @@ -0,0 +1,128 @@ +# Build and publish the cross-org GitHub Actions runner image. +# +# Image: ghcr.io/chillwhales/actions-runner +# +# Tags: +# On push to main (or weekly/manual): +# - node22- (immutable, used for Helm pins) +# - node22 (rolling) +# - latest (rolling) +# On pull_request touching runner/**: +# - pr-- (immutable, scoped to PR for runner smoke-testing) +# - pr- (rolling within the PR) +# +# Triggers: +# - pull_request (same paths) validate the build + pre-publish PR tag +# - push to main touching runner/** promote to canonical tags +# - weekly cron pick up base image security patches +# - workflow_dispatch manual rebuild +name: Runner image + +on: + pull_request: + paths: + - 'runner/**' + - '.github/workflows/runner-image.yml' + push: + branches: [main] + paths: + - 'runner/**' + - '.github/workflows/runner-image.yml' + schedule: + # Mondays 06:00 UTC — fresh base image weekly + - cron: '0 6 * * 1' + workflow_dispatch: + +concurrency: + group: runner-image-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + resolve-runner: + uses: ./.github/workflows/resolve-runner.yml + + build: + name: Build and push runner image + runs-on: ${{ needs.resolve-runner.outputs.heavy }} + needs: [resolve-runner] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Compute tags + id: tags + run: | + short_sha="${GITHUB_SHA::7}" + image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/actions-runner" + echo "image=${image}" >> "$GITHUB_OUTPUT" + { + echo "tags<> "$GITHUB_OUTPUT" + else + echo "${image}:node22-${short_sha}" + echo "${image}:node22" + echo "${image}:latest" + echo "pin_tag=node22-${short_sha}" >> "$GITHUB_OUTPUT" + fi + echo "EOF" + } >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to ghcr.io + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: runner + file: runner/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=registry,ref=${{ steps.tags.outputs.image }}:buildcache + cache-to: ${{ github.event_name == 'pull_request' && '' || format('type=registry,ref={0}:buildcache,mode=max', steps.tags.outputs.image) }} + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.description=GitHub Actions runner with Node 22 + pnpm baked in + + - name: Smoke test image + run: | + docker run --rm --entrypoint bash "${{ steps.tags.outputs.image }}:${{ steps.tags.outputs.pin_tag }}" -lc ' + set -euo pipefail + node --version + corepack --version + pnpm --version + git --version + ' + + - name: Summary + run: | + { + echo "## Runner image published" + echo "" + echo "Image: \`${{ steps.tags.outputs.image }}:${{ steps.tags.outputs.pin_tag }}\`" + echo "" + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + echo "PR-scoped image — pin a runner to this tag to smoke-test before merge:" + else + echo "Pin Helm values to the immutable tag:" + fi + echo "" + echo '```yaml' + echo "image:" + echo " repository: ${{ steps.tags.outputs.image }}" + echo " tag: ${{ steps.tags.outputs.pin_tag }}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/runner/Dockerfile b/runner/Dockerfile new file mode 100644 index 0000000..49ed40c --- /dev/null +++ b/runner/Dockerfile @@ -0,0 +1,41 @@ +# Custom GitHub Actions runner image with Node 22 + pnpm + corepack baked in. +# +# Why: stock ghcr.io/actions/actions-runner:latest has no Node toolchain, so every +# job spends ~1m25s on actions/setup-node@v4 downloading + installing Node from +# scratch. Baking it into the image drops setup-node to ~2s (toolchain present; +# the action just resolves it). +# +# Tagging: published as ghcr.io/chillwhales/actions-runner:{latest,node22,node22-} +# from chillwhales/.github by .github/workflows/runner-image.yml. Helm values in +# k8s-cluster pin to the immutable node22- tag for deliberate upgrades. +FROM ghcr.io/actions/actions-runner:latest + +ARG NODE_MAJOR=22 +ARG PNPM_VERSION=10.18.2 + +USER root + +# Base utilities + Node 22 LTS via NodeSource. +# git/curl/ca-certificates are required by most CI flows; build-essential +# covers native module compiles (better-sqlite3, etc.) without surprise failures. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gnupg \ + build-essential \ + python3 \ + && curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Corepack ships with Node; activate the pinned pnpm version system-wide so +# every job sees the same pnpm regardless of packageManager field in repo. +RUN corepack enable \ + && corepack prepare "pnpm@${PNPM_VERSION}" --activate + +USER runner + +RUN node --version && pnpm --version diff --git a/runner/README.md b/runner/README.md new file mode 100644 index 0000000..04773b0 --- /dev/null +++ b/runner/README.md @@ -0,0 +1,32 @@ +# Custom Actions Runner Image + +Cross-org GitHub Actions runner image with Node 22 + pnpm + corepack baked in. + +## Why + +Stock `ghcr.io/actions/actions-runner:latest` ships without a Node toolchain. Every job that uses `actions/setup-node@v4` spends ~1m25s downloading and installing Node from scratch on the ephemeral pod. Baking the toolchain into the image cuts that to ~2s. + +## Image + +- **Registry:** `ghcr.io/chillwhales/actions-runner` +- **Tags:** + - `node22-` — immutable, recommended for Helm pins + - `node22` — latest Node 22 build (mutable) + - `latest` — alias for current default +- **Base:** `ghcr.io/actions/actions-runner:latest` +- **Includes:** Node 22 LTS, pnpm 10.18.2 (via corepack), git, curl, build-essential, python3 + +## Build + +Published by `.github/workflows/runner-image.yml`: +- on push to `main` touching `runner/**` +- weekly cron rebuild for upstream base image patches +- manual `workflow_dispatch` + +## Consumers + +ARC `runnerScaleSetName: self-hosted` deployments in `k8s-cluster/infrastructure/actions-runner-set-*/values.yaml`. Pin `image.repository` + `image.tag` to an immutable `node22-` tag and bump deliberately. + +## Updating pnpm version + +Edit the `PNPM_VERSION` build arg in `Dockerfile`. Builds are cheap; let the cron rebuild pick it up or trigger `workflow_dispatch`.