From aa8ee201d052ac286149066dc3e97704321d5e74 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 18:17:22 +0000 Subject: [PATCH 1/2] feat(linters): add osexitinlibrary linter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reports os.Exit calls inside library (pkg/) packages. Calling os.Exit in a library package bypasses all deferred cleanup (file closes, mutex unlocks, cleanup goroutines) and makes the code path impossible to test. Only cmd/ entry-points should terminate the process. Evidence from code scan: - pkg/cli/upgrade_command.go:178 — os.Exit(0) after successful upgrade - pkg/cli/upgrade_command.go:376 — os.Exit(exitErr.ExitCode()) The linter skips packages whose import path contains /cmd/ or ends in /main so that true entry-points are never flagged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cmd/linters/main.go | 2 + .../osexitinlibrary/osexitinlibrary.go | 52 +++++++++++++++++++ .../osexitinlibrary/osexitinlibrary_test.go | 16 ++++++ .../src/osexitinlibrary/osexitinlibrary.go | 13 +++++ 4 files changed, 83 insertions(+) create mode 100644 pkg/linters/osexitinlibrary/osexitinlibrary.go create mode 100644 pkg/linters/osexitinlibrary/osexitinlibrary_test.go create mode 100644 pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go diff --git a/cmd/linters/main.go b/cmd/linters/main.go index d27850f6053..c9397e6cf5e 100644 --- a/cmd/linters/main.go +++ b/cmd/linters/main.go @@ -18,11 +18,13 @@ import ( "github.com/github/gh-aw/pkg/linters/excessivefuncparams" "github.com/github/gh-aw/pkg/linters/largefunc" + "github.com/github/gh-aw/pkg/linters/osexitinlibrary" ) func main() { multichecker.Main( excessivefuncparams.Analyzer, largefunc.Analyzer, + osexitinlibrary.Analyzer, ) } diff --git a/pkg/linters/osexitinlibrary/osexitinlibrary.go b/pkg/linters/osexitinlibrary/osexitinlibrary.go new file mode 100644 index 00000000000..a7d23f41ba2 --- /dev/null +++ b/pkg/linters/osexitinlibrary/osexitinlibrary.go @@ -0,0 +1,52 @@ +// Package osexitinlibrary implements a Go analysis linter that flags +// os.Exit calls in library (pkg/) packages. +package osexitinlibrary + +import ( + "go/ast" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// Analyzer is the os-exit-in-library analysis pass. +var Analyzer = &analysis.Analyzer{ + Name: "osexitinlibrary", + Doc: "reports os.Exit calls inside library packages where they bypass deferred cleanup and prevent testing", + URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/osexitinlibrary", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +func run(pass *analysis.Pass) (any, error) { + pkgPath := pass.Pkg.Path() + // Skip packages under cmd/ entry-points — they are allowed to call os.Exit. + if strings.HasSuffix(pkgPath, "/main") || strings.Contains(pkgPath, "/cmd/") { + return nil, nil + } + + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + + insp.Preorder(nodeFilter, func(n ast.Node) { + call := n.(*ast.CallExpr) + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return + } + ident, ok := sel.X.(*ast.Ident) + if !ok { + return + } + if ident.Name == "os" && sel.Sel.Name == "Exit" { + pass.Reportf(call.Pos(), "os.Exit called in library package %s; move process termination to a cmd/ entry-point", pkgPath) + } + }) + + return nil, nil +} diff --git a/pkg/linters/osexitinlibrary/osexitinlibrary_test.go b/pkg/linters/osexitinlibrary/osexitinlibrary_test.go new file mode 100644 index 00000000000..a0cd11e3cc9 --- /dev/null +++ b/pkg/linters/osexitinlibrary/osexitinlibrary_test.go @@ -0,0 +1,16 @@ +//go:build !integration + +package osexitinlibrary_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + + "github.com/github/gh-aw/pkg/linters/osexitinlibrary" +) + +func TestOsExitInLibrary(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, osexitinlibrary.Analyzer, "osexitinlibrary") +} diff --git a/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go new file mode 100644 index 00000000000..0032ad5f146 --- /dev/null +++ b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go @@ -0,0 +1,13 @@ +package osexitinlibrary + +import "os" + +// bad: os.Exit in a pkg/ package. +func stopProcess() { + os.Exit(1) // want `os.Exit called in library package` +} + +// ok: helper that does NOT call os.Exit. +func doWork() error { + return nil +} From 235ceb0e3e12a530f43fc5c63607b9125f7b5289 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 21:45:37 +0000 Subject: [PATCH 2/2] fix(linters): ignore test packages/files in osexitinlibrary Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/linters/osexitinlibrary/osexitinlibrary.go | 4 ++++ .../src/osexitinlibrary/osexitinlibrary_test.go | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go diff --git a/pkg/linters/osexitinlibrary/osexitinlibrary.go b/pkg/linters/osexitinlibrary/osexitinlibrary.go index a7d23f41ba2..fced873fc9b 100644 --- a/pkg/linters/osexitinlibrary/osexitinlibrary.go +++ b/pkg/linters/osexitinlibrary/osexitinlibrary.go @@ -4,6 +4,7 @@ package osexitinlibrary import ( "go/ast" + "path/filepath" "strings" "golang.org/x/tools/go/analysis" @@ -35,6 +36,9 @@ func run(pass *analysis.Pass) (any, error) { insp.Preorder(nodeFilter, func(n ast.Node) { call := n.(*ast.CallExpr) + if strings.HasSuffix(pkgPath, ".test") || strings.HasSuffix(filepath.Base(pass.Fset.Position(call.Pos()).Filename), "_test.go") { + return + } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return diff --git a/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go new file mode 100644 index 00000000000..da948ad9abd --- /dev/null +++ b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go @@ -0,0 +1,10 @@ +package osexitinlibrary + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +}