Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/issue-triage.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .github/workflows/test-claude.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .github/workflows/test-codex.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .github/workflows/weekly-research.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions pkg/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions pkg/cli/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
39 changes: 36 additions & 3 deletions pkg/workflow/agentic_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
}

Expand All @@ -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)
}
7 changes: 7 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

}

Expand Down