A CodeBuild GitHub Actions runner alternative, and the cutover
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.
runs-on: codebuild-myProject-${{ github.run_id }}-${{ github.run_attempt }}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 |
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 |
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:andfleet: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/cachesteps 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.
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 verifyWhen 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.
Frequently asked questions
What is the runs-on label for a CodeBuild-hosted GitHub Actions runner?
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?
Does my buildspec run on a CodeBuild GitHub Actions runner?
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?
general1.small or arm1.small, and 6,000 Lambda build seconds.Related guides
References
- AWS CodeBuild user guide: configure a CodeBuild-hosted GitHub Actions runner (verified 2026-09-21)
- AWS CodeBuild user guide: compute images supported with the GitHub Actions runner (verified 2026-09-21)
- AWS CodeBuild pricing: on-demand EC2 and Lambda examples, rounding and free tier (verified 2026-09-21)
- GitHub Docs: dependency caching reference, scope and eviction (verified 2026-09-21)
- GitHub Actions documentation