Skip to content

fix(coverage-floor): install go.mod's toolchain release, not its go-line floor - #308

Merged
topcoder1 merged 4 commits into
mainfrom
fix/coverage-floor-go-toolchain
Oct 26, 2026
Merged

topcoder1 merged 4 commits into
mainfrom
fix/coverage-floor-go-toolchain

Conversation

@topcoder1

Copy link
Copy Markdown
Owner

What was wrong

coverage-floor.yml's detect step read only go.mod's go directive and gave it to actions/setup-go as an exact version. setup-go then exports GOTOOLCHAIN=local (its setGoToolchain()), so go runs whatever was installed and skips go.mod's toolchain line. A repo that pins a patched toolchain had its required coverage lane measured on the unpatched floor, green and with no warning.

Seen on whois-api-llc/techrecon, which pins toolchain go1.26.10 for GO-2026-6809 (crypto/x509 Certificate.Verify) over go 1.26.1:

  • whois-api-llc/techrecon#1213, job 120625436528: Setup go version spec 1.26.1, go version go1.26.1, GOTOOLCHAIN='local'.
  • techrecon main today, same commit b853a3a44: ci.yml's test job (it uses go-version-file: go.mod) runs go1.26.10. Coverage-floor run 40261908797 runs go1.26.1.

The fix

When the caller passes no go_version and the run measures coverage itself, the detect step resolves the Go release this way:

  1. A caller's go_version input wins, used as given. Unchanged.
  2. Both directives are read the way Go reads them, without a trailing CR or a // comment. Go ends a token at // even with no space before it (toolchain go1.26.10//pin).
  3. A toolchain goV pin, or goV-suffix for a custom build of V, installs the newer of V and the go line, compared in Go's own order. A pin below the go line is stale: Go ignores it, and under GOTOOLCHAIN=local the older release cannot build the module (go: go.mod requires go >= 1.26.2 (running go 1.26.1; GOTOOLCHAIN=local)).
  4. toolchain default, or no toolchain line, installs the go line. Unchanged.
  5. A release candidate, on either line, goes to setup-go as SemVer's 1.27.0-rc.2. setup-go's findMatch normalizes the candidate versions, not the requested one, so Go's 1.27rc2 matches nothing; that was already true for an rc go line. The comparison happens before this mapping, in Go's spelling. sort -V orders that spelling as Go does (1.27 < 1.27rc1 < 1.27rc2 < 1.27.0 < 1.27.1, checked on GNU coreutils 9.11 and macOS sort), so a stale rc pin still loses to its release.
  6. A toolchain line that names no Go release (toolchain 1.26.10, toolchain go1.26.10x) fails the job with an ::error:: that quotes the line, instead of measuring the floor again unseen.
  7. No go.mod installs 1.23. Unchanged.

A pre-measured run (pre_measured_coverage_artifact) installs no Go, so it never reads go.mod, and no toolchain line can fail it. The go_version input gets a description that states the default.

Why not go-version-file: go.mod: setup-go v7's own version-file mode has these same gaps. It reads toolchain only when GOTOOLCHAIN isn't local at parse time. It falls back to the go line silently on a toolchain line its regex can't match. It takes a pin below the go line as given. And it returns rc toolchains as 1.27rc2, which its own matcher then can't find. Resolving in the detect step also keeps the chosen version visible as the step's output, and lets the selftest run the shipped step with literal expectations.

Fleet repos whose behavior changes

I enumerated all 145 non-archived repos in topcoder1 and whois-api-llc through the tree API (no truncated trees), not code search alone. 22 have a go.mod, and 2 have a toolchain line. Code search across all owners found no callers anywhere else; its positive controls (techrecon, wxa-graph) appeared.

Repo Caller go.mod Installs before → after
whois-api-llc/techrecon language: go go 1.26.1 + toolchain go1.26.10 1.26.1 → 1.26.10
whois-api-llc/wxa-graph language: go go 1.26.10 1.26.10 (unchanged)
whois-api-llc/wxa-mcp-server language: go go 1.26.10 1.26.10 (unchanged)
topcoder1/ipgeo_core language: python go 1.26.1 + toolchain go1.26.10 no Go install (unchanged)
every other caller python / js — unchanged

techrecon is the only repo that changes. Every other Go lane there already runs 1.26.10: they read go.mod through go-version-file, and deploy.yml pins 1.26.10 exactly. Its ci.yml test job is green on main on 1.26.10, so the coverage lane joins them.

