From 0e5d50a3c911ac673d39ed8a4cbf4e076127d584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:11:26 +0000 Subject: [PATCH 1/4] Initial plan From d8ac23672b63fd2ac9cbb435f1a3345650baf916 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:21:33 +0000 Subject: [PATCH 2/4] Add GITHUB_AW_OUTPUT artifact upload to workflow compiler Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/issue-triage.lock.yml | 7 +++++ .github/workflows/test-claude.lock.yml | 7 +++++ .github/workflows/test-codex.lock.yml | 7 +++++ .github/workflows/weekly-research.lock.yml | 7 +++++ pkg/workflow/agentic_output_test.go | 34 ++++++++++++++++++++-- pkg/workflow/compiler.go | 7 +++++ 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index 6bbe7a5e5e4..a88ab989f33 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -402,6 +402,13 @@ jobs: echo '``````markdown' >> $GITHUB_STEP_SUMMARY cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_output.txt + path: ${{ env.GITHUB_AW_OUTPUT }} + if-no-files-found: warn - name: Check if workflow-complete.txt exists, if so upload it id: check_file run: | diff --git a/.github/workflows/test-claude.lock.yml b/.github/workflows/test-claude.lock.yml index c055e0cdef2..c515946eb23 100644 --- a/.github/workflows/test-claude.lock.yml +++ b/.github/workflows/test-claude.lock.yml @@ -418,6 +418,13 @@ jobs: echo '``````markdown' >> $GITHUB_STEP_SUMMARY cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_output.txt + path: ${{ env.GITHUB_AW_OUTPUT }} + if-no-files-found: warn - name: Check if workflow-complete.txt exists, if so upload it id: check_file run: | diff --git a/.github/workflows/test-codex.lock.yml b/.github/workflows/test-codex.lock.yml index a9fb5d400ff..718fa129848 100644 --- a/.github/workflows/test-codex.lock.yml +++ b/.github/workflows/test-codex.lock.yml @@ -339,6 +339,13 @@ jobs: echo '``````markdown' >> $GITHUB_STEP_SUMMARY cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_output.txt + path: ${{ env.GITHUB_AW_OUTPUT }} + if-no-files-found: warn - name: Check if workflow-complete.txt exists, if so upload it id: check_file run: | diff --git a/.github/workflows/weekly-research.lock.yml b/.github/workflows/weekly-research.lock.yml index da8a7413a3a..a2a3b5c3901 100644 --- a/.github/workflows/weekly-research.lock.yml +++ b/.github/workflows/weekly-research.lock.yml @@ -371,6 +371,13 @@ jobs: echo '``````markdown' >> $GITHUB_STEP_SUMMARY cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY + - name: Upload agentic output file + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_output.txt + path: ${{ env.GITHUB_AW_OUTPUT }} + if-no-files-found: warn - name: Check if workflow-complete.txt exists, if so upload it id: check_file run: | diff --git a/pkg/workflow/agentic_output_test.go b/pkg/workflow/agentic_output_test.go index 6760b4da142..da2f68e938d 100644 --- a/pkg/workflow/agentic_output_test.go +++ b/pkg/workflow/agentic_output_test.go @@ -117,10 +117,34 @@ This workflow tests the agentic output collection functionality. t.Error("Expected job output declaration for 'output'") } + // Verify artifact upload step: Upload agentic output file step exists + if !strings.Contains(lockContent, "- name: Upload agentic output file") { + t.Error("Expected 'Upload agentic output file' step to be in generated workflow") + } + + // Verify the upload step uses actions/upload-artifact@v4 + if !strings.Contains(lockContent, "uses: actions/upload-artifact@v4") { + t.Error("Expected upload-artifact action to be used for artifact upload step") + } + + // Verify the artifact upload configuration + if !strings.Contains(lockContent, "name: aw_output.txt") { + t.Error("Expected artifact name to be 'aw_output.txt'") + } + + if !strings.Contains(lockContent, "path: ${{ env.GITHUB_AW_OUTPUT }}") { + t.Error("Expected artifact path to use GITHUB_AW_OUTPUT environment variable") + } + + if !strings.Contains(lockContent, "if-no-files-found: warn") { + t.Error("Expected if-no-files-found: warn configuration for artifact upload") + } + // Verify step order: setup should come before agentic execution, collection should come after setupIndex := strings.Index(lockContent, "- name: Setup Agent Output File (GITHUB_AW_OUTPUT)") executeIndex := strings.Index(lockContent, "- name: Execute Claude Code") collectIndex := strings.Index(lockContent, "- name: Collect agentic output") + uploadIndex := strings.Index(lockContent, "- name: Upload agentic output file") // If "Execute Claude Code" isn't found, try alternative step names if executeIndex == -1 { @@ -130,7 +154,7 @@ This workflow tests the agentic output collection functionality. executeIndex = strings.Index(lockContent, "uses: githubnext/claude-action") } - if setupIndex == -1 || executeIndex == -1 || collectIndex == -1 { + if setupIndex == -1 || executeIndex == -1 || collectIndex == -1 || uploadIndex == -1 { t.Fatal("Could not find expected steps in generated workflow") } @@ -142,6 +166,10 @@ This workflow tests the agentic output collection functionality. t.Error("Collection step should appear after agentic execution step") } - t.Logf("Step order verified: Setup (%d) < Execute (%d) < Collect (%d)", - setupIndex, executeIndex, collectIndex) + if uploadIndex <= collectIndex { + t.Error("Upload step should appear after collection step") + } + + t.Logf("Step order verified: Setup (%d) < Execute (%d) < Collect (%d) < Upload (%d)", + setupIndex, executeIndex, collectIndex, uploadIndex) } diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 3c22b7f82cd..bfb2040ed34 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -2322,6 +2322,13 @@ func (c *Compiler) generateOutputCollectionStep(yaml *strings.Builder, data *Wor yaml.WriteString(" echo '``````markdown' >> $GITHUB_STEP_SUMMARY\n") yaml.WriteString(" cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY\n") yaml.WriteString(" echo '``````' >> $GITHUB_STEP_SUMMARY\n") + yaml.WriteString(" - name: Upload agentic output file\n") + yaml.WriteString(" if: always()\n") + yaml.WriteString(" uses: actions/upload-artifact@v4\n") + yaml.WriteString(" with:\n") + yaml.WriteString(" name: aw_output.txt\n") + yaml.WriteString(" path: ${{ env.GITHUB_AW_OUTPUT }}\n") + yaml.WriteString(" if-no-files-found: warn\n") } From c01a83d0c84255c1e9d9f0f5aa7ab49360297be7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:45:22 +0000 Subject: [PATCH 3/4] Add non-empty output file check to artifact upload step - Modified upload step condition from `if: always()` to `if: always() && steps.collect_output.outputs.output != ''` - Only uploads artifact when the collection step found non-empty content - Updated test to verify the new condition is present - Recompiled all existing workflow lock files with the new condition Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/issue-triage.lock.yml | 2 +- .github/workflows/test-claude.lock.yml | 2 +- .github/workflows/test-codex.lock.yml | 2 +- .github/workflows/weekly-research.lock.yml | 2 +- pkg/workflow/agentic_output_test.go | 5 +++++ pkg/workflow/compiler.go | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index a88ab989f33..c3c08953aad 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -403,7 +403,7 @@ jobs: cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - name: Upload agentic output file - if: always() + if: always() && steps.collect_output.outputs.output != '' uses: actions/upload-artifact@v4 with: name: aw_output.txt diff --git a/.github/workflows/test-claude.lock.yml b/.github/workflows/test-claude.lock.yml index c515946eb23..728b92880cc 100644 --- a/.github/workflows/test-claude.lock.yml +++ b/.github/workflows/test-claude.lock.yml @@ -419,7 +419,7 @@ jobs: cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - name: Upload agentic output file - if: always() + if: always() && steps.collect_output.outputs.output != '' uses: actions/upload-artifact@v4 with: name: aw_output.txt diff --git a/.github/workflows/test-codex.lock.yml b/.github/workflows/test-codex.lock.yml index 718fa129848..831923f7ce4 100644 --- a/.github/workflows/test-codex.lock.yml +++ b/.github/workflows/test-codex.lock.yml @@ -340,7 +340,7 @@ jobs: cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - name: Upload agentic output file - if: always() + if: always() && steps.collect_output.outputs.output != '' uses: actions/upload-artifact@v4 with: name: aw_output.txt diff --git a/.github/workflows/weekly-research.lock.yml b/.github/workflows/weekly-research.lock.yml index a2a3b5c3901..01037549073 100644 --- a/.github/workflows/weekly-research.lock.yml +++ b/.github/workflows/weekly-research.lock.yml @@ -372,7 +372,7 @@ jobs: cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY echo '``````' >> $GITHUB_STEP_SUMMARY - name: Upload agentic output file - if: always() + if: always() && steps.collect_output.outputs.output != '' uses: actions/upload-artifact@v4 with: name: aw_output.txt diff --git a/pkg/workflow/agentic_output_test.go b/pkg/workflow/agentic_output_test.go index da2f68e938d..42dc55e1dca 100644 --- a/pkg/workflow/agentic_output_test.go +++ b/pkg/workflow/agentic_output_test.go @@ -140,6 +140,11 @@ This workflow tests the agentic output collection functionality. t.Error("Expected if-no-files-found: warn configuration for artifact upload") } + // Verify the upload step condition checks for non-empty output + if !strings.Contains(lockContent, "if: always() && steps.collect_output.outputs.output != ''") { + t.Error("Expected upload step to check for non-empty output from collection step") + } + // Verify step order: setup should come before agentic execution, collection should come after setupIndex := strings.Index(lockContent, "- name: Setup Agent Output File (GITHUB_AW_OUTPUT)") executeIndex := strings.Index(lockContent, "- name: Execute Claude Code") diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index bfb2040ed34..834c98c0472 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -2323,7 +2323,7 @@ func (c *Compiler) generateOutputCollectionStep(yaml *strings.Builder, data *Wor yaml.WriteString(" cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY\n") yaml.WriteString(" echo '``````' >> $GITHUB_STEP_SUMMARY\n") yaml.WriteString(" - name: Upload agentic output file\n") - yaml.WriteString(" if: always()\n") + yaml.WriteString(" if: always() && steps.collect_output.outputs.output != ''\n") yaml.WriteString(" uses: actions/upload-artifact@v4\n") yaml.WriteString(" with:\n") yaml.WriteString(" name: aw_output.txt\n") From 9930c9d1da97f7efd67c68b0aba7b1e728491641 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:58:49 +0000 Subject: [PATCH 4/4] Make logs command aware of new aw_output.txt artifact Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs.go | 43 +++++++++++++++++++++++++++++++++++ pkg/cli/logs_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/pkg/cli/logs.go b/pkg/cli/logs.go index b6b289562bb..45ceb3c8164 100644 --- a/pkg/cli/logs.go +++ b/pkg/cli/logs.go @@ -81,6 +81,11 @@ This command fetches workflow runs, downloads their artifacts, and extracts them organized folders named by run ID. It also provides an overview table with aggregate metrics including duration, token usage, and cost information. +Downloaded artifacts include: +- aw_info.json: Engine configuration and workflow metadata +- aw_output.txt: Agent's final output content (available when non-empty) +- Various log files with execution details and metrics + The agentic-workflow-id is the basename of the markdown file without the .md extension. For example, for 'weekly-research.md', use 'weekly-research' as the workflow ID. @@ -591,6 +596,18 @@ func extractLogMetrics(logDir string, verbose bool) (LogMetrics, error) { } } + // Check for aw_output.txt artifact file + awOutputPath := filepath.Join(logDir, "aw_output.txt") + if _, err := os.Stat(awOutputPath); err == nil { + if verbose { + // Report that the agentic output file was found + fileInfo, statErr := os.Stat(awOutputPath) + if statErr == nil { + fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Found agentic output file: aw_output.txt (%s)", formatFileSize(fileInfo.Size())))) + } + } + } + // Walk through all files in the log directory err := filepath.Walk(logDir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -864,6 +881,32 @@ func formatNumber(n int) string { } } +// formatFileSize formats file sizes in a human-readable way (e.g., "1.2 KB", "3.4 MB") +func formatFileSize(size int64) string { + if size == 0 { + return "0 B" + } + + const unit = 1024 + if size < unit { + return fmt.Sprintf("%d B", size) + } + + div, exp := int64(unit), 0 + for n := size / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + + units := []string{"KB", "MB", "GB", "TB"} + if exp >= len(units) { + exp = len(units) - 1 + div = int64(1) << (10 * (exp + 1)) + } + + return fmt.Sprintf("%.1f %s", float64(size)/float64(div), units[exp]) +} + // dirExists checks if a directory exists func dirExists(path string) bool { info, err := os.Stat(path) diff --git a/pkg/cli/logs_test.go b/pkg/cli/logs_test.go index 79f4810b430..4e3726f1631 100644 --- a/pkg/cli/logs_test.go +++ b/pkg/cli/logs_test.go @@ -817,3 +817,57 @@ func TestLogsCommandFlags(t *testing.T) { t.Errorf("Expected engine flag default value to be empty, got: %s", engineFlag.DefValue) } } + +func TestFormatFileSize(t *testing.T) { + tests := []struct { + size int64 + expected string + }{ + {0, "0 B"}, + {100, "100 B"}, + {1024, "1.0 KB"}, + {1536, "1.5 KB"}, // 1.5 * 1024 + {1048576, "1.0 MB"}, // 1024 * 1024 + {2097152, "2.0 MB"}, // 2 * 1024 * 1024 + {1073741824, "1.0 GB"}, // 1024^3 + {1099511627776, "1.0 TB"}, // 1024^4 + } + + for _, tt := range tests { + result := formatFileSize(tt.size) + if result != tt.expected { + t.Errorf("formatFileSize(%d) = %q, expected %q", tt.size, result, tt.expected) + } + } +} + +func TestExtractLogMetricsWithAwOutputFile(t *testing.T) { + // Create a temporary directory with aw_output.txt + tmpDir := t.TempDir() + + // Create aw_output.txt file + awOutputPath := filepath.Join(tmpDir, "aw_output.txt") + awOutputContent := "This is the agent's output content.\nIt contains multiple lines." + err := os.WriteFile(awOutputPath, []byte(awOutputContent), 0644) + if err != nil { + t.Fatalf("Failed to create aw_output.txt: %v", err) + } + + // Test that extractLogMetrics doesn't fail with aw_output.txt present + metrics, err := extractLogMetrics(tmpDir, false) + if err != nil { + t.Fatalf("extractLogMetrics failed: %v", err) + } + + // Without an engine, should return empty metrics but not error + if metrics.TokenUsage != 0 { + t.Errorf("Expected token usage 0 (no engine), got %d", metrics.TokenUsage) + } + + // Test verbose mode to ensure it detects the file + // We can't easily test the console output, but we can ensure it doesn't error + metrics, err = extractLogMetrics(tmpDir, true) + if err != nil { + t.Fatalf("extractLogMetrics in verbose mode failed: %v", err) + } +}