Documentation
¶
Overview ¶
Package ghrelproxy implements proxies.CacheProxy for GitHub release artifacts: the release metadata a build tool reads, and the asset bytes that metadata points at.
WHY THIS EXISTS ¶
The other proxies in this package cache a package registry. This one caches a distribution channel that has no registry: a great deal of CI input arrives as "a file attached to a GitHub release" — prebuilt toolchains, vendored library tarballs, SDK archives — and every job on a node fetches the same bytes over the WAN, every time.
Measured on this fleet (php-sdk, x86_64 gnu, 2026-09-20): 40 source artifacts took 87 s of a 591 s build, a large share of them GitHub release downloads. Nothing about those bytes changes between jobs.
THE INTERCEPTION PROBLEM ¶
Go has GOPROXY, Cargo has source replacement, pip has PIP_INDEX_URL. A GitHub release download has no such redirect — it is a plain https://github.com/... URL baked into whatever tool is fetching it. There are only two ways to intercept that: terminate TLS for job containers (a CA in every image, and a real change to what untrusted job code is exposed to), or have the tool ASK for a proxy.
This proxy takes the second route, in the shape Go already established: ephemerd advertises GHREL_PROXY, and a tool that understands it uses it. A tool that does not is unaffected, and an operator who leaves the proxy off gets upstream — the env var is simply absent, so the usual
base := getenv("GHREL_PROXY") ?: "https://api.github.com"
falls back on its own. Nothing in a workflow has to change, and a build stays correct off-fleet, which a workflow rewritten to hardcode a proxy address would not.
WHAT IS CACHED, AND THE MUTABILITY PROBLEM ¶
Release METADATA is mutable — a release gains assets, a tag can be moved — so it is cached with a short TTL and then revalidated with a conditional GET, exactly as the npm/pip proxies treat their index documents.
Release ASSETS are where this ecosystem differs from every other proxy here, and it is worth being explicit because getting it wrong is silent. PyPI REFUSES to let a file be re-uploaded even after deletion; npm and pub are similar. That is why those proxies mark artifacts Immutable and never look at them again. GitHub enforces nothing of the kind: an asset can be deleted and replaced under the same release, keeping its name and URL.
So assets are NOT immutable by default here. They are cached with a TTL and revalidated, which costs one conditional request per asset per TTL and returns 304 for unchanged bytes — cheap next to re-downloading tens of megabytes, and correct even if someone republishes.
ImmutableAssets turns that off for operators whose publishing discipline guarantees it (assets are never overwritten; a new build gets a new tag). It is faster and it is a promise the proxy cannot verify, which is why it is opt-in rather than the default.
Index ¶
Constants ¶
const ( // DefaultAPIUpstream is GitHub's REST API origin. DefaultAPIUpstream = "https://api.github.com" // DefaultDownloadUpstream is where release assets actually live. // Separate from the API origin because they are different hosts and an // operator pointing at GitHub Enterprise must be able to move both. DefaultDownloadUpstream = "https://github.com" // DefaultMetadataTTL is how long release metadata is served before a // conditional GET. Matches the npm/pip index default: long enough that // a burst of jobs costs one upstream request, short enough that an // asset published minutes ago is not invisible. DefaultMetadataTTL = 5 * time.Minute // DefaultAssetTTL is how long an asset is served before revalidation. // Much longer than metadata: the bytes behind a published asset almost // never change, so this is a backstop against silent replacement rather // than an expectation of churn. DefaultAssetTTL = 24 * time.Hour // DefaultPort continues the sequence npm 8084, pip 8085, pub 8086. DefaultPort = 8087 )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// CacheDir is the on-disk cache root (e.g. <data>/cache/ghrel).
CacheDir string
// APIUpstream overrides the REST API origin (GitHub Enterprise).
APIUpstream string
// DownloadUpstream overrides the asset origin.
DownloadUpstream string
// ListenAddr is the address to bind and advertise.
ListenAddr string
// MetadataTTL is the revalidation interval for release metadata.
// Zero takes DefaultMetadataTTL; negative revalidates every request.
MetadataTTL time.Duration
// AssetTTL is the revalidation interval for asset bytes. Ignored when
// ImmutableAssets is set. Zero takes DefaultAssetTTL.
AssetTTL time.Duration
// ImmutableAssets caches asset bytes permanently and never revalidates
// them. Only correct when assets are never overwritten in place; see
// the package comment.
ImmutableAssets bool
// MaxBytes is the cache disk budget.
MaxBytes int64
// AllowedHosts extends the asset-fetch allowlist.
AllowedHosts []string
// Cleanup wipes the cache on Stop. Default false.
Cleanup bool
Log *slog.Logger
}
Config configures the proxy.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is the GitHub release caching proxy.
func (*Proxy) EnvVars ¶
EnvVars advertises the proxy to job containers.
GHREL_PROXY is a base URL, not a full endpoint, because a consumer needs both routes off it: the API for metadata and /download/ for bytes. A tool that does not recognise the variable ignores it and goes upstream, which is the entire point of advertising rather than intercepting.