GitHub Actions / developer guide

One variable.
The right runner.

Route private jobs through your provider policy. Keep public jobs on GitHub-hosted runners.

Inspect the routing Organization setup ↓
01 / Repository boundaryWorkflow input

Private reads the variable.
Public takes the fallback.

Private repository
CI_PRIVATE_RUNNERConfigured runner label → or ubuntu-latest when unset.
Public repository
ubuntu-latestThe private-only organization variable is not exposed.

The boundary is the variable’s private visibility—not a condition in every workflow.

In your workflowruns-on: ${{ vars.CI_PRIVATE_RUNNER || 'ubuntu-latest' }}

02 / Policy evaluation

Trace the runner decision.

Evaluate providers in order. Select the first with remaining minutes above its reserve. Unknown usage is skipped.

Illustrative data · not live usage
Try a quota scenario
Ordered runner providers evaluated using illustrative usage
Order / providerIncludedUsedRemainingReserveDecision
1GitHub-hosted2,0001,900100100Skip · at reserve
2Blacksmith3,0008502,150100Selected
3Fallback————Not evaluated
Selected runnerblacksmith-4vcpu-ubuntu-2404GitHub is at its reserve. Blacksmith has quota available.

Minutes shown use the checked-in quota defaults. The fallback example assumes FALLBACK_RUNNER is configured. Evaluation stops after the first available provider.

What this repository does today

Implemented

Bootstrap & evaluate

GitHub App authentication verifies organization access and creates the private-only variable without overwriting an existing value. The Python engine evaluates a supplied usage file.

Not integrated

Collect & update

Provider usage collection and writing policy decisions back to the organization variable are separate integration work. The checked-in workflow runs bootstrap—not the full routing loop.

An organization.
An App. A first run.

Fork the gateway into the organization you want to manage. Keep the App credentials in repository settings, never in source.

Read the GitHub App guide
  1. Fork into your organization

    Create an organization-owned fork of runner-gateway.

  2. Create and install a GitHub App

    In organization settings, create an App, disable webhooks, and install it on that organization. Generate a private key.

    Organization permissions
    PermissionAccess
    AdministrationRead-only
    VariablesRead and write

    No repository write permission is required.

  3. Add the credentials to your fork

    APP_CLIENT_ID
    Repository variable · App client ID
    APP_PRIVATE_KEY
    Repository secret · generated PEM key
  4. Run Runner Gateway once

    Open Actions → Runner Gateway → Run workflow. Bootstrap verifies billing and variable API access, then creates CI_PRIVATE_RUNNER=ubuntu-latest with private visibility if it does not exist.

    Existing variable values are never overwritten by bootstrap.

Bring the usage. Let the policy decide.

Usage collection stays outside the selector. Supply measured minutes, evaluate the rules, and inspect the returned decision.

Policy reference
usage.json · example input
{
  "github": 1250,
  "blacksmith": 850
}
Evaluate locally
FALLBACK_RUNNER=other-provider-runner \
  python3 runner_gateway.py \
  --usage usage.json

Configure your actual allowances

Edit config/policy.json. Defaults are 2,000 GitHub minutes and 3,000 Blacksmith minutes, each with a 100-minute reserve. GitHub allowances vary by plan.

Unknown is not available

A provider without supplied usage is skipped. Use an authoritative usage source; do not treat a missing reading as unused quota. An unset fallback runner is skipped too.

Schedule outside the public workflow

Use an external scheduler for collection, evaluation and variable updates. A systemd timer, container cron or cloud job can orchestrate the loop. The repository workflow is not that integration.

Keep the rule in one place.

Configure at the organization level. Keep application workflows simple.

Explore the repository