Skip to content

A module-wide check is triggered by the module, not by the file that changed - #129

Merged
HackingGate merged 1 commit into
mainfrom
go-ids-run-on-every-change
Sep 2, 2026
Merged

A module-wide check is triggered by the module, not by the file that changed#129
HackingGate merged 1 commit into
mainfrom
go-ids-run-on-every-change

Conversation

@HackingGate

@HackingGate HackingGate commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Two defects in the four published Go ids, found by a fleet migrating off its
own hand-copied guards manifest onto uphold-gofmt / uphold-go-vet /
uphold-go-build / uphold-go-test.

The trigger asked about the file, the command reads the module (#127)

uphold-go-vet, uphold-go-build and uphold-go-test carried
pass_filenames: false and files: '(\.go|go\.mod|go\.sum)$'. A commit staging
only a testdata fixture, a //go:embed asset or a golden file changes what the
module is compiled and tested against and skipped all three:

prek run --files testdata/x.json   ->   (no files to check)Skipped

pre-commit answers identically. CI running --all-files finds it afterwards,
so the local gate is a green the later red contradicts, at the seam that
promised to catch it first -- and the fleet declarations these ids replaced
carried no filter, so the published id was a regression for the repositories
adopting it.

The regex comes off those three and nothing replaces it. Measured on both
runners: with no files: the staged set is filtered by nothing, so the hook
runs on any commit and is skipped only where the file set is empty. That is
exactly the wanted spelling -- the whole module on any change, nothing on an
empty file set. always_run: true was the other candidate and goes one step
too far: it runs where a runner was handed no files at all.

uphold-gofmt keeps the regex. Formatting is the one question here whose
answer a file that is not Go cannot change.

stages: was narrowed for a lefthook reason that is not a prek reason

The four declared stages: [pre-commit] where the guards manifest they replaced
declared [pre-commit, pre-merge-commit, pre-push, manual], and the reason
written in the file is the lefthook group limitation: lefthook run <group>
applies no glob, so a Go job reachable that way fires in a repository with no
Go in it. That reason was never about pre-commit or prek, which have a file set
at every one of the four stages. It is fixed here: all four now declare all
four stages. The merge stage matters most -- git runs pre-merge-commit for a
merge and pre-commit for a commit, so a merge bringing in a module change ran
none of these four.

hooks/lefthook.yml keeps its Go jobs at pre-commit and conditions three of
them on test -f go.mod rather than on a glob. This is the one place the two
published files stop matching token for token, and the asymmetry is between the
distribution shapes rather than between the checks: a .pre-commit-config.yaml
consumer NAMES the id, so an unconditional id fires only where it was asked for,
while a lefthook remote config is merged into the consumer's own wholesale, so
an unconditional job arrives in every consuming repository on a ref: bump, Go
or not. That is the same argument this file already makes for keeping
uphold-supply-chain out of it. The condition moves from the staged set to the
repository, which is the level it was always about. Verified against lefthook
2.1.9 directly: the go.mod condition and the {{.Name}} template both survive
lefthook's own command templating.

go build -o <dir> ./... refuses every library-only module (#128)

-o keeps a single main package from writing its executable into the tree the
hook has just pronounced clean, and it also exits 1 with go: no main packages to build on a module with no package main in it, which is most of a Go fleet.
A repository like that could not commit at all, and the message it was refused
with said nothing about whether the module compiles.

The shape is asked about first now:

if go list -f {{.Name}} ./... 2>/dev/null | grep -qx main; then
    out="$(mktemp -d)"; go build -o "$out" ./...; status=$?; rm -rf "$out"; exit $status
else
    go build ./...
fi

go build ./... with no -o compiles every package and writes nothing, which
is the right command for the library case and the wrong one for the single-main
case. go list is silenced because a module too broken to enumerate takes the
bare branch, which prints the compiler's own error rather than a listing failure
standing in for one.

Tests

scripts/consumer_check.sh is where the four ids are driven, and it gains the
two questions that would have caught these:

  • 10. plants a failing test with hooks off, then commits ONLY a testdata
    file -- the commit the regex skipped.
  • 11. turns the consumer's module into a library -- the shape -o refuses.

Both were run against the manifest as it stood and both fail there
(uphold-go-test did not refuse, and go: no main packages to build), and both
pass against this branch. The script also now pins the four ids at question 9,
when the consumer becomes a repository with Go in it, rather than from the
start: three of them run on every commit by design now, and a module-wide check
pinned by a repository with no module fails on every commit, loudly and
correctly. lefthook needs no equivalent, which is what its go.mod condition is
for.

Run locally: cargo build, cargo test (725 passing), cargo clippy --all-targets, cargo fmt --check, prek run --all-files (21 hooks, all
passed), and scripts/consumer_check.sh prek (all eleven questions).

Closes #127
Closes #128

Summary by CodeRabbit

  • Enhancements

    • Go formatting, vetting, building, and testing checks now run across commit, merge, push, and manual stages.
    • Go checks run for Go modules even when no Go files changed.
    • Go builds now support library-only modules and single-main-package modules more reliably.
  • Documentation

    • Updated setup guidance to reflect the broader Go check coverage and module-based configuration.
  • Tests

    • Expanded consumer validation to cover unchanged Go files and library-only modules.

https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd

…changed

`uphold-go-vet`, `uphold-go-build` and `uphold-go-test` declared
`pass_filenames: false` and a `files:` regex on Go paths, and the two did not
agree. The command reads the whole module; the trigger asked whether a Go path
was staged. A commit staging only a testdata fixture, a //go:embed asset or a
golden file changes what the module is compiled and tested against and skipped
all three, measured in a consumer as

  prek run --files testdata/x.json   ->   (no files to check)Skipped

CI running --all-files found it afterwards, which makes the local gate a green
the later red contradicts, at the seam that promised to catch it first. The
hand-copied declarations these ids replaced across the fleet carried no filter,
so the filter was a regression handed to every repository that adopted the id.

The regex comes off those three and nothing takes its place. With no `files:` a
runner filters the staged set by nothing, so the list is non-empty on any commit
and empty only where nothing was staged: the whole module on any change, and
nothing on an empty file set, which both runners agree on. `always_run: true`
would be one step further and run `go test ./...` where a runner was handed no
files at all. `uphold-gofmt` keeps the regex, because formatting is the one
question here whose answer a file that is not Go cannot change.

`stages:` reaches every stage a runner has a file set at, which is what the
fleet declarations these ids replaced declared. The `[pre-commit]` they carried
was a lefthook reason -- `lefthook run <group>` applies no `glob` -- and that
reason was never about pre-commit or prek. The merge stage is where the
strongest version of the same defect lives: git runs pre-merge-commit for a
merge and pre-commit for a commit, so a merge bringing in a module change ran
none of these four.

hooks/lefthook.yml conditions its three on `go.mod` existing instead of on a
glob, because the two files are distributed differently: a manifest id is NAMED
by the consumer, so an unconditional id fires only where it was asked for, while
a lefthook remote config is merged wholesale and an unconditional job would
arrive in every consuming repository on a `ref:` bump, Go or not. The condition
moves from the commit to the repository, which is the level it was always about.

Second defect, same four ids. `uphold-go-build` ran `go build -o <dir> ./...`,
and `-o` is right for one shape of module and a refusal for the other: it keeps
a single main package from writing its executable into the tree the hook has
just pronounced clean, and it exits 1 with "go: no main packages to build" on
every library-only module, which is most of a Go fleet. A repository with no
`package main` in it could not commit. The packages are asked about first now,
and a module with no main is compiled by the bare `go build ./...`, which builds
every package and writes nothing.

scripts/consumer_check.sh gains the two questions that would have caught them.
Question 10 plants a failing test with hooks off and then commits ONLY a testdata
file, which is the commit the regex skipped; question 11 makes the module a
library, which is the shape `-o` refuses. Both fail against the manifest as it
stood and pass against this one, under prek locally and under all three runners
in CI. The four ids are now pinned by that consumer at question 9, when it
becomes a repository with Go in it, rather than from the start: a module-wide
check pinned by a repository with no module fails on every commit, loudly and
correctly.

Closes #127
Closes #128

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 8e72210a-32a5-480f-b981-ef8a4e4a4ca5

📥 Commits

Reviewing files that changed from the base of the PR and between 001f8ab and 1a4fbfc.

📒 Files selected for processing (4)
  • .pre-commit-hooks.yaml
  • README.md
  • hooks/lefthook.yml
  • scripts/consumer_check.sh

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The pull request updates Go hook triggers across supported stages, changes Lefthook gating to detect go.mod, selects build behavior by package type, and expands consumer checks for fixture-only changes and library modules.

Changes

Go hook execution and validation

Layer / File(s) Summary
Go hook trigger contract
.pre-commit-hooks.yaml, hooks/lefthook.yml
uphold-go-vet, uphold-go-build, and uphold-go-test run without staged-file filters across four stages. uphold-gofmt keeps its Go-file filter and uses the same stages. Lefthook gates Go jobs on go.mod.
Build command selection
hooks/lefthook.yml
uphold-go-build inspects package names. It uses temporary output for a single main package and a bare build for library-only or multi-package modules.
Consumer coverage and documentation
README.md, scripts/consumer_check.sh
Documentation reflects module-wide execution. Consumer checks cover non-Go staged changes and library-only module builds.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 1a4fb

The changes correct hook triggering and library-only Go build behavior, with targeted consumer checks and passing validation reported; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant HookRunner
  participant Lefthook
  participant GoToolchain
  HookRunner->>Lefthook: run Go validation jobs
  Lefthook->>Lefthook: check for go.mod
  Lefthook->>GoToolchain: inspect package names
  GoToolchain-->>Lefthook: return package names
  Lefthook->>GoToolchain: build with temporary output or bare build
  GoToolchain-->>Lefthook: return validation status
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy both linked issues. They remove staged-file filters from the three module-wide Go hooks while preserving the gofmt filter, restore all required stages, add the go.mod condition for…
Out of Scope Changes check ✅ Passed The changed hook declarations, lefthook configuration, README documentation, and consumer regression tests directly support the linked objectives. No unrelated code changes are identified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: module-wide Go checks now depend on the module rather than the changed file. It is concise, specific, and directly related to the pull request.
Full details: Linked Issues check

Explanation

The changes satisfy both linked issues. They remove staged-file filters from the three module-wide Go hooks while preserving the gofmt filter, restore all required stages, add the go.mod condition for lefthook, select the correct build mode for main and library-only modules, and add regression coverage [#127] [#128].

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1 files. (3 skipped: 3 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch go-ids-run-on-every-change

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.12%. Comparing base (001f8ab) to head (1a4fbfc).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #129   +/-   ##
=======================================
  Coverage   94.12%   94.12%           
=======================================
  Files          37       37           
  Lines       13493    13493           
=======================================
  Hits        12700    12700           
  Misses        793      793           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@HackingGate
HackingGate merged commit 646a230 into main Sep 2, 2026
12 checks passed
@HackingGate
HackingGate deleted the go-ids-run-on-every-change branch September 2, 2026 11:29
HackingGate added a commit that referenced this pull request Sep 2, 2026
This branch sat on integration-2026-09-02, whose five branches have now
landed on main as squashes (#116, #117, #123, #124, #126) alongside #129,
so git saw the same lines added twice in src/scan.rs, src/shim.rs,
src/text.rs, src/config.rs and tests/structural_documentation.rs.
Resolved to the tree a rebase of this branch's own commit onto main
produces: main's copy of all six, plus this branch's rule-as-a-type
refactor on top.

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
HackingGate added a commit that referenced this pull request Sep 2, 2026
… unknown that is no longer a pass (#132)

Point the documented pins at 1.14.0 and bump the crate version.

Minor, not patch. This release carries names a policy written against 1.13.0
cannot use: the bundled set mismatched-author; api:* as a matchable verb on
the gh and glab shim tables, so a forge call that carries a body is read the
way gh pr create is; unresolved on a [[shim]] table, whose default refuses
before exec where a scope could not be evaluated; and refuse_unknown,
foreign_hosts and redact_matches on the private-name family.

It also closes four seams that reported a pass they had not established. The
MCP hook seam consulted no prose rule, so every prose_regexp was dark exactly
where an agent publishes. Five command lines walked past the shim, four of
them ending in exit 0 with nothing printed. A blob no charset decodes was read
as clean rather than refused. A forge host nobody could ask about produced no
finding, no report and no exit code.

Landed in this release: #116, #117, #123, #124, #126, #129, #131.

A policy written against 1.13.0 loads unchanged, and every new name has to be
named or inherited to run. Two things do change for a consumer who only bumps
a rev or a ref: the seam fixes above start refusing text and command lines
that used to pass unexamined, and the four published Go ids are triggered by
the module rather than by the file that changed, at all four stages rather
than at pre-commit alone.

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants