Can ARM64 and x64 Jobs Run in One Workflow?
Yes. runs-on is set per job, so one matrix leg can run on warp-ubuntu-latest-arm64-8x at $0.012 per minute and another on an x64 label in the same run.
Last verified:
Yes. runs-on is a per-job field, so one workflow file can place a job on an ARM64 label and another job on an x64 label in the same run, and a matrix is the normal way to write that (GitHub workflow syntax reference). WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, so both legs resolve against one catalog: warp-ubuntu-latest-arm64-8x at $0.012 per minute and warp-ubuntu-latest-x64-8x at $0.016 per minute (cloud runners documentation, checked on 2026-08-13).
Answer
A workflow is a set of jobs, and every job carries its own runs-on. A matrix expands into one job per entry, each entry can carry its own runner label, and the expanded jobs are scheduled independently. Nothing in GitHub Actions ties the architectures inside a workflow to a single value.
The pattern below builds a container image on both architectures in one run. Each leg pushes an untagged image by digest, and a final job binds the two digests to one tag.
name: build-both-architectures
on:
push:
branches: [main]
pull_request:
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: warp-ubuntu-latest-x64-8x
- arch: arm64
platform: linux/arm64
runner: warp-ubuntu-latest-arm64-8x
runs-on: ${{ matrix.runner }}
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 }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
- name: Export the digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1fail-fast: false keeps the x64 leg running when the ARM64 leg fails, which is what you want while you work through architecture-specific breakage. Each entry maps to a label in the cloud runners documentation, so the ARM64 leg executes on aarch64 hardware and the x64 leg on x86-64 hardware, with no translation layer on either side.
Per-minute rates for every label are on the pricing page.
Detail
Merging the two outputs into one image reference
The two jobs run on separate machines and share no filesystem, so the join happens through the registry. Each leg pushed a manifest by digest and wrote the digest name into an artifact. A third job downloads both artifacts and calls docker buildx imagetools create, which writes one manifest list pointing at both digests. The Docker multi-platform GitHub Actions guide documents this shape.
merge:
needs: build
runs-on: warp-ubuntu-latest-arm64-2x
steps:
- uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create the manifest list
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t ghcr.io/${{ github.repository }}:${{ github.sha }} \
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
- name: Inspect the result
run: docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ github.sha }}After that job, ghcr.io/OWNER/REPO:SHA is one tag that resolves to the aarch64 layers on ARM64 hosts and the x86-64 layers everywhere else. The merge job does no building, so a 2 vCPU runner is enough. The manifest list answer covers the digest bookkeeping in more detail.
Pricing a two-architecture pipeline
Every rate below comes from the cloud runners documentation, checked on 2026-08-13.
| Shape | Linux x64 label | Per minute | Linux ARM64 label | Per minute |
|---|---|---|---|---|
| 2 vCPU, 8 GB | warp-ubuntu-latest-x64-2x | $0.004 | warp-ubuntu-latest-arm64-2x | $0.003 |
| 4 vCPU, 16 GB | warp-ubuntu-latest-x64-4x | $0.008 | warp-ubuntu-latest-arm64-4x | $0.006 |
| 8 vCPU, 32 GB | warp-ubuntu-latest-x64-8x | $0.016 | warp-ubuntu-latest-arm64-8x | $0.012 |
| 16 vCPU, 64 GB | warp-ubuntu-latest-x64-16x | $0.032 | warp-ubuntu-latest-arm64-16x | $0.024 |
| 32 vCPU, 128 GB | warp-ubuntu-latest-x64-32x | $0.064 | warp-ubuntu-latest-arm64-32x | $0.048 |
The two legs bill separately, at the rate of the label each one lands on.
Worked model. A repository runs the workflow above 400 times a month. Each build leg takes 7 minutes on the 8 vCPU size and the merge job takes 1 minute on the 2 vCPU ARM64 size.
- x64 leg: 400 x 7 x $0.016 = $44.80 per month.
- ARM64 leg: 400 x 7 x $0.012 = $33.60 per month.
- Merge job: 400 x 1 x $0.003 = $1.20 per month.
- Total: $79.60 per month, and the $10 in signup credits covers the first $10, so month one lands at $69.60.
The same 2,800 minutes per leg against GitHub-hosted larger runners, using the list prices in the GitHub Actions billing reference checked on 2026-08-13:
- warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner (8 vCPU, 32 GB): 27 percent lower list price. That leg is $61.60 a month at the GitHub list price against $44.80.
- warp-ubuntu-latest-arm64-8x (8 vCPU, 32 GB) costs $0.012 per minute against $0.014 per minute for the 8-core Linux ARM64 larger runner (8 vCPU, 32 GB): 14 percent lower list price. That leg is $39.20 a month at the GitHub list price against $33.60.
The two build legs together are $78.40 a month here against $100.80 at the GitHub list prices for the same shapes and the same minutes. Shapes come from the GitHub-hosted runner specifications.
What differs between the two legs
Four documented differences account for most first-run failures on the ARM64 leg (cloud runners documentation).
The work directory. The arm64 images for Ubuntu 24.04 set the work dir to /runner/_work, which differs from GitHub's /home/runner/work/ for the same instance. Any step with a hardcoded /home/runner/work/... path breaks on the ARM64 leg while it passes on the x64 leg. Use ${{ github.workspace }} in YAML and $GITHUB_WORKSPACE in shell steps, and check Docker -v mounts and cache path globs for literals.
Ubuntu 22.04 has no ARM64 image. The arm64 images for ubuntu-22.04 were deprecated on March 31, 2025, so a matrix pinned to 22.04 needs the ARM64 entry moved to a warp-ubuntu-2404-arm64 or warp-ubuntu-2604-arm64 label. The x64 entry can stay on 22.04.
Nested virtualization stays on x86-64. Workloads that need /dev/kvm, including Android emulator jobs, are not supported on ARM64 runners. Route those steps to the x64 entry with the nested-virtualization.enabled=true dynamic label.
Architecture-specific downloads. Steps that fetch a release binary by URL or pull a single-arch container tag need an aarch64 variant on the ARM64 leg. Drive the choice from the matrix value rather than hardcoding one architecture, for example curl -L .../tool-linux-${{ matrix.arch }}.tar.gz.
Ten ARM64 labels are available across Ubuntu 24.04 and Ubuntu 26.04, from 2 vCPU to 32 vCPU, listed in the Linux ARM64 runner catalog.
The single-job alternative
A remote Docker builder changes the shape of this workflow. Instead of two matrix legs, one job sets platforms: linux/amd64,linux/arm64 and sends the build to builder virtual machines with a persistent layer cache, and buildx pushes the manifest list directly, so no merge job is needed (Docker builders documentation).
Enable both architectures on the builder profile before setting platforms. Each architecture runs on a separate builder instance, producing one session per architecture, and each session is billed independently at the profile rate. The 16 vCPU, 32 GB, 100GB disk profile is $0.06 per minute per session, and arm64 and multi-arch profiles cap at 64 vCPU. The multi-platform Docker build guide walks through both routes and when each one pays off.
Related Questions
Do the ARM64 job and the x64 job share a filesystem?
No. Each job in a matrix gets its own ephemeral machine, so data moves between the legs through artifacts, job outputs, or a registry. For image builds, each leg pushes by digest and a later job merges the digests, as covered in the manifest list answer.
Do I need a separate merge job to get one image tag?
On the native runner route, yes. Each architecture pushes an untagged manifest by digest, and a final job runs docker buildx imagetools create to bind both digests to one tag. On a remote Docker builder with both architectures enabled, buildx emits the manifest list itself (Docker builders documentation).
Can macOS and Windows jobs sit in the same matrix as the ARM64 and x64 legs?
Yes. runs-on is per job, so a matrix entry can name a macOS ARM64 label or a Windows label alongside the Linux labels. The Windows catalog is x86-64 only, so a Windows entry stays on a warp-windows label (cloud runners documentation).
Which ARM64 sizes can the ARM64 leg use?
Any of the ten labels in the Linux ARM64 runner catalog, from warp-ubuntu-latest-arm64-2x at $0.003 per minute to warp-ubuntu-latest-arm64-32x at $0.048 per minute. The two legs do not have to match in size, so a compile-heavy x64 leg can take 16 vCPU while the ARM64 leg takes 8 vCPU.
Pick the labels for each leg from the Linux ARM64 runner catalog, price the pipeline against your own job minutes on the pricing page, and wire up the image merge with the multi-platform Docker build guide.
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.