Self-hosted service
uraimo/run-on-arch-action avatar
uraimo/run-on-arch-action

run-on-arch-action puts ARM, s390x and ppc64le builds into ordinary GitHub Actions workflows

A Github Action that executes jobs/commands on non-x86 cpu architectures (ARMv6, ARMv7, aarch64, s390x, ppc64le, riscv64) via QEMU

752 stars168 forksShellBSD-3-Clause

At a glance

What is it?
This GitHub Action executes a workflow step inside a QEMU emulated container for a chosen CPU architecture and Linux distribution, so a single hosted runner can produce armv7, aarch64, s390x or ppc64le output. The caching and image features are the strong parts, and the public image cache is the part to think about before you use it.
Who is it for?
Adopt run-on-arch-action if you must ship a binary for a non x86 target from a hosted runner and cannot rent native ARM or s390x hardware. Do not adopt it for compile speed on platforms where native runners exist, since every command runs under emulation.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 27 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Hosted runners are x86, and the work still has to be built for ARM

GitHub Actions gives you a Linux runner, and that runner is an x86 machine. If your product is a Raspberry Pi image, an embedded device, a mainframe binary or a PowerPC appliance, you now face a choice: rent native hardware for the build, or emulate the target architecture. run-on-arch-action takes the second route. As the README puts it, it is a GitHub Action that executes commands on non x86 CPU architecture through QEMU, and the architectures it names are armv6, armv7, aarch64, loongarch64, s390x and ppc64le.

The audience is therefore narrow and identifiable: maintainers of multi-arch packages who want a single workflow file to produce and test binaries for several targets, teams that cannot justify dedicated build machines, and anyone who has resorted to a hacky cross-compile step that only covers one architecture. The action does not try to be a cross compiler. It starts a container for the target architecture and runs your shell commands inside it, which means your build tooling behaves the way it would on the real device, at the cost of running under emulation.

That framing matters for expectations. Emulation is a way to get correct architecture specific output from a general purpose CI system. It is not a performance strategy, and the README does not make any claim about how fast the emulated commands run.

Three required inputs decide which container gets started

The action's contract is small and explicit. Three input parameters are required. The first is `arch`, the CPU architecture, and the values the README lists are `armv6`, `armv7`, `aarch64`, `loongarch64`, `riscv64`, `s390x` and `ppc64le`. The second is `distro`, the Linux distribution name, with `ubuntu24.04`, `ubuntu22.04`, `ubuntu20.04`, `bookworm`, `bullseye`, `buster`, `stretch`, `fedora_latest`, `alpine_latest` and `archarm_latest` given as the accepted values. The third is `run`, the shell commands to execute in the container.

Behind those parameters sits a directory of Dockerfiles in the repository, one per architecture and distribution pairing, and the action selects the matching image and starts it. The optional `base_image` input lets you bypass that selection entirely: set `arch` and `distro` to `none` and the action uses your image reference directly in the FROM clause of the internal Docker container, so you do not need a Dockerfile for that combination. The README gives a concrete form of this, using a platform prefix when the image requires it.

Several optional parameters shape what runs and how the result is reused. `shell` chooses the shell inside the container, defaulting to `/bin/sh` on Alpine and `/bin/bash` for other distributions. `env` propagates environment variables and has to be written as YAML beginning with a pipe character. `setup` runs on the host before the container starts, and `dockerRunArgs` passes extra arguments to `docker run`, such as volume mappings.

A first armv7 job on an ubuntu-24.04 host

The minimal usage is short. The host runner has to be Linux, you check out your code, and you add one step. This is the opening of the README's basic example, trimmed to the required inputs:

yaml
on: [push, pull_request]

jobs:
  armv7_job:
    # The host should always be Linux
    runs-on: ubuntu-24.04
    name: Build on ubuntu-24.04 armv7
    steps:
      - uses: actions/checkout@v4
      - uses: uraimo/run-on-arch-action@v3
        name: Run commands
        id: runcmd
        with:
          arch: armv7
          distro: ubuntu24.04

