From d0105ca92c8ffc500325462c90405206bd818502 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:16:04 +0000 Subject: [PATCH] chore: sync actions from gh-aw@v0.84.2 --- .github/aw/compat.json | 2 +- setup/js/add_comment.cjs | 16 +++- setup/js/add_reaction.cjs | 2 +- setup/js/ai_credits_context.cjs | 39 +-------- setup/js/create_pull_request.cjs | 42 +++++++++- setup/js/dispatch_workflow.cjs | 92 ++++++++++++++++++++-- setup/js/file_helpers.cjs | 13 ++- setup/js/handle_agent_failure.cjs | 56 +++++++++++-- setup/js/models.json | 12 +-- setup/js/setup_threat_detection.cjs | 10 ++- setup/md/ai_credits_rate_limit_throttle.md | 14 ++++ setup/md/detection_runs_comment.md | 8 +- setup/sh/install_copilot_cli.sh | 2 +- 13 files changed, 232 insertions(+), 76 deletions(-) create mode 100644 setup/md/ai_credits_rate_limit_throttle.md diff --git a/.github/aw/compat.json b/.github/aw/compat.json index 0cc6b903..44a679cb 100644 --- a/.github/aw/compat.json +++ b/.github/aw/compat.json @@ -10,7 +10,7 @@ "min-gh-aw": "0.72.0", "max-gh-aw": "*", "min-agent": "1.0.21", - "max-agent": "1.0.75", + "max-agent": "1.0.77", "open": true }, { diff --git a/setup/js/add_comment.cjs b/setup/js/add_comment.cjs index 7078fc56..00c86fde 100644 --- a/setup/js/add_comment.cjs +++ b/setup/js/add_comment.cjs @@ -72,6 +72,16 @@ function normalizeWorkflowIdList(ids) { ]; } +/** + * Normalize a list of mention aliases: trim, strip leading "@" characters, and drop empty entries. + * @param {unknown} aliases + * @returns {string[]} + */ +function normalizeMentionAliases(aliases) { + if (!Array.isArray(aliases)) return []; + return aliases.map(alias => (typeof alias === "string" ? alias.trim().replace(/^@+/, "") : "")).filter(alias => alias.length > 0); +} + /** * Resolve effective event name/payload for native and forwarded contexts. * Supports: @@ -408,10 +418,8 @@ async function main(config = {}) { const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : []; const requiredTitlePrefix = config.required_title_prefix || ""; const mentionsDisabled = config.mentions === false || config.mentions?.enabled === false; - const preResolvedMentionAliases = - !mentionsDisabled && Array.isArray(config.allowedMentionAliases) ? config.allowedMentionAliases.map(alias => (typeof alias === "string" ? alias.trim().replace(/^@+/, "") : "")).filter(alias => alias.length > 0) : []; - const configuredMentionAliases = - !mentionsDisabled && Array.isArray(config.mentions?.allowed) ? config.mentions.allowed.map(alias => (typeof alias === "string" ? alias.trim().replace(/^@+/, "") : "")).filter(alias => alias.length > 0) : []; + const preResolvedMentionAliases = !mentionsDisabled ? normalizeMentionAliases(config.allowedMentionAliases) : []; + const configuredMentionAliases = !mentionsDisabled ? normalizeMentionAliases(config.mentions?.allowed) : []; // Create an authenticated GitHub client. Uses config["github-token"] when set // (for cross-repository operations), otherwise falls back to the step-level github. diff --git a/setup/js/add_reaction.cjs b/setup/js/add_reaction.cjs index 2c73eda1..75a6916a 100644 --- a/setup/js/add_reaction.cjs +++ b/setup/js/add_reaction.cjs @@ -30,7 +30,7 @@ async function main() { core.info(`Adding reaction: ${reaction}`); // Validate reaction type - if (!Object.prototype.hasOwnProperty.call(REACTION_MAP, reaction)) { + if (!Object.hasOwn(REACTION_MAP, reaction)) { core.setFailed(`${ERR_VALIDATION}: Invalid reaction type: ${reaction}. Valid reactions are: ${Object.keys(REACTION_MAP).join(", ")}`); return; } diff --git a/setup/js/ai_credits_context.cjs b/setup/js/ai_credits_context.cjs index b2134d26..824ee467 100644 --- a/setup/js/ai_credits_context.cjs +++ b/setup/js/ai_credits_context.cjs @@ -43,28 +43,12 @@ function parsePositiveNumberString(value) { return ""; } -/** - * @param {string} left - * @param {string} right - * @returns {boolean} - */ -function isNumberStringGreaterThanOrEqual(left, right) { - if (!left || !right) return false; - const leftNumber = Number.parseFloat(left); - const rightNumber = Number.parseFloat(right); - return Number.isFinite(leftNumber) && Number.isFinite(rightNumber) && leftNumber >= rightNumber; -} - /** * @param {boolean} hasRateLimitSignal - * @param {string} aiCredits - * @param {string} maxAICredits * @returns {boolean} */ -function shouldReportAICreditsRateLimitError(hasRateLimitSignal, aiCredits, maxAICredits) { - if (!hasRateLimitSignal) return false; - if (!aiCredits || !maxAICredits) return true; - return isNumberStringGreaterThanOrEqual(aiCredits, maxAICredits); +function shouldReportAICreditsRateLimitError(hasRateLimitSignal) { + return hasRateLimitSignal; } /** @@ -222,22 +206,7 @@ function parseAICreditsErrorInfoFromAuditEntry(entry) { function iterateAuditEntries(auditJsonlPathOverride, defaultValue, contentGuard, accumulate) { try { const auditJsonlPath = resolveFirewallAuditLogPath(auditJsonlPathOverride); - if (!fs.existsSync(auditJsonlPath)) return defaultValue; - const content = fs.readFileSync(auditJsonlPath, "utf8"); - 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 nextResult = accumulate(result, JSON.parse(trimmed)); - if (nextResult !== undefined) result = nextResult; - } catch { - // ignore malformed lines - } - } - return result; + return iterateJSONLFiles([auditJsonlPath], defaultValue, contentGuard, accumulate); } catch { return defaultValue; } @@ -525,7 +494,7 @@ function resolveAICreditsFailureState({ logProvenance = true } = {}) { const aiCredits = auditAICredits || stdioSignals.aiCredits || envAICredits || ""; const maxAICredits = auditMaxAICredits || stdioSignals.maxAICredits || envMaxAICredits || ""; const rawAICreditsRateLimitError = auditRateLimitError || stdioSignals.rateLimitError || envRateLimitSignalHasEvidence; - const aiCreditsRateLimitError = shouldReportAICreditsRateLimitError(rawAICreditsRateLimitError, aiCredits, maxAICredits); + const aiCreditsRateLimitError = shouldReportAICreditsRateLimitError(rawAICreditsRateLimitError); return { aiCredits, maxAICredits, aiCreditsRateLimitError, maxAICreditsExceeded: auditMaxAICreditsExceeded || stdioSignals.maxAICreditsExceeded }; } diff --git a/setup/js/create_pull_request.cjs b/setup/js/create_pull_request.cjs index b56c1ddd..10546c66 100644 --- a/setup/js/create_pull_request.cjs +++ b/setup/js/create_pull_request.cjs @@ -177,6 +177,39 @@ async function tryRecoverGitAmAddAddConflict(execApi) { } } +/** + * Resolves auto-merge enablement and merge method from the handler config. + * + * Supported values: + * - false / "false" / empty => disabled + * - true / "true" => enabled with SQUASH as the default merge strategy + * - "squash" | "merge" | "rebase" => enabled with explicit strategy + * - any other value => disabled with a warning (fail-closed) + * + * @param {any} value + * @returns {{ enabled: boolean, mergeMethod?: "SQUASH" | "MERGE" | "REBASE" }} + */ +function parseAutoMergeConfig(value) { + const normalized = String(value ?? "") + .trim() + .toLowerCase(); + if (!normalized || normalized === "false") { + return { enabled: false }; + } + switch (normalized) { + case "squash": + case "true": + return { enabled: true, mergeMethod: "SQUASH" }; + case "merge": + return { enabled: true, mergeMethod: "MERGE" }; + case "rebase": + return { enabled: true, mergeMethod: "REBASE" }; + default: + core.warning(`Unrecognized auto-merge value "${value}". Expected true, false, "squash", "merge", or "rebase". Auto-merge will be disabled.`); + return { enabled: false }; + } +} + /** * Apply a git bundle to a local branch without fetching directly into the branch ref. * Fetching directly into refs/heads/ fails when that branch is currently checked out. @@ -724,7 +757,7 @@ async function main(config = {}) { const draftDefault = parseBoolTemplatable(config.draft, true); const ifNoChanges = config.if_no_changes || "warn"; const allowEmpty = parseBoolTemplatable(config.allow_empty, false); - const autoMerge = parseBoolTemplatable(config.auto_merge, false); + const { enabled: autoMerge, mergeMethod: autoMergeMethod } = parseAutoMergeConfig(config.auto_merge); const preserveBranchName = config.preserve_branch_name === true; const recreateRef = config.recreate_ref === true; const signedCommits = config.signed_commits !== false; @@ -2559,8 +2592,8 @@ ${patchPreview}`; if (autoMerge) { try { await githubClient.graphql( - `mutation($prId: ID!) { - enablePullRequestAutoMerge(input: {pullRequestId: $prId}) { + `mutation($prId: ID!, $mergeMethod: PullRequestMergeMethod) { + enablePullRequestAutoMerge(input: {pullRequestId: $prId, mergeMethod: $mergeMethod}) { pullRequest { id } @@ -2568,6 +2601,7 @@ ${patchPreview}`; }`, { prId: pullRequest.node_id, + mergeMethod: autoMergeMethod, } ); core.info(`Enabled auto-merge for pull request #${pullRequest.number}`); @@ -2786,4 +2820,4 @@ ${patchPreview}`; }; // End of handleCreatePullRequest } // End of main -module.exports = { main, enforcePullRequestLimits, countUniquePatchFiles, parseDiffGitHeader, applyBundleToBranch, rewriteBundleBranchAsSingleCommit }; +module.exports = { main, enforcePullRequestLimits, countUniquePatchFiles, parseDiffGitHeader, applyBundleToBranch, rewriteBundleBranchAsSingleCommit, parseAutoMergeConfig }; diff --git a/setup/js/dispatch_workflow.cjs b/setup/js/dispatch_workflow.cjs index de8e490b..3c049eb0 100644 --- a/setup/js/dispatch_workflow.cjs +++ b/setup/js/dispatch_workflow.cjs @@ -9,6 +9,7 @@ const HANDLER_TYPE = "dispatch_workflow"; const { getErrorMessage } = require("./error_helpers.cjs"); +const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { resolveTargetRepoConfig, parseRepoSlug, validateTargetRepo } = require("./repo_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); @@ -29,6 +30,8 @@ async function main(config = {}) { const awContextWorkflows = new Set(config.aw_context_workflows || []); // Workflows that accept aw_context input const githubClient = await createAuthenticatedGitHubClient(config); const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); + const allowedRefPatterns = parseAllowedRefPatterns(config.allowed_refs); + const allowedRefRegexes = allowedRefPatterns.map(pattern => globPatternToRegex(pattern, { pathMode: true, caseSensitive: true })); // Resolve the dispatch destination repository from target-repo config, falling back to context.repo const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`; @@ -107,23 +110,23 @@ async function main(config = {}) { // GITHUB_HEAD_REF which contains the actual PR branch name. // For cross-repo dispatch (workflow_call relay), the caller's GITHUB_REF has no meaning on // the target repository, so we use the compiler-injected target-ref instead. - let ref; + let defaultRef; if (config["target-ref"]) { // Compiler-injected target ref for cross-repo dispatch (workflow_call relay pattern). // Takes precedence over all environment variables to avoid using the caller's ref. - ref = config["target-ref"]; - core.info(`Using configured target-ref: ${ref}`); + defaultRef = config["target-ref"]; + core.info(`Using configured target-ref: ${defaultRef}`); } else if (process.env.GITHUB_HEAD_REF) { // We're in a pull_request event, use the PR branch ref - ref = `refs/heads/${process.env.GITHUB_HEAD_REF}`; - core.info(`Using PR branch ref: ${ref}`); + defaultRef = `refs/heads/${process.env.GITHUB_HEAD_REF}`; + core.info(`Using PR branch ref: ${defaultRef}`); } else if (process.env.GITHUB_REF || context.ref) { // Use GITHUB_REF for non-PR contexts (push, workflow_dispatch, etc.) - ref = process.env.GITHUB_REF || context.ref; + defaultRef = process.env.GITHUB_REF || context.ref; } else { // Last resort: fetch the repository's default branch - ref = await getDefaultBranchRef(); - core.info(`Using default branch ref: ${ref}`); + defaultRef = await getDefaultBranchRef(); + core.info(`Using default branch ref: ${defaultRef}`); } /** @@ -177,6 +180,31 @@ async function main(config = {}) { core.info(`Dispatching workflow: ${workflowName}`); + if (message.ref !== undefined && message.ref !== null && typeof message.ref !== "string") { + core.warning(`message.ref must be a string; ignoring non-string value (type: ${typeof message.ref})`); + } + const outputRef = typeof message.ref === "string" ? message.ref.trim() : ""; + let ref = defaultRef; + if (outputRef) { + ref = normalizeRef(outputRef); + if (allowedRefRegexes.length === 0) { + const error = "message.ref is not allowed unless 'allowed-refs' is configured in safe-outputs.dispatch-workflow"; + core.warning(error); + return { + success: false, + error, + }; + } + if (!allowedRefRegexes.some(pattern => pattern.test(ref))) { + const error = `Ref '${ref}' is not in allowed-refs: ${allowedRefPatterns.join(", ")}`; + core.warning(error); + return { + success: false, + error, + }; + } + } + // Prepare inputs - convert all values to strings as required by workflow_dispatch // and resolve any #temporary_id references before dispatching /** @type {Record} */ @@ -320,4 +348,52 @@ async function main(config = {}) { }; } +/** + * @param {string[]|string|undefined} allowedRefsValue + * @returns {string[]} + */ +function parseAllowedRefPatterns(allowedRefsValue) { + /** @type {string[]} */ + const refs = []; + if (Array.isArray(allowedRefsValue)) { + for (const pattern of allowedRefsValue) { + if (typeof pattern === "string") { + const trimmed = pattern.trim(); + if (trimmed) { + refs.push(normalizeRefPattern(trimmed)); + } + } + } + return refs; + } + if (typeof allowedRefsValue === "string") { + return allowedRefsValue + .split(",") + .map(pattern => pattern.trim()) + .filter(Boolean) + .map(normalizeRefPattern); + } + return refs; +} + +/** + * @param {string} refOrBranch + * @returns {string} + */ +function normalizeRef(refOrBranch) { + if (refOrBranch.startsWith("refs/")) return refOrBranch; + if (refOrBranch.startsWith("tags/")) return `refs/${refOrBranch}`; + return `refs/heads/${refOrBranch}`; +} + +/** + * @param {string} pattern + * @returns {string} + */ +function normalizeRefPattern(pattern) { + if (pattern.startsWith("refs/")) return pattern; + if (pattern.startsWith("tags/")) return `refs/${pattern}`; + return `refs/heads/${pattern}`; +} + module.exports = { main }; diff --git a/setup/js/file_helpers.cjs b/setup/js/file_helpers.cjs index b7979950..2c9e5541 100644 --- a/setup/js/file_helpers.cjs +++ b/setup/js/file_helpers.cjs @@ -48,9 +48,10 @@ function listFilesRecursively(dirPath, relativeTo) { * @param {string} artifactDir - The artifact directory to list if file not found * @param {string} fileDescription - Description of the file (e.g., "Prompt file", "Agent output file") * @param {boolean} required - Whether the file is required + * @param {boolean} [continueOnError=false] - Whether missing required files should warn instead of failing * @returns {boolean} True if file exists (or not required), false otherwise */ -function checkFileExists(filePath, artifactDir, fileDescription, required) { +function checkFileExists(filePath, artifactDir, fileDescription, required, continueOnError = false) { if (fs.existsSync(filePath)) { try { const stats = fs.statSync(filePath); @@ -63,7 +64,9 @@ function checkFileExists(filePath, artifactDir, fileDescription, required) { } } else { if (required) { - core.error("❌ " + fileDescription + " not found at: " + filePath); + if (!continueOnError) { + core.error("❌ " + fileDescription + " not found at: " + filePath); + } // List all files in artifact directory for debugging core.info("📁 Listing all files in artifact directory: " + artifactDir); const files = listFilesRecursively(artifactDir, artifactDir); @@ -73,7 +76,11 @@ function checkFileExists(filePath, artifactDir, fileDescription, required) { core.info(" Found " + files.length + " file(s):"); files.forEach(file => core.info(" - " + file)); } - core.setFailed(`${ERR_SYSTEM}: ❌ ${fileDescription} not found at: ${filePath}`); + if (continueOnError) { + core.warning(`${ERR_SYSTEM}: ❌ ${fileDescription} not found at: ${filePath}. Continuing because GH_AW_DETECTION_CONTINUE_ON_ERROR=true`); + } else { + core.setFailed(`${ERR_SYSTEM}: ❌ ${fileDescription} not found at: ${filePath}`); + } return false; } else { core.info("No " + fileDescription.toLowerCase() + " found at: " + filePath); diff --git a/setup/js/handle_agent_failure.cjs b/setup/js/handle_agent_failure.cjs index 9648332e..09e7fde6 100644 --- a/setup/js/handle_agent_failure.cjs +++ b/setup/js/handle_agent_failure.cjs @@ -253,6 +253,7 @@ function buildFailureMatchCategories(options) { if (options.modelNotSupportedError) categories.push("model_not_supported_error"); if (options.http400ResponseError) categories.push("http_400_response_error"); if (options.aiCreditsRateLimitError) categories.push("ai_credits_rate_limit_error"); + if (options.hasEngineRateLimit429) categories.push("engine_rate_limit_429"); if (options.unknownModelAICredits) categories.push("unknown_model_ai_credits"); if (options.missingModelPricingError) categories.push("missing_model_pricing"); if (options.maxAICreditsExceeded) categories.push("max_ai_credits_exceeded"); @@ -291,6 +292,7 @@ function buildFailureMatchCategories(options) { * @param {boolean} options.hasStaleLockFileFailed * @param {boolean} options.hasDailyAICExceeded * @param {boolean} options.aiCreditsRateLimitError + * @param {boolean} options.hasEngineRateLimit429 * @param {boolean} options.maxAICreditsExceeded * @param {boolean} options.hasAssignmentErrors * @param {boolean} options.http400ResponseError @@ -304,6 +306,7 @@ function buildFailureIssueTitle(options) { if (options.hasDailyAICExceeded) return `[aw] ${workflowName} exceeded daily AI credits budget`; if (options.maxAICreditsExceeded) return `[aw] ${workflowName} exceeded max AI credits`; if (options.aiCreditsRateLimitError) return `[aw] ${workflowName} hit AI credits rate limit`; + if (options.hasEngineRateLimit429) return `[aw] ${workflowName} hit engine rate limit (HTTP 429)`; // Missing model pricing is surfaced by the proxy as HTTP 400, so prefer the // specialized title before falling back to the generic transport-level error. if (options.missingModelPricingError) { @@ -2050,9 +2053,10 @@ function readTokenUsageMarkdown() { * @param {string} aiCredits * @param {string} maxAICredits * @param {string} runUrl + * @param {boolean} [isBudgetExceeded] - true when the agent exceeded the configured max-ai-credits budget; false when the 429 was a throughput throttle * @returns {string} */ -function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredits, maxAICredits, runUrl) { +function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredits, maxAICredits, runUrl, isBudgetExceeded = false) { if (!hasAICreditsRateLimitError) { return ""; } @@ -2076,11 +2080,9 @@ function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredi metricsSummary = ` Used \`${formattedAICredits}\`.`; } - // Suggest a new limit: 2x current max, or 2x actual usage if max is unknown, or a reasonable default - const baseForSuggestion = Number.isFinite(numericMaxAICredits) && numericMaxAICredits > 0 ? numericMaxAICredits : Number.isFinite(numericAICredits) && numericAICredits > 0 ? numericAICredits : 0; - const suggestedCredits = baseForSuggestion > 0 ? Math.ceil(baseForSuggestion * 2) : 2000; - - const templateName = "ai_credits_rate_limit_error.md"; + // Use the budget-exceeded template when the agent exhausted its configured limit; + // use the throughput-throttle template when the 429 arrived before the budget was spent. + const templateName = isBudgetExceeded ? "ai_credits_rate_limit_error.md" : "ai_credits_rate_limit_throttle.md"; let templatePath = ""; try { templatePath = getPromptPath(templateName); @@ -2088,6 +2090,13 @@ function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredi throw new Error(`failed to resolve template path for ${templateName} (${getErrorMessage(error)}); ensure RUNNER_TEMP or GH_AW_PROMPTS_DIR is set and the template file exists`, { cause: error }); } + let suggestedCredits; + if (isBudgetExceeded) { + // Suggest a new limit: 2x current max, or 2x actual usage if max is unknown, or a reasonable default. + const baseForSuggestion = Number.isFinite(numericMaxAICredits) && numericMaxAICredits > 0 ? numericMaxAICredits : Number.isFinite(numericAICredits) && numericAICredits > 0 ? numericAICredits : 0; + suggestedCredits = baseForSuggestion > 0 ? Math.ceil(baseForSuggestion * 2) : 2000; + } + try { return ( "\n" + @@ -2608,6 +2617,33 @@ function detectAWFFirewallStartupFailureFromLog() { } } +/** + * Detect whether the agent failure was caused by engine HTTP 429/rate limiting. + * Checks agent-stdio.log first, then falls back to OTLP mirror payloads. + * @returns {boolean} + */ +function detectEngineRateLimit429Failure() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + const stdioLogPath = agentOutputFile ? path.join(path.dirname(agentOutputFile), "agent-stdio.log") : "/tmp/gh-aw/agent-stdio.log"; + try { + if (fs.existsSync(stdioLogPath)) { + const logContent = fs.readFileSync(stdioLogPath, "utf8"); + // If the agent completed successfully (terminal_reason: "completed"), the failure + // was caused by something other than the agent itself. Suppress the 429 signal to + // avoid giving a rate-limit title to an unrelated post-processing failure. + if (/"terminal_reason"[ ]?:[ ]?"completed"/.test(logContent)) { + return false; + } + if (hasEngineRateLimit429Signal(logContent)) { + return true; + } + } + } catch { + // Ignore read errors and continue with OTLP mirror fallback. + } + return hasEngineRateLimit429InOTELMirror(); +} + /** * Extract terminal error messages from agent-stdio.log to surface engine failures. * First tries to match known error patterns (ERROR:, Error:, Fatal:, panic:, Reconnecting...). @@ -3383,6 +3419,7 @@ async function main() { if (hasToolDenialsExceeded) { core.info(`Detected ${toolDenialsExceededEvents.length} guard.tool_denials_exceeded event(s) from Copilot SDK events.jsonl`); } + const hasEngineRateLimit429 = agentConclusion === "failure" && !maxAICreditsExceeded && !aiCreditsRateLimitError && detectEngineRateLimit429Failure(); // Detect cache-miss misconfiguration: the agent reported a missing_data with reason // "cache_memory_miss" after a cache restore matched. This indicates the prompt @@ -3531,6 +3568,7 @@ async function main() { hasStaleLockFileFailed, hasDailyAICExceeded, aiCreditsRateLimitError, + hasEngineRateLimit429, maxAICreditsExceeded, hasAssignmentErrors, http400ResponseError, @@ -3560,6 +3598,7 @@ async function main() { modelNotSupportedError, http400ResponseError, aiCreditsRateLimitError, + hasEngineRateLimit429, unknownModelAICredits, missingModelPricingError, maxAICreditsExceeded, @@ -3733,7 +3772,7 @@ async function main() { // Build model not supported error context const modelNotSupportedErrorContext = buildModelNotSupportedErrorContext(modelNotSupportedError); const http400ResponseErrorContext = buildHTTP400ResponseErrorContext(http400ResponseError); - const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl); + const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl, maxAICreditsExceeded); const unknownModelAICreditsContext = buildUnknownModelAICreditsContext(unknownModelAICredits); // Build GitHub App token minting failure context @@ -3955,7 +3994,7 @@ async function main() { // Build model not supported error context const modelNotSupportedErrorContext = buildModelNotSupportedErrorContext(modelNotSupportedError); const http400ResponseErrorContext = buildHTTP400ResponseErrorContext(http400ResponseError); - const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl); + const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl, maxAICreditsExceeded); const unknownModelAICreditsContext = buildUnknownModelAICreditsContext(unknownModelAICredits); // Build GitHub App token minting failure context @@ -4135,6 +4174,7 @@ module.exports = { hasEngineMaxRunsExceededSignal, hasEngineRateLimit429Signal, hasEngineRateLimit429InOTELMirror, + detectEngineRateLimit429Failure, buildEngineMaxRunsExceededContext, buildEngineRateLimit429Context, hasEngineMaxCacheMissesExceededSignal, diff --git a/setup/js/models.json b/setup/js/models.json index 35b728a0..eb8c4b7c 100644 --- a/setup/js/models.json +++ b/setup/js/models.json @@ -478,8 +478,8 @@ }, "gpt-5.6-luna": { "cost": { - "input": "1e-06", - "output": "6e-06", + "input": "2e-07", + "output": "1.2e-06", "cache_read": "1e-07" }, "provider_type": "openai", @@ -496,8 +496,8 @@ }, "gpt-5.6-terra": { "cost": { - "input": "2.5e-06", - "output": "1.5e-05", + "input": "2e-06", + "output": "1.2e-05", "cache_read": "2.5e-07" }, "provider_type": "openai", @@ -532,8 +532,8 @@ }, "grok-4.5": { "cost": { - "input": "2e-07", - "output": "6e-07", + "input": "2e-06", + "output": "6e-06", "cache_read": "5e-08" }, "provider_type": "openai", diff --git a/setup/js/setup_threat_detection.cjs b/setup/js/setup_threat_detection.cjs index ae5d1252..336844d5 100644 --- a/setup/js/setup_threat_detection.cjs +++ b/setup/js/setup_threat_detection.cjs @@ -25,6 +25,8 @@ const { getPromptPath } = require("./messages_core.cjs"); * @returns {Promise} */ async function main() { + const continueOnError = (process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR || "true").toLowerCase() !== "false"; + // Read the threat detection template from file const templatePath = getPromptPath("threat_detection.md"); if (!fs.existsSync(templatePath)) { @@ -72,7 +74,7 @@ async function main() { // The agent-output artifact is also downloaded to /tmp/gh-aw/threat-detection/ // The artifact contains /tmp/gh-aw/agent_output.json which becomes /tmp/gh-aw/threat-detection/agent_output.json const agentOutputPath = path.join(threatDetectionDir, AGENT_OUTPUT_FILENAME); - if (!checkFileExists(agentOutputPath, threatDetectionDir, "Agent output file", true)) { + if (!checkFileExists(agentOutputPath, threatDetectionDir, "Agent output file", true, continueOnError)) { return; } @@ -94,7 +96,11 @@ async function main() { } if (patchFiles.length === 0 && hasPatch) { - core.setFailed(`${ERR_VALIDATION}: Patch/bundle file(s) expected but not found in: ${threatDetectionDir}`); + if (continueOnError) { + core.warning(`${ERR_VALIDATION}: Patch/bundle file(s) expected but not found in: ${threatDetectionDir}. Continuing because GH_AW_DETECTION_CONTINUE_ON_ERROR=true`); + } else { + core.setFailed(`${ERR_VALIDATION}: Patch/bundle file(s) expected but not found in: ${threatDetectionDir}`); + } return; } diff --git a/setup/md/ai_credits_rate_limit_throttle.md b/setup/md/ai_credits_rate_limit_throttle.md new file mode 100644 index 00000000..b69c3f99 --- /dev/null +++ b/setup/md/ai_credits_rate_limit_throttle.md @@ -0,0 +1,14 @@ +> [!WARNING] +> **AI Credits Rate Limit** +> +> The Copilot API returned a rate limit response (HTTP 429), but the workflow did not report the explicit AI credits budget-exceeded guardrail signal.{metrics_summary} + +
+Tips for reducing rate limit issues + +- Review the [cost optimization guidance](https://github.github.com/gh-aw/reference/cost-management/). +- Reduce unnecessary model or tool calls in the prompt. +- Trim large inputs or excess context that does not change the outcome. +- Split large tasks across smaller runs when possible. + +
diff --git a/setup/md/detection_runs_comment.md b/setup/md/detection_runs_comment.md index a8ac9baa..68cf428f 100644 --- a/setup/md/detection_runs_comment.md +++ b/setup/md/detection_runs_comment.md @@ -1,5 +1,7 @@ ### {workflow_name} -**Conclusion:** {conclusion} | **Reason:** {reason} - -> Generated from [{workflow_name}]({run_url}) +| Field | Value | +|---|---| +| Conclusion | `{conclusion}` | +| Reason | `{reason}` | +| Run | [View run]({run_url}) | diff --git a/setup/sh/install_copilot_cli.sh b/setup/sh/install_copilot_cli.sh index d50fc82a..cb795653 100755 --- a/setup/sh/install_copilot_cli.sh +++ b/setup/sh/install_copilot_cli.sh @@ -33,7 +33,7 @@ COPILOT_TOOLCACHE_MAX_DEPTH=4 # argument nor a GH_AW_COMPILED_VERSION-backed compat.json lookup is available. # It is the last resort (priority 3) after engine.version (priority 1) and # compat.json toolcache lookup (priority 2). -DEFAULT_COPILOT_VERSION="1.0.75" +DEFAULT_COPILOT_VERSION="1.0.77" COMPAT_URL="${COPILOT_COMPAT_URL:-https://raw.githubusercontent.com/github/gh-aw-actions/main/.github/aw/compat.json}" COMPILED_GH_AW_VERSION="${GH_AW_COMPILED_VERSION:-}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"