billet

module
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: Apache-2.0

README

billet

CI Coverage Go Reference Docs

Self-hosted GitHub Actions runners on your own hardware, with the cloud as fallback and a cache beside the compute.

A box under your desk is the cheapest fast CI you will ever have, and the reason people do not rely on it is that houses lose power. billet is built so that one runs-on label can mean "the machine at home if it is up, EC2 if it is not". The control plane talks to GitHub over an outbound long-poll, so GitHub never connects to you; a single-box deployment opens nothing at all. One static binary, no hosted component, Apache-2.0.

Pre-alpha. Jobs run end to end through every backend against real GitHub and AWS, but do not point release or deploy pipelines at it yet. What is proven says exactly what has run for real, by backend, with dates.

Contents

Why billet

  • One label, several machines, in your order of preference. A tier can name [firecracker, ec2], and the control plane picks the host when the job is admitted: the box at home first, the cloud when it is not there. Failover under one unchanged label has completed against real GitHub and AWS.
  • A kernel per job on your own hardware. Firecracker microVMs under the jailer on Linux, tart VMs on an Apple Silicon Mac, one EC2 instance or CodeBuild build per job in AWS. Docker is there for trials.
  • A cache beside the compute. Guest images, sticky disks, BuildKit state, the Docker image store and, on Linux, actions/cache itself, served from a Ceph pool or EBS at the same site instead of crossing the internet.
  • Nothing hosted. No SaaS, no webhook endpoint, no public IP, no tunnel. Nodes dial the control plane; the control plane dials GitHub; that is all.

Quick start

One Linux machine with Docker, a GitHub organization you own, and about fifteen minutes.

curl -fsSL https://raw.githubusercontent.com/junioryono/billet/main/scripts/install.sh | sh

billet init --org your-org --runner-group billet-trusted \
  --workflow 'your-org/your-repo/.github/workflows/ci.yml@refs/heads/main' --config ~/billet.yaml
billet github-app create --org your-org --config ~/billet.yaml   # creates and installs the App
billet check --config ~/billet.yaml                              # proves the credential and the config

billet server --config ~/billet.yaml   # the control plane; and in a second terminal:
billet node   --config ~/billet.yaml   # the machine that runs jobs

Then, in a workflow:

jobs:
  build:
    runs-on: billet-2vcpu

The runner group must be workflow-restricted before init, and the App is created in a browser. Get started walks every step, including the one that costs an hour.

Deployment shapes

Goal Control plane Compute Guide
Try it on one computer billet server and SQLite on it Docker on the same machine, trusted workflows only Single host with Docker
One Linux server, real isolation, a persistent cache on the server Firecracker guests and a Ceph pool on its disks, converged by Ansible A Linux Firecracker host
macOS or arm64 Linux jobs on a Mac you own local, on the Mac, or in AWS tart VMs on Apple Silicon Run jobs on a Mac
Everything in AWS a small EC2 instance, SQLite on EBS one EC2 instance per job AWS with EC2
Managed macOS in AWS in AWS CodeBuild on a reserved Apple Silicon fleet AWS with CodeBuild
Owned hardware first, the cloud when it is not there a small EC2 instance local Firecracker, then EC2, under one label Hybrid
Controller failover active and passive controllers on PostgreSQL any fleet PostgreSQL and active-passive

Choose a shape compares them by failure domain.

Backends

Backend Runs on Boundary Fork pull requests
firecracker Linux with KVM and Ceph a microVM under the jailer: its own kernel, chroot, uid, cgroup, seccomp with a separate untrusted bridge
tart Apple Silicon a VM under Apple's hypervisor; macOS or arm64 Linux guests with softnet isolation
ec2 AWS one instance per job, destroyed with it with a separate security group
codebuild AWS one build per job on managed hosts, Linux or Apple Silicon refused
docker anywhere with Docker a container sharing the host kernel refused

Storage is a property of the site: a Ceph pool for Firecracker hosts, EBS snapshots with an S3 pointer for EC2. The ledger is SQLite on local disk by default, or PostgreSQL when the controller should be rebuildable rather than restored.

How it works

                 GitHub  ◄── outbound long-poll only; nothing inbound
                    ▲
              ┌─────┴──────┐
              │   server   │   scale-set listeners · capacity ledger · scheduler
              └─────┬──────┘
                    │  nodes dial OUT over mTLS (plain HTTP on loopback)
        ┌───────────┼──────────────┬───────────────┐
        ▼           ▼              ▼               ▼
   ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────────┐
   │bare metal│ │ Apple Si │ │    EC2    │ │  CodeBuild │
   │firecracker│ │   tart   │ │ per job   │ │  per job   │
   └──────────┘ └──────────┘ └───────────┘ └────────────┘

One binary, two roles. billet server creates one GitHub runner scale set per tier, reserves capacity in its ledger before it advertises a slot, accepts a job, chooses the machine, and tells that machine's node to launch. billet node runs one provider, polls for commands, and hands each guest a single-use runner registration off argv. When GitHub reports completion the node destroys the compute and the capacity comes back, and only then. A control plane that restarts re-adopts the jobs already running; nothing billet does on its own destroys a running job. How billet works and Tiers and capacity go deeper.

Security

billet is not a sandbox for untrusted code, and says so. Trust belongs to a runner pool, not to the event that scaled it up: every tier is untrusted unless promoted, and a trusted tier requires a workflow-restricted runner group that billet re-checks against GitHub before every registration it mints. Fork pull requests get arbitrary code execution on your hardware and are admitted only on a backend that is a real boundary with a network of its own; GitHub's own guidance against self-hosted runners on public repositories applies. The App billet creates holds two permissions (metadata read, organization self-hosted runners read and write) and cannot read your code. The control plane runs unprivileged; the Linux node runs as root because Firecracker and the Docker socket require it, and the jailer drops every VM to its own uid. Caches are a deliberate cross-job channel, so do not cache secrets. Read Trust and isolation before pointing billet at anything. To report a vulnerability, email the maintainer rather than opening an issue.

Documentation

Everything is at billet.readthedocs.io.

  • Get started: from nothing to a job in fifteen minutes.
  • Concepts: how it works, tiers and capacity, trust and isolation, sites and storage, identity and security, state and controllers.
  • Deploying: choose a shape; a single host; a Firecracker host; a Mac; AWS with EC2 or CodeBuild; hybrid; PostgreSQL and active-passive controllers; reaching your hosts.
  • Operating: status and leases, nodes, guest images, the Actions cache, upgrades (automatic by default, on Linux, macOS and PostgreSQL controllers alike, and unable to go backwards by accident), backup and recovery, draining and stopping, CA rotation, troubleshooting.
  • Reference: every command and every configuration key, what is proven, the reference hardware, the architecture decisions and the acceptance records.

Terraform modules live under terraform/modules, the Ansible collection under ansible_collections/junioryono/billet, and the published Actions under actions/.

Alternatives

Use one of these if it fits. actions-runner-controller is GitHub's own and needs Kubernetes. terraform-aws-github-runner is AWS-only. GARM is the closest open-source multi-provider control plane, without a cache. Ubicloud is the best open reference for how a commercial runner cloud is built, under AGPL. Actuated, Blacksmith, Namespace, WarpBuild and Depot are managed products. billet is one runs-on label spanning bare metal, Apple Silicon and the cloud with failover between them, plus a colocated cache, in one Apache-2.0 binary with no Kubernetes; every piece of that exists somewhere in that list and the combination does not. billet borrows several of Blacksmith's published designs and says so.

Contributing

See CONTRIBUTING.md. make check is the gate; the skills under .claude/skills are the project's written-down rules.

License

Apache-2.0. See LICENSE. Apple Silicon support requires Tart, which is licensed FSL-1.1-ALv2 and is an external dependency you install yourself, like Docker or Ceph.

Directories

Path Synopsis
cmd
billet command
Command billet runs a self-hosted GitHub Actions runner platform.
Command billet runs a self-hosted GitHub Actions runner platform.
Package deploy carries the service definitions billet ships — systemd units for Linux and launchd agents for macOS — embedded so a running binary can compare what a host actually has against what this build expects.
Package deploy carries the service definitions billet ships — systemd units for Linux and launchd agents for macOS — embedded so a running binary can compare what a host actually has against what this build expects.
internal
alloc
Package alloc is billet's global capacity allocator.
Package alloc is billet's global capacity allocator.
archivestore
Package archivestore puts a deployment archive somewhere other than the disk it protects, and fetches it back.
Package archivestore puts a deployment archive somewhere other than the disk it protects, and fetches it back.
awscreds
Package awscreds resolves the AWS credentials billet signs requests with.
Package awscreds resolves the AWS credentials billet signs requests with.
awsjson
Package awsjson is billet's client for AWS services that speak JSON 1.1.
Package awsjson is billet's client for AWS services that speak JSON 1.1.
awspolicy
Package awspolicy assembles the least-privilege IAM policy an ec2 billet node (or the AMI builder) needs, from the action constants each owning package declares.
Package awspolicy assembles the least-privilege IAM policy an ec2 billet node (or the AMI builder) needs, from the action constants each owning package declares.
awsquota
Package awsquota reads an AWS account's Service Quotas.
Package awsquota reads an AWS account's Service Quotas.
awss3
Package awss3 reads what S3 said in the body of a refusal.
Package awss3 reads what S3 said in the body of a refusal.
awssig
Package awssig signs AWS Signature Version 4 requests without an SDK dependency.
Package awssig signs AWS Signature Version 4 requests without an SDK dependency.
awsssm
Package awsssm reads and writes AWS Systems Manager Parameter Store values.
Package awsssm reads and writes AWS Systems Manager Parameter Store values.
awssts
Package awssts answers one question: which AWS account is this credential in.
Package awssts answers one question: which AWS account is this credential in.
config
Package config defines billet's on-disk configuration and its validation rules.
Package config defines billet's on-disk configuration and its validation rules.
deployarchive
Package deployarchive captures and restores a billet deployment as ONE unit.
Package deployarchive captures and restores a billet deployment as ONE unit.
deploymentid
Package deploymentid validates the stable identity shared by a billet control plane and its nodes.
Package deploymentid validates the stable identity shared by a billet control plane and its nodes.
durablefile
Package durablefile installs a file so that a power loss cannot leave anything else pointing at a name that is not there.
Package durablefile installs a file so that a power loss cannot leave anything else pointing at a name that is not there.
fakeactions
Package fakeactions is a stand-in for GitHub's Actions service.
Package fakeactions is a stand-in for GitHub's Actions service.
github
Package github talks to GitHub's App and Runner Scale Set APIs.
Package github talks to GitHub's App and Runner Scale Set APIs.
guestassets
Package guestassets carries scripts installed into every managed runner image.
Package guestassets carries scripts installed into every managed runner image.
hostupgrade
Package hostupgrade replaces billet on one machine, transactionally.
Package hostupgrade replaces billet on one machine, transactionally.
imagesource
Package imagesource describes where a published guest image comes from and what has to be true about it before any of its bytes reach the cluster.
Package imagesource describes where a published guest image comes from and what has to be true about it before any of its bytes reach the cluster.
importcheck
Package importcheck answers which non-test Go files in this module import a package.
Package importcheck answers which non-test Go files in this module import a package.
initconfig
Package initconfig renders a runnable billet.yaml for `billet init`.
Package initconfig renders a runnable billet.yaml for `billet init`.
lifeops
Package lifeops answers what a local billet deployment is actually doing.
Package lifeops answers what a local billet deployment is actually doing.
lifeops/launchd
Package launchd drives macOS launch agents for billet's local lifecycle commands, as internal/lifeops drives systemd for Linux.
Package launchd drives macOS launch agents for billet's local lifecycle commands, as internal/lifeops drives systemd for Linux.
node
Package node turns assigned leases into running compute.
Package node turns assigned leases into running compute.
nodeapi
Package nodeapi is the wire between a control plane and a compute host.
Package nodeapi is the wire between a control plane and a compute host.
nodeclient
Package nodeclient is a compute host's half of the node wire.
Package nodeclient is a compute host's half of the node wire.
nodeplane
Package nodeplane is the control plane's half of the node wire.
Package nodeplane is the control plane's half of the node wire.
provenance
Package provenance records which release manifest produced the billet that is installed on this machine, and proves the record still describes it.
Package provenance records which release manifest produced the billet that is installed on this machine, and proves the record still describes it.
provider
Package provider launches and destroys the compute a job runs on.
Package provider launches and destroys the compute a job runs on.
provider/codebuild
Package codebuild runs one AWS CodeBuild build per job.
Package codebuild runs one AWS CodeBuild build per job.
provider/docker
Package docker runs jobs in containers.
Package docker runs jobs in containers.
provider/ec2
Package ec2 launches one instance per job in a cloud region.
Package ec2 launches one instance per job in a cloud region.
provider/firecracker
Package firecracker runs each job in its own microVM on bare metal.
Package firecracker runs each job in its own microVM on bare metal.
provider/simulated
Package simulated is a compute backend that starts no compute.
Package simulated is a compute backend that starts no compute.
provider/tart
Package tart runs macOS and Linux arm64 guests on Apple Silicon through the tart CLI (github.com/openai/tart).
Package tart runs macOS and Linux arm64 guests on Apple Silicon through the tart CLI (github.com/openai/tart).
releasesource
Package releasesource describes what one billet release contains and what has to be true about it before any of its bytes replace a running binary.
Package releasesource describes what one billet release contains and what has to be true about it before any of its bytes replace a running binary.
replay
Package replay drives a trace of job arrivals through the real control plane, node wire, node runtime and simulated backend at compressed time, and reads what billet recorded about each job back out of the ledger.
Package replay drives a trace of job arrivals through the real control plane, node wire, node runtime and simulated backend at compressed time, and reads what billet recorded about each job back out of the ledger.
rollout
Package rollout is billet's durable fleet decision: one immutable target, and where every controller and node has got to on the way to it.
Package rollout is billet's durable fleet decision: one immutable target, and where every controller and node has got to on the way to it.
runnerimages
Package runnerimages is what billet knows about GitHub's own runner image.
Package runnerimages is what billet knows about GitHub's own runner image.
runnerrelease
Package runnerrelease says which actions/runner billet installs, and how close that is to being refused by GitHub.
Package runnerrelease says which actions/runner billet installs, and how close that is to being refused by GitHub.
scaleset
Package scaleset is the ONLY place billet imports GitHub's scale-set client.
Package scaleset is the ONLY place billet imports GitHub's scale-set client.
server
Package server is billet's control plane: the per-tier scale-set listeners and the scheduler that turns assigned jobs into launched instances.
Package server is billet's control plane: the per-tier scale-set listeners and the scheduler that turns assigned jobs into launched instances.
state
Package state is billet's durable control-plane store.
Package state is billet's durable control-plane store.
store
Package store defines the cache-volume contract shared by storage backends.
Package store defines the cache-volume contract shared by storage backends.
store/ceph
Package ceph reaches the RBD pools a site keeps.
Package ceph reaches the RBD pools a site keeps.
store/ebss3
Package ebss3 implements site-local cache generations with EBS snapshots and S3 state.
Package ebss3 implements site-local cache generations with EBS snapshots and S3 state.
tfclass
Package tfclass reads a Terraform plan and says which of its changes billet's deployment has to be drained for.
Package tfclass reads a Terraform plan and says which of its changes billet's deployment has to be drained for.
tfpolicy
Package tfpolicy has no runtime code.
Package tfpolicy has no runtime code.
version
Package version reports which billet this is.
Package version reports which billet this is.
wirecert
Package wirecert issues the certificates the node wire authenticates with.
Package wirecert issues the certificates the node wire authenticates with.
wireshare
Package wireshare carries a deployment's node-wire authority between the controllers that share it.
Package wireshare carries a deployment's node-wire authority between the controllers that share it.
wiring
Package wiring adapts the scale-set client to what the control plane and the node consume.
Package wiring adapts the scale-set client to what the control plane and the node consume.
scripts
mkchannelstatement command
Command mkchannelstatement writes one signed-channel pointer.
Command mkchannelstatement writes one signed-channel pointer.
mkreleasemanifest command
Command mkreleasemanifest writes the signed-release manifest for one tag.
Command mkreleasemanifest writes the signed-release manifest for one tag.
tfclassify command
tfclassify reads a Terraform plan and says which of its changes billet has to be drained for.
tfclassify reads a Terraform plan and says which of its changes billet has to be drained for.

Jump to

Keyboard shortcuts

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