Not in this PR

tests-runner.yml's detect step has the same go-line-only resolver it shared with this file. No fleet repo calls tests-runner.yml with Go today, so it changes nothing yet. It's a follow-up.

Tests

selftest/test_coverage_floor_go_version.py runs the shipped detect step against hand-written go.mod files, and every expected version is a literal. It has 24 cases:

  • 17 resolve cases: go-only, techrecon's go+toolchain shape, a patch that crosses a digit (go 1.26.9 + go1.26.10), toolchain above the go line, a spaced and an unspaced // comment, two stale pins below the go line, an rc pin, a stale rc pin below its release, an rc go line, CRLF, default, -suffix, caller input wins (twice), no go.mod.
  • 3 toolchain lines the job must fail on: 1.26.10, go1.26.10x, banana.
  • 2 pre-measured runs, with those lines, that must not fail.
  • A negative control: a commented-out // toolchain, a misspelled toolchian and a require …/toolchain module path must not move the version. A positive control through the same harness then finds a real directive.
  • A wiring check: setup-go installs the detect step's output, and detect's env carries go_version and pre_measured_coverage_artifact.

Results:

  • RED before each fix:
    • 10 failed on the original resolver.
    • 4 failed on the first commit: the stale pins, the unspaced comment, rc.
    • 3 failed on the second: pre-measured runs and their wiring.
    • 3 failed on the third: the rc cases.
  • GREEN after: 24 passed, under bash 5.3, under macOS /bin/bash 3.2, and with GNU sort first on PATH.
  • Mutation check: 18 of 18 mutants killed. Among them: the pin taken even below the go line, a text sort instead of a version sort, rc mapped before comparing or left in Go's spelling, no comment or CR strip, unusable lines falling back, the pre-measured gate and its env wiring dropped, setup-go wired to the raw input, and the pre-fix resolver.
  • Lint: pinned ruff 0.15.10 check and format are clean, actionlint 1.7.12 is clean, and shellcheck is clean on the extracted step.
  • Full selftest suite on 14f6780: 1304 passed, 1 skipped, in 9m18s. The skip is environmental and predates this change.
  • Evidence: each review finding below was reproduced with a real go binary, or from setup-go's source and README, before it was fixed.

Auto-merge rationale: manual merge. PRs to topcoder1/ci-workflows are always click-merged, and this changes a reusable that every Go repo's required coverage lane calls.

Codex rounds: 8, run in pairs on each commit, and every finding was reproduced before it was fixed:

  • Rounds 1–2, on edce29b: 3 P2s. A stale pin below the go line won, an unspaced // comment failed the job, and the rc spelling was one setup-go can't install. Fixed in 2ccd64f; the rc part was revised in 14f6780.
  • Rounds 3–4, on 2ccd64f: 1 P2. Pre-measured runs failed on go.mod. Fixed in 5740d56.
  • Rounds 5–6, on 5740d56: clean.
  • Rounds 7–8, on 14f6780: clean.

An independent code-reviewer agent separately found the stale-pin and rc issues on edce29b. It approved edce29b..5740d56 with one pre-existing P3 (an rc go line) and recommended normalizing rc rather than failing it. That is 14f6780, which also fixes the P3. Its check of 14f6780: approve, with no finding over the bar. Its probes under bash 5.3 and 3.2, with both BSD and GNU sort, all agree.

🤖 Generated with Claude Code

topcoder1 and others added 4 commits October 25, 2026 20:26
…ine floor

