Skip to content

Handle required-workflow daily AIC guardrail lookups and expose structural failure state - #49961

Open
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-daily-ai-credits-guardrail
Open

Handle required-workflow daily AIC guardrail lookups and expose structural failure state#49961
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-daily-ai-credits-guardrail

Conversation

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Organization-level required workflows can execute in a repository without being members of that repository’s workflow collection. In that case, the daily AI Credits guardrail resolved the current run successfully, then 404ed when querying run history by workflow_id, permanently failing open and making that structural failure indistinguishable from a transient skip.

  • Run-history lookup fallback

    • Keep the existing workflow_id query as the primary path.
    • On 404, fall back to repository-level run listing and filter by workflow name, so required-workflow injection still produces a usable 24h run window.
    • Preserve pagination behavior in fallback mode, including pages that contain zero matches but are not yet exhausted.
  • Structural vs transient failure signaling

    • Add daily_ai_credits_guardrail_status output to distinguish:
      • under_budget
      • exceeded
      • transient_error
      • structural_error
      • skipped / disabled / not_run
    • Keep fail-open behavior for unexpected runtime/API faults, but stop collapsing permanent 404 conditions into the same state as transient errors.
  • Focused guardrail regression coverage

    • Cover the required-workflow shape where:
      • getWorkflowRun() succeeds for the current run
      • listWorkflowRuns({ workflow_id }) returns 404
      • repository-level run history contains matching runs by workflow name
    • Cover the case where both lookup paths 404, asserting a structural error state rather than a generic transient skip.
try {
  return await githubClient.rest.actions.listWorkflowRuns({
    owner,
    repo,
    workflow_id: workflowId,
    status: "completed",
    per_page: perPage,
    page,
  });
} catch (error) {
  if (hasHttpStatus(error, 404) && workflowName) {
    const response = await githubClient.rest.actions.listWorkflowRunsForRepo({
      owner,
      repo,
      status: "completed",
      per_page: perPage,
      page,
    });
    response.data.workflow_runs = (response.data.workflow_runs || []).filter(
      run => run.name === workflowName
    );
    return response;
  }
  throw error;
}

run: https://github.com/github/gh-aw/actions/runs/30815680828

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 9.88 AIC · ⌖ 4.97 AIC · ⊞ 8.3K ·
Comment /souschef to run again


Run: https://github.com/github/gh-aw/actions/runs/30820387900

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.2 AIC · ⌖ 5.18 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix daily AI credits guardrail failing open for org-level workflows Handle required-workflow daily AIC guardrail lookups and expose structural failure state Aug 3, 2026
Copilot AI requested a review from pelikhan August 3, 2026 12:22
@pelikhan
pelikhan marked this pull request as ready for review August 3, 2026 12:30
Copilot AI review requested due to automatic review settings August 3, 2026 12:30
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories.

Copilot AI 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.

Pull request overview

Adds required-workflow fallback handling and distinct guardrail execution statuses.

Changes:

  • Falls back to repository run history on workflow-specific 404s.
  • Distinguishes structural, transient, skipped, disabled, and budget states.
  • Adds focused regression tests for fallback pagination and structural errors.
Show a summary per file
File Description
actions/setup/js/check_daily_aic_workflow_guardrail.cjs Implements fallback lookup and status reporting.
actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs Tests fallback and status behavior.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

core.setOutput("daily_ai_credits_exceeded", "false");
core.setOutput("daily_ai_credits_total_effective_tokens", "");
core.setOutput("daily_ai_credits_threshold", "");
core.setOutput("daily_ai_credits_guardrail_status", "not_run");

@github-actions github-actions Bot 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.

The implementation is correct and well-structured.

  • hasHttpStatus safely handles both error.status and error.response.status error shapes.
  • sourceRunCount tracks the unfiltered API page size in fallback mode, so pagination correctly continues through pages where name-filtering yields zero matches — exactly the required behavior.
  • daily_ai_credits_guardrail_status consistently covers all exit paths: not_run, disabled, skipped, under_budget, exceeded, transient_error, structural_error.
  • isStructuralGuardrailError limits the structural_error classification to 404s only, keeping other faults as transient_error.
  • The two new tests cover both the required-workflow fallback path and the double-404 structural error path with appropriate assertions.

No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 25.6 AIC · ⌖ 10.3 AIC · ⊞ 5.4K

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 84/100 — Excellent

Analyzed 37 test(s): 35 design, 2 implementation, 0 violation(s).

📊 Metrics (37 tests)
Metric Value
Analyzed 37 (Go: 0, JS: 37)
✅ Design 35 (95%)
⚠️ Implementation 2 (5%)
Edge/error coverage 20 (54%)
Duplicate clusters 0
Inflation N/A (diff-numstat unavailable)
🚨 Violations 0
Test File Classification Issues
skips workflow_call, repository_dispatch, workflow_dispatch .test.cjs design_test None
skips for label command triggers in aw_context .test.cjs design_test None
skips for slash command triggers in aw_context .test.cjs design_test None
skips for malformed aw_context .test.cjs design_test None — error edge
skips for slash/label command flag toggles .test.cjs design_test None
matches usage artifacts only .test.cjs design_test None
sums AI Credits across multiple JSONL files .test.cjs design_test None
computes aggregate AIC statistics .test.cjs design_test None
caps inspection on low rate-limit headroom .test.cjs design_test None
formats structured log messages .test.cjs design_test None
renders summary — zero counts (no prior runs) .test.cjs design_test None
renders summary — with stats and prior runs .test.cjs design_test None
main() does not fail step on API throws .test.cjs design_test Minor: log-text regex
main() logs rate limit consumption delta .test.cjs design_test None
main() stops paginating on stale run .test.cjs design_test None
main() does not mark step failed when guardrail exceeded .test.cjs design_test None
main() stops loop on in-loop rate-limit exhaustion .test.cjs design_test None
falls back to repo run history on required-workflow 404 .test.cjs design_test None — core PR feature
marks permanent 404 as structural_error .test.cjs implementation_test Minor: log-text regex
loadAICUsageCache — empty/nonexistent/valid/malformed/dedup .test.cjs design_test None
loadAICUsageCache — TTL (recent / stale / no timestamp) .test.cjs design_test None
appendZeroAICEntriesToCache — null/empty/create/append/dirs .test.cjs design_test None
appendZeroAICEntriesToCache — round-trip / timestamp / error .test.cjs design_test None
marks transient API errors as transient_error status .test.cjs implementation_test Minor: log-text regex
⚠️ Flagged Tests (2 — low severity)

main() does not fail the step when GitHub API calls throw — asserts coreWarnings.some(w => /unexpected error.*skipped/i.test(w)). This couples the test to exact warning message phrasing. The output contract (daily_ai_credits_exceeded: false, guardrail_status: transient_error) is solid; only the log-text regex is fragile.

marks permanent 404 guardrail failures as structural errors — same /unexpected error.*skipped/i pattern. Same low-severity note.

Suggested fix: Drop the log-text regex assertions; the daily_ai_credits_guardrail_status output assertion is already sufficient to verify the behavioral contract.

Verdict

Passed. 5% implementation tests (threshold: 30%). No violations. Strong behavioral coverage for the new required-workflow 404 fallback path and structural_error vs transient_error status distinction. Two tests have minor log-text regex fragility but do not affect the correctness of the behavioral contract.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 55.2 AIC · ⌖ 7.54 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 84/100. 5% implementation tests (threshold: 30%). No violations.

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on a few correctness and coverage gaps.

📋 Key Themes & Highlights

Key Themes

  • isStructuralGuardrailError scope: Only HTTP 404 is treated as structural; other permanent 4xx errors (403, 410) will be misclassified as transient. The intent should be documented or the function renamed.
  • Fallback pagination efficiency: In repo_workflow_name_fallback mode, sourceRunCount tracks unfiltered page size, which can keep pagination alive long after all matching runs have been scanned past the cutoff window.
  • Missing test coverage: The sourceRunCount === 0 early-exit path in fallback mode, and the no-workflowName + 404 path, are not directly tested.
  • Untyped lookupMode string: A JSDoc @typedef union would eliminate the typo risk.

Positive Highlights

  • ✅ Excellent use of two-level error detection (hasHttpStatus) covering both direct and nested Octokit error shapes.
  • ✅ Fallback is transparent: the caller learns the mode transition via the returned lookupMode and the log message is clear.
  • ✅ Seven-state daily_ai_credits_guardrail_status output is a great observability improvement — callers can now route on structural vs transient failure without re-parsing warning text.
  • ✅ The new httpError test helper is clean and reusable across the test file.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 50 AIC · ⌖ 12.1 AIC · ⊞ 7.1K
Comment /matt to run again

* @returns {boolean}
*/
function isStructuralGuardrailError(error) {
return hasHttpStatus(error, 404);

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.

[/diagnosing-bugs] isStructuralGuardrailError currently maps exclusively to HTTP 404. Other permanent failures (e.g. 403 on missing scope) will be misclassified as transient_error, causing the guardrail to keep failing open permanently.

💡 Suggestion

Either rename to is404Error for precision, or document explicitly in the JSDoc why only 404 is structural and all other 4xx errors are intentionally treated as transient.

/**
 * Returns true when the error is a permanent HTTP 404 — the only status
 * treated as structural. Other 4xx errors are treated as transient because
 * they may resolve on retry or credential refresh.
 */
function isStructuralGuardrailError(error) {
  return hasHttpStatus(error, 404);
}

@copilot please address this.

*/
async function listCompletedWorkflowRunsPage(githubClient, params) {
const { owner, repo, workflowId, workflowName, page, perPage, lookupMode } = params;
if (lookupMode === "repo_workflow_name_fallback") {

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.

[/codebase-design] The lookupMode parameter is a free-form string ("workflow_id" / "repo_workflow_name_fallback"). Using an untyped string literal makes callers fragile to typos and means the exhaustiveness of the switch isn't enforced.

💡 Suggestion

Define a JSDoc @typedef for the mode union so tooling catches invalid values at call sites:

/** `@typedef` {'workflow_id' | 'repo_workflow_name_fallback'} WorkflowRunLookupMode */

Then annotate the param and internal variable accordingly.

@copilot please address this.

...response.data,
workflow_runs: filteredRuns,
},
},

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.

[/diagnosing-bugs] When in fallback mode, sourceRunCount is set to allRuns.length (total unfiltered runs), but the outer loop uses sourceRunCount < 100 to decide whether to stop paginating. If the repo has exactly 100 runs on a page but only 0 match the workflow name, the loop will correctly continue — but if the last page of unfiltered results is full (100 items) yet all filtered results are exhausted, pagination never terminates early.

💡 Example scenario
  • Repo has 500 runs, none named workflowName on pages 3-5.
  • Pages 1-2 each return 100 unfiltered runs, some matching.
  • Pages 3-5 return 100 unfiltered runs, 0 matching — loop keeps going until MAX_WORKFLOW_RUN_PAGES.

This wastes rate-limit budget. Consider stopping when the oldest run on a page predates the cutoff regardless of sourceRunCount.

@copilot please address this.

page,
perPage,
lookupMode: "repo_workflow_name_fallback",
});

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.

[/diagnosing-bugs] When workflowName is an empty string or undefined and a 404 is thrown, the condition !workflowName is truthy, so the error is re-thrown directly. That means the outer catch will classify it as structural_error (since it's a 404), even though the root cause is a missing workflow name — a configuration issue, not a required-workflow shape.

💡 Suggestion

Distinguish the two 404 cases explicitly:

if (!hasHttpStatus(error, 404)) {
  throw error;
}
if (!workflowName) {
  // 404 with no name to fall back on — rethrow as a structural error
  throw error;
}
// ... proceed with fallback

Add a comment explaining why a missing workflowName means we cannot fall back, so future maintainers don't collapse these two arms.

@copilot please address this.


const mockGithub = {
rest: {
rateLimit: {

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.

[/tdd] The fallback test asserts listWorkflowRunsForRepoCalls === 2, which implies page 1 returns 100 unfiltered runs (all unrelated) and page 2 returns the matching run. But the test never asserts that reachedCutoff or the cutoff-based loop-termination condition fired — meaning the loop only stops because page 2 returns fewer than 100 runs. The empty-page fast-exit (sourceRunCount === 0) path is untested in the fallback scenario.

💡 Suggestion

Add a test where the fallback runs across a page that has zero unfiltered runs (workflow_runs: []) and assert that the loop terminates without querying a further page. This exercises the sourceRunCount === 0 early-exit branch under fallback mode.

@copilot please address this.

global.github = mockGithub;
global.context = { repo: { owner: "test-owner", repo: "test-repo" }, runId: 42 };

process.env.GH_AW_MAX_DAILY_AI_CREDITS = "10";

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.

[/tdd] The structural-error test sets GH_AW_WORKFLOW_NAME so both listWorkflowRuns and listWorkflowRunsForRepo get called. However there is no test covering the case where GH_AW_WORKFLOW_NAME is absent (empty) and listWorkflowRuns returns 404 — which would take the !workflowName branch in listCompletedWorkflowRunsPage and skip the fallback entirely. That branch currently ends in a rethrow, so the outer catch should still produce structural_error, but it is not explicitly verified.

💡 Suggestion

Add a test: no GH_AW_WORKFLOW_NAME, listWorkflowRuns throws 404, assert daily_ai_credits_guardrail_status === 'structural_error' and that listWorkflowRunsForRepo is never called.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot sous-chef triage:

Failed checks:

Please refresh the branch if needed and then run the pr-finisher skill.

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 9.88 AIC · ⌖ 4.97 AIC · ⊞ 8.3K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔍 PR Triage

Category: bug · Risk: medium · Total score: 65/100

Impact Urgency Quality
32/50 18/30 15/20

Fixes a structural failure-detection gap in the daily AI Credits guardrail for required workflows (404 fallback + explicit status states). Includes regression tests. CI pending (1 in-progress check).

Recommended action: fast_track — addresses a guardrail correctness bug with good test coverage.

Generated by 🔧 PR Triage Agent · auto · 55.7 AIC · ⌖ 4.24 AIC · ⊞ 8K ·

…tputs

- Add daily_ai_credits_guardrail_status to compiler_activation_outputs.go
  so downstream jobs can observe structural vs transient failure state
- Update 13 golden test snapshots with the new output in alphabetical position
- Add test assertion in daily_aic_workflow_guardrail_test.go
- Recompile 268 workflow lock files to include the new activation output

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot sous-chef triage:

Failed checks:...

The daily_ai_credits_guardrail_status output is now wired through the compiler in compiler_activation_outputs.go alongside the three existing guardrail outputs (commit Wire daily_ai_credits_guardrail_status through compiler activation outputs). All 268 affected lock files were recompiled, 13 golden test snapshots updated, and an assertion added to TestDailyAICWorkflowGuardrailInCompiledWorkflow. All tests pass and no workflow drift remains.

Copilot AI requested a review from gh-aw-bot August 3, 2026 13:32
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot sous-chef triage:

Please refresh the branch if needed and then run the skill.

Generated by 👨🍳 PR Sous Chef · run: https://github.com/github/gh-aw/actions/runs/30820387900

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 13.2 AIC · ⌖ 5.18 AIC · ⊞ 8.3K ·
Comment /souschef to run again

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Daily AI Credits guardrail permanently fails open under org-level required workflows

4 participants