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.

BoundaryShared by defaultWhy
Two workflows, one repository, same branchYesThe workflow file name is absent from the address, covered in the answer on sharing between workflows
Two jobs in one workflowYes, after the writer's save step finishesThe reader needs the writer in its needs list
Feature branch reading the default branchYesBranch access restrictions allow the parent direction only
Two sibling feature branchesNoAccess restrictions isolate them
Two repositories in one organizationNoThe entry lives in the writing repository's namespace
Two repositories in different organizationsNoSame reason, plus no shared token
A fork writing to the upstream cacheNoA fork run cannot write into the base repository
Linux entry read by a macOS or Windows jobNoThe 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.

MechanismWhat crossesWho can read itImmutableBest fit
Package or image by digestA published artifactWhoever has registry read accessYes, by digestShared inputs consumed rather than rebuilt
Registry layer cache refSerialized BuildKit layersWhoever has read access to the cache refNo, the ref is overwrittenRunner-local buildx with a heavy build stage
Shared builder profileNothing, the layers stay putAny workflow holding the profile name and API keyNoMany repositories building similar images
Cache actionNothing across repositoriesRuns in the writing repository onlyNoDependency 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-base

Each 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 test

The 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.

MetricHostedBYOC
Cache storage$0.20 per GB-monthFree
Cache write, restore, or list$0.0001 per operationFree
Docker builder, 16 vCPU and 32 GB, 100 GB disk$0.06 per minuten/a
Docker builder, 32 vCPU and 64 GB, 200 GB disk$0.12 per minuten/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-8x at $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.
LineRebuild in every repositoryPublish once, consume by digest
Base build minutes720 runs x 6.0 = 4,32030 builds x 6.0 = 180
Base build cost$69.12$2.88
Consumer pull minutes0720 x 0.5 = 360
Consumer pull cost$0.00$5.76
Cache storage, 4 GB per repository24 GB at $0.20 = $4.804 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.