Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
---
title: "Custom Linters in Practice: Sergo, Linter Miner, and LintMonster"
description: "How gh-aw grows, audits, and applies its custom Go linters with three cooperating workflows and a trail of linked PRs and issues."
authors:
- copilot
date: 2026-06-26
metadata:
seoDescription: "See how gh-aw uses custom Go linters, Linter Miner, Sergo, and LintMonster, with linked workflow files, PRs, issues, and ADRs."
---

`gh-aw` now registers **35 custom Go analyzers** in
[`cmd/linters/main.go`](https://github.com/github/gh-aw/blob/main/cmd/linters/main.go).
That linter surface is not maintained by hand alone.
It is grown, audited, and applied by three separate
workflows:

- [Linter Miner](https://github.com/github/gh-aw/blob/main/.github/workflows/linter-miner.md)
proposes new analyzers from recurring patterns.
- [Sergo](https://github.com/github/gh-aw/blob/main/.github/workflows/sergo.md)
stress-tests those analyzers for false positives,
false negatives, and suppression gaps.
- [LintMonster](https://github.com/github/gh-aw/blob/main/.github/workflows/lint-monster.md)
runs the custom suite and turns findings into
tracked cleanup work.

The interesting part is not that each workflow exists.
It is that they form a loop: one workflow adds lint
rules, another challenges them, and a third drives the
codebase toward compliance.

## Linter Miner keeps adding new rules

The
[workflow definition](https://github.com/github/gh-aw/blob/main/.github/workflows/linter-miner.md)
is explicit about its job: mine discussions, issues,
and Go source, pick one new linter idea, implement it,
and open a PR. GitHub search currently shows a long run
of
[`[linter-miner]` PRs](https://github.com/search?q=repo%3Agithub%2Fgh-aw+is%3Apr+%22%5Blinter-miner%5D%22&type=pullrequests),
and the recent examples are concrete:

- [`fprintlnsprintf`](https://github.com/github/gh-aw/pull/34498)
flags `fmt.Fprintln(w, fmt.Sprintf(...))` and links to
[ADR 34498](https://github.com/github/gh-aw/blob/main/docs/adr/34498-add-fprintlnsprintf-linter.md).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Only fprintlnsprintf gets an inline ADR link here; the other five linters also have ADRs but their decision records are deferred to the "Further evidence" section. This inconsistency can make fprintlnsprintf look special rather than representative.

💡 Options

Option A — Keep inline links for all examples that have ADRs:

- [`timeafterleak`](https://github.com/github/gh-aw/pull/39133)[ADR 39133](...) ...
- [`errorfwrapv`](https://github.com/github/gh-aw/pull/39263)[ADR 39263](...) ...

Option B — Remove the inline ADR link from fprintlnsprintf and rely on the "Further evidence" section for all ADR pointers (keeps the list compact and uniform).

Option B is simpler and avoids repetition between the list and the reference section.

- [`timeafterleak`](https://github.com/github/gh-aw/pull/39133)
catches `time.After(...)` inside `for`+`select`
loops.
- [`errorfwrapv`](https://github.com/github/gh-aw/pull/39263)
flags `fmt.Errorf(...%v..., err)` where `%w` should
preserve the error chain.
Comment on lines +49 to +50
- [`wgdonenotdeferred`](https://github.com/github/gh-aw/pull/40837)
catches non-deferred `sync.WaitGroup.Done()` calls.
- [`lenstringsplit`](https://github.com/github/gh-aw/pull/41090)
rewrites `len(strings.Split(s, sep))` to
`strings.Count(s, sep)+1` when the separator is
provably non-empty.
- [`stringreplaceminusone`](https://github.com/github/gh-aw/pull/41285)
rewrites `strings.Replace(..., -1)` to
`strings.ReplaceAll(...)`.

This is not a one-off burst. The same theme appears in
the blog's own weekly updates:
[May 25](https://github.com/github/gh-aw/blob/main/docs/src/content/docs/blog/2026-05-25-weekly-update.md),
[June 15](https://github.com/github/gh-aw/blob/main/docs/src/content/docs/blog/2026-06-15-weekly-update.md),
and
[June 22](https://github.com/github/gh-aw/blob/main/docs/src/content/docs/blog/2026-06-22-weekly-update.md).
Those posts document `fprintlnsprintf`,
`timeafterleak`, `errorfwrapv`, and `deferinloop` as
shipped work rather than aspirational ideas.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] deferinloop is cited here as shipped work but is absent from the concrete examples list above (lines 42–59). This creates a small gap: readers who follow the examples list won't see deferinloop, then encounter it as an afterthought in the trailing sentence.

💡 Suggestion

Either add deferinloop as a seventh bullet in the examples list:

- [`deferinloop`](https://github.com/github/gh-aw/pull/40679)
  flags `defer` inside `for`-loop bodies where cleanup
  fires at function return, not loop iteration end.

Or remove the specific mention from line 68 and keep the weekly update links as general cross-references without naming individual linters.

The current mix (6 examples with links, then a 7th name-only mention) weakens the parallel structure.


## Sergo pressure-tests the linters after they land

Where Linter Miner expands the rule set,
[Sergo](https://github.com/github/gh-aw/blob/main/.github/workflows/sergo.md)
does the adversarial follow-up. The workflow is focused
on actionable Go analysis using Serena, and its issue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Serena is introduced without explanation — first-time readers won't know it refers to the MCP-based Go language-service protocol used inside Sergo. A brief parenthetical or link would prevent confusion with the workflow name Sergo itself.

💡 Suggestion

Consider:

on actionable Go analysis using [Serena](https://github.com/github/gh-aw/blob/main/shared/mcp/serena-go.md)
(a Serena MCP language-service layer), and its issue ...

Or at minimum: using Serena (an MCP Go analysis service).

history shows a steady pattern:
find a precision gap, write a tightly scoped issue, and
let the next PR harden the analyzer.

The clearest evidence is the issue-to-PR chain:

- [Issue #40244](https://github.com/github/gh-aw/issues/40244)
found that `errstringmatch` only handled
`strings.Contains(err.Error(), ...)`;
[PR #40248](https://github.com/github/gh-aw/pull/40248)
extended coverage to `HasPrefix`, `HasSuffix`,
`EqualFold`, `Index`, `LastIndex`, and `Compare`.
Comment on lines +87 to +88
- [Issue #41377](https://github.com/github/gh-aw/issues/41377)
found missing `//nolint:` support across four
context-family linters;
[PR #41382](https://github.com/github/gh-aw/pull/41382)
added suppression parity.
- [Issue #41376](https://github.com/github/gh-aw/issues/41376)
found a false negative in `manualmutexunlock` when
two struct instances shared the same mutex field;
[PR #41383](https://github.com/github/gh-aw/pull/41383)
fixed the keying model.
- [Issue #40947](https://github.com/github/gh-aw/issues/40947)
found that `wgdonenotdeferred` missed goroutine
closures launched inside loops;
[PR #41026](https://github.com/github/gh-aw/pull/41026)
fixed the function-literal scope boundary.
- [Issue #41163](https://github.com/github/gh-aw/issues/41163)
found that `lenstringsplit` mishandled an empty
raw-string separator;
[PR #41188](https://github.com/github/gh-aw/pull/41188)
fixed the false positive and the broken autofix.

There is also useful evidence in the failures. Sergo's
[Issue #40243](https://github.com/github/gh-aw/issues/40243)
bundled several package-identity precision fixes into
one direction, and
[PR #40247](https://github.com/github/gh-aw/pull/40247)
closed unmerged after sprawling into a large branch.
The narrower follow-up work still landed, including
[PR #40248](https://github.com/github/gh-aw/pull/40248).
That is a good sign: the workflow is producing reviewable
problems, not just optimistic reports.

## LintMonster turns diagnostics into repository work

[LintMonster](https://github.com/github/gh-aw/blob/main/.github/workflows/lint-monster.md)
operates later in the loop. It runs
`make golint-custom`, groups findings by root cause,
creates or updates issues, and can assign up to three
Copilot agent sessions to fix them.

Its evidence trail is easy to follow:

- [Issue #40932](https://github.com/github/gh-aw/issues/40932)
grouped four resource-lifecycle and context-propagation
findings; [PR #41589](https://github.com/github/gh-aw/pull/41589)
merged the targeted fixes.
- [Issue #40933](https://github.com/github/gh-aw/issues/40933)
tracked hard-coded path constants;
[PR #41611](https://github.com/github/gh-aw/pull/41611)
replaced the flagged literals with existing constants.
- [Issue #39314](https://github.com/github/gh-aw/issues/39314)
established an authoritative function-length backlog
for **653 findings**.
- [Issue #41466](https://github.com/github/gh-aw/issues/41466)
refreshed that same backlog at **660 findings** and
kept it consolidated instead of spawning duplicate
tracking issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/zoom-out] The function-length backlog grew from 653 to 660 findings across the two consolidated issues, which sits in tension with the surrounding narrative that LintMonster "drives the codebase toward compliance." A one-sentence explanation would resolve this — the backlog tracks a single evolving rule, and new code additions naturally introduce new violations.

💡 Suggestion

Add a sentence after line 145 such as:

The count ticked up slightly because the funclength rule now covers a broader set of signatures and new code keeps arriving; the backlog exists to absorb that ongoing pressure systematically rather than silently.

Without this, a reader could reasonably conclude that LintMonster is making things worse for the function-length rule specifically.


This is what makes the custom linter suite operational
instead of decorative. Rules only matter if they change
the repository. LintMonster is the workflow that turns
diagnostics into queues, slices, assignments, and merged
cleanup work.

## Why the three-workflow loop matters

Taken together, the workflows separate three jobs that
usually get conflated:

1. **Invent a rule from a real pattern.**
Linter Miner does this with new analyzers such as
[`timeafterleak`](https://github.com/github/gh-aw/pull/39133)
and
[`lenstringsplit`](https://github.com/github/gh-aw/pull/41090).
2. **Challenge the rule's correctness.**
Sergo does this with issues such as
[#40947](https://github.com/github/gh-aw/issues/40947)
and
[#41163](https://github.com/github/gh-aw/issues/41163).
3. **Apply the rule to production code.**
LintMonster does this with issue-to-PR chains such as
[#40932 → #41589](https://github.com/github/gh-aw/issues/40932)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading arrow link: the link text #40932 → #41589 implies an issue-to-PR chain but the URL resolves only to issue #40932 — PR #41589 is unreachable from this click.

💡 Suggested fix

Split into two inline links so both artifacts are reachable:

[#40932](https://github.com/github/gh-aw/issues/40932)[#41589](https://github.com/github/gh-aw/pull/41589)

Same pattern applies to [#40933 → #41611] on line 172.

and
[#40933 → #41611](https://github.com/github/gh-aw/issues/40933).

That split is why the system looks durable. New rules
keep arriving. Old rules keep getting corrected. The
repository keeps absorbing the results.

## Further evidence

If you want to inspect the trail directly, start here:

- Source workflows:
[Linter Miner](https://github.com/github/gh-aw/blob/main/.github/workflows/linter-miner.md),
[Sergo](https://github.com/github/gh-aw/blob/main/.github/workflows/sergo.md),
[LintMonster](https://github.com/github/gh-aw/blob/main/.github/workflows/lint-monster.md)
- Linter registry:
[`cmd/linters/main.go`](https://github.com/github/gh-aw/blob/main/cmd/linters/main.go)
- ADRs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ADR reference: the post highlights errorfwrapv as one of six key linter-miner examples, but its ADR (39263) is absent from the ADR list here — the only featured linter with no entry in this section.

💡 Suggested fix

Add the errorfwrapv ADR after 39133:

- ADRs:
  [34498](...),
  [39133](...),
  [39263](https://github.com/github/gh-aw/blob/main/docs/adr/39263-custom-linter-for-errorf-percent-v-error-wrapping.md),
  [40837](...),
  ...

The file docs/adr/39263-custom-linter-for-errorf-percent-v-error-wrapping.md exists in the repo.

[34498](https://github.com/github/gh-aw/blob/main/docs/adr/34498-add-fprintlnsprintf-linter.md),
[39133](https://github.com/github/gh-aw/blob/main/docs/adr/39133-custom-linter-for-time-after-leaks-in-loops.md),
[40837](https://github.com/github/gh-aw/blob/main/docs/adr/40837-add-wgdonenotdeferred-linter.md),
[41090](https://github.com/github/gh-aw/blob/main/docs/adr/41090-add-lenstringsplit-linter.md),
[41285](https://github.com/github/gh-aw/blob/main/docs/adr/41285-add-stringreplaceminusone-linter.md)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The ADR list covers five of the six concrete linters from the examples section, but omits errorfwrapv — which does have an ADR (39263-custom-linter-for-errorf-percent-v-error-wrapping.md). This breaks the evidence trail for a reader who wants to follow every example to its decision record.

💡 Suggestion

Add the missing ADR between the 39133 and 40837 entries:

- ADRs:
  [34498](...fprintlnsprintf...),
  [39133](...time-after-leaks...),
  [39263](https://github.com/github/gh-aw/blob/main/docs/adr/39263-custom-linter-for-errorf-percent-v-error-wrapping.md),
  [40837](...wgdonenotdeferred...),
  [41090](...lenstringsplit...),
  [41285](...stringreplaceminusone...)

- Search views:
[`[linter-miner]` PRs](https://github.com/search?q=repo%3Agithub%2Fgh-aw+is%3Apr+%22%5Blinter-miner%5D%22&type=pullrequests),
[`label:sergo` issues](https://github.com/search?q=repo%3Agithub%2Fgh-aw+is%3Aissue+label%3Asergo&type=issues),
[`label:lint-monster` issues](https://github.com/search?q=repo%3Agithub%2Fgh-aw+is%3Aissue+label%3Alint-monster&type=issues)

This is a useful pattern beyond `gh-aw`: treat static
analysis as a living workflow system, not just a binary
that runs in CI.
Loading