Skip to content

feat: add ai-budget-exceeded PR label when cost cap is hit - #60

Merged
adalton merged 3 commits into
flightctl:mainfrom
adalton:andalton/cost-cap-hit-label
Jul 27, 2026
Merged

adalton merged 3 commits into
flightctl:mainfrom
adalton:andalton/cost-cap-hit-label

Conversation

@adalton

@adalton adalton commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • When the per-ticket AI cost cap is exceeded, applies an ai-budget-exceeded GitHub PR label to all open PRs for the ticket, making the bot's state visible where reviewers are actually looking
  • The label reflects live state: re-applied if removed while the condition holds, cleared when the bot runs again (e.g., after the cap is raised)
  • Participates in PR validation label mutual exclusivity (replaces ai-validation-failed / ai-nonzero-exit when set, and vice versa)
  • Defaults to "ai-budget-exceeded" when not configured; set cost_cap_exceeded: "" to disable
  • Stops setting the Jira blocked label for cost-cap-exceeded (still set for general pipeline failures)

Motivation: PR osac-project/fulfillment-service#917 — a reviewer asked the bot to fix a failing test, but the bot silently stopped because the $20 per-ticket cost cap was exhausted. The only signal was a Jira label the reviewer never saw.

Test plan

  • go test -race ./models/...CostCapLabel(), All(), YAML loading with nil/empty/set pointer
  • go test -race ./projectresolver/... — default applied when nil, custom value passthrough, explicit empty disables
  • go test -race ./executor/...applyCostCapPRLabel (apply, remove, disabled no-op, multi-repo, error swallowing), mutual exclusivity with all 3 labels
  • make lint — clean (only pre-existing gosec warnings)

Assisted-by: Claude noreply@anthropic.com

Packages affected

  • executor/: Implements per-ticket AI cost-cap GitHub PR labeling (ai-budget-exceeded by default, configurable) across all configured repositories. Applies/removes/reattaches the label on each bot run while the condition remains active, enforces mutual exclusivity with ai-validation-failed and ai-nonzero-exit, and changes cost-cap violations to no longer set the Jira blocked label. Adds/expands test coverage for multi-repo behavior, configuration disablement, label removal, lookup-error resilience (best-effort), and exclusivity behavior.
  • models/: Extends PR validation label configuration with CostCapExceeded (optional) and helpers to default to ai-budget-exceeded and allow disabling via cost_cap_exceeded: "". Updates aggregation of validation labels to include the new cost-cap label.
  • projectresolver/: Normalizes project settings so the cost-cap label defaults to ai-budget-exceeded when not explicitly configured.

Execution pipeline

  • Updates ticket feedback handling: when the per-ticket AI cost cap is exceeded, the bot applies the configured cost-cap PR label across all open PRs for the ticket (and removes it when not exceeded), and returns the cost-cap exceeded error without using the Jira blocked label for that condition.
  • Ensures label lifecycle behavior: re-apply while active, clear on subsequent bot runs when the condition is no longer met.
  • Maintains mutual exclusivity among PR validation labels (ai-budget-exceeded vs. ai-validation-failed / ai-nonzero-exit).

Configuration and documentation

  • config.example.yaml: Adds commented configuration for pr_validation_labels.cost_cap_exceeded.
  • AGENTS.md: Updates documentation for the pr_validation_labels feature, including the new cost-cap label’s defaulting, disablement behavior, mutual exclusivity, and how it should be refreshed/cleared based on live cost-cap state.

When the per-ticket AI session cost cap is exceeded, the bot now applies
an `ai-budget-exceeded` label to all open PRs for the ticket. Previously
the only signal was a generic `blocked` Jira label that PR reviewers
were unlikely to see.

The label reflects live state: re-applied each scan cycle if removed
while the condition holds, and automatically cleared when the bot
successfully runs again (e.g., after the cap is raised). It participates
in the existing PR validation label mutual exclusivity group alongside
`ai-validation-failed` and `ai-nonzero-exit`.

The label defaults to "ai-budget-exceeded" when not configured — the
only PR validation label with a non-empty default. Set
`cost_cap_exceeded: ""` in `pr_validation_labels` to disable.

The Jira `blocked` label is no longer set for cost-cap-exceeded (it is
still set for general pipeline failures).

