runnerimages

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package runnerimages is what billet knows about GitHub's own runner image.

WHY THIS EXISTS, AND WHY IT IS A VENDORED FILE RATHER THAN A FETCH.

GitHub does not publish the image its hosted runners boot. `actions/runner-images` is SOURCE: Packer templates whose only builder is `azure-arm`, producing a managed image in the builder's own Azure subscription. Every image release there carries exactly one asset, a ~50KB `internal.<image>.json`; there is no VHD, no qcow2 and no rootfs to download anywhere. Anyone matching that image — Blacksmith included — is rebuilding it, not booting it.

What IS published is enough to rebuild: `toolset-<version>.json` declares every apt package, toolcache entry, JDK, NDK and pinned tool version, and eighty plain bash installers sit beside it. That file is what this package vendors, and it is the single answer to "what should be in a billet runner image" for BOTH backends.

ONE MANIFEST, TWO BACKENDS. The firecracker guest image and the ec2 AMI used to carry two hand-maintained package lists in two languages, which is the same shape as the runner-version bug `runnerrelease` exists to prevent: two pins is one pin that is wrong. The shell build reads this same file through `jq`, so a package added for one backend cannot silently miss the other.

PINNED, NOT TRACKED. An image is a thing you reproduce. Reading upstream `main` at build time would make two runs of the same command produce different images, and the difference would surface as a job failing on one generation and not another — which is the argument `billet ami` already makes about the runner version.

Index

Constants

This section is empty.

Variables

View Source
var InstallToolcacheScript string

InstallToolcacheScript is the toolcache installers both backends run.

EMBEDDED AND ALSO SOURCED FROM DISK, deliberately: scripts/build-guest-image.sh dots this same file in by path, so there is one implementation rather than a Go copy and a shell copy that drift. The EC2 backend has no path to it on the builder, so it carries these bytes in the provisioning script.

Functions

func AptAliases

func AptAliases() map[string]string

AptAliases is the declared-name to installable-name mapping.

A MISSING OR MALFORMED FILE YIELDS AN EMPTY MAP RATHER THAN AN ERROR, because the mapping is an exception list: having none is the ordinary case. What it must never do is silently DROP an entry it was given, so the file is embedded rather than read from disk and a parse failure is impossible at runtime for a binary that compiled.

func InstallerToolset

func InstallerToolset() ([]byte, error)

InstallerToolset is the declaration the installers read, and nothing else.

THE APT SECTIONS STAY BEHIND. `.apt` and the compiler sections are read on THIS side and interpolated as a command rather than shipped as data, and the dozen upstream keys nothing reads -- android, brew, selenium and the rest -- are what makes the full file cost five times this one.

IT EXISTS FOR A BUDGET, AND THE BUDGET IS THE REASON TO BE CAREFUL. The EC2 backend delivers the whole provisioning script through user data, which EC2 caps at 16384 bytes; the full declaration costs 1673 of those compressed and this costs 329. That is the difference between an image that can carry a toolcache for both architectures and one that cannot.

A SUBSET IS A SECOND REPRESENTATION, WHICH IS THE RISK. If this ever drops a key an installer reads, the installer sees an empty answer -- and read_toolset_versions refuses an empty answer loudly, which is the shape that saves it. The stronger guard is TestTheProjectionAnswersEveryReaderTheSameWay, which runs the shell's own readers against both this and the full file and requires identical answers.

func InstallerToolsetSHA256

func InstallerToolsetSHA256() (string, error)

InstallerToolsetSHA256 is the digest of what InstallerToolset returns.

NOT PinnedSHA256, AND THE DIFFERENCE IS WORTH STATING. The pinned digest proves the vendored file is upstream's; this one proves the script received exactly what billet sent. Load has already checked the first before this can be called, so the chain is: upstream's file, verified here, projected here, and the projection verified on the builder.

func PinnedCommit

func PinnedCommit() string

PinnedCommit is the actions/runner-images revision toolset-2404.json came from.

func PinnedSHA256

func PinnedSHA256() string

PinnedSHA256 is the digest of the vendored toolset as it should be on disk.

func SourceURL

func SourceURL() string

SourceURL names where the vendored copy came from, at the pinned commit.

func ToolsetBytes

func ToolsetBytes() []byte

ToolsetBytes is the vendored file exactly as it is on disk.

RETURNED AS A COPY. The embedded slice is package state, and a caller that sliced into it could change what every later digest check hashes — which would make the integrity check agree with the tampering rather than catch it.

func ToolsetJSON

func ToolsetJSON() string

ToolsetJSON is the pinned declaration, verbatim.

VERBATIM IS THE POINT. The EC2 build carries these bytes to its builder and checks PinnedSHA256 against them THERE, so what the installers read is what the pin names rather than something billet reshaped in transit. Callers that want the parsed form use Load; this is for delivery.

