Sharing a Build Cache Across Repositories
A GitHub Actions cache entry is addressed inside one repository, so reuse across repositories runs through a registry. Patterns, permissions, and rates.
A GitHub Actions cache entry cannot be restored from a second repository, because the entry is addressed inside the repository that wrote it and the token a run uses to read it carries that same repository scope. Reuse across a repository boundary runs through a registry that both sides authenticate to, or through a builder that holds the layers in one place and is addressed by name from either side.
This guide states what cache scoping allows before any workaround, gives the registry-backed pattern for artifacts that genuinely need to cross the boundary, and works through the permission question that decides the design: which repositories may read what another repository built.
Diagnosis
Start with the address, because the failure is silent and reads as an ordinary cache miss in the log.
A cache entry is addressed by three things. The key string you write in the workflow, the version hash computed over the compression tool and the list of cached paths, and the branch that saved it. The WarpBuild caching documentation states the same three-part scope for WarpBuilds/cache@v1, and GitHub's dependency caching reference documents the branch half as an access restriction: a run restores entries written on its own branch or on the default branch, and sibling branches stay isolated from each other. The cache scope glossary entry unpacks the term, and the answer on branches covers the branch rule on its own.
The repository is above all three. It is the namespace the address lives in rather than a field inside the address, which is why writing an identical key in two repositories produces two unrelated entries instead of one shared entry. There is no permission error to find, because the second repository never asks for the first repository's entry at all.
Here is the full grid of what a cache reaches.
| Boundary | Shared by default | Why |
|---|---|---|
| Two workflows, one repository, same branch | Yes | The workflow file name is absent from the address, covered in the answer on sharing between workflows |
| Two jobs in one workflow | Yes, after the writer's save step finishes | The reader needs the writer in its needs list |
| Feature branch reading the default branch | Yes | Branch access restrictions allow the parent direction only |
| Two sibling feature branches | No | Access restrictions isolate them |
| Two repositories in one organization | No | The entry lives in the writing repository's namespace |
| Two repositories in different organizations | No | Same reason, plus no shared token |
| A fork writing to the upstream cache | No | A fork run cannot write into the base repository |
| Linux entry read by a macOS or Windows job | No | The version hash differs across platforms |
Two consequences follow. A monorepo turns most of this table into the first row, which is one of the reasons teams consolidate. And a reusable workflow does not change anything here, because it executes in the caller's repository, so the caller's namespace is the one that gets read and written.
Fix
Decide what actually has to cross the boundary before picking a mechanism, because the three candidates carry different content and different permission models.
Publish a package or an image, and consume it by digest. This is the registry-backed pattern, and it is the right default for anything a second repository consumes as an input rather than rebuilds: a base image, a compiled shared library, a generated client, a toolchain bundle. One repository builds the artifact, tags it immutably, and pushes it to a registry. Every consumer pulls it by digest. The boundary becomes a published version instead of a cache key, which means the consumer gets reproducibility and an audit trail that a cache entry never offered.
Export a registry-backed layer cache when you stay on runner-local buildx. BuildKit can write its layer cache to a separate registry ref with type=registry, documented in the Docker registry cache backend reference, and any repository with read access to that ref can import it. The cost is that every layer is serialized, pushed, and pulled again on each build.
Point both repositories at one remote Docker builder profile. A builder profile is an account-level resource addressed by profile-name, and it works from WarpBuild runners and from non-WarpBuild runners through an API key, so workflows in different repositories can target the same profile. The layers stay on the builder's own disk, so there is no export and no import step. Remote Docker builders sit alongside snapshot runners, CI observability, an MCP server, and the Action Debugger in the WarpBuild product surface.
| Mechanism | What crosses | Who can read it | Immutable | Best fit |
|---|---|---|---|---|
| Package or image by digest | A published artifact | Whoever has registry read access | Yes, by digest | Shared inputs consumed rather than rebuilt |
| Registry layer cache ref | Serialized BuildKit layers | Whoever has read access to the cache ref | No, the ref is overwritten | Runner-local buildx with a heavy build stage |
| Shared builder profile | Nothing, the layers stay put | Any workflow holding the profile name and API key | No | Many repositories building similar images |
| Cache action | Nothing across repositories | Runs in the writing repository only | No | Dependency trees inside one repository |
The permission question that decides it
Every cross-repository pattern answers one question, and the answer belongs in the design rather than in a follow-up. Which repositories may read what another repository built?
A cache action never had to answer it, because the repository boundary answered it by default. A registry makes the answer explicit and therefore your responsibility. Grant read access per repository rather than to the whole organization, keep the publishing repository as the only writer, and give consumers packages: read with no write scope. Consuming by digest closes the remaining gap, since a mutable tag lets whoever holds write access change what a consumer pulls tomorrow.
WarpBuild's side of the boundary is documented in the security documentation: each runner gets its own ephemeral virtual machine that is destroyed after the build, and cache storage is encrypted and reachable only by your runner. Compliance evidence for that surface is SOC 2 Type 2, with the report available through trust.warpbuild.com.
Configuration
The publishing repository builds once and pushes an immutable tag.
name: publish-base-image
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- name: Log in to the container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push the base image
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/acme/base-toolchain:${{ github.sha }}
profile-name: platform-baseEach consuming repository pins the digest and asks for read scope only.
name: build
on: [push]
permissions:
contents: read
packages: read
jobs:
test:
runs-on: warp-ubuntu-latest-x64-4x
container:
image: ghcr.io/acme/base-toolchain@sha256:2f9c...
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
env:
WARPBUILD_RUNNER_VERIFICATION_TOKEN: ${{ env.WARPBUILD_RUNNER_VERIFICATION_TOKEN }}
steps:
- uses: actions/checkout@v5
- run: make testThe WARPBUILD_RUNNER_VERIFICATION_TOKEN line matters only when steps inside the container also use the cache action. That variable is always present on WarpBuild runners and has to be passed into the container explicitly, along with wget and zstd being available in the image.
To share a builder instead of an artifact, give every repository the same profile-name. The API key is required only when the job runs on a runner that is not a WarpBuild runner.
- name: Build with the shared builder profile
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/acme/service-a:${{ github.sha }}
profile-name: platform-base
api-key: ${{ secrets.WARPBUILD_API_KEY }}Two properties of a shared profile are worth writing down before a rollout. The layer cache on a profile is shared but eventually consistent, so layers written by one concurrent build may be invisible to another build running at the same moment and visible to later builds after synchronization. And the builder cache has a TTL of 10 days, so a profile left unused for longer is reset and the next build on it runs cold.
Keep the cache action for what it is good at, which is dependency trees inside one repository. WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 with the same key, path, and restore-keys inputs. The cache is enabled by default on Linux runners and is not supported on WarpBuild Windows runners. Entries expire 7 days after last use. The guide to persistent caches covers the intra-repository choices in full, and the remote build cache glossary entry covers the tool-level caches that Bazel and Gradle address by input hash rather than by branch.
Cost or Time Model
Rates first, from the pricing page.
| Metric | Hosted | BYOC |
|---|---|---|
| Cache storage | $0.20 per GB-month | Free |
| Cache write, restore, or list | $0.0001 per operation | Free |
| Docker builder, 16 vCPU and 32 GB, 100 GB disk | $0.06 per minute | n/a |
| Docker builder, 32 vCPU and 64 GB, 200 GB disk | $0.12 per minute | n/a |
| warp-ubuntu-latest-x64-4x, 4 vCPU and 16 GB | $0.008 per minute | $0.002 per minute |
| warp-ubuntu-latest-x64-8x, 8 vCPU and 32 GB | $0.016 per minute | $0.002 per minute |
For the baseline on the runner line, warp-ubuntu-latest-x64-4x costs $0.008 per minute against $0.012 per minute for the 4-core GitHub-hosted Linux larger runner at the same 4 vCPU and 16 GB, which is 33 percent lower list price (GitHub billing reference, checked on 2026-08-13).
Worked model
Assumptions, stated so you can substitute your own step timings:
- Six application repositories, 120 workflow runs each per month, so 720 runs.
- Every job in the model runs on
warp-ubuntu-latest-x64-8xat $0.016 per minute. - A shared base toolchain that takes 6.0 minutes to build.
- The base changes 30 times per month.
- Pulling the published image adds 0.5 minutes to a consumer run.
- Each repository holding its own 4 GB cache entry for the same base.
| Line | Rebuild in every repository | Publish once, consume by digest |
|---|---|---|
| Base build minutes | 720 runs x 6.0 = 4,320 | 30 builds x 6.0 = 180 |
| Base build cost | $69.12 | $2.88 |
| Consumer pull minutes | 0 | 720 x 0.5 = 360 |
| Consumer pull cost | $0.00 | $5.76 |
| Cache storage, 4 GB per repository | 24 GB at $0.20 = $4.80 | 4 GB at $0.20 = $0.80 |
| Monthly total | $73.92 | $9.44 |
The money line moves by $64.48 per month. The wall clock line moves further: 5.5 minutes leave the front of each of 720 consumer runs, which is 3,960 minutes, or 66 hours per month that no developer spends watching a check.
Two sensitivities decide whether the pattern is worth the plumbing. Base change frequency sets the publish cost, so a base rebuilt on every commit collapses the gap. Consumer run count sets the saving, so the pattern earns more as repositories are added, since the publish side is fixed.
FAQ
Can two repositories share a GitHub Actions cache?
No. A cache entry is addressed by key, version, and branch inside the repository that wrote it, and the run token that authenticates a restore is scoped to the repository running the workflow. A second repository asking for the same key computes a valid address in its own namespace, misses, and logs a normal cache miss with no permission error. Reuse across repositories has to travel through a registry or a shared builder.
What is the safest way to reuse a build output in another repository?
Publish it once as a versioned package or an OCI image, consume it by digest, and grant read access per repository rather than to the whole organization. The publishing repository stays the only writer, consumers pin an immutable digest, and the permission model answers the question of which repositories may read what another repository built.
What does the cross-repository pattern cost on WarpBuild?
Cache storage is $0.20 per GB-month and each cache write, restore, or list is $0.0001 per operation on hosted runners, and both are free on BYOC. A remote Docker builder profile at 16 vCPU and 32 GB is $0.06 per minute and keeps its layer cache on the builder, so repositories that target the same profile name reuse those layers with no export step.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.