From 6f9fd6f4e5091eeddc94f6386e2012073a461550 Mon Sep 17 00:00:00 2001 From: Paolo Dettori Date: Wed, 2 Sep 2026 14:53:47 -0400 Subject: [PATCH] build: decouple the Go modules with a workspace so Dependabot PRs stay green `remote-worker` consumes `gen/go` through a local `replace`, and the `proto` job builds both modules. Dependabot opens one PR per `go.mod`, so any bump to a `gen/go` dependency left `remote-worker/go.mod` pinning stale indirect versions and CI failed with `go: updates to go.mod needed`. Dependabot cannot fix that itself -- the tidy has to land in the same commit as the bump, and its branches are not maintainer-writable -- so #210 had to be bundled by hand into #211. Add a root `go.work` covering both modules. In workspace mode the go command resolves one MVS across them, so a stale consumer `go.mod` no longer breaks the build and the two modules can be bumped independently. Verified against the tree that failed in #210 (`gen/go` bumped, `remote-worker` left at the pre-#211 state): `go vet` and `go test -race` went from `updates to go.mod needed` to passing, with `gen/go` build and tests unaffected. `directories:` (plural) is a shorthand here, not the fix: Dependabot still opens one PR per directory. Bundling would need `group-by: dependency-name`, which is version-updates-only and splits one PR per dependency, so it would leave the security bumps this entry exists for unbundled. It collapses a duplicated `grpc-protobuf` group block, nothing more. `go mod tidy -diff` still reports drift in workspace mode, so it runs as an advisory step. Failing on it would put the coupled bumps back in the red, and neither module is consumed externally -- `gen/go` is reached only through the local `replace` -- so per-module tidiness is hygiene, not correctness. A module left out of the workspace would silently reintroduce the failure, so a blocking step checks every tracked `go.mod` is listed in `go.work`. `remote-worker/Dockerfile` builds from the repo root and now copies `go.work`; without it that build resolves each module alone and would still hit a stale `go.mod`. Closes #212 Assisted-By: Claude (Anthropic AI) Signed-off-by: Paolo Dettori --- .github/dependabot.yml | 22 +++++++++------------- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ .gitignore | 4 ++++ go.work | 15 +++++++++++++++ remote-worker/Dockerfile | 3 +++ 5 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 go.work diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c1f974..f70ced3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,9 +26,16 @@ updates: # Go modules were not covered above, so Go advisories reached us only as a red # trivy-scan gate and had to be bumped by hand -- CVE-2026-84304 (grpc) in #203. - # Dependabot needs one entry per go.mod; there is no recursive directory match. + # + # `directories` is a shorthand for the two go.mod files, NOT a way to bundle them: + # Dependabot still opens one PR per directory. Bundling would need + # `group-by: dependency-name`, which is version-updates-only and splits one PR per + # dependency -- so it would leave security bumps, the reason this entry exists, + # unbundled. The root go.work is what keeps those per-directory PRs green; see #212. - package-ecosystem: gomod - directory: /gen/go + directories: + - /gen/go + - /remote-worker schedule: interval: weekly groups: @@ -39,14 +46,3 @@ updates: - 'google.golang.org/grpc*' - 'google.golang.org/protobuf*' - 'google.golang.org/genproto*' - - - package-ecosystem: gomod - directory: /remote-worker - schedule: - interval: weekly - groups: - grpc-protobuf: - patterns: - - 'google.golang.org/grpc*' - - 'google.golang.org/protobuf*' - - 'google.golang.org/genproto*' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77a7736..e0c0161 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,19 @@ jobs: git diff --cached --exit-code -- packages/k8s-sandbox/src/gen gen \ || { echo "::error::Generated stubs are out of date or untracked. Run 'buf generate' and commit."; exit 1; } + # A module left out of go.work falls back to standalone resolution and brings + # back the stale-go.mod failure the workspace exists to prevent (#212). + - name: Verify go.work covers every module + run: | + covered="$(go work edit -json | jq -r '.Use[].DiskPath')" + rc=0 + while read -r mod; do + dir="./$(dirname "$mod")" + grep -qxF "$dir" <<<"$covered" \ + || { echo "::error file=$mod::$mod is not listed in go.work -- add \"$dir\" to the use block."; rc=1; } + done < <(git ls-files | grep -E '(^|/)go\.mod$') + exit $rc + - name: Build and test Go stubs run: cd gen/go && go build ./... && go test ./... @@ -154,3 +167,15 @@ jobs: test -z "$(gofmt -l .)" || { echo "::error::gofmt needs running:"; gofmt -l .; exit 1; } go vet ./... go test -race ./... + + # Advisory only. In workspace mode a stale go.mod no longer breaks the build, so + # this is hygiene rather than correctness -- and failing here would put Dependabot's + # coupled bumps back in the red, which is the whole point of #212. Neither module is + # consumed externally (gen/go is reached only through the local replace), so drift + # can be tidied whenever convenient. + - name: Check go.mod tidiness (advisory) + run: | + for mod in gen/go remote-worker; do + (cd "$mod" && go mod tidy -diff) \ + || echo "::warning file=$mod/go.mod::$mod/go.mod is not tidy -- run 'cd $mod && go mod tidy'" + done diff --git a/.gitignore b/.gitignore index fbfa053..114c5b8 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ docs/plans/* # E2 writes each run's measured table here; experiments/RESULTS.md is the committed # baseline and is no longer overwritten by a test run (see experiments/README.md). experiments/.results/ + +# Workspace-only checksums. Redundant with each module go.sum, and the checksum +# database still verifies downloads without it -- one less sum file to keep in sync. +go.work.sum diff --git a/go.work b/go.work new file mode 100644 index 0000000..b662f44 --- /dev/null +++ b/go.work @@ -0,0 +1,15 @@ +// Both Go modules are coupled: remote-worker consumes gen/go through a local +// `replace`, and the `proto` CI job builds both. Without a workspace, any bump to a +// gen/go dependency leaves remote-worker/go.mod stale and the build fails with +// "updates to go.mod needed" -- which Dependabot cannot fix, because the tidy has to +// land in the same commit as the bump and its branches are not maintainer-writable. +// In workspace mode the go command resolves one MVS across both modules, so the +// modules can be bumped independently. See issue #212. +// +// Keep this `go` line in sync with both go.mod files (and CI's go-version-file). +go 1.25.0 + +use ( + ./gen/go + ./remote-worker +) diff --git a/remote-worker/Dockerfile b/remote-worker/Dockerfile index b2ff5f0..5d822a5 100644 --- a/remote-worker/Dockerfile +++ b/remote-worker/Dockerfile @@ -8,6 +8,9 @@ # RELAY_ADDR=sandbox-relay.default.svc:8443 (no port-forward needed in-cluster). FROM golang:1.25-alpine AS build WORKDIR /src +# go.work first: it is what lets the two modules be bumped independently (see #212). +# Without it the build here resolves each go.mod on its own and fails on a stale one. +COPY go.work ./ COPY gen/go/ ./gen/go/ COPY remote-worker/ ./remote-worker/ WORKDIR /src/remote-worker