diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2eaa3353a..d5cec9993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,9 @@ jobs: - name: checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Expose GitHub tokens for caching + uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0 + - name: Update Docker run: | set -e @@ -294,7 +297,13 @@ jobs: run: | set -eu - docker buildx bake frontend + # gha cache: buildx auto-detects the runtime token/url from the + # environment exposed by ghaction-github-runtime above. The frontend + # is built per-run so it uses a gha scope; the worker reuses ghcr + # caches below. + docker buildx bake frontend \ + --set "*.cache-from=type=gha,scope=ci-frontend" \ + --set "*.cache-to=type=gha,mode=max,scope=ci-frontend" if [ "${TEST_SUITE}" = "other" ]; then exit 0 @@ -306,7 +315,13 @@ jobs: worker="windowscross" fi export WORKER_TARGET=${worker}/worker - docker buildx bake worker + REPO="${GITHUB_REPOSITORY,,}" + # Workers are cached in ghcr (uncapped): :main-cache is the mode=max + # cache published on merge, :latest the released image. No gha here, so + # the 10GB cap is left to the per-test cache. Cold sources skip silently. + docker buildx bake worker \ + --set "*.cache-from=type=registry,ref=ghcr.io/${REPO}/${WORKER_TARGET}:main-cache" \ + --set "*.cache-from=type=registry,ref=ghcr.io/${REPO}/${WORKER_TARGET}:latest" env: TEST_SUITE: ${{ matrix.suite }} - name: Run integration tests (with coverage tracking) diff --git a/.github/workflows/worker-images.yml b/.github/workflows/worker-images.yml index a7402c528..06093f1bf 100644 --- a/.github/workflows/worker-images.yml +++ b/.github/workflows/worker-images.yml @@ -137,7 +137,13 @@ jobs: name: Setup (merged) run: | set -ex -o pipefail - EXTRA_FLAGS="--set worker.cache-to=type=gha,scope=main.${{matrix.target}},mode=max --set worker.cache-from=type=gha,scope=${MAIN_CACHE_SCOPE}" + # Publish the worker plus a registry cache (mode=max). Registry caches + # live in ghcr (uncapped) so PR CI can cache-from intermediates without + # touching the 10GB gha cap. + ref="ghcr.io/${BASE_REPO}:main" + cache="ghcr.io/${BASE_REPO}:main-cache" + EXTRA_FLAGS="--set worker.tags=${ref} --push" + EXTRA_FLAGS+=" --set worker.cache-to=type=registry,ref=${cache},mode=max --set worker.cache-from=type=registry,ref=${cache}" echo "EXTRA_FLAGS=${EXTRA_FLAGS}" >> $GITHUB_ENV - name: Setup QEMU run: docker run --rm --privileged tonistiigi/binfmt:latest --install all diff --git a/test/testenv/buildkit.go b/test/testenv/buildkit.go index b04ebd674..deafa66ba 100644 --- a/test/testenv/buildkit.go +++ b/test/testenv/buildkit.go @@ -1,8 +1,11 @@ package testenv import ( + "fmt" + "os" "strconv" "strings" + "testing" "github.com/moby/buildkit/client" ) @@ -61,3 +64,57 @@ func supportsFrontendAsInput(info *client.Info) bool { return minor >= minVersion.Minor } + +// withGHCache adds the necessary cache export and import options to the solve request in order to use the GitHub Actions cache. +// It uses the test name as a scope for the cache. Each test will have its own scope. +// This means that caches are not shared between tests, but it also means that tests won't overwrite each other's cache. +// +// Github Actions sets some specific environment variables that we'll look for to even determine if we should configure the cache or not. +// +// This is effectively what `docker build --cache-from=gha,scope=foo --cache-to=gha,scope=foo` would do. +// Export uses the default min mode: only the final layers are pushed. The heavy +// base/worker layers are cached separately via the prebuilt worker images, so +// there's no need to bloat each test scope (and the 10GB cap) with mode=max. +// +// Note: we talk to buildkit via its Go client rather than buildctl/buildx, so +// the daemon does not auto-detect these values from the environment; we must +// pass them as attrs. The token/url env vars are only present in GitHub Actions +// jobs that expose the runtime (see crazy-max/ghaction-github-runtime). We only +// target the v2 cache service; v1 was retired in March 2025. +func withGHCache(t *testing.T, so *client.SolveOpt) { + if os.Getenv("GITHUB_ACTIONS") != "true" { + // This is not running in GitHub Actions, so we don't need to configure the cache. + return + } + + token := os.Getenv("ACTIONS_RUNTIME_TOKEN") + if token == "" { + fmt.Fprintln(os.Stderr, "::warning::ACTIONS_RUNTIME_TOKEN is not set, skipping cache export") + return + } + + url := os.Getenv("ACTIONS_RESULTS_URL") + if url == "" { + fmt.Fprintln(os.Stderr, "::warning::ACTIONS_RESULTS_URL is not set, skipping cache export") + return + } + + scope := "test-integration-" + t.Name() + + so.CacheExports = append(so.CacheExports, client.CacheOptionsEntry{ + Type: "gha", + Attrs: map[string]string{ + "scope": scope, + "token": token, + "url_v2": url, + }, + }) + so.CacheImports = append(so.CacheImports, client.CacheOptionsEntry{ + Type: "gha", + Attrs: map[string]string{ + "scope": scope, + "token": token, + "url_v2": url, + }, + }) +} diff --git a/test/testenv/buildx.go b/test/testenv/buildx.go index 46f5c3d14..415541f2e 100644 --- a/test/testenv/buildx.go +++ b/test/testenv/buildx.go @@ -379,12 +379,13 @@ func WithSocketProxies(proxies ...socketprovider.ProxyConfig) TestRunnerOpt { } } -func setSolveOpts(cfg TestRunnerConfig, so *client.SolveOpt) error { +func setSolveOpts(t *testing.T, cfg TestRunnerConfig, so *client.SolveOpt) error { if err := withProjectRoot(so); err != nil { return err } withResolveLocal(so) + withGHCache(t, so) err := withSocketProxies(cfg.SocketProxies)(so) if err != nil { return err @@ -417,7 +418,7 @@ func (b *BuildxEnv) runTestWithStatus(ctx context.Context, t *testing.T, f TestF assert.NilError(t, err) var so client.SolveOpt - err = setSolveOpts(cfg, &so) + err = setSolveOpts(t, cfg, &so) assert.NilError(t, err) ctx, cancel := context.WithCancel(ctx)