Building for Graviton Targets on GitHub Actions

Run the GitHub Actions build and test jobs that produce your Graviton artifacts on native ARM64 runner labels, so architecture faults surface in the pipeline.

Last verified:

A pipeline that deploys to Graviton instances should compile and test on ARM64 machines, because an artifact built on x64 was validated against a different instruction set, a different memory ordering model, and a different set of prebuilt dependency binaries than the one production runs. On GitHub Actions that is a runs-on edit: send the build and test jobs to a native ARM64 label such as warp-ubuntu-latest-arm64-8x, and the pipeline starts exercising the architecture you ship to.

This guide covers the diagnosis, the workflow change, the image and path differences worth knowing before you flip a label, and a cost model at a stated job volume. The full label and rate list lives on the Linux ARM64 runner catalog. If you are still weighing native builds against GOARCH=arm64 style cross-compilation, cross-compiling compared with native ARM64 builds is the page for that decision.

Diagnosis

The mismatch is quiet. Production runs on ARM64 instances while the build and test pipeline runs on x64, every job is green, and the artifact still deploys. What the pipeline never checked is whether the code it compiled behaves the same on the architecture it lands on, so architecture-specific faults surface after deploy instead of in a pull request.

Confirm the target is actually ARM64

Start on the deploy side, because teams often inherit a Graviton fleet without the pipeline noticing.

  • EC2 instance families with a g in the type suffix run ARM64. Read the launch template or Auto Scaling group rather than the console default.
  • ECS task definitions carry runtimePlatform.cpuArchitecture: ARM64. EKS node groups carry an ARM64 AMI family.
  • Lambda functions declare Architectures: [arm64].
  • Container images that deploy to any of the above resolve the linux/arm64 entry in the manifest, which is a different set of layers from the linux/amd64 entry your x64 job built and smoke tested.

Then run uname -m on one production host and in one pipeline job. When the first prints aarch64 and the second prints x86_64, everything below applies.

What the x64 pipeline never validated

What differs on ARM64Where it shows up after deploy
Prebuilt native dependency binariesPackage managers resolve a different artifact per platform. A dependency with no aarch64 wheel or no prebuilt .node binary compiles from source on the target or fails at install.
Lockfile platform entriesLockfiles record platform-specific optional dependencies for the platform that generated them, so an x64-only lockfile can be missing the ARM64 entry a bundler or image toolchain needs.
Compiler and build flags-march=native, x86 intrinsics, and hand-written assembly in compression and crypto paths need an ARM64 branch. A guard that keys off the compiler rather than the target silently drops the optimized path.
Memory orderingThe ARM64 model is weaker than the x64 model, so a data race that x64 hid behind stronger ordering becomes a reproducible failure or an intermittent one under load.
Type and floating point detailschar is unsigned on aarch64 and signed on x86-64, and floating point contraction differs, which moves results at the last bits and trips exact-equality assertions.
Base image availabilityA base image or service container with no linux/arm64 variant fails at pull time in the environment you deploy into.

Rank the jobs before you move any

Not every job belongs on ARM64. The rule is narrow: any job that compiles, links, packages, or tests the artifact you deploy to Graviton moves, and everything else can stay where it is. A repository keeps its x64 jobs on x64 labels and moves only the work that has an architecture.

Order the move by blast radius. The test job is the cheapest signal, because it fails loudly and takes minutes to evaluate. The image build job comes second. Any job that only lints, formats, or renders docs stays on x64 and saves you a migration you do not need.

Fix

The change is one pull request per repository, and it is reviewable in a diff.

Send build and test to a native ARM64 label

name: service
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: warp-ubuntu-latest-arm64-8x
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.24"
      - name: confirm the deploy architecture
        run: |
          uname -m
          go env GOARCH
      - run: go build ./...
      - run: go test -race ./...

  image:
    needs: test
    runs-on: warp-ubuntu-latest-arm64-8x
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
      - name: smoke test the image on the architecture that will run it
        run: docker run --rm ghcr.io/${{ github.repository }}:${{ github.sha }} --version

Two details in that workflow do the real work. The test job runs go test -race on an ARM64 host, so races that the x64 memory model masked now fail in the pull request. The image job builds and then runs the image on the same machine, which turns a missing linux/arm64 base layer into a red job instead of a failed deployment.

The work directory differs on the Ubuntu 24.04 ARM64 image

The Ubuntu 24.04 ARM64 image sets the work dir to /runner/_work, which differs from GitHub's /home/runner/work/ on the equivalent instance. Any step that writes an absolute runner path breaks on the first ARM64 run: volume mounts, coverage reports that record file paths, tool configuration with an absolute root, and shell helpers copied between repositories.

Read the path from the environment instead of typing it.

      - name: mount the workspace into a container step
        run: |
          test -d "$GITHUB_WORKSPACE"
          docker run --rm -v "$GITHUB_WORKSPACE:/src" -w /src golang:1.24 go vet ./...

Inside uses: steps and expressions the same value is available as the github.workspace context. Grep the repository for /home/runner/work before you flip the label and you will find every offender in one pass.

When the failure only reproduces on ARM64

Some faults appear only on the target architecture and leave nothing useful in the log. The Action Debugger pauses the workflow and opens an SSH session on the runner, so you can attach a debugger to the aarch64 process, inspect the linked libraries, and re-run the failing test on the machine that produced it. Snapshot runners, remote Docker builders, and an MCP server sit alongside it on the same product surface.

Running two architectures also doubles the number of jobs a push starts. Run as many jobs as your workflows need. Generally available Linux and Windows runners do not have plan-level concurrency caps, and Braintrust reports consistent sub-minute runner provisioning on both ARM64 and x86 WarpBuild runners, which is the property that keeps a two-architecture matrix from adding a queue.

Configuration

ARM64 labels and rates against the x64 equivalents

Five sizes carry the Ubuntu 24.04 ARM64 image under the latest alias, and the same five exist for Ubuntu 26.04 under warp-ubuntu-2604-arm64-<size>. The delta against the x64 label with the identical vCPU count is visible in the last column.

SizeARM64 labelvCPURAMARM64 per minutex64 label at the same sizex64 per minuteDifference per minute
2xwarp-ubuntu-latest-arm64-2x28GB$0.003warp-ubuntu-latest-x64-2x$0.004$0.001
4xwarp-ubuntu-latest-arm64-4x416GB$0.006warp-ubuntu-latest-x64-4x$0.008$0.002
8xwarp-ubuntu-latest-arm64-8x832GB$0.012warp-ubuntu-latest-x64-8x$0.016$0.004
16xwarp-ubuntu-latest-arm64-16x1664GB$0.024warp-ubuntu-latest-x64-16x$0.032$0.008
32xwarp-ubuntu-latest-arm64-32x32128GB$0.048warp-ubuntu-latest-x64-32x$0.064$0.016

Every ARM64 size carries 150GB of SSD storage. Size by the parallelism your build actually reaches rather than by the shape of the production instance, because a Graviton service sized for request concurrency and a build sized for compiler jobs are unrelated numbers.

Pin an image that still exists

The ARM64 images for Ubuntu 22.04 were deprecated on March 31, 2025, so pin a 24.04 or 26.04 label rather than carrying an old tag into the migration. The Ubuntu 24.04 ARM64 runners are compatible with GitHub's Ubuntu 24.04 ARM64 runners, and the tooling list is published in the partner runner images reference for the ARM Ubuntu 24 image. The preinstalled software documentation maps every image to its upstream list, which is the fastest way to check a tool your workflow assumes before the first run.

Two checks are worth doing in the same pass. Third-party actions sometimes publish x64 release assets only, so read the action release page before you assume it runs. Container jobs and service containers need an arm64 variant of the image they name, which most official images publish and some internal base images do not.

Cache keys and the work that stays on x64

Cache entries are keyed by a hash covering the runner OS compression tool and the cached paths, so put runner.arch in the key and let the two fleets stop invalidating each other.

Nested virtualization and /dev/kvm access are available on Linux x64 runners through the nested-virtualization.enabled=true label and are unavailable on ARM64 runners today. Android emulator jobs and anything else that boots a VM inside the runner keep an x64 label such as warp-ubuntu-latest-x64-8x.

Running the ARM64 fleet inside your own account

Teams that want the runners next to the Graviton services they build for can place them in the same account. BYOC runs on AWS, GCP, and Azure, and Terraform support exists for BYOC on AWS, so the runner fleet is declared alongside the rest of your infrastructure. The full size and platform list is in the cloud runners documentation.

Cost or Time Model

Model the move at a fixed volume so the rate change and the minute change stay separate.

Assumptions

  • 3,000 build-and-test jobs per month, which is 150 jobs per weekday across 20 weekdays.
  • 9 minutes of runner time per job, giving 27,000 runner minutes per month.
  • Billing is per minute of runner time.
  • GitHub publishes its own per-minute rates for GitHub-hosted Linux ARM64 runners on the Actions minute multipliers reference, checked on 2026-08-13.

Monthly cost at 27,000 runner minutes

SizeWarpBuild ARM64 per minuteMonthly on WarpBuild ARM64GitHub-hosted Linux ARM64 at the same core countMonthly on GitHub-hosted ARM64WarpBuild x64 at the same size, monthly
2x$0.003$81.00$0.005$135.00$108.00
4x$0.006$162.00$0.008$216.00$216.00
8x$0.012$324.00$0.014$378.00$432.00
16x$0.024$648.00$0.026$702.00$864.00
32x$0.048$1,296.00$0.05$1,350.00$1,728.00

Reading the 8x row: warp-ubuntu-latest-arm64-8x is $0.012 per minute against $0.014 per minute for the 8-core Linux ARM64 larger runner, a 14 percent lower list price (GitHub Actions minute multipliers, checked on 2026-08-13). At the 2 vCPU size the same comparison is $0.003 against $0.005, a 40 percent lower list price, and at 32 vCPU it is $0.048 against $0.05, a 4 percent lower list price. The gap is widest at the small sizes, which is where most test jobs sit.

If you keep an x64 leg

Some teams still ship an amd64 image for local development while production runs Graviton. That doubles the compile work, and at this volume the second leg on warp-ubuntu-latest-arm64-8x adds $324.00 per month while the x64 leg keeps its $432.00. Before you accept both, check whether the x64 image needs a full test suite or only a build, because a build-only x64 leg usually costs a third of the minutes the full leg does.

The minutes side of the model

The rate table above holds the job length constant so the arithmetic stays readable. Job length is the number to measure in your own pipeline, using the run history for the same workflow before and after the label change.

Every cost and performance number on this page carries a source link and a checked-on date, and the same numbers appear on the pricing page. Whether ARM64 wins on the invoice as well as on correctness is worked through separately in is ARM64 cheaper than x64 for GitHub Actions.

FAQ

Do my GitHub Actions jobs have to run on ARM64 if production runs on Graviton?

The jobs that produce or validate the deployed artifact do. Compiling and testing on x64 exercises a different instruction set, a different memory ordering model, and a different set of prebuilt dependency binaries than the machine you deploy to, so anything architecture-specific stays hidden until the artifact reaches production. Jobs that touch no compiled output, such as linting or documentation checks, can stay on x64.

What usually breaks first when a pipeline that built on x64 starts running on ARM64?

Four things, in roughly this order. Lockfiles that recorded platform-specific optional dependencies for x64 only, base images and service containers with no linux/arm64 variant, compiler and build flags such as -march or x86 intrinsics guarded by the wrong condition, and scripts with a hardcoded /home/runner/work path. The last one is specific to the Ubuntu 24.04 ARM64 image, which sets the work dir to /runner/_work.

Which WarpBuild labels match a Graviton deployment, and what do they cost?

warp-ubuntu-latest-arm64-2x through warp-ubuntu-latest-arm64-32x, running Ubuntu 24.04 at $0.003 to $0.048 per minute, with the same five sizes on Ubuntu 26.04 under warp-ubuntu-2604-arm64 labels. Pick the size by the parallelism your build actually uses rather than by matching the production instance shape. The Linux ARM64 runner catalog lists every row with storage and aliases.

Can I keep Android emulator or KVM jobs while the rest of the pipeline moves to ARM64?

Yes. Nested virtualization and /dev/kvm access are available on Linux x64 runners through the nested-virtualization.enabled=true label and are unavailable on ARM64 runners today, so those jobs keep an x64 label while the compile and test jobs move. Language-specific wiring for the jobs that do move is covered in the Go on GitHub Actions solution page.

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.