What you should see on a healthy run is the output of the commands in the `run` block, in the container, under the job name Build on ubuntu-24.04 armv7. The same example then shows the `run` block setting an output parameter for later steps:

yaml
          run: |
            uname -a
            printf 'uname=%s\n' "$(uname -a)" >> "$GITHUB_OUTPUT"

The `uname -a` line is the quickest confirmation that the emulation is real, since it reports the emulated machine rather than the host. The second line appends the same value to `$GITHUB_OUTPUT` so a later step can read it through `steps.runcmd.outputs.uname`, which the README demonstrates. Note the reference to `@v3`: the action is versioned by major tag, and the version 3 line is the one the README asks you to use.

The install step becomes part of a publicly cached image layer

The optional `install` input runs shell commands inside the container as part of `docker build`, and its purpose is speed. When `githubToken` is also supplied, the resulting image layer is cached, so dependencies are installed once and reused by later builds. The README's advanced example dispatches on the distribution to install git:

yaml
          install: |
            case "${{ matrix.distro }}" in
              ubuntu*|jessie|stretch|buster|bullseye)
                apt-get update -q -y
                apt-get install -q -y git
                ;;
              fedora*)
                dnf -y update
                dnf -y install git which
                ;;
              alpine*)
                apk update
                apk add git
                ;;
            esac

The same README text carries the warning that matters most in this action. The image layer will be publicly available in your project's GitHub Package Registry, so you have to make sure the resulting image does not have any secrets cached in logs or state. The advanced example repeats it in stronger terms: the README insists that no secrets are present in the container state or logs.

That is a real operational constraint, not a formality. Anything your install step downloads, writes or leaves in a credential helper ends up in a layer that is pushed to a registry and reused by later builds. The `githubToken` parameter is what enables the caching, and the README marks it as not required but highly recommended for speed. The trade-off is speed against a public artefact you do not fully control.

Passing artifacts back out through a mounted volume

Getting a compiled binary out of the container is the part that catches newcomers. The action gives you three parameters for it. `setup` runs on the host before the container starts, which is where you create the directory you intend to mount. `dockerRunArgs` carries the extra `docker run` arguments, in this case the volume mapping. `env` passes values into the container so the command inside knows the file name to write.

The README's advanced example wires this together by creating a directory on the host, mounting it at `/artifacts` inside the container, and then copying the built binary to that path with the environment variable expanded:

yaml
          dockerRunArgs: |
            --volume "${PWD}/artifacts:/artifacts"

The consequence is worth stating plainly. A file written to `/artifacts` in the container is a file under the artifacts directory on the host, and the host path is what a later step can upload as a workflow artefact. The advanced example ends with a step that lists that directory, which is the quickest way to confirm the plumbing worked. The alternative, copying results out with the job's own output mechanism, is more awkward because the build happens inside an emulated container rather than on the runner.

One more knob in the same area is `shell`. Setting it to `/bin/sh` on an Alpine base changes what your script can assume about available interpreters, so scripts written for a Debian derived base and then moved to Alpine will break on bashisms that the `run` block would otherwise have accepted.

Version 3 exists because of segmentation faults, and the versioning is deliberate

The README opens with a note about a new major release addressing sporadic segmentation faults. The release 3.0.0 addresses sporadic segmentation faults noticed with recent kernel releases, and the instruction is to update your action to this new major release, for example `@v3`, if you start noticing random or systematic CI failures.

That is worth taking at face value. Emulated execution depends on the interaction between QEMU, the container runtime and the host kernel, so a kernel update on the runner side can surface as crashes inside your build that look like failures in your own code. The major version bump is the mechanism for stepping past that: the action is referenced by a major tag, and the newer major line contains the fixes.

