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")