The detect step read only go.mod's `go` directive and handed it to
setup-go as an exact version. setup-go then exports GOTOOLCHAIN=local, so
a repo pinning `toolchain go1.26.10` over `go 1.26.1` had its required
coverage lane measured on go1.26.1, green and with no warning
(whois-api-llc/techrecon#1213, job 120625436528).

Prefer a `toolchain goV` line, fall back to the `go` line, and fail on a
toolchain line that names no Go release instead of measuring the floor
again. A caller's go_version input still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… does

Codex pre-review of the toolchain fix found three valid go.mod files it
mishandled, each reproduced:
- `go 1.26.1` + `toolchain go1.26.0` installed 1.26.0, which cannot build
  the module under GOTOOLCHAIN=local (`go: go.mod requires go >= 1.26.1`).
  Install the newer of the two, as Go treats a lower pin as stale.
- `toolchain go1.26.10//pin` failed the job, though Go ends the token at
  `//`. Strip comments and CR before reading either directive.
- `toolchain go1.27rc2` went to setup-go as 1.27rc2, which it cannot match
  (it compares against 1.27.0-rc.2). Fail it with a message that names the
  go_version form instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex pre-review: a caller passing pre_measured_coverage_artifact
installs no Go, yet a toolchain line the detect step will not install
from (go1.27rc2, valid Go) failed that caller's job before the artifact
download. Resolve the Go release only when the run measures coverage
itself, and pin the env wiring that tells detect which run it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review: failing an rc toolchain sent owners to a go_version
override, and that override outlives the pin, the same silent mismatch
this series removes. An rc go line (`go 1.27rc1`) also already reached
setup-go as 1.27rc1, which it cannot match.

Compare in Go's spelling, which sort -V orders as Go does (1.27rc2 <
1.27.0, checked on GNU and BSD sort), then hand the winner to setup-go as
1.27.0-rc.2, the form its README documents and its matcher accepts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@topcoder1 topcoder1 added the manual-merge Durable hold: never auto-merge; a human must click. Outranks all automerge bypasses. label Oct 26, 2026
@github-actions github-actions Bot added the risk:blocked Risk class: blocked label Oct 26, 2026
@github-actions

Copy link
Copy Markdown

Risk class: blocked — manual merge required.

This PR touches one of the blocked path categories from .github/risk-paths.yml (Dockerfiles, docker-compose, .github/workflows/**, **/.env*, **/secrets*, infra/, terraform/, k8s/, or the classifier config itself).

Auto-merge is refused by claude-author-automerge.yml. A maintainer should review the diff and click "Squash and merge" themselves.

(This is a policy notice, not a code-quality failure. The classify job itself does not fail — required CI checks remain authoritative for "is the code green.")

@github-actions

Copy link
Copy Markdown

The diff is small and focused. I've reviewed both changed files:

.github/workflows/coverage-floor.yml

  • The new INPUT_PRE_MEASURED guard correctly prevents go.mod from being read on pre-measured runs (no Go is installed, so parsing toolchain is pointless and could fail the job on an unreadable line).
  • The directive() awk function correctly strips CRLF and // comments before comparing field 1, and the exit after the first match handles duplicate directives safely.
  • sort -V | tail -n 1 picks the newer of the go line and the toolchain capture — the comment explicitly notes this relies on sort -V ordering 1.27rc2 < 1.27.0 (Go's own order), which is tested in stale-rc-pin-below-release.
  • The RC conversion (1.27rc2 → 1.27.0-rc.2) only needs to handle the final GO_VER value, which is correct: whether the RC came from the go line or was selected as the newer toolchain version, it passes through the same RC_RE check.
  • Error path for unrecognized toolchain spellings (e.g., go1.26.10x, banana) is explicit and intentional — failing loudly avoids the silent re-measurement of the floor.

selftest/test_coverage_floor_go_version.py

  • Test harness feeds the shipped run: script through bash, reading outputs from a fake GITHUB_OUTPUT file — a faithful replica of how Actions runs a run: step.
  • Coverage is comprehensive: go-line-only, toolchain pin, stale pin below go line, RC in toolchain, RC in go line, CRLF, default, custom suffix, caller override, no go.mod, pre-measured bypass, lookalike directives, and the structural assertion that setup-go actually uses steps.detect.outputs.go_version.

No issues found. Diff is well-tested and the logic is correct.

Claude Review · workflow run

@topcoder1
topcoder1 merged commit c0a0d0b into main Oct 26, 2026
18 checks passed
@topcoder1
topcoder1 deleted the fix/coverage-floor-go-toolchain branch October 26, 2026 04:54
topcoder1 added a commit that referenced this pull request Oct 26, 2026
…e Go job (#309)

## What was wrong

`tests-runner.yml`'s `detect` job resolved the Go version the same way
`coverage-floor.yml` did. It read only go.mod's `go` directive and gave
it to `actions/setup-go`, which then exports `GOTOOLCHAIN=local`, so
`go` skips go.mod's `toolchain` line. A Go repo that pins a patched
toolchain would run its tests on the unpatched floor, green and with no
warning.

Reproduced against this branch's base: on techrecon's go.mod (`go
1.26.1` + `toolchain go1.26.10`), the shipped `detect` step outputs
`go_version=1.26.1`. #308 fixes the same code in
coverage-floor.yml, where it measured techrecon's coverage on go1.26.1.
This PR ports that fix.

## The fix

- The resolver from #308, unchanged in its rules:
  - A caller's `go_version` input wins, used as given.
- Otherwise it installs the newer of the `toolchain goV` pin and the
`go` line, compared in Go's order (`1.27rc2 < 1.27.0`).
  - `//` comments and CR are stripped first.
- A release candidate goes to setup-go as `1.27.0-rc.2`, the only
spelling setup-go matches.
- A toolchain line that names no Go release fails with an `::error::`
quoting the line.
- `toolchain default`, or no toolchain line, installs the `go` line; no
go.mod installs `1.23`.
- **It runs in `run-go`, in a new step between checkout and setup-go,
not in `detect`.** If the step failed in `detect`, `run-go` would be
skipped through `needs`, and a skipped required check passes. Today a
line like `toolchain banana` reaches `go test`, which rejects go.mod and
turns the Go check red. Failing in `detect` would make that a skip. In
`run-go`, the failure lands on the Go check itself.
- `detect` loses its `go_version` output, its `INPUT_GO_VERSION` env and
its go.mod read. Nothing else consumed them: `run-go` was the only
reader, and `workflow_call` exposes no outputs.
- The `go_version` input's description now states the default.

The resolver is copied, not shared. A shared action would have to be
pinned by SHA and land in a separate merge first, as `pinned-gate-tests`
is. If #308 changes in review, keep the two copies in step.

## Fleet repos whose behavior changes

None.
- No repo with a go.mod calls `tests-runner.yml`. That comes from a tree
survey of all 145 non-archived repos in `topcoder1` and `whois-api-llc`.
- The 7 callers code search finds (attaxion_dev, crawl-infra, dotclaude,
wxa-jake-ai, wxa-secrets, wxa_marketing, wxa_sanctions) are Python or
JS. For them `run-go` never runs, and `detect` only drops an output they
cannot read.

## Tests

`selftest/test_tests_runner_go_version.py` runs the shipped resolver
step, in its working directory, against hand-written go.mod files. Every
expected version is a literal. It has 22 cases:

- 17 resolve cases, the same set as #308's.
- 3 toolchain lines that must fail the Go job: `1.26.10`, `go1.26.10x`,
`banana`.
- A negative control: a commented-out `// toolchain`, a misspelled
`toolchian` and a `require …/toolchain` path must not move the version.
A positive control through the same harness then finds a real directive.
- A wiring check:
  - The resolver sits in `run-go`, after checkout and before setup-go.
  - setup-go installs its output.
  - It sees `inputs.working_directory` and `inputs.go_version`.

Results:

- **RED on `main`:** 22 failed, because `run-go` has no resolver step.
The old `detect` step separately reproduced `1.26.1` on techrecon's
go.mod.
- **GREEN after:** 22 passed, under bash 5.3, under macOS `/bin/bash`
3.2, and with GNU `sort` first on PATH.
- **Mutation check:** 20 of 20 mutants killed. They were the 15 resolver
mutants from #308's check, setup-go reading `detect`'s old output, no
`working-directory`, `go_version` not wired, the resolver moved after
setup-go, and the resolver moved back into `detect`.
- **Existing guards:** `test_tests_runner_pinned_gates.py`, which drives
`detect`, still passes (24 tests).
- **Lint:** pinned ruff 0.15.10 check and format are clean, actionlint
1.7.12 is clean, and shellcheck is clean on the new step and on
`detect`.
- **Full selftest suite on `2c49b0c`:** 1302 passed, 1 skipped, in
9m56s. The skip is environmental and predates this change.

Auto-merge rationale: manual merge. PRs to topcoder1/ci-workflows are
always click-merged, and this changes a reusable that fleet repos call
for their required test lanes.

Codex rounds: 2, run in parallel on `2c49b0c`. Both were clean. The
resolver itself already went through 8 rounds on #308.

An independent code-reviewer agent approved `2c49b0c` with no finding
over the bar:
- Python, JS and pinned-gate callers are unaffected.
- No `needs.detect.outputs.go_version` remains, and actionlint would
flag a dangling one.
- The step order is checkout → resolve → setup-go → tests.
- The step's executable lines match coverage-floor.yml's at #308's head,
apart from the two conditions that don't apply here (language and
pre-measured).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-merge Durable hold: never auto-merge; a human must click. Outranks all automerge bypasses. risk:blocked Risk class: blocked

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant