From 1a87039c6b69ea9ae2d7da48e3a9700e6c88c314 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:11:06 +0000 Subject: [PATCH 1/2] Plan: add scheduled trace indexer cache job for maintenance workflow Agent-Logs-Url: https://github.com/github/gh-aw/sessions/7c1aea85-29d5-4c14-8d19-34c3a54e8e3d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-project.lock.yml | 4 ++-- .github/workflows/test-project-url-default.lock.yml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 228c765f09b..3bd1d6c005e 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -1130,7 +1130,7 @@ jobs: permissions: contents: write discussions: write - issues: write + issues: read pull-requests: write concurrency: group: "gh-aw-conclusion-smoke-project" @@ -1499,7 +1499,7 @@ jobs: permissions: contents: write discussions: write - issues: write + issues: read pull-requests: write timeout-minutes: 15 env: diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index 98b8937c414..c4b7f8d3f78 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -892,6 +892,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: read + issues: read concurrency: group: "gh-aw-conclusion-test-project-url-default" cancel-in-progress: false @@ -1197,6 +1198,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: read + issues: read timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/test-project-url-default" From 0f63c057a99d13180ee8d78c473811db574b071d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:15:47 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Add=20scheduled=20=E2=80=9CAgentic=20workfl?= =?UTF-8?q?ow=20logs=E2=80=9D=20trace=20indexer=20and=20refactor=20activit?= =?UTF-8?q?y=20reports=20to=20consume=20cached=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/github/gh-aw/sessions/7c1aea85-29d5-4c14-8d19-34c3a54e8e3d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_activity_report.cjs | 92 +++------------ actions/setup/js/run_activity_report.test.cjs | 49 ++++---- actions/setup/js/run_trace_indexer.cjs | 111 ++++++++++++++++++ actions/setup/js/run_trace_indexer.test.cjs | 90 ++++++++++++++ pkg/workflow/maintenance_workflow_test.go | 35 ++++-- pkg/workflow/maintenance_workflow_yaml.go | 74 ++++++++++-- pkg/workflow/side_repo_maintenance.go | 75 ++++++++++-- .../side_repo_maintenance_integration_test.go | 14 ++- 8 files changed, 411 insertions(+), 129 deletions(-) create mode 100644 actions/setup/js/run_trace_indexer.cjs create mode 100644 actions/setup/js/run_trace_indexer.test.cjs diff --git a/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs index 27cd0cb920a..2aec8aeffae 100644 --- a/actions/setup/js/run_activity_report.cjs +++ b/actions/setup/js/run_activity_report.cjs @@ -1,98 +1,44 @@ // @ts-check /// -const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs"); +const fs = require("node:fs/promises"); +const path = require("node:path"); const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); 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 DEFAULT_REPORT_OUTPUT_DIR = "./.cache/gh-aw/agentic-workflow-logs"; +const REPORT_SECTION_DIR = "activity-report"; -/** @typedef {{ key: string, heading: string, startDate: string, optionalOnRateLimit: boolean }} ActivityRange */ +/** @typedef {{ key: string, heading: string }} ActivityRange */ /** @type {ActivityRange[]} */ const REPORT_RANGES = [ - { key: "24h", heading: "Last 24 hours", startDate: "-1d", optionalOnRateLimit: false }, - { key: "7d", heading: "Last 7 days", startDate: "-1w", optionalOnRateLimit: false }, + { key: "24h", heading: "Last 24 hours" }, + { key: "7d", heading: "Last 7 days" }, ]; /** - * @param {string} text - * @returns {boolean} - */ -function hasRateLimitText(text) { - return /\bapi rate limit\b|\brate limit exceeded\b|\bsecondary rate limit\b|\b429\b/i.test(text); -} - -/** - * Run the logs command for a configured report range. + * Read pre-indexed report markdown from the cache directory. * - * @param {string} bin - * @param {string[]} prefixArgs - * @param {string} repoSlug * @param {ActivityRange} range * @param {string} outputDir * @returns {Promise<{ heading: string, body: string }>} */ -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(" ")}`); - +async function readCachedRangeReport(range, outputDir) { + const rangeReportPath = path.join(outputDir, REPORT_SECTION_DIR, `${range.key}.md`); try { - const result = await exec.getExecOutput(bin, args, { ignoreReturnCode: true }); - const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim(); - const rateLimited = hasRateLimitText(output); - - if (result.exitCode === 0 && result.stdout.trim()) { - return { - heading: range.heading, - body: normalizeReportMarkdown(sanitizeContent(result.stdout.trim())), - }; - } - - if (rateLimited && range.optionalOnRateLimit) { - core.warning(`Skipping ${range.heading} report due to GitHub API rate limiting`); - return { - heading: range.heading, - body: "_Skipped due to GitHub API rate limiting._", - }; - } - - if (rateLimited) { - return { - heading: range.heading, - body: "_Could not generate this section due to GitHub API rate limiting._", - }; - } - + const markdown = await fs.readFile(rangeReportPath, "utf8"); return { heading: range.heading, - body: `_Report command failed (exit code ${result.exitCode})._\n\n\`\`\`\n${sanitizeContent(output || "No command output was captured.")}\n\`\`\``, + body: normalizeReportMarkdown(sanitizeContent(markdown.trim())), }; - } catch (error) { - const errorMessage = getErrorMessage(error); - const rateLimited = isRateLimitError(error) || hasRateLimitText(errorMessage); - - if (rateLimited && range.optionalOnRateLimit) { - core.warning(`Skipping ${range.heading} report due to GitHub API rate limiting`); - return { - heading: range.heading, - body: "_Skipped due to GitHub API rate limiting._", - }; - } - - if (rateLimited) { - return { - heading: range.heading, - body: "_Could not generate this section due to GitHub API rate limiting._", - }; - } - + } catch { + core.warning(`Missing cached report for ${range.heading}: ${rangeReportPath}`); return { heading: range.heading, - body: `_Report command failed: ${sanitizeContent(errorMessage)}_`, + body: "_No cached trace index is available for this range yet._", }; } } @@ -117,17 +63,15 @@ function normalizeReportMarkdown(markdown) { * @returns {Promise} */ 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); const { owner, repo } = resolveExecutionOwnerRepo(); const repoSlug = `${owner}/${repo}`; - core.info(`Generating agentic workflow activity report for ${repoSlug}`); + core.info(`Generating agentic workflow activity report for ${repoSlug} from cached trace index data`); const sections = []; for (const range of REPORT_RANGES) { - sections.push(await runRangeReport(bin, prefixArgs, repoSlug, range, reportOutputDir)); + sections.push(await readCachedRangeReport(range, reportOutputDir)); } const headerLines = ["### Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, ""]; @@ -145,4 +89,4 @@ async function main() { core.info(`Created issue #${createdIssue.data.number}: ${createdIssue.data.html_url}`); } -module.exports = { main, hasRateLimitText, runRangeReport, normalizeReportMarkdown }; +module.exports = { main, readCachedRangeReport, normalizeReportMarkdown }; diff --git a/actions/setup/js/run_activity_report.test.cjs b/actions/setup/js/run_activity_report.test.cjs index e858ee64d48..5961210c088 100644 --- a/actions/setup/js/run_activity_report.test.cjs +++ b/actions/setup/js/run_activity_report.test.cjs @@ -1,5 +1,8 @@ // @ts-check import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; describe("run_activity_report", () => { let originalGlobals; @@ -7,17 +10,17 @@ describe("run_activity_report", () => { let mockCore; let mockGithub; let mockContext; - let mockExec; + let tempOutputDir; beforeEach(() => { originalEnv = { ...process.env }; - process.env.GH_AW_CMD_PREFIX = "gh aw"; + tempOutputDir = path.join(os.tmpdir(), `run-activity-report-${Date.now()}-${Math.random().toString(36).slice(2)}`); + process.env.GH_AW_ACTIVITY_REPORT_OUTPUT_DIR = tempOutputDir; originalGlobals = { core: global.core, github: global.github, context: global.context, - exec: global.exec, }; mockCore = { @@ -39,44 +42,29 @@ describe("run_activity_report", () => { repo: "testrepo", }, }; - mockExec = { - getExecOutput: vi.fn(), - }; global.core = mockCore; global.github = mockGithub; global.context = mockContext; - global.exec = mockExec; }); - afterEach(() => { + afterEach(async () => { process.env = originalEnv; global.core = originalGlobals.core; global.github = originalGlobals.github; global.context = originalGlobals.context; - global.exec = originalGlobals.exec; + await fs.rm(tempOutputDir, { recursive: true, force: true }); 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 }); + it("creates an activity report issue using cached 24h and 7d reports", async () => { + await fs.mkdir(path.join(tempOutputDir, "activity-report"), { recursive: true }); + await fs.writeFile(path.join(tempOutputDir, "activity-report", "24h.md"), "## 24h report\nok\n", "utf8"); + await fs.writeFile(path.join(tempOutputDir, "activity-report", "7d.md"), "## 7d report\nok\n", "utf8"); const { main } = await import("./run_activity_report.cjs"); await main(); - 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"]), - expect.objectContaining({ ignoreReturnCode: true }) - ); - 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"]), - expect.objectContaining({ ignoreReturnCode: true }) - ); expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( expect.objectContaining({ owner: "testowner", @@ -95,11 +83,14 @@ describe("run_activity_report", () => { expect(issueBody).toContain("#### 24h report"); }); - it("detects rate limit text helper", async () => { - const { hasRateLimitText } = await import("./run_activity_report.cjs"); - expect(hasRateLimitText("API rate limit exceeded")).toBe(true); - expect(hasRateLimitText("secondary rate limit")).toBe(true); - expect(hasRateLimitText("normal output")).toBe(false); + it("uses fallback text when cached range reports are missing", async () => { + const { main } = await import("./run_activity_report.cjs"); + await main(); + + const issueBody = mockGithub.rest.issues.create.mock.calls[0][0].body; + expect(issueBody).toContain("Last 24 hours"); + expect(issueBody).toContain("_No cached trace index is available for this range yet._"); + expect(mockCore.warning).toHaveBeenCalled(); }); it("demotes report headings by two levels", async () => { diff --git a/actions/setup/js/run_trace_indexer.cjs b/actions/setup/js/run_trace_indexer.cjs new file mode 100644 index 00000000000..b07fea78317 --- /dev/null +++ b/actions/setup/js/run_trace_indexer.cjs @@ -0,0 +1,111 @@ +// @ts-check +/// + +const fs = require("node:fs/promises"); +const path = require("node:path"); + +const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs"); +const { resolveExecutionOwnerRepo } = require("./repo_helpers.cjs"); +const { sanitizeContent } = require("./sanitize_content.cjs"); + +const REPORT_COUNT = 1000; +const DEFAULT_TRACE_OUTPUT_DIR = "./.cache/gh-aw/agentic-workflow-logs"; +const REPORT_SECTION_DIR = "activity-report"; + +/** @typedef {{ key: string, heading: string, startDate: string }} TraceRange */ + +/** @type {TraceRange[]} */ +const TRACE_RANGES = [ + { key: "24h", heading: "Last 24 hours", startDate: "-1d" }, + { key: "7d", heading: "Last 7 days", startDate: "-1w" }, +]; + +/** + * @param {string} text + * @returns {boolean} + */ +function hasRateLimitText(text) { + return /\bapi rate limit\b|\brate limit exceeded\b|\bsecondary rate limit\b|\b429\b/i.test(text); +} + +/** + * @param {string} filePath + * @param {string} content + * @returns {Promise} + */ +async function writeReportSection(filePath, content) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${content.trim()}\n`, "utf8"); +} + +/** + * @param {string} bin + * @param {string[]} prefixArgs + * @param {string} repoSlug + * @param {string} outputDir + * @param {TraceRange} range + * @returns {Promise} + */ +async function runTraceRange(bin, prefixArgs, repoSlug, outputDir, range) { + const rangeReportPath = path.join(outputDir, REPORT_SECTION_DIR, `${range.key}.md`); + const args = [...prefixArgs, "logs", "--repo", repoSlug, "--start-date", range.startDate, "--count", String(REPORT_COUNT), "--output", outputDir, "--format", "markdown"]; + core.info(`Running trace indexer: ${bin} ${args.join(" ")}`); + + try { + const result = await exec.getExecOutput(bin, args, { ignoreReturnCode: true }); + const stdout = (result.stdout || "").trim(); + const stderr = (result.stderr || "").trim(); + const output = `${stdout}\n${stderr}`.trim(); + const rateLimited = hasRateLimitText(output); + + if (result.exitCode === 0 && stdout) { + await writeReportSection(rangeReportPath, sanitizeContent(stdout)); + return true; + } + + if (rateLimited) { + await writeReportSection(rangeReportPath, "_Could not refresh this range due to GitHub API rate limiting._"); + return false; + } + + await writeReportSection(rangeReportPath, `_Trace indexing failed (exit code ${result.exitCode})._\n\n\`\`\`\n${sanitizeContent(output || "No command output was captured.")}\n\`\`\``); + return false; + } catch (error) { + const errorMessage = getErrorMessage(error); + const rateLimited = isRateLimitError(error) || hasRateLimitText(errorMessage); + if (rateLimited) { + await writeReportSection(rangeReportPath, "_Could not refresh this range due to GitHub API rate limiting._"); + return false; + } + await writeReportSection(rangeReportPath, `_Trace indexing failed: ${sanitizeContent(errorMessage)}_`); + return false; + } +} + +/** + * Refresh cached logs and report sections for activity reporting. + * @returns {Promise} + */ +async function main() { + const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw"; + const traceOutputDir = process.env.GH_AW_TRACE_INDEX_OUTPUT_DIR || DEFAULT_TRACE_OUTPUT_DIR; + const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean); + const { owner, repo } = resolveExecutionOwnerRepo(); + const repoSlug = `${owner}/${repo}`; + + core.info(`Refreshing agentic workflow logs cache for ${repoSlug}`); + + let allRangesSucceeded = true; + for (const range of TRACE_RANGES) { + const ok = await runTraceRange(bin, prefixArgs, repoSlug, traceOutputDir, range); + if (!ok) { + allRangesSucceeded = false; + } + } + + if (!allRangesSucceeded) { + throw new Error("Trace indexing completed with one or more range failures"); + } +} + +module.exports = { main, hasRateLimitText, runTraceRange }; diff --git a/actions/setup/js/run_trace_indexer.test.cjs b/actions/setup/js/run_trace_indexer.test.cjs new file mode 100644 index 00000000000..a0d135d8411 --- /dev/null +++ b/actions/setup/js/run_trace_indexer.test.cjs @@ -0,0 +1,90 @@ +// @ts-check +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; + +describe("run_trace_indexer", () => { + let originalGlobals; + let originalEnv; + let mockCore; + let mockContext; + let mockExec; + let tempOutputDir; + + beforeEach(() => { + originalEnv = { ...process.env }; + tempOutputDir = path.join(os.tmpdir(), `run-trace-indexer-${Date.now()}-${Math.random().toString(36).slice(2)}`); + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_AW_TRACE_INDEX_OUTPUT_DIR = tempOutputDir; + + originalGlobals = { + core: global.core, + context: global.context, + exec: global.exec, + }; + + mockCore = { + info: vi.fn(), + warning: vi.fn(), + }; + mockContext = { + repo: { + owner: "testowner", + repo: "testrepo", + }, + }; + mockExec = { + getExecOutput: vi.fn(), + }; + + global.core = mockCore; + global.context = mockContext; + global.exec = mockExec; + }); + + afterEach(async () => { + process.env = originalEnv; + global.core = originalGlobals.core; + global.context = originalGlobals.context; + global.exec = originalGlobals.exec; + await fs.rm(tempOutputDir, { recursive: true, force: true }); + vi.clearAllMocks(); + }); + + it("runs 24h and 7d trace indexing and stores markdown sections", async () => { + mockExec.getExecOutput.mockResolvedValueOnce({ stdout: "## 24h report\nok", stderr: "", exitCode: 0 }).mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 }); + + const { main } = await import("./run_trace_indexer.cjs"); + await main(); + + expect(mockExec.getExecOutput).toHaveBeenCalledTimes(2); + expect(mockExec.getExecOutput).toHaveBeenNthCalledWith( + 1, + "gh", + expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1d", "--count", "1000", "--output", tempOutputDir, "--format", "markdown"]), + expect.objectContaining({ ignoreReturnCode: true }) + ); + expect(mockExec.getExecOutput).toHaveBeenNthCalledWith( + 2, + "gh", + expect.arrayContaining(["aw", "logs", "--repo", "testowner/testrepo", "--start-date", "-1w", "--count", "1000", "--output", tempOutputDir, "--format", "markdown"]), + expect.objectContaining({ ignoreReturnCode: true }) + ); + + const report24h = await fs.readFile(path.join(tempOutputDir, "activity-report", "24h.md"), "utf8"); + const report7d = await fs.readFile(path.join(tempOutputDir, "activity-report", "7d.md"), "utf8"); + expect(report24h).toContain("## 24h report"); + expect(report7d).toContain("## 7d report"); + }); + + it("writes fallback report text and fails when trace indexing has range failures", async () => { + mockExec.getExecOutput.mockResolvedValueOnce({ stdout: "", stderr: "API rate limit exceeded", exitCode: 1 }).mockResolvedValueOnce({ stdout: "## 7d report\nok", stderr: "", exitCode: 0 }); + + const { main } = await import("./run_trace_indexer.cjs"); + await expect(main()).rejects.toThrow("Trace indexing completed with one or more range failures"); + + const report24h = await fs.readFile(path.join(tempOutputDir, "activity-report", "24h.md"), "utf8"); + expect(report24h).toContain("rate limiting"); + }); +}); diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index a8174de8eb0..6201d62d1c9 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -288,6 +288,7 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { activityReportCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report'` closeAgenticWorkflowIssuesCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues'` cleanCacheMemoriesCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories'` + traceIndexerCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'activity_report'` const jobSectionSearchRange = 300 const runOpSectionSearchRange = 500 @@ -319,6 +320,23 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { } } + // agentic_workflow_logs job should run on schedule, empty operation, or activity_report operation + traceIndexerIdx := strings.Index(yaml, "\n agentic_workflow_logs:") + if traceIndexerIdx == -1 { + t.Errorf("Job agentic_workflow_logs not found in generated workflow") + } else { + traceIndexerSection := yaml[traceIndexerIdx : traceIndexerIdx+runOpSectionSearchRange] + if !strings.Contains(traceIndexerSection, "name: Agentic workflow logs") { + t.Errorf("Job agentic_workflow_logs should include a clear job name in:\n%s", traceIndexerSection) + } + if !strings.Contains(traceIndexerSection, traceIndexerCondition) { + t.Errorf("Job agentic_workflow_logs should have the trace indexer condition %q in:\n%s", traceIndexerCondition, traceIndexerSection) + } + if !strings.Contains(traceIndexerSection, "continue-on-error: true") { + t.Errorf("Job agentic_workflow_logs should set continue-on-error for trace refresh step in:\n%s", traceIndexerSection) + } + } + // run_operation job should NOT have the skip condition but should have its own activation condition // and should exclude safe_outputs runOpIdx := strings.Index(yaml, "\n run_operation:") @@ -383,15 +401,18 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { if !strings.Contains(activityReportSection, "timeout-minutes: 120") { t.Errorf("Job activity_report should set timeout-minutes: 120 in:\n%s", activityReportSection) } + if !strings.Contains(activityReportSection, "needs:\n - agentic_workflow_logs") { + t.Errorf("Job activity_report should depend on agentic_workflow_logs in:\n%s", activityReportSection) + } } - if !strings.Contains(yaml, "Cache activity report logs") { - t.Errorf("Job activity_report should include a cache step in:\n%s", yaml) + if !strings.Contains(yaml, "Restore agentic workflow logs cache") { + t.Errorf("Workflow should include a cache restore step for agentic workflow logs in:\n%s", yaml) } if !strings.Contains(yaml, "${{ github.run_id }}") { 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") { + if !strings.Contains(yaml, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/agentic-workflow-logs") { t.Errorf("Job activity_report should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR in:\n%s", yaml) } @@ -712,12 +733,12 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Fatalf("Expected maintenance workflow to be generated: %v", err) } yaml := string(content) - // run_operation, create_labels, activity_report, validate_workflows, and compile_workflows should use the same setup-go version - // (all use getActionPin, not hardcoded pins). Exactly 5 occurrences expected. + // run_operation, create_labels, agentic_workflow_logs, activity_report, validate_workflows, + // and compile_workflows should use the same setup-go version (all use getActionPin, not hardcoded pins). setupGoPin := getActionPin("actions/setup-go") occurrences := strings.Count(yaml, setupGoPin) - if occurrences != 5 { - t.Errorf("Expected exactly 5 occurrences of pinned setup-go ref %q (run_operation + create_labels + activity_report + validate_workflows + compile_workflows), got %d in:\n%s", + if occurrences != 6 { + t.Errorf("Expected exactly 6 occurrences of pinned setup-go ref %q (run_operation + create_labels + agentic_workflow_logs + activity_report + validate_workflows + compile_workflows), got %d in:\n%s", setupGoPin, occurrences, yaml) } }) diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index 91a701fb911..ebf5d48eb8c 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -350,10 +350,69 @@ jobs: await main(); `) + // Add agentic_workflow_logs trace indexer job for schedule and activity_report operation. + traceIndexerCondition := buildNotForkAndScheduledOrOperation("activity_report") + yaml.WriteString(` + agentic_workflow_logs: + name: Agentic workflow logs + if: ${{ ` + RenderCondition(traceIndexerCondition) + ` }} + runs-on: ` + runsOnValue + ` + timeout-minutes: 120 + permissions: + actions: read + contents: read + steps: + - name: Checkout repository + uses: ` + getActionPin("actions/checkout") + ` + with: + persist-credentials: false + + - name: Setup Scripts + uses: ` + setupActionRef + ` + with: + destination: ${{ runner.temp }}/gh-aw/actions + +`) + + yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(` - name: Restore agentic workflow logs cache + uses: ` + getActionPin("actions/cache/restore") + ` + with: + path: ./.cache/gh-aw/agentic-workflow-logs + key: ${{ runner.os }}-agentic-workflow-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-agentic-workflow-logs-${{ github.repository }}- + ${{ runner.os }}-agentic-workflow-logs- + + - name: Refresh agentic workflow logs trace index + continue-on-error: true + uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + ` + GH_AW_TRACE_INDEX_OUTPUT_DIR: ./.cache/gh-aw/agentic-workflow-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_trace_indexer.cjs'); + await main(); + + - name: Save agentic workflow logs cache + if: ${{ always() }} + uses: ` + getActionPin("actions/cache/save") + ` + with: + path: ./.cache/gh-aw/agentic-workflow-logs + key: ${{ runner.os }}-agentic-workflow-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} +`) + // Add activity_report job for workflow_dispatch with operation == 'activity_report' yaml.WriteString(` activity_report: if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }} + needs: + - agentic_workflow_logs runs-on: ` + runsOnValue + ` timeout-minutes: 120 permissions: @@ -384,21 +443,20 @@ jobs: `) yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) - yaml.WriteString(` - name: Cache activity report logs - uses: ` + getActionPin("actions/cache") + ` + yaml.WriteString(` - name: Restore agentic workflow logs cache + uses: ` + getActionPin("actions/cache/restore") + ` with: - path: ./.cache/gh-aw/activity-report-logs - key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} + path: ./.cache/gh-aw/agentic-workflow-logs + key: ${{ runner.os }}-agentic-workflow-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} restore-keys: | - ${{ runner.os }}-activity-report-logs-${{ github.repository }}- - ${{ runner.os }}-activity-report-logs- + ${{ runner.os }}-agentic-workflow-logs-${{ github.repository }}- + ${{ runner.os }}-agentic-workflow-logs- `) yaml.WriteString(` - name: Generate agentic workflow activity report 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 + GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/agentic-workflow-logs with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index a490ee7a171..bb8b4a8616f 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -425,10 +425,70 @@ jobs: await main(); `) + // Add agentic_workflow_logs trace indexer job for schedule and activity_report operation. + traceIndexerCondition := buildNotForkAndScheduledOrOperation("activity_report") + yaml.WriteString(` + agentic_workflow_logs: + name: Agentic workflow logs + if: ${{ ` + RenderCondition(traceIndexerCondition) + ` }} + runs-on: ` + runsOnValue + ` + timeout-minutes: 120 + permissions: + actions: read + contents: read + steps: + - name: Checkout repository + uses: ` + getActionPin("actions/checkout") + ` + with: + persist-credentials: false + + - name: Setup Scripts + uses: ` + setupActionRef + ` + with: + destination: ${{ runner.temp }}/gh-aw/actions + +`) + + yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(` - name: Restore agentic workflow logs cache + uses: ` + getActionPin("actions/cache/restore") + ` + with: + path: ./.cache/gh-aw/agentic-workflow-logs + key: ${{ runner.os }}-agentic-workflow-logs-` + repoSlug + `-${{ github.ref_name }}-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-agentic-workflow-logs-` + repoSlug + `- + ${{ runner.os }}-agentic-workflow-logs- + + - name: Refresh agentic workflow logs trace index in target repository + continue-on-error: true + uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` + env: + GH_TOKEN: ` + token + ` + GH_AW_CMD_PREFIX: ` + getCLICmdPrefix(actionMode) + ` + GH_AW_TARGET_REPO_SLUG: "` + repoSlug + `" + GH_AW_TRACE_INDEX_OUTPUT_DIR: ./.cache/gh-aw/agentic-workflow-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_trace_indexer.cjs'); + await main(); + + - name: Save agentic workflow logs cache + if: ${{ always() }} + uses: ` + getActionPin("actions/cache/save") + ` + with: + path: ./.cache/gh-aw/agentic-workflow-logs + key: ${{ runner.os }}-agentic-workflow-logs-` + repoSlug + `-${{ github.ref_name }}-${{ github.run_id }} +`) + // Add activity_report job for workflow_dispatch/workflow_call with operation == 'activity_report' yaml.WriteString(` activity_report: if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }} + needs: + - agentic_workflow_logs runs-on: ` + runsOnValue + ` timeout-minutes: 120 permissions: @@ -459,22 +519,21 @@ jobs: `) yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) - yaml.WriteString(` - name: Cache activity report logs - uses: ` + getActionPin("actions/cache") + ` + yaml.WriteString(` - name: Restore agentic workflow logs cache + uses: ` + getActionPin("actions/cache/restore") + ` with: - path: ./.cache/gh-aw/activity-report-logs - key: ${{ runner.os }}-activity-report-logs-` + repoSlug + `-${{ github.ref_name }}-${{ github.run_id }} + path: ./.cache/gh-aw/agentic-workflow-logs + key: ${{ runner.os }}-agentic-workflow-logs-` + repoSlug + `-${{ github.ref_name }}-${{ github.run_id }} restore-keys: | - ${{ runner.os }}-activity-report-logs-` + repoSlug + `- - ${{ runner.os }}-activity-report-logs- + ${{ runner.os }}-agentic-workflow-logs-` + repoSlug + `- + ${{ runner.os }}-agentic-workflow-logs- `) yaml.WriteString(` - name: Generate agentic workflow activity report in target repository 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 + GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/agentic-workflow-logs with: github-token: ` + token + ` script: | diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index 9cdfcb3cec6..7aac95d73f1 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -90,9 +90,17 @@ This workflow operates on a separate repository. // Must have activity_report job. assert.Contains(t, contentStr, "activity_report:", "generated workflow should include activity_report job") - assert.Contains(t, contentStr, "Cache activity report logs", - "generated workflow should include cache step for activity_report logs") - assert.Contains(t, contentStr, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/activity-report-logs", + assert.Contains(t, contentStr, "agentic_workflow_logs:", + "generated workflow should include the trace indexer job") + assert.Contains(t, contentStr, "name: Agentic workflow logs", + "generated workflow should include clear trace indexer job naming") + assert.Contains(t, contentStr, "Restore agentic workflow logs cache", + "generated workflow should include cache restore for activity_report logs") + assert.Contains(t, contentStr, "Save agentic workflow logs cache", + "generated workflow should include cache save for indexed logs") + assert.Contains(t, contentStr, "continue-on-error: true", + "trace indexer should use continue-on-error so cache update still runs") + assert.Contains(t, contentStr, "GH_AW_ACTIVITY_REPORT_OUTPUT_DIR: ./.cache/gh-aw/agentic-workflow-logs", "generated workflow should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR for activity_report logs") assert.Contains(t, contentStr, "actions: read\n contents: read\n issues: write", "activity_report job should include contents: read with explicit permissions")