From c03bf95221ba8c2ad8e8b43d962d221aa3ec26fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:39:57 +0000 Subject: [PATCH 1/4] fix: add --report-file flag to gh-aw logs to avoid shell redirect failure The activity_report job in agentics-maintenance.yml failed with: "No such file or directory" when trying to redirect stdout to ./.cache/gh-aw/activity-report-logs/report.md Root cause: bash evaluates the `>` redirect before the command runs, so the output directory must already exist. When the cache is not restored the directory doesn't exist yet. Fix: add --report-file flag to `gh-aw logs` that writes the markdown output directly to a file (creating parent directories via constants.DirPermPublic), removing the shell redirect entirely. Updated templates: maintenance_workflow_yaml.go, side_repo_maintenance.go, and agentics-maintenance.yml all use --report-file instead of `>`. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 2 +- pkg/cli/logs_command.go | 5 +++++ pkg/cli/logs_orchestrator.go | 22 +++++++++++++++++++++- pkg/workflow/maintenance_workflow_yaml.go | 2 +- pkg/workflow/side_repo_maintenance.go | 2 +- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 04c6d5c02c1..75e7134023e 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -427,7 +427,7 @@ jobs: --count 100 \ --output ./.cache/gh-aw/activity-report-logs \ --format markdown \ - > ./.cache/gh-aw/activity-report-logs/report.md + --report-file ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} diff --git a/pkg/cli/logs_command.go b/pkg/cli/logs_command.go index 514110610e0..24e5e1774fd 100644 --- a/pkg/cli/logs_command.go +++ b/pkg/cli/logs_command.go @@ -153,6 +153,7 @@ Downloaded artifacts include (when using --artifacts all): filteredIntegrity, _ := cmd.Flags().GetBool("filtered-integrity") train, _ := cmd.Flags().GetBool("train") format, _ := cmd.Flags().GetString("format") + reportFile, _ := cmd.Flags().GetString("report-file") artifacts, _ := cmd.Flags().GetStringSlice("artifacts") if engine != "" { @@ -182,6 +183,7 @@ Downloaded artifacts include (when using --artifacts all): FilteredIntegrity: filteredIntegrity, Train: train, Format: format, + ReportFile: reportFile, ArtifactSets: artifacts, }) } @@ -268,6 +270,7 @@ Downloaded artifacts include (when using --artifacts all): filteredIntegrity, _ := cmd.Flags().GetBool("filtered-integrity") train, _ := cmd.Flags().GetBool("train") format, _ := cmd.Flags().GetString("format") + reportFile, _ := cmd.Flags().GetString("report-file") artifacts, _ := cmd.Flags().GetStringSlice("artifacts") cacheBefore, _ := cmd.Flags().GetString("cache-before") if !cmd.Flags().Changed("cache-before") { @@ -333,6 +336,7 @@ Downloaded artifacts include (when using --artifacts all): FilteredIntegrity: filteredIntegrity, Train: train, Format: format, + ReportFile: reportFile, ArtifactSets: artifacts, After: cacheBefore, }) @@ -361,6 +365,7 @@ Downloaded artifacts include (when using --artifacts all): logsCmd.Flags().String("summary-file", "summary.json", "Path to write the summary JSON file relative to output directory (use empty string to disable)") logsCmd.Flags().Bool("train", false, "Analyze log patterns across downloaded runs and save pattern weights to drain3_weights.json in the output directory") logsCmd.Flags().String("format", "", "Output format: console (decorated tables), tsv (tab-separated), pretty (cross-run report), markdown (cross-run Markdown). Default: compact agent-optimized output") + logsCmd.Flags().String("report-file", "", "Write --format markdown output directly to this file path instead of stdout (creates parent directories as needed)") logsCmd.Flags().Int("last", 0, "Alias for --count: number of recent runs to download") logsCmd.Flags().StringSlice("artifacts", []string{"usage"}, "Artifact sets to download (default: usage). Use 'all' for everything, or comma-separate sets. Valid sets: "+validArtifactSets) logsCmd.Flags().String("cache-before", "", "(Cache eviction) Evict locally cached run folders for runs before this date, prior to downloading. Accepts deltas like -1d, -1w, -1mo (or explicit day counts like -30d), or an absolute date YYYY-MM-DD. Unlike --start-date, this only clears local cache and does not filter which runs are fetched.") diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index 3c6d82ba3f0..b74bcb4b2e9 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -61,6 +61,7 @@ type LogsDownloadOptions struct { Format string ArtifactSets []string After string + ReportFile string } func shouldStopPagination(totalFetched, batchSize int) bool { @@ -634,6 +635,7 @@ func DownloadWorkflowLogs(ctx context.Context, opts LogsDownloadOptions) error { outputDir: outputDir, summaryFile: summaryFile, format: format, + reportFile: opts.ReportFile, jsonOutput: jsonOutput, toolGraph: toolGraph, train: train, @@ -648,6 +650,7 @@ type renderLogsOutputOptions struct { outputDir string summaryFile string format string + reportFile string jsonOutput bool toolGraph bool train bool @@ -730,7 +733,22 @@ func renderLogsOutput(processedRuns []ProcessedRun, opts renderLogsOutputOptions renderLogsArtifactHint(os.Stderr, logsData.Message) return nil } - renderCrossRunReportMarkdown(report) + if opts.reportFile != "" { + if err := os.MkdirAll(filepath.Dir(opts.reportFile), constants.DirPermPublic); err != nil { + return fmt.Errorf("failed to create report file directory: %w", err) + } + f, err := os.Create(opts.reportFile) + if err != nil { + return fmt.Errorf("failed to create report file: %w", err) + } + defer f.Close() //nolint:errcheck + oldStdout := os.Stdout + os.Stdout = f + renderCrossRunReportMarkdown(report) + os.Stdout = oldStdout + } else { + renderCrossRunReportMarkdown(report) + } renderLogsArtifactHint(os.Stderr, logsData.Message) return nil @@ -794,6 +812,7 @@ type StdinLogsOptions struct { FilteredIntegrity bool Train bool Format string + ReportFile string // ArtifactSets defaults to nil (download all artifacts) when this API is used // programmatically. The CLI passes ["usage"] to match the logs command default. ArtifactSets []string @@ -1110,6 +1129,7 @@ func DownloadWorkflowLogsFromStdin(ctx context.Context, opts StdinLogsOptions) e outputDir: opts.OutputDir, summaryFile: opts.SummaryFile, format: opts.Format, + reportFile: opts.ReportFile, jsonOutput: opts.JSONOutput, toolGraph: opts.ToolGraph, train: opts.Train, diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index 34ce21c6a73..784ad4f1668 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -511,7 +511,7 @@ jobs: --count 100 \ --output ./.cache/gh-aw/activity-report-logs \ --format markdown \ - > ./.cache/gh-aw/activity-report-logs/report.md + --report-file ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index b35c3d5c596..a3c16283cd9 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -520,7 +520,7 @@ jobs: --count 100 \ --output ./.cache/gh-aw/activity-report-logs \ --format markdown \ - > ./.cache/gh-aw/activity-report-logs/report.md + --report-file ./.cache/gh-aw/activity-report-logs/report.md - name: Save activity report logs cache if: ${{ always() }} From f23c6eacf6bf38a6d27f26aa10aae88f3997eef0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:45:26 +0000 Subject: [PATCH 2/4] fix: safe stdout restore and propagate close error in --report-file Use an IIFE so defer restores os.Stdout even on panic, and return the f.Close() error so incomplete writes (e.g. disk full) are surfaced. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs_orchestrator.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index b74bcb4b2e9..564f79d446a 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -741,11 +741,15 @@ func renderLogsOutput(processedRuns []ProcessedRun, opts renderLogsOutputOptions if err != nil { return fmt.Errorf("failed to create report file: %w", err) } - defer f.Close() //nolint:errcheck - oldStdout := os.Stdout - os.Stdout = f - renderCrossRunReportMarkdown(report) - os.Stdout = oldStdout + if err := func() error { + oldStdout := os.Stdout + defer func() { os.Stdout = oldStdout }() + os.Stdout = f + renderCrossRunReportMarkdown(report) + return f.Close() + }(); err != nil { + return fmt.Errorf("failed to write report file: %w", err) + } } else { renderCrossRunReportMarkdown(report) } From 6e20994becf11541f4b06c41d7d1c58559cbd77a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:15:38 +0000 Subject: [PATCH 3/4] fix: defer f.Close() for panic safety, add --report-file validation, add test Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs_command.go | 18 +++++++++++++ pkg/cli/logs_orchestrator.go | 9 +++++-- pkg/cli/logs_report_file_test.go | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 pkg/cli/logs_report_file_test.go diff --git a/pkg/cli/logs_command.go b/pkg/cli/logs_command.go index 24e5e1774fd..4b21ab79b80 100644 --- a/pkg/cli/logs_command.go +++ b/pkg/cli/logs_command.go @@ -165,6 +165,15 @@ Downloaded artifacts include (when using --artifacts all): } } + if reportFile != "" { + if format != "markdown" { + return errors.New("--report-file requires --format markdown") + } + if jsonOutput { + return errors.New("--report-file cannot be used with --json") + } + } + return DownloadWorkflowLogsFromStdin(cmd.Context(), StdinLogsOptions{ RunURLs: runURLs, OutputDir: outputDir, @@ -310,6 +319,15 @@ Downloaded artifacts include (when using --artifacts all): } } + if reportFile != "" { + if format != "markdown" { + return errors.New("--report-file requires --format markdown") + } + if jsonOutput { + return errors.New("--report-file cannot be used with --json") + } + } + logsCommandLog.Printf("Executing logs download: workflow=%s, count=%d, engine=%s, train=%v, cache_before=%s", workflowName, count, engine, train, cacheBefore) return DownloadWorkflowLogs(cmd.Context(), LogsDownloadOptions{ diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index 564f79d446a..952f689501d 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -741,12 +741,17 @@ func renderLogsOutput(processedRuns []ProcessedRun, opts renderLogsOutputOptions if err != nil { return fmt.Errorf("failed to create report file: %w", err) } - if err := func() error { + if err := func() (retErr error) { + defer func() { + if cerr := f.Close(); cerr != nil && retErr == nil { + retErr = cerr + } + }() oldStdout := os.Stdout defer func() { os.Stdout = oldStdout }() os.Stdout = f renderCrossRunReportMarkdown(report) - return f.Close() + return nil }(); err != nil { return fmt.Errorf("failed to write report file: %w", err) } diff --git a/pkg/cli/logs_report_file_test.go b/pkg/cli/logs_report_file_test.go new file mode 100644 index 00000000000..4806f07eb1f --- /dev/null +++ b/pkg/cli/logs_report_file_test.go @@ -0,0 +1,45 @@ +//go:build !integration + +package cli + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestRenderLogsOutputReportFileWritesMarkdownToFile verifies that when --report-file +// is set with --format markdown, the markdown report is written to the file and +// stdout receives no markdown report content. +func TestRenderLogsOutputReportFileWritesMarkdownToFile(t *testing.T) { + tmpDir := t.TempDir() + reportFile := filepath.Join(tmpDir, "report.md") + + processedRuns := []ProcessedRun{{ + Run: WorkflowRun{ + DatabaseID: 1, + Status: "completed", + WorkflowName: "logs", + CreatedAt: time.Now(), + }, + }} + + stdout, _ := captureOutput(t, func() error { + return renderLogsOutput(processedRuns, renderLogsOutputOptions{ + outputDir: tmpDir, + format: "markdown", + reportFile: reportFile, + artifactFilter: []string{"usage"}, + }) + }) + + assert.NotContains(t, stdout, "# Audit Report", "stdout should not contain the markdown report when --report-file is set") + + content, err := os.ReadFile(reportFile) + require.NoError(t, err, "report file should be created by renderLogsOutput") + assert.Contains(t, string(content), "# Audit Report — Cross-Run Analysis", "report file should contain the markdown report header") +} From 7832c597ad38830c53ee1c2111112a43b7c9cbde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:18:47 +0000 Subject: [PATCH 4/4] refactor: extract validateReportFileFlags helper to avoid duplication Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs_command.go | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pkg/cli/logs_command.go b/pkg/cli/logs_command.go index 4b21ab79b80..947a1386cae 100644 --- a/pkg/cli/logs_command.go +++ b/pkg/cli/logs_command.go @@ -165,13 +165,8 @@ Downloaded artifacts include (when using --artifacts all): } } - if reportFile != "" { - if format != "markdown" { - return errors.New("--report-file requires --format markdown") - } - if jsonOutput { - return errors.New("--report-file cannot be used with --json") - } + if err := validateReportFileFlags(reportFile, format, jsonOutput); err != nil { + return err } return DownloadWorkflowLogsFromStdin(cmd.Context(), StdinLogsOptions{ @@ -319,13 +314,8 @@ Downloaded artifacts include (when using --artifacts all): } } - if reportFile != "" { - if format != "markdown" { - return errors.New("--report-file requires --format markdown") - } - if jsonOutput { - return errors.New("--report-file cannot be used with --json") - } + if err := validateReportFileFlags(reportFile, format, jsonOutput); err != nil { + return err } logsCommandLog.Printf("Executing logs download: workflow=%s, count=%d, engine=%s, train=%v, cache_before=%s", workflowName, count, engine, train, cacheBefore) @@ -473,3 +463,19 @@ func repoIsLocal(repo string) bool { } return strings.EqualFold(ownerRepo, currentRepo) } + +// validateReportFileFlags returns an error if --report-file is combined with an +// incompatible flag. --report-file only takes effect for --format markdown output +// and is bypassed when --json is set. +func validateReportFileFlags(reportFile, format string, jsonOutput bool) error { + if reportFile == "" { + return nil + } + if format != "markdown" { + return errors.New("--report-file requires --format markdown") + } + if jsonOutput { + return errors.New("--report-file cannot be used with --json") + } + return nil +}