outrunner

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Apr 6, 2026 License: MIT Imports: 13 Imported by: 0

README

gha-outrunner

CI Release Go Report Card

Ephemeral GitHub Actions runners, no Kubernetes required.

How gha-outrunner works

outrunner provisions fresh Docker containers or VMs for each GitHub Actions job, then destroys them when the job completes. It uses GitHub's scaleset API to register as an autoscaling runner group.

Why outrunner?

GitHub's Actions Runner Controller (ARC) requires Kubernetes. If you're running on bare metal or a simple VPS, you shouldn't need a cluster just to get ephemeral runners. outrunner gives you the same isolation guarantees with Docker, libvirt, or Tart. No additional orchestrator needed.

Read more about why outrunner, the architecture, and the security model.

Provisioners

Provisioner Host OS Runner OS How it works
Docker Linux, macOS Linux Container per job. Fastest startup.
libvirt Linux Windows, Linux KVM VM from qcow2 golden image with CoW overlays. QEMU Guest Agent for command execution.
Tart macOS (Apple Silicon) macOS, Linux (ARM64) VM clone per job. Tart guest agent for command execution.

See the provisioner reference for lifecycle details and runner image requirements for what each backend expects.

Get Started

Install outrunner for your platform:

Each guide gets you to a working Docker runner in minutes.

All packages and binaries are on the Releases page.

Going Further

Other backends
Custom runner images
Reference

Used by

  • delo.so - Desktop, offline-first CAD for makers. Uses outrunner for CI, build and test pipelines.

Using outrunner? Open a PR to add your project.

Author

Built by Paweł Subocz at Netwind.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ResolveToken

func ResolveToken(flagToken string, cfg *Config) (string, error)

ResolveToken determines the GitHub token using the following precedence:

  1. flagToken (--token CLI flag)
  2. GITHUB_TOKEN environment variable
  3. $CREDENTIALS_DIRECTORY/github-token (systemd-creds)
  4. token_file from config

func ResolveURL

func ResolveURL(flagURL string, cfg *Config) (string, error)

ResolveURL determines the GitHub URL using the following precedence:

  1. flagURL (--url CLI flag)
  2. url from config

Types

type Config

type Config struct {
	URL       string                  `yaml:"url"`
	TokenFile string                  `yaml:"token_file"`
	Runners   map[string]RunnerConfig `yaml:"runners"`
}

Config is the outrunner configuration file format.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and parses a config file.

type DockerImage

type DockerImage struct {
	Image     string        `yaml:"image"`
	RunnerCmd string        `yaml:"runner_cmd"`
	Mounts    []DockerMount `yaml:"mounts"`
}

DockerImage configures a Docker-based runner.

type DockerMount added in v1.1.0

type DockerMount struct {
	Source   string `yaml:"source"`
	Target   string `yaml:"target"`
	ReadOnly bool   `yaml:"read_only"`
}

DockerMount defines a bind mount for a Docker container.

type LibvirtImage

type LibvirtImage struct {
	Path      string        `yaml:"path"`
	RunnerCmd string        `yaml:"runner_cmd"`
	Socket    string        `yaml:"socket"`
	CPUs      int           `yaml:"cpus"`
	MemoryMB  int           `yaml:"memory"`
	Mount     *LibvirtMount `yaml:"mount"`
}

LibvirtImage configures a libvirt/QEMU-based runner.

type LibvirtMount added in v1.1.0

type LibvirtMount struct {
	Source string `yaml:"source"`
}

LibvirtMount defines a virtiofs host directory share for a libvirt VM. The directory is exposed via virtiofs; the tag is derived from the source basename. On Windows guests, VirtioFsSvc mounts it automatically as a drive letter.

type Provisioner

type Provisioner interface {
	// Start provisions a new runner environment and starts the GitHub Actions
	// runner process inside it. The runner should use the JIT config to
	// register itself with GitHub and pick up the assigned job.
	//
	// Start must return after the runner process has been launched.
	// It does not need to wait for the job to complete.
	Start(ctx context.Context, req *RunnerRequest) error

	// Stop tears down the runner environment. Called after the job completes
	// or if the runner needs to be forcefully removed.
	Stop(ctx context.Context, name string) error

	// Close releases any resources held by the provisioner (e.g., Docker client).
	Close() error
}

Provisioner creates and destroys ephemeral runner environments. Each implementation handles a different backend (Docker, libvirt, etc.).

type RunnerConfig

type RunnerConfig struct {
	Labels     []string      `yaml:"labels"`
	MaxRunners int           `yaml:"max_runners,omitempty"`
	Docker     *DockerImage  `yaml:"docker,omitempty"`
	Libvirt    *LibvirtImage `yaml:"libvirt,omitempty"`
	Tart       *TartImage    `yaml:"tart,omitempty"`
}

RunnerConfig defines a runner environment and the scale set it registers. The map key in Config.Runners is used as the scale set name. Exactly one of Docker, Libvirt, or Tart must be set.

func (*RunnerConfig) ProviderType

func (r *RunnerConfig) ProviderType() string

ProviderType returns which provisioner backend this runner uses.

type RunnerPhase

type RunnerPhase int

RunnerPhase represents the current lifecycle phase of a runner.

const (
	RunnerProvisioning RunnerPhase = iota
	RunnerIdle
	RunnerRunning
	RunnerStopping
)

func (RunnerPhase) String

func (p RunnerPhase) String() string

type RunnerRequest

type RunnerRequest struct {
	// Name is a unique identifier for this runner instance.
	Name string

	// JITConfig is the base64-encoded JIT configuration from GitHub.
	// Pass to: ./run.sh --jitconfig <JITConfig>
	JITConfig string

	// Runner is the matched runner configuration.
	Runner *RunnerConfig
}

RunnerRequest contains everything a provisioner needs to start a runner.

type RunnerSnapshot

type RunnerSnapshot struct {
	Name      string
	RunnerID  int
	Phase     RunnerPhase
	CreatedAt time.Time
	StartedAt time.Time
}

RunnerSnapshot is a point-in-time copy of a runner's state, safe to read without holding the scaler's mutex.

type RunnerState

type RunnerState struct {
	Name      string
	RunnerID  int // from GenerateJitRunnerConfig().Runner.ID
	Phase     RunnerPhase
	CreatedAt time.Time
	StartedAt time.Time // when Start() completed (provisioning finished)
	// contains filtered or unexported fields
}

RunnerState holds the full state of a single runner instance.

func (*RunnerState) SignalDone

func (r *RunnerState) SignalDone()

SignalDone closes the done channel, signaling the runner goroutine to stop. Safe to call multiple times.

type ScaleSetClient

type ScaleSetClient interface {
	GenerateJitRunnerConfig(ctx context.Context, setting *scaleset.RunnerScaleSetJitRunnerSetting, scaleSetID int) (*scaleset.RunnerScaleSetJitRunnerConfig, error)
	RemoveRunner(ctx context.Context, runnerID int64) error
}

ScaleSetClient is the subset of scaleset.Client that Scaler uses. Extracted as an interface for testability.

type Scaler

type Scaler struct {
	// contains filtered or unexported fields
}

Scaler implements listener.Scaler by provisioning real runner environments. Each runner gets its own goroutine that manages the full lifecycle: provisioning, waiting for job completion, stopping, and deregistration.

func NewScaler

func NewScaler(logger *slog.Logger, client ScaleSetClient, scaleSetID, maxRunners int, namePrefix string, runner *RunnerConfig, prov Provisioner) *Scaler

func (*Scaler) HandleDesiredRunnerCount

func (s *Scaler) HandleDesiredRunnerCount(ctx context.Context, count int) (int, error)

func (*Scaler) HandleJobCompleted

func (s *Scaler) HandleJobCompleted(ctx context.Context, jobInfo *scaleset.JobCompleted) error

func (*Scaler) HandleJobStarted

func (s *Scaler) HandleJobStarted(ctx context.Context, jobInfo *scaleset.JobStarted) error

func (*Scaler) Runners

func (s *Scaler) Runners() []RunnerSnapshot

Runners returns a snapshot of current runner states.

func (*Scaler) Shutdown

func (s *Scaler) Shutdown(ctx context.Context)

Shutdown cancels all runner goroutines and waits for them to finish.

type SimpleHandler

type SimpleHandler struct {
	// contains filtered or unexported fields
}

SimpleHandler outputs logs in a human-readable format:

2026-03-30 21:08:31 INFO Loaded config runners=1

func NewSimpleHandler

func NewSimpleHandler(w io.Writer, level slog.Leveler) *SimpleHandler

func (*SimpleHandler) Enabled

func (h *SimpleHandler) Enabled(_ context.Context, level slog.Level) bool

func (*SimpleHandler) Handle

func (h *SimpleHandler) Handle(_ context.Context, r slog.Record) error

func (*SimpleHandler) WithAttrs

func (h *SimpleHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*SimpleHandler) WithGroup

func (h *SimpleHandler) WithGroup(name string) slog.Handler

type TartImage

type TartImage struct {
	Image     string      `yaml:"image"` // OCI image or local VM name
	RunnerCmd string      `yaml:"runner_cmd"`
	CPUs      int         `yaml:"cpus"`
	MemoryMB  int         `yaml:"memory"`
	Mounts    []TartMount `yaml:"mounts"`
}

TartImage configures a Tart-based runner (macOS/Linux on Apple Silicon).

type TartMount added in v1.1.0

type TartMount struct {
	Name     string `yaml:"name"`
	Source   string `yaml:"source"`
	ReadOnly bool   `yaml:"read_only"`
}

TartMount defines a shared directory for a Tart VM. Name is passed as the --dir label; it becomes the subdirectory name under the mount point inside the guest.

Directories

Path Synopsis
cmd
outrunner command
provisioner

Jump to

Keyboard shortcuts

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