diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index 6bbe7a5e5e4..c3c08953aad 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() && steps.collect_output.outputs.output != '' + 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..728b92880cc 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() && steps.collect_output.outputs.output != '' + 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..831923f7ce4 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() && steps.collect_output.outputs.output != '' + 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..01037549073 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() && steps.collect_output.outputs.output != '' + 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/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) + } +} diff --git a/pkg/workflow/agentic_output_test.go b/pkg/workflow/agentic_output_test.go index 6760b4da142..42dc55e1dca 100644 --- a/pkg/workflow/agentic_output_test.go +++ b/pkg/workflow/agentic_output_test.go @@ -117,10 +117,39 @@ 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 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") 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 +159,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 +171,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..834c98c0472 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() && 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") + yaml.WriteString(" path: ${{ env.GITHUB_AW_OUTPUT }}\n") + yaml.WriteString(" if-no-files-found: warn\n") }