Assisted-by: Claude Opus 4.6 (1M) <noreply@anthropic.com>
@adalton adalton self-assigned this Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds configurable cost-cap PR labels, defaults the label when omitted, integrates label application into cost-cap failures, and tests configuration, mutual exclusivity, repository handling, disabled behavior, and lookup errors.

Changes

Cost-cap PR labeling

Layer / File(s) Summary
Label configuration and resolution
models/config.go, models/config_test.go, projectresolver/..., config.example.yaml, AGENTS.md
Adds the optional CostCapExceeded label, default ai-budget-exceeded, explicit disablement, resolver normalization, configuration tests, examples, and documentation.
Cost-cap label application
executor/labels.go, executor/feedback.go, executor/pipeline.go, executor/export_test.go
Locates open PRs across configured repositories, applies or removes the cost-cap label, and replaces blocked pipeline labeling in cost-cap failure paths.
PR label behavior validation
executor/labels_test.go
Covers mutual exclusivity, enabled and disabled behavior, multi-repository handling, missing PRs, and lookup failures.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: executor, config

Suggested reviewers: itzikezra-rh

Sequence Diagram(s)

sequenceDiagram
  participant TicketPipeline
  participant CostCapLabeler
  participant GitHub
  TicketPipeline->>CostCapLabeler: handle cost-cap state
  CostCapLabeler->>GitHub: find open PRs by branch heads
  GitHub-->>CostCapLabeler: return matching PRs
  CostCapLabeler->>GitHub: apply or remove cost-cap label
Loading
🚥 Pre-merge checks | ✅ 12 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (12 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: introducing the ai-budget-exceeded PR label for per-ticket cost-cap hits.
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 or credential-like literals were added; the new string literals are non-sensitive PR label names and config examples.
No-Weak-Crypto ✅ Passed No weak crypto, custom crypto, or non-constant-time secret comparisons appear in the touched code; changes are limited to PR-label/config logic.
No-Injection-Vectors ✅ Passed Diff adds label/config logic only; no SQL/shell/eval/yaml.load/os.system/dangerouslySetInnerHTML or untrusted exec paths were introduced.
Container-Privileges ✅ Passed No changed container/K8s manifests; scans found no privileged:true, hostPID/Network/IPC, SYS_ADMIN, or allowPrivilegeEscalation:true in touched config files.
No-Sensitive-Data-In-Logs ✅ Passed New logs only emit ticket/repo/PR/cost/error context; no passwords, tokens, PII, hostnames, or customer data were added.
Resource-Leaks ✅ Passed PASS: The added cost-cap label flow only reuses existing GitHub calls; no new files, response bodies, DB handles, or goroutines are opened without cleanup.
Unchecked-Errors ✅ Passed No new blank-identifier error ignores or silent swallows appear in the PR diff; ignored errors are either commented or logged.
Ai-Attribution ✅ Passed PASS: The PR includes an acceptable Assisted-by: Claude <noreply@anthropic.com> trailer, and no AI Co-Authored-By attribution is present.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
executor/pipeline.go (1)

136-143: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a symmetric clear-on-resolved call here too.

For consistency with the fix proposed for executor/feedback.go, add p.applyCostCapPRLabel(logger, job.TicketKey, settings, false) right after the cost-cap check passes, so any pre-existing stale cost-cap label (e.g., from a recovered/retried ticket that already has a PR) is proactively cleared rather than relying on downstream label logic to run. See the consolidated comment for full rationale.

🤖 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 `@executor/pipeline.go` around lines 136 - 143, After the cost-cap check in the
pipeline flow, invoke applyCostCapPRLabel with job.TicketKey, settings, and
false so stale cost-cap labels are cleared when the ticket is within the cap;
leave the existing exceeded-path call and return behavior unchanged.
executor/feedback.go (1)

49-56: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cost-cap PR label is never proactively cleared when the condition resolves — only two files, one missing call.

Both cost-cap gates call applyCostCapPRLabel(..., true) when exceeded, but neither calls it with false once the check passes. Clearing today depends entirely on vl.All() being swept by later validation-label logic (Step 16a in pipeline.go / Step 17b in feedback.go) — logic that has early-return paths bypassing it (concretely, feedback.go's "no new comments or CI failures" return at lines 118-121). Result: a PR can keep the ai-budget-exceeded label indefinitely after the ticket is no longer over budget, contradicting the documented "removed when the bot successfully runs again" behavior.

  • executor/feedback.go#L49-L56: add p.applyCostCapPRLabel(logger, job.TicketKey, settings, false) immediately after the ticketCostCapExceeded check passes, before any early returns (e.g., the no-new-comments branch at lines 118-121).
  • executor/pipeline.go#L136-L143: add the same p.applyCostCapPRLabel(logger, job.TicketKey, settings, false) call after the check passes, for symmetry and to cover recovered/retried tickets that may already have a PR.
🤖 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 `@executor/feedback.go` around lines 49 - 56, Cost-cap labels are not
proactively removed after a ticket returns within budget. In
executor/feedback.go lines 49-56, call applyCostCapPRLabel with false
immediately after ticketCostCapExceeded passes, before any early return; make
the same change in executor/pipeline.go lines 136-143 after its cost-cap check
passes to cover both execution paths.
🤖 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 `@executor/labels_test.go`:
- Around line 853-862: Update the “PR lookup errors are swallowed” subtest to
assert that ApplyCostCapPRLabel does not invoke any label add or remove
operation when GetPRForBranchFunc returns an error. Use the test dependency’s
existing label-operation tracking or mocks, and keep the error scenario setup
unchanged.

---

Outside diff comments:
In `@executor/feedback.go`:
- Around line 49-56: Cost-cap labels are not proactively removed after a ticket
returns within budget. In executor/feedback.go lines 49-56, call
applyCostCapPRLabel with false immediately after ticketCostCapExceeded passes,
before any early return; make the same change in executor/pipeline.go lines
136-143 after its cost-cap check passes to cover both execution paths.

In `@executor/pipeline.go`:
- Around line 136-143: After the cost-cap check in the pipeline flow, invoke
applyCostCapPRLabel with job.TicketKey, settings, and false so stale cost-cap
labels are cleared when the ticket is within the cap; leave the existing
exceeded-path call and return behavior unchanged.
🪄 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: 2648ddbe-40e9-42b5-95f5-410bc0bfd54a

📥 Commits

Reviewing files that changed from the base of the PR and between 032ce05 and c0758d0.

📒 Files selected for processing (11)
  • AGENTS.md
  • config.example.yaml
  • executor/export_test.go
  • executor/feedback.go
  • executor/labels.go
  • executor/labels_test.go
  • executor/pipeline.go
  • models/config.go
  • models/config_test.go
  • projectresolver/resolver.go
  • projectresolver/resolver_test.go

Comment thread executor/labels_test.go
Add applyCostCapPRLabel(..., false) immediately after the cost cap check
passes in both pipeline.go and feedback.go. Without this, a stale
ai-budget-exceeded label could persist on PRs when the feedback path
takes an early return (e.g., no new comments) before the downstream
validation label logic runs.

Also add assertions to the "PR lookup errors are swallowed" test to
verify no label operations occur when the PR lookup fails.

Assisted-by: Claude Opus 4.6 (1M) <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 `@executor/pipeline.go`:
- Around line 141-144: The cost-cap error path still receives the generic
blocked Jira label through executeNewTicket’s deferred handleFailure. Update
handleFailure to skip applying settings.FailureLabels.Blocked specifically for
errTicketCostCapExceeded, while preserving blocked labeling for all other
failures; retain the PR label behavior in applyCostCapPRLabel and add a
regression test covering the new-ticket path.
🪄 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: 91e3c87a-86da-4751-b9ef-d853ed6c7861

📥 Commits

Reviewing files that changed from the base of the PR and between c0758d0 and efbd7c4.

📒 Files selected for processing (3)
  • executor/feedback.go
  • executor/labels_test.go
  • executor/pipeline.go

Comment thread executor/pipeline.go
@flightctl flightctl deleted a comment from coderabbitai Bot Jul 27, 2026
@adalton
adalton merged commit b61bbb5 into flightctl:main Jul 27, 2026
3 checks passed
@adalton
adalton deleted the andalton/cost-cap-hit-label branch July 27, 2026 16:13
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