From 3acba07d98455e762bbda6995835b7757a6694ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 04:26:00 +0000 Subject: [PATCH 1/2] chore: placeholder for progress tracking Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../smoke-copilot-aoai-entra.lock.yml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index 4718430c760..4b991a5c414 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -1718,7 +1718,7 @@ jobs: DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_MCP_SCRIPTS_PORT -e GH_AW_MCP_SCRIPTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -e GH_TOKEN -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.25' - mkdir -p /home/runner/.copilot + mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_51d567a2fd4f7ca5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { @@ -1849,9 +1849,11 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" + export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -1889,7 +1891,6 @@ jobs: COPILOT_PROVIDER_BASE_URL: ${{ secrets.FOUNDRY_OPENAI_ENDPOINT }} COPILOT_PROVIDER_WIRE_API: responses GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} @@ -1910,7 +1911,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Stop CLI Proxy if: always() continue-on-error: true @@ -2452,7 +2452,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json + rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' @@ -2510,9 +2510,10 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json + trap 'rm -f "$HOME/.copilot/settings.json"' EXIT + mkdir -p "$HOME/.copilot" + printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" + export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN @@ -2567,7 +2568,6 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() From af854c2f6520a6c788462e23ce7ef09f05478429 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 04:34:20 +0000 Subject: [PATCH 2/2] optimize: stop paginating listWorkflowRuns once 24h cutoff is reached - Add early exit when a stale run is found (API returns newest-first), avoiding unnecessary page fetches and GitHub API calls - Remove dead countedRuns >= maxInspectableRuns guard in artifact loop - Replace verbose per-page runIds log (up to 100 IDs) with firstRunId/lastRunId - Add test proving pagination stops after the first stale run Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../js/check_daily_aic_workflow_guardrail.cjs | 19 ++-- ...heck_daily_aic_workflow_guardrail.test.cjs | 96 +++++++++++++++++++ pkg/workflow/action_resolver.go | 16 ++-- 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs index 6896ffab26a..905f42c0aa9 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs @@ -382,6 +382,11 @@ async function main() { const candidateRuns = []; let page = 1; let truncatedByRateLimit = false; + // listWorkflowRuns returns runs in descending creation order (newest first). + // The first run whose created_at falls before the cutoff means all remaining + // runs on this page and every subsequent page are also outside the window, so + // we can stop paginating immediately rather than exhausting the page budget. + let reachedCutoff = false; while (page <= MAX_WORKFLOW_RUN_PAGES) { logDailyGuardrail("Querying completed workflow runs", { workflowId: currentRun.data.workflow_id, @@ -401,7 +406,8 @@ async function main() { logDailyGuardrail("Received workflow runs page", { page, runCount: runs.length, - runIds: runs.map(run => run?.id).filter(Boolean), + firstRunId: runs[0]?.id ?? null, + lastRunId: runs[runs.length - 1]?.id ?? null, }); if (runs.length === 0) { break; @@ -412,7 +418,10 @@ async function main() { } const createdAtMs = Date.parse(run.created_at || ""); if (!Number.isFinite(createdAtMs) || createdAtMs < cutoffMs) { - continue; + // Runs are newest-first; any run older than the cutoff means all + // remaining runs (and pages) are also outside the 24h window. + reachedCutoff = true; + break; } candidateRuns.push(run); if (candidateRuns.length >= maxInspectableRuns) { @@ -420,7 +429,7 @@ async function main() { break; } } - if (candidateRuns.length >= maxInspectableRuns || runs.length < 100) { + if (reachedCutoff || candidateRuns.length >= maxInspectableRuns || runs.length < 100) { break; } page += 1; @@ -437,10 +446,6 @@ async function main() { /** @type {Array<{id:number, html_url:string, created_at:string, conclusion:string, aic:number}>} */ const countedRuns = []; for (const run of candidateRuns) { - if (countedRuns.length >= maxInspectableRuns) { - truncatedByRateLimit = true; - break; - } try { const runAIC = await module.exports.getRunAIC(artifactClient, run.id, token, owner, repo); if (runAIC <= 0) { diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs index 2e7fce51ccd..2759efe2579 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs @@ -358,6 +358,102 @@ describe("check_daily_aic_workflow_guardrail", () => { } }); + it("main() stops paginating as soon as a run older than the 24h cutoff is found", async () => { + const nowIso = new Date().toISOString(); + const staleIso = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(); + + let listCallCount = 0; + const mockGithub = { + rest: { + rateLimit: { + get: async () => ({ + data: { + resources: { + core: { limit: 5000, remaining: 4990, used: 10, reset: Math.floor(Date.now() / 1000) + 3600 }, + }, + }, + headers: {}, + }), + }, + actions: { + getWorkflowRun: async () => ({ + data: { workflow_id: 999, actor: { login: "bot" }, triggering_actor: { login: "bot" } }, + headers: {}, + }), + listWorkflowRuns: async ({ page }) => { + listCallCount += 1; + // Page 1: one recent run followed by a stale run. + // If early-exit works, page 2 should never be requested. + if (page === 1) { + return { + data: { + workflow_runs: [ + { id: 10, html_url: "https://example.test/runs/10", created_at: nowIso, conclusion: "success" }, + // A stale run: encountering this should immediately stop pagination. + { id: 9, html_url: "https://example.test/runs/9", created_at: staleIso, conclusion: "success" }, + ], + }, + headers: {}, + }; + } + // Should never reach page 2. + return { data: { workflow_runs: [] }, headers: {} }; + }, + }, + }, + }; + + const getRunAICSpy = vi.spyOn(exports, "getRunAIC").mockResolvedValue(50); + + const coreOutputs = {}; + const mockCore = { + setOutput: (key, value) => { + coreOutputs[key] = value; + }, + info: () => {}, + warning: () => {}, + summary: { + addDetails: function () { + return this; + }, + write: async () => {}, + }, + }; + + const mockContext = { repo: { owner: "test-owner", repo: "test-repo" }, runId: 42 }; + + global.core = mockCore; + global.github = mockGithub; + global.context = mockContext; + + process.env.GH_AW_MAX_DAILY_AI_CREDITS = "1000"; + process.env.GH_AW_GITHUB_TOKEN = "fake-token"; + process.env.GITHUB_EVENT_NAME = "pull_request"; + + try { + await expect(exports.main()).resolves.toBeUndefined(); + + // Only page 1 should have been fetched; the stale run should have + // terminated pagination before page 2 was requested. + expect(listCallCount).toBe(1); + + // Only the recent run (id: 10) should have been inspected. + expect(getRunAICSpy).toHaveBeenCalledTimes(1); + expect(getRunAICSpy.mock.calls[0][1]).toBe(10); + + // Guardrail not exceeded (50 < 1000). + expect(coreOutputs["daily_ai_credits_exceeded"]).toBe("false"); + } finally { + delete global.core; + delete global.github; + delete global.context; + delete process.env.GH_AW_MAX_DAILY_AI_CREDITS; + delete process.env.GH_AW_GITHUB_TOKEN; + delete process.env.GITHUB_EVENT_NAME; + getRunAICSpy.mockRestore(); + } + }); + it("main() marks the step failed when the daily AI Credits guardrail is exceeded", async () => { const getRunAICSpy = vi.spyOn(exports, "getRunAIC").mockResolvedValue(200); diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 34df2779e61..cf132cc2a18 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -166,15 +166,15 @@ func ResolveGhAwRef(ctx context.Context, ref string) (string, error) { apiPath := fmt.Sprintf("/repos/github/gh-aw/commits/%s", ref) callCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() -cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha") -output, err := cmd.CombinedOutput() -if err != nil { - msg := strings.TrimSpace(string(output)) - if msg != "" { - return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err) + cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha") + output, err := cmd.CombinedOutput() + if err != nil { + msg := strings.TrimSpace(string(output)) + if msg != "" { + return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err) + } + return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err) } - return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err) -} sha := strings.TrimSpace(string(output)) if !gitutil.IsValidFullSHA(sha) { return "", fmt.Errorf("unexpected response resolving gh-aw ref %q: got %q (expected 40-char hex SHA)", ref, sha)