func ValidateAptAliases

func ValidateAptAliases() error

ValidateAptAliases reports every alias entry that names no installable package.

SEPARATE FROM AptAliases SO Load CAN FAIL ON IT. An unusable entry must not be a map the callers quietly route around: the shell readers turn one into a blank package name, which removes the package from what is installed and from what the gate requires in the same step, and nothing then reports it missing.

func VerifyToolset

func VerifyToolset(data []byte, wantDigest string) error

VerifyToolset reports whether these bytes are the ones the pin names.

TAKES ITS INPUTS RATHER THAN READING PACKAGE STATE, which is what makes it testable. The first version of this check ran only over the embedded file, and the test that claimed to cover it did its OWN comparison of the same bytes against the same pin before calling Load — so deleting the production check left that test green, and corrupting the pin proved only that the test's duplicate comparison worked. A guard nothing can drive with wrong input is a guard nothing has checked.

Types

type Android

type Android struct {
	CmdlineTools         string   `json:"cmdline-tools"`
	PlatformMinVersion   string   `json:"platform_min_version"`
	BuildToolsMinVersion string   `json:"build_tools_min_version"`
	ExtraList            []string `json:"extra_list"`
	AddonList            []string `json:"addon_list"`
	AdditionalTools      []string `json:"additional_tools"`
	NDK                  NDK      `json:"ndk"`
}

Android is the SDK, its NDKs, and the extras a build expects to find offline.

type Apt

type Apt struct {
	VitalPackages  []string `json:"vital_packages"`
	CommonPackages []string `json:"common_packages"`
	CmdPackages    []string `json:"cmd_packages"`
}

Apt is the three package lists, which upstream installs in this order.

type Clang

type Clang struct {
	Versions       []string `json:"versions"`
	DefaultVersion string   `json:"default_version"`
}

Clang carries a default because the unsuffixed `clang` must resolve to one.

type Docker

type Docker struct {
	Components []DockerComponent `json:"components"`
	Plugins    []DockerPlugin    `json:"plugins"`
}

Docker is the engine components and CLI plugins, pinned by upstream.

type DockerComponent

type DockerComponent struct {
	Package string `json:"package"`
	Version string `json:"version"`
}

DockerComponent is one apt package of the engine.

type DockerPlugin

type DockerPlugin struct {
	Plugin  string `json:"plugin"`
	Version string `json:"version"`
	Asset   string `json:"asset"`
}

DockerPlugin is one CLI plugin and the asset naming it ships under.

type DotNet

type DotNet struct {
	Versions []string     `json:"versions"`
	Tools    []DotNetTool `json:"tools"`
}

DotNet is the SDK feature bands and the global tools installed beside them.

type DotNetTool

type DotNetTool struct {
	Name string `json:"name"`
}

DotNetTool is one `dotnet tool install` entry.

type Java

type Java struct {
	Default  string   `json:"default"`
	Versions []string `json:"versions"`
	Maven    string   `json:"maven"`
}

Java is the JDK set, and the default JAVA_HOME points at Default.

type NDK

type NDK struct {
	Default  string   `json:"default"`
	Versions []string `json:"versions"`
}

NDK is the native development kit set.

type NodeDefault

type NodeDefault struct {
	Default string `json:"default"`
}

NodeDefault is the system-wide node, distinct from the toolcache entries.

type NodeModule

type NodeModule struct {
	Name    string `json:"name"`
	Command string `json:"command"`
}

NodeModule is one globally installed npm package and the command it provides.

type PSModule

type PSModule struct {
	Name     string   `json:"name"`
	Versions []string `json:"versions"`
}

PSModule is one module installed from PSGallery.

Versions IS A LIST AND USUALLY EMPTY. Upstream pins two of the four it declares and leaves the rest floating, so the absence of a pin is the declaration saying "newest", not a field billet failed to read.

type PipxPackage

type PipxPackage struct {
	Package string `json:"package"`
	Cmd     string `json:"cmd"`
}

PipxPackage is one isolated python application.

type ToolcacheEntry

type ToolcacheEntry struct {
	Name            string   `json:"name"`
	Arch            string   `json:"arch"`
	Platform        string   `json:"platform"`
	PlatformVersion string   `json:"platform_version"`
	Versions        []string `json:"versions"`
	Default         string   `json:"default"`
}

ToolcacheEntry is one tool billet bakes into /opt/hostedtoolcache.

Versions are upstream's glob forms ("3.13.*", "22.*"), not resolved versions: the toolcache is a cache rather than a contract, so the build resolves the newest matching release at build time and checksum-verifies what it downloads.

type Toolset

type Toolset struct {
	Toolcache []ToolcacheEntry `json:"toolcache"`
	Java      Java             `json:"java"`
	Android   Android          `json:"android"`
	// TWO LISTS, ONE MECHANISM. Upstream maintains these apart because different
	// teams own them; both are Install-Module from PSGallery, and a single field
	// would misrepresent the declaration while two installers would duplicate it.
	PowerShellModules []PSModule    `json:"powershellModules"`
	AzureModules      []PSModule    `json:"azureModules"`
	Apt               Apt           `json:"apt"`
	Docker            Docker        `json:"docker"`
	DotNet            DotNet        `json:"dotnet"`
	Clang             Clang         `json:"clang"`
	GCC               Versions      `json:"gcc"`
	GFortran          Versions      `json:"gfortran"`
	PHP               Versions      `json:"php"`
	Node              NodeDefault   `json:"node"`
	PowerShell        Version       `json:"pwsh"`
	PostgreSQL        Version       `json:"postgresql"`
	CMake             Version       `json:"cmake"`
	Pipx              []PipxPackage `json:"pipx"`
	NodeModules       []NodeModule  `json:"node_modules"`
}

Toolset is the half of GitHub's declaration billet acts on.

DELIBERATELY NOT THE WHOLE DOCUMENT. The upstream file also describes PowerShell modules, Azure modules and Homebrew formulae, which are either Windows-only or things billet installs by another route. Decoding only what is used means an upstream addition billet ignores is visibly absent here rather than silently carried as a field nothing reads.

func Load

func Load() (Toolset, error)

Load parses the vendored toolset, after proving it is the file that was pinned.

THE DIGEST IS CHECKED BEFORE THE CONTENT IS USED, not as a separate audit somebody remembers to run. This file decides what goes into an image that runs other people's CI on the operator's own hardware, and an edit to it is an edit to every image built afterwards — so "is this still the reviewed file" has to be answered on the path that reads it rather than beside it.

PARSED AFRESH ON EVERY CALL, AND NOT CACHED. A cached Toolset returned by value still hands out the package's own slices: a caller that changed one apt package would change it for every later caller, and the digest check — which ran once, over the bytes rather than the parsed value — would never notice. Reparsing a few kilobytes is cheaper than any scheme for making shared mutable state safe.

func (Toolset) AptPackages

func (t Toolset) AptPackages() []string

AptPackages is every package the image installs, in upstream's own order.

ORDER IS PRESERVED AND DUPLICATES ARE REMOVED. Upstream installs vital, then common, then cmd, and a package named in two lists is one install; emitting it twice makes an apt command longer for no reason and makes a diff of this list read as though something changed. The names are the ones apt can install, which is not always the name upstream declared: see AptAliases.

func (Toolset) JavaHomeVars

func (t Toolset) JavaHomeVars(root string) map[string]string

JavaHomeVars is the JAVA_HOME_<version>_X64 environment every hosted image sets.

THESE ARE NOT DECORATION. setup-java reads JAVA_HOME_<version>_X64 to find a JDK already on the machine, and a workflow that pins a toolchain by environment variable rather than by action reads them directly. An image carrying the JDKs without the variables has them installed and unfindable, which is the toolcache failure one directory over: nothing errors, the job just does the slow thing.

func (Toolset) ToolchainPackages

func (t Toolset) ToolchainPackages() []string

ToolchainPackages are the compiler and language sections, as apt package names.

FIVE SECTIONS, THREE SHAPES, AND THE DECLARATION IS NOT CONSISTENT ABOUT THEM. `gcc` and `gfortran` list COMPLETE package names ("g++-13", "gfortran-13"); `clang` lists bare majors ("18") that a name has to be built from; `php` and `postgresql` carry a single version each rather than a list. Reading them as one shape produces names apt has never heard of, and the build fails minutes in with a message about a package rather than about a declaration.

MEASURED AGAINST ubuntu-24.04's OWN main+universe, on BOTH architectures and with identical answers: clang-16/17/18, g++-12/13/14, gfortran-12/13/14, php8.3-cli and postgresql-client-16 all resolve. No third-party apt repository is needed for any of them -- an earlier probe said otherwise and was reading a package list left empty by an `apt-get update` whose failure it had sent to /dev/null.

THE CLIENT FOR POSTGRES AND THE CLI FOR PHP, not the server and not the whole stack. What a job uses is `psql` and `php`; `postgresql-16` would install and enable a database in every image for the benefit of nobody, and upstream's own image reaches these through a service container instead.

type Version

type Version struct {
	Version string `json:"version"`
}

Version is a single pinned version.

type Versions

type Versions struct {
	Versions []string `json:"versions"`
}

Versions is a bare version list, used where upstream needs nothing else.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL