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 ¶
- Variables
- func AptAliases() map[string]string
- func InstallerToolset() ([]byte, error)
- func InstallerToolsetSHA256() (string, error)
- func PinnedCommit() string
- func PinnedSHA256() string
- func SourceURL() string
- func ToolsetBytes() []byte
- func ToolsetJSON() string
- func ValidateAptAliases() error
- func VerifyToolset(data []byte, wantDigest string) error
- type Android
- type Apt
- type Clang
- type Docker
- type DockerComponent
- type DockerPlugin
- type DotNet
- type DotNetTool
- type Java
- type NDK
- type NodeDefault
- type NodeModule
- type PSModule
- type PipxPackage
- type ToolcacheEntry
- type Toolset
- type Version
- type Versions
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 NodeDefault ¶
type NodeDefault struct {
Default string `json:"default"`
}
NodeDefault is the system-wide node, distinct from the toolcache entries.
type NodeModule ¶
NodeModule is one globally installed npm package and the command it provides.
type PSModule ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.