The release history backs up the maintenance claim. The repository is not archived, and the last push was on 2026-09-26, the same day as the v3.2.1 tag, with v3.2.0 published on 2026-08-18 and v3.1.0 on 2026-04-26. The licence is BSD-3-Clause, with the licence text in the LICENSE file at the top level. The implementation is mostly Shell, with the entry point in action.yml and the source under src/, alongside a package.json whose dependency list is short: `@actions/core`, `@actions/exec`, `shlex` and `yaml`.

One inconsistency is visible in the layout. The package.json still carries version 2.7.0 while the published tags are at 3.2.1, so the manifest version is not a useful signal of which release you are running. Pin to the tag or the major tag, and read the release list rather than the manifest.

Where this action is the wrong tool

There are clear cases where the emulation route is a mistake. The first is raw speed. If a build is CPU bound and you need it in minutes rather than hours, running the compiler under QEMU on an x86 host is the wrong approach, and the action offers no acceleration or caching of your build tree beyond the container image layer. Native runners for ARM exist in the hosted CI world, and for a matrix of a dozen targets the difference compounds.

The second is coverage. The set of usable combinations is bounded by the Dockerfiles present in the repository. The architecture and distribution values are enumerated in the README, and the README's supported platforms table is where you would check whether your specific pairing exists. If your target is a distribution combination that is not there, the `base_image` escape hatch is the workaround, and the README records a known limitation there: only one base_image configuration for each workflow if you use GitHub images caching.

The third is anything sensitive. Because the image layer is cached in a package registry, workflows that need credentials during the install step are a poor fit unless you already control what is published. And the third parameter, `run`, executes arbitrary shell commands in a container that emulates another architecture, which is fine for building software and not the place for production workloads.

Editorial conclusion

Adopt run-on-arch-action if you must ship a binary for a non x86 target from a hosted runner and cannot rent native ARM or s390x hardware. Do not adopt it for compile speed on platforms where native runners exist, since every command runs under emulation. Before wiring it into a release pipeline, read the install and githubToken sections of the action inputs again, because anything installed in that step is baked into an image layer cached in your project package registry, and the base_image option allows only one configuration per workflow when that caching is enabled.

Frequently asked questions

What is the run-on-arch-action GitHub Action used for?

It executes shell commands inside a QEMU emulated container for a non x86 architecture, so a GitHub Actions job running on a Linux x86 runner can build or test software for targets such as armv7, aarch64, s390x, ppc64le and riscv64.

Which parameters does run-on-arch-action require?

Three are required: `arch` for the CPU architecture, `distro` for the Linux distribution, and `run` for the shell commands to execute in the container. Optional inputs include `githubToken`, `env`, `shell`, `dockerRunArgs`, `setup`, `install` and `base_image`.

Is it safe to use the install step with GitHub image caching?

The README warns that the image layer will be publicly available in your project's GitHub Package Registry, so you must make sure the resulting image holds no secrets in logs or state. The `install` step runs as part of `docker build`, so anything it installs becomes part of that cached layer.

How do I run a job on a custom base image with run-on-arch-action?

Set `arch` and `distro` to `none` and supply `base_image`, which the README explains lets you choose the image used in the FROM clause of the internal Docker container. You can prefix the platform identifier, for example `--platform=linux/armv7 arm32v7/debian:buster`. The known limitation is that only one base_image configuration is allowed per workflow when GitHub images caching is used.

How do I get build artifacts out of the run-on-arch-action container?

Create the directory on the host in the `setup` step, then pass the mount through `dockerRunArgs`, for example `--volume "${PWD}/artifacts:/artifacts"`, and write the result to that path from the `run` step. Items placed in /artifacts in the container are in the host artifacts directory.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. README
  4. Releases
  5. uraimo/run-on-arch-action on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/uraimo-run-on-arch-action.svg)](https://hysenlabs.com/projects/uraimo-run-on-arch-action)

Was this useful?