What Changes When a Workflow Moves to ARM64?
The runs-on label changes. After that, check three things: binaries downloaded with an architecture suffix, native modules, and hardcoded absolute paths.
The runs-on label changes, and in a plain workflow that is the whole edit: swap warp-ubuntu-latest-x64-4x for warp-ubuntu-latest-arm64-4x and the same steps run on an aarch64 machine. After the label, three classes of step need checking before the first green run: steps that download an architecture-specific binary, steps that build or install native modules, and steps that carry an absolute path copied from the GitHub-hosted layout (WarpBuild cloud runners documentation, checked on 2026-08-13).
Answer
Four categories account for the failures teams hit on the first ARM64 job. Each one has a recognizable error string, which makes triage fast.
| Category | What it looks like on the first ARM64 run | The change |
|---|---|---|
| Downloaded binaries with an architecture suffix | cannot execute binary file: Exec format error | Drive the architecture token in the URL from the same variable that drives the label |
| Native modules and compiled dependencies | Install or import failure naming a platform package such as @esbuild/linux-x64 | Regenerate the lockfile with the aarch64 variant present, or install with optional dependencies enabled |
| Container base images | no matching manifest for linux/arm64/v8 in the manifest list entries | Pick a base image published with a linux/arm64 manifest, or build one |
| Absolute paths | A file that the log says exists is not found at the path a later step reads | Read $GITHUB_WORKSPACE and $RUNNER_TEMP instead of writing the path out |
Everything else in a normal workflow behaves the same. actions/checkout, the language setup actions, actions/cache, actions/upload-artifact, service containers, and job containers all run on the ARM64 images. The runner agent itself ships a linux-arm64 release (actions/runner), so there is no compatibility layer between your steps and the machine.
The one WarpBuild-specific difference worth knowing before the first run is the work directory: the arm64 images for Ubuntu 24.04 set it to /runner/_work, which differs from GitHub's /home/runner/work/ for the same instance (cloud runners documentation). That difference is covered in full in do ARM64 runners use a different work directory.
WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners. The Linux ARM64 line runs from warp-ubuntu-latest-arm64-2x at 2 vCPU and 8 GB for $0.003 per minute to warp-ubuntu-latest-arm64-32x at 32 vCPU and 128 GB for $0.048 per minute, across Ubuntu 24.04 and Ubuntu 26.04 (cloud runners documentation, checked on 2026-08-13). Every label sits in the Linux ARM64 runner catalog.
One variable driving the label and the download URLs
The pattern that keeps a migrated workflow honest is a single matrix entry that carries the runner label and every architecture token the steps need. The tokens differ by ecosystem, so putting them next to each other in one place stops the mismatch that produces Exec format error.
name: release
on:
push:
tags: ["v*"]
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: warp-ubuntu-latest-x64-4x
kubectl_arch: amd64
rust_target: x86_64-unknown-linux-gnu
- arch: arm64
runner: warp-ubuntu-latest-arm64-4x
kubectl_arch: arm64
rust_target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- name: Install kubectl for ${{ matrix.arch }}
run: |
version="$(curl -fsSL https://dl.k8s.io/release/stable.txt)"
curl -fsSLo kubectl \
"https://dl.k8s.io/release/${version}/bin/linux/${{ matrix.kubectl_arch }}/kubectl"
sudo install -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
- uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: cargo-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock') }}
- name: Build
run: cargo build --release --target ${{ matrix.rust_target }}
- name: Verify the artifact architecture
run: file "target/${{ matrix.rust_target }}/release/app"
- uses: actions/upload-artifact@v4
with:
name: app-linux-${{ matrix.arch }}
path: target/${{ matrix.rust_target }}/release/appfail-fast: false keeps the x64 leg running while the ARM64 leg is still failing, which is what you want during the move. The file step turns a silent mismatch into a readable line in the log, and ${{ runner.arch }} in the cache key keeps the two legs from overwriting each other (GitHub contexts reference).
Detail
The architecture tokens are inconsistent across ecosystems
Nothing standardized on one spelling. A step that hardcodes amd64 in a URL and a step that hardcodes x86_64 in a wheel name are both wrong on ARM64, and they are wrong with different strings. This table covers the tokens that appear most often in a workflow file.
| Where the token appears | x86-64 value | ARM64 value |
|---|---|---|
uname -m in a shell step | x86_64 | aarch64 |
${{ runner.arch }} context | X64 | ARM64 |
Go toolchain (GOARCH, go env) | amd64 | arm64 |
| Rust target triple | x86_64-unknown-linux-gnu | aarch64-unknown-linux-gnu |
| Docker and OCI platform strings | linux/amd64 | linux/arm64 |
| Python wheel tags | manylinux_2_28_x86_64 | manylinux_2_28_aarch64 |
| Node platform packages | linux-x64 | linux-arm64 |
The runner.arch values come from the GitHub contexts reference. Two rules keep this manageable. Keep every token explicit instead of deriving one from another with a sed expression inside a run: block, because the mapping between them is arbitrary. Put the tokens in the matrix entry, next to the runner label, so adding a third architecture later is one block of YAML rather than a search across the file.
Native modules are the category that fails quietly
Downloads fail loudly. Native modules sometimes install and then break at run time, which costs more debugging than the label change itself.
Node packages that ship prebuilt binaries publish one optional package per platform, so a lockfile written on an x86-64 machine can record the linux-x64 variant and omit the linux-arm64 one. esbuild documents this platform-package layout in its own repository (evanw/esbuild), and the same pattern covers image processing, native crypto, and database driver packages. Regenerate the lockfile with the ARM64 variant resolvable, or install with optional dependencies enabled, then confirm the resolved package name in the install log rather than trusting the exit code.
Python wheels follow the tag in the filename. Where the index carries no aarch64 wheel for a pinned version, pip falls back to building from source, which needs the compiler and headers on the runner that a prebuilt wheel makes unnecessary. That fallback shows up as a long install step before it shows up as a failure, so read the install log for lines that say the package is being built rather than downloaded.
JVM and Go projects are the easy cases. Go cross-compiles by setting GOARCH, and the JVM runs the same bytecode, with the exception of libraries that ship bundled native code and select it by classifier at run time. Check those classifiers the same way you check a wheel tag.
Container base images need a linux/arm64 manifest
A job that uses container:, a step that runs docker run, or a Dockerfile with a FROM line all resolve an image for the host architecture. When the tag has no linux/arm64 entry in its manifest list, the pull fails with no matching manifest. Check before the migration rather than during it:
docker manifest inspect nginx:1.27 \
| grep -A2 '"platform"' | grep architectureOfficial images on Docker Hub are built for multiple architectures by the docker-library/official-images build system, so the common bases are usually covered. Internal images built by an x86-64-only pipeline usually are not, and those are the ones to find first. Building both architectures natively, one job per architecture on a matching runner, avoids the emulation path entirely; the ARM64 migration checklist for GitHub Actions sequences that work.
Absolute paths, and what to do about the work directory
The fourth category is the one that has nothing to do with the instruction set. Steps that write /home/runner/work/... as a literal break on the ARM64 images because the work directory is /runner/_work there (cloud runners documentation). Replace the literal with ${{ github.workspace }} in YAML and $GITHUB_WORKSPACE in shell steps, which resolve correctly on every image and on GitHub-hosted runners (GitHub variables reference). Docker -v mounts, coverage upload paths, and checked-in tool configuration are the usual carriers.
What the move costs
Rates come from the cloud runners documentation, checked on 2026-08-13. A team running 10,000 job minutes a month on a 4 vCPU shape pays 10,000 x $0.008 = $80.00 on warp-ubuntu-latest-x64-4x and 10,000 x $0.006 = $60.00 on warp-ubuntu-latest-arm64-4x, a difference of $20.00 a month at identical vCPU and RAM. Per-size rates for every label are on the pricing page.
When the first ARM64 job fails and the log does not say why, the surrounding product surface is faster than adding print statements. The Action Debugger opens a shell on the failed runner so you can run uname -m and file against the artifact that broke, and CI observability shows whether the failure is on one architecture or both. Jobs that never reach a runner at all are a different problem, covered in the common issues documentation.
Related Questions
What happens if a step downloads an x86-64 binary on an ARM64 runner?
The download succeeds and the first execution fails with cannot execute binary file: Exec format error. The kernel reads the ELF header, sees a machine type it cannot run, and refuses. The fix is to drive the architecture token in the URL from the same matrix variable that drives the runs-on label, or from uname -m inside the step, so the token always matches the machine. Add a file step after the download while you are migrating, because it names the target architecture in the log before anything tries to execute it.
Do the ARM64 images ship the same preinstalled tools as the x64 images?
Close, and the lists are published rather than assumed. The Ubuntu 24.04 ARM64 runners are compatible with GitHub's Ubuntu 24.04 ARM64 runners, and the package list is at actions/partner-runner-images; the Ubuntu 26.04 ARM64 list is at actions/runner-images. Read the list before you assume a language version is present. Sizes and Ubuntu releases for both lines are in the Linux ARM64 runner catalog.
Can I move one job to ARM64 without moving the whole workflow?
Yes. runs-on is set per job, so a workflow can run its test job on warp-ubuntu-latest-arm64-4x and leave packaging on an x86-64 label until the artifacts are sorted out. Most teams move one job, keep fail-fast: false while the architecture-specific breakage is worked through, then move the rest. The ARM64 migration checklist for GitHub Actions covers the order, and do ARM64 runners use a different work directory covers the path audit to run first.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.