ghrelproxy

package
v0.2.16 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 12 Imported by: 0

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

View Source
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 New

func New(cfg Config) (*Proxy, error)

New creates a GitHub release caching proxy. Call Start() to begin serving.

func (*Proxy) Addr

func (p *Proxy) Addr() string

Addr returns the bound address.

func (*Proxy) EnvVars

func (p *Proxy) EnvVars() []string

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.

func (*Proxy) Healthy

func (p *Proxy) Healthy() bool

Healthy reports whether the proxy is serving.

func (*Proxy) Name

func (p *Proxy) Name() string

Name returns the proxy name for logs.

func (*Proxy) Start

func (p *Proxy) Start() error

Start begins serving. Returns after the listener is bound.

func (*Proxy) Stop

func (p *Proxy) Stop() error

Stop shuts the proxy down.

Jump to

Keyboard shortcuts

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