STAC-25429 Warm the Go module cache with a prebuilt, content-addressed CI image - #445
Merged
LouisParkin merged 5 commits intoJul 29, 2026
Merged
Conversation
…e image Speeds up the migrated stackstate-agent GitHub Actions build/test jobs by replacing each job's cold `go clean -modcache` + `inv deps` (go mod download + tidy) reconcile with a warm GOMODCACHE delivered as a prebuilt CI image, keyed by a content hash of the module graph. Mirrors the StackGraph ci-metadata / build-ci-image pattern. - .github/scripts/agent-godeps-cache-metadata.sh: content-addressed image tag from every go.mod/go.sum + go.work + modules.yml plus the Dockerfile/script themselves, so any graph or mechanism change rotates the tag. - .github/docker/godeps-cache/Dockerfile: FROM datadog_build, bakes external modules into GOMODCACHE via a per-module `go mod download` loop. - .github/workflows/godeps-cache.yml: reusable workflow (tag lookup + build only when missing); pushes to quay.io/stackstate (private), pulled by consumers through the registry.tooling quay proxy with the existing REGISTRY_* creds. - build-binaries.yml / lint-and-unit-tests.yml: add the godeps-cache job, repoint the 4 heavy jobs to the cache image, drop the per-job modcache reset + inv deps (keep `go work sync` / `go work vendor` against the warm cache). Zizmor clean (narrow unpinned-images ignore on the content-addressed container refs, which are effectively digest-pinned but computed per run). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LouisParkin
force-pushed
the
STAC-25142-agent-lint-unit
branch
from
July 28, 2026 09:40
c2e9da2 to
c07d0c8
Compare
LouisParkin
force-pushed
the
STAC-25429-godeps-cache-image
branch
from
July 28, 2026 09:40
642d09b to
ae94fa8
Compare
RUN ran under dash so the read -d '' loop never iterated; -mod=mod is rejected in workspace mode.
LouisParkin
marked this pull request as ready for review
July 29, 2026 13:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Part of the GitLab→GitHub agent-CI migration (STAC-25142) speedup work (STAC-25396).
Today each of the 4 heavy jobs (2 binary builds + 2 unit-test suites) starts cold:
This replaces the per-job cold reconcile with a warm
GOMODCACHEdelivered as aprebuilt CI image, keyed by a content hash of the module graph. It is the exact
pattern StackGraph already runs (
.github/workflows/ci.ymlthere:ci-metadata→build-ci-image→ work jobs runcontainer: ci_image).How it works
.github/scripts/agent-godeps-cache-metadata.sh— computes a content-addressedtag
stackstate-agent-godeps-<sha>from everygo.mod/go.sum+go.work+modules.ymlplus the Dockerfile and this script, so any change to the modulegraph or the cache mechanism rotates the tag. Verified it hashes all 198
go.mod/ 191
go.sum(git pathspec*go.modmatches nested modules)..github/docker/godeps-cache/Dockerfile—FROM datadog_build, bakes theworkspace's external modules into
GOMODCACHEvia a per-modulego mod downloadloop.
manifests/preserves repo paths so nested modules and their localreplace ../targets resolve..github/workflows/godeps-cache.yml— reusable workflow: ametadatajobcomputes the tag and
docker manifest inspects it; abuildjob runs only whenthe tag is missing (
if: image_exists != 'true') and pushes to the dedicatedrepo
quay.io/stackstate/stackstate-agent-godeps-cache. Consumers pull itthrough the
registry.toolingquay proxy with the same read-onlyREGISTRY_*creds they already use for
datadog_build. Runs ondocker-public(DinD,STAC-25351).
The 4 heavy jobs now
needs: godeps-cache, setcontainer.imageto the cache image,and drop
go clean -modcache+inv deps(keepinggo work sync/go work vendor,now offline against the warm cache).
filename-lintingis unchanged (needs no deps).The unit-test jobs also had a redundant double
inv deps(the second, commented"re-run deps to get tool binaries", never installed tools —
invoke install-toolsdoes); both are removed.
Registry choice and visibility
The cache image gets its own Quay repository rather than the shared
sts-ci-images.sts-ci-imagesalso holds the ARC runner images, andstackstate-agentis public, so any org member's same-repo branch can usewhatever push credential this workflow carries — a dedicated repo keeps that reach
down to a rebuildable cache and away from the images the CI fleet boots from.
The repository is public, deliberately:
committed
go.modfiles — nothing private is baked in.quay.io/stackstate/datadog_build_linux_x64is itself public(verified live via the Quay API and in
terraform-infraquay/locals.tf), sothere are no private base layers to leak. Earlier revisions of this branch claimed
the base was private and that the cache "must stay private" — that was wrong, and
the comments have been corrected.
registry.toolingproxy robot'sprivate-repo grants, which it does not hold (
harbor_proxyis a member of no teamin the Quay config).
Push credentials reuse the existing agent-domain robot
stackstate+agent, alreadyused by
datadog-agent-buildimagesandkubernetes-rbac-agent— no new robot, nonew secret. The rejected alternative was pointing this repo at
stackstate+stackstate_ci_images, which would put write credentials for the sharedCI image repository into a public repo.
Security / conventions
--collect=workflows,actions,dependabot):No findings to report (4 ignored, 6 suppressed). The 4 ignores are a narrowunpinned-imagesignore onthe
container.image: ${{ needs.godeps-cache.outputs.ci_image }}refs — the tag isa sha256 content hash (effectively a digest pin) but can't be a static digest since
it's computed per run. Same pattern StackGraph uses unignored.
permissions: contents: readeverywhere; secrets passed explicitly to thereusable workflow (no
secrets: inherit);persist-credentials: false; externalactions SHA-pinned; no
pull_request_target; fork PRs skip these jobs (secrets notexposed). actionlint: no schema errors (only the pre-existing
docker-public/xlarge-publicself-hosted-label notes). shellcheck clean.Dependencies and assumptions — resolved
Quay repository + credentials — two infra PRs, both merged 2026-07-28:
declares the public
stackstate-agent-godeps-cacherepository and grants theagentteam write. Grants there are enumerated per repository (no prefix globs),so it had to be applied by Atlantis first, or the push 403s. Merged
2026-07-28 12:34Z.
wires
QUAY_USER(variable) /QUAY_PASSWORD(secret) into this repo. Withoutit the
buildjob failed atdocker login quay.iowithusername is empty.Merged 2026-07-28 12:36Z.
Note this is not an org-secret visibility problem (an earlier revision said it
was, by analogy with STAC-25350's
REGISTRY_*fix).QUAY_*are per-repovariables/secrets, where
visibilitydoes not apply —stackstate-agentsimplyhad no entry.
The
registry.toolingquay proxy must servequay/stackstate/stackstate-agent-godeps-cache(it already servesquay/stackstate/datadog_build_linux_x64). Public visibility means no proxy-robotgrant is required.
GOPROXY reachability from inside the image build on the
docker-publicDinDsidecar — resolved. The
go mod downloadloop reaches the module proxy; thebuildjob published successfully (552s) on the run below.Cross-workflow race:
build-binariesandlint-and-unit-testsboth call thereusable workflow, so two concurrent runs could both build the same tag —
idempotent (content-addressed, immutable), acceptable.
Follow-up — module tidiness. An earlier revision of this description claimed
this PR stopped verifying
go.modtidiness per job. That was wrong:inv depsisdownload_go_dependenciesonly and never tidied, and thego mod tidyin the oldGitLab
deps_debreconcile mutated the tree rather than failing on a diff. Therepo has never had a tidiness gate. Adding one is
#447 (STAC-25459), which
wires the long-existing but never-referenced
inv check-mod-tidyand gates it onthis PR's content-addressed cache tag — a cache hit means the manifests were already
verified, so it costs nothing on PRs that don't move the module graph. Its first run
found a genuine pre-existing defect (STAC-25466). Neither blocks this PR.
Validation
Both workflows green on the head commit
4e986befe4(lint,
binaries):
The cache-hit path is exercised too: on the binaries run the
build and publishjobwas correctly
skippedagainst an already-published tag.What is not measured yet. There is no clean before/after against the old per-job
go clean -modcache+inv depsreconcile. A single cold-vs-warm pair on this branch(cold: agent 12m17s, cluster-agent 8m20s; warm: 8m35s / 8m31s) is n=1 and
noise-dominated — the cluster-agent was marginally slower warm — so it is not
offered as evidence of the speedup. The design rationale stands on removing ~277s of
CPU per job, but if reviewers want a number before merging, say so and I will run
3–4 repetitions per variant against the base branch.
Base branch
Deliberately targets
STAC-25142-agent-lint-unit(PR #444, the migration integrationbranch), not
stackstate-7.78.2. This is layered on the migration work and shouldland with it, not ahead of it.