diff --git a/Makefile b/Makefile index dbe9e66b093..00c3030e19f 100644 --- a/Makefile +++ b/Makefile @@ -259,6 +259,7 @@ check-cjs-syntax: .PHONY: test-js test-js: build-js cd actions/setup/js && npm run test:js -- --no-file-parallelism + cd eslint-factory && npm test # Test impacted JavaScript unit tests only (excluding integration tests) .PHONY: test-impacted-js @@ -806,6 +807,7 @@ deps: check-node-version go mod download go mod tidy cd actions/setup/js && npm ci + cd eslint-factory && npm ci # Install development tools (including linter) .PHONY: deps-dev diff --git a/actions/setup/js/add_comment.test.cjs b/actions/setup/js/add_comment.test.cjs index 32b0d906c5c..420c16e9af8 100644 --- a/actions/setup/js/add_comment.test.cjs +++ b/actions/setup/js/add_comment.test.cjs @@ -1,5 +1,5 @@ // @ts-check -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; @@ -7,7 +7,20 @@ import { syncRuntimePromptTemplates } from "./test_prompt_templates.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -syncRuntimePromptTemplates(import.meta.url); +const { runtimePromptsDir } = syncRuntimePromptTemplates(import.meta.url); +const originalPromptsDir = process.env.GH_AW_PROMPTS_DIR; + +beforeAll(() => { + process.env.GH_AW_PROMPTS_DIR = runtimePromptsDir; +}); + +afterAll(() => { + if (originalPromptsDir === undefined) { + delete process.env.GH_AW_PROMPTS_DIR; + } else { + process.env.GH_AW_PROMPTS_DIR = originalPromptsDir; + } +}); describe("add_comment", () => { let mockCore; diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index 437d694f44d..1609e97abea 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -25,6 +25,15 @@ const tls = require("tls"); const { withRetry, sleep } = require("./error_recovery.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +function parseReflectTimeoutMs(value) { + const rawValue = String(value || "").trim(); + if (!/^\d+$/.test(rawValue)) { + return 60000; + } + const timeoutMs = Number(rawValue); + return Number.isSafeInteger(timeoutMs) ? timeoutMs : 60000; +} + // AWF API proxy management endpoint for discovering configured LLM providers and available models. // The api-proxy sidecar exposes /reflect on its management port (port 10000) inside the AWF // Docker network. From the agent container, the proxy is reachable via the "api-proxy" hostname. @@ -32,7 +41,7 @@ const AWF_API_PROXY_REFLECT_URL = "http://api-proxy:10000/reflect"; // Persist outside the read-only gh-aw infrastructure mount. const AWF_REFLECT_OUTPUT_PATH = path.join(process.env.RUNNER_TEMP || os.tmpdir(), "awf-reflect.json"); // Milliseconds to wait for the /reflect endpoint before giving up. -const AWF_REFLECT_TIMEOUT_MS = 60000; +const AWF_REFLECT_TIMEOUT_MS = parseReflectTimeoutMs(process.env.GH_AW_REFLECT_TIMEOUT_MS); // Milliseconds to wait for each models_url fallback fetch (shorter than the main reflect timeout). const AWF_MODELS_URL_TIMEOUT_MS = 3000; // Milliseconds to wait for an api-proxy provider listener to accept a real TCP connection. @@ -367,7 +376,7 @@ async function enrichReflectModels(reflectData, timeoutMs, logger) { * outputPath: string, * bytesWritten?: number, * reflectData?: object, - * reason?: "unexpected_status"|"timeout"|"request_failed", + * reason?: "disabled"|"unexpected_status"|"timeout"|"request_failed", * status?: number, * error?: string, * }>} @@ -380,6 +389,11 @@ async function fetchAWFReflect(options) { const logger = (options && options.logger) || DEFAULT_REFLECT_LOGGER; const writeFile = (options && options.writeFileSync) || fs.writeFileSync; + if (process.env.GH_AW_SKIP_REFLECT === "true") { + logger("awf-reflect: disabled by GH_AW_SKIP_REFLECT"); + return { ok: false, reflectUrl, outputPath, reason: "disabled" }; + } + logger(`awf-reflect: fetching ${reflectUrl} (timeout=${timeoutMs}ms)`); const ac = new AbortController(); @@ -1022,6 +1036,7 @@ if (typeof module !== "undefined" && module.exports) { AWF_PROVIDER_LISTENER_READY_PROBE_TIMEOUT_MS, DEFAULT_API_PROXY_HOST_BRIDGE, GEMINI_MODEL_NAME_PREFIX, + parseReflectTimeoutMs, enrichReflectModels, extractModelIds, fetchAWFReflect, diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index 483a030a27f..244f30f02a6 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -28,6 +28,7 @@ const { hasAPIProxyLocalhostAlias, inferProviderTypeForModel, inferWireApiForModel, + parseReflectTimeoutMs, resolveOpenAICompatibleEndpointFromReflect, resolveProviderEndpointFromReflect, resolveMultiProviderFromReflect, @@ -50,6 +51,14 @@ describe("awf_reflect.cjs", () => { expect(DEFAULT_API_PROXY_HOST_BRIDGE).toBe("host.docker.internal"); expect(GEMINI_MODEL_NAME_PREFIX).toBe("models/"); }); + + it("falls back to the default reflect timeout when the environment value is invalid", () => { + expect(parseReflectTimeoutMs("")).toBe(60000); + expect(parseReflectTimeoutMs("not-a-number")).toBe(60000); + expect(parseReflectTimeoutMs("12abc")).toBe(60000); + expect(parseReflectTimeoutMs("999999999999999999999999")).toBe(60000); + expect(parseReflectTimeoutMs("1234")).toBe(1234); + }); }); describe("waitForProviderListenerReady", () => { @@ -639,6 +648,29 @@ describe("awf_reflect.cjs", () => { describe("fetchAWFReflect", () => { afterEach(() => { vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + }); + + it("skips network requests when reflection is disabled", async () => { + const fetchMock = vi.fn(); + const logs = []; + vi.stubGlobal("fetch", fetchMock); + vi.stubEnv("GH_AW_SKIP_REFLECT", "true"); + + await expect( + fetchAWFReflect({ + reflectUrl: "http://api-proxy:10000/reflect", + outputPath: "/tmp/gh-aw-test-noop.json", + logger: msg => logs.push(msg), + }) + ).resolves.toEqual({ + ok: false, + reflectUrl: "http://api-proxy:10000/reflect", + outputPath: "/tmp/gh-aw-test-noop.json", + reason: "disabled", + }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(logs).toContain("awf-reflect: disabled by GH_AW_SKIP_REFLECT"); }); it("saves enriched reflect data when api-proxy returns null models for configured provider", async () => { diff --git a/actions/setup/js/claude_harness.test.cjs b/actions/setup/js/claude_harness.test.cjs index ebc4d65494c..014ef09c75f 100644 --- a/actions/setup/js/claude_harness.test.cjs +++ b/actions/setup/js/claude_harness.test.cjs @@ -30,6 +30,12 @@ const { } = require("./claude_harness.cjs"); const agentTempDir = "/tmp/gh-aw/agent"; +const harnessChildEnv = { + ...process.env, + GH_AW_HARNESS_INITIAL_DELAY_MS: "1", + GH_AW_HARNESS_MAX_DELAY_MS: "1", + GH_AW_SKIP_REFLECT: "true", +}; function makeHarnessTempDir(name) { fs.mkdirSync(agentTempDir, { recursive: true }); @@ -46,7 +52,7 @@ function runHarnessWithStub({ stubScript, prompt = "fix the bug", extraArgs = [] const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", ...extraArgs, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), - env: { ...process.env, ...extraEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath }, + env: { ...harnessChildEnv, ...extraEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath }, encoding: "utf8", timeout: 45000, }); @@ -896,7 +902,7 @@ process.exit(0);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), - env: { ...process.env, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, + env: { ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, encoding: "utf8", timeout: 10000, }); @@ -928,7 +934,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), - env: { ...process.env, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, + env: { ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, encoding: "utf8", timeout: 10000, }); @@ -975,7 +981,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), - env: { ...process.env, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, + env: { ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, encoding: "utf8", timeout: 10000, }); @@ -1008,7 +1014,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), - env: { ...process.env, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, + env: { ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, encoding: "utf8", timeout: 10000, }); @@ -1040,7 +1046,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath, @@ -1077,7 +1083,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_MAX_RETRIES: "0", @@ -1112,7 +1118,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_MAX_RETRIES: "0", diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 3d20dd50e7f..fc99af63d09 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -71,6 +71,12 @@ const { const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard } = require("./harness_retry_guard.cjs"); const agentTempDir = "/tmp/gh-aw/agent"; +const harnessChildEnv = { + ...process.env, + GH_AW_HARNESS_INITIAL_DELAY_MS: "1", + GH_AW_HARNESS_MAX_DELAY_MS: "1", + GH_AW_SKIP_REFLECT: "true", +}; function makeHarnessTempDir(name) { fs.mkdirSync(agentTempDir, { recursive: true }); @@ -2617,7 +2623,7 @@ process.exit(0);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, encoding: "utf8", timeout: 10000, }); @@ -2649,7 +2655,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, encoding: "utf8", timeout: 10000, }); @@ -2686,7 +2692,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_SAFEOUTPUTS_CLI: "true", @@ -2728,7 +2734,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, encoding: "utf8", timeout: 15000, }); @@ -2760,7 +2766,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_SAFEOUTPUTS_CLI: "true" }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_SAFEOUTPUTS_CLI: "true" }, encoding: "utf8", timeout: 15000, }); @@ -2791,7 +2797,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath }, encoding: "utf8", timeout: 15000, }); @@ -2830,7 +2836,7 @@ setInterval(() => {}, 1000);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_WATCHDOG_TIMEOUT_MS: "100", @@ -2870,7 +2876,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, // Override retry config to keep the test fast. @@ -2914,7 +2920,7 @@ setInterval(() => {}, 1000);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_WATCHDOG_TIMEOUT_MS: "100", @@ -2958,7 +2964,7 @@ setInterval(() => {}, 1000);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_WATCHDOG_TIMEOUT_MS: "100", @@ -2996,7 +3002,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, }, @@ -3043,7 +3049,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, encoding: "utf8", timeout: 10000, }); @@ -3076,7 +3082,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...process.env, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath }, encoding: "utf8", timeout: 10000, }); @@ -3107,7 +3113,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_MAX_RETRIES: "0", @@ -3144,7 +3150,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_AGENT_OUTPUT: agentOutputPath, @@ -3178,7 +3184,7 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { - ...process.env, + ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_HARNESS_MAX_RETRIES: "0", diff --git a/actions/setup/js/generate_usage_activity_summary.cjs b/actions/setup/js/generate_usage_activity_summary.cjs index 20c31939bc5..9b0b801b795 100644 --- a/actions/setup/js/generate_usage_activity_summary.cjs +++ b/actions/setup/js/generate_usage_activity_summary.cjs @@ -11,7 +11,6 @@ // working_set: cumulative input-token traffic relative to peak invocation input const fs = require("fs"); -const { globSync } = require("node:fs"); const path = require("path"); const { readExperimentAssignments } = require("./experiment_helpers.cjs"); const { calculateWorkingSetFromJSONL } = require("./working_set_metrics.cjs"); @@ -29,6 +28,45 @@ const PLACEHOLDER_DEST_KEY = "-:-"; const ERROR_DOMAIN_PREFIX = "error:"; const AGENT_TOKEN_USAGE_PATH = "/tmp/gh-aw/usage/agent/token_usage.jsonl"; +function findFiles(rootDir, shouldIncludeFile, maxDepth = Number.POSITIVE_INFINITY, currentDepth = 0) { + if (!fs.existsSync(rootDir)) { + return []; + } + + const files = []; + let entries; + try { + entries = fs.readdirSync(rootDir, { withFileTypes: true }); + } catch { + return []; + } + + for (const entry of entries) { + const entryPath = path.join(rootDir, entry.name); + if (entry.isDirectory()) { + if (currentDepth < maxDepth) { + files.push(...findFiles(entryPath, shouldIncludeFile, maxDepth, currentDepth + 1)); + } + } else if (entry.isFile() && shouldIncludeFile(entry)) { + files.push(entryPath); + } + } + return files; +} + +function findPrefixedDirectories(parentDir, prefix) { + if (!fs.existsSync(parentDir)) { + return []; + } + let entries; + try { + entries = fs.readdirSync(parentDir, { withFileTypes: true }); + } catch { + return []; + } + return entries.filter(entry => entry.isDirectory() && entry.name.startsWith(prefix)).map(entry => path.join(parentDir, entry.name)); +} + /** * @param {string} [tokenUsagePath] * @returns {{ workingSet: ReturnType["workingSet"], ignoredRecords: number }} @@ -112,13 +150,15 @@ function parseFirewallLogs() { requests_by_domain: {}, }; - // The sandbox firewall logs may be emitted in nested directories (for example, - // api-proxy-logs/*.log), so these patterns are intentionally recursive. - const firewallPaths = ["/tmp/gh-aw/sandbox/firewall/logs/**/*.log", "/tmp/gh-aw/threat-detection/sandbox/firewall/logs/**/*.log", "/tmp/gh-aw/squid-logs-*/**/*.log", "/tmp/gh-aw/threat-detection/squid-logs-*/**/*.log"]; + const firewallLogDirs = [ + "/tmp/gh-aw/sandbox/firewall/logs", + "/tmp/gh-aw/threat-detection/sandbox/firewall/logs", + ...findPrefixedDirectories("/tmp/gh-aw", "squid-logs-"), + ...findPrefixedDirectories("/tmp/gh-aw/threat-detection", "squid-logs-"), + ]; - for (const pattern of firewallPaths) { - const files = globSync(pattern); - for (const logPath of files) { + for (const logDir of firewallLogDirs) { + for (const logPath of findFiles(logDir, entry => entry.name.endsWith(".log"))) { try { const content = fs.readFileSync(logPath, "utf-8"); const lines = content.split("\n"); @@ -222,7 +262,7 @@ function parseFirewallLogs() { /** * Parse Copilot session event logs and aggregate counters */ -function parseSessionLogs() { +function parseSessionLogs(sessionLogDirs = ["/tmp/gh-aw/sandbox/agent/logs/copilot-session-state", "/tmp/gh-aw/threat-detection/sandbox/agent/logs/copilot-session-state"]) { const session = { total_events: 0, session_starts: 0, @@ -235,11 +275,8 @@ function parseSessionLogs() { failed_tool_executions: 0, }; - const sessionPaths = ["/tmp/gh-aw/sandbox/agent/logs/copilot-session-state/*/events.jsonl", "/tmp/gh-aw/threat-detection/sandbox/agent/logs/copilot-session-state/*/events.jsonl"]; - - for (const pattern of sessionPaths) { - const files = globSync(pattern); - for (const eventsPath of files) { + for (const logDir of sessionLogDirs) { + for (const eventsPath of findFiles(logDir, entry => entry.name === "events.jsonl", 1)) { try { const content = fs.readFileSync(eventsPath, "utf-8"); const lines = content.split("\n"); diff --git a/actions/setup/js/generate_usage_activity_summary.test.cjs b/actions/setup/js/generate_usage_activity_summary.test.cjs index 6d6fac8c0a9..91508573a61 100644 --- a/actions/setup/js/generate_usage_activity_summary.test.cjs +++ b/actions/setup/js/generate_usage_activity_summary.test.cjs @@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const req = createRequire(import.meta.url); -const { parseFirewallLogs, parseSafeOutputsManifest, parseExperimentsData, calculateWorkingSetFromJSONL, parseWorkingSetMetrics, MANIFEST_FILE_PATH } = req("./generate_usage_activity_summary.cjs"); +const { parseFirewallLogs, parseSessionLogs, parseSafeOutputsManifest, parseExperimentsData, calculateWorkingSetFromJSONL, parseWorkingSetMetrics, MANIFEST_FILE_PATH } = req("./generate_usage_activity_summary.cjs"); describe("generate_usage_activity_summary.cjs", () => { /** Unique directory for each test to avoid cross-test interference */ @@ -96,6 +96,26 @@ describe("generate_usage_activity_summary.cjs", () => { }); }); + describe("parseSessionLogs", () => { + it("matches events.jsonl one directory below the session-state directory", () => { + const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), "session-logs-test-")); + try { + fs.mkdirSync(path.join(sessionRoot, "session-1"), { recursive: true }); + fs.writeFileSync(path.join(sessionRoot, "session-1", "events.jsonl"), `${JSON.stringify({ type: "session.start" })}\n`); + fs.mkdirSync(path.join(sessionRoot, "nested", "too-deep"), { recursive: true }); + fs.writeFileSync(path.join(sessionRoot, "nested", "too-deep", "events.jsonl"), `${JSON.stringify({ type: "assistant.message" })}\n`); + + expect(parseSessionLogs([sessionRoot])).toMatchObject({ + total_events: 1, + session_starts: 1, + assistant_messages: 0, + }); + } finally { + fs.rmSync(sessionRoot, { recursive: true, force: true }); + } + }); + }); + describe("parseSafeOutputsManifest", () => { /** Unique manifest file path per test to avoid cross-test interference */ let manifestPath; diff --git a/actions/setup/js/notify_comment_error.test.cjs b/actions/setup/js/notify_comment_error.test.cjs index 6bf3d60d8d4..4f2db5df845 100644 --- a/actions/setup/js/notify_comment_error.test.cjs +++ b/actions/setup/js/notify_comment_error.test.cjs @@ -1,6 +1,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "fs"; import path from "path"; +import { syncRuntimePromptTemplates } from "./test_prompt_templates.js"; + +const { runtimePromptsDir } = syncRuntimePromptTemplates(import.meta.url); const { ERR_VALIDATION } = require("./error_codes.cjs"); const mockCore = { debug: vi.fn(), @@ -48,6 +51,7 @@ const mockCore = { (originalEnv = { GH_AW_COMMENT_ID: process.env.GH_AW_COMMENT_ID, GH_AW_COMMENT_REPO: process.env.GH_AW_COMMENT_REPO, + GH_AW_PROMPTS_DIR: process.env.GH_AW_PROMPTS_DIR, GH_AW_RUN_URL: process.env.GH_AW_RUN_URL, GH_AW_WORKFLOW_NAME: process.env.GH_AW_WORKFLOW_NAME, GH_AW_AGENT_CONCLUSION: process.env.GH_AW_AGENT_CONCLUSION, @@ -60,7 +64,8 @@ const mockCore = { GH_AW_OUTPUT_CREATE_ISSUE_ISSUE_URL: process.env.GH_AW_OUTPUT_CREATE_ISSUE_ISSUE_URL, GH_AW_OUTPUT_ADD_COMMENT_COMMENT_URL: process.env.GH_AW_OUTPUT_ADD_COMMENT_COMMENT_URL, GH_AW_OUTPUT_CREATE_PULL_REQUEST_PULL_REQUEST_URL: process.env.GH_AW_OUTPUT_CREATE_PULL_REQUEST_PULL_REQUEST_URL, - })); + }), + (process.env.GH_AW_PROMPTS_DIR = runtimePromptsDir)); const scriptPath = path.join(process.cwd(), "notify_comment_error.cjs"); notifyCommentScript = fs.readFileSync(scriptPath, "utf8"); }), diff --git a/actions/setup/js/package-lock.json b/actions/setup/js/package-lock.json index f5844e065e8..dc9cad88da9 100644 --- a/actions/setup/js/package-lock.json +++ b/actions/setup/js/package-lock.json @@ -643,9 +643,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -663,9 +660,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -683,9 +677,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -703,9 +694,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -765,24 +753,6 @@ "copilot-win32-x64": "copilot.exe" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1397,9 +1367,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1417,9 +1384,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1437,9 +1401,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1457,9 +1418,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1477,9 +1435,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1497,9 +1452,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2174,9 +2126,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { @@ -2250,6 +2202,112 @@ "node": ">= 14" } }, + "node_modules/archiver-utils/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/archiver-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2865,54 +2923,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3069,22 +3079,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -3326,9 +3320,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3350,9 +3341,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3374,9 +3362,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3398,9 +3383,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3463,13 +3445,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3647,23 +3622,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", diff --git a/actions/setup/js/safe_outputs_mcp_server_defaults.test.cjs b/actions/setup/js/safe_outputs_mcp_server_defaults.test.cjs index e1d0d144e8e..eef109bbf1f 100644 --- a/actions/setup/js/safe_outputs_mcp_server_defaults.test.cjs +++ b/actions/setup/js/safe_outputs_mcp_server_defaults.test.cjs @@ -1,8 +1,22 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest"; import fs from "fs"; +import os from "os"; import path from "path"; import { spawn } from "child_process"; +const originalRunnerTemp = process.env.RUNNER_TEMP; +const localRunnerTemp = originalRunnerTemp ? null : fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-runner-temp-")); +if (localRunnerTemp) { + process.env.RUNNER_TEMP = localRunnerTemp; +} + +afterAll(() => { + if (localRunnerTemp) { + fs.rmSync(localRunnerTemp, { recursive: true, force: true }); + delete process.env.RUNNER_TEMP; + } +}); + // Check if ${RUNNER_TEMP}/gh-aw/safeoutputs is writable (only available in agent container) function canWriteToDefaultPath() { try { diff --git a/pkg/cli/context_cancellation_test.go b/pkg/cli/context_cancellation_test.go index 6e81480c55e..176bf5e114a 100644 --- a/pkg/cli/context_cancellation_test.go +++ b/pkg/cli/context_cancellation_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestRunWorkflowOnGitHubWithCancellation tests that RunWorkflowOnGitHub respects context cancellation @@ -110,19 +111,26 @@ func TestRunWorkflowsOnGitHubCancellationDuringExecution(t *testing.T) { // TestDownloadWorkflowLogsTimeoutRespected tests that timeout-minutes is respected func TestDownloadWorkflowLogsTimeoutRespected(t *testing.T) { - // Use a short timeout in minutes and verify fast-fail behavior still returns quickly - ctx := context.Background() + originalFetch := logsFetchWorkflowRunBatch + t.Cleanup(func() { + logsFetchWorkflowRunBatch = originalFetch + }) + logsFetchWorkflowRunBatch = func(ctx context.Context, _ LogsDownloadOptions, _ string, _ int, _ bool) (workflowRunBatch, error) { + <-ctx.Done() + return workflowRunBatch{}, ctx.Err() + } start := time.Now() - // Use a workflow name that doesn't exist to avoid actual network calls - _ = DownloadWorkflowLogs(ctx, LogsDownloadOptions{ - WorkflowName: "nonexistent-workflow-12345", + err := DownloadWorkflowLogs(context.Background(), LogsDownloadOptions{ + WorkflowName: "test-workflow", Count: 100, - OutputDir: "/tmp/test-logs", + OutputDir: t.TempDir(), TimeoutMinutes: 1, + TimeoutSeconds: 1, }) elapsed := time.Since(start) - // Should complete within reasonable time (give 5 seconds buffer for test overhead) - assert.Less(t, elapsed, 5*time.Second, "Should complete quickly when workflow doesn't exist") + require.NoError(t, err) + assert.GreaterOrEqual(t, elapsed, time.Second, "Should wait for the configured timeout") + assert.Less(t, elapsed, 3*time.Second, "Should stop promptly after the configured timeout") } diff --git a/pkg/cli/install_copilot_cli_test.go b/pkg/cli/install_copilot_cli_test.go index 39d28d3c738..fd5b9f79091 100644 --- a/pkg/cli/install_copilot_cli_test.go +++ b/pkg/cli/install_copilot_cli_test.go @@ -95,12 +95,21 @@ func TestInstallCopilotCLIScriptPreservesCachedBinaryAtInstallPath(t *testing.T) cachedCopilot := filepath.Join(toolcacheBin, "copilot") cachedContents := []byte("#!/usr/bin/env bash\necho 'copilot 1.2.3 preserved'\n") require.NoError(t, os.WriteFile(cachedCopilot, cachedContents, 0o755)) + fakeBinDir := filepath.Join(tempDir, "fake-bin") + require.NoError(t, os.MkdirAll(fakeBinDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(fakeBinDir, "sudo"), []byte(`#!/usr/bin/env bash +if [ "${1:-}" = "chown" ]; then + exit 0 +fi +exec "$@" +`), 0o755)) cmd := exec.Command("bash", installScript, "1.2.3") cmd.Env = append(os.Environ(), "RUNNER_TOOL_CACHE="+filepath.Join(tempDir, "toolcache"), "GITHUB_PATH="+filepath.Join(tempDir, "github-path"), "COPILOT_INSTALL_DIR="+toolcacheBin, + "PATH="+fakeBinDir+":"+os.Getenv("PATH"), ) output, err := cmd.CombinedOutput() diff --git a/scripts/agent-report-progress.sh b/scripts/agent-report-progress.sh index 77c94b0c2cb..ef39a7ca555 100755 --- a/scripts/agent-report-progress.sh +++ b/scripts/agent-report-progress.sh @@ -157,10 +157,96 @@ make --no-print-directory build mapfile -t go_packages < <(printf '%s\n' "${go_packages[@]}" | sed '/^$/d' | LC_ALL=C sort -u) +normalize_repo_path() { + local file="$1" + file="${file#"$PWD"/}" + file="${file#./}" + printf '%s\n' "$file" +} + +is_changed_go_file() { + local diagnostic_file + local changed_file + + diagnostic_file=$(normalize_repo_path "$1") + for changed_file in "${go_files[@]}"; do + if [ "$diagnostic_file" = "$(normalize_repo_path "$changed_file")" ]; then + return 0 + fi + done + return 1 +} + +is_linter_summary_line() { + local line="$1" + [[ "$line" =~ ^[0-9]+[[:space:]]issues?:$ ]] || + [[ "$line" =~ ^\*[[:space:]][^:]+:[[:space:]][0-9]+$ ]] || + [[ "$line" = "Building custom linters..." ]] || + [[ "$line" = "Running custom linters (largefunc max-lines=60)..." ]] || + [[ "$line" =~ ^make\[[0-9]+\]:[[:space:]]\*\*\*[[:space:]]\[Makefile:[0-9]+:[[:space:]]golint-custom\][[:space:]]Error[[:space:]][0-9]+$ ]] +} + +run_change_scoped_go_linter() { + local label="$1" + shift + local output + local status + local diagnostic_found=0 + local relevant_diagnostic_found=0 + local non_diagnostic_failure=0 + local last_diagnostic_relevant=0 + local diagnostic_detail_lines_remaining=0 + local line + + set +e + output=$("$@" 2>&1) + status=$? + set -e + + if [ "$status" -eq 0 ]; then + printf '%s\n' "$output" + return 0 + fi + + while IFS= read -r line; do + if [[ "$line" =~ ^([^:]+\.go):[0-9]+:[0-9]+: ]]; then + diagnostic_found=1 + if is_changed_go_file "${BASH_REMATCH[1]}"; then + printf '%s\n' "$line" + relevant_diagnostic_found=1 + last_diagnostic_relevant=1 + else + last_diagnostic_relevant=0 + fi + diagnostic_detail_lines_remaining=2 + elif [ "$diagnostic_detail_lines_remaining" -gt 0 ]; then + if [ "$last_diagnostic_relevant" -eq 1 ]; then + printf '%s\n' "$line" + fi + diagnostic_detail_lines_remaining=$((diagnostic_detail_lines_remaining - 1)) + elif [[ "$line" =~ ^[[:space:]] || "$line" =~ ^[[:space:]]*\^+$ ]]; then + continue + elif is_linter_summary_line "$line"; then + continue + else + printf '%s\n' "$line" + non_diagnostic_failure=1 + last_diagnostic_relevant=0 + fi + done <<< "$output" + + if [ "$relevant_diagnostic_found" -eq 1 ] || [ "$non_diagnostic_failure" -eq 1 ] || [ "$diagnostic_found" -eq 0 ]; then + return "$status" + fi + + echo "$label diagnostics were limited to unchanged files; skipping them for this change-scoped gate." + return 0 +} + lint_go_packages() { GOPATH=$(go env GOPATH) if command -v golangci-lint >/dev/null 2>&1 || [ -x "$GOPATH/bin/golangci-lint" ]; then - PATH="$GOPATH/bin:$PATH" golangci-lint run "${go_packages[@]}" + run_change_scoped_go_linter "Go linter" env PATH="$GOPATH/bin:$PATH" golangci-lint run "${go_packages[@]}" else echo "golangci-lint is not installed. Run 'make deps-dev' to install dependencies." >&2 return 1 @@ -168,7 +254,7 @@ lint_go_packages() { } lint_custom_go_packages() { - make --no-print-directory golint-custom LINTER_PACKAGES="${go_packages[*]}" + run_change_scoped_go_linter "Custom Go linter" make --no-print-directory golint-custom LINTER_PACKAGES="${go_packages[*]}" } lint_javascript() {