Route CI to your self-hosted runner when it's online, and to a fallback when it
isn't. Fail-open by design: any API error, bad token, or rate limit picks the
fallback with a warning and never blocks CI. Composite action, no Node runtime
(just curl + jq).
jobs:
determine-runner:
runs-on: ubuntu-24.04
outputs:
runner: ${{ steps.pick.outputs.runner }}
steps:
- uses: devtime-ltd/runner-failover@v1
id: pick
with:
runners: |
self-hosted
ubuntu-24.04
token: ${{ secrets.RUNNER_READ_PAT }}
build:
needs: determine-runner
runs-on: ${{ fromJson(needs.determine-runner.outputs.runner) }}runners is priority-ordered, one runner label per line. The first line with an
online runner wins; the last line is the unconditional fallback.
| Input | Description |
|---|---|
runners |
Priority-ordered candidates (see above). |
token |
Fine-grained PAT with Administration: read-only on the repo. GITHUB_TOKEN can't list runners. |
| Output | Description |
|---|---|
runner |
JSON label array; use with fromJson() in runs-on. |
used-fallback |
"true" when a non-preferred candidate was chosen. |
Create the PAT prefilled, set the resource owner to your org, scope to your repo, and save as a secret. If it expires, CI just uses the fallback until you rotate.
MIT