ushr

module
v0.2.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: Apache-2.0

README

ushr

Cross-org priority scheduler for self-hosted GitHub Actions runners. Architecture in DESIGN.md.

Status: v0.2.10, Apache-2.0. Multi-host, keyless: the agent holds your GitHub App key and the control plane never sees a credential.

Run it two ways. Self-host the controller alongside the agent (ushr setup) and nothing leaves your network. Or point the agent at the hosted control plane at ushr.io (ushr login) for cross-machine scheduling and a dashboard — the key still never leaves your box.

Components

Binary Role
ushr User-facing CLI. ushr login sets a host up end to end for the hosted plane; ushr setup --org NAME creates the GitHub App and records it in agent.yaml; ushr doctor checks host health; ushr init [dir] rewrites a repo's workflow runs-on targets to ushr labels (--scale-set NAME targets a scale set).
ushr-controller Schedules queued jobs across agents over a long-poll HTTP API (keyless: agents report metadata, the controller holds no GitHub credentials)
ushr-agent Runs on the runner host. Holds the GitHub App keys, watches GitHub for queued jobs, mints JIT configs, and uses the configured Driver (Tart on macOS, docker/podman on Linux) to provision/destroy ephemeral runners

Quick start — one command per host

Install the binaries:

curl -fsSL https://get.ushr.io | sh
# or, from a checkout: ./deploy/install.sh

Then, on the runner host:

# Hosted plane (ushr.io): guided end-to-end setup —
# config, prereq installs (tart / podman, runner image), browser
# enrollment, GitHub App creation, service install + start.
ushr login

# Local/OSS single host, no account: GitHub App + local
# controller/agent services.
ushr setup --org YOUR_ORG

Both are interactive but zero-edit: prereqs are offered as guided installs (brew install openai/tools/tart, image pulls, podman), the GitHub App flow is two browser clicks, and the org block is written into ~/.config/ushr/agent.yaml for you. On Linux the per-repo docker build cache is on by default.

If cosign is on PATH, the installer also verifies the release signature. Otherwise it verifies the sha256 checksums only. It prints which mode ran. A release with no signature bundle gets the sha256 check only, and the installer warns.

Check a host any time:

ushr doctor

macOS drivers: Tart and Lume

Tart is the default macOS driver. Cirrus Labs, the Tart maintainers, announced on 7 Apr 2026 that they would join OpenAI (announcement). The Tart repository is now openai/tart. Since 5 Jun 2026, Tart uses the FSL-1.1-ALv2 license. ushr also ships a Lume driver as a fallback. Set driver.type: lume in agent.yaml to use it.

Action cache

Each host keeps a cache of the action archives that jobs use (uses:). After a job finishes, the agent reads the job log and downloads each listed action archive once. New runners then copy the archive from the host instead of downloading it from GitHub. The agent caches only actions from public repositories, because all orgs on a host share the cache. The cache is in <state_dir>/actions (default ~/.local/share/ushr/actions) and holds at most 2 GB. Each runner gets the cache as a read-only mount, and ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE gives its path. Both sources fill the cache. The poll source fills it after each dispatch. The scaleset source fills it when GitHub reports a job complete. It finds the job in the workflow run by the runner name.

Scale-set source

With source.type: scaleset, GitHub pushes each scale set's desired runner count to the agent. The agent does not poll. A scale set's name is the runs-on label that workflows target. Declare scale sets under an org, or under a repo target:

orgs:
  - name: YOUR_ORG
    app_id: 12
    private_key_path: ~/.secrets/ushr-YOUR_ORG.pem
    scale_sets:
      - name: ushr-linux
        max_runners: 8
repos:
  - owner: YOUR_USER
    repo: YOUR_REPO
    app_id: 34
    private_key_path: ~/.secrets/ushr-YOUR_USER-YOUR_REPO.pem
    scale_sets:
      - name: ushr-mac
        max_runners: 2
source:
  type: scaleset

Personal accounts have no org runner pool, so they use repo targets. A repo scale set serves only its repo, and it is in the repo's default runner group. In scaleset mode, each org and repo target must declare scale_sets. To point a repo's workflows at a scale set, run ushr init --scale-set NAME [dir]. It rewrites the same runs-on targets as ushr init, but to the plain label NAME.

GitHub Enterprise Server

ushr uses github.com by default. To serve an org or repo on a GitHub Enterprise Server instance, give its root URL to ushr setup:

ushr setup --org YOUR_ORG --base-url https://ghe.example.com

Setup creates the GitHub App on that instance and writes base_url into the target in agent.yaml:

orgs:
  - name: YOUR_ORG
    app_id: 12
    private_key_path: ~/.secrets/ushr-abc123-YOUR_ORG.pem
    base_url: https://ghe.example.com

The value is the instance root, with no path. ushr derives the API root (/api/v3) from it. The agent uses it for App authentication, JIT runner registration and the scaleset source. Targets with no base_url stay on github.com, so one agent can serve both. One agent cannot serve the same org or repo name on two instances. ushr keys jobs by name only. ushr doctor checks that each configured instance is reachable.

The Tart and Lume drivers still download the runner from github.com/actions/runner releases. GHES does not host these releases, so the host needs outbound access to github.com. ushr login with the hosted plane supports github.com only.

Managed agent updates

Workspace admins can select Update agent on a host in the Fleet dashboard. The host finishes active jobs before it installs the latest stable agent release. It downloads the official release and verifies the checksum published with that release. A supervisor restores the previous agent if the new process cannot contact the control plane within 90 seconds. The dashboard shows the installed version, update progress, and failures.

Existing hosts need one manual installation of v0.2.7 to enable the recovery protocol. The recovery process stays running until the service restarts. Each update starts a new supervisor and worker under that recovery process. A new dashboard request can retry a failed release. Older targets cannot downgrade an agent. Managed updates replace the agent binary. Use the installer to update the CLI and a local controller.

Priority policy

The control plane now owns each scope's priority. A scope is a GitHub org login or owner/repo. Before this change, each agent sent the priority from its own agent.yaml. That value still applies when no policy exists for the scope. A policy value replaces the agent value for every agent that reports the scope. Aging still adds boost_per_minute for each minute a job waits.

Self-hosted controllers read the policy from controller.yaml:

policy:
  type: priority
  aging:
    boost_per_minute: 1
  priorities:
    example-org: 100
    example-owner/example-repo: 20

The hosted control plane reads the priority_policies table. Workspace admins set it in the Workspace settings page. A row applies only while its workspace owns the scope. If the table read fails, the control plane uses the agent values for that poll.

Job telemetry

The dashboard combines runner dispatch records with signed GitHub workflow_job events. Set USHR_WEBHOOK_SECRET on the control plane and configure the GitHub App's webhook with the same secret. Point the webhook at /webhook on the control plane and subscribe to Workflow job events. Start events supply live job links. Completion events supply workflow names, results, and execution times. The dashboard identifies missing results and estimated slot durations.

Verify a release

Each release signs checksums.txt with keyless cosign through the release workflow's GitHub OIDC identity. Each archive also has an SPDX SBOM from syft.

cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp '^https://github\.com/paddo-tech/ushr/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt
sha256sum --ignore-missing -c checksums.txt

Metrics

The controller and the agent export Prometheus metrics at /metrics.

The controller serves /metrics on its normal listener. With a static token (USHR_TOKEN), a scrape must send Authorization: Bearer <token>. Without a static token, only a direct loopback peer can scrape. The controller refuses scrapes that carry X-Forwarded-For, Forwarded or Fly-Client-IP. Enrolled agent tokens never unlock /metrics, because the series cover every tenant.

Controller series Type Meaning
ushr_controller_offers_total counter Dispatch offers made to agents
ushr_controller_claims_total counter Offers that agents claimed
ushr_controller_dispatch_latency_seconds histogram Job wait from GitHub queue to offer, from the agent-reported wait
ushr_controller_agents_lost_total counter Agents dropped after missed polls
ushr_controller_agents_active gauge Agents heard from within the liveness window
ushr_controller_queued_jobs_seen gauge Distinct queued jobs in the latest poll of each active agent

The agent serves no metrics by default. Set metrics_listen in agent.yaml to serve them:

metrics_listen: 127.0.0.1:9464

The agent listener has no auth. Bind it to loopback or a private interface.

Agent series Type Labels Meaning
ushr_agent_slots_busy gauge Slots reserved or running a job
ushr_agent_slots_total gauge Slots the driver offers
ushr_agent_provision_duration_seconds histogram driver Time to provision a runner, successes only
ushr_agent_provision_failures_total counter driver Provision attempts that failed
ushr_agent_disk_blocked gauge 1 while the disk gate or a reclaim drain refuses work
ushr_agent_github_api_errors_total counter org Failed GitHub API calls by the poll source
ushr_agent_github_breaker_open gauge org 1 while the poll source pauses an org
ushr_agent_poll_errors_total counter Failed polls to the controller

The GitHub series come from the poll source only. The scaleset source does not export them. Both processes also export the standard Go runtime and process series.

Development

make build          # bin/ushr, bin/ushr-controller, bin/ushr-agent
make test           # go test ./...
make lint           # golangci-lint run

End-to-end Tart driver smoke (requires real Tart + an org with the App installed):

go run ./cmd/integration-test -trigger-repo=example-repo

Note: mints a real org-level runner registration in GitHub, dispatches a real workflow on -trigger-repo, and deletes both on success. Don't point at a production org for casual testing.

Roadmap

Shipped: Tart / Lume / Docker / SSH drivers, polling and scaleset sources, priority + aging, multi-host, two-phase keyless dispatch, per-agent enrollment, persistent dispatch ledger, ushr cost, one-command host setup, disk-pressure gating and reclaim, shared runner pools, managed agent updates, job telemetry, GitHub App setup from any browser, Prometheus metrics.

Next: Kubernetes driver, docs site.

Auth model

  • Controller ↔ Agent: per-agent enrollment tokens minted by ushr login, scoped to the orgs the agent may serve. Self-hosted single-host instead uses a shared bearer token, or no token at all when bound to loopback.
  • Agent ↔ GitHub: GitHub App per org via ghinstallation/v2. App ID + private key path in agent.yaml; installation ID auto-discovered. The control plane never sees a GitHub credential.
  • JIT runner registration: the agent mints a fresh JIT config per dispatch via Actions.GenerateOrgJITConfig. Runner is single-use and self-removes from GitHub when the job finishes.

Contributing and security

See CONTRIBUTING.md and CHANGELOG.md. Report vulnerabilities privately as described in SECURITY.md.

License

Apache-2.0.

Directories

Path Synopsis
cmd
agent command
controller command
integration-test command
Command integration-test exercises the Tart driver end-to-end against a real paddo-runner-mac image.
Command integration-test exercises the Tart driver end-to-end against a real paddo-runner-mac image.
ushr command
Command ushr is the user-facing CLI.
Command ushr is the user-facing CLI.
internal
actioncache
Package actioncache keeps a host-local cache of GitHub Action archives in the layout the actions runner reads through ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE: <dir>/<owner>_<repo>/<sha>.tar.gz.
Package actioncache keeps a host-local cache of GitHub Action archives in the layout the actions runner reads through ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE: <dir>/<owner>_<repo>/<sha>.tar.gz.
agent
Package agent runs the long-poll loop on the runner host.
Package agent runs the long-poll loop on the runner host.
api
Package api defines the control-plane<->agent HTTP protocol.
Package api defines the control-plane<->agent HTTP protocol.
disk
Package disk reports free space on a runner host's image store.
Package disk reports free space on a runner host's image store.
dispatch
Package dispatch is the controller's persistent record of in-flight dispatches.
Package dispatch is the controller's persistent record of in-flight dispatches.
dispatch/pg
Package pg is a Postgres-backed dispatch.Store for the hosted control plane.
Package pg is a Postgres-backed dispatch.Store for the hosted control plane.
domain
Package domain holds neutral types shared across components.
Package domain holds neutral types shared across components.
driver/docker
Package docker implements the Driver interface by running each ephemeral runner as a container via the Docker or Podman CLI (autodetected, or pinned via Options.Runtime — the two are drop-in for the subcommands used here).
Package docker implements the Driver interface by running each ephemeral runner as a container via the Docker or Podman CLI (autodetected, or pinned via Options.Runtime — the two are drop-in for the subcommands used here).
driver/factory
Package factory builds a Driver from agent config.
Package factory builds a Driver from agent config.
driver/lume
Package lume binds the lume CLI (MIT, Apple Virtualization.framework) to the generic VM driver in internal/driver/vmdriver.
Package lume binds the lume CLI (MIT, Apple Virtualization.framework) to the generic VM driver in internal/driver/vmdriver.
driver/sshrunner
Package sshrunner holds the SSH-based runner bootstrap shared by the macOS drivers (tart, lume).
Package sshrunner holds the SSH-based runner bootstrap shared by the macOS drivers (tart, lume).
driver/tart
Package tart binds the tart CLI (Apple Virtualization.framework) to the generic VM driver in internal/driver/vmdriver.
Package tart binds the tart CLI (Apple Virtualization.framework) to the generic VM driver in internal/driver/vmdriver.
driver/vmdriver
Package vmdriver provides a generic ephemeral-VM Driver shared by the macOS drivers (tart, lume).
Package vmdriver provides a generic ephemeral-VM Driver shared by the macOS drivers (tart, lume).
enroll
Package enroll handles agent authentication for the hosted control plane: per-agent enrollment tokens (validated against Neon) and the browser-session + PKCE handshake behind `ushr login`.
Package enroll handles agent authentication for the hosted control plane: per-agent enrollment tokens (validated against Neon) and the browser-session + PKCE handshake behind `ushr login`.
jit
Package jit mints GitHub Actions runner JIT configs per org installation.
Package jit mints GitHub Actions runner JIT configs per org installation.
ledger
Package ledger is an append-only store of completed ushr jobs.
Package ledger is an append-only store of completed ushr jobs.
metrics
Package metrics defines the Prometheus series the controller and the agent export.
Package metrics defines the Prometheus series the controller and the agent export.
source/github
Package github implements a polling Source backed by the GitHub Actions API.
Package github implements a polling Source backed by the GitHub Actions API.
source/scaleset
Package scaleset implements a Source backed by GitHub's Runner Scale Set API (the mechanism behind actions-runner-controller, via the standalone actions/scaleset client).
Package scaleset implements a Source backed by GitHub's Runner Scale Set API (the mechanism behind actions-runner-controller, via the standalone actions/scaleset client).
svc
Package svc installs and controls the ushr launchd/systemd user services, so the CLI can take a host from enrolled to running without a repo checkout or hand-run launchctl/systemctl incantations.
Package svc installs and controls the ushr launchd/systemd user services, so the CLI can take a host from enrolled to running without a repo checkout or hand-run launchctl/systemctl incantations.
telemetry
Package telemetry persists the dispatch story the control plane otherwise drops: finished jobs (job_runs), agent liveness (agent_status), and a live transition feed (dispatch_events).
Package telemetry persists the dispatch story the control plane otherwise drops: finished jobs (job_runs), agent liveness (agent_status), and a live transition feed (dispatch_events).
update
Package update installs official agent releases after the host drains.
Package update installs official agent releases after the host drains.
webhook
Package webhook receives GitHub lifecycle events for jobs served by this fleet.
Package webhook receives GitHub lifecycle events for jobs served by this fleet.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL