fix: wire GH_AW_AIC and GH_AW_MAX_AI_CREDITS to Handle agent failure step#38327
Conversation
…step, add provenance logging - `Handle agent failure` step was missing `GH_AW_AIC` (credits used) and `GH_AW_MAX_AI_CREDITS` (configured limit), causing "AI Credits Budget Exceeded" issues to show no data - Wire both env vars in `notify_comment.go` agentFailureEnvVars: AIC from agent job output, max from compile-time literal (when set in frontmatter) or runtime vars expression - Add provenance logging to `resolveAICreditsFailureState()` so each value's source (audit_log / agent_stdio / env / none) is visible in the step log Fixes #38322 and #38324 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes missing AI Credits usage/limit data in the Handle agent failure step by wiring the relevant env vars through to the failure handler and adding step-log provenance to aid debugging when credit data is absent.
Changes:
- Pass
GH_AW_AICandGH_AW_MAX_AI_CREDITSinto the agent-failure handler env so failure issues can include credits used + configured limit. - Add provenance logging in
resolveAICreditsFailureState()to show which source (audit log / agent stdio / env / none) provided AI credits values. - Regenerate compiled workflow
.lock.ymlfiles to reflect the new env wiring.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/notify_comment.go | Threads AI credits usage (GH_AW_AIC) and budget limit (GH_AW_MAX_AI_CREDITS) into the agent-failure handler step env. |
| actions/setup/js/ai_credits_context.cjs | Logs provenance for resolved AI credits values during failure handling. |
| .github/workflows/*.lock.yml | Regenerated compiled workflows to include the new env vars in Handle agent failure. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
actions/setup/js/ai_credits_context.cjs:315
max-ai-creditssupports the sentinel value-1(disable enforcement), but this resolver only parses positive numbers. IfGH_AW_MAX_AI_CREDITSis "-1" (from frontmatter orvars.GH_AW_DEFAULT_MAX_AI_CREDITS),maxAICreditsbecomes empty andshouldReportAICreditsRateLimitErrorwill still conservatively flag an AI-credits budget failure; the failure handler will also log the configured max as "(none)". Consider treating "-1" as an explicit "no limit" value and forcingaiCreditsRateLimitErrorto false in that case.
- Files reviewed: 247/247 changed files
- Comments generated: 0
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #38327 does not have the implementation label (has_implementation_label=false) and adds only 13 lines in business logic directories, well below the 100-line threshold (requires_adr_by_default_volume=false). |
|
🧪 Test Quality Sentinel completed test quality analysis. No test files were added or modified in this PR. Test Quality Sentinel skipped. |
|
✅ PR Code Quality Reviewer completed the code quality review. |
There was a problem hiding this comment.
REQUEST_CHANGES — one correctness bug must be fixed before merge
The core wiring is correct: GH_AW_AIC and GH_AW_MAX_AI_CREDITS were genuinely missing from the failure handler env block, and the provenance logging adds useful diagnostic signal. Three issues need attention:
### Blocking: wrong comparator lets the `-1` (unlimited) sentinel produce broken YAML
MaxAICredits != 0 is true for -1, which is the valid spec-supported unlimited sentinel (max-ai-credits: -1). The literal branch then emits GH_AW_MAX_AI_CREDITS: "-1", which parsePositiveNumberString rejects — leaving maxAICredits empty in the failure comment and the provenance log saying source=none while the var is clearly set. Fix: > 0 instead of != 0, and drop the dead data != nil conjunct while there. See inline comment.
### Non-blocking: missing tests and unreadable ternary
- The new
GH_AW_AIC/GH_AW_MAX_AI_CREDITSenv-var wiring has no test coverage. The two-branch branching logic is the most failure-prone part and is entirely unasserted. See inline comment. - The
rawRateLimitSignalSourceternary on line 309 ofai_credits_context.cjsis 200+ characters in a function that uses cleanif/else ifblocks for every other signal. See inline comment.
🔎 Code quality review by PR Code Quality Reviewer · ⌖ 13.5 AIC
| agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_EFFECTIVE_TOKENS: ${{ needs.%s.outputs.effective_tokens || '' }}\n", mainJobName)) | ||
| agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.%s.outputs.ai_credits_rate_limit_error || 'false' }}\n", mainJobName)) | ||
| agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_AIC: ${{ needs.%s.outputs.aic }}\n", mainJobName)) | ||
| if data != nil && data.EngineConfig != nil && data.EngineConfig.MaxAICredits != 0 { |
There was a problem hiding this comment.
Wrong comparator != 0 will silently discard the -1 (unlimited) sentinel and mislead the failure handler. When max-ai-credits: -1 is set (explicitly supported by the spec to disable budget enforcement), MaxAICredits != 0 is true, so the literal branch fires and emits GH_AW_MAX_AI_CREDITS: "-1" in the YAML. On the JS side, parsePositiveNumberString("-1") returns "" (it requires value > 0), so maxAICredits resolves to empty string and the provenance log records source=none GH_AW_MAX_AI_CREDITS=-1 — a confusing contradiction. The condition should be > 0.
💡 Suggested fix
// Before
if data != nil && data.EngineConfig != nil && data.EngineConfig.MaxAICredits != 0 {
// After
if data.EngineConfig != nil && data.EngineConfig.MaxAICredits > 0 {(Note: the data != nil guard can be dropped — data is unconditionally dereferenced many lines earlier in this function, e.g. at data.SafeOutputs on line 33, so it cannot be nil here. The extra conjunct is dead code that misleads readers into believing a nil-data code path exists.)
| console.log(`[ai-credits] maxAICredits source=none GH_AW_MAX_AI_CREDITS=${process.env.GH_AW_MAX_AI_CREDITS || "(unset)"}`); | ||
| } | ||
|
|
||
| const rawRateLimitSignalSource = auditRateLimitError ? "audit_log" : stdioSignals.rateLimitError ? "agent_stdio" : process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true" ? "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)" : "none"; |
There was a problem hiding this comment.
200+ character single-line ternary is unreadable and cannot be diffed. This is immediately above two if/else if blocks that handle aiCredits and maxAICredits identically — the inconsistency makes it look like an oversight.
💡 Suggested fix
// Before (one opaque line)
const rawRateLimitSignalSource = auditRateLimitError ? "audit_log" : stdioSignals.rateLimitError ? "agent_stdio" : process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true" ? "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)" : "none";
// After — consistent with the two blocks above it
let rawRateLimitSignalSource;
if (auditRateLimitError) {
rawRateLimitSignalSource = "audit_log";
} else if (stdioSignals.rateLimitError) {
rawRateLimitSignalSource = "agent_stdio";
} else if (process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true") {
rawRateLimitSignalSource = "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)";
} else {
rawRateLimitSignalSource = "none";
}| agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.%s.outputs.ai_credits_rate_limit_error || 'false' }}\n", mainJobName)) | ||
| agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_AIC: ${{ needs.%s.outputs.aic }}\n", mainJobName)) | ||
| if data != nil && data.EngineConfig != nil && data.EngineConfig.MaxAICredits != 0 { | ||
| agentFailureEnvVars = append(agentFailureEnvVars, fmt.Sprintf(" GH_AW_MAX_AI_CREDITS: %q\n", strconv.FormatInt(data.EngineConfig.MaxAICredits, 10))) |
There was a problem hiding this comment.
No test asserts GH_AW_AIC or GH_AW_MAX_AI_CREDITS appear in the conclusion job env block. The two-branch logic (MaxAICredits > 0 literal vs. expression fallback) is completely uncovered. A regression that drops either line, misspells the key, or inverts the branch condition would pass CI undetected.
💡 What to test
Two new test cases in notify_comment_test.go:
-
Literal path — set
EngineConfig.MaxAICredits = 500, callbuildConclusionJob, assert the output containsGH_AW_MAX_AI_CREDITS: "500"andGH_AW_AIC: ${{ needs.agent.outputs.aic }}. -
Expression path — leave
EngineConfig.MaxAICredits = 0(orEngineConfig = nil), assert the output containsGH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS. -
Unlimited sentinel (once the
!= 0→> 0fix is applied) — setMaxAICredits = -1, assert it falls through to the expression path rather than emitting"-1".
Handle agent failurereceivedGH_AW_AI_CREDITS_RATE_LIMIT_ERROR=truebut never the actual credit values, so failure issues showed "AI Credits Budget Exceeded" with no usage or limit data. The noop step already hadGH_AW_AIC; the failure step did not.Root cause
resolveAICreditsFailureState()falls back toprocess.env.GH_AW_AIC/process.env.GH_AW_MAX_AI_CREDITS, but neither was in the step'senv:block.shouldReportAICreditsRateLimitError(true, "", "")conservatively returnstruewhen data is absent — correct behavior, wrong inputs.Changes
pkg/workflow/notify_comment.go— adds two env vars toagentFailureEnvVars:GH_AW_AIC: ${{ needs.agent.outputs.aic }}— credits consumedGH_AW_MAX_AI_CREDITS— compile-time literal whenmax-ai-creditsis in frontmatter;${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}otherwise (mirrors the firewall config logic)actions/setup/js/ai_credits_context.cjs— adds provenance logging toresolveAICreditsFailureState(), reporting the source of each resolved value (audit_log/agent_stdio/env(GH_AW_AIC)/none) to the step log245 lock files recompiled