Toolchains
A runner VM boots from an image you build, and
actions/setup-python and
actions/setup-node resolve the
version a job asks for from that image’s hosted tool cache. Declaring
toolchains is how versions get into that cache, so a workflow’s setup-* steps
work the way they do on a GitHub-hosted runner.
A toolchain is specifically a language runtime placed in the tool cache for
those actions to find, and RunnerToolchain installs two: Python and Node.
Software a job invokes directly, rather than through a setup-* action, goes
into an image through RunnerImage’s systemPackages, setupCommands, and
assets options, which Runner images covers along with how images
are built and how large they can be.
Declaring a toolchain
Section titled “Declaring a toolchain”RunnerToolchain has two methods:
RunnerToolchain.python('3.12.7');RunnerToolchain.node('22.11.0');Pass them to a RunnerImage, and give that image to the runner class whose
jobs need them:
runners.addRunnerClass('microvm', { size: MicrovmSize.GB4, image: RunnerImage.fromOptions({ toolchains: [ RunnerToolchain.python('3.12.7'), RunnerToolchain.node('22.11.0'), ], }),});A workflow that targets that class then uses setup-* as written:
runs-on: [self-hosted, microvm]steps: - uses: actions/setup-python@v7 with: python-version: '3.12'toolchains defaults to an empty list, so an image carries the versions it
names. Declare them per runner class, for the versions that class’s workflows
use.
Versions
Section titled “Versions”Each toolchain pins a full three-part semver, because the version names the
cache directory it is baked into — /opt/hostedtoolcache/Python/3.12.7/arm64.
A partial version throws at synth time:
RunnerToolchain: version must be a full semver like '3.12.7', got '3.12'.Several versions of the same language live in one image, each in its own directory:
toolchains: [ RunnerToolchain.python('3.11.9'), RunnerToolchain.python('3.12.7'), RunnerToolchain.node('20.18.0'), RunnerToolchain.node('22.11.0'),],A workflow’s version selector is a semver range, so python-version: '3.12'
resolves to the baked 3.12.7 and node-version: '20' to the baked 20.18.0.
Moving from 3.12.6 to 3.12.7 is an edit to the version string. That changes
the rendered Dockerfile and with it the image’s content hash, which builds a new
image version, the same as any other change to RunnerImage.fromOptions()’s
options.
When a workflow asks for a version the image does not carry
Section titled “When a workflow asks for a version the image does not carry”Both actions read the tool cache first and otherwise resolve the version from GitHub’s published builds, and they differ in what is published for the runner’s architecture.
actions/python-versions publishes no linux-arm64 builds, so a Python version
the image does not carry ends the job at that step:
The version '3.12' with architecture 'arm64' was not found for thisoperating systemBake every Python version your workflows request. That coverage is
per-version, across every repository the runner set serves: an image baked
with 3.12.7 for one repository fails another repository’s
python-version: '3.11' job at exactly this step, and the fix is baking
3.11.9 alongside, as the multi-version example above does.
actions/node-versions does publish linux-arm64 builds, so setup-node
downloads a Node version the image does not carry and the job runs on it. That
costs about 24 seconds, and since each job gets its own VM, the download
repeats on every job that asks for an un-baked version rather than accumulating
in a shared cache. Bake the Node versions your workflows use when you would
rather not pay that each time.
Several baked versions add up against the image’s disk allowance; Runner images covers the budget and how to fit inside it.