diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e3aa8831..d30068edb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v7 with: - version: latest + version: v2.12.0 args: --timeout=10m build: @@ -199,7 +199,7 @@ jobs: - name: Run golangci-lint on examples uses: golangci/golangci-lint-action@v7 with: - version: latest + version: v2.12.0 args: --timeout=10m working-directory: example diff --git a/.golangci.yml b/.golangci.yml index 7fa97b089..e3e0dfd88 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -35,6 +35,7 @@ linters: - G703 # Path traversal via taint - paths come from admin config or are internal CI/CD pipeline paths, not user input - G705 # XSS via taint - raw-response/static-file steps write admin-configured content by design - G706 # Log injection via taint - step.log and publish steps are designed to log arbitrary admin-configured content + - G710 # Open redirect via taint - redirect target is the admin-configured OAuth2 provider authorization URL gocritic: enabled-tags: - diagnostic @@ -54,14 +55,9 @@ linters: linters: - staticcheck text: "SA5011" - # Third-party vendored Go code in ui/node_modules is not ours to fix - - path: ui/node_modules/ - linters: - - govet - - staticcheck - - unused - - gocritic - - gosec + paths: + # Exclude third-party vendored Go code bundled under ui/node_modules from analysis entirely + - ui/node_modules presets: - std-error-handling diff --git a/cmd/iac-codemod/add_validate_plan.go b/cmd/iac-codemod/add_validate_plan.go index f2e743660..948bd7b53 100644 --- a/cmd/iac-codemod/add_validate_plan.go +++ b/cmd/iac-codemod/add_validate_plan.go @@ -12,6 +12,7 @@ import ( "go/token" "io" "io/fs" + "os" "path/filepath" "sort" "strings" @@ -642,6 +643,52 @@ func qualifierFromProviderMethods(methods []*ast.FuncDecl) string { return "" } +// siblingUsesInterfacesImport returns true if any non-test .go file +// in dir (other than excludePath) imports +// github.com/GoCodeAlone/workflow/interfaces. Used to decide whether +// to inject an interfaces import into a file that doesn't have one +// when emitting a qualified ValidatePlan stub (review round-4 #1). +// +//nolint:unused +func siblingUsesInterfacesImport(dir, excludePath string) bool { + const wantPath = "github.com/GoCodeAlone/workflow/interfaces" + entries, err := os.ReadDir(dir) + if err != nil { + return false + } + for _, e := range entries { + if e.IsDir() { + continue + } + name := e.Name() + if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + fpath := filepath.Join(dir, name) + if fpath == excludePath { + continue + } + src, err := readFile(fpath) + if err != nil { + continue + } + fs := token.NewFileSet() + sib, err := parser.ParseFile(fs, fpath, src, parser.ImportsOnly) + if err != nil { + continue + } + for _, imp := range sib.Imports { + if imp.Path == nil { + continue + } + if strings.Trim(imp.Path.Value, `"`) == wantPath { + return true + } + } + } + return false +} + // interfacesQualifier returns the package alias `file` uses for // github.com/GoCodeAlone/workflow/interfaces. If the import is // renamed (`alias "github.com/.../interfaces"`), the alias name is diff --git a/cmd/iac-codemod/lint.go b/cmd/iac-codemod/lint.go index abb749bb0..dc66d54b3 100644 --- a/cmd/iac-codemod/lint.go +++ b/cmd/iac-codemod/lint.go @@ -1039,6 +1039,41 @@ func receiverTypeName(fn *ast.FuncDecl) string { return id.Name } +// bodyCallsSelector reports whether the function body contains a +// CallExpr whose callee is a SelectorExpr with the given X.Name and +// Sel.Name, e.g. `wfctlhelpers.Plan(...)`. +// +//nolint:unused +func bodyCallsSelector(body *ast.BlockStmt, pkgIdent, selName string) bool { + if body == nil { + return false + } + found := false + ast.Inspect(body, func(n ast.Node) bool { + if found { + return false + } + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + x, ok := sel.X.(*ast.Ident) + if !ok { + return true + } + if x.Name == pkgIdent && sel.Sel.Name == selName { + found = true + return false + } + return true + }) + return found +} + // bodyReferencesField reports whether the function body references any // SelectorExpr with the given Sel.Name, e.g. any `.ForceNew`. func bodyReferencesField(body *ast.BlockStmt, fieldName string) bool { diff --git a/cmd/iac-codemod/refactor_apply.go b/cmd/iac-codemod/refactor_apply.go index ad23d38ba..21fe5b8ac 100644 --- a/cmd/iac-codemod/refactor_apply.go +++ b/cmd/iac-codemod/refactor_apply.go @@ -22,6 +22,12 @@ func init() { modes["refactor-apply"] = runRefactorApply } +// applyCanonicalCallExpr is the canonical replacement-body expression +// emitted by refactor-apply. +// +//nolint:unused +const applyCanonicalCallExpr = "wfctlhelpers.ApplyPlan(ctx, p, plan)" + // applyClassification labels the disposition of a single Apply() // method site. The non-canonical idioms are surfaced as distinct // classes so the report can suggest the right hand-port handling. diff --git a/cmd/iac-codemod/refactor_plan.go b/cmd/iac-codemod/refactor_plan.go index c5ba42847..f90086681 100644 --- a/cmd/iac-codemod/refactor_plan.go +++ b/cmd/iac-codemod/refactor_plan.go @@ -39,6 +39,13 @@ const helperImportPath = "github.com/GoCodeAlone/workflow/iac/wfctlhelpers" // files would fail to compile". const planHelperImportPath = "github.com/GoCodeAlone/workflow/platform" +// planCanonicalCallExpr is the canonical replacement-body expression +// emitted by refactor-plan. Calls platform.ComputePlan (the real helper); +// see planHelperImportPath above for the review-correction rationale. +// +//nolint:unused +const planCanonicalCallExpr = "platform.ComputePlan(ctx, p, desired, current)" + // planClassification labels the disposition of a single Plan() method // site. Each report entry carries one classification; the rewriter // honors only `planCanonical`. @@ -1103,13 +1110,13 @@ func rewritePlanBody(fn *ast.FuncDecl, file *ast.File) { ast.NewIdent(currentName), }, } - // generates: plan, err := platform.ComputePlan(ctx, p, desired, current) + // emits an assignment: plan, err := platform.ComputePlan(ctx, p, desired, current) planAssign := &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("plan"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{call}, } - // generates: return &plan, err + // emits a return statement: return &plan, err returnStmt := &ast.ReturnStmt{ Results: []ast.Expr{ &ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("plan")}, diff --git a/cmd/wfctl/infra_bootstrap.go b/cmd/wfctl/infra_bootstrap.go index 5524b2c6d..86db8b301 100644 --- a/cmd/wfctl/infra_bootstrap.go +++ b/cmd/wfctl/infra_bootstrap.go @@ -429,7 +429,7 @@ func bootstrapSecrets(ctx context.Context, provider secrets.Provider, cfg *Secre if forceRotate[gen.Key] { deleteKey := gen.Key if delErr := provider.Delete(ctx, deleteKey); delErr != nil { - // Log and continue regardless of the error kind — both absent/unsupported + // Log and continue regardless of the error kind — both absent/unsupported // secrets and unexpected errors should not block the rotation flow. fmt.Fprintf(os.Stderr, "warn: rotate-pre-delete %s: %v (continuing)\n", deleteKey, delErr) } diff --git a/cmd/wfctl/infra_provider_dispatch_test.go b/cmd/wfctl/infra_provider_dispatch_test.go index 31e926aec..4d19989c2 100644 --- a/cmd/wfctl/infra_provider_dispatch_test.go +++ b/cmd/wfctl/infra_provider_dispatch_test.go @@ -286,6 +286,7 @@ modules: } } +//nolint:unused func specNames(specs []interfaces.ResourceSpec) []string { names := make([]string, len(specs)) for i, s := range specs {