Skip to content

feat: add stale label automation for closed bugs#8

Merged
adalton merged 3 commits into
flightctl:mainfrom
ItzikEzra-rh:feat/stale-label
Jun 24, 2026
Merged

feat: add stale label automation for closed bugs#8
adalton merged 3 commits into
flightctl:mainfrom
ItzikEzra-rh:feat/stale-label

Conversation

@ItzikEzra-rh

@ItzikEzra-rh ItzikEzra-rh commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds periodic stale scan alongside the existing triage scan
  • Finds closed bugs with autofix label but no progression label (merged/review/rejected/blocked/ci-failing/max-retries) and relabels them as stale
  • All labels are config-driven via progression_labels — no hardcoded coupling to Bug Buddy's label scheme
  • Includes stale label in the JQL exclusion list so partial failures don't cause infinite re-processing
  • Disabled by default (stale_label: "" in shared-values); enabled for OSAC with jira-triage-stale

Changes

  • config/config.go — add StaleLabel and ProgressionLabels to TriageConfig
  • scanner/scanner.go — add scanStale() and buildStaleJQL() methods
  • scanner/scanner_test.go — add TestBuildStaleJQL with 3 test cases (first test file for scanner package)
  • deploy/osac/values.yaml — set stale label + progression labels for OSAC
  • deploy/shared-values.yaml — add disabled defaults for new fields

Test plan

  • go build ./... passes
  • go test ./scanner/ ./config/ — all pass (3 new scanner tests)
  • gofmt clean
  • After deploy with dry_run: true: verify stale scan logs correct JQL and "DRY RUN: would mark stale" for expected tickets
  • After deploy with dry_run: false: verify closed autofix tickets get relabeled to stale

🤖 Generated with Claude Code

Affected packages: config/, scanner/ (plus Helm chart/deployment values under chart/triage-bot and deploy/)

Control plane impact: yes — the scanner polling loop now runs an additional “stale-issue” pass alongside the existing closed-bug triage scan, using Jira label search + mutations to relabel closed autofix issues as stale. No changes to webhooks, comment state machine, or the AI invocation/executor path.

Configuration: yes — adds triage.stale_label and triage.progression_labels to config/env bindings; stale detection is driven by progression_labels (no hardcoded coupling), and stale label is included in the JQL exclusion set to avoid repeated reprocessing after partial failures.

Helm/deployment: yes — updates shared defaults to disable stale scanning (stale_label: "", empty progression_labels) and enables it for OSAC via deploy/osac/values.yaml using stale_label: "jira-triage-stale" and progression label list.

Testing: yes — adds/updates scanner tests for buildStaleJQL and scanStale, including guard clauses (skip when config is empty), exact JQL expectations, dry-run behavior, happy path label updates, and partial-failure handling (continue on AddLabel errors).

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds stale Jira issue detection to the triage bot. New stale-label configuration is wired through Helm, env bindings, and scanner config. The scanner now runs a stale scan each cycle, builds stale JQL from progression labels, and relabels matching Jira issues with test coverage.

Changes

Stale Issue Scan Feature

Layer / File(s) Summary
Config fields and deploy values
config/config.go, chart/triage-bot/values.yaml, deploy/shared-values.yaml, deploy/osac/values.yaml
TriageConfig gains StaleLabel and ProgressionLabels; env bindings are added; Helm and deploy values define stale-label defaults and progression labels.
Scanner interface and run-loop wiring
scanner/scanner.go
ScannerJiraClient replaces the concrete Jira client type in Scanner and NewScanner, and scanStale(ctx) is invoked after scan(ctx) on startup and each ticker tick.
scanStale and JQL construction
scanner/scanner.go
scanStale validates stale-scan config, paginates Jira search results, and adds/removes labels for stale issues; buildStaleJQL builds the excluded-label query using progression labels and the stale label.
JQL and scanStale tests
scanner/scanner_test.go
mockJiraClient supports search and label mutation assertions; tests cover stale JQL output, skip paths, relabeling, dry-run behavior, and partial failure handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

config, scanner, helm/deploy

Suggested reviewers

  • adalton

Poem

A ticket went quiet, a label went stale,
The bot now can spot it and follow the trail.
It searches, relabels, and honors dry run,
A small workflow shift, neatly done.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Ai-Attribution ❌ Error FAIL: PR says generated with Claude Code, and the HEAD commit uses Co-Authored-By: Claude Opus 4.6, which this check forbids. Replace the AI co-author footer with an accepted trailer, e.g. Generated-by: Claude Code or Made-with: Cursor.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (11 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding stale-label automation for closed bugs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets found; changes add label/config strings only, and existing secret refs remain references, not credentials.
No-Weak-Crypto ✅ Passed PASS: Changed files only add config/JQL/label handling; no weak ciphers, custom crypto, or secret comparisons found.
No-Injection-Vectors ✅ Passed PASS: no eval/shell/pickle/yaml/os.system sinks were added; new JQL values are quoted and project keys are validated.
Container-Privileges ✅ Passed PASS: The PR only adds triage label config; manifests keep runAsNonRoot=true and allowPrivilegeEscalation=false, with no privileged/host* or SYS_ADMIN settings.
No-Sensitive-Data-In-Logs ✅ Passed PASS: New logs only emit issue keys, labels, and JQL; no passwords, tokens, PII, hostnames, or customer data are logged.
Resource-Leaks ✅ Passed No new file/HTTP/db handles are opened; the added goroutines are context-cancellable or WaitGroup-bound, and the ticker is deferred-stopped.
Unchecked-Errors ✅ Passed No unchecked error returns found; all new Jira/config errors are handled by return, panic, or logged-and-continued logic.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@adalton adalton left a comment

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.

Review: feature makes sense in triage-bot (label hygiene is a triage concern, not an autofix concern). One bug to fix before merge, plus design feedback.

Comment thread scanner/scanner.go Outdated
autofixLabel := s.cfg.Triage.AutoFixLabel
staleLabel := s.cfg.Triage.StaleLabel

allExcluded := append(s.cfg.Triage.ProgressionLabels, staleLabel)

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.

Bug: append may mutate the config slice.

append(s.cfg.Triage.ProgressionLabels, staleLabel) will modify the underlying array of ProgressionLabels if it has spare capacity. Since buildStaleJQL() is called on every tick, the second call could see a corrupted config.

Safe alternative:

allExcluded := make([]string, len(s.cfg.Triage.ProgressionLabels)+1)
copy(allExcluded, s.cfg.Triage.ProgressionLabels)
allExcluded[len(allExcluded)-1] = staleLabel

Or with Go 1.21+: allExcluded := slices.Concat(s.cfg.Triage.ProgressionLabels, []string{staleLabel})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — using slices.Concat(s.cfg.Triage.ProgressionLabels, []string{staleLabel}) which always allocates a new slice. Go 1.26 so slices is available.

Comment thread scanner/scanner.go
wg.Wait()
}

func (s *Scanner) scanStale(ctx context.Context) {

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.

Design: scanStale bypasses the InFlight semaphore.

The main scan() method respects ai.max_concurrent via the semaphore and worker pool. scanStale() issues Jira API calls (search + label add/remove) sequentially in the scanner goroutine with no concurrency bounding.

This is probably fine since these are lightweight label mutations (not AI invocations), but it blocks the next scan() tick if there are many stale issues. Worth a short comment explaining why the semaphore is intentionally not used here, so a future reader doesn't "fix" it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment above the for loop explaining this is intentional — label mutations are lightweight API calls, not AI invocations.

Comment thread scanner/scanner.go
continue
}

if err := s.jiraClient.AddLabel(ctx, issue.Key, staleLabel); err != nil {

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.

Design: scanner uses concrete *jira.Client, making scanStale untestable with mocks.

The existing scan() delegates Jira interactions to IssueProcessor (which has its own JiraClient interface, enabling mocks). scanStale() calls s.jiraClient.AddLabel / RemoveLabel / SearchTickets directly on the concrete client.

This means the stale scan path can't be unit-tested without a real HTTP server. Consider extracting a StaleLabelClient interface (or extending IssueProcessor) so the interesting logic — partial failure handling, dry-run, pagination — can be tested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted ScannerJiraClient interface with SearchTickets, AddLabel, RemoveLabel. Scanner field and NewScanner param now use the interface. Tests use a mockJiraClient that implements it — 7 test cases covering guard clauses, happy path, dry-run, and partial failure.

Comment thread deploy/osac/values.yaml Outdated
- "jira-autofix-ci-failing"
- "jira-autofix-rejected"
- "jira-autofix-blocked"
- "jira-autofix-max-retries"

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.

Question: does jira-autofix-max-retries actually exist?

Bug Buddy's config model defines FailureLabels (ci-failing, rejected, blocked) and LifecycleLabels (queued, review, merged) — I don't see a max-retries label in its model or anywhere it's applied. Is this label actually set by Bug Buddy today, or is it aspirational?

If it's never applied, it's harmless in the JQL NOT IN clause but misleading in the config — it suggests the stale scan accounts for a state that doesn't exist yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — removed jira-autofix-max-retries from the OSAC config. Can add it back if Bug Buddy starts using it.

Comment thread scanner/scanner_test.go
"triage-bot/config"
)

func TestBuildStaleJQL(t *testing.T) {

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.

Test coverage: buildStaleJQL is well-tested, but scanStale has no tests.

The interesting behavior lives in scanStale: dry-run logging, partial failure (add stale succeeds but remove autofix fails), empty-result early return, pagination, and the "stale label empty → skip" guard. None of these are tested.

This is partly a consequence of the concrete *jira.Client usage (see other comment). If an interface is extracted, these paths become straightforward to cover with table-driven tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added TestScanStale with 7 cases via the new mockJiraClient: skips when stale_label empty, skips when autofix_label empty, skips when progression_labels empty, no issues found, marks stale issue (verifies both AddLabel and RemoveLabel), dry-run (no mutations), partial failure (AddLabel fails → no RemoveLabel).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scanner/scanner_test.go`:
- Around line 29-31: The skip-case tests currently only assert outputs, so they
do not prove that scanStale avoids Jira lookups when guard conditions are met.
Update mockJiraClient and the scanStale-related tests to track whether
SearchTickets is invoked, then add explicit “no Jira call” assertions in the
guard-clause cases while keeping the existing output checks. Use the
mockJiraClient.SearchTickets method and the scanStale tests around the skip
scenarios to locate the changes.

In `@scanner/scanner.go`:
- Around line 206-219: The stale-issue logging in scanner.go is misleading
because Marked issue as stale is emitted even when jiraClient.RemoveLabel fails.
Update the stale-label flow in the scanner logic so the success log only runs
after both AddLabel and RemoveLabel succeed, and either return or continue on
RemoveLabel failure in the same way as AddLabel; use the existing scanner method
and jiraClient.AddLabel/RemoveLabel calls to keep the partial-failure state from
being logged as complete.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 50405679-13eb-410b-8a1e-4f1234e17251

📥 Commits

Reviewing files that changed from the base of the PR and between 5b49c66 and 7510a14.

📒 Files selected for processing (6)
  • chart/triage-bot/values.yaml
  • config/config.go
  • deploy/osac/values.yaml
  • deploy/shared-values.yaml
  • scanner/scanner.go
  • scanner/scanner_test.go

Comment thread scanner/scanner_test.go
Comment thread scanner/scanner.go
ItzikEzra-rh and others added 3 commits June 24, 2026 16:17
Adds a periodic scan that detects closed OSAC bugs with the autofix
label but no progression label (merged/review/rejected/etc.) and
relabels them as stale. All labels are config-driven via
progression_labels to avoid hardcoded coupling to Bug Buddy's label
scheme.

Includes the stale label in the JQL exclusion list so partial
failures (add stale succeeds, remove autofix fails) don't cause
infinite re-processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix slice mutation bug: use slices.Concat instead of append on
  config-owned ProgressionLabels slice (Andy's review)
- Extract ScannerJiraClient interface so scanStale is testable with
  mocks, matching the IssueProcessor pattern used by scan()
- Add comment explaining why scanStale skips the concurrency semaphore
- Remove jira-autofix-max-retries from OSAC config (not set by Bug Buddy)
- Add stale_label and progression_labels to chart values with Helm docs
- Add TestScanStale with 7 test cases: guard clauses, happy path,
  dry-run, and partial failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add continue after RemoveLabel failure so success log only fires
  when both label operations succeed
- Track searchCalls in mock to prove guard-clause tests skip all
  Jira interactions, not just label mutations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scanner/scanner_test.go`:
- Around line 21-22: Add a focused subtest for the partial-failure relabel path
in scanStale where AddLabel succeeds but RemoveLabel returns an error, using
mockJiraClient with removeErr set and asserting the RemoveLabel attempt occurs;
place it alongside the existing Scanner test cases so the error branch is
covered in addition to the happy path. Use the existing mockJiraClient,
Scanner.scanStale, addLabelCalls, and removeCalls fields to verify the flow, and
keep the setup consistent with the current jira searchResults-based tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: b1f749ba-c009-4855-968f-ce2357a14d27

📥 Commits

Reviewing files that changed from the base of the PR and between 7510a14 and e8aa5d1.

📒 Files selected for processing (6)
  • chart/triage-bot/values.yaml
  • config/config.go
  • deploy/osac/values.yaml
  • deploy/shared-values.yaml
  • scanner/scanner.go
  • scanner/scanner_test.go

Comment thread scanner/scanner_test.go
Comment on lines +21 to +22
removeCalls []labelCall
removeErr error

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the RemoveLabel-failure branch.

removeErr is wired into mockJiraClient but no subtest sets it, so the RemoveLabel error path in scanStale (where AddLabel succeeds but RemoveLabel fails, leaving the issue with both labels) is never exercised. This is the more interesting partial-failure case for the relabel flow. As per coding guidelines, error cases must be covered alongside the happy path.

💚 Suggested subtest
	t.Run("add succeeds but remove fails", func(t *testing.T) {
		mock := &mockJiraClient{
			searchResults: &jira.JiraSearchResponse{
				Issues: []jira.JiraIssue{{Key: "OSAC-400"}},
				IsLast: true,
			},
			removeErr: fmt.Errorf("503 Service Unavailable"),
		}
		s := &Scanner{jiraClient: mock, cfg: baseCfg, logger: zap.NewNop()}

		s.scanStale(context.Background())

		if len(mock.addLabelCalls) != 1 {
			t.Errorf("expected 1 AddLabel call, got %d", len(mock.addLabelCalls))
		}
		if len(mock.removeCalls) != 1 {
			t.Errorf("expected 1 RemoveLabel attempt, got %d", len(mock.removeCalls))
		}
	})

Also applies to: 261-282

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scanner/scanner_test.go` around lines 21 - 22, Add a focused subtest for the
partial-failure relabel path in scanStale where AddLabel succeeds but
RemoveLabel returns an error, using mockJiraClient with removeErr set and
asserting the RemoveLabel attempt occurs; place it alongside the existing
Scanner test cases so the error branch is covered in addition to the happy path.
Use the existing mockJiraClient, Scanner.scanStale, addLabelCalls, and
removeCalls fields to verify the flow, and keep the setup consistent with the
current jira searchResults-based tests.

Source: Coding guidelines

@adalton adalton merged commit 19d636b into flightctl:main Jun 24, 2026
4 of 5 checks passed
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.

2 participants