From 6d55cacb8097bd4d1f432576d4e8d93694622af2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:36:12 +0000 Subject: [PATCH 1/5] feat: harden activity report log download timeout and cleanup Agent-Logs-Url: https://github.com/github/gh-aw/sessions/36652c5b-9bcd-47c8-82c8-9bc21ab30538 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 1 + actions/setup/js/run_activity_report.cjs | 116 +++++++++++++++++- actions/setup/js/run_activity_report.test.cjs | 28 ++--- pkg/workflow/maintenance_workflow_test.go | 3 + pkg/workflow/maintenance_workflow_yaml.go | 1 + pkg/workflow/side_repo_maintenance.go | 1 + .../side_repo_maintenance_integration_test.go | 2 + 7 files changed, 132 insertions(+), 20 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 95c05e7fd29..657ef168da3 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -360,6 +360,7 @@ jobs: ${{ runner.os }}-activity-report-logs-${{ github.repository }}- ${{ runner.os }}-activity-report-logs- - name: Generate agentic workflow activity report + timeout-minutes: 20 uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs index 27cd0cb920a..5fc04456a3b 100644 --- a/actions/setup/js/run_activity_report.cjs +++ b/actions/setup/js/run_activity_report.cjs @@ -4,11 +4,14 @@ const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs"); const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); +const { spawn } = require("node:child_process"); const ISSUE_TITLE = "[aw] agentic status report"; const REPORT_COUNT = 1000; const HEADING_DEMOTION_LEVELS = 2; const DEFAULT_REPORT_OUTPUT_DIR = "./.cache/gh-aw/activity-report-logs"; +const LOG_DOWNLOAD_TIMEOUT_MS = 20 * 60 * 1000; +const POST_DOWNLOAD_SETTLE_DELAY_MS = 10 * 1000; /** @typedef {{ key: string, heading: string, startDate: string, optionalOnRateLimit: boolean }} ActivityRange */ @@ -36,12 +39,16 @@ function hasRateLimitText(text) { * @param {string} outputDir * @returns {Promise<{ heading: string, body: string }>} */ -async function runRangeReport(bin, prefixArgs, repoSlug, range, outputDir) { +async function runRangeReport(bin, prefixArgs, repoSlug, range, outputDir, options = {}) { + const commandRunner = options.commandRunner || runCommandWithTimeout; + const sleepFn = options.sleepFn || sleep; + const timeoutMs = options.timeoutMs || LOG_DOWNLOAD_TIMEOUT_MS; + const settleDelayMs = options.settleDelayMs || POST_DOWNLOAD_SETTLE_DELAY_MS; const args = [...prefixArgs, "logs", "--repo", repoSlug, "--start-date", range.startDate, "--count", String(REPORT_COUNT), "--output", outputDir, "--format", "markdown"]; core.info(`Running: ${bin} ${args.join(" ")}`); try { - const result = await exec.getExecOutput(bin, args, { ignoreReturnCode: true }); + const result = await commandRunner(bin, args, timeoutMs); const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim(); const rateLimited = hasRateLimitText(output); @@ -94,9 +101,101 @@ async function runRangeReport(bin, prefixArgs, repoSlug, range, outputDir) { heading: range.heading, body: `_Report command failed: ${sanitizeContent(errorMessage)}_`, }; + } finally { + core.info(`Waiting ${Math.floor(settleDelayMs / 1000)}s for log copy operations to settle`); + await sleepFn(settleDelayMs); } } +/** + * Execute command with timeout and process lifecycle controls. + * + * @param {string} command + * @param {string[]} args + * @param {number} timeoutMs + * @returns {Promise<{ exitCode: number, stdout: string, stderr: string }>} + */ +function runCommandWithTimeout(command, args, timeoutMs) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: ["ignore", "pipe", "pipe"], + shell: false, + env: process.env, + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + let settled = false; + const childPid = child.pid; + core.info(`Started log download process with PID ${String(childPid || "unknown")}`); + + const killTimer = setTimeout(() => { + timedOut = true; + + if (!childPid) { + core.warning(`Log download exceeded ${Math.floor(timeoutMs / 1000)}s timeout and has no PID to kill`); + return; + } + + core.warning(`Log download exceeded ${Math.floor(timeoutMs / 1000)}s timeout; sending SIGTERM to gh aw PID ${childPid}`); + try { + process.kill(childPid, "SIGTERM"); + } catch (error) { + core.warning(`Could not SIGTERM PID ${childPid}: ${getErrorMessage(error)}`); + } + + setTimeout(() => { + if (settled) { + return; + } + + core.warning(`gh aw PID ${childPid} still running; sending SIGKILL`); + try { + process.kill(childPid, "SIGKILL"); + } catch (error) { + core.warning(`Could not SIGKILL PID ${childPid}: ${getErrorMessage(error)}`); + } + }, 5000); + }, timeoutMs); + + child.stdout.on("data", chunk => { + stdout += chunk.toString(); + }); + child.stderr.on("data", chunk => { + stderr += chunk.toString(); + }); + child.on("error", error => { + if (settled) { + return; + } + settled = true; + clearTimeout(killTimer); + reject(error); + }); + child.on("close", code => { + if (settled) { + return; + } + settled = true; + clearTimeout(killTimer); + resolve({ + exitCode: typeof code === "number" ? code : 1, + stdout, + stderr: timedOut ? `${stderr}\nProcess timed out after ${Math.floor(timeoutMs / 1000)}s and was terminated.`.trim() : stderr, + }); + }); + }); +} + +/** + * @param {number} ms + * @returns {Promise} + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + /** * Normalize report markdown for issue rendering. * Demotes headings so top-level report headings start at H3. @@ -116,7 +215,7 @@ function normalizeReportMarkdown(markdown) { * Generate an agentic workflow activity report issue. * @returns {Promise} */ -async function main() { +async function main(options = {}) { const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw"; const reportOutputDir = process.env.GH_AW_ACTIVITY_REPORT_OUTPUT_DIR || DEFAULT_REPORT_OUTPUT_DIR; const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean); @@ -127,7 +226,14 @@ async function main() { const sections = []; for (const range of REPORT_RANGES) { - sections.push(await runRangeReport(bin, prefixArgs, repoSlug, range, reportOutputDir)); + sections.push( + await runRangeReport(bin, prefixArgs, repoSlug, range, reportOutputDir, { + commandRunner: options.commandRunner, + sleepFn: options.sleepFn, + timeoutMs: options.timeoutMs, + settleDelayMs: options.settleDelayMs, + }) + ); } const headerLines = ["### Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, ""]; @@ -145,4 +251,4 @@ async function main() { core.info(`Created issue #${createdIssue.data.number}: ${createdIssue.data.html_url}`); } -module.exports = { main, hasRateLimitText, runRangeReport, normalizeReportMarkdown }; +module.exports = { main, hasRateLimitText, runRangeReport, normalizeReportMarkdown, runCommandWithTimeout, sleep }; diff --git a/actions/setup/js/run_activity_report.test.cjs b/actions/setup/js/run_activity_report.test.cjs index e858ee64d48..de244223e2c 100644 --- a/actions/setup/js/run_activity_report.test.cjs +++ b/actions/setup/js/run_activity_report.test.cjs @@ -7,7 +7,8 @@ describe("run_activity_report", () => { let mockCore; let mockGithub; let mockContext; - let mockExec; + let mockCommandRunner; + let mockSleepFn; beforeEach(() => { originalEnv = { ...process.env }; @@ -17,8 +18,7 @@ describe("run_activity_report", () => { core: global.core, github: global.github, context: global.context, - exec: global.exec, - }; + }; mockCore = { info: vi.fn(), @@ -39,14 +39,12 @@ describe("run_activity_report", () => { repo: "testrepo", }, }; - mockExec = { - getExecOutput: vi.fn(), - }; + mockCommandRunner = vi.fn(); + mockSleepFn = vi.fn().mockResolvedValue(); global.core = mockCore; global.github = mockGithub; global.context = mockContext; - global.exec = mockExec; }); afterEach(() => { @@ -54,29 +52,29 @@ describe("run_activity_report", () => { global.core = originalGlobals.core; global.github = originalGlobals.github; global.context = originalGlobals.context; - global.exec = originalGlobals.exec; vi.clearAllMocks(); }); it("creates an activity report issue with 24h and 7d time ranges", async () => { - mockExec.getExecOutput.mockResolvedValueOnce({ stdout: "## 24h report\nok", stderr: "", exitCode: 0 }).mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 }); + mockCommandRunner.mockResolvedValueOnce({ stdout: "## 24h report\nok", stderr: "", exitCode: 0 }).mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 }); const { main } = await import("./run_activity_report.cjs"); - await main(); + await main({ commandRunner: mockCommandRunner, sleepFn: mockSleepFn, settleDelayMs: 1 }); - expect(mockExec.getExecOutput).toHaveBeenCalledTimes(2); - expect(mockExec.getExecOutput).toHaveBeenNthCalledWith( + expect(mockCommandRunner).toHaveBeenCalledTimes(2); + expect(mockCommandRunner).toHaveBeenNthCalledWith( 1, "gh", expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1d", "--count", "1000", "--output", "./.cache/gh-aw/activity-report-logs", "--format", "markdown"]), - expect.objectContaining({ ignoreReturnCode: true }) + 1200000 ); - expect(mockExec.getExecOutput).toHaveBeenNthCalledWith( + expect(mockCommandRunner).toHaveBeenNthCalledWith( 2, "gh", expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1w", "--count", "1000", "--output", "./.cache/gh-aw/activity-report-logs", "--format", "markdown"]), - expect.objectContaining({ ignoreReturnCode: true }) + 1200000 ); + expect(mockSleepFn).toHaveBeenCalledTimes(2); expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( expect.objectContaining({ owner: "testowner", diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 0ec6a3cb3ca..d47b21d77cd 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -403,6 +403,9 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { if !strings.Contains(yaml, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs") { t.Errorf("Job activity_report should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR in:\n%s", yaml) } + if !strings.Contains(yaml, "Generate agentic workflow activity report\n timeout-minutes: 20") { + t.Errorf("Job activity_report report generation step should set timeout-minutes: 20 in:\n%s", yaml) + } // close_agentic_workflows_issues job should be triggered when operation == 'close_agentic_workflows_issues' closeAgenticWorkflowIssuesIdx := strings.Index(yaml, "\n close_agentic_workflows_issues:") diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index dd55cc3ffec..7de5448be42 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -395,6 +395,7 @@ jobs: ${{ runner.os }}-activity-report-logs- `) yaml.WriteString(` - name: Generate agentic workflow activity report + timeout-minutes: 20 uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index add1c5d87e5..db3981d5ab4 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -470,6 +470,7 @@ jobs: ${{ runner.os }}-activity-report-logs- `) yaml.WriteString(` - name: Generate agentic workflow activity report in target repository + timeout-minutes: 20 uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: GH_TOKEN: ` + token + ` diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index fe9ff6905fe..d9922eb1d56 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -100,6 +100,8 @@ This workflow operates on a separate repository. "generated workflow should save activity_report logs using the cache primary key") assert.Contains(t, contentStr, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs", "generated workflow should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR for activity_report logs") + assert.Contains(t, contentStr, "Generate agentic workflow activity report in target repository\n timeout-minutes: 20", + "generated workflow should set a 20-minute timeout for the activity_report generation step") assert.Contains(t, contentStr, "actions: read\n contents: read\n issues: write", "activity_report job should include contents: read with explicit permissions") assert.Contains(t, contentStr, "timeout-minutes: 120", From 699b83528f796921eb50bdcf661fa4ab857e2522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:39:43 +0000 Subject: [PATCH 2/5] test: format activity report test updates Agent-Logs-Url: https://github.com/github/gh-aw/sessions/36652c5b-9bcd-47c8-82c8-9bc21ab30538 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_activity_report.test.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/run_activity_report.test.cjs b/actions/setup/js/run_activity_report.test.cjs index de244223e2c..0b78a712bae 100644 --- a/actions/setup/js/run_activity_report.test.cjs +++ b/actions/setup/js/run_activity_report.test.cjs @@ -18,7 +18,7 @@ describe("run_activity_report", () => { core: global.core, github: global.github, context: global.context, - }; + }; mockCore = { info: vi.fn(), From 091674e6194d10a943a05693cfb361ac7e2b3eb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:46:22 +0000 Subject: [PATCH 3/5] test: stabilize timeout assertions and polish activity report runner Agent-Logs-Url: https://github.com/github/gh-aw/sessions/36652c5b-9bcd-47c8-82c8-9bc21ab30538 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_activity_report.cjs | 4 ++-- pkg/workflow/maintenance_workflow_test.go | 5 ++++- pkg/workflow/side_repo_maintenance_integration_test.go | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs index 5fc04456a3b..a477d9d40f3 100644 --- a/actions/setup/js/run_activity_report.cjs +++ b/actions/setup/js/run_activity_report.cjs @@ -128,7 +128,7 @@ function runCommandWithTimeout(command, args, timeoutMs) { let timedOut = false; let settled = false; const childPid = child.pid; - core.info(`Started log download process with PID ${String(childPid || "unknown")}`); + core.info(`Started log download process with PID ${childPid || "unknown"}`); const killTimer = setTimeout(() => { timedOut = true; @@ -182,7 +182,7 @@ function runCommandWithTimeout(command, args, timeoutMs) { resolve({ exitCode: typeof code === "number" ? code : 1, stdout, - stderr: timedOut ? `${stderr}\nProcess timed out after ${Math.floor(timeoutMs / 1000)}s and was terminated.`.trim() : stderr, + stderr: timedOut ? `${stderr.trim()}\nProcess timed out after ${Math.floor(timeoutMs / 1000)}s and was terminated.`.trim() : stderr.trim(), }); }); }); diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index d47b21d77cd..8afb115ccc3 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -403,7 +403,10 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { if !strings.Contains(yaml, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs") { t.Errorf("Job activity_report should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR in:\n%s", yaml) } - if !strings.Contains(yaml, "Generate agentic workflow activity report\n timeout-minutes: 20") { + if !strings.Contains(yaml, "Generate agentic workflow activity report") { + t.Errorf("Job activity_report should include report generation step in:\n%s", yaml) + } + if !strings.Contains(yaml, "timeout-minutes: 20") { t.Errorf("Job activity_report report generation step should set timeout-minutes: 20 in:\n%s", yaml) } diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index d9922eb1d56..76384dcdc97 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -100,7 +100,9 @@ This workflow operates on a separate repository. "generated workflow should save activity_report logs using the cache primary key") assert.Contains(t, contentStr, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs", "generated workflow should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR for activity_report logs") - assert.Contains(t, contentStr, "Generate agentic workflow activity report in target repository\n timeout-minutes: 20", + assert.Contains(t, contentStr, "Generate agentic workflow activity report in target repository", + "generated workflow should include the activity_report generation step") + assert.Contains(t, contentStr, "timeout-minutes: 20", "generated workflow should set a 20-minute timeout for the activity_report generation step") assert.Contains(t, contentStr, "actions: read\n contents: read\n issues: write", "activity_report job should include contents: read with explicit permissions") From 89313c79cdb1742ef7cba554d4eea8306e708205 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 04:27:44 +0000 Subject: [PATCH 4/5] refactor: replace activity_report wrapper with direct gh aw logs call Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c20c68a3-5880-45d7-b66c-eb70d9a4eb50 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 17 ++- actions/setup/js/run_activity_report.cjs | 116 +----------------- actions/setup/js/run_activity_report.test.cjs | 26 ++-- pkg/workflow/maintenance_workflow_test.go | 18 ++- pkg/workflow/maintenance_workflow_yaml.go | 17 ++- pkg/workflow/side_repo_maintenance.go | 17 ++- .../side_repo_maintenance_integration_test.go | 14 ++- 7 files changed, 61 insertions(+), 164 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 657ef168da3..e9bb22b4f16 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -359,20 +359,17 @@ jobs: restore-keys: | ${{ runner.os }}-activity-report-logs-${{ github.repository }}- ${{ runner.os }}-activity-report-logs- - - name: Generate agentic workflow activity report + - name: Download activity report logs timeout-minutes: 20 - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_AW_CMD_PREFIX: ./gh-aw - GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/run_activity_report.cjs'); - await main(); + run: | + ${GH_AW_CMD_PREFIX} logs \ + --repo "${{ github.repository }}" \ + --start-date -1w \ + --count 100 \ + --output ./.cache/gh-aw/activity-report-logs - name: Save activity report logs cache if: ${{ always() }} diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs index a477d9d40f3..27cd0cb920a 100644 --- a/actions/setup/js/run_activity_report.cjs +++ b/actions/setup/js/run_activity_report.cjs @@ -4,14 +4,11 @@ const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs"); const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); -const { spawn } = require("node:child_process"); const ISSUE_TITLE = "[aw] agentic status report"; const REPORT_COUNT = 1000; const HEADING_DEMOTION_LEVELS = 2; const DEFAULT_REPORT_OUTPUT_DIR = "./.cache/gh-aw/activity-report-logs"; -const LOG_DOWNLOAD_TIMEOUT_MS = 20 * 60 * 1000; -const POST_DOWNLOAD_SETTLE_DELAY_MS = 10 * 1000; /** @typedef {{ key: string, heading: string, startDate: string, optionalOnRateLimit: boolean }} ActivityRange */ @@ -39,16 +36,12 @@ function hasRateLimitText(text) { * @param {string} outputDir * @returns {Promise<{ heading: string, body: string }>} */ -async function runRangeReport(bin, prefixArgs, repoSlug, range, outputDir, options = {}) { - const commandRunner = options.commandRunner || runCommandWithTimeout; - const sleepFn = options.sleepFn || sleep; - const timeoutMs = options.timeoutMs || LOG_DOWNLOAD_TIMEOUT_MS; - const settleDelayMs = options.settleDelayMs || POST_DOWNLOAD_SETTLE_DELAY_MS; +async function runRangeReport(bin, prefixArgs, repoSlug, range, outputDir) { const args = [...prefixArgs, "logs", "--repo", repoSlug, "--start-date", range.startDate, "--count", String(REPORT_COUNT), "--output", outputDir, "--format", "markdown"]; core.info(`Running: ${bin} ${args.join(" ")}`); try { - const result = await commandRunner(bin, args, timeoutMs); + const result = await exec.getExecOutput(bin, args, { ignoreReturnCode: true }); const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim(); const rateLimited = hasRateLimitText(output); @@ -101,101 +94,9 @@ async function runRangeReport(bin, prefixArgs, repoSlug, range, outputDir, optio heading: range.heading, body: `_Report command failed: ${sanitizeContent(errorMessage)}_`, }; - } finally { - core.info(`Waiting ${Math.floor(settleDelayMs / 1000)}s for log copy operations to settle`); - await sleepFn(settleDelayMs); } } -/** - * Execute command with timeout and process lifecycle controls. - * - * @param {string} command - * @param {string[]} args - * @param {number} timeoutMs - * @returns {Promise<{ exitCode: number, stdout: string, stderr: string }>} - */ -function runCommandWithTimeout(command, args, timeoutMs) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - stdio: ["ignore", "pipe", "pipe"], - shell: false, - env: process.env, - }); - - let stdout = ""; - let stderr = ""; - let timedOut = false; - let settled = false; - const childPid = child.pid; - core.info(`Started log download process with PID ${childPid || "unknown"}`); - - const killTimer = setTimeout(() => { - timedOut = true; - - if (!childPid) { - core.warning(`Log download exceeded ${Math.floor(timeoutMs / 1000)}s timeout and has no PID to kill`); - return; - } - - core.warning(`Log download exceeded ${Math.floor(timeoutMs / 1000)}s timeout; sending SIGTERM to gh aw PID ${childPid}`); - try { - process.kill(childPid, "SIGTERM"); - } catch (error) { - core.warning(`Could not SIGTERM PID ${childPid}: ${getErrorMessage(error)}`); - } - - setTimeout(() => { - if (settled) { - return; - } - - core.warning(`gh aw PID ${childPid} still running; sending SIGKILL`); - try { - process.kill(childPid, "SIGKILL"); - } catch (error) { - core.warning(`Could not SIGKILL PID ${childPid}: ${getErrorMessage(error)}`); - } - }, 5000); - }, timeoutMs); - - child.stdout.on("data", chunk => { - stdout += chunk.toString(); - }); - child.stderr.on("data", chunk => { - stderr += chunk.toString(); - }); - child.on("error", error => { - if (settled) { - return; - } - settled = true; - clearTimeout(killTimer); - reject(error); - }); - child.on("close", code => { - if (settled) { - return; - } - settled = true; - clearTimeout(killTimer); - resolve({ - exitCode: typeof code === "number" ? code : 1, - stdout, - stderr: timedOut ? `${stderr.trim()}\nProcess timed out after ${Math.floor(timeoutMs / 1000)}s and was terminated.`.trim() : stderr.trim(), - }); - }); - }); -} - -/** - * @param {number} ms - * @returns {Promise} - */ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - /** * Normalize report markdown for issue rendering. * Demotes headings so top-level report headings start at H3. @@ -215,7 +116,7 @@ function normalizeReportMarkdown(markdown) { * Generate an agentic workflow activity report issue. * @returns {Promise} */ -async function main(options = {}) { +async function main() { const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw"; const reportOutputDir = process.env.GH_AW_ACTIVITY_REPORT_OUTPUT_DIR || DEFAULT_REPORT_OUTPUT_DIR; const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean); @@ -226,14 +127,7 @@ async function main(options = {}) { const sections = []; for (const range of REPORT_RANGES) { - sections.push( - await runRangeReport(bin, prefixArgs, repoSlug, range, reportOutputDir, { - commandRunner: options.commandRunner, - sleepFn: options.sleepFn, - timeoutMs: options.timeoutMs, - settleDelayMs: options.settleDelayMs, - }) - ); + sections.push(await runRangeReport(bin, prefixArgs, repoSlug, range, reportOutputDir)); } const headerLines = ["### Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, ""]; @@ -251,4 +145,4 @@ async function main(options = {}) { core.info(`Created issue #${createdIssue.data.number}: ${createdIssue.data.html_url}`); } -module.exports = { main, hasRateLimitText, runRangeReport, normalizeReportMarkdown, runCommandWithTimeout, sleep }; +module.exports = { main, hasRateLimitText, runRangeReport, normalizeReportMarkdown }; diff --git a/actions/setup/js/run_activity_report.test.cjs b/actions/setup/js/run_activity_report.test.cjs index 0b78a712bae..e858ee64d48 100644 --- a/actions/setup/js/run_activity_report.test.cjs +++ b/actions/setup/js/run_activity_report.test.cjs @@ -7,8 +7,7 @@ describe("run_activity_report", () => { let mockCore; let mockGithub; let mockContext; - let mockCommandRunner; - let mockSleepFn; + let mockExec; beforeEach(() => { originalEnv = { ...process.env }; @@ -18,6 +17,7 @@ describe("run_activity_report", () => { core: global.core, github: global.github, context: global.context, + exec: global.exec, }; mockCore = { @@ -39,12 +39,14 @@ describe("run_activity_report", () => { repo: "testrepo", }, }; - mockCommandRunner = vi.fn(); - mockSleepFn = vi.fn().mockResolvedValue(); + mockExec = { + getExecOutput: vi.fn(), + }; global.core = mockCore; global.github = mockGithub; global.context = mockContext; + global.exec = mockExec; }); afterEach(() => { @@ -52,29 +54,29 @@ describe("run_activity_report", () => { global.core = originalGlobals.core; global.github = originalGlobals.github; global.context = originalGlobals.context; + global.exec = originalGlobals.exec; vi.clearAllMocks(); }); it("creates an activity report issue with 24h and 7d time ranges", async () => { - mockCommandRunner.mockResolvedValueOnce({ stdout: "## 24h report\nok", stderr: "", exitCode: 0 }).mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 }); + mockExec.getExecOutput.mockResolvedValueOnce({ stdout: "## 24h report\nok", stderr: "", exitCode: 0 }).mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 }); const { main } = await import("./run_activity_report.cjs"); - await main({ commandRunner: mockCommandRunner, sleepFn: mockSleepFn, settleDelayMs: 1 }); + await main(); - expect(mockCommandRunner).toHaveBeenCalledTimes(2); - expect(mockCommandRunner).toHaveBeenNthCalledWith( + expect(mockExec.getExecOutput).toHaveBeenCalledTimes(2); + expect(mockExec.getExecOutput).toHaveBeenNthCalledWith( 1, "gh", expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1d", "--count", "1000", "--output", "./.cache/gh-aw/activity-report-logs", "--format", "markdown"]), - 1200000 + expect.objectContaining({ ignoreReturnCode: true }) ); - expect(mockCommandRunner).toHaveBeenNthCalledWith( + expect(mockExec.getExecOutput).toHaveBeenNthCalledWith( 2, "gh", expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1w", "--count", "1000", "--output", "./.cache/gh-aw/activity-report-logs", "--format", "markdown"]), - 1200000 + expect.objectContaining({ ignoreReturnCode: true }) ); - expect(mockSleepFn).toHaveBeenCalledTimes(2); expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( expect.objectContaining({ owner: "testowner", diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 8afb115ccc3..450c2615f5b 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -400,14 +400,20 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { t.Errorf("Job activity_report cache key should include run_id for latest-cache resolution in:\n%s", yaml) } - if !strings.Contains(yaml, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs") { - t.Errorf("Job activity_report should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR in:\n%s", yaml) - } - if !strings.Contains(yaml, "Generate agentic workflow activity report") { - t.Errorf("Job activity_report should include report generation step in:\n%s", yaml) + if !strings.Contains(yaml, "Download activity report logs") { + t.Errorf("Job activity_report should include direct logs download step in:\n%s", yaml) } if !strings.Contains(yaml, "timeout-minutes: 20") { - t.Errorf("Job activity_report report generation step should set timeout-minutes: 20 in:\n%s", yaml) + t.Errorf("Job activity_report logs download step should set timeout-minutes: 20 in:\n%s", yaml) + } + if !strings.Contains(yaml, "${GH_AW_CMD_PREFIX} logs") { + t.Errorf("Job activity_report should run gh aw logs directly in:\n%s", yaml) + } + if !strings.Contains(yaml, "--start-date -1w") { + t.Errorf("Job activity_report gh aw logs command should include --start-date -1w in:\n%s", yaml) + } + if !strings.Contains(yaml, "--count 100") { + t.Errorf("Job activity_report gh aw logs command should include --count 100 in:\n%s", yaml) } // close_agentic_workflows_issues job should be triggered when operation == 'close_agentic_workflows_issues' diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index 7de5448be42..58349bef6f7 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -394,20 +394,17 @@ jobs: ${{ runner.os }}-activity-report-logs-${{ github.repository }}- ${{ runner.os }}-activity-report-logs- `) - yaml.WriteString(` - name: Generate agentic workflow activity report + yaml.WriteString(` - name: Download activity report logs timeout-minutes: 20 - uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + ` - GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/run_activity_report.cjs'); - await main(); + run: | + ${GH_AW_CMD_PREFIX} logs \ + --repo "${{ github.repository }}" \ + --start-date -1w \ + --count 100 \ + --output ./.cache/gh-aw/activity-report-logs - name: Save activity report logs cache if: ${{ always() }} diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index db3981d5ab4..6b1e5cf6027 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -469,21 +469,18 @@ jobs: ${{ runner.os }}-activity-report-logs-` + repoSlug + `- ${{ runner.os }}-activity-report-logs- `) - yaml.WriteString(` - name: Generate agentic workflow activity report in target repository + yaml.WriteString(` - name: Download activity report logs in target repository timeout-minutes: 20 - uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: GH_TOKEN: ` + token + ` GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + ` GH_AW_TARGET_REPO_SLUG: "` + repoSlug + `" - GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs - with: - github-token: ` + token + ` - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/run_activity_report.cjs'); - await main(); + run: | + ${GH_AW_CMD_PREFIX} logs \ + --repo "${GH_AW_TARGET_REPO_SLUG}" \ + --start-date -1w \ + --count 100 \ + --output ./.cache/gh-aw/activity-report-logs - name: Save activity report logs cache if: ${{ always() }} diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index 76384dcdc97..168c5ea4796 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -98,12 +98,16 @@ This workflow operates on a separate repository. "generated workflow should save activity_report logs cache even if report generation fails") assert.Contains(t, contentStr, "steps.activity_report_logs_cache.outputs.cache-primary-key", "generated workflow should save activity_report logs using the cache primary key") - assert.Contains(t, contentStr, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs", - "generated workflow should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR for activity_report logs") - assert.Contains(t, contentStr, "Generate agentic workflow activity report in target repository", - "generated workflow should include the activity_report generation step") + assert.Contains(t, contentStr, "Download activity report logs in target repository", + "generated workflow should include direct logs download step for activity_report") assert.Contains(t, contentStr, "timeout-minutes: 20", - "generated workflow should set a 20-minute timeout for the activity_report generation step") + "generated workflow should set a 20-minute timeout for the activity_report logs download step") + assert.Contains(t, contentStr, "${GH_AW_CMD_PREFIX} logs", + "generated workflow should run gh aw logs directly") + assert.Contains(t, contentStr, "--start-date -1w", + "generated workflow should download 7 days of logs for activity_report") + assert.Contains(t, contentStr, "--count 100", + "generated workflow should limit activity_report log downloads to at most 100 runs") assert.Contains(t, contentStr, "actions: read\n contents: read\n issues: write", "activity_report job should include contents: read with explicit permissions") assert.Contains(t, contentStr, "timeout-minutes: 120", From 9ebb673452031e5919fb1b0867016a72d9f9f3e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 05:06:46 +0000 Subject: [PATCH 5/5] fix: generate activity report issue after logs cache save Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8621b272-b02f-42a0-9df9-28efb8eb5043 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 45 ++++++++++++++++- pkg/workflow/maintenance_workflow_test.go | 12 +++++ pkg/workflow/maintenance_workflow_yaml.go | 45 ++++++++++++++++- pkg/workflow/side_repo_maintenance.go | 50 ++++++++++++++++++- .../side_repo_maintenance_integration_test.go | 8 +++ 5 files changed, 157 insertions(+), 3 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index e9bb22b4f16..8e44c718271 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -361,6 +361,7 @@ jobs: ${{ runner.os }}-activity-report-logs- - name: Download activity report logs timeout-minutes: 20 + shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_AW_CMD_PREFIX: ./gh-aw @@ -369,7 +370,9 @@ jobs: --repo "${{ github.repository }}" \ --start-date -1w \ --count 100 \ - --output ./.cache/gh-aw/activity-report-logs + --output ./.cache/gh-aw/activity-report-logs \ + --format markdown \ + > ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} @@ -378,6 +381,46 @@ jobs: path: ./.cache/gh-aw/activity-report-logs key: ${{ steps.activity_report_logs_cache.outputs.cache-primary-key }} + - name: Generate activity report issue + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('node:fs'); + const reportPath = './.cache/gh-aw/activity-report-logs/report.md'; + if (!fs.existsSync(reportPath)) { + core.warning(`Activity report markdown not found at ${reportPath}; skipping issue creation.`); + return; + } + let reportBody = ''; + try { + reportBody = fs.readFileSync(reportPath, 'utf8').trim(); + } catch (error) { + core.warning(`Failed to read activity report markdown at ${reportPath}: ${error.message}`); + return; + } + if (!reportBody) { + core.warning(`Activity report markdown is empty at ${reportPath}; skipping issue creation.`); + return; + } + const repoSlug = `${context.repo.owner}/${context.repo.repo}`; + const body = [ + '### Agentic workflow activity report', + '', + `Repository: ${repoSlug}`, + `Generated at: ${new Date().toISOString()}`, + '', + reportBody, + ].join('\n'); + const createdIssue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '[aw] agentic status report', + body, + labels: ['agentic-workflows'], + }); + core.info(`Created issue #${createdIssue.data.number}: ${createdIssue.data.html_url}`); + close_agentic_workflows_issues: if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues' && (!(github.event.repository.fork)) }} runs-on: ubuntu-slim diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 450c2615f5b..852f3fd78d3 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -415,6 +415,18 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { if !strings.Contains(yaml, "--count 100") { t.Errorf("Job activity_report gh aw logs command should include --count 100 in:\n%s", yaml) } + if !strings.Contains(yaml, "--format markdown") { + t.Errorf("Job activity_report gh aw logs command should include --format markdown in:\n%s", yaml) + } + if !strings.Contains(yaml, "./.cache/gh-aw/activity-report-logs/report.md") { + t.Errorf("Job activity_report gh aw logs command should write report markdown output to report.md in:\n%s", yaml) + } + if !strings.Contains(yaml, "Generate activity report issue") { + t.Errorf("Job activity_report should include issue generation step after cache save in:\n%s", yaml) + } + if !strings.Contains(yaml, "title: '[aw] agentic status report'") { + t.Errorf("Job activity_report issue generation step should create the activity report issue title in:\n%s", yaml) + } // close_agentic_workflows_issues job should be triggered when operation == 'close_agentic_workflows_issues' closeAgenticWorkflowIssuesIdx := strings.Index(yaml, "\n close_agentic_workflows_issues:") diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index 58349bef6f7..3dd0362056d 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -396,6 +396,7 @@ jobs: `) yaml.WriteString(` - name: Download activity report logs timeout-minutes: 20 + shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + ` @@ -404,7 +405,9 @@ jobs: --repo "${{ github.repository }}" \ --start-date -1w \ --count 100 \ - --output ./.cache/gh-aw/activity-report-logs + --output ./.cache/gh-aw/activity-report-logs \ + --format markdown \ + > ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} @@ -412,6 +415,46 @@ jobs: with: path: ./.cache/gh-aw/activity-report-logs key: ${{ steps.activity_report_logs_cache.outputs.cache-primary-key }} + + - name: Generate activity report issue + uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('node:fs'); + const reportPath = './.cache/gh-aw/activity-report-logs/report.md'; + if (!fs.existsSync(reportPath)) { + core.warning('Activity report markdown not found at ' + reportPath + '; skipping issue creation.'); + return; + } + let reportBody = ''; + try { + reportBody = fs.readFileSync(reportPath, 'utf8').trim(); + } catch (error) { + core.warning('Failed to read activity report markdown at ' + reportPath + ': ' + error.message); + return; + } + if (!reportBody) { + core.warning('Activity report markdown is empty at ' + reportPath + '; skipping issue creation.'); + return; + } + const repoSlug = context.repo.owner + '/' + context.repo.repo; + const body = [ + '### Agentic workflow activity report', + '', + 'Repository: ' + repoSlug, + 'Generated at: ' + new Date().toISOString(), + '', + reportBody, + ].join('\n'); + const createdIssue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '[aw] agentic status report', + body, + labels: ['agentic-workflows'], + }); + core.info('Created issue #' + createdIssue.data.number + ': ' + createdIssue.data.html_url); `) // Add close_agentic_workflows_issues job for workflow_dispatch with operation == 'close_agentic_workflows_issues' diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index 6b1e5cf6027..c33233082ab 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -471,6 +471,7 @@ jobs: `) yaml.WriteString(` - name: Download activity report logs in target repository timeout-minutes: 20 + shell: bash env: GH_TOKEN: ` + token + ` GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + ` @@ -480,7 +481,9 @@ jobs: --repo "${GH_AW_TARGET_REPO_SLUG}" \ --start-date -1w \ --count 100 \ - --output ./.cache/gh-aw/activity-report-logs + --output ./.cache/gh-aw/activity-report-logs \ + --format markdown \ + > ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} @@ -488,6 +491,51 @@ jobs: with: path: ./.cache/gh-aw/activity-report-logs key: ${{ steps.activity_report_logs_cache.outputs.cache-primary-key }} + + - name: Generate activity report issue in target repository + uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` + with: + github-token: ` + token + ` + script: | + const fs = require('node:fs'); + const reportPath = './.cache/gh-aw/activity-report-logs/report.md'; + if (!fs.existsSync(reportPath)) { + core.warning('Activity report markdown not found at ' + reportPath + '; skipping issue creation.'); + return; + } + let reportBody = ''; + try { + reportBody = fs.readFileSync(reportPath, 'utf8').trim(); + } catch (error) { + core.warning('Failed to read activity report markdown at ' + reportPath + ': ' + error.message); + return; + } + if (!reportBody) { + core.warning('Activity report markdown is empty at ' + reportPath + '; skipping issue creation.'); + return; + } + const repoSlug = process.env.GH_AW_TARGET_REPO_SLUG || ''; + const [owner, repo] = repoSlug.split('/'); + if (!owner || !repo) { + core.setFailed('Invalid GH_AW_TARGET_REPO_SLUG: ' + repoSlug); + return; + } + const body = [ + '### Agentic workflow activity report', + '', + 'Repository: ' + repoSlug, + 'Generated at: ' + new Date().toISOString(), + '', + reportBody, + ].join('\n'); + const createdIssue = await github.rest.issues.create({ + owner, + repo, + title: '[aw] agentic status report', + body, + labels: ['agentic-workflows'], + }); + core.info('Created issue #' + createdIssue.data.number + ': ' + createdIssue.data.html_url); `) // Add validate_workflows job for workflow_dispatch/workflow_call with operation == 'validate' diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index 168c5ea4796..7e0080ee35b 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -108,6 +108,14 @@ This workflow operates on a separate repository. "generated workflow should download 7 days of logs for activity_report") assert.Contains(t, contentStr, "--count 100", "generated workflow should limit activity_report log downloads to at most 100 runs") + assert.Contains(t, contentStr, "--format markdown", + "generated workflow should request markdown report output from gh aw logs") + assert.Contains(t, contentStr, "./.cache/gh-aw/activity-report-logs/report.md", + "generated workflow should write activity_report markdown output to report.md") + assert.Contains(t, contentStr, "Generate activity report issue in target repository", + "generated workflow should include activity_report issue generation step after cache save") + assert.Contains(t, contentStr, "title: '[aw] agentic status report'", + "generated workflow should create the activity_report issue with the expected title") assert.Contains(t, contentStr, "actions: read\n contents: read\n issues: write", "activity_report job should include contents: read with explicit permissions") assert.Contains(t, contentStr, "timeout-minutes: 120",