Documentation
¶
Overview ¶
Package actionscache serves GitHub Actions' cache service (v2) from the LAN, backed by a writable OCI registry, so `actions/cache` and everything built on it (Swatinem/rust-cache, setup-* caching) stop moving gigabyte-sized archives over the WAN on every job.
How a job reaches it. The runner exports ACTIONS_RESULTS_URL to every step from the job message, overwriting the container's environment, so ephemerd patches that name out of the job's own copy of Runner.Worker.dll (see pkg/runner.PatchResultsURL) and sets ACTIONS_RESULTS_URL to this server. The cache client then talks to us; GitHub's real address survives under a renamed variable and is what we relay everything that is not the cache to.
Who a request is from. The cache client builds its twirp URLs with an absolute path ("/twirp/..."), which discards any path we could have put a per-job secret in, and it sends GitHub's own runtime token, which we cannot verify. So a request is attributed by its SOURCE ADDRESS: every container of a job joins that job's network under the job's id (networking.JobForIP), and job containers cannot spoof another's address on the bridge. The upload and download URLs we hand out are used verbatim, so those additionally carry an unguessable token.
What a job may touch. Not decided here: a job is unbound until the scheduler binds it to a Scope derived from the forge's record of the job that ACTUALLY took the runner (GitHub hands same-label JIT runners to any queued job, so the job a runner was started for is not evidence). Until then every lookup misses and every save is refused.
Failure is always a cache miss. Unbound, registry down, entry missing: the twirp answer is ok=false (which the client treats as "no cache" or "could not reserve", warning and moving on), never a 5xx, which it would retry five times and then log as an error.
Index ¶
- type Config
- type Proxy
- func (p *Proxy) Addr() string
- func (p *Proxy) Bind(runner string, s Scope)
- func (p *Proxy) Forget(runner string)
- func (p *Proxy) Healthy() bool
- func (p *Proxy) ResultsURL() string
- func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (p *Proxy) Start() error
- func (p *Proxy) Stop() error
- func (p *Proxy) Track(runner string)
- type Scope
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// ListenAddr is the address to bind, normally the bridge gateway
// ("10.88.0.1:8089").
ListenAddr string
// AdvertiseBase is the base URL job containers reach this server at
// ("http://10.88.0.1:8089"). Upload and download URLs are built on it.
AdvertiseBase string
// Registry is the writable registry's base URL ("http://cache:5001").
Registry string
RegistryUser string
RegistryPassword string
// MaxBytesPerRepo caps each forge repo's total cache; the least
// recently used entries are deleted past it. 0 disables eviction.
MaxBytesPerRepo int64
// MaxEntryBytes refuses a single save larger than this. Default 10 GiB
// (GitHub's own per-repo limit).
MaxEntryBytes int64
// UpstreamResultsURL is GitHub's results service, where every request
// that is not the cache (artifact uploads, …) is relayed unchanged.
// Default https://results-receiver.actions.githubusercontent.com.
UpstreamResultsURL string
// JobForIP attributes a request to a job by its source address
// (networking.Manager.JobForIP). Required.
JobForIP func(ip string) string
// HealthInterval is how often the registry is probed; negative disables
// the loop (tests). Default 30s.
HealthInterval time.Duration
// HTTPClient talks to the registry. Default: a client without a total
// timeout (blobs are large) but with dial/response-header bounds.
HTTPClient *http.Client
Log *slog.Logger
}
Config configures the cache server.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is the cache server.
func (*Proxy) Bind ¶
Bind grants a tracked runner its scope. It is a no-op for an untracked runner (the cache was not turned on for it) and refuses to re-bind: a runner runs exactly one job, so a second, different binding is a bug or an attack, and the safe answer is to drop the runner's access entirely.
func (*Proxy) Forget ¶
Forget drops a runner and everything it was granted. Call it when the runner's environment is destroyed.
func (*Proxy) Healthy ¶
Healthy reports whether the registry answered the last probe. The scheduler only turns the cache on for a new job while this is true.
func (*Proxy) ResultsURL ¶
ResultsURL is the ACTIONS_RESULTS_URL value for job containers.
func (*Proxy) ServeHTTP ¶
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP routes cache calls here and relays everything else to GitHub.
type Scope ¶
type Scope struct {
// Repo is the forge-native "owner/repo" the job belongs to. Entries of
// different repos never meet: each repo gets its own registry namespace.
Repo string
// Write is the ref this job saves entries under, e.g. "refs/heads/main"
// or "refs/pull/12/merge". Empty means read-only.
Write string
// Reads are the refs a restore searches, in order — the job's own ref
// first, then the default branch. This mirrors GitHub: a PR can restore
// what main saved, but nothing a PR saves is ever visible to main.
Reads []string
}
Scope is what one job may do with the cache. It is decided by ephemerd from the forge's own record of the job (see the scheduler's in_progress binding), never from anything the job sends.