From eaa855a466d7d8838804016d4de32a79fb1aa12f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:05:03 +0000 Subject: [PATCH 1/4] simplify: extract iterateAuditEntries helper and eliminate double audit log read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ai_credits_context.cjs: - Extract iterateAuditEntries() generic JSONL scaffolding helper (path resolution, existsSync, readFileSync, line-split loop, JSON.parse with catch, outer try/catch) — eliminates ~40 lines of near-identical boilerplate duplicated across parseMaxAICreditsFromAuditLog and parseAICreditsErrorInfoFromAuditLog - Add parseAuditLogCombined() single-pass helper that accumulates all three fields (aiCredits, maxAICredits, rateLimitError) in one file read - resolveAICreditsFailureState now calls parseAuditLogCombined() once instead of calling two separate log-parse functions (was: 2x existsSync + 2x readFileSync + 2x full line iteration on every failure-state resolution) - Use inner loop over ['log.jsonl', 'audit.jsonl'] in resolveFirewallAuditLogPath to replace two copy-pasted probe blocks (simplification D) - Remove unreachable candidateBases[0] || fallback in resolveFirewallAuditLogPath — two unconditional pushes guarantee the array is never empty (simplification C) - Document intentional asymmetry: parseAICreditsErrorInfoFromAuditLog has no content-guard fast-path because AI_CREDITS_RATE_LIMIT_TEXT_FIELDS contains ubiquitous field names; a pre-scan would almost never skip iteration check_daily_aic_workflow_guardrail.cjs: - Collapse shouldSkipDailyAICGuardrail into a single boolean expression - Remove unreachable Math.round(value || 0) dead code in formatInteger — Number.isFinite already excludes non-finite values; 0/-0 are unaffected - Move let runs = [] inside the while-loop body as const runs — was declared before the loop but unconditionally reassigned on every iteration - Use ...summaryMeta spread in logDailyGuardrail call to avoid re-listing the same two property names already in the summaryMeta object All public-API signatures and behavior preserved. No logic changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- actions/setup/js/ai_credits_context.cjs | 123 ++++++++++++------ .../js/check_daily_aic_workflow_guardrail.cjs | 18 ++- 2 files changed, 89 insertions(+), 52 deletions(-) diff --git a/actions/setup/js/ai_credits_context.cjs b/actions/setup/js/ai_credits_context.cjs index c8849c159ef..e1eff75774c 100644 --- a/actions/setup/js/ai_credits_context.cjs +++ b/actions/setup/js/ai_credits_context.cjs @@ -6,6 +6,8 @@ const path = require("path"); const MAX_AI_CREDITS_FIELDS = new Set(["max_ai_credits", "maxAiCredits"]); const AI_CREDITS_FIELDS = new Set(["ai_credits", "aiCredits"]); const AI_CREDITS_RATE_LIMIT_ERROR_FIELDS = new Set(["ai_credits_rate_limit_error", "aiCreditsRateLimitError"]); +// Note: these text fields are intentionally broad (common field names like "error", "message") because +// rate-limit signals can appear in any of them. This asymmetry vs parseMaxAICreditsFromAuditLog is deliberate. const AI_CREDITS_RATE_LIMIT_TEXT_FIELDS = new Set(["error", "message", "reason", "details", "detail", "type", "code"]); const AI_CREDITS_RATE_LIMIT_PATTERNS = [/ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i, /(?:rate[\s-]*limit|too many requests).*(?:ai[\s_-]*credits?)/i, /\bai_credits_limit_exceeded\b/i]; @@ -74,12 +76,12 @@ function resolveFirewallAuditLogPath(auditJsonlPathOverride) { candidateBases.push("/tmp/gh-aw/sandbox/firewall/logs"); for (const base of candidateBases) { - const logPath = path.join(base, "log.jsonl"); - if (fs.existsSync(logPath)) return logPath; - const auditPath = path.join(base, "audit.jsonl"); - if (fs.existsSync(auditPath)) return auditPath; + for (const filename of ["log.jsonl", "audit.jsonl"]) { + const candidate = path.join(base, filename); + if (fs.existsSync(candidate)) return candidate; + } } - return path.join(candidateBases[0] || "/tmp/gh-aw/sandbox/firewall/audit", "log.jsonl"); + return path.join(candidateBases[0], "log.jsonl"); } /** @@ -131,73 +133,110 @@ function parseAICreditsErrorInfoFromAuditEntry(entry) { } /** - * @param {string} [auditJsonlPathOverride] - * @returns {string} + * Reads a firewall audit JSONL file and calls accumulate for each parsed entry. + * Returns the accumulated result, or defaultValue on missing file or any error. + * + * @template T + * @param {string | undefined} auditJsonlPathOverride + * @param {T} defaultValue + * @param {((content: string) => boolean) | null} contentGuard - When non-null, called with raw file + * content before iteration; return false to skip parsing entirely (fast-path optimization). + * @param {(acc: T, entry: unknown) => T} accumulate + * @returns {T} */ -function parseMaxAICreditsFromAuditLog(auditJsonlPathOverride) { +function iterateAuditEntries(auditJsonlPathOverride, defaultValue, contentGuard, accumulate) { try { const auditJsonlPath = resolveFirewallAuditLogPath(auditJsonlPathOverride); - if (!fs.existsSync(auditJsonlPath)) return ""; + if (!fs.existsSync(auditJsonlPath)) return defaultValue; const content = fs.readFileSync(auditJsonlPath, "utf8"); - if (!content.trim() || !/(?:max_ai_credits|maxAiCredits)/.test(content)) return ""; - let parsedMaxAICredits = ""; + if (!content.trim()) return defaultValue; + if (contentGuard && !contentGuard(content)) return defaultValue; + let result = defaultValue; for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed[0] !== "{") continue; try { - const entry = JSON.parse(trimmed); - const value = parseMaxAICreditsFromAuditEntry(entry); - if (value) parsedMaxAICredits = value; + result = accumulate(result, JSON.parse(trimmed)); } catch { // ignore malformed lines } } - return parsedMaxAICredits; + return result; } catch { - return ""; + return defaultValue; } } +/** + * @param {string} [auditJsonlPathOverride] + * @returns {string} + */ +function parseMaxAICreditsFromAuditLog(auditJsonlPathOverride) { + return iterateAuditEntries( + auditJsonlPathOverride, + "", + content => /(?:max_ai_credits|maxAiCredits)/.test(content), + (acc, entry) => parseMaxAICreditsFromAuditEntry(entry) || acc + ); +} + /** * @param {string} [auditJsonlPathOverride] * @returns {{ aiCredits: string, rateLimitError: boolean }} */ function parseAICreditsErrorInfoFromAuditLog(auditJsonlPathOverride) { - try { - const auditJsonlPath = resolveFirewallAuditLogPath(auditJsonlPathOverride); - if (!fs.existsSync(auditJsonlPath)) return { aiCredits: "", rateLimitError: false }; - const content = fs.readFileSync(auditJsonlPath, "utf8"); - if (!content.trim()) return { aiCredits: "", rateLimitError: false }; - let parsedAICredits = ""; - let hasRateLimitError = false; - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed[0] !== "{") continue; - try { - const entry = JSON.parse(trimmed); - const parsed = parseAICreditsErrorInfoFromAuditEntry(entry); - if (parsed.aiCredits) parsedAICredits = parsed.aiCredits; - if (parsed.rateLimitError) hasRateLimitError = true; - } catch { - // ignore malformed lines - } + // No content-guard fast-path: the rate-limit signal appears in common field names + // (error, message, reason…) that are present in almost every entry, making a + // field-name pre-scan near-useless. The asymmetry vs parseMaxAICreditsFromAuditLog + // is intentional — see AI_CREDITS_RATE_LIMIT_TEXT_FIELDS comment above. + return iterateAuditEntries( + auditJsonlPathOverride, + { aiCredits: "", rateLimitError: false }, + null, + (acc, entry) => { + const parsed = parseAICreditsErrorInfoFromAuditEntry(entry); + return { + aiCredits: parsed.aiCredits || acc.aiCredits, + rateLimitError: acc.rateLimitError || parsed.rateLimitError, + }; } - return { aiCredits: parsedAICredits, rateLimitError: hasRateLimitError }; - } catch { - return { aiCredits: "", rateLimitError: false }; - } + ); +} + +/** + * Single-pass combined read of the audit log, returning all AI credits fields at once. + * Used by resolveAICreditsFailureState to avoid reading the same file twice. + * + * @param {string} [auditJsonlPathOverride] + * @returns {{ aiCredits: string, maxAICredits: string, rateLimitError: boolean }} + */ +function parseAuditLogCombined(auditJsonlPathOverride) { + return iterateAuditEntries( + auditJsonlPathOverride, + { aiCredits: "", maxAICredits: "", rateLimitError: false }, + null, + (acc, entry) => { + const errorInfo = parseAICreditsErrorInfoFromAuditEntry(entry); + const max = parseMaxAICreditsFromAuditEntry(entry); + return { + aiCredits: errorInfo.aiCredits || acc.aiCredits, + maxAICredits: max || acc.maxAICredits, + rateLimitError: acc.rateLimitError || errorInfo.rateLimitError, + }; + } + ); } /** * @returns {{ aiCredits: string, maxAICredits: string, aiCreditsRateLimitError: boolean }} */ function resolveAICreditsFailureState() { - const parsedAICreditsErrorInfo = parseAICreditsErrorInfoFromAuditLog(); + const { aiCredits: auditAICredits, maxAICredits: auditMaxAICredits, rateLimitError: auditRateLimitError } = parseAuditLogCombined(); const envAICredits = parsePositiveNumberString(process.env.GH_AW_AIC); const envMaxAICredits = parsePositiveNumberString(process.env.GH_AW_MAX_AI_CREDITS); - const aiCredits = parsedAICreditsErrorInfo.aiCredits || envAICredits || ""; - const maxAICredits = parseMaxAICreditsFromAuditLog() || envMaxAICredits || ""; - const rawAICreditsRateLimitError = parsedAICreditsErrorInfo.rateLimitError || process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true"; + const aiCredits = auditAICredits || envAICredits || ""; + const maxAICredits = auditMaxAICredits || envMaxAICredits || ""; + const rawAICreditsRateLimitError = auditRateLimitError || process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true"; const aiCreditsRateLimitError = shouldReportAICreditsRateLimitError(rawAICreditsRateLimitError, aiCredits, maxAICredits); return { aiCredits, maxAICredits, aiCreditsRateLimitError }; } diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs index 0d301ec6281..a08151c7430 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs @@ -60,10 +60,11 @@ function logDailyGuardrail(message, details) { */ function shouldSkipDailyAICGuardrail() { const eventName = process.env.GITHUB_EVENT_NAME || ""; - if (eventName === "workflow_call" || eventName === "repository_dispatch") { - return true; - } - return eventName === "workflow_dispatch" && (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== ""; + return ( + eventName === "workflow_call" || + eventName === "repository_dispatch" || + (eventName === "workflow_dispatch" && (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== "") + ); } /** @@ -156,7 +157,7 @@ async function getRunAIC(artifactClient, runId, token, owner, repo) { * @returns {string} */ function formatInteger(value) { - const safeValue = Number.isFinite(value) ? Math.round(value || 0) : 0; + const safeValue = Number.isFinite(value) ? Math.round(value) : 0; return INTEGER_FORMATTER.format(safeValue); } @@ -349,8 +350,6 @@ async function main() { const cutoffMs = Date.now() - DAILY_WORKFLOW_WINDOW_MS; /** @type {Array<{id:number, html_url:string, created_at:string, conclusion:string}>} */ const candidateRuns = []; - /** @type {Array} */ - let runs = []; let page = 1; let truncatedByRateLimit = false; while (page <= MAX_WORKFLOW_RUN_PAGES) { @@ -368,7 +367,7 @@ async function main() { per_page: 100, page, }); - runs = response.data.workflow_runs || []; + const runs = response.data.workflow_runs || []; logDailyGuardrail("Received workflow runs page", { page, runCount: runs.length, @@ -453,8 +452,7 @@ async function main() { truncatedByRateLimit, }; logDailyGuardrail("Completed AIC inspection window", { - candidateRunsCount: summaryMeta.candidateRunsCount, - inspectedRunsCount: summaryMeta.inspectedRunsCount, + ...summaryMeta, countedRunIds: countedRuns.map(run => run.id), currentAIC: totalAIC, threshold, From 997708dd272307963cbe56a710508e6a029a3d89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:00:32 +0000 Subject: [PATCH 2/4] Fix js-typecheck and lint regressions in AIC audit parsing Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/ai_credits_context.cjs | 51 +++++++++---------- .../js/check_daily_aic_workflow_guardrail.cjs | 11 ++-- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/actions/setup/js/ai_credits_context.cjs b/actions/setup/js/ai_credits_context.cjs index e1eff75774c..35336d65451 100644 --- a/actions/setup/js/ai_credits_context.cjs +++ b/actions/setup/js/ai_credits_context.cjs @@ -156,7 +156,8 @@ function iterateAuditEntries(auditJsonlPathOverride, defaultValue, contentGuard, const trimmed = line.trim(); if (!trimmed || trimmed[0] !== "{") continue; try { - result = accumulate(result, JSON.parse(trimmed)); + const nextResult = accumulate(result, JSON.parse(trimmed)); + if (nextResult !== undefined) result = nextResult; } catch { // ignore malformed lines } @@ -189,42 +190,38 @@ function parseAICreditsErrorInfoFromAuditLog(auditJsonlPathOverride) { // (error, message, reason…) that are present in almost every entry, making a // field-name pre-scan near-useless. The asymmetry vs parseMaxAICreditsFromAuditLog // is intentional — see AI_CREDITS_RATE_LIMIT_TEXT_FIELDS comment above. - return iterateAuditEntries( - auditJsonlPathOverride, - { aiCredits: "", rateLimitError: false }, - null, - (acc, entry) => { - const parsed = parseAICreditsErrorInfoFromAuditEntry(entry); - return { - aiCredits: parsed.aiCredits || acc.aiCredits, - rateLimitError: acc.rateLimitError || parsed.rateLimitError, - }; - } - ); + /** @type {{ aiCredits: string, rateLimitError: boolean }} */ + const initial = { aiCredits: "", rateLimitError: false }; + return iterateAuditEntries(auditJsonlPathOverride, initial, null, (acc, entry) => { + const parsed = parseAICreditsErrorInfoFromAuditEntry(entry); + return { + aiCredits: parsed.aiCredits || acc.aiCredits, + rateLimitError: acc.rateLimitError || parsed.rateLimitError, + }; + }); } /** * Single-pass combined read of the audit log, returning all AI credits fields at once. * Used by resolveAICreditsFailureState to avoid reading the same file twice. + * No contentGuard is applied: rate-limit signal detection must scan all entries anyway, + * so a single full pass is cheaper than two guarded passes. * * @param {string} [auditJsonlPathOverride] * @returns {{ aiCredits: string, maxAICredits: string, rateLimitError: boolean }} */ function parseAuditLogCombined(auditJsonlPathOverride) { - return iterateAuditEntries( - auditJsonlPathOverride, - { aiCredits: "", maxAICredits: "", rateLimitError: false }, - null, - (acc, entry) => { - const errorInfo = parseAICreditsErrorInfoFromAuditEntry(entry); - const max = parseMaxAICreditsFromAuditEntry(entry); - return { - aiCredits: errorInfo.aiCredits || acc.aiCredits, - maxAICredits: max || acc.maxAICredits, - rateLimitError: acc.rateLimitError || errorInfo.rateLimitError, - }; - } - ); + /** @type {{ aiCredits: string, maxAICredits: string, rateLimitError: boolean }} */ + const initial = { aiCredits: "", maxAICredits: "", rateLimitError: false }; + return iterateAuditEntries(auditJsonlPathOverride, initial, null, (acc, entry) => { + const errorInfo = parseAICreditsErrorInfoFromAuditEntry(entry); + const max = parseMaxAICreditsFromAuditEntry(entry); + return { + aiCredits: errorInfo.aiCredits || acc.aiCredits, + maxAICredits: max || acc.maxAICredits, + rateLimitError: acc.rateLimitError || errorInfo.rateLimitError, + }; + }); } /** diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs index a08151c7430..865f1f98ab8 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs @@ -60,11 +60,7 @@ function logDailyGuardrail(message, details) { */ function shouldSkipDailyAICGuardrail() { const eventName = process.env.GITHUB_EVENT_NAME || ""; - return ( - eventName === "workflow_call" || - eventName === "repository_dispatch" || - (eventName === "workflow_dispatch" && (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== "") - ); + return eventName === "workflow_call" || eventName === "repository_dispatch" || (eventName === "workflow_dispatch" && (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== ""); } /** @@ -157,7 +153,7 @@ async function getRunAIC(artifactClient, runId, token, owner, repo) { * @returns {string} */ function formatInteger(value) { - const safeValue = Number.isFinite(value) ? Math.round(value) : 0; + const safeValue = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0; return INTEGER_FORMATTER.format(safeValue); } @@ -452,7 +448,8 @@ async function main() { truncatedByRateLimit, }; logDailyGuardrail("Completed AIC inspection window", { - ...summaryMeta, + candidateRunsCount: summaryMeta.candidateRunsCount, + inspectedRunsCount: summaryMeta.inspectedRunsCount, countedRunIds: countedRuns.map(run => run.id), currentAIC: totalAIC, threshold, From e14f4e27895a5d294b6399190e5f5942b7573e29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:01:51 +0000 Subject: [PATCH 3/4] Refine guardrail readability and log-shape intent Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_daily_aic_workflow_guardrail.cjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs index 865f1f98ab8..24a5310af82 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs @@ -60,7 +60,10 @@ function logDailyGuardrail(message, details) { */ function shouldSkipDailyAICGuardrail() { const eventName = process.env.GITHUB_EVENT_NAME || ""; - return eventName === "workflow_call" || eventName === "repository_dispatch" || (eventName === "workflow_dispatch" && (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== ""); + const isWorkflowCall = eventName === "workflow_call"; + const isRepositoryDispatch = eventName === "repository_dispatch"; + const hasDispatchContext = (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== ""; + return isWorkflowCall || isRepositoryDispatch || (eventName === "workflow_dispatch" && hasDispatchContext); } /** @@ -448,6 +451,7 @@ async function main() { truncatedByRateLimit, }; logDailyGuardrail("Completed AIC inspection window", { + // Keep these explicit to preserve existing log shape (exclude truncatedByRateLimit). candidateRunsCount: summaryMeta.candidateRunsCount, inspectedRunsCount: summaryMeta.inspectedRunsCount, countedRunIds: countedRuns.map(run => run.id), From 6249726f03765845ee4101b42270be3a905857db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:02:44 +0000 Subject: [PATCH 4/4] Document defensive accumulator behavior in audit parser Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/ai_credits_context.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/ai_credits_context.cjs b/actions/setup/js/ai_credits_context.cjs index 35336d65451..b25d9827cbc 100644 --- a/actions/setup/js/ai_credits_context.cjs +++ b/actions/setup/js/ai_credits_context.cjs @@ -141,7 +141,8 @@ function parseAICreditsErrorInfoFromAuditEntry(entry) { * @param {T} defaultValue * @param {((content: string) => boolean) | null} contentGuard - When non-null, called with raw file * content before iteration; return false to skip parsing entirely (fast-path optimization). - * @param {(acc: T, entry: unknown) => T} accumulate + * @param {(acc: T, entry: unknown) => T | undefined} accumulate - Callers should return a defined + * value; undefined is ignored defensively to preserve the previous accumulator. * @returns {T} */ function iterateAuditEntries(auditJsonlPathOverride, defaultValue, contentGuard, accumulate) {