# cacher S3-backed CI cache helper. A single static Go binary that downloads, uploads, lists, and invalidates cached build artifacts in any S3-compatible bucket. Built for [builds.sr.ht](https://builds.sr.ht) but works anywhere you can run a binary and reach an S3 endpoint. Replaces the typical CI cache shell loop: ```sh # before — install awscli, write ~/.aws/config, then in every task: if aws s3api head-object --bucket "$B" --key "$K" >/dev/null 2>&1; then aws s3 cp "s3://$B/$K" "$out" else curl -sSL "$url" -o "$out" aws s3 cp "$out" "s3://$B/$K" fi ``` ```sh # after — one binary, one config, one command: cacher download "$key" "$out" --url "$url" ``` Same collapse for docker images: ```sh cacher docker download "$key" "$image:tag" --pull ``` Releases and changelog: [bigbes.pages.srht.bigb.es/ci-cacher](https://bigbes.pages.srht.bigb.es/ci-cacher/). ## Install ```sh # Pre-built binary (latest tag). Substitute your platform: # cacher-linux-amd64, cacher-linux-arm64, # cacher-darwin-amd64, cacher-darwin-arm64 wget https://bigbes.pages.srht.bigb.es/ci-cacher/cacher-linux-amd64 -O ~/.local/bin/cacher chmod +x ~/.local/bin/cacher # Pin to a known sha256 (checksums.txt lives next to the binaries): wget https://bigbes.pages.srht.bigb.es/ci-cacher/cacher-linux-amd64 -O ~/.local/bin/cacher echo " ~/.local/bin/cacher" | sha256sum -c # From source: go install go.bigb.es/cacher@latest ``` Verify with `cacher version`. ## Setup `cacher init` writes `~/.config/cacher/config.toml` and runs a smoke test to fail fast on bad credentials. Run it once per CI job after secrets are mounted: ```sh cacher init \ --endpoint https://s3.example.com \ --region us-east-1 \ --bucket ci-cache \ --prefix my-project \ --key-file ~/.s3-cache-key-id \ --secret-file ~/.s3-cache-key-secret ``` Credentials resolve as **`CACHER_S3_KEY_ID` / `CACHER_S3_SECRET` env vars > `--key-file` / `--secret-file` > files recorded in config**. Use the env vars for local-dev runs; use the files in CI where secrets are mounted. `cacher doctor` repeats the smoke test (HEAD bucket + 1-byte write/read/delete canary) and prints credential-length diagnostics without leaking the secret. ## Usage ### Single file — fetch-or-download ```sh cacher download go-1.26.3.tar.gz /tmp/go.tar.gz \ --url https://go.dev/dl/go1.26.3.linux-amd64.tar.gz \ --sha256 abc123… ``` Cache HIT: pulls from S3. Cache MISS: GETs the URL, verifies the optional sha256, writes to the destination, **and uploads to S3** so the next run hits. ```sh cacher upload my-key /path/to/artifact # skip if present cacher upload my-key /path/to/artifact --force # overwrite cacher exists my-key # exit 0 hit, 1 miss cacher list my-prefix # /-delimited (aws s3 ls) cacher list my-prefix --recursive # flat cacher list --root # ignore configured prefix cacher delete my-key # invalidate ``` ### Docker images — streamed save/load For an image pulled from a registry, the cache-or-pull pattern is a single command: ```sh # Cache HIT → docker load from S3. MISS → docker pull + seed S3 + tag stays local. cacher docker download "docker/dxflrs-garage-v2.3.0.tar.zst" \ dxflrs/garage:v2.3.0 --pull ``` For images you build locally, drive the cache by hand — same primitives, key by Dockerfile content: ```sh KEY=$(cacher key "images/{hash}.tar.zst" --hash-from Dockerfile) if ! cacher docker exists "$KEY"; then docker build -t myimage:latest . cacher docker upload "$KEY" myimage:latest else cacher docker download "$KEY" myimage:latest fi ``` The save/load pipeline is fully streamed — `docker save | zstd | s3` and the inverse, no on-disk tempfile. The zstd codec is pure Go ([klauspost/compress](https://github.com/klauspost/compress)), so no external `zstd` binary is needed. ### Directory caching — the real CI speedup Cache resolved trees keyed by a lockfile hash. Skip resolution entirely when nothing changed: ```sh # Go module cache, keyed by go.sum: KEY=$(cacher key "go-mod/{hash}.tar.zst" --hash-from go.sum) cacher dir download "$KEY" ~/go/pkg/mod 2>/dev/null || { go mod download cacher dir upload "$KEY" ~/go/pkg/mod } # Lua rocks tree, keyed by rockspec: KEY=$(cacher key "rocks/{hash}.tar.zst" --hash-from project-scm-1.rockspec) cacher dir download "$KEY" .rocks 2>/dev/null || { tt rocks install ... cacher dir upload "$KEY" .rocks } ``` ### Key derivation `--hash-from ` is repeatable; files are hashed by content, directories are hashed by recursively walking entries in sorted relative-path order. The resulting hex digest is truncated to `--hash-length` characters (default 16, matching the `sha256sum file | cut -c1-16` convention). For a single file path, the digest equals `sha256sum file | head -c 16` exactly — so you can migrate existing keys without recomputing them. Substitution into the key template: | Template | --hash-from | Result | |--------------------------------|---------------|-------------------------------------| | `img/{hash}.tar.zst` | `Dockerfile` | `img/abcd1234….tar.zst` | | `img/build.tar.zst` | `Dockerfile` | `img/build-abcd1234….tar.zst` | | `bin/cacher` + `--arch-suffix` | (none) | `bin/cacher-linux-amd64` | `cacher key