-
Notifications
You must be signed in to change notification settings - Fork 552
CNTRLPLANE-4008: feat: add hypershiftlinter golangci-lint plugin #9237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
openshift-merge-bot
merged 6 commits into
openshift:main
from
bryan-cox:worktree-hypershiftlinter-plugin
Aug 17, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6468258
chore(hack/tools): vendor golang.org/x/tools analysistest packages
bryan-cox 47c85aa
feat(hack/tools): add hypershiftlinter golangci-lint plugin framework
bryan-cox 933ab4b
feat(hack/tools): add hypershiftlinter static analysis analyzers
bryan-cox bbc3ebf
ci: add GHA workflows for hypershiftlinter tests
bryan-cox bf691b5
fix(hack/tools): tighten testcasename map detection and exempt AfterS…
bryan-cox 91e3068
docs(hack/tools): add README explaining the hypershiftlinter plugin
bryan-cox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,3 +11,4 @@ MIs | |
| AfterAll | ||
| SME | ||
| uptodate | ||
| enbale | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| name: Unit Tests (HyperShift Linter) (Reusable) | ||
|
|
||
| on: | ||
| workflow_call: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test-linter: | ||
| name: HyperShift Linter | ||
| runs-on: arc-runner-set | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| - run: make test-linter |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| name: Unit Tests (HyperShift Linter) | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - main | ||
| - release-4.22 | ||
| paths: | ||
| - 'hack/tools/hypershiftlinter/**' | ||
| - 'hack/tools/go.mod' | ||
| - 'hack/tools/go.sum' | ||
| - 'hack/tools/vendor/**' | ||
| - 'Makefile' | ||
| - '.github/workflows/test-linter.yaml' | ||
| - '.github/workflows/test-linter-reusable.yaml' | ||
|
|
||
| jobs: | ||
| test-linter: | ||
| uses: openshift/hypershift/.github/workflows/test-linter-reusable.yaml@main | ||
| permissions: | ||
| contents: read | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # hypershiftlinter | ||
|
|
||
| `hypershiftlinter` is a custom [golangci-lint](https://golangci-lint.run/) plugin | ||
| that automatically enforces HyperShift's testing conventions through static | ||
| analysis. | ||
|
|
||
| ## Why this exists | ||
|
|
||
| We already document our testing conventions in | ||
| [`TESTING.md`](../../../TESTING.md) and | ||
| [`test/e2e/v2/AGENTS.md`](../../../test/e2e/v2/AGENTS.md), but until now nothing | ||
| enforced them. Conventions that live only in docs get followed inconsistently — | ||
| reviewers have to catch violations by hand, and many slip through. | ||
|
|
||
| This plugin turns those conventions into machine-enforced checks instead of | ||
| relying on reviewer memory. That matters for several reasons: | ||
|
|
||
| 1. **Machine-enforced consistency.** Conventions become automated checks rather | ||
| than tribal knowledge. This is what caught real issues in review — for | ||
| example, tests that silently skipped ~60 lines of assertions because guard | ||
| strings no longer matched renamed test cases, and vacuously-passing tests. | ||
| 2. **Better test quality and reliability.** The `vacuouspass` analyzer catches | ||
| tests that pass without actually asserting anything, a common source of false | ||
| confidence in a test suite. | ||
| 3. **Cleaner Sippy/CI signal.** Enforcing correct | ||
| `[sig-hypershift][Jira:Hypershift]` and `[Feature:X]` annotations keeps our | ||
| e2e results properly categorized in Sippy. | ||
| 4. **Lower review burden.** Reviewers spend less time on mechanical naming and | ||
| convention nits and more on substance. | ||
|
|
||
| ## Analyzers | ||
|
|
||
| The plugin ships 7 analyzers, scoped so each rule only fires where it applies. | ||
|
|
||
| ### Unit test conventions (`TESTING.md`, unit tests only) | ||
|
|
||
| | Analyzer | Enforces | | ||
| | -------------- | ----------------------------------------------------------------------------------------- | | ||
| | `testcasename` | Test case name fields match `When <condition>, it should <expected behavior>`. | | ||
| | `testfuncname` | Test functions do not use the `Test_` prefix; use `TestFunctionName` instead. | | ||
|
|
||
| ### E2E conventions (`test/e2e/v2/` only) | ||
|
|
||
| | Analyzer | Enforces | | ||
| | ------------------- | ------------------------------------------------------------------------------------------------ | | ||
| | `guestcluster` | Bans "guest cluster" terminology; use "hosted cluster" instead. | | ||
| | `contextbackground` | Bans `context.Background()` / `context.TODO()` in tests; use `tc.Context` instead. | | ||
| | `vacuouspass` | Flags vacuously-passing tests that iterate a collection without asserting it is non-empty. | | ||
| | `ipv6url` | Detects `fmt.Sprintf` URL patterns that break with IPv6; use `net.JoinHostPort` instead. | | ||
| | `sippyannotation` | Requires the correct Sippy/Jira `[Feature:X]` annotations on Ginkgo `Describe` blocks. | | ||
|
|
||
| ## How it's built and run | ||
|
|
||
| The plugin builds as a Go shared library (`.so`) via | ||
| `go build -buildmode=plugin`. The plugin and the golangci-lint host binary must | ||
| be compiled from the same `hack/tools/go.mod` — a `golang.org/x/tools` version | ||
| mismatch causes `plugin.Open()` to fail at runtime. | ||
|
|
||
| Relevant Makefile targets: | ||
|
|
||
| - `make hypershiftlinter.so` — build the plugin shared library. | ||
| - `make hypershift-lint-all` — opt-in target to run the analyzers against the | ||
| current tree. | ||
| - `make test-linter` — run the analyzers' own unit tests | ||
| (`go test ./hypershiftlinter/analyzers/...`). | ||
|
|
||
| Each analyzer has [`analysistest`](https://pkg.go.dev/golang.org/x/tools/go/analysis/analysistest)-based | ||
| unit tests with good/bad `testdata/` fixtures. | ||
|
|
||
| ## Staged rollout | ||
|
|
||
| Enablement is intentionally staged. The initial change lands the plugin, the | ||
| analyzers, and their unit tests only — **it does not enable enforcement**. A | ||
| follow-up wires the plugin into `.golangci.yml` / `make lint` and fixes the | ||
| existing violations in the tree. | ||
|
|
||
| Splitting it this way keeps the review surface small and lets CI actually run the | ||
| analyzers' own tests before enforcement is turned on. (A brand-new reusable | ||
| workflow can't get a green pre-merge run on the PR that introduces it, because | ||
| GitHub resolves `uses: ...@main` and the `pull_request` trigger from the base | ||
| branch — so the foundational plumbing has to land on `main` first.) |
112 changes: 112 additions & 0 deletions
112
hack/tools/hypershiftlinter/analyzers/contextbackground/contextbackground.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package contextbackground | ||
|
|
||
| import ( | ||
| "go/ast" | ||
| "strings" | ||
|
|
||
| "github.com/openshift/hypershift/hack/tools/hypershiftlinter/analyzers/pathutil" | ||
|
|
||
| "golang.org/x/tools/go/analysis" | ||
| ) | ||
|
|
||
| var Analyzer = &analysis.Analyzer{ | ||
| Name: "contextbackground", | ||
| Doc: "bans context.Background() and context.TODO() in test files; use tc.Context instead", | ||
| Run: run, | ||
| } | ||
|
|
||
| func run(pass *analysis.Pass) (any, error) { | ||
| for _, file := range pass.Files { | ||
| filename := pass.Fset.File(file.Pos()).Name() | ||
| if !pathutil.IsV2E2ETest(filename) || !strings.HasSuffix(filename, "_test.go") { | ||
| continue | ||
| } | ||
|
|
||
| ast.Inspect(file, func(n ast.Node) bool { | ||
| call, ok := n.(*ast.CallExpr) | ||
| if !ok { | ||
| return true | ||
| } | ||
|
|
||
| if !isContextBackgroundOrTODO(call) { | ||
| return true | ||
| } | ||
|
|
||
| if isInsideExemptFunc(file, call) { | ||
| return true | ||
| } | ||
|
|
||
| pass.Report(analysis.Diagnostic{ | ||
| Pos: call.Pos(), | ||
| End: call.End(), | ||
| Message: "use tc.Context instead of context.Background()/context.TODO()", | ||
| }) | ||
| return true | ||
| }) | ||
| } | ||
| return nil, nil | ||
| } | ||
|
|
||
| func isContextBackgroundOrTODO(call *ast.CallExpr) bool { | ||
| sel, ok := call.Fun.(*ast.SelectorExpr) | ||
| if !ok { | ||
| return false | ||
| } | ||
| ident, ok := sel.X.(*ast.Ident) | ||
| if !ok { | ||
| return false | ||
| } | ||
| return ident.Name == "context" && (sel.Sel.Name == "Background" || sel.Sel.Name == "TODO") | ||
| } | ||
|
|
||
| func isInsideExemptFunc(file *ast.File, target *ast.CallExpr) bool { | ||
| exempt := false | ||
| ast.Inspect(file, func(n ast.Node) bool { | ||
| if exempt { | ||
| return false | ||
| } | ||
| call, ok := n.(*ast.CallExpr) | ||
| if !ok { | ||
| return true | ||
| } | ||
| name := callName(call) | ||
| // Only suite-level setup/teardown hooks are exempt: they run before | ||
| // TestContext is initialized (or after it would be meaningful), so | ||
| // context.Background() is the correct choice there. DeferCleanup is NOT | ||
| // exempt — per test/e2e/v2/AGENTS.md, TestContext.Context is initialized | ||
| // once in BeforeSuite and is not canceled during cleanup, so cleanup | ||
| // callbacks that have access to tc must use tc.Context. | ||
| if name == "BeforeSuite" || name == "AfterSuite" || name == "SynchronizedBeforeSuite" || name == "SynchronizedAfterSuite" { | ||
| for _, arg := range call.Args { | ||
| if containsNode(arg, target) { | ||
| exempt = true | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| return true | ||
| }) | ||
| return exempt | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| func callName(call *ast.CallExpr) string { | ||
| switch fn := call.Fun.(type) { | ||
| case *ast.Ident: | ||
| return fn.Name | ||
| case *ast.SelectorExpr: | ||
| return fn.Sel.Name | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func containsNode(tree ast.Node, target ast.Node) bool { | ||
| found := false | ||
| ast.Inspect(tree, func(n ast.Node) bool { | ||
| if n == target { | ||
| found = true | ||
| return false | ||
| } | ||
| return !found | ||
| }) | ||
| return found | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
hack/tools/hypershiftlinter/analyzers/contextbackground/contextbackground_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package contextbackground | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "golang.org/x/tools/go/analysis/analysistest" | ||
| ) | ||
|
|
||
| func TestAnalyzer(t *testing.T) { | ||
| testdata := analysistest.TestData() | ||
| analysistest.Run(t, testdata, Analyzer, "test/e2e/v2/good", "test/e2e/v2/bad") | ||
| } |
77 changes: 77 additions & 0 deletions
77
...ols/hypershiftlinter/analyzers/contextbackground/testdata/src/test/e2e/v2/bad/bad_test.go
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.