From 930f6b7ec68874a323904fece8b640aa5eb3b2ef Mon Sep 17 00:00:00 2001 From: Blair Hamilton Date: Sat, 15 Aug 2026 08:35:58 -0400 Subject: [PATCH] feat(setup-go): key Go caches per calling job, not per go.sum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/setup-go's built-in cache keys once per go.sum for the whole repo: the first job to finish saves the key, and every sibling job restores artifacts compiled with the wrong flags, recompiles from scratch, and never saves its own ('Cache hit occurred on the primary key …, not saving cache'). In k5s the Test job spent ~204 s of its 242 s recompiling the dependency graph with -race -coverprofile on EVERY run, against ~30 s of actual test time — permanently, because the shared key already held the build job's plain artifacts. Replace the built-in cache with an actions/cache step over ~/go/pkg/mod + ~/.cache/go-build keyed on setup-go-job----, with restore-keys falling back to the same scope on a dep bump and then to any scope (a cold job still inherits a sibling's module cache). New cache-name input overrides the scope; 'false' disables. Propagates to every @main caller on merge — the per-job keys are new, so each repo's first run per job recompiles once and is warm after. --- README.md | 5 +++- actions/setup-go/action.yml | 50 ++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2ec05a9..a6e9997 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Why `.github` and not a dedicated `github-actions` repo: `.github` is *the* GitH | `setup-python-uv` | Install uv + a pinned Python version + (default-on) `uv sync`. | | `setup-node-pnpm` | corepack + setup-node@v4 with pnpm cache + (default-on) `pnpm install --frozen-lockfile`. Accepts a `pnpm-filter` input for workspace filtering. | | `setup-dotnet` | setup-dotnet@v5 with NuGet cache keyed on `**/*.csproj` + (default-off) `dotnet tool restore`. | -| `setup-go` | setup-go@v6 reading version from `go.mod`. Optional `private-modules: true` mints a short-lived read-only `pinpredict-argocd` App token and configures git + `GOPRIVATE` so `go`/`golangci-lint`/`goreleaser` fetch a private pinpredict module (e.g. `github.com/pinpredict/ppkit`) without vendoring — the non-Docker analogue of `docker-release.yml`'s `private-modules` secret. Default false. | +| `setup-go` | setup-go@v6 reading version from `go.mod`, with the Go module + build caches keyed **per calling job** (`github.job`) instead of setup-go's built-in single-per-go.sum key — under the built-in scheme the first job to save wins the key and e.g. a `go test -race -cover…` job recompiles the whole dependency graph on every run (~3.5 min in k5s vs ~30 s of tests) because its race-instrumented artifacts are never saved. `cache-name` overrides the scope (fixed name to share between jobs; `"false"` disables). Optional `private-modules: true` mints a short-lived read-only `pinpredict-argocd` App token and configures git + `GOPRIVATE` so `go`/`golangci-lint`/`goreleaser` fetch a private pinpredict module (e.g. `github.com/pinpredict/ppkit`) without vendoring — the non-Docker analogue of `docker-release.yml`'s `private-modules` secret. Default false. | #### Language setup composites — usage @@ -58,6 +58,9 @@ Why `.github` and not a dedicated `github-actions` repo: `.github` is *the* GitH - uses: pinpredict/.github/actions/setup-go@main with: go-version-file: "go.mod" # optional; default "go.mod" + # cache-name: "shared" # optional; default "" = per-job cache scope + # (github.job). Fixed name shares a cache across + # jobs that compile identically; "false" disables. # Go, fetching a private pinpredict module without vendoring (e.g. k4a → ppkit) - uses: pinpredict/.github/actions/setup-go@main diff --git a/actions/setup-go/action.yml b/actions/setup-go/action.yml index 1d67c2a..d6191bc 100644 --- a/actions/setup-go/action.yml +++ b/actions/setup-go/action.yml @@ -4,10 +4,20 @@ description: | recipe for pinpredict Go repos (service-template and anything templated from it). - actions/setup-go@v6 caches the Go SDK + module cache by default; warm - runs skip both downloads. Reading the version from go.mod means the - caller doesn't pin Go in two places — bumping `go x.yy.z` in go.mod - drives CI. + Caches the Go module + build caches per CALLING JOB (the cache key + includes `github.job`) instead of using setup-go@v6's built-in cache. + The built-in cache keys once per go.sum for the whole repo, so the + first job to save wins the key and every sibling job restores + artifacts compiled with the wrong flags, recompiles from scratch, and + never saves its own ("Cache hit occurred on the primary key …, not + saving cache"). Concretely: a `go test -race -coverprofile` job kept + restoring the build job's plain artifacts and recompiled the entire + dependency graph on every run (~3.5 min in k5s against ~30 s of actual + test time). Per-job keys keep each flag set (plain build, race+cover + test, goreleaser) warm independently. `cache-name` overrides the + scope for jobs that should share; `cache-name: "false"` disables + caching. Reading the version from go.mod means the caller doesn't pin + Go in two places — bumping `go x.yy.z` in go.mod drives CI. Optional `private-modules: true` mints a short-lived, read-only pinpredict-argocd App token and configures git + GOPRIVATE so `go`, @@ -21,6 +31,15 @@ inputs: description: "Path to go.mod (or any file with a `go` directive)." required: false default: "go.mod" + cache-name: + description: >- + Scope name for the Go module + build cache. Empty (the default) + scopes the cache to the calling job's id (`github.job`), so each + job's differently-flagged compile artifacts stay warm + independently. Set a fixed name to share one cache between jobs + that compile identically, or "false" to skip caching entirely. + required: false + default: "" private-modules: description: >- When true, mint a read-only pinpredict-argocd App token from @@ -51,6 +70,29 @@ runs: uses: actions/setup-go@v6 with: go-version-file: ${{ inputs.go-version-file }} + # Built-in caching stays off: it keys ONE cache per go.sum for every + # job in the repo, so only the first job to save ever populates it and + # differently-flagged sibling jobs (e.g. `go test -race -cover…`) + # recompile the world on every run. The per-job cache below replaces it. + cache: false + + # Module + build caches, scoped per calling job (or per `cache-name`). + # restore-keys fall back first to the same scope with an older go.sum + # (near-warm after a dep bump), then to any scope (a cold test job still + # inherits the build job's module download cache). Paths are the Linux + # defaults — every pinpredict runner (GitHub-hosted and the EKS/ARC scale + # sets) is Linux; a macOS caller would just miss the cache, not break. + - name: Restore Go module + build caches + if: ${{ inputs.cache-name != 'false' }} + uses: actions/cache@v6 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: setup-go-job-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name || github.job }}-${{ hashFiles(inputs.go-version-file, '**/go.sum') }} + restore-keys: | + setup-go-job-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name || github.job }}- + setup-go-job-${{ runner.os }}-${{ runner.arch }}- # Opt-in (private-modules: true): mint a short-lived, read-only token for # the org-wide pinpredict-argocd App so `go mod download` / golangci /