Documentation
¶
Index ¶
- type BuildKitConfig
- func (b *BuildKitConfig) BuildKitGCEnabled() bool
- func (b *BuildKitConfig) BuildKitGCEphemeralKeepDuration() time.Duration
- func (b *BuildKitConfig) BuildKitGCEphemeralMaxUsedBytes() int64
- func (b *BuildKitConfig) BuildKitGCKeepDuration() time.Duration
- func (b *BuildKitConfig) BuildKitGCMaxUsedBytes() int64
- func (b *BuildKitConfig) BuildKitGCMinFreeBytes() int64
- func (b *BuildKitConfig) BuildKitGCReservedBytes() int64
- type ClaimRetryToml
- type Config
- type ContainerdConfig
- type DindConfig
- type DispatchConfig
- type DispatchPolicyConfig
- type ForgejoConfig
- type GitHubConfig
- type GitLabConfig
- type GiteaConfig
- type ImageGCConfig
- func (i *ImageGCConfig) ImageGCCheckInterval() time.Duration
- func (i *ImageGCConfig) ImageGCEnabled() bool
- func (i *ImageGCConfig) ImageGCHighWatermarkPercent() float64
- func (i *ImageGCConfig) ImageGCLowWatermarkPercent() float64
- func (i *ImageGCConfig) ImageGCMaxAge() time.Duration
- func (i *ImageGCConfig) ImageGCMinFreeBytes() uint64
- func (i *ImageGCConfig) ImageGCTargetFreeBytes() uint64
- type LinuxVMToml
- type LogConfig
- type MacOSRunnerConfig
- type MacOSVMToml
- type MetricsConfig
- type ModuleProxyConfig
- type NetworkConfig
- type OrphanSweepToml
- type RunnerConfig
- func (r *RunnerConfig) ClaimRetryEnabled() bool
- func (r *RunnerConfig) ClaimRetryJitter() float64
- func (r *RunnerConfig) ClaimRetryMaxAge() time.Duration
- func (r *RunnerConfig) ClaimRetrySchedule() []time.Duration
- func (r *RunnerConfig) ImageForRepo(repo string) string
- func (r *RunnerConfig) ImageForRepoOS(repo, os string) string
- func (r *RunnerConfig) OrphanSweepEnabled() bool
- func (r *RunnerConfig) OrphanSweepGrace() time.Duration
- func (r *RunnerConfig) ParsedJobTimeout() time.Duration
- func (r *RunnerConfig) ParsedShutdownTimeout() time.Duration
- type RuntimeConfig
- type RuntimeRlimits
- type VMConfig
- type WebhookConfig
- type WindowsRunnerToml
- type WoodpeckerConfig
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 (*Config) EnsureDispatchToken ¶ added in v0.1.4
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) PinnedRunnerImages ¶ added in v0.1.8
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.
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 ¶
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:
- Exact match on "org/repo"
- Wildcard match on "org/*"
- Short-name fallback: if repo has no "/", match any "org/<repo>" key
- Top-level mode
- 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 string `toml:"subnet"` // container subnet (auto-selected if empty)
MTU int `toml:"mtu"` // bridge MTU (auto-detected from host if 0)
}
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 ¶
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.