diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index 59d99c41a74..226acf666f4 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -50,6 +50,7 @@ on:
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
+ - 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'validate'
@@ -61,7 +62,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, close_agentic_workflows_issues, clean_cache_memories, validate)'
+ description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)'
required: false
type: string
default: ''
@@ -156,7 +157,7 @@ jobs:
await main();
run_operation:
- if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -311,6 +312,66 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/create_labels.cjs');
await main();
+ activity_report:
+ if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report' && (!(github.event.repository.fork)) }}
+ runs-on: ubuntu-slim
+ timeout-minutes: 120
+ permissions:
+ actions: read
+ contents: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ 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/check_team_member.cjs');
+ await main();
+
+ - name: Setup Go
+ uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+ cache: true
+
+ - name: Build gh-aw
+ run: make build
+
+ - name: Cache activity report logs
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ with:
+ path: ./.cache/gh-aw/activity-report-logs
+ key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }}
+ restore-keys: |
+ ${{ runner.os }}-activity-report-logs-${{ github.repository }}-
+ ${{ runner.os }}-activity-report-logs-
+ - name: Generate agentic workflow activity report
+ 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();
+
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/actions/setup/js/run_activity_report.cjs b/actions/setup/js/run_activity_report.cjs
new file mode 100644
index 00000000000..27cd0cb920a
--- /dev/null
+++ b/actions/setup/js/run_activity_report.cjs
@@ -0,0 +1,148 @@
+// @ts-check
+///
+
+const { getErrorMessage, isRateLimitError } = require("./error_helpers.cjs");
+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";
+
+/** @typedef {{ key: string, heading: string, startDate: string, optionalOnRateLimit: boolean }} 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 },
+];
+
+/**
+ * @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.
+ *
+ * @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(" ")}`);
+
+ 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._",
+ };
+ }
+
+ return {
+ heading: range.heading,
+ body: `_Report command failed (exit code ${result.exitCode})._\n\n\`\`\`\n${sanitizeContent(output || "No command output was captured.")}\n\`\`\``,
+ };
+ } 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._",
+ };
+ }
+
+ return {
+ heading: range.heading,
+ body: `_Report command failed: ${sanitizeContent(errorMessage)}_`,
+ };
+ }
+}
+
+/**
+ * Normalize report markdown for issue rendering.
+ * Demotes headings so top-level report headings start at H3.
+ *
+ * @param {string} markdown
+ * @returns {string}
+ */
+function normalizeReportMarkdown(markdown) {
+ return markdown.replace(/^(#{1,6})\s+/gm, (_, hashes) => {
+ const headingLevel = hashes.length;
+ const demotedHeadingLevel = Math.min(6, headingLevel + HEADING_DEMOTION_LEVELS);
+ return `${"#".repeat(demotedHeadingLevel)} `;
+ });
+}
+
+/**
+ * Generate an agentic workflow activity report issue.
+ * @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}`);
+
+ const sections = [];
+ for (const range of REPORT_RANGES) {
+ sections.push(await runRangeReport(bin, prefixArgs, repoSlug, range, reportOutputDir));
+ }
+
+ const headerLines = ["### Agentic workflow activity report", "", `Repository: \`${repoSlug}\``, `Generated at: ${new Date().toISOString()}`, ""];
+ const sectionLines = sections.flatMap(section => ["", `${section.heading}
`, "", section.body, "", " ", ""]);
+ const body = [...headerLines, ...sectionLines].join("\n");
+
+ const createdIssue = await github.rest.issues.create({
+ owner,
+ repo,
+ title: ISSUE_TITLE,
+ body,
+ labels: ["agentic-workflows"],
+ });
+
+ core.info(`Created issue #${createdIssue.data.number}: ${createdIssue.data.html_url}`);
+}
+
+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
new file mode 100644
index 00000000000..e858ee64d48
--- /dev/null
+++ b/actions/setup/js/run_activity_report.test.cjs
@@ -0,0 +1,112 @@
+// @ts-check
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+
+describe("run_activity_report", () => {
+ let originalGlobals;
+ let originalEnv;
+ let mockCore;
+ let mockGithub;
+ let mockContext;
+ let mockExec;
+
+ beforeEach(() => {
+ originalEnv = { ...process.env };
+ process.env.GH_AW_CMD_PREFIX = "gh aw";
+
+ originalGlobals = {
+ core: global.core,
+ github: global.github,
+ context: global.context,
+ exec: global.exec,
+ };
+
+ mockCore = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ };
+ mockGithub = {
+ rest: {
+ issues: {
+ create: vi.fn().mockResolvedValue({
+ data: { number: 42, html_url: "https://github.com/testowner/testrepo/issues/42" },
+ }),
+ },
+ },
+ };
+ mockContext = {
+ repo: {
+ owner: "testowner",
+ repo: "testrepo",
+ },
+ };
+ mockExec = {
+ getExecOutput: vi.fn(),
+ };
+
+ global.core = mockCore;
+ global.github = mockGithub;
+ global.context = mockContext;
+ global.exec = mockExec;
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ 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 });
+
+ 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",
+ repo: "testrepo",
+ title: "[aw] agentic status report",
+ labels: ["agentic-workflows"],
+ })
+ );
+
+ const issueBody = mockGithub.rest.issues.create.mock.calls[0][0].body;
+ expect(issueBody).toContain("### Agentic workflow activity report");
+ expect(issueBody).toContain("");
+ expect(issueBody).toContain("Last 24 hours
");
+ expect(issueBody).toContain("Last 7 days
");
+ expect(issueBody).not.toContain("Last 30 days
");
+ 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("demotes report headings by two levels", async () => {
+ const { normalizeReportMarkdown } = await import("./run_activity_report.cjs");
+ const transformed = normalizeReportMarkdown("# H1\n## H2\n### H3");
+ expect(transformed).toContain("### H1");
+ expect(transformed).toContain("#### H2");
+ expect(transformed).toContain("##### H3");
+ });
+});
diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go
index 985d9b5a813..a8174de8eb0 100644
--- a/pkg/workflow/maintenance_workflow_test.go
+++ b/pkg/workflow/maintenance_workflow_test.go
@@ -282,9 +282,10 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
yaml := string(content)
operationSkipCondition := `github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == ''`
- operationRunCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate'`
+ operationRunCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'validate'`
applySafeOutputsCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs'`
createLabelsCondition := `(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels'`
+ 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'`
@@ -367,6 +368,33 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
}
}
+ // activity_report job should be triggered when operation == 'activity_report'
+ activityReportIdx := strings.Index(yaml, "\n activity_report:")
+ if activityReportIdx == -1 {
+ t.Errorf("Job activity_report not found in generated workflow")
+ } else {
+ activityReportSection := yaml[activityReportIdx : activityReportIdx+runOpSectionSearchRange]
+ if !strings.Contains(activityReportSection, activityReportCondition) {
+ t.Errorf("Job activity_report should have the activation condition %q in:\n%s", activityReportCondition, activityReportSection)
+ }
+ if !strings.Contains(activityReportSection, "contents: read") {
+ t.Errorf("Job activity_report should include contents: read permission in:\n%s", activityReportSection)
+ }
+ if !strings.Contains(activityReportSection, "timeout-minutes: 120") {
+ t.Errorf("Job activity_report should set timeout-minutes: 120 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, "${{ 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") {
+ t.Errorf("Job activity_report should set GH_AW_ACTIVITY_REPORT_OUTPUT_DIR 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:")
if closeAgenticWorkflowIssuesIdx == -1 {
@@ -398,6 +426,11 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
t.Error("workflow_dispatch operation choices should include 'validate'")
}
+ // Verify activity_report is an option in the operation choices
+ if !strings.Contains(yaml, "- 'activity_report'") {
+ t.Error("workflow_dispatch operation choices should include 'activity_report'")
+ }
+
// Verify close_agentic_workflows_issues is an option in the operation choices
if !strings.Contains(yaml, "- 'close_agentic_workflows_issues'") {
t.Error("workflow_dispatch operation choices should include 'close_agentic_workflows_issues'")
@@ -430,9 +463,10 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
// Verify run_operation job exposes outputs
runOpIdx2 := strings.Index(yaml, "\n run_operation:")
if runOpIdx2 != -1 {
- runOpSection2 := yaml[runOpIdx2 : runOpIdx2+600]
+ runOpEnd := min(runOpIdx2+1200, len(yaml))
+ runOpSection2 := yaml[runOpIdx2:runOpEnd]
if !strings.Contains(runOpSection2, "outputs:\n operation: ${{ steps.record.outputs.operation }}") {
- t.Errorf("run_operation job should declare operation output, got:\n%s", runOpSection2[:300])
+ t.Errorf("run_operation job should declare operation output, got:\n%s", runOpSection2[:min(300, len(runOpSection2))])
}
}
@@ -678,12 +712,12 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) {
t.Fatalf("Expected maintenance workflow to be generated: %v", err)
}
yaml := string(content)
- // run_operation, create_labels, validate_workflows, and compile_workflows should use the same setup-go version
- // (all use getActionPin, not hardcoded pins). Exactly 4 occurrences expected.
+ // 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.
setupGoPin := getActionPin("actions/setup-go")
occurrences := strings.Count(yaml, setupGoPin)
- if occurrences != 4 {
- t.Errorf("Expected exactly 4 occurrences of pinned setup-go ref %q (run_operation + create_labels + validate_workflows + compile_workflows), got %d in:\n%s",
+ 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",
setupGoPin, occurrences, yaml)
}
})
@@ -1143,6 +1177,9 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) {
if !strings.Contains(contentStr, "create_labels") {
t.Errorf("Side-repo maintenance should include create_labels job, got content length %d", len(contentStr))
}
+ if !strings.Contains(contentStr, "activity_report") {
+ t.Errorf("Side-repo maintenance should include activity_report job, got content length %d", len(contentStr))
+ }
})
t.Run("no side-repo file generated when no current checkout", func(t *testing.T) {
@@ -1172,7 +1209,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) {
}
})
- t.Run("side-repo generated without expires uses safe_outputs and create_labels only", func(t *testing.T) {
+ t.Run("side-repo generated without expires uses safe_outputs, create_labels, and activity_report", func(t *testing.T) {
tmpDir := t.TempDir()
workflowDataList := []*WorkflowData{
{
@@ -1213,6 +1250,9 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) {
if strings.Contains(contentStr, "close-expired-entities") {
t.Errorf("Side-repo maintenance should NOT include close-expired-entities when no expires, got content length %d", len(contentStr))
}
+ if !strings.Contains(contentStr, "activity_report") {
+ t.Errorf("Side-repo maintenance should include activity_report when no expires, got content length %d", len(contentStr))
+ }
})
t.Run("expression-based repository does not generate side-repo maintenance", func(t *testing.T) {
diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go
index 0415c3de54f..91a701fb911 100644
--- a/pkg/workflow/maintenance_workflow_yaml.go
+++ b/pkg/workflow/maintenance_workflow_yaml.go
@@ -59,6 +59,7 @@ on:
- 'upgrade'
- 'safe_outputs'
- 'create_labels'
+ - 'activity_report'
- 'close_agentic_workflows_issues'
- 'clean_cache_memories'
- 'validate'
@@ -70,7 +71,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, close_agentic_workflows_issues, clean_cache_memories, validate)'
+ description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)'
required: false
type: string
default: ''
@@ -195,8 +196,8 @@ jobs:
`)
// Add unified run_operation job for all dispatch operations except those with dedicated jobs
- // (safe_outputs, create_labels, close_agentic_workflows_issues, clean_cache_memories, validate)
- runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "close_agentic_workflows_issues", "clean_cache_memories", "validate")
+ // (safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, validate)
+ runOperationCondition := buildRunOperationCondition("safe_outputs", "create_labels", "activity_report", "close_agentic_workflows_issues", "clean_cache_memories", "validate")
yaml.WriteString(`
run_operation:
if: ${{ ` + RenderCondition(runOperationCondition) + ` }}
@@ -349,6 +350,64 @@ jobs:
await main();
`)
+ // Add activity_report job for workflow_dispatch with operation == 'activity_report'
+ yaml.WriteString(`
+ activity_report:
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }}
+ runs-on: ` + runsOnValue + `
+ timeout-minutes: 120
+ permissions:
+ actions: read
+ contents: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: ` + getActionPin("actions/checkout") + `
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ` + setupActionRef + `
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ 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/check_team_member.cjs');
+ await main();
+
+`)
+
+ yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver))
+ yaml.WriteString(` - name: Cache activity report logs
+ uses: ` + getActionPin("actions/cache") + `
+ with:
+ path: ./.cache/gh-aw/activity-report-logs
+ key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }}
+ restore-keys: |
+ ${{ runner.os }}-activity-report-logs-${{ github.repository }}-
+ ${{ runner.os }}-activity-report-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
+ 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();
+`)
+
// Add close_agentic_workflows_issues job for workflow_dispatch with operation == 'close_agentic_workflows_issues'
yaml.WriteString(`
close_agentic_workflows_issues:
diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go
index 4e8aa64082a..a490ee7a171 100644
--- a/pkg/workflow/side_repo_maintenance.go
+++ b/pkg/workflow/side_repo_maintenance.go
@@ -216,6 +216,7 @@ on:
- ''
- 'safe_outputs'
- 'create_labels'
+ - 'activity_report'
- 'validate'
run_url:
description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
@@ -225,7 +226,7 @@ on:
workflow_call:
inputs:
operation:
- description: 'Optional maintenance operation to run (safe_outputs, create_labels, validate)'
+ description: 'Optional maintenance operation to run (safe_outputs, create_labels, activity_report, validate)'
required: false
type: string
default: ''
@@ -424,6 +425,65 @@ jobs:
await main();
`)
+ // Add activity_report job for workflow_dispatch/workflow_call with operation == 'activity_report'
+ yaml.WriteString(`
+ activity_report:
+ if: ${{ ` + RenderCondition(buildDispatchOperationCondition("activity_report")) + ` }}
+ runs-on: ` + runsOnValue + `
+ timeout-minutes: 120
+ permissions:
+ actions: read
+ contents: read
+ issues: write
+ steps:
+ - name: Checkout repository
+ uses: ` + getActionPin("actions/checkout") + `
+ with:
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ` + setupActionRef + `
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + `
+ 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/check_team_member.cjs');
+ await main();
+
+`)
+
+ yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver))
+ yaml.WriteString(` - name: Cache activity report logs
+ uses: ` + getActionPin("actions/cache") + `
+ with:
+ path: ./.cache/gh-aw/activity-report-logs
+ key: ${{ runner.os }}-activity-report-logs-` + repoSlug + `-${{ github.ref_name }}-${{ github.run_id }}
+ restore-keys: |
+ ${{ runner.os }}-activity-report-logs-` + repoSlug + `-
+ ${{ runner.os }}-activity-report-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
+ 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();
+`)
+
// Add validate_workflows job for workflow_dispatch/workflow_call with operation == 'validate'
validateRunsOnValue := FormatRunsOn(nil, "ubuntu-latest")
yaml.WriteString(`
diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go
index dda9d21e4ef..9cdfcb3cec6 100644
--- a/pkg/workflow/side_repo_maintenance_integration_test.go
+++ b/pkg/workflow/side_repo_maintenance_integration_test.go
@@ -87,6 +87,20 @@ This workflow operates on a separate repository.
assert.Contains(t, contentStr, "create_labels:",
"generated workflow should include create_labels job")
+ // 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",
+ "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")
+ assert.Contains(t, contentStr, "timeout-minutes: 120",
+ "activity_report job should include a 2 hour timeout")
+ assert.Contains(t, contentStr, "${{ github.run_id }}",
+ "activity_report cache key should include run id for latest-cache resolution")
+
// GH_AW_TARGET_REPO_SLUG must be wired with the correct slug.
assert.Contains(t, contentStr, `GH_AW_TARGET_REPO_SLUG: "my-org/target-repo"`,
"GH_AW_TARGET_REPO_SLUG should be set to the target repo slug")