config

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BuildKitConfig added in v0.1.8

type BuildKitConfig struct {
	// GCEnabled toggles BuildKit's cache garbage collection. Nil = default
	// true. Setting false restores the old unbounded behavior and is only
	// sensible while debugging a cache-correctness problem.
	GCEnabled *bool `toml:"gc_enabled"`

	// GCReservedGB is build cache, in GiB, that is never collected even
	// when idle. The warm floor. Default 5.
	GCReservedGB uint64 `toml:"gc_reserved_gb"`

	// GCMaxUsedGB is the hard ceiling, in GiB, on total build cache.
	// Anything above it is collected regardless of age. Default 25.
	GCMaxUsedGB uint64 `toml:"gc_max_used_gb"`

	// GCMinFreeGB makes BuildKit collect whatever it must to keep at least
	// this much free space, in GiB, on the filesystem — overriding
	// GCReservedGB. This is the arm that rescues a node whose disk is
	// being consumed by something other than the build cache. Default 20,
	// matching [image_gc].min_free_gb so the two collectors agree on when
	// the node is tight.
	GCMinFreeGB uint64 `toml:"gc_min_free_gb"`

	// GCKeepDuration is the age past which cache records are collected
	// once usage exceeds GCReservedGB. Default 168h (7 days). BuildKit's
	// own default is 60 days, which is far too long for a CI node that
	// rebuilds the same images many times a day.
	GCKeepDuration time.Duration `toml:"gc_keep_duration"`

	// GCEphemeralKeepDuration and GCEphemeralMaxUsedGB bound the cheaply
	// reproducible record types (local build contexts, RUN --mount=cache
	// mounts, git checkouts). Re-creating those costs a local copy rather
	// than a network round trip, so they are collected far more eagerly.
	// Defaults 48h and 2 GiB.
	GCEphemeralKeepDuration time.Duration `toml:"gc_ephemeral_keep_duration"`
	GCEphemeralMaxUsedGB    uint64        `toml:"gc_ephemeral_max_used_gb"`
}

BuildKitConfig bounds the embedded BuildKit solver's on-disk build cache.

WHY THIS TABLE EXISTS: BuildKit only garbage-collects when its worker is given a GC policy, and ephemerd never gave it one. Every `docker build` run by every CI job therefore added cache records, snapshots and `containerd.io/gc.flat` leases to the shared "buildkit" containerd namespace that nothing ever released. A production node accumulated 76 image records, 302 snapshots and 481 leases — about 44 GB of a 116 GB disk — spanning 49 dead jobs over two and a half weeks, which is what actually filled the disk and froze the VM.

The defaults aim for a WARM BUT BOUNDED cache. Pruning hard on every build would trade the disk problem for a network problem (re-downloading and re-building layers on every job), which is the opposite of what these nodes need. So there is a floor that is never collected, a ceiling that always is, and a free-space guard that overrides the floor when the node is genuinely tight.

func (*BuildKitConfig) BuildKitGCEnabled added in v0.1.8

func (b *BuildKitConfig) BuildKitGCEnabled() bool

BuildKitGCEnabled reports whether the build cache is bounded. Default true.

func (*BuildKitConfig) BuildKitGCEphemeralKeepDuration added in v0.1.8

func (b *BuildKitConfig) BuildKitGCEphemeralKeepDuration() time.Duration

BuildKitGCEphemeralKeepDuration returns the ephemeral-record age limit, default 48h.

func (*BuildKitConfig) BuildKitGCEphemeralMaxUsedBytes added in v0.1.8

func (b *BuildKitConfig) BuildKitGCEphemeralMaxUsedBytes() int64

BuildKitGCEphemeralMaxUsedBytes returns the ephemeral-record ceiling, default 2 GiB.

func (*BuildKitConfig) BuildKitGCKeepDuration added in v0.1.8

func (b *BuildKitConfig) BuildKitGCKeepDuration() time.Duration

BuildKitGCKeepDuration returns the record age limit, default 7 days. A negative value disables the age arm (size bounds still apply).

func (*BuildKitConfig) BuildKitGCMaxUsedBytes added in v0.1.8

func (b *BuildKitConfig) BuildKitGCMaxUsedBytes() int64

BuildKitGCMaxUsedBytes returns the hard ceiling, default 25 GiB.

func (*BuildKitConfig) BuildKitGCMinFreeBytes added in v0.1.8

func (b *BuildKitConfig) BuildKitGCMinFreeBytes() int64

BuildKitGCMinFreeBytes returns the free-space guard, default 20 GiB.

func (*BuildKitConfig) BuildKitGCReservedBytes added in v0.1.8

func (b *BuildKitConfig) BuildKitGCReservedBytes() int64

BuildKitGCReservedBytes returns the never-collected floor, default 5 GiB.

type ClaimRetryToml

type ClaimRetryToml struct {
	// Enabled toggles the retry queue. Nil = default true; operators
	// disable by explicitly setting enabled = false.
	Enabled *bool `toml:"enabled"`

	// MaxAge is the total time budget from first failure to giving up.
	// Accepts Go duration strings ("90m", "1h30m"). Default 90m.
	MaxAge string `toml:"max_age"`

	// Schedule is the ordered backoff ladder. Each entry is the base
	// delay for one attempt; jitter is applied on top. Default is
	// {30s, 1m, 2m, 5m, 10m}. Use a shorter schedule for shorter
	// MaxAge, or a longer one to let jobs marinate through extended
	// outages.
	Schedule []string `toml:"schedule"`

	// Jitter is the +/- fraction applied to each delay (0.0-1.0).
	// Default 0.2 (+/-20%). Set 0 to disable jitter (useful in tests,
	// rarely useful in production).
	Jitter *float64 `toml:"jitter"`
}

ClaimRetryToml configures the scheduler's claim-retry queue.

Enabled defaults to true when the [runner.claim_retry] table is omitted (see RunnerConfig.ClaimRetryEnabled). Losing queued jobs to transient errors is almost never what an operator wants; set enabled = false to restore the pre-existing "log and drop" behavior.

type Config

type Config struct {
	GitHub      GitHubConfig      `toml:"github"`
	GitHubExtra []GitHubConfig    `toml:"github_extra"` // additional GitHub owners, each with its own auth ([[github_extra]])
	Forgejo     ForgejoConfig     `toml:"forgejo"`
	Gitea       GiteaConfig       `toml:"gitea"`
	GitLab      GitLabConfig      `toml:"gitlab"`
	Woodpecker  WoodpeckerConfig  `toml:"woodpecker"`
	Webhook     WebhookConfig     `toml:"webhook"`
	Containerd  ContainerdConfig  `toml:"containerd"`
	Network     NetworkConfig     `toml:"network"`
	VM          VMConfig          `toml:"vm"`
	Dind        DindConfig        `toml:"dind"`
	BuildKit    BuildKitConfig    `toml:"buildkit"`
	ImageGC     ImageGCConfig     `toml:"image_gc"`
	ModuleProxy ModuleProxyConfig `toml:"module_proxy"`
	Runtime     RuntimeConfig     `toml:"runtime"`
	Runner      RunnerConfig      `toml:"runner"`
	Metrics     MetricsConfig     `toml:"metrics"`
	Dispatch    DispatchConfig    `toml:"dispatch"`
	Log         LogConfig         `toml:"log"`
}

func Load

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

func (*Config) EnsureDispatchToken added in v0.1.4

func (c *Config) EnsureDispatchToken(path string, gen func() (string, error)) error

EnsureDispatchToken guarantees c.Dispatch.Token is set, minting a fresh token and persisting it into the config file at path when it is currently empty. The generated token is appended as a "[dispatch]" block so the operator's existing content (and comments) are preserved; the persisted file then rides into the Linux VM through the normal config-delivery channel, giving the host client and the in-VM dispatch server the same shared secret.

It is a no-op when a token is already set (whether from config or a prior mint). Only the host that boots the VM needs to call this; the in-VM daemon simply reads the token that was delivered inside config.toml.

gen supplies the token bytes (crypto/rand-backed in production); injecting it keeps this package free of a scheduler import and makes the persistence path testable.

func (*Config) GitHubTargets added in v0.1.3

func (c *Config) GitHubTargets() []GitHubConfig

GitHubTargets returns every configured GitHub target: the primary [github] (when set) followed by any [[github_extra]] entries. Each target carries its own owner + auth, so ephemerd can serve multiple owners at once (e.g. an org via a GitHub App and a personal account via a PAT).

func (*Config) Logger

func (c *Config) Logger() *slog.Logger

func (*Config) PinnedRunnerImages added in v0.1.8

func (c *Config) PinnedRunnerImages() []string

PinnedRunnerImages returns every image ref this node is configured to run runners from: the [runner] default, every per-repo [runner.images.<repo>] entry, and each provider's per-OS defaults.

The image GC treats these as never-evictable. Dropping one guarantees a re-pull on the very next job of that shape, which is exactly the network thrash pressure-triggered GC exists to avoid; they are also the images most likely to look "stale" to an LRU sweep on a node that has been busy with third-party container: images.

Refs are returned in a stable order with duplicates removed.

func (*Config) Provider

func (c *Config) Provider() string

Provider returns the name of the first configured forge provider. Returns "github" by default for backward compatibility.

func (*Config) Providers

func (c *Config) Providers() []string

Providers returns all configured provider names. A provider is "configured" if its credential/URL fields are set.

type ContainerdConfig

type ContainerdConfig struct {
}

type DindConfig

type DindConfig struct {
	Enabled bool `toml:"enabled"` // mount /var/run/docker.sock with a fake Docker API

	// CachePruneInterval is how often the per-repo image cache pruner runs.
	// Accepts standard Go duration strings ("24h", "30m"). Set to 0 to
	// disable pruning entirely. Default 24h.
	CachePruneInterval time.Duration `toml:"cache_prune_interval"`

	// CacheMaxAge is an OPTIONAL age backstop for cached image records:
	// any record whose ephemerd.io/last-accessed label (or UpdatedAt as
	// fallback) is older than this gets removed on the next prune pass.
	// Containerd's content GC then reclaims the unreferenced blobs.
	//
	// BEHAVIOR CHANGE: this used to default to 168h (7 days) and was the
	// only image eviction mechanism ephemerd had. It now defaults to 0
	// (disabled), because disk pressure — not age — is the correct
	// trigger: evicting a warm cache while the disk is half empty just
	// forces re-downloads. See [image_gc], which supersedes this for
	// both the dind cache namespaces and the main runtime namespace.
	//
	// An explicit value is still honored, and still applies only to the
	// ephemerd-dind-cache-* namespaces. Empty cache namespaces are
	// reaped on every prune pass regardless of this setting.
	CacheMaxAge time.Duration `toml:"cache_max_age"`

	// AllowPrivileged controls whether `docker run --privileged` (or
	// HostConfig.Privileged=true / HostConfig.CapAdd) from inside a job
	// is honored. When true, a sibling container can request the full
	// elevation stack (all caps, all devices, seccomp/apparmor off,
	// writable sysfs/cgroupfs) — needed for KIND clusters, nested
	// containerd, /dev/fuse-style mounts, etc. When false, such requests
	// are rejected with HTTP 403.
	//
	// SECURITY: a privileged sibling container is effectively root on
	// whatever host runs the containerd that backs dind. On Windows and
	// macOS hosts that backing containerd lives inside a managed Linux
	// VM (WSL2 / Hyper-V / Vz), so an escape only reaches the VM. On a
	// Linux host with no VM fence, an escape reaches the bare-metal host
	// — set this to false unless every workload is trusted.
	//
	// Use the pointer form so an empty/missing TOML key is
	// distinguishable from an explicit `allow_privileged = false`. See
	// ResolvedAllowPrivileged for the default policy.
	AllowPrivileged *bool `toml:"allow_privileged"`
}

DindConfig configures the fake Docker daemon mounted into job containers.

func (*DindConfig) DindCacheMaxAge

func (d *DindConfig) DindCacheMaxAge() time.Duration

DindCacheMaxAge returns the optional age backstop for dind cache namespaces. Zero means disabled, which is now the default — see DindConfig.CacheMaxAge for why. A negative value is also treated as disabled so a typo cannot evict everything.

func (*DindConfig) DindCachePruneInterval

func (d *DindConfig) DindCachePruneInterval() time.Duration

DindCachePruneInterval returns the prune interval with the default applied when unset (or set to 0).

func (*DindConfig) ResolvedAllowPrivileged

func (d *DindConfig) ResolvedAllowPrivileged() bool

ResolvedAllowPrivileged returns whether privileged dind sibling containers are allowed, applying the secure default when the operator hasn't set the key explicitly.

Default policy: false on ALL platforms. Privileged is opt-in everywhere. A privileged sibling container is effectively root on whatever host runs the backing containerd, so shipping it on-by-default is unsafe even where a VM fence limits the blast radius (Windows/macOS) — an operator that needs it (KIND clusters, nested containerd, /dev/fuse) opts in explicitly with `allow_privileged = true`.

type DispatchConfig added in v0.1.4

type DispatchConfig struct {
	// Token is the shared bearer token the host client presents and the in-VM
	// dispatch server requires on every RPC (constant-time compared). It is
	// delivered to the VM inside config.toml through the existing config-share
	// channel, so both sides converge on the same value with no extra plumbing.
	//
	// Leave empty for the daemon to mint a 256-bit token on first run and
	// persist it back into config.toml (see EnsureDispatchToken) — operators
	// normally never set this by hand. Set it explicitly only to pin a known
	// value (e.g. to rotate, or in a config baked read-only).
	Token string `toml:"token"`
}

DispatchConfig configures the host<->VM dispatch gRPC channel. On Windows (Hyper-V) and macOS (Vz) hosts, Linux jobs run inside a long-lived Linux VM and the host dispatches Create/Wait/Destroy over gRPC. That surface can spawn and kill jobs with a caller-supplied image, so it is authenticated with a shared bearer token.

type DispatchPolicyConfig added in v0.1.4

type DispatchPolicyConfig struct {
	// AllowedRepos, when non-empty, restricts dispatch to jobs whose repository
	// name is in this list. Empty means "all tracked repos" (current behavior).
	// Repo names are matched exactly against the webhook's repository name (the
	// same value used by [github].repos).
	AllowedRepos []string `toml:"allowed_repos"`

	// RequiredLabels, when non-empty, requires an incoming job to carry at
	// least one of these labels (beyond the mandatory "self-hosted" gate)
	// before it dispatches. Empty means "no extra label requirement". Use this
	// to pin dispatch to jobs that opt in with a distinctive label an outside
	// fork is unlikely to request (e.g. "ephemerd").
	RequiredLabels []string `toml:"required_labels"`
}

DispatchPolicyConfig is an opt-in allowlist restricting which webhook jobs may dispatch a runner. All fields default to "no restriction"; setting any field narrows dispatch. It is a safety net layered under GitHub's outside- collaborator approval gate, not a substitute for it.

func (DispatchPolicyConfig) IsZero added in v0.1.4

func (d DispatchPolicyConfig) IsZero() bool

IsZero reports whether the policy imposes no restrictions (the default).

type ForgejoConfig

type ForgejoConfig struct {
	InstanceURL         string   `toml:"instance_url"`          // Forgejo instance URL (e.g., "https://codeberg.org")
	Token               string   `toml:"token"`                 // runner registration token from Forgejo admin
	Owner               string   `toml:"owner"`                 // org or user (empty = instance-level runner)
	Repos               []string `toml:"repos"`                 // limit to specific repos (empty = all)
	Labels              []string `toml:"labels"`                // runner labels (default: ["ubuntu-latest:docker://<job_image>"])
	DefaultImage        string   `toml:"default_image"`         // runner daemon image (default: "data.forgejo.org/forgejo/runner:12")
	DefaultImageLinux   string   `toml:"default_image_linux"`   // per-OS Linux runner image (wins over default_image when set)
	DefaultImageWindows string   `toml:"default_image_windows"` // per-OS Windows runner image
	JobImage            string   `toml:"job_image"`             // job execution image (default: "gitea/runner-images:ubuntu-24.04")
}

ForgejoConfig configures the Forgejo Actions provider. Set instance_url and token to enable Forgejo instead of GitHub. Uses forgejo-runner binary with one-job --handle mode.

Forgejo's runner daemon (DefaultImage) is Linux-only; setting DefaultImageWindows is allowed for completeness but no upstream Windows build of forgejo-runner exists today.

func (*ForgejoConfig) DefaultImageFor

func (f *ForgejoConfig) DefaultImageFor(os string) string

DefaultImageFor returns the provider-level default for the given OS.

type GitHubConfig

type GitHubConfig struct {
	// Authentication: either a PAT or GitHub App
	Token          string `toml:"token"`
	AppID          int64  `toml:"app_id"`
	InstallationID int64  `toml:"installation_id"`
	PrivateKeyPath string `toml:"private_key_path"`

	// Which org/user and repos to register runners for
	Owner string   `toml:"owner"`
	Repos []string `toml:"repos"`

	// DispatchPolicy is an OPTIONAL defense-in-depth allowlist that gates which
	// incoming workflow_job webhook events are allowed to dispatch a runner.
	// It is IN ADDITION to (not a replacement for) GitHub's own "require
	// approval for outside collaborators" setting, which remains the primary
	// control against fork-PR abuse. Empty (the default) preserves today's
	// behavior: any tracked repo with a self-hosted label dispatches.
	DispatchPolicy DispatchPolicyConfig `toml:"dispatch_policy"`

	// Job discovery: polling interval (default "30s")
	PollInterval string `toml:"poll_interval"`

	// DefaultImage is the legacy single-image override (Linux only).
	// Kept for backward compatibility — prefer DefaultImageLinux /
	// DefaultImageWindows for new configs. When DefaultImageLinux is empty
	// and this is set, it's treated as the Linux default.
	// Linux fallback (when nothing is set): "ghcr.io/actions/actions-runner:latest".
	DefaultImage string `toml:"default_image"`

	// DefaultImageLinux is the provider-level default image for Linux jobs.
	// Per-repo entries in [runner.images.<repo>].linux win over this.
	DefaultImageLinux string `toml:"default_image_linux"`

	// DefaultImageWindows is the provider-level default image for Windows
	// jobs. Per-repo entries in [runner.images.<repo>].windows win over
	// this. Falls through to the runtime's host-matched servercore default
	// (pkg/runtime/image_windows.go) when unset.
	DefaultImageWindows string `toml:"default_image_windows"`
}

func (*GitHubConfig) DefaultImageFor

func (g *GitHubConfig) DefaultImageFor(os string) string

DefaultImageFor returns the provider-level default image for the given OS. Resolution: per-OS field → legacy DefaultImage (Linux only) → empty. Empty means "no provider default — let the runtime pick its OS-native fallback (e.g. mcr.microsoft.com/windows/servercore:ltsc20XX on Windows)".

func (*GitHubConfig) ParsedPollInterval

func (g *GitHubConfig) ParsedPollInterval() time.Duration

ParsedPollInterval returns the poll interval as a time.Duration.

type GitLabConfig

type GitLabConfig struct {
	InstanceURL         string   `toml:"instance_url"`          // GitLab instance URL (e.g., "https://gitlab.com")
	Token               string   `toml:"token"`                 // runner authentication token (glrt-xxx for GitLab 16+)
	Tags                []string `toml:"tags"`                  // runner tags for job matching
	DefaultImage        string   `toml:"default_image"`         // runner image (default: "ghcr.io/ephpm/runner-gitlab:latest")
	DefaultImageLinux   string   `toml:"default_image_linux"`   // per-OS Linux runner image (wins over default_image when set)
	DefaultImageWindows string   `toml:"default_image_windows"` // per-OS Windows runner image
}

GitLabConfig configures the GitLab CI provider. Set instance_url and token to enable GitLab instead of GitHub.

func (*GitLabConfig) DefaultImageFor

func (g *GitLabConfig) DefaultImageFor(os string) string

DefaultImageFor returns the provider-level default for the given OS.

type GiteaConfig

type GiteaConfig struct {
	InstanceURL         string   `toml:"instance_url"`          // Gitea instance URL (e.g., "https://gitea.example.com")
	Token               string   `toml:"token"`                 // runner registration token from Gitea admin
	Owner               string   `toml:"owner"`                 // org or user (empty = instance-level runner)
	Repos               []string `toml:"repos"`                 // limit to specific repos (empty = all)
	Labels              []string `toml:"labels"`                // runner labels (default: ["ubuntu-latest:docker://<job_image>"])
	DefaultImage        string   `toml:"default_image"`         // runner daemon image (default: "docker.io/gitea/act_runner:latest")
	DefaultImageLinux   string   `toml:"default_image_linux"`   // per-OS Linux runner image (wins over default_image when set)
	DefaultImageWindows string   `toml:"default_image_windows"` // per-OS Windows runner image
	JobImage            string   `toml:"job_image"`             // job execution image (default: "gitea/runner-images:ubuntu-24.04")
}

GiteaConfig configures the Gitea Actions provider. Set instance_url and token to enable Gitea instead of GitHub. Uses act_runner binary with --ephemeral mode.

func (*GiteaConfig) DefaultImageFor

func (g *GiteaConfig) DefaultImageFor(os string) string

DefaultImageFor returns the provider-level default for the given OS.

type ImageGCConfig added in v0.1.8

type ImageGCConfig struct {
	// Enabled toggles collection. Nil = default true; operators disable
	// by explicitly setting enabled = false.
	Enabled *bool `toml:"enabled"`

	// CheckInterval is how often disk usage is sampled. The sample is one
	// statfs-class syscall (microseconds), so this can be short. Default
	// 60s. Set to 0 to disable the periodic sweep — the pre-pull headroom
	// check still runs.
	CheckInterval time.Duration `toml:"check_interval"`

	// HighWatermarkPercent is the disk used-percentage at which a
	// collection pass triggers. Default 85. Set to 0 to disable the
	// percentage arm and rely on min_free_gb alone.
	HighWatermarkPercent float64 `toml:"high_watermark_percent"`

	// LowWatermarkPercent is the used-percentage a triggered pass evicts
	// down to. Default 70. Must be below HighWatermarkPercent; a value at
	// or above it degrades to single-threshold behavior.
	LowWatermarkPercent float64 `toml:"low_watermark_percent"`

	// MinFreeGB is the absolute free-space floor, in GiB, below which a
	// pass triggers regardless of percentage. Default 20. Set to 0 to
	// disable the absolute arm.
	MinFreeGB uint64 `toml:"min_free_gb"`

	// TargetFreeGB is the free space, in GiB, a pass triggered by
	// MinFreeGB evicts back up to. Defaults to twice MinFreeGB, mirroring
	// the default 85%/70% percentage gap. Values below MinFreeGB are
	// clamped up to it.
	TargetFreeGB uint64 `toml:"target_free_gb"`

	// MaxAge is an OPTIONAL age backstop applied to every collected
	// namespace: records idle longer than this are evicted whether or not
	// the disk is under pressure. Default 0 (disabled) — age is
	// deliberately NOT the primary mechanism, because evicting a warm
	// cache while the disk is half empty just forces re-downloads.
	MaxAge time.Duration `toml:"max_age"`
}

ImageGCConfig configures disk-pressure-triggered container image garbage collection.

Model (kubelet's): disk pressure is the TRIGGER, least-recently-used is the ORDER. Collection starts when usage crosses a high watermark and evicts LRU-first until a distinctly lower low watermark is reached, then stops. Two watermarks rather than one line is what prevents thrashing at the boundary.

Two independent trigger arms exist and the more conservative one wins: a percentage (high_watermark_percent) and an absolute floor (min_free_gb). Neither is safe alone — 15% free of a 1 TB node is 150 GB and evicting there is pointless, while 15% free of a 100 GB node is 15 GB, which three concurrent jobs writing ~5 GB of container layers each can eat between ticks. Size min_free_gb relative to runner.max_concurrent times the expected per-job writable layer.

Scope is the main "ephemerd" runtime namespace AND the per-repo "ephemerd-dind-cache-*" namespaces. Images referenced by an existing container, and the node's configured runner images, are never evicted.

func (*ImageGCConfig) ImageGCCheckInterval added in v0.1.8

func (i *ImageGCConfig) ImageGCCheckInterval() time.Duration

ImageGCCheckInterval returns the sampling interval, defaulting to 60s. A negative value is treated as 0 (periodic sweep off).

func (*ImageGCConfig) ImageGCEnabled added in v0.1.8

func (i *ImageGCConfig) ImageGCEnabled() bool

ImageGCEnabled reports whether image garbage collection runs. Default true.

func (*ImageGCConfig) ImageGCHighWatermarkPercent added in v0.1.8

func (i *ImageGCConfig) ImageGCHighWatermarkPercent() float64

ImageGCHighWatermarkPercent returns the trigger percentage, defaulting to 85. Out-of-range values fall back to the default rather than failing startup — a misconfigured watermark should not stop the node collecting.

func (*ImageGCConfig) ImageGCLowWatermarkPercent added in v0.1.8

func (i *ImageGCConfig) ImageGCLowWatermarkPercent() float64

ImageGCLowWatermarkPercent returns the stop percentage, defaulting to 70. A value at or above the high watermark is clamped to it by the collector.

func (*ImageGCConfig) ImageGCMaxAge added in v0.1.8

func (i *ImageGCConfig) ImageGCMaxAge() time.Duration

ImageGCMaxAge returns the optional age backstop. Zero (the default) and any negative value mean disabled.

func (*ImageGCConfig) ImageGCMinFreeBytes added in v0.1.8

func (i *ImageGCConfig) ImageGCMinFreeBytes() uint64

ImageGCMinFreeBytes returns the absolute floor in bytes, defaulting to 20 GiB.

func (*ImageGCConfig) ImageGCTargetFreeBytes added in v0.1.8

func (i *ImageGCConfig) ImageGCTargetFreeBytes() uint64

ImageGCTargetFreeBytes returns the absolute target in bytes, defaulting to twice the floor.

type LinuxVMToml

type LinuxVMToml struct {
	// Enabled controls whether this host serves Linux jobs through the VM.
	// Pointer so "not set" is distinguishable from an explicit choice: the
	// platform default is preserved when omitted — ON for darwin (macOS has
	// always booted the VM unconditionally), OFF for windows (the WSL VM has
	// always been opt-in). Set `enabled = false` on a darwin host to stop it
	// claiming Linux jobs — e.g. when a dedicated Linux box now serves the
	// same labels and dual-claiming would orphan a runner per job. (The VM
	// itself still boots on darwin for now; skipping the boot entirely is a
	// separate change.)
	Enabled    *bool  `toml:"enabled"`
	CPUs       uint   `toml:"cpus"`         // virtual CPUs (default: 2)
	MemoryMB   uint64 `toml:"memory_mb"`    // memory in MB (default: 2048)
	DiskSizeGB uint64 `toml:"disk_size_gb"` // sparse disk size in GB (default: 50)
}

LinuxVMToml configures the long-running Linux VM for Linux jobs on Windows (Hyper-V) and macOS (Virtualization.framework) hosts.

func (*LinuxVMToml) ResolvedEnabled added in v0.1.5

func (l *LinuxVMToml) ResolvedEnabled() bool

ResolvedEnabled returns whether the host should serve Linux jobs via the Linux VM, defaulting per platform when the key is not set: darwin true (historical always-on), everything else false (windows is opt-in; on a linux host the field is meaningless — jobs run natively).

type LogConfig

type LogConfig struct {
	Level        string `toml:"level"`
	Format       string `toml:"format"`        // "text" or "json"
	LogRetention string `toml:"log_retention"` // max age for job log files (e.g. "7d", "24h"); default "7d"

	// Writer overrides the log output destination. When nil, logs go to
	// stderr. Set by the Windows Service handler to route logs to the
	// Windows Event Log.
	Writer io.Writer `toml:"-"`
}

func (LogConfig) LogRetentionDuration

func (lc LogConfig) LogRetentionDuration() time.Duration

LogRetentionDuration returns the parsed log retention duration. Supports Go duration strings (e.g. "168h") and a "d" suffix for days (e.g. "7d"). Returns 7 days if the value is empty or invalid.

type MacOSRunnerConfig

type MacOSRunnerConfig struct {
	Mode      string            `toml:"mode"`       // "vm" (default) or "native"
	MaxNative int               `toml:"max_native"` // max concurrent native jobs (default 4)
	User      string            `toml:"user"`       // existing user for native runners (empty = ephemeral per-job user, recommended)
	Repos     map[string]string `toml:"repos"`      // "org/repo" -> "vm" or "native"

	// SandboxStrict switches the native sandbox profile from allow-by-default
	// (deny-list) to deny-by-default (allow-list). Default false. Strict mode
	// is a much stronger posture but requires enumerating every path a GHA
	// runner + toolchain legitimately touches, so it is opt-in and needs a
	// live smoke test on the target host before enabling.
	SandboxStrict bool `toml:"sandbox_strict"`

	// MaxProcesses caps the number of processes (ulimit -u) a native job may
	// spawn, providing fork-bomb defense. Default 2048 (generous — clang/php
	// fork heavily). 0 = unlimited (no ulimit set). Note: macOS has no
	// cgroups, so RAM and disk cannot be hard-capped on the native path; use
	// the VM path for untrusted memory/disk DoS resistance.
	MaxProcesses *int `toml:"max_processes"`
}

MacOSRunnerConfig controls macOS job routing. It lives under [runner] (not [vm.macos]) because native jobs don't involve VMs.

TOML shape:

[runner.macos]
mode = "vm"         # default mode: "vm" or "native"
max_native = 4      # max concurrent native jobs
# user = "ciuser"   # optional: existing user for native runners.
#                   # Default (unset): an ephemeral hidden user is
#                   # created per job and deleted on cleanup.

[runner.macos.repos]
"ephpm/*"           = "native"  # all repos in org
"ephpm/secret-repo" = "vm"     # except this one (exact wins over wildcard)
"someuser/ephemerd" = "vm"     # fork stays on VM

func (*MacOSRunnerConfig) ModeForRepo

func (m *MacOSRunnerConfig) ModeForRepo(repo string) string

ModeForRepo returns "native" or "vm" for the given repo. Resolution order:

  1. Exact match on "org/repo"
  2. Wildcard match on "org/*"
  3. Short-name fallback: if repo has no "/", match any "org/<repo>" key
  4. Top-level mode
  5. Default: "vm"

The short-name fallback exists because some providers (GitHub polling) currently emit event.Repo as just the repo name without the org prefix. Config keys should always use "org/repo" format for disambiguation.

func (*MacOSRunnerConfig) ResolvedMaxNative

func (m *MacOSRunnerConfig) ResolvedMaxNative() int

ResolvedMaxNative returns the max concurrent native macOS jobs, defaulting to 4 if unset or non-positive.

func (*MacOSRunnerConfig) ResolvedMaxProcesses

func (m *MacOSRunnerConfig) ResolvedMaxProcesses() int

ResolvedMaxProcesses returns the ulimit -u value for native jobs. Unset (nil) defaults to 2048. An explicit 0 means unlimited (return 0 so the caller skips the ulimit). A negative value is treated as unlimited.

func (*MacOSRunnerConfig) StrictSandbox

func (m *MacOSRunnerConfig) StrictSandbox() bool

StrictSandbox reports whether deny-by-default sandbox mode is enabled.

type MacOSVMToml

type MacOSVMToml struct {
	// DiskImage is an optional path to a pre-installed macOS VM disk
	// (produced by `ephemerd vm setup-macos` or an operator-supplied
	// restore of an Apple IPSW). If empty, ephemerd downloads the latest
	// Apple-signed IPSW on first boot and installs stock macOS into
	// <data_dir>/vm/macos/base.img. Distinct from the OCI base image
	// overlaid per job — that's fetched from the job's image label.
	DiskImage     string `toml:"disk_image"`
	CPUs          uint   `toml:"cpus"`           // CPUs per VM (default: 4)
	MemoryMB      uint64 `toml:"memory_mb"`      // memory per VM in MB (default: 8192)
	MaxConcurrent int    `toml:"max_concurrent"` // max simultaneous macOS VMs (default: auto-detected from host CPUs)
}

MacOSVMToml configures per-job macOS VMs. macOS jobs always run in a per-job VM on darwin hosts — there's no other way on Apple Silicon — so there's no enable/disable toggle. On non-darwin hosts this block is ignored.

type MetricsConfig

type MetricsConfig struct {
	Enabled bool   `toml:"enabled"`  // enable /metrics endpoint (default false)
	Port    int    `toml:"port"`     // listen port (default 9090)
	Path    string `toml:"path"`     // metrics path (default "/metrics")
	TLSCert string `toml:"tls_cert"` // TLS certificate path (optional)
	TLSKey  string `toml:"tls_key"`  // TLS private key path (optional)
	// BindAddr is the interface the metrics listener binds to. The endpoint is
	// unauthenticated, so it defaults to "127.0.0.1" (loopback only). Set to
	// "0.0.0.0" to scrape from another host, and firewall the port and/or set
	// tls_cert/tls_key when you do.
	BindAddr string `toml:"bind_addr"`
	// ContainerStatsInterval is how often per-container resource samples are
	// taken (CPU, memory). Used both by the host's local sampler ticker and as
	// the cadence the host requests from the in-VM Dispatch StreamContainerStats
	// stream. Default 10s.
	ContainerStatsInterval string `toml:"container_stats_interval"`
}

MetricsConfig configures the Prometheus metrics endpoint. Disabled by default. Set enabled = true to expose /metrics.

func (MetricsConfig) ParsedContainerStatsInterval

func (m MetricsConfig) ParsedContainerStatsInterval() time.Duration

ParsedContainerStatsInterval returns the configured per-container sampling interval, applying the default (10s) when unset. Falls back to the default on parse error rather than failing the daemon — the metric series simply won't update if this is misconfigured, which is not worth aborting startup.

type ModuleProxyConfig

type ModuleProxyConfig struct {
	Enabled  bool   `toml:"enabled"`  // enable Go module caching proxy
	Port     int    `toml:"port"`     // listen port on bridge gateway (default 8082)
	Upstream string `toml:"upstream"` // upstream proxy URL (default "https://proxy.golang.org")
	Cleanup  bool   `toml:"cleanup"`  // wipe cache on shutdown (default true)
}

ModuleProxyConfig configures the Go module caching proxy. When enabled, ephemerd runs a local GOPROXY on the bridge gateway that caches module downloads. Containers receive GOPROXY env var automatically.

type NetworkConfig

type NetworkConfig struct {
	// Subnet is the container subnet.
	//
	// Linux (CNI bridge): auto-selected when empty, avoiding ranges already in
	// use on the host.
	//
	// Windows L2Bridge (l2bridge_egress = true): the CIDR of the LAN the bridge
	// is attached to — containers are peers on it, not behind NAT — declared as
	// the HNS network's Ipam subnet. Auto-derived from the address configured on
	// host_nic when empty, which is the expected setting. Pin it only when the
	// adapter carries a prefix that differs from the LAN you want declared.
	//
	// Windows NAT (the default): not consulted; the HNS NAT network always uses
	// the built-in 10.88.0.0/16.
	Subnet string `toml:"subnet"`

	MTU int `toml:"mtu"` // bridge MTU (auto-detected from host if 0)

	// L2BridgeEgress opts a Windows pool into L2Bridge container networking
	// with VFP-enforced egress filtering, instead of the default HNS NAT.
	// NAT cannot software-filter Windows container egress (VFP does not engage
	// on a NAT network); L2Bridge puts the container on a VFP-managed vSwitch
	// port so per-endpoint ACLs actually enforce. Windows only; ignored on
	// Linux/macOS. Default false — NAT stays the default and this flag flips
	// nothing until an operator opts a pool in.
	L2BridgeEgress bool `toml:"l2bridge_egress"`

	// HostNIC is the host network adapter name the L2Bridge binds onto
	// (e.g. "Ethernet"). REQUIRED when L2BridgeEgress is true — the bridge
	// has no uplink without it. There is no default: the correct NIC name is
	// host-specific (do not assume "Ethernet 2", which was a spike's
	// hot-added test NIC). Ignored when L2BridgeEgress is false.
	HostNIC string `toml:"host_nic"`

	// IPPool is the range of LAN addresses ephemerd may assign to job
	// containers on the L2Bridge path. REQUIRED when L2BridgeEgress is true,
	// with no default, and validated at load time.
	//
	// Why it cannot be inferred: an L2Bridge network must declare a subnet (HNS
	// rejects a subnet-less one outright), and once it has one HNS will assign
	// endpoint addresses from anywhere inside it — on a real LAN, straight into
	// the site DHCP server's scope. ephemerd therefore allocates addresses
	// itself, and only the operator knows which slice of their LAN the DHCP
	// server is configured never to lease.
	//
	// Accepts a CIDR ("192.0.2.192/27" — network and broadcast excluded) or an
	// inclusive range ("192.0.2.200-192.0.2.230"). Must lie inside Subnet and
	// must not contain the host's own address or the LAN gateway. Size it for
	// at least runner.max_concurrent addresses. Ignored when L2BridgeEgress is
	// false.
	IPPool string `toml:"ip_pool"`

	// Gateway is the LAN router the L2Bridge default route points at (the HNS
	// Ipam route next hop). Auto-derived from the default route on HostNIC when
	// empty, which is the expected setting; pin it only when the adapter has no
	// default route of its own or carries more than one.
	//
	// Containers route THROUGH this address while the egress ACLs stop them
	// ADDRESSING it — the gateway gets no allow rule. Windows L2Bridge only.
	Gateway string `toml:"gateway"`

	// PublicDNS is the DNS resolver list handed to L2Bridge containers. Public
	// resolvers keep container DNS off the LAN router (which the egress ACLs
	// block along with the rest of RFC1918). Empty falls back to a built-in
	// public default (1.1.1.1, 8.8.8.8). Only consulted on the L2Bridge path.
	PublicDNS []string `toml:"public_dns"`

	// ExtraAllowedDestinations are additional CIDRs permitted through the
	// L2Bridge egress ACLs at a precedence ABOVE the RFC1918 block (so a
	// listed destination wins over the block). Reserved for future use —
	// default empty, which reproduces the strict Linux end-state (no RFC1918
	// carve-outs at all). Only consulted on the L2Bridge path.
	ExtraAllowedDestinations []string `toml:"extra_allowed_destinations"`
}

NetworkConfig configures container networking.

type OrphanSweepToml

type OrphanSweepToml struct {
	// Enabled toggles the sweep. Nil = default true; operators disable
	// by explicitly setting enabled = false.
	Enabled *bool `toml:"enabled"`

	// Grace is how long a dispatched runner may sit without being
	// assigned a job before it is destroyed and deregistered. Accepts
	// Go duration strings ("10m", "1h"). Default 10m.
	Grace string `toml:"grace"`
}

OrphanSweepToml configures the scheduler's orphaned-runner sweep.

Enabled defaults to true when the [runner.orphan_sweep] table is omitted. The sweep only acts in webhook mode (in polling mode there are no in_progress events, so ephemerd cannot tell an orphaned runner from a busy one) and only for providers that report runner assignments (GitHub).

type RunnerConfig

type RunnerConfig struct {
	MaxConcurrent int      `toml:"max_concurrent"`
	ExtraLabels   []string `toml:"extra_labels"`
	DefaultImage  string   `toml:"default_image"`

	// Images maps repo → OS → image. TOML shape:
	//
	//   [runner.images.ephemerd]
	//   linux   = "ephpm/ephemerd:runner-ci-linux-amd64"
	//   windows = "ephpm/ephemerd:runner-ci-windows"
	//
	// A repo can specify just one OS — the others fall through to the
	// provider per-OS default and then the runtime fallback.
	Images map[string]map[string]string `toml:"images"`

	JobTimeout      string            `toml:"job_timeout"`
	ShutdownTimeout string            `toml:"shutdown_timeout"`
	Windows         WindowsRunnerToml `toml:"windows"`
	MacOS           MacOSRunnerConfig `toml:"macos"`

	// ClaimRetry controls the in-memory retry queue for jobs whose
	// initial claim / provision attempt fails with a transient error
	// (rate limit, 5xx, network). GitHub does not re-deliver
	// workflow_job webhooks, so without this queue any queued job that
	// hit an API blip at claim time would be lost until human
	// intervention.
	ClaimRetry ClaimRetryToml `toml:"claim_retry"`

	// OrphanSweep controls teardown of runners that were dispatched for
	// a job but never observed picking one up. GitHub assigns JIT
	// runners to ANY queued job with matching labels, so runner
	// lifecycle is keyed to the observed assignment; a runner whose
	// intended job went elsewhere and that never got a job of its own is
	// destroyed after the grace window.
	OrphanSweep OrphanSweepToml `toml:"orphan_sweep"`
}

func (*RunnerConfig) ClaimRetryEnabled

func (r *RunnerConfig) ClaimRetryEnabled() bool

ClaimRetryEnabled reports whether the claim retry queue should run. Defaults to true (retries on) when the table is omitted or Enabled is nil; operators opt out with `enabled = false`.

func (*RunnerConfig) ClaimRetryJitter

func (r *RunnerConfig) ClaimRetryJitter() float64

ClaimRetryJitter returns the retry backoff jitter fraction. Defaults to 0.2 (+/-20%). Values outside [0,1] are clamped to the default.

func (*RunnerConfig) ClaimRetryMaxAge

func (r *RunnerConfig) ClaimRetryMaxAge() time.Duration

ClaimRetryMaxAge returns the retry queue give-up threshold. Defaults to 90 minutes when unset or unparseable.

func (*RunnerConfig) ClaimRetrySchedule

func (r *RunnerConfig) ClaimRetrySchedule() []time.Duration

ClaimRetrySchedule returns the retry backoff ladder. Defaults to {30s, 1m, 2m, 5m, 10m} when unset. Unparseable entries are dropped; if all entries fail to parse, the default is returned.

func (*RunnerConfig) ImageForRepo

func (r *RunnerConfig) ImageForRepo(repo string) string

ImageForRepo is the legacy helper kept for callers that haven't migrated. Prefer ImageForRepoOS. Returns the Linux image override (if set), then DefaultImage. Empty when neither is set.

func (*RunnerConfig) ImageForRepoOS

func (r *RunnerConfig) ImageForRepoOS(repo, os string) string

ImageForRepoOS returns the per-repo, per-OS image override, or empty if no override is configured for that combination. Caller falls back to the provider default and then the runtime default.

func (*RunnerConfig) OrphanSweepEnabled

func (r *RunnerConfig) OrphanSweepEnabled() bool

OrphanSweepEnabled reports whether the orphaned-runner sweep should run. Defaults to true when the table is omitted or Enabled is nil; operators opt out with `enabled = false`.

func (*RunnerConfig) OrphanSweepGrace

func (r *RunnerConfig) OrphanSweepGrace() time.Duration

OrphanSweepGrace returns how long a dispatched runner may remain unassigned before the sweep destroys it. Defaults to 10 minutes when unset or unparseable.

func (*RunnerConfig) ParsedJobTimeout

func (r *RunnerConfig) ParsedJobTimeout() time.Duration

ParsedJobTimeout returns the job timeout as a time.Duration.

func (*RunnerConfig) ParsedShutdownTimeout

func (r *RunnerConfig) ParsedShutdownTimeout() time.Duration

ParsedShutdownTimeout returns the shutdown timeout as a time.Duration.

type RuntimeConfig

type RuntimeConfig struct {
	Rlimits RuntimeRlimits `toml:"rlimits"`

	// AllowNewPrivileges controls whether a process in the runner
	// container may gain privileges through execve — i.e. whether setuid
	// binaries and file capabilities take effect. When true the OCI spec
	// carries NoNewPrivileges=false, which is what makes `sudo` work.
	//
	// Jobs generally need this: `sudo apt-get install` is routine in CI,
	// and the stock actions-runner image expects it. That is why the
	// default is true.
	//
	// SECURITY: the runner container already runs as root with a trimmed
	// capability set (see runtime.containerCapabilities), so with
	// NoNewPrivileges=false the seccomp and AppArmor profiles carry
	// nearly all of the containment on their own. A pool that builds
	// untrusted code — fork PRs especially — should set this to false and
	// accept that `sudo` and `apt-get install` stop working there.
	//
	// This is NOT the same knob as dind's allow_privileged: that one
	// gates what a *sibling* container launched through the fake Docker
	// API may request, while this one applies to the runner container
	// itself. See DindConfig.AllowPrivileged.
	//
	// Use the pointer form so a missing TOML key is distinguishable from
	// an explicit `allow_new_privileges = false`. See
	// ResolvedAllowNewPrivileges for the default policy.
	AllowNewPrivileges *bool `toml:"allow_new_privileges"`
}

RuntimeConfig configures behavior of the per-job container runtime — things that apply to the OCI spec rather than to a specific subsystem like dind or networking.

func (RuntimeConfig) ResolvedAllowNewPrivileges added in v0.1.5

func (r RuntimeConfig) ResolvedAllowNewPrivileges() bool

ResolvedAllowNewPrivileges returns whether the runner container may gain privileges through execve, applying the default when the operator hasn't set the key explicitly.

Default policy: true on all platforms — this preserves the behavior ephemerd has always had. Unlike dind's allow_privileged, a secure-by -default of false would break `sudo apt-get install` in every existing workflow, so tightening it is an explicit operator decision per pool.

type RuntimeRlimits

type RuntimeRlimits struct {
	// Nofile is RLIMIT_NOFILE (max open file descriptors). Both soft
	// and hard get set to this value. Default 1024 (containerd default).
	Nofile int64 `toml:"nofile"`
	// Nproc is RLIMIT_NPROC (max processes/threads for the container's
	// user). Both soft and hard get set to this value. Default 1024.
	Nproc int64 `toml:"nproc"`
}

RuntimeRlimits sets POSIX resource limits (RLIMIT_*) on each runner container's OCI spec. Defaults match containerd's built-in OCI spec (nofile=1024, nproc=1024) so an empty config is a no-behavior-change.

Set higher when CI workloads need more file descriptors or processes than containerd's defaults allow. Common case: a build tool calling `ulimit -n 2048` to raise its open-file ceiling. That fails with "Operation not permitted" if the container's hard limit is 1024 — raising the hard limit needs CAP_SYS_RESOURCE, which we deliberately don't grant. Setting nofile higher here lets the same `ulimit` call succeed without granting the capability, because lowering is always allowed and the OCI hard limit is now generous.

func (RuntimeRlimits) Resolved

func (r RuntimeRlimits) Resolved() RuntimeRlimits

Resolved returns the rlimits with defaults filled in for any unset (zero or negative) field. Always returns positive values so callers can blindly emit OCI rlimit entries.

type VMConfig

type VMConfig struct {
	// CrossPlatform enables macOS and Windows VM support. Default true.
	// Set to false for platforms like Gitea/Forgejo that only support
	// Linux runners — this skips macOS image pulls and Windows VM setup.
	CrossPlatform *bool       `toml:"cross_platform"`
	Linux         LinuxVMToml `toml:"linux"`
	MacOS         MacOSVMToml `toml:"macos"`
}

VMConfig configures virtual machines for cross-OS job execution.

func (*VMConfig) CrossPlatformEnabled

func (v *VMConfig) CrossPlatformEnabled() bool

CrossPlatformEnabled returns whether macOS/Windows VM support is enabled. Defaults to true when not set.

type WebhookConfig

type WebhookConfig struct {
	Secret  string `toml:"secret"`   // webhook HMAC secret (auto-generated for managed tunnels; required for "external")
	Port    int    `toml:"port"`     // listen port for health endpoint (default 8080)
	TLSCert string `toml:"tls_cert"` // TLS certificate path (direct TLS, no tunnel)
	TLSKey  string `toml:"tls_key"`  // TLS private key path
	Tunnel  string `toml:"tunnel"`   // "none" (default, polling), "external" (unmanaged ingress), "localtunnel", "ngrok", or "cloudflared"
	// ExternalURL is the public base URL of the externally-managed tunnel
	// (e.g. https://mac.tricorder.cc). When set with tunnel="external",
	// ephemerd registers each tracked repo's webhook to
	// <external_url>/webhook/<provider> using the secret. Ignored for managed
	// tunnels (they use the tunnel's own URL) and for polling.
	ExternalURL      string `toml:"external_url"`
	TunnelURL        string `toml:"tunnel_url"`         // localtunnel: self-hosted server URL
	NgrokAuthtoken   string `toml:"ngrok_authtoken"`    // ngrok auth token (or use NGROK_AUTHTOKEN env)
	TunnelMaxRetries int    `toml:"tunnel_max_retries"` // max consecutive reconnect failures before falling back to polling (default 5)

	// cloudflared: tunnel = "cloudflared". Ephemerd runs cloudflared as a
	// managed subprocess bound to its own lifetime (child gets SIGTERM on
	// parent exit). The tunnel and its DNS record must be provisioned in
	// Cloudflare beforehand — ephemerd only runs the client. The token
	// authenticates and identifies which tunnel to connect.
	CloudflaredToken    string `toml:"cloudflared_token"`    // tunnel run token (or use CLOUDFLARE_TUNNEL_TOKEN env)
	CloudflaredHostname string `toml:"cloudflared_hostname"` // public FQDN of the tunnel (e.g. "runner.example.com"); required for GitHub webhook registration
	CloudflaredVersion  string `toml:"cloudflared_version"`  // pinned cloudflared release (e.g. "2026.6.1"); defaults to a known-good version if empty

	// Pool marks this instance as one member of a pool of ephemerd nodes
	// sharing a single public webhook URL (e.g. cloudflared tunnel replicas
	// behind one hostname). In pool mode webhook registration is
	// adopt-or-create (an existing hook with the same URL is converged, not
	// duplicated), the hook is never deregistered on shutdown (pool-mates
	// still need it), and the startup stale-hook sweep is skipped (it cannot
	// tell a pool-mate's live hook from a stale one). Requires an explicit
	// shared webhook.secret — every pool member must present the same one.
	Pool bool `toml:"pool"`

	// ReconcileInterval controls the webhook-mode reconcile sweep: how often
	// ephemerd re-runs the catch-up poll to pick up jobs that got stranded.
	//
	// This is a LAST-RESORT backstop only for genuinely DROPPED webhook
	// deliveries (network/tunnel loss where GitHub's own redelivery also
	// missed us). The common stranding case — a fungibly-reassigned runner
	// leaving its dispatched job queued — is now healed instantly and
	// event-drivenly by the scheduler on runner exit, without polling. So this
	// runs at a low frequency: empty = default 30m; a zero/negative duration
	// disables it entirely (relying purely on the event-driven path + GitHub's
	// delivery retries).
	ReconcileInterval string `toml:"reconcile_interval"`
}

WebhookConfig configures webhook delivery and tunnel providers. By default, ephemerd uses polling (tunnel = "none"). Set tunnel = "localtunnel", "ngrok", or "cloudflared" for ephemerd to create and manage a tunnel and auto-register the GitHub webhook. Set tunnel = "external" when a tunnel is provided by something else: ephemerd then serves the webhook receiver and disables polling, but does not create a tunnel or register the webhook — that is owned externally, so a matching secret is required.

func (*WebhookConfig) ResolvedReconcileInterval

func (w *WebhookConfig) ResolvedReconcileInterval() time.Duration

ResolvedReconcileInterval returns the webhook-mode reconcile sweep interval: 30m by default (empty or unparseable), the parsed value otherwise, and 0 (disabled) only when explicitly set to a zero/negative duration.

type WindowsRunnerToml

type WindowsRunnerToml struct {
	MemoryMB uint64 `toml:"memory_mb"` // memory in MB (default: 4096)
	CPUs     uint64 `toml:"cpus"`      // virtual CPUs (default: 2)
}

WindowsRunnerToml configures resource limits for Hyper-V isolated Windows runner containers. Without limits Hyper-V containers default to ~1 GB RAM, which is too small for MSVC + parallel cl.exe builds.

func (WindowsRunnerToml) CPUCount

func (w WindowsRunnerToml) CPUCount() uint64

CPUCount returns the CPU count, applying the default if unset.

func (WindowsRunnerToml) MemoryBytes

func (w WindowsRunnerToml) MemoryBytes() uint64

MemoryBytes returns the memory limit in bytes, applying the default if unset.

type WoodpeckerConfig

type WoodpeckerConfig struct {
	ServerURL   string `toml:"server_url"`   // Woodpecker server gRPC URL (e.g., "woodpecker.example.com:9000")
	AgentSecret string `toml:"agent_secret"` // shared secret for agent authentication
}

WoodpeckerConfig configures the Woodpecker CI provider. Set server_url and agent_secret to enable Woodpecker instead of GitHub. Woodpecker requires a forge backend (Gitea/Forgejo) for repo management; ephemerd manages the agent lifecycle, not the server.

Jump to

Keyboard shortcuts

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