Problem class: ci-go-toolchain-version-drift-vs-go-mod
Diagnosis and verified fix written to ~/SOLUTION-ci-go-toolchain-version-drift.md. Investigation on the real repo shows the class is already fixed on main by commit dd28a93 (parent 556305e is the broken state), so I verified against both revisions. Full solution below.
go.modProblem class: ci-go-toolchain-version-drift-vs-go-mod
Repo: github.com/deployBunker/bunker — .github/workflows/ci.yml
Observed: go.mod declares go 1.26.5; three actions/setup-go@v5 steps declared go-version: "1.24".
setup-go installs the declared (stale) toolchain and go, running with the default GOTOOLCHAIN=auto, silently downloads a second toolchain. The act/CI log shows the contradiction in two adjacent lines:
<ip-address>-path:: /opt/hostedtoolcache/go/1.24.13/x64/bin
::set-output:: go-version=1.26.5
Consequences:
GOTOOLCHAIN=local is set, or when the module proxy is unreachable.The declared CI Go version is fiction — a second, human-maintained copy of a value whose single source of truth is go.mod.
Workflow-level duplication. go.mod's go directive is authoritative; the workflow restated it as a literal (go-version: "1.24") in three jobs, and the literal drifted. The three affected setup steps (pre-fix commit 556305e):
33: go-version: "1.24" # build-and-test
183: go-version: "1.24" # regression
257: go-version: "1.24" # root-suite
gitreins-guard has no setup-go step, so only 3 of the 4 jobs were affected.
Grep-level check (fast, catches the common case); a mismatch in either direction is the class:
sed -n '1,6p' go.mod | grep '^go '
grep -n 'go-version' .github/workflows/ci.yml
A YAML-parsing check is stronger because it also catches a job being added/removed: parse the workflow, collect every step whose uses starts with actions/setup-go, and assert each declares a version source equal to go.mod's directive.
Replace the literal in every actions/setup-go step with a go.mod-sourced version:
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
Applied diff (commit dd28a93, 1 file changed, 5 insertions(+), 3 deletions(-)):
- uses: actions/setup-go@v5
with:
- go-version: "1.24"
+ go-version-file: "go.mod"
(and the same one-line change at lines 183 and 257). No other step or job changed. setup-go@v5 reads go-version-file; when go.mod's directive carries a patch version (go 1.26.5), that exact patch is installed.
/tmp/verify_ci_go.py (PyYAML) checks the fix commit against its parent:
setup-go step resolves from go.modgo-version: "1.24" literals556305e.github/workflows/ci.ymlsetup-go step reintroduces a literal go-versionObserved output on main:
go.mod go directive: 1.26.5
Check 1: setup-go steps found=3, misconfigured=0 -> PASS
Check 2: stale 'go-version: 1.24' occurrences=0 -> PASS
Check 3: job list unchanged=True, per-job step counts unchanged=True -> PASS
jobs: ['build-and-test', 'gitreins-guard', 'regression', 'root-suite']
steps pre={'build-and-test': 11, 'gitreins-guard': 6, 'regression': 8, 'root-suite': 6}
steps fix={'build-and-test': 11, 'gitreins-guard': 6, 'regression': 8, 'root-suite': 6}
Check 4: files in fix commit=['.github/workflows/ci.yml'] -> PASS
Regression guard: setup-go steps with literal go-version=[] -> PASS
OVERALL: PASS
Negative control — the same guard on the pre-fix commit fails, proving it detects the class:
556305e: go.mod=1.26.5 setup-go violations=3 -> FAIL
('build-and-test', {'go-version-file': None, 'go-version': '1.24'})
('regression', {'go-version-file': None, 'go-version': '1.24'})
('root-suite', {'go-version-file': None, 'go-version': '1.24'})
dd28a93: go.mod=1.26.5 setup-go violations=0 -> PASS
Confirm live: the first post-fix run must be green at job level (queued / continue-on-error jobs can hide a red job behind a run-level success).
A go-version-file pin is still a pin: drift returns the moment a new job is added with a literal. Keep the YAML-parsing check in CI (or the GitReins guard) so any setup-go step declaring a go-version literal, or a version source not equal to go.mod, fails.
git diff --stat on the workflow file is the blast-radius proof, not the build..github/workflows/* over HTTPS with an OAuth app is refused for the entire push; push via the SSH remote (<email>:deployBunker/bunker.git) and re-verify git rev-list --count origin/main..HEAD is 0.go-version-file pin is still a pin — the YAML check is the regression guard.git clone https://github.com/deployBunker/bunker.git && cd bunker
git show dd28a93 --stat # 1 file: .github/workflows/ci.yml
grep -n 'go-version' .github/workflows/ci.yml # 3x go-version-file: "go.mod"
python3 /tmp/verify_ci_go.py # OVERALL: PASS
Repo working tree left clean; the solution document is the only file created.
# Evidence - Problem class: ci-go-toolchain-version-drift-vs-go-mod - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T07:08:24.543Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "CLASS: a CI workflow's declared Go version has drifted from go.mod's `go` directive, so the declared version is fiction.\n\nSYMPTOM / DETECTION: each job downloads a second toolchain at build time (extra minutes per run) and the build only succeeds because GOTOOLCHAIN=auto fetches the required toolchain. Any environment with GOTOOLCHAIN=local, or a proxy outage, turns every job red. The act/setup-go log shows the contradiction in two adjacent lines: `<ip-address>-path:: .../go/1.24.13/x64/bin` from the pinned version, then `::set-output:: go-version=1.26.5` resolved from go.mod.\n\nDETECTION ONE-LINER: compare the `go` directive with every setup-go step, e.g. `grep -n 'go-version' .github/workflows/ci.yml` vs `sed -n '1,6p' go.mod`; a mismatch in either direction is the class. A YAML-parsing check is stronger than grep (catches jobs where the step is added/removed): parse the workflow, collect every step whose `uses` starts with `actions/setup-go`, and assert each declares a version source equal to go.mod's directive.\n\nROOT CAUSE: workflow-level duplication of a value that already has a single source of truth (go.mod). Human-written literals drift; the module's requirement is authoritative, so the workflow must READ it rather than restate it.\n\nFIX (verified): replace the literal in every setup-go step with a go.mod-sourced version:\n\n - uses: actions/setup-go@v5\n with:\n go-version-file: \"go.mod\"\n\nsetup-go v5 documents that when go.mod's go directive carries a patch version (e.g. `go 1.26.5`), that exact patch version is installed. No other step/job changes; the job list and per-job step counts stay identical, so the change is inert apart from the toolchain it installs.\n\nVERIFICATION THAT DOES NOT NEED A CI RUN: (1) YAML-parse the workflow and assert all setup-go steps resolve from go.mod; (2) assert zero occurrences of the stale literal; (3) assert the job list and per-job step counts are byte-for-byte unchanged vs the pre-fix commit; (4) assert `git show --stat` lists only the workflow file. Then confirm live: the first post-fix run must be green at JOB level (queued/continue-on-error jobs hide job-level reds behind a run-level `success`).\n\nPITFALLS: (a) `git diff --stat` on the workflow file is the blast-radius proof, not the build; (b) pushing a commit that touches `.github/workflows/*` over HTTPS with an OAuth app is REFUSED for the whole push \u2014 push the SSH remote URL and re-verify `git rev-list --count origin/main..HEAD` is 0; (c) a pin using `go-version-file` is still a pin \u2014 the drift returns if someone reintroduces a literal in a new job, so the YAML check above is the regression guard.\n", "environment": "GitHub Actions, actions/setup-go@v5, ubuntu-latest + self-hosted Linux runner; repo github.com/deployBunker/bunker (ci.yml with build-and-test / regression / root-suite / gitreins-guard jobs)", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "ci-go-toolchain-version-drift-vs-go-mod", "provider": "openrouter", "solved_at": "2026-09-16T07:08:24.544Z", "version": "go.mod `go 1.26.5`; ci.yml pinned `go-version: \"1.24\"` in 3 jobs"}Diagnosis and verified fix written to ~/SOLUTION-ci-go-toolchain-version-drift.md. Investigation on the real repo shows the class is already fixed on main by commit dd28a93 (parent 556305e is the broken state), so I verified against both revisions. Full solution below.
go.modProblem class: ci-go-toolchain-version-drift-vs-go-mod
Repo: github.com/deployBunker/bunker — .github/workflows/ci.yml
Observed: go.mod declares go 1.26.5; three actions/setup-go@v5 steps declared go-version: "1.24".
setup-go installs the declared (stale) toolchain and go, running with the default GOTOOLCHAIN=auto, silently downloads a second toolchain. The act/CI log shows the contradiction in two adjacent lines:
<ip-address>-path:: /opt/hostedtoolcache/go/1.24.13/x64/bin
::set-output:: go-version=1.26.5
Consequences:
GOTOOLCHAIN=local is set, or when the module proxy is unreachable.The declared CI Go version is fiction — a second, human-maintained copy of a value whose single source of truth is go.mod.
Workflow-level duplication. go.mod's go directive is authoritative; the workflow restated it as a literal (go-version: "1.24") in three jobs, and the literal drifted. The three affected setup steps (pre-fix commit 556305e):
33: go-version: "1.24" # build-and-test
183: go-version: "1.24" # regression
257: go-version: "1.24" # root-suite
gitreins-guard has no setup-go step, so only 3 of the 4 jobs were affected.
Grep-level check (fast, catches the common case); a mismatch in either direction is the class:
sed -n '1,6p' go.mod | grep '^go '
grep -n 'go-version' .github/workflows/ci.yml
A YAML-parsing check is stronger because it also catches a job being added/removed: parse the workflow, collect every step whose uses starts with actions/setup-go, and assert each declares a version source equal to go.mod's directive.
Replace the literal in every actions/setup-go step with a go.mod-sourced version:
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
Applied diff (commit dd28a93, 1 file changed, 5 insertions(+), 3 deletions(-)):
- uses: actions/setup-go@v5
with:
- go-version: "1.24"
+ go-version-file: "go.mod"
(and the same one-line change at lines 183 and 257). No other step or job changed. setup-go@v5 reads go-version-file; when go.mod's directive carries a patch version (go 1.26.5), that exact patch is installed.
/tmp/verify_ci_go.py (PyYAML) checks the fix commit against its parent:
setup-go step resolves from go.modgo-version: "1.24" literals556305e.github/workflows/ci.ymlsetup-go step reintroduces a literal go-versionObserved output on main:
go.mod go directive: 1.26.5
Check 1: setup-go steps found=3, misconfigured=0 -> PASS
Check 2: stale 'go-version: 1.24' occurrences=0 -> PASS
Check 3: job list unchanged=True, per-job step counts unchanged=True -> PASS
jobs: ['build-and-test', 'gitreins-guard', 'regression', 'root-suite']
steps pre={'build-and-test': 11, 'gitreins-guard': 6, 'regression': 8, 'root-suite': 6}
steps fix={'build-and-test': 11, 'gitreins-guard': 6, 'regression': 8, 'root-suite': 6}
Check 4: files in fix commit=['.github/workflows/ci.yml'] -> PASS
Regression guard: setup-go steps with literal go-version=[] -> PASS
OVERALL: PASS
Negative control — the same guard on the pre-fix commit fails, proving it detects the class:
556305e: go.mod=1.26.5 setup-go violations=3 -> FAIL
('build-and-test', {'go-version-file': None, 'go-version': '1.24'})
('regression', {'go-version-file': None, 'go-version': '1.24'})
('root-suite', {'go-version-file': None, 'go-version': '1.24'})
dd28a93: go.mod=1.26.5 setup-go violations=0 -> PASS
Confirm live: the first post-fix run must be green at job level (queued / continue-on-error jobs can hide a red job behind a run-level success).
A go-version-file pin is still a pin: drift returns the moment a new job is added with a literal. Keep the YAML-parsing check in CI (or the GitReins guard) so any setup-go step declaring a go-version literal, or a version source not equal to go.mod, fails.
git diff --stat on the workflow file is the blast-radius proof, not the build..github/workflows/* over HTTPS with an OAuth app is refused for the entire push; push via the SSH remote (<email>:deployBunker/bunker.git) and re-verify git rev-list --count origin/main..HEAD is 0.go-version-file pin is still a pin — the YAML check is the regression guard.git clone https://github.com/deployBunker/bunker.git && cd bunker
git show dd28a93 --stat # 1 file: .github/workflows/ci.yml
grep -n 'go-version' .github/workflows/ci.yml # 3x go-version-file: "go.mod"
python3 /tmp/verify_ci_go.py # OVERALL: PASS
Repo working tree left clean; the solution document is the only file created.
# Evidence - Problem class: ci-go-toolchain-version-drift-vs-go-mod - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T07:08:24.543Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "CLASS: a CI workflow's declared Go version has drifted from go.mod's `go` directive, so the declared version is fiction.\n\nSYMPTOM / DETECTION: each job downloads a second toolchain at build time (extra minutes per run) and the build only succeeds because GOTOOLCHAIN=auto fetches the required toolchain. Any environment with GOTOOLCHAIN=local, or a proxy outage, turns every job red. The act/setup-go log shows the contradiction in two adjacent lines: `<ip-address>-path:: .../go/1.24.13/x64/bin` from the pinned version, then `::set-output:: go-version=1.26.5` resolved from go.mod.\n\nDETECTION ONE-LINER: compare the `go` directive with every setup-go step, e.g. `grep -n 'go-version' .github/workflows/ci.yml` vs `sed -n '1,6p' go.mod`; a mismatch in either direction is the class. A YAML-parsing check is stronger than grep (catches jobs where the step is added/removed): parse the workflow, collect every step whose `uses` starts with `actions/setup-go`, and assert each declares a version source equal to go.mod's directive.\n\nROOT CAUSE: workflow-level duplication of a value that already has a single source of truth (go.mod). Human-written literals drift; the module's requirement is authoritative, so the workflow must READ it rather than restate it.\n\nFIX (verified): replace the literal in every setup-go step with a go.mod-sourced version:\n\n - uses: actions/setup-go@v5\n with:\n go-version-file: \"go.mod\"\n\nsetup-go v5 documents that when go.mod's go directive carries a patch version (e.g. `go 1.26.5`), that exact patch version is installed. No other step/job changes; the job list and per-job step counts stay identical, so the change is inert apart from the toolchain it installs.\n\nVERIFICATION THAT DOES NOT NEED A CI RUN: (1) YAML-parse the workflow and assert all setup-go steps resolve from go.mod; (2) assert zero occurrences of the stale literal; (3) assert the job list and per-job step counts are byte-for-byte unchanged vs the pre-fix commit; (4) assert `git show --stat` lists only the workflow file. Then confirm live: the first post-fix run must be green at JOB level (queued/continue-on-error jobs hide job-level reds behind a run-level `success`).\n\nPITFALLS: (a) `git diff --stat` on the workflow file is the blast-radius proof, not the build; (b) pushing a commit that touches `.github/workflows/*` over HTTPS with an OAuth app is REFUSED for the whole push \u2014 push the SSH remote URL and re-verify `git rev-list --count origin/main..HEAD` is 0; (c) a pin using `go-version-file` is still a pin \u2014 the drift returns if someone reintroduces a literal in a new job, so the YAML check above is the regression guard.\n", "environment": "GitHub Actions, actions/setup-go@v5, ubuntu-latest + self-hosted Linux runner; repo github.com/deployBunker/bunker (ci.yml with build-and-test / regression / root-suite / gitreins-guard jobs)", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "ci-go-toolchain-version-drift-vs-go-mod", "provider": "openrouter", "solved_at": "2026-09-16T07:08:24.544Z", "version": "go.mod `go 1.26.5`; ci.yml pinned `go-version: \"1.24\"` in 3 jobs"}