Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions .github/workflows/runner-image.yml
Original file line number Diff line number Diff line change
@@ -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-<short-sha> (immutable, used for Helm pins)
# - node22 (rolling)
# - latest (rolling)
# On pull_request touching runner/**:
# - pr-<number>-<short-sha> (immutable, scoped to PR for runner smoke-testing)
# - pr-<number> (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<<EOF"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
pr="${{ github.event.pull_request.number }}"
echo "${image}:pr-${pr}-${short_sha}"
echo "${image}:pr-${pr}"
echo "pin_tag=pr-${pr}-${short_sha}" >> "$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"
41 changes: 41 additions & 0 deletions runner/Dockerfile
Original file line number Diff line number Diff line change
@@ -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-<sha>}
# from chillwhales/.github by .github/workflows/runner-image.yml. Helm values in
# k8s-cluster pin to the immutable node22-<sha> 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
32 changes: 32 additions & 0 deletions runner/README.md
Original file line number Diff line number Diff line change
@@ -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-<sha>` — 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-<sha>` 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`.
Loading