Skip to content

feat(lint): custom action framework linter#1789

Draft
osmman wants to merge 4 commits into
mainfrom
tturek/custom-action-linter
Draft

feat(lint): custom action framework linter#1789
osmman wants to merge 4 commits into
mainfrom
tturek/custom-action-linter

Conversation

@osmman
Copy link
Copy Markdown
Collaborator

@osmman osmman commented May 11, 2026

Summary

  • Adds custom golangci-lint v2 module plugin with two analyzers for the action framework:
    • persiststatuserr: flags PersistStatus() calls where the error return is discarded
    • specmutation: flags instance.Spec mutations inside action types (only Status is persisted)
  • Linter is a separate Go module (pkg/analyzer/) to keep operator deps clean
  • CI workflow updated to build and run the custom binary via golangci-lint-action with install-mode: none for inline PR annotations
  • Fixes all 3 existing spec mutation violations in ctlog and fulcio actions

Blocked on #1779 — this PR is based on refactor/separate-persist-status-from-flow-control and should be merged after #1779 is accepted.

Test plan

  • Analyzer unit tests pass (go test ./pkg/analyzer/...)
  • Controller tests pass for modified ctlog and fulcio packages
  • Custom linter reports 0 issues after fixes
  • CI workflow tested locally with act
  • CI passes on GitHub

🤖 Generated with Claude Code

osmman and others added 4 commits May 11, 2026 14:36
Replace StatusUpdate(ctx, obj) *Result with PersistStatus(ctx, obj)
(bool, error) that only persists status without controlling reconciler
flow. Developers now explicitly choose flow control after persisting.

The old StatusUpdate always called Status().Update() and returned a
*Result, conflating persistence with flow control. When the status was
unchanged, it still wrote to the API server. The new PersistStatus skips
the API call when status is unchanged (no-op optimization) and returns a
changed flag so callers can react appropriately:
- changed=true: API write happened, watch event will fire → use Return()
- changed=false: no write, no watch event → use Requeue() as safety net

Migration applied to all ~124 call sites across 60+ action files.

Other changes:
- Add RequeueAfter(delay) for explicit polling delays
- Change Requeue() default from 5s to 100ms (safety-net, not polling)
- Polling sites now use RequeueAfter(5 * time.Second) explicitly
- Fix missing return before Error() in rekor sharding_config.go
- Fix tuf resolve_keys.go: persist status before Continue()
- Add package-level doc.go with patterns and anti-patterns
- Add PersistStatus test coverage including conflict retry
- Add godoc to all BaseAction methods and Action interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Tomas Turek <tturek@redhat.com>
Extract custom config handling into handleCustomConfig method to bring
cyclomatic complexity below the gocyclo threshold of 30.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add two analyzers as a separate Go module (pkg/analyzer/) integrated
via golangci-lint v2 module plugin:

- persiststatuserr: flags PersistStatus() calls where the error is discarded
- specmutation: flags Spec field mutations inside action types

CI workflow updated to build the custom golangci-lint binary and use it
with golangci-lint-action for inline PR annotations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ctlog: extract resolveTrillianAddress() helper to avoid mutating
  instance.Spec.Trillian.Address when defaulting empty values
- fulcio: write calculated hostname directly to
  Status.Certificate.CommonName instead of Spec.ExternalAccess.Host

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@qodo-for-securesign
Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: golangci

Failed stage: golangci-lint [❌]

Failed test name: ""

Failure summary:

The action failed during the golangci-lint step because a linter error was reported and
golangci-lint exited non-zero (##[error]issues found).
-
internal/controller/tsa/actions/generate_signer.go:401:3 triggered specmutation (from actionlint):
action type must not mutate instance.Spec; only Status is persisted by the action framework.
- The
offending code shown in the log mutates the spec: instance.Spec.Signer.File = new(v1alpha1.File).

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

257:  go: downloading github.com/lasiar/canonicalheader v1.1.2
258:  go: downloading github.com/sivchari/containedctx v1.0.3
259:  go: downloading github.com/kkHAIKE/contextcheck v1.1.6
260:  go: downloading github.com/karamaru-alpha/copyloopvar v1.2.2
261:  go: downloading github.com/bkielbasa/cyclop v1.2.3
262:  go: downloading gitlab.com/bosi/decorder v0.4.2
263:  go: downloading github.com/OpenPeeDeeP/depguard/v2 v2.2.1
264:  go: downloading github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32
265:  go: downloading github.com/Abirdcfly/dupword v0.1.7
266:  go: downloading github.com/charithe/durationcheck v0.0.11
267:  go: downloading github.com/manuelarte/embeddedstructfieldcheck v0.4.0
268:  go: downloading github.com/Djarvur/go-err113 v0.1.1
269:  go: downloading github.com/kisielk/errcheck v1.9.0
270:  go: downloading github.com/breml/errchkjson v0.4.1
271:  go: downloading github.com/Antonboom/errname v1.1.1
272:  go: downloading codeberg.org/polyfloyd/go-errorlint v1.9.0
273:  go: downloading github.com/nishanths/exhaustive v0.12.0
...

430:  Checking for go.mod: go.mod
431:  (node:12717) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
432:  (Use `node --trace-deprecation ...` to show where the warning was created)
433:  Cache not found for input keys: golangci-lint.cache-Linux-2940-93e8a17b4f356a599dc80e9a78e422abd3301a4d, golangci-lint.cache-Linux-2940-
434:  ##[endgroup]
435:  ##[group]Install
436:  Found configuration for the plugin module system : /home/runner/work/secure-sign-operator/secure-sign-operator/.custom-gcl.yml
437:  Building and installing custom golangci-lint binary...
438:  Running [/usr/local/bin/golangci-lint custom] in [/home/runner/work/secure-sign-operator/secure-sign-operator] ...
439:  Built custom golangci-lint binary in 9717ms
440:  ##[endgroup]
441:  ##[group]run golangci-lint
442:  Running [/home/runner/work/secure-sign-operator/secure-sign-operator/custom-gcl config path] in [/home/runner/work/secure-sign-operator/secure-sign-operator] ...
443:  Running [/home/runner/work/secure-sign-operator/secure-sign-operator/custom-gcl config verify] in [/home/runner/work/secure-sign-operator/secure-sign-operator] ...
444:  Running [/home/runner/work/secure-sign-operator/secure-sign-operator/custom-gcl run  --verbose --timeout=15m] in [/home/runner/work/secure-sign-operator/secure-sign-operator] ...
445:  ##[error]internal/controller/tsa/actions/generate_signer.go:401:3: specmutation: action type must not mutate instance.Spec; only Status is persisted by the action framework (actionlint)
446:  instance.Spec.Signer.File = new(v1alpha1.File)
...

461:  level=info msg="[linters_context/goanalysis] analyzers took 2m53.03208349s with top 10 stages: buildir: 2m6.094352605s, goimports: 10.468958332s, unconvert: 5.676202813s, unparam: 4.253808528s, nilness: 3.080895357s, goconst: 2.880099971s, printf: 2.559791914s, fact_purity: 1.939626118s, inspect: 1.865039596s, fact_deprecated: 1.653563191s"
462:  level=info msg="[runner/exclusion_paths] Skipped 0 issues by pattern \"third_party$\""
463:  level=info msg="[runner/exclusion_paths] Skipped 0 issues by pattern \"builtin$\""
464:  level=info msg="[runner/exclusion_paths] Skipped 0 issues by pattern \"examples$\""
465:  level=info msg="[runner/exclusion_rules] Skipped 0 issues by rules: [Path: \"third_party$\", Linters: \"gofmt, goimports\"]"
466:  level=info msg="[runner/exclusion_rules] Skipped 0 issues by rules: [Path: \"builtin$\", Linters: \"gofmt, goimports\"]"
467:  level=info msg="[runner/exclusion_rules] Skipped 0 issues by rules: [Path: \"examples$\", Linters: \"gofmt, goimports\"]"
468:  level=info msg="[runner/exclusion_rules] Skipped 0 issues by rules: [Text: \"SA1019: manager.GetEventRecorderFor is deprecated\", Linters: \"staticcheck\"]"
469:  level=info msg="[runner] Issues before processing: 17, after processing: 1"
470:  level=info msg="[runner] Processors filtering stat (in/out): nolint_filter: 16/1, path_shortener: 1/1, sort_results: 1/1, diff: 1/1, fixer: 1/1, max_same_issues: 1/1, max_from_linter: 1/1, path_prettifier: 1/1, max_per_file_from_linter: 1/1, cgo: 17/17, invalid_issue: 17/17, generated_file_filter: 17/16, source_code: 1/1, exclusion_paths: 17/17, uniq_by_line: 1/1, severity-rules: 1/1, path_absoluter: 17/17, filename_unadjuster: 17/17, path_relativity: 17/17, exclusion_rules: 16/16"
471:  level=info msg="[runner] processing took 4.199781ms with stages: nolint_filter: 3.87736ms, generated_file_filter: 203.329µs, exclusion_paths: 33.653µs, exclusion_rules: 28.894µs, source_code: 25.778µs, path_relativity: 18.494µs, uniq_by_line: 2.224µs, cgo: 1.793µs, sort_results: 1.463µs, path_absoluter: 1.453µs, max_same_issues: 1.192µs, invalid_issue: 1.092µs, path_shortener: 671ns, filename_unadjuster: 571ns, max_from_linter: 552ns, diff: 331ns, fixer: 320ns, path_prettifier: 270ns, max_per_file_from_linter: 231ns, severity-rules: 110ns"
472:  level=info msg="[runner] linters took 42.008455957s with stages: goanalysis_metalinter: 42.004196585s"
473:  level=info msg="File cache stats: 1 entries of total size 15.1KiB"
474:  level=info msg="Memory: 1030 samples, avg is 542.9MB, max is 2456.5MB"
475:  level=info msg="Execution took 1m49.190352044s"
476:  ##[error]issues found
477:  Ran golangci-lint in 109413ms
478:  ##[endgroup]
479:  Post job cleanup.
480:  (node:20370) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
481:  (Use `node --trace-deprecation ...` to show where the warning was created)
482:  [command]/usr/bin/tar --posix -cf cache.tzst --exclude cache.tzst -P -C /home/runner/work/secure-sign-operator/secure-sign-operator --files-from manifest.txt --use-compress-program zstdmt
483:  (node:20370) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
484:  Sent 1291555 of 1291555 (100.0%), 4.6 MBs/sec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant