Build your own GitHub Actions runner images — Windows & Linux — the easy way.
This tracks actions/runner-images (GitHub's own image-build scripts, pinned to a release) and builds them locally with Packer into a ready-to-boot qcow2 for self-hosted runners.
Licensing — read this. The build scripts here are open; the images are licensed in use. You bring your own OS media/license — this repo never ships or downloads Windows for you. Build images for your own use; do not redistribute Windows or macOS images. (Same model as
runner-imagesitself: open scripts, licensed images.) Ubuntu/Linux images are open and freely redistributable.
You need a Linux host with /dev/kvm for Windows & Linux images (a spare box or a Linux
VM), or an Apple Silicon Mac with tart + sshpass for macOS images.
git clone https://github.com/premex-ab/runner-images-factory
cd runner-images-factory
./build.sh list
# Linux — fully automatic (pulls the cloud image itself, no media needed):
./build.sh ubuntu-2404
# Windows — you supply the eval ISO (no product key needed):
# https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025
./build.sh windows-2025 --iso ~/Downloads/server2025-eval.iso
# → out/windows-2025/windows-2025.qcow2 (+ .sha256)
# macOS — on an Apple Silicon Mac via Tart (needs `tart` + `sshpass`), no ISO:
./build.sh macos-tahoe
# arm64 Linux — also on an Apple Silicon Mac via Tart (no arm64 KVM host needed):
./build.sh ubuntu-2404-arm64
# Verify a built image — boots it + runs the toolchain for real (pass/fail):
./build.sh verify ubuntu-2404The first run bootstraps prereqs (packer, qemu, vncdotool); see lib/common.sh.
The build drops out/<name>/<name>.qcow2 (+ .sha256) — host it however suits your
runners (object store, a file server, a local registry, your orchestrator's image cache …).
images/<name>/— our thin Packer overlay per image. The Windows cell stagesrunner-imagesat a pinned ref (images/windows-2025/windows-2025.pkr.hcl→ri_ref), puts theirImageHelpersmodule onPSModulePath, ships theirtoolset.json, stubs out their Pester validation, and runs the full set of theirInstall-*.ps1— so the toolchain matcheswindows-latest, tracked by the tag (see the parity table for the handful of tools deliberately excluded). The Ubuntu cell does the same over SSH: it stages theirhelpers/+build/scripts +toolset.jsoninto/imagegenerationand runs the full set ofinstall-*.sh. We never edit their tree (consume, don't fork) → no merge conflicts.build.sh— the single entry point: prereq bootstrap, host checks, Packer + the boot-prompt helper, output + checksum.build.sh verify <image>— real verification: boots the built image and runs the toolchain, pass/fail. Linux uses a cloud-init seed (serial); Windows a cloudbase-init#ps1(COM1); macOS boots the Tart VM and SSHes in. The genuine functional test — not "scripts exited 0".- macOS is a different path — built with Tart (Apple Silicon Mac only), not Packer/QEMU. It clones the cirruslabs macOS base (a maintained CI image — the "consume" analog), bakes the runner, and verifies over SSH. Not redistributable (Apple EULA), which fits the model.
- arm64 Linux is the same Tart path —
ubuntu-2404-arm64builds on an Apple Silicon Mac too (there's no arm64 KVM host), cloning the cirruslabs Ubuntu Tart base and provisioning a broad toolset over SSH (build_linux_tart/verify_linux_tart). It's the arm64 sibling of the x86ubuntu-2404(Packer/qcow2) cell. Scope is a solid, broad arm64 toolset (docker, the build toolchain, git/git-lfs, Node, Python, Go, .NET, GitHub CLI, the linux-arm64 runner) — not fullubuntu-24.04-armparity (the runner-images install scripts are x86-centric); the cell'sprovision.shlists the GitHub-hosted tools deliberately left out as arm64 gaps. The Ubuntu base is freely redistributable. - Keeping up with upstream: bump the pinned
ri_ref, rebuild, re-verify.
The goal is the full runner-images toolset per image — not a curated subset — built and
boot-verified. Verification boots the finished image and runs the toolchain for real;
on Windows/Ubuntu it compares the installed tools against GitHub's own toolset-<ver>.json
manifest at the pinned ref. See PARITY.md for the per-script checklist.
| Image | Toolset | Boot-verified | Not in parity (excluded on purpose) |
|---|---|---|---|
ubuntu-2204 |
full set (77/77 scripts) | ✅ toolchain | — none |
ubuntu-2404 |
full set (67/67 scripts) | ✅ manifest parity | — none |
ubuntu-2404-arm64 (Tart, Apple Silicon) |
broad arm64 toolset [5] | ▢ via Tart (verify ubuntu-2404-arm64) |
browsers/Selenium, Android SDK, cloud CLIs, pwsh, toolcache [5] |
windows-2025 |
full set + Visual Studio 2022 | ✅ manifest parity | Android SDK [1]; 2 VS extensions [2] |
windows-2022 |
full set + Visual Studio 2022 | ✅ cell [3] | Android SDK [1]; 2 VS extensions [2] |
macos-13/14/15/26 |
cirruslabs base + GitHub runner | ✅ over SSH | n/a [4] |
Everything else GitHub ships is in parity: the languages (Python/Go/Node/Ruby/PHP/Rust/Java 8-25/Kotlin/…), the toolcache, .NET 8/9/10 SDKs, the databases (MySQL/PostgreSQL/MongoDB), the cloud CLIs (Azure/AWS/GCP), browsers + Selenium, and the build tooling — including Visual Studio 2022 with the rest of its workloads and extensions.
Deliberately excluded until fixed (the build skips these rather than failing on them — both are memory-pressure build failures on a tight host, not missing-tool bugs):
- Android SDK — skipped pending #32:
the multi-package
sdk installbatch hits a JVM native OOM ("Failed to commit metaspace") under full build load. Re-enabled by re-adding one line to the toolset loop once #32 has a working fix. - 2 Visual Studio extensions — Installer Projects and Analysis Services Modeling Projects,
dropped pending #23:
VSIXInstaller.exeSTATUS_STACK_OVERFLOW(0xC00000FD) under memory pressure. The other VS extensions (e.g. SQL Server Reporting/Report Projects) install normally. windows-2022shares the exact same cell structure as the verifiedwindows-2025and builds the same full toolset; the freshly boot-verified Windows artifact in the current batch iswindows-2025.- macOS is built from the maintained cirruslabs base image via Tart (the "consume, don't fork"
analog for Apple hardware), not the runner-images install scripts — so it tracks that base, not the
toolset.jsonmanifest. ubuntu-2404-arm64is not fullubuntu-24.04-armparity. There's no arm64 KVM host here, so it builds via Tart on an Apple Silicon Mac (like the macOS cells), cloning the cirruslabs Ubuntu base and installing a broad toolset that builds reliably on arm64 from apt + first-party arm64 upstreams (the runner-images install set is x86-centric). Baked: docker.io, the build toolchain (gcc/g++/make/ cmake/ninja/pkg-config), git + git-lfs, curl/wget/unzip/zip/tar/jq/zstd, Python 3 (+pip/venv/dev), Node.js LTS, Go, .NET 8 SDK, GitHub CLI, and the linux-arm64 Actions runner. Deliberately omitted as arm64 gaps (no clean arm64 install or heavy/flaky): Google Chrome + Edge + Selenium, the Android SDK, Azure/AWS/GCP CLIs, PowerShell, and the multi-version hosted toolcache — seeimages/ubuntu-2404-arm64/provision.shfor the full list a human can opt back in.
-
ubuntu-2404cell (the fully-automatic example) — builds green (19 G qcow2), boot-verified - Real verification harness (
build.sh verify) — boots the image + runs the toolchain (ubuntu + windows verified) - Vendor
runner-imagesas a submodule + a daily bump → build → test → promote pipeline (AI agent for triage only) - Self-hosted build runners (Linux for win/ubuntu, Mac for macOS)
- Full
windows-latesttoolset (VS 2022 + languages/SDKs/toolcache), manifest-parity verified - Close the two excluded Windows tools — Android SDK (#32) + 2 VS extensions (#23) — both host-memory-pressure build failures
- Real Pester validation (replace the stubbed
Invoke-PesterTests) - Optional cloud finalize (AMI / GCE image / Azure VHD) — deferred until needed