How We Set Up Multi-Architecture CI for an Open Source Project Using GitHub Actions (And Why You Should Too)

1 comment
(GitHub and Open Source) - Running tests only on x86? You’re shipping bugs to ARM users. Here’s how we built a multi-architecture CI pipeline with GitHub Actions, cut build times by 40%, and kept our open source contributors happy—all with the help of a remote team in Vietnam.

How We Set Up Multi-Architecture CI for an Open Source Project Using GitHub Actions (And Why You Should Too)

If your open source project only tests on x86, you’re lying to yourself.

You think you support ARM. Maybe you have a note in the README saying “works on Apple Silicon too.” But when a user on a Raspberry Pi opens an issue about a segfault in your C extension, you have no data. You can’t reproduce it. You’re blind.

Why Smart Developers Are Ditching IDEs for Terminal-Based AI Development Tools

Why Smart Developers Are Ditching IDEs for Terminal-Based AI Development Tools

TL;DR: Terminal-based AI development tools are transforming how developers code, debug, and ship software. By combining command-line speed… ...

We were blind too. Until we built a proper multi-architecture CI pipeline using GitHub Actions. It wasn’t easy, but it cut our ARM-related bug reports by 82% and saved our maintainers from burnout.

Here’s exactly how we did it.

The Pull Request Playbook: What I Learned from Reviewing 1,000+ PRs with a Remote Vietnamese Team

The Pull Request Playbook: What I Learned from Reviewing 1,000+ PRs with a Remote Vietnamese Team

The Pull Request Playbook: What I Learned from Reviewing 1,000+ PRs with a Remote Vietnamese Team Let me… ...

Why Multi-Architecture Matters for Open Source

The world isn’t just x86 anymore.

  • Apple Silicon (M1/M2/M3) – almost every new Mac.
  • ARM64 servers – AWS Graviton, Azure Ampere, GCP Tau.
  • Edge devices – Raspberry Pis, Rockchip boards, smart TVs.

Your users run on all of them. If you only test on GitHub’s default `ubuntu-latest` (x86), you’re shipping untested code to a huge portion of your audience.

I learned this the hard way. A library we maintained for image processing worked flawlessly on our dev machines. A user on an M2 MacBook reported a 5x performance regression. We couldn’t replicate it. Turns out, we had a subtle alignment issue that only appeared on ARM. That issue sat open for three weeks while we fumbled with manual testing.

Never again.

The Two Approaches: Emulation vs Native

You have two ways to run ARM CI on GitHub Actions:

  1. QEMU emulation – run ARM containers on x86 runners.
  2. Native ARM runners – use the `ubuntu-24.04-arm` runner (GitHub’s newest addition).

I’ll be blunt: QEMU is slow. Really slow. Running a full test suite under emulation can take 3x to 5x longer than native. For a project with 500+ tests, that’s the difference between a 10-minute CI and a 45-minute one.

But native ARM runners weren’t available until late 2023. Many projects still default to emulation because they don’t know better.

Here’s our recommendation: Always prefer native runners when possible. If your tests are containerized, you can often use the same Docker image on both architectures.

Our Setup: A Single Matrix, Two Architectures

We maintain a Python library with C extensions. We test on Python 3.10–3.12 and need both x86 and ARM64 coverage.

We use a GitHub Actions matrix strategy with two operating systems: `ubuntu-24.04` (x86) and `ubuntu-24.04-arm` (ARM64).

yaml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-24.04, ubuntu-24.04-arm]
        python-version: ["3.10", "3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"

      - name: Run tests
        run: pytest --cov --junitxml=report.xml

      - name: Upload coverage
        if: always()
        uses: codecov/codecov-action@v4

That’s it. Six parallel jobs – three Python versions on two architectures. The whole suite takes around 12 minutes on native ARM runners. Under QEMU it would be over 40.

But there’s a catch: native ARM runners are limited. GitHub’s free tier gives you only a few concurrent ARM jobs. For open source projects with high PR throughput, you might hit queue delays.

We solved this by prioritizing critical paths. We run the full matrix only on the default branch and release tags. For pull requests, we run a reduced matrix (only Python 3.12 on ARM) and run the full x86 matrix. That way we catch ARM regressions fast without slowing down every PR.

The Hidden Cost: Cross-Compilation

If your project compiles native extensions (C, C++, Rust), you need to handle cross-compilation. It’s a headache.

We use `cibuildwheel` for our Python wheels. It handles cross-compilation natively when run on the appropriate runner. But you must set the `CIBW_ARCHS` environment variable.

yaml
- name: Build wheels
  env:
    CIBW_ARCHS: ${{ matrix.os == 'ubuntu-24.04' && 'x86_64' || 'aarch64' }}
  run: cibuildwheel --output-dir wheelhouse

We learned the hard way that silently falling back to x86 compilation on an ARM runner produces a broken wheel. The wheel will claim to be `aarch64` but contain x86 code. Our users got segfaults at import time.

Why We Chose Native Runners Over Self-Hosted

Self-hosted ARM runners sound appealing. You can spin up a cheap ARM server (like a $10/month Oracle Cloud instance) and connect it to GitHub.

We tried it. Three problems:

  • Maintenance overhead. The runner goes down, you don’t notice until CI hangs for 2 hours.
  • Security. Self-hosted runners with `pull_request` events can run untrusted code. You need to isolate them.
  • Scale. One instance can’t handle multiple concurrent jobs. You need a fleet.

For a small open source project, self-hosted might work. For anything with 10+ contributors, stick with GitHub-hosted ARM runners.

But honestly, if you’re on a tight budget, consider using a remote team to set up and maintain your self-hosted infrastructure. That’s what we did. Our team in Ho Chi Minh City managed a cluster of ARM instances behind a GitHub Actions runner group. It worked, but we eventually moved to GitHub-native when it became available.

Real Metrics: Before and After

Metric Before (x86 only) After (multi-arch CI)
ARM bug reports per month 12–15 2–3
Time to fix ARM issues 4–5 days 6–12 hours
PR merge velocity

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.