# A CodeBuild GitHub Actions runner alternative, and the cutover

> A CodeBuild GitHub Actions runner alternative means undoing a project, a webhook and a composite label: the label map, the buildspec trap, billing.

Source: https://latchkey.dev/learn/runners/migrate-from-aws-codebuild-runners  
Updated: 2026-09-21

Looking for a CodeBuild GitHub Actions runner alternative usually means one of two things: the label has become unreadable, or the AWS account has become the thing you maintain instead of the pipeline. Either way the exit is smaller than the entry was, because most of what CodeBuild required is infrastructure you are about to stop owning rather than workflow you have to rewrite.

CodeBuild can host GitHub Actions jobs, and the way it does it is unusual enough to be worth restating before you leave. You create a project whose type is Runner, connect it to GitHub, and a webhook delivers a `WORKFLOW_JOB_QUEUED` event for each queued job. AWS documents what happens next: for each job in the workflow, CodeBuild starts a build to run an ephemeral GitHub Actions runner, that runner executes a single workflow job, and the runner and the associated build process are immediately terminated.

So four things exist on the AWS side that are about to stop existing: a source credential, a project, a webhook, and a label that names all of it. This page is what each of those turns into on a managed runner, in the order the migration actually hits them.

## The label is a composite, and one part must match exactly

A CodeBuild job is not selected by a size or an image the way every other runner label works. AWS documents the form as `runs-on: codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }}`, which is three values joined: the project to route to, and the two context values that let CodeBuild map a build to a workflow run and stop it when the run is cancelled.

The project name is the part with no tolerance. AWS is explicit about the failure: make sure that your project name matches the name of the project you created, and if it does not match, CodeBuild will not process the webhook and the GitHub Actions workflow might hang. That is not an error message you get back, it is a job that queues forever, which reads exactly like a runner capacity problem until somebody compares two strings.

```The documented label format from the AWS CodeBuild user guide, with the sample project name AWS uses in its own example
runs-on: codebuild-myProject-${{ github.run_id }}-${{ github.run_attempt }}
```

> One more behavior to know before you start deleting labels: AWS documents that labels outside the set CodeBuild supports are ignored for the purpose of overriding attributes of the build but will not fail the webhook request, and names `testLabel` as an example. So a half-finished migration does not announce itself; it silently uses project defaults.

## What the override labels were doing

Beyond the project name, CodeBuild lets a workflow override the project settings from the label itself, which is how a single project serves jobs of different shapes. Each of these has a plain equivalent on a managed runner, and in every case the equivalent is a different label rather than a different line of YAML, so the migration is mechanical.

The one that is not mechanical is `fleet:`, which points at reserved capacity. If you moved to reserved fleets for start-up latency, you were buying warm machines, and that is a property of the destination rather than something the label carries. Managed vendors answer it with their own pool rather than with a name you supply.

| CodeBuild label suffix | What it selects | On a managed runner |
| --- | --- | --- |
| `image:arm-3.0` | A curated environment image and its architecture | The runner label itself, or a `container:` on the job |
| `instance-size:small` | The compute size, overriding the project default | The size is in the label, for example `latchkey-medium` |
| `fleet:myFleet` | A reserved-capacity fleet | No equivalent; warm capacity is the vendor's pool |
| `buildspec-override:true` | Run buildspec commands as well as the runner | No equivalent, and nothing to replace: the workflow runs the steps |
| `organization-registration-name:myOrg` | Register the runner at organization level | Handled by the vendor's GitHub App installation scope |
| `registration-group-id:3` | A specific runner group id | The vendor's own runner group, if it exposes one |

> Every suffix and example value is quoted from the AWS CodeBuild user guide page on configuring the runner, read 2026-09-21. AWS also lists the supported environment types, which include `linux`, `linux-ec2`, `arm`, `arm-ec2`, `ubuntu`, `windows`, `windows-ec2`, `linux-lambda` and `arm-lambda`, so a migration off CodeBuild has to account for anything that is not plain x86_64 Linux.

## The buildspec override is the one that hides work

By default CodeBuild ignores your buildspec entirely and replaces it with commands that set up the ephemeral runner. Teams that needed setup before the runner started added `buildspec-override:true`, and that path carries four documented limitations which are worth reading as a list of things you were already living with.

AWS states them plainly: buildspec commands do not run during the `BUILD` phase, because the runner itself runs there; no primary or secondary sources are downloaded during `DOWNLOAD_SOURCE`, so only the buildspec file arrives; a failure in `PRE_BUILD` or `INSTALL` means the runner never starts and the workflow job has to be cancelled by hand; and the runner token is fetched during `DOWNLOAD_SOURCE` with an expiration time of one hour, so a long install can expire the token before the runner starts.

On any ordinary runner all four of these stop being true, because there is no second phase model: the workflow steps are the build. Whatever was in the overridden buildspec becomes an ordinary first step or, better, moves into a container image so it is not paid for per job.

## The billing shape changes twice

CodeBuild bills differently depending on which compute you chose, and both halves change on the way out. AWS documents EC2 compute as calculated in minutes and rounded up to the nearest minute, and Lambda compute as calculated in seconds and rounded up to the nearest second. Its own worked example on the pricing page prices 400 on-demand `general1.small` build minutes at $0.005 a minute, and its Lambda example prices `lambda.arm.1GB` at $0.00001 a second.

The free tier also disappears, and it is not nothing on a small repository: AWS publishes 100 total build minutes a month on `general1.small` or `arm1.small` using on-demand EC2, and 6,000 total build seconds a month using on-demand Lambda. A managed vendor answers that with its own allowance, which differs by vendor rather than by compute type, so compare allowance against allowance rather than assuming the free tier carried.

| Property | CodeBuild, on-demand EC2 | CodeBuild, on-demand Lambda | A per-minute managed runner |
| --- | --- | --- | --- |
| Rounding | Up to the nearest minute | Up to the nearest second | Up to the nearest minute, per job |
| Published example rate | $0.005 a minute at `general1.small` | $0.00001 a second at `lambda.arm.1GB` | $0.0025 a minute at 2 vCPU with 8 GB |
| Free allowance | 100 build minutes a month | 6,000 build seconds a month | By vendor plan, not by compute type |
| What you operate | The project, webhook and connection | The project, webhook and connection | Nothing |

> CodeBuild rates and free-tier terms read from the AWS CodeBuild pricing page on 2026-09-21; AWS does not state a region on that page and its prices vary by region, so treat the rate as an example rather than as your rate. The managed column uses Latchkey's published list rate on the same date, for a shape with four times the memory of `general1.small`, so it is a shape comparison as much as a price one.

## The pre-flight checklist

Seven items, and the first two are greps rather than decisions. Do them before you change anything, because the label is a composite and a partial migration fails quietly rather than loudly.

- **Labels.** Grep the workflow directory for `runs-on:.*codebuild-` to find every job, including the ones whose overrides put them on arm or on Lambda compute.
- **Overrides.** For each hit, note the `image:`, `instance-size:` and `fleet:` suffixes. Those are the size and architecture requirements you are matching on the other side.
- **Buildspec.** Grep for `buildspec-override`. Anything that relied on it becomes an ordinary workflow step, or an image, and the four phase limitations stop applying.
- **Non-Linux and non-x64.** Windows, arm and Lambda compute types all have to land somewhere. Several managed vendors are Linux x86_64 only, which usually makes this a split rather than a switch.
- **Secrets and roles.** Anything the build read from the CodeBuild service role has to become an OIDC role assumed from the workflow, or a repository secret. This is the item that takes real time.
- **The webhook and the project.** Leave both in place until the last job has moved. Deleting the project first turns every unmigrated job into a hang rather than an error.
- **Cache.** CodeBuild local and S3 caching does not travel. Add `actions/cache` steps with real keys before the first run, or the first run is a cold one you then have to explain.

## The before and after

The workflow change itself is one line per job plus the cache work. What is missing from the diff is the part that makes this worth doing: the project, the webhook, the source credential and the service role all stop existing, and so does the pager rotation that covers them.

```.github/workflows/ci.yml
jobs:
  build:
    # before
    # runs-on:
    #   - codebuild-myProject-${{ github.run_id }}-${{ github.run_attempt }}
    #     image:arm-3.0
    #     instance-size:small
    # after
    runs-on: latchkey-small
    steps:
      - uses: actions/checkout@v5
      - uses: actions/cache@v4
        with:
          path: ~/.m2/repository
          key: maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: |
            maven-
      - run: mvn -B verify
```

> The `image:arm-3.0` line in the before block is the one to look at twice. An arm environment on CodeBuild has to land on an arm runner somewhere, and a destination that is Linux x86_64 only cannot take it at any price. Check that before you price anything.

## When staying on CodeBuild is right

Three cases, and they are all about the AWS account rather than about the runner. If your builds need resources reachable only from inside a VPC, CodeBuild is already in the right place and a managed runner is not. If you have committed AWS spend that CI can be charged against, moving those minutes to a third party throws the commitment away. And if the reserved-capacity fleets are what fixed your start-up latency, you have already paid for warm machines and a per-minute vendor is selling you the same thing again.

What leaving buys, if none of those holds, is the removal of four AWS objects and the composite label that names them. The workflow YAML gets shorter and more legible, a hang caused by a project rename becomes impossible, and the per-job billing stops depending on which compute type somebody selected in a console two years ago.

## Why no recorded run backs this page

Reproducing this would mean standing up an AWS account, a source credential, a Runner-type project and an organization webhook, then deliberately renaming the project to observe a job that hangs rather than fails. The observable result of the most interesting failure here is the absence of anything: no log, no error, a job queued forever. A harness records logs, so it would have nothing to record.

Everything above is therefore read out of AWS's own user guide and pricing page on 2026-09-21, with the label format and the four buildspec limitations quoted rather than summarized, because those are the parts a migration gets wrong. The prices are AWS's published examples and carry no region, which is a real limitation of the source rather than of the reading.

## FAQ

### What is the runs-on label for a CodeBuild-hosted GitHub Actions runner?

AWS documents it as `codebuild-<project-name>-${{ github.run_id }}-${{ github.run_attempt }}`. The project name must match the Runner-type project exactly; AWS states that if it does not match, CodeBuild will not process the webhook and the workflow might hang. The run id and run attempt let CodeBuild map builds to workflow runs and stop a build when the run is cancelled.

### Why is my CodeBuild-backed job queued forever with no error?

Usually a project name that no longer matches the label, which AWS documents as producing exactly that: an unprocessed webhook and a workflow that might hang. Unsupported label suffixes make it worse, because AWS ignores them rather than failing the request, so a typo in an override never surfaces. Compare the label string with the project name character by character before looking anywhere else.

### Does my buildspec run on a CodeBuild GitHub Actions runner?

Not unless you add `buildspec-override:true`. Without it, CodeBuild replaces the buildspec with the commands that set up the ephemeral runner. With it, four documented limits apply: no buildspec commands in the `BUILD` phase, no sources downloaded, a failed `PRE_BUILD` or `INSTALL` means cancelling the workflow job by hand, and a runner token that expires one hour after `DOWNLOAD_SOURCE`.

### How does CodeBuild billing compare with a per-minute managed runner?

CodeBuild EC2 compute is calculated in minutes and rounded up to the nearest minute, so it rounds like a managed runner does; its Lambda compute is calculated in seconds and rounded up to the nearest second, which nothing in the managed market matches. The free tier also differs: AWS publishes 100 build minutes a month on `general1.small` or `arm1.small`, and 6,000 Lambda build seconds.

## References

- [AWS CodeBuild user guide: configure a CodeBuild-hosted GitHub Actions runner (verified 2026-09-21)](https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner.html)
- [AWS CodeBuild user guide: compute images supported with the GitHub Actions runner (verified 2026-09-21)](https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners-update-yaml.images.html)
- [AWS CodeBuild pricing: on-demand EC2 and Lambda examples, rounding and free tier (verified 2026-09-21)](https://aws.amazon.com/codebuild/pricing/)
- [GitHub Docs: dependency caching reference, scope and eviction (verified 2026-09-21)](https://docs.github.com/en/actions/reference/dependency-caching-reference)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
