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
40 changes: 36 additions & 4 deletions pkg/cli/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ Examples:
` + constants.CLIExtensionPrefix + ` logs --start-date -1mo # Filter runs from last month
` + constants.CLIExtensionPrefix + ` logs --engine claude # Filter logs by claude engine
` + constants.CLIExtensionPrefix + ` logs --engine codex # Filter logs by codex engine
` + constants.CLIExtensionPrefix + ` logs --branch main # Filter logs by branch name
` + constants.CLIExtensionPrefix + ` logs --branch feature-xyz # Filter logs by feature branch
` + constants.CLIExtensionPrefix + ` logs --after-run-id 1000 # Filter runs after run ID 1000
` + constants.CLIExtensionPrefix + ` logs --before-run-id 2000 # Filter runs before run ID 2000
` + constants.CLIExtensionPrefix + ` logs --after-run-id 1000 --before-run-id 2000 # Filter runs in range
` + constants.CLIExtensionPrefix + ` logs -o ./my-logs # Custom output directory
` + constants.CLIExtensionPrefix + ` logs --tool-graph # Generate Mermaid tool sequence graph`,
Run: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -180,6 +185,9 @@ Examples:
endDate, _ := cmd.Flags().GetString("end-date")
outputDir, _ := cmd.Flags().GetString("output")
engine, _ := cmd.Flags().GetString("engine")
branch, _ := cmd.Flags().GetString("branch")
beforeRunID, _ := cmd.Flags().GetInt64("before-run-id")
afterRunID, _ := cmd.Flags().GetInt64("after-run-id")
verbose, _ := cmd.Flags().GetBool("verbose")
toolGraph, _ := cmd.Flags().GetBool("tool-graph")
noStaged, _ := cmd.Flags().GetBool("no-staged")
Expand Down Expand Up @@ -222,7 +230,7 @@ Examples:
}
}

