Skip to content

About

Free cross-repo CI build cache for the CIRIS substrate, backed entirely by GitHub Container Registry (GHCR).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CIRISCache

Free, cross-repo CI build cache for the CIRIS substrate — backed entirely by GitHub Container Registry (GHCR).

No third-party account, no S3, no stored secret beyond the built-in GITHUB_TOKEN. Public GHCR packages get unlimited storage and bandwidth for free, and they're reachable from any repo — which is exactly what a shared build cache needs and what GitHub's own actions/cache (repo-scoped, 10 GB) cannot do.

It's two tiny composite actions wrapping ORAS push/pull of a tarball to/from a GHCR package:

  • CIRISAI/CIRISCache/restore@v1 — pull + extract the cache before a build.
  • CIRISAI/CIRISCache/save@v1 — tar + push the cache after a build.

Quick start

permissions:
  contents: read
  packages: write          # only the job that SAVEs needs this

steps:
  - uses: actions/checkout@v4
  - uses: dtolnay/rust-toolchain@stable

  - uses: CIRISAI/CIRISCache/restore@v1
    id: cache
    # public package → anonymous pull, no token needed

  - run: cargo build --release   # your build

  - uses: CIRISAI/CIRISCache/save@v1
    if: github.ref == 'refs/heads/main'   # save on the warmer, not every PR/tag
    with:
      token: ${{ secrets.GITHUB_TOKEN }}

Restore is anonymous (public package). Save uses the job's own GITHUB_TOKEN — no PAT, no org secret — once the saving repo has been granted write access to the package (one-time, see docs/DESIGN.md).

Cross-repo sharing

By default the cache key is auto-derived per repo (rustc + Cargo.lock hash) — i.e. a drop-in rust-cache that lives in GHCR instead of the per-repo GHA cache (so: unlimited storage, survives across the tag-ref boundary).

To let one repo's compile feed another (e.g. warm the substrate once, restore it everywhere), pass an explicit key derived from shared inputs — the substrate triple + rustc — so every repo computes the same key:

  - uses: CIRISAI/CIRISCache/restore@v1
    with:
      key: substrate-persist10.2.0-edge7.0.10-verify7.4.0-rustc1.90

See docs/DESIGN.md for the design, the honest granularity ceiling (this is coarse whole-dir caching, not sccache per-crate addressing), and the one-time package setup.

Multi-key: push once, alias the rest (alias-keys)

When one build's target/ must be reachable under several keys — e.g. a versioned exact-pin and a rolling phead for cross-version incremental restore — don't run save twice. A second save re-tars, re-gzips and re-uploads the byte-identical directory (a large target/ serializes several minutes each). Instead push once and pass the extra tags to alias-keys; they are applied as a registry-side oras tag of the pushed digest (~1 s, no blob re-upload):

  - uses: CIRISAI/CIRISCache/save@v1
    with:
      key: ciris-substrate-v1-linux-x86_64-...-p17.5.0-v10.2.0   # primary blob
      alias-keys: ciris-substrate-v1-linux-x86_64-...-phead-v10.2.0   # retag, no re-push
      token: ${{ secrets.GITHUB_TOKEN }}

An alias that already exists is moved to the new manifest (correct for a rolling phead). A retag failure is non-fatal — the primary save still counts.

Did the save publish? (published, strict)

A save is fail-open by default: if any stage fails (oras login, the max-size-mb prune, tar, oras push or its 20-minute timeout) the step emits a ::warning:: naming the stage, sets published=false and reason, and exits 0 so the build stays green. A retag failure after the push is also a warning, and published stays true (the primary key is up).

output value
published true only when the blob was pushed under key
reason why not, e.g. prune failed (exit 2), oras push failed (exit 124), nothing to cache; empty when published
key the normalized key the save targeted

Check published, not the step's outcome. Fail-open means a failed save does not fail the job, and neither does a caller's continue-on-error: true. Before CIRISCache#3 a save could fail on every run for weeks behind continue-on-error with nothing but a hidden red step to show for it. You no longer need continue-on-error on save; if you keep it, the warning is still emitted. Where a missing blob must red the build (a release that downstream repos exact-pin), set strict: "true": the failure becomes an ::error:: and fails the step. Or gate a later step on the output:

  - uses: CIRISAI/CIRISCache/save@v1
    id: save
    with:
      key: ${{ env.CACHE_KEY }}
      token: ${{ secrets.GITHUB_TOKEN }}
  - if: steps.save.outputs.published != 'true'
    run: |
      echo "::error::cache not published: ${{ steps.save.outputs.reason }}"
      exit 1

Tests

bash tests/prune_test.sh and bash tests/save_step_test.sh (bash, coreutils, GNU find; no registry) run on every PR via .github/workflows/test.yml. The first checks the LRU prune under the step's bash -e -o pipefail, with SIGPIPE both default and ignored as on the runner. The second runs the save step's script verbatim with a stub oras and checks every failure path's annotation and outputs. selftest.yml is the live GHCR round-trip.

Why not R2 / S3 / sccache-remote?

Those work, but each adds an external account + credential = another point of failure. CIRISCache keeps the whole thing inside GitHub's trust domain. The trade is granularity: a content-addressed remote (sccache → S3/R2) dedups per crate; CIRISCache dedups per cache key (a whole tarball). For the CIRIS substrate — where every repo pins the same triple — a shared-key tarball captures the bulk of the win without leaving GitHub.

About

Free cross-repo CI build cache for the CIRIS substrate, backed entirely by GitHub Container Registry (GHCR).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages