githubcache

package
v0.9.1-rc.2 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package githubcache gives the dashboard's GitHub clients an HTTP cache that spends no rate-limit budget on answers it already has.

── WHY, IN ONE PARAGRAPH ───────────────────────────────────────────────────

Every GitHub REST response carries an `ETag` (and usually a `Last-Modified`). If a later request repeats it in `If-None-Match`, GitHub answers `304 Not Modified` with no body — and **a 304 does not count against the rate limit** (https://docs.github.com/rest/using-the-rest-api/best-practices#use-conditional-requests-if-appropriate). So the cheap request is not "the one we skipped", it is "the one we asked conditionally". That is what this package makes every GET do.

── FOLLOWING THE ORG'S PATTERN, NOT INVENTING ONE ──────────────────────────

`environment-controller` already caches GitHub through `sigs.k8s.io/prow`'s `ghcache` (`cmd/main.go`: `ghcache.NewMemCache(baseTransport, 10, …)`), which is built on `github.com/cjwagner/httpcache`. This package uses the same library and reproduces ghcache's two load-bearing decisions:

  1. **Partition the cache by `Authorization`.** ghcache hashes the header and keeps one cache per hash. The dashboard needs this even more than a controller does: `/api/rollouts/*/commits` acts as the VIEWING USER, so a shared cache would let one operator read a private repo through another operator's cached bytes. Partitioning makes that structurally impossible.
  2. **Rewrite the upstream `Cache-Control` so entries are stored but never served blind.** GitHub says `private, max-age=60`, which would let a cache answer for a minute WITHOUT asking. ghcache overwrites it with `no-cache` ("store it, always revalidate"), and errors with `no-store` ("never store it — an error cannot be revalidated into a saving").

What this package does NOT take from ghcache is prow's dependency tree (prometheus, redis, diskv, logrus) and its request coalescing/throttling, which exist for a fleet of bots sharing one token budget. This is a single-process dashboard with one cache per human.

── THE ONE PLACE STALENESS IS ALLOWED, AND WHY IT IS SAFE ──────────────────

A cache that serves a stale answer for MUTABLE data is worse than no cache. So `no-cache` (revalidate every time) is the default for everything — `/user`, a compare against a branch name, anything whose answer can change under a fixed URL.

`GET /repos/{owner}/{repo}/compare/{base}...{head}` where BOTH refs are commit shas is the exception: the set of commits between two immutable objects, and the diffstat over them, cannot change. That range is given a real freshness window (`ImmutableTTL`), so a repeat inside the window costs no request at all — not even a conditional one. The window is deliberately short rather than infinite because the ANSWER is immutable while the READER'S RIGHT TO SEE IT is not: a revoked collaborator keeps their own already-fetched bytes for at most one window.

── WHERE IT SITS IN THE TRANSPORT CHAIN, AND WHY IT MATTERS ────────────────

It goes in `http.Client.Transport`, UNDER go-github's auth round tripper — i.e. `github.NewClient(WithTransport(githubcache.Shared()), WithAuthToken(t))`. `newClient` applies `WithTransport` first and then wraps whatever is there with the token setter, so requests reach this package WITH their final `Authorization` header. Both properties depend on that ordering:

  • the partition key exists at all (the header is set by the layer above), and
  • a client that REFRESHES its credential (a GitHub App installation token is rotated roughly hourly) lands in a new partition instead of reusing an entry fetched under a grant it no longer holds.

Putting the cache ABOVE the auth layer would key every identity's responses together under one unauthenticated-looking request. That is the security bug this comment exists to prevent.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsCommitSHA

func IsCommitSHA(ref string) bool

IsCommitSHA reports whether a git ref is a commit sha — i.e. names an object that can never change — as opposed to a branch or tag name, which moves. This is the whole basis on which anything here is allowed to skip asking GitHub, so it is deliberately strict: hex only, 7 to 40 characters.

func IsImmutableRequest

func IsImmutableRequest(r *http.Request) bool

IsImmutableRequest reports whether a request's answer is fixed for all time: a compare of two commit shas. A compare against a branch or tag name is NOT immutable — the ref moves — and neither is any other endpoint.

func New

func New(base http.RoundTripper, opts Options) http.RoundTripper

New builds a caching round tripper on top of base (nil means http.DefaultTransport).

func PartitionKey

func PartitionKey(r *http.Request) string

PartitionKey is the cache partition a request belongs to: a hash of its credential, so the key can never leak the credential itself.

func Shared

func Shared() http.RoundTripper

Shared is the process-wide transport. One cache for the whole dashboard means two browser tabs asking the same question spend one conditional request.

Types

type Options

type Options struct {
	// Maximum number of Authorization partitions kept at once. Each connected
	// user is one partition; the least recently used is dropped past this.
	MaxPartitions int
	// Byte budget for one partition's stored responses.
	MaxBytesPerPartition int
	// Freshness window granted to an immutable sha-to-sha compare. Zero means
	// "no window" — revalidate everything, always.
	ImmutableTTL time.Duration
}

Options tunes the shared transport. The zero value is filled in by New.

Jump to

Keyboard shortcuts

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