if err := DownloadWorkflowLogs(workflowName, count, startDate, endDate, outputDir, engine, verbose, toolGraph, noStaged); err != nil {
if err := DownloadWorkflowLogs(workflowName, count, startDate, endDate, outputDir, engine, branch, beforeRunID, afterRunID, verbose, toolGraph, noStaged); err != nil {
fmt.Fprintln(os.Stderr, console.FormatError(console.CompilerError{
Type: "error",
Message: err.Error(),
Expand All @@ -238,6 +246,9 @@ Examples:
logsCmd.Flags().String("end-date", "", "Filter runs created before this date (YYYY-MM-DD or delta like -1d, -1w, -1mo)")
logsCmd.Flags().StringP("output", "o", "./logs", "Output directory for downloaded logs and artifacts")
logsCmd.Flags().String("engine", "", "Filter logs by agentic engine type (claude, codex)")
logsCmd.Flags().String("branch", "", "Filter runs by branch name (e.g., main, feature-branch)")
logsCmd.Flags().Int64("before-run-id", 0, "Filter runs with database ID before this value (exclusive)")
logsCmd.Flags().Int64("after-run-id", 0, "Filter runs with database ID after this value (exclusive)")
logsCmd.Flags().BoolP("verbose", "v", false, "Show individual tool names instead of grouping by MCP server")
logsCmd.Flags().Bool("tool-graph", false, "Generate Mermaid tool sequence graph from agent logs")
logsCmd.Flags().Bool("no-staged", false, "Filter out staged workflow runs (exclude runs with staged: true in aw_info.json)")
Expand All @@ -246,7 +257,7 @@ Examples:
}

// DownloadWorkflowLogs downloads and analyzes workflow logs with metrics
func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, outputDir, engine string, verbose bool, toolGraph bool, noStaged bool) error {
func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, outputDir, engine, branch string, beforeRunID, afterRunID int64, verbose bool, toolGraph bool, noStaged bool) error {
if verbose {
fmt.Println(console.FormatInfoMessage("Fetching workflow runs from GitHub Actions..."))
}
Expand Down Expand Up @@ -284,7 +295,7 @@ func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, ou
}
}

runs, err := listWorkflowRunsWithPagination(workflowName, batchSize, startDate, endDate, beforeDate, verbose)
runs, err := listWorkflowRunsWithPagination(workflowName, batchSize, startDate, endDate, beforeDate, branch, beforeRunID, afterRunID, verbose)
if err != nil {
return err
}
Expand Down Expand Up @@ -555,7 +566,7 @@ func downloadRunArtifactsConcurrent(runs []WorkflowRun, outputDir string, verbos
}

// listWorkflowRunsWithPagination fetches workflow runs from GitHub with pagination support
func listWorkflowRunsWithPagination(workflowName string, count int, startDate, endDate, beforeDate string, verbose bool) ([]WorkflowRun, error) {
func listWorkflowRunsWithPagination(workflowName string, count int, startDate, endDate, beforeDate, branch string, beforeRunID, afterRunID int64, verbose bool) ([]WorkflowRun, error) {
args := []string{"run", "list", "--json", "databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle"}

// Add filters
Expand All @@ -575,6 +586,10 @@ func listWorkflowRunsWithPagination(workflowName string, count int, startDate, e
if beforeDate != "" {
args = append(args, "--created", "<"+beforeDate)
}
// Add branch filter
if branch != "" {
args = append(args, "--branch", branch)
}

if verbose {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Executing: gh %s", strings.Join(args, " "))))
Expand Down Expand Up @@ -641,6 +656,23 @@ func listWorkflowRunsWithPagination(workflowName string, count int, startDate, e
agenticRuns = runs
}

// Apply run ID filtering if specified
if beforeRunID > 0 || afterRunID > 0 {
var filteredRuns []WorkflowRun
for _, run := range agenticRuns {
// Apply before-run-id filter (exclusive)
if beforeRunID > 0 && run.DatabaseID >= beforeRunID {
continue
}
// Apply after-run-id filter (exclusive)
if afterRunID > 0 && run.DatabaseID <= afterRunID {
continue
}
filteredRuns = append(filteredRuns, run)
}
agenticRuns = filteredRuns
}

return agenticRuns, nil
}

Expand Down
136 changes: 132 additions & 4 deletions pkg/cli/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestDownloadWorkflowLogs(t *testing.T) {
// Test the DownloadWorkflowLogs function
// This should either fail with auth error (if not authenticated)
// or succeed with no results (if authenticated but no workflows match)
err := DownloadWorkflowLogs("", 1, "", "", "./test-logs", "", false, false, false)
err := DownloadWorkflowLogs("", 1, "", "", "./test-logs", "", "", 0, 0, false, false, false)

// If GitHub CLI is authenticated, the function may succeed but find no results
// If not authenticated, it should return an auth error
Expand Down Expand Up @@ -359,7 +359,7 @@ func TestListWorkflowRunsWithPagination(t *testing.T) {

// This should fail with authentication error (if not authenticated)
// or succeed with empty results (if authenticated but no workflows match)
runs, err := listWorkflowRunsWithPagination("nonexistent-workflow", 5, "", "", "2024-01-01T00:00:00Z", false)
runs, err := listWorkflowRunsWithPagination("nonexistent-workflow", 5, "", "", "2024-01-01T00:00:00Z", "", 0, 0, false)

if err != nil {
// If there's an error, it should be an authentication error or workflow not found
Expand Down Expand Up @@ -793,7 +793,7 @@ func TestDownloadWorkflowLogsWithEngineFilter(t *testing.T) {
if !tt.expectError {
// For valid engines, test that the function can be called without panic
// It may still fail with auth errors, which is expected
err := DownloadWorkflowLogs("", 1, "", "", "./test-logs", tt.engine, false, false, false)
err := DownloadWorkflowLogs("", 1, "", "", "./test-logs", tt.engine, "", 0, 0, false, false, false)

// Clean up any created directories
os.RemoveAll("./test-logs")
Expand All @@ -814,7 +814,7 @@ func TestLogsCommandFlags(t *testing.T) {
cmd := NewLogsCommand()

// Check that all expected flags are present
expectedFlags := []string{"count", "start-date", "end-date", "output", "engine"}
expectedFlags := []string{"count", "start-date", "end-date", "output", "engine", "branch", "before-run-id", "after-run-id"}

for _, flagName := range expectedFlags {
flag := cmd.Flags().Lookup(flagName)
Expand All @@ -823,6 +823,40 @@ func TestLogsCommandFlags(t *testing.T) {
}
}

// Test branch flag specifically
branchFlag := cmd.Flags().Lookup("branch")
if branchFlag == nil {
t.Fatal("Branch flag not found")
}

if branchFlag.Usage != "Filter runs by branch name (e.g., main, feature-branch)" {
t.Errorf("Unexpected branch flag usage text: %s", branchFlag.Usage)
}

if branchFlag.DefValue != "" {
t.Errorf("Expected branch flag default value to be empty, got: %s", branchFlag.DefValue)
}

// Test before-run-id flag
beforeRunIDFlag := cmd.Flags().Lookup("before-run-id")
if beforeRunIDFlag == nil {
t.Fatal("Before-run-id flag not found")
}

if beforeRunIDFlag.Usage != "Filter runs with database ID before this value (exclusive)" {
t.Errorf("Unexpected before-run-id flag usage text: %s", beforeRunIDFlag.Usage)
}

// Test after-run-id flag
afterRunIDFlag := cmd.Flags().Lookup("after-run-id")
if afterRunIDFlag == nil {
t.Fatal("After-run-id flag not found")
}

if afterRunIDFlag.Usage != "Filter runs with database ID after this value (exclusive)" {
t.Errorf("Unexpected after-run-id flag usage text: %s", afterRunIDFlag.Usage)
}

// Test engine flag specifically
engineFlag := cmd.Flags().Lookup("engine")
if engineFlag == nil {
Expand Down Expand Up @@ -956,3 +990,97 @@ Now I'll implement the solution.
t.Errorf("Expected error count 1 from generic logs, got %d", metrics.ErrorCount)
}
}

func TestRunIDFilteringLogic(t *testing.T) {
// Test the run ID filtering logic in isolation
testRuns := []WorkflowRun{
{DatabaseID: 1000, WorkflowName: "Test Workflow"},
{DatabaseID: 1500, WorkflowName: "Test Workflow"},
{DatabaseID: 2000, WorkflowName: "Test Workflow"},
{DatabaseID: 2500, WorkflowName: "Test Workflow"},
{DatabaseID: 3000, WorkflowName: "Test Workflow"},
}

// Test before-run-id filter (exclusive)
var filteredRuns []WorkflowRun
beforeRunID := int64(2000)
for _, run := range testRuns {
if beforeRunID > 0 && run.DatabaseID >= beforeRunID {
continue
}
filteredRuns = append(filteredRuns, run)
}

if len(filteredRuns) != 2 {
t.Errorf("Expected 2 runs before ID 2000 (exclusive), got %d", len(filteredRuns))
}
if filteredRuns[0].DatabaseID != 1000 || filteredRuns[1].DatabaseID != 1500 {
t.Errorf("Expected runs 1000 and 1500, got %d and %d", filteredRuns[0].DatabaseID, filteredRuns[1].DatabaseID)
}

// Test after-run-id filter (exclusive)
filteredRuns = nil
afterRunID := int64(2000)
for _, run := range testRuns {
if afterRunID > 0 && run.DatabaseID <= afterRunID {
continue
}
filteredRuns = append(filteredRuns, run)
}

if len(filteredRuns) != 2 {
t.Errorf("Expected 2 runs after ID 2000 (exclusive), got %d", len(filteredRuns))
}
if filteredRuns[0].DatabaseID != 2500 || filteredRuns[1].DatabaseID != 3000 {
t.Errorf("Expected runs 2500 and 3000, got %d and %d", filteredRuns[0].DatabaseID, filteredRuns[1].DatabaseID)
}

// Test range filter (both before and after)
filteredRuns = nil
beforeRunID = int64(2500)
afterRunID = int64(1000)
for _, run := range testRuns {
// Apply before-run-id filter (exclusive)
if beforeRunID > 0 && run.DatabaseID >= beforeRunID {
continue
}
// Apply after-run-id filter (exclusive)
if afterRunID > 0 && run.DatabaseID <= afterRunID {
continue
}
filteredRuns = append(filteredRuns, run)
}

if len(filteredRuns) != 2 {
t.Errorf("Expected 2 runs in range (1000, 2500), got %d", len(filteredRuns))
}
if filteredRuns[0].DatabaseID != 1500 || filteredRuns[1].DatabaseID != 2000 {
t.Errorf("Expected runs 1500 and 2000, got %d and %d", filteredRuns[0].DatabaseID, filteredRuns[1].DatabaseID)
}
}

func TestBranchFilteringWithGitHubCLI(t *testing.T) {
// Test that branch filtering is properly added to GitHub CLI args
// This is a unit test for the args construction, not a network test

// Simulate args construction for branch filtering
args := []string{"run", "list", "--json", "databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle"}

branch := "feature-branch"
if branch != "" {
args = append(args, "--branch", branch)
}

// Verify that the branch filter was added correctly
found := false
for i, arg := range args {
if arg == "--branch" && i+1 < len(args) && args[i+1] == "feature-branch" {
found = true
break
}
}

if !found {
t.Errorf("Expected branch filter '--branch feature-branch' not found in args: %v", args)
}
}
Loading