Skip to content

Commit b1ac6dd

Browse files
authored
Add workflow run ignore list to logs (#59697)
1 parent 2d8478f commit b1ac6dd

11 files changed

Lines changed: 189 additions & 46 deletions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# ADR-59697: Add ignore-workflow-runs to logs
2+
3+
**Date**: 2026-09-09
4+
**Status**: Draft
5+
**Deciders**: gh-aw maintainers
6+
7+
---
8+
9+
### Context
10+
11+
The `gh aw logs` command collects workflow runs and currently lets users constrain results with workflow, date, ref, and run ID filters, but it cannot explicitly skip known unwanted runs while still returning the requested count of useful results. This PR adds a new exclusion input that accepts workflow run IDs in plain numeric form or qualified `slug/ID` form, applies the exclusion before artifact processing, and preserves it in continuation state for resumed downloads. The implementation evidence shows a need to omit specific runs without shrinking the effective result set or forcing users to narrow broader filters. The repository needs an explicit decision on whether run exclusion should be treated as a first-class logs query parameter across CLI parsing, pagination, orchestration, and reporting.
12+
13+
### Decision
14+
15+
We will add an `--ignore-workflow-runs` option to `gh aw logs` that accepts positive workflow run database IDs, including qualified `slug/ID` inputs, normalizes them to unique numeric IDs, and excludes matching runs from collection. We will apply the ignore list before downstream artifact processing and persist it through orchestration and continuation payloads so resumed or paginated log downloads keep honoring the same exclusion set. We chose this approach because it gives users precise control over unwanted runs without changing the requested result count or overloading existing date, ref, or before/after run filters.
16+
17+
### Alternatives Considered
18+
19+
#### Alternative 1: Require users to refine existing filters such as `--ref`, `--before-run-id`, or `--after-run-id`
20+
21+
The project could continue relying on broader query filters and ask users to manually narrow the candidate run set. This was considered because it avoids adding a new option and reuses existing filtering semantics. It was not chosen because the PR evidence explicitly addresses the need to skip specific known runs while still preserving the overall count and broader search scope.
22+
23+
#### Alternative 2: Support exclusion only for raw numeric run IDs
24+
25+
Another option would be to accept only integer run IDs and reject qualified `slug/ID` inputs. This was considered because it would simplify parsing and validation. It was not chosen because the PR explicitly supports both numeric IDs and qualified values, which improves usability when users copy workflow run references from repository-qualified contexts.
26+
27+
#### Alternative 3: Exclude runs only in the final output after artifacts are processed
28+
29+
The implementation could defer exclusion until after run data is downloaded and assembled. This was considered because it would minimize changes to earlier pagination and orchestration code paths. It was not chosen because the PR description and code both indicate exclusions should happen before artifact processing so ignored runs do not consume result slots or processing work.
30+
31+
### Consequences
32+
33+
#### Positive
34+
- Users can omit known irrelevant or problematic workflow runs without reducing the requested number of collected results.
35+
- The same exclusion behavior is preserved across pagination and continuation data, making resumed downloads more predictable.
36+
- Supporting both numeric and `slug/ID` inputs reduces friction when specifying runs from copied GitHub references.
37+
38+
#### Negative
39+
- The logs query path becomes more complex because ignore-list parsing, deduplication, filtering, and continuation serialization must all stay in sync.
40+
- Invalid or non-positive run identifiers now introduce an additional user-facing validation failure mode.
41+
- Exclusion behavior must be covered in multiple test layers to avoid regressions across CLI and orchestration flows.
42+
43+
#### Neutral
44+
- `LogsDownloadOptions`, pagination options, and continuation payloads gain an `IgnoreWorkflowRuns` field.
45+
- Run filtering semantics now include explicit exclusions in addition to inclusive workflow, date, ref, and run ID bounds.
46+
- The implementation uses run database IDs as the stable identifier for exclusion across query and resume boundaries.
47+
48+
---
49+
50+
*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*

pkg/cli/logs_command.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"os"
1717
"path/filepath"
1818
"slices"
19+
"strconv"
1920
"strings"
2021
"time"
2122

@@ -429,6 +430,10 @@ func loadCommonLogsOptions(cmd *cobra.Command) (LogsDownloadOptions, error) {
429430
ArtifactSets: getStringSliceFlag(cmd, "artifacts"),
430431
CachedJSON: getStringFlag(cmd, "cached-json"),
431432
}
433+
options.IgnoreWorkflowRuns, err = parseIgnoredWorkflowRunIDs(getStringSliceFlag(cmd, "ignore-workflow-runs"))
434+
if err != nil {
435+
return LogsDownloadOptions{}, err
436+
}
432437
if err := validateLogsOptions(options); err != nil {
433438
return LogsDownloadOptions{}, err
434439
}
@@ -439,6 +444,25 @@ func loadCommonLogsOptions(cmd *cobra.Command) (LogsDownloadOptions, error) {
439444
return options, nil
440445
}
441446

447+
func parseIgnoredWorkflowRunIDs(values []string) ([]int64, error) {
448+
runIDs := make([]int64, 0, len(values))
449+
for _, value := range values {
450+
originalValue := value
451+
value = strings.TrimSpace(value)
452+
if index := strings.LastIndexByte(value, '/'); index >= 0 {
453+
value = value[index+1:]
454+
}
455+
runID, err := strconv.ParseInt(value, 10, 64)
456+
if err != nil || runID <= 0 {
457+
return nil, fmt.Errorf("invalid workflow run %q: expected a positive run ID or slug/ID (for example, 123 or github/gh-aw/123)", originalValue)
458+
}
459+
if !slices.Contains(runIDs, runID) {
460+
runIDs = append(runIDs, runID)
461+
}
462+
}
463+
return runIDs, nil
464+
}
465+
442466
func resolveLogsDateRange(startDate, endDate string, now time.Time) (string, string, error) {
443467
resolve := func(label, value string) (string, error) {
444468
if value == "" {
@@ -545,6 +569,7 @@ func addLogsCommandFlags(logsCmd *cobra.Command, validArtifactSets string) {
545569
logsCmd.Flags().String("ref", "", "Filter runs by branch or tag name (e.g., main, v1.0.0)")
546570
logsCmd.Flags().Int64("before-run-id", 0, "Filter runs with database ID before this value (exclusive)")
547571
logsCmd.Flags().Int64("after-run-id", 0, "Filter runs with database ID after this value (exclusive)")
572+
logsCmd.Flags().StringSlice("ignore-workflow-runs", nil, "Workflow run IDs or slug/ID values to exclude (slug is informational; matching uses the numeric ID)")
548573
addRepoFlag(logsCmd)
549574
logsCmd.Flags().Bool("tool-graph", false, "Generate Mermaid tool sequence graph from agent logs")
550575
logsCmd.Flags().Bool("exclude-staged", false, "Exclude workflow runs that executed in staged mode (safe outputs previewed but not applied)")

pkg/cli/logs_command_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ func TestNewLogsCommand(t *testing.T) {
6464
assert.NotNil(t, afterRunIDFlag, "Should have 'after-run-id' flag")
6565
beforeRunIDFlag := flags.Lookup("before-run-id")
6666
assert.NotNil(t, beforeRunIDFlag, "Should have 'before-run-id' flag")
67+
ignoreWorkflowRunsFlag := flags.Lookup("ignore-workflow-runs")
68+
assert.NotNil(t, ignoreWorkflowRunsFlag, "Should have 'ignore-workflow-runs' flag")
6769
lastFlag := flags.Lookup("last")
6870
assert.NotNil(t, lastFlag, "Should have 'last' flag")
6971
assert.Contains(t, lastFlag.Usage, "--count/-c", "--last usage should mention the canonical --count/-c flag")
@@ -331,6 +333,28 @@ func TestLogsCommandRunIDFilters(t *testing.T) {
331333
}
332334
}
333335

336+
func TestLogsCommandIgnoreWorkflowRuns(t *testing.T) {
337+
cmd := NewLogsCommand()
338+
require.NoError(t, cmd.Flags().Set("ignore-workflow-runs", "123,github/gh-aw/456,123"))
339+
340+
opts, err := loadCommonLogsOptions(cmd)
341+
342+
require.NoError(t, err)
343+
assert.Equal(t, []int64{123, 456}, opts.IgnoreWorkflowRuns)
344+
}
345+
346+
func TestLogsCommandRejectsInvalidIgnoredWorkflowRun(t *testing.T) {
347+
cmd := NewLogsCommand()
348+
require.NoError(t, cmd.Flags().Set("ignore-workflow-runs", "github/gh-aw/not-a-run"))
349+
350+
_, err := loadCommonLogsOptions(cmd)
351+
352+
require.Error(t, err)
353+
assert.Contains(t, err.Error(), "github/gh-aw/not-a-run")
354+
assert.Contains(t, err.Error(), "expected a positive run ID or slug/ID")
355+
assert.Contains(t, err.Error(), "123 or github/gh-aw/123")
356+
}
357+
334358
func TestLogsCommandOutputFlag(t *testing.T) {
335359
t.Parallel()
336360
cmd := NewLogsCommand()

pkg/cli/logs_github_api.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -365,17 +365,18 @@ func fetchJobStatuses(ctx context.Context, runID int64, verbose bool) (int, erro
365365

366366
// ListWorkflowRunsOptions holds the options for listWorkflowRunsWithPagination
367367
type ListWorkflowRunsOptions struct {
368-
Context context.Context
369-
WorkflowName string // filter by specific workflow (if empty, fetches all agentic workflows)
370-
Status string // filter by run status/conclusion (for example: completed, success, failure)
371-
Limit int // maximum number of runs to fetch in this API call (batch size)
372-
StartDate string // filter by creation date (>=); combined with EndDate/BeforeDate into a single --created range
373-
EndDate string // filter by creation date (<=); combined with StartDate into a single --created range
374-
BeforeDate string // exclusive upper bound used for pagination (<); combined with StartDate into a single --created range
375-
Ref string // filter by branch or tag name
376-
BeforeRunID int64 // filter by run database ID (< this ID)
377-
AfterRunID int64 // filter by run database ID (> this ID)
378-
RepoOverride string // fetch from a specific repository instead of current
368+
Context context.Context
369+
WorkflowName string // filter by specific workflow (if empty, fetches all agentic workflows)
370+
Status string // filter by run status/conclusion (for example: completed, success, failure)
371+
Limit int // maximum number of runs to fetch in this API call (batch size)
372+
StartDate string // filter by creation date (>=); combined with EndDate/BeforeDate into a single --created range
373+
EndDate string // filter by creation date (<=); combined with StartDate into a single --created range
374+
BeforeDate string // exclusive upper bound used for pagination (<); combined with StartDate into a single --created range
375+
Ref string // filter by branch or tag name
376+
BeforeRunID int64 // filter by run database ID (< this ID)
377+
AfterRunID int64 // filter by run database ID (> this ID)
378+
IgnoreWorkflowRuns []int64 // exclude these workflow run database IDs
379+
RepoOverride string // fetch from a specific repository instead of current
379380
// OldestFetchedCreatedAt, when set, is populated with the oldest run creation
380381
// timestamp returned by GitHub in this batch before any workflow/conclusion filtering.
381382
OldestFetchedCreatedAt *time.Time
@@ -569,6 +570,7 @@ func listWorkflowRunsWithPagination(opts ListWorkflowRunsOptions) ([]WorkflowRun
569570
if opts.BeforeRunID > 0 && run.DatabaseID >= opts.BeforeRunID {
570571
continue
571572
}
573+
572574
// Apply after-run-id filter (exclusive)
573575
if opts.AfterRunID > 0 && run.DatabaseID <= opts.AfterRunID {
574576
continue
@@ -578,6 +580,8 @@ func listWorkflowRunsWithPagination(opts ListWorkflowRunsOptions) ([]WorkflowRun
578580
agenticRuns = filteredRuns
579581
}
580582

583+
agenticRuns = filterIgnoredWorkflowRuns(agenticRuns, opts.IgnoreWorkflowRuns)
584+
581585
// Filter out runs that never dispatched an agentic job — skipped and
582586
// action_required runs carry no useful agentic data — along with cancelled
583587
// runs. None of them should count toward the requested run count.
@@ -595,6 +599,19 @@ func listWorkflowRunsWithPagination(opts ListWorkflowRunsOptions) ([]WorkflowRun
595599
return agenticRuns, totalFetched, nil
596600
}
597601

602+
func filterIgnoredWorkflowRuns(runs []WorkflowRun, ignoredRunIDs []int64) []WorkflowRun {
603+
if len(ignoredRunIDs) == 0 {
604+
return runs
605+
}
606+
filtered := make([]WorkflowRun, 0, len(runs))
607+
for _, run := range runs {
608+
if !slices.Contains(ignoredRunIDs, run.DatabaseID) {
609+
filtered = append(filtered, run)
610+
}
611+
}
612+
return filtered
613+
}
614+
598615
func applyWorkflowRunListRepository(runs []WorkflowRun, repoOverride string) {
599616
if len(runs) == 0 {
600617
return

pkg/cli/logs_github_api_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ func TestWorkflowRunUnmarshal(t *testing.T) {
3131
"startedAt": "2026-01-01T00:00:01Z",
3232
"updatedAt": "2026-01-01T00:01:00Z",
3333
"attempt": 2
34-
}
35-
]`
34+
}]`
3635

3736
var runs []WorkflowRun
3837
require.NoError(t, json.Unmarshal([]byte(rawJSON), &runs), "unmarshal should succeed")
@@ -44,6 +43,16 @@ func TestWorkflowRunUnmarshal(t *testing.T) {
4443
assert.Equal(t, 2, runs[0].Attempt, "Attempt should be populated")
4544
}
4645

46+
func TestFilterIgnoredWorkflowRuns(t *testing.T) {
47+
runs := []WorkflowRun{{DatabaseID: 100}, {DatabaseID: 200}, {DatabaseID: 300}}
48+
49+
filtered := filterIgnoredWorkflowRuns(runs, []int64{100, 300})
50+
51+
require.Len(t, filtered, 1)
52+
assert.Equal(t, int64(200), filtered[0].DatabaseID)
53+
assert.Equal(t, []WorkflowRun{{DatabaseID: 100}, {DatabaseID: 200}, {DatabaseID: 300}}, runs)
54+
}
55+
4756
func TestApplyWorkflowRunListRepository(t *testing.T) {
4857
runs := []WorkflowRun{
4958
{DatabaseID: 1, Repository: ""},

pkg/cli/logs_orchestrator.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ func buildContinuationIfNeeded(
287287
Branch: opts.branch,
288288
AfterRunID: opts.afterRunID,
289289
BeforeRunID: oldestRunID,
290+
IgnoreWorkflowRuns: opts.ignoreWorkflowRuns,
290291
Timeout: opts.timeoutMinutes,
291292
MaxGitHubAPIRateLimit: opts.maxGitHubAPIRateLimit,
292293
MaxStorageMB: opts.maxStorageMB,
@@ -350,6 +351,7 @@ func collectWorkflowLogs(ctx context.Context, opts LogsDownloadOptions) (workflo
350351
engine: opts.Engine,
351352
branch: opts.Ref,
352353
afterRunID: opts.AfterRunID,
354+
ignoreWorkflowRuns: opts.IgnoreWorkflowRuns,
353355
count: opts.Count,
354356
timeoutMinutes: opts.TimeoutMinutes,
355357
maxGitHubAPIRateLimit: opts.MaxGitHubAPIRateLimit,

pkg/cli/logs_orchestrator_download.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ func fetchWorkflowRunBatch(ctx context.Context, opts LogsDownloadOptions, before
408408
Ref: opts.Ref,
409409
BeforeRunID: opts.BeforeRunID,
410410
AfterRunID: opts.AfterRunID,
411+
IgnoreWorkflowRuns: opts.IgnoreWorkflowRuns,
411412
RepoOverride: opts.RepoOverride,
412413
OldestFetchedCreatedAt: &oldestFetchedCreatedAt,
413414
ProcessedCount: processedCount,

pkg/cli/logs_orchestrator_types.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,27 @@ package cli
66

77
// LogsDownloadOptions holds parameters for DownloadWorkflowLogs.
88
type LogsDownloadOptions struct {
9-
WorkflowName string
10-
Count int
11-
StartDate string
12-
EndDate string
13-
OutputDir string
14-
Engine string
15-
Runtime string
16-
Ref string
17-
BeforeRunID int64
18-
AfterRunID int64
19-
RepoOverride string
20-
Verbose bool
21-
ToolGraph bool
22-
NoStaged bool
23-
FirewallOnly bool
24-
NoFirewall bool
25-
Parse bool
26-
JSONOutput bool
27-
TimeoutMinutes int
28-
TimeoutSeconds int
9+
WorkflowName string
10+
Count int
11+
StartDate string
12+
EndDate string
13+
OutputDir string
14+
Engine string
15+
Runtime string
16+
Ref string
17+
BeforeRunID int64
18+
AfterRunID int64
19+
IgnoreWorkflowRuns []int64
20+
RepoOverride string
21+
Verbose bool
22+
ToolGraph bool
23+
NoStaged bool
24+
FirewallOnly bool
25+
NoFirewall bool
26+
Parse bool
27+
JSONOutput bool
28+
TimeoutMinutes int
29+
TimeoutSeconds int
2930
// MaxGitHubAPIRateLimit is the maximum number of core API requests that may
3031
// be used in the current window before downloads wait for the reset. Negative
3132
// values reserve that many requests from the API-reported limit.
@@ -111,6 +112,7 @@ type continuationOptions struct {
111112
engine string
112113
branch string
113114
afterRunID int64
115+
ignoreWorkflowRuns []int64
114116
count int
115117
timeoutMinutes int
116118
maxGitHubAPIRateLimit int

pkg/cli/logs_report.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,20 @@ type LogsData struct {
4848

4949
// ContinuationData provides parameters to continue an incomplete logs query.
5050
type ContinuationData struct {
51-
Message string `json:"message"`
52-
WorkflowName string `json:"workflow_name,omitempty"`
53-
Count int `json:"count,omitempty"`
54-
StartDate string `json:"start_date,omitempty"`
55-
EndDate string `json:"end_date,omitempty"`
56-
Engine string `json:"engine,omitempty"`
57-
Branch string `json:"branch,omitempty"`
58-
AfterRunID int64 `json:"after_run_id,omitempty"`
59-
BeforeRunID int64 `json:"before_run_id,omitempty"`
60-
Timeout int `json:"timeout,omitempty"`
61-
MaxGitHubAPIRateLimit int `json:"max_github_api_rate_limit,omitempty"`
62-
MaxStorageMB int `json:"max_storage,omitempty"`
63-
PruneOlderRuns bool `json:"prune_older_runs,omitempty"`
51+
Message string `json:"message"`
52+
WorkflowName string `json:"workflow_name,omitempty"`
53+
Count int `json:"count,omitempty"`
54+
StartDate string `json:"start_date,omitempty"`
55+
EndDate string `json:"end_date,omitempty"`
56+
Engine string `json:"engine,omitempty"`
57+
Branch string `json:"branch,omitempty"`
58+
AfterRunID int64 `json:"after_run_id,omitempty"`
59+
BeforeRunID int64 `json:"before_run_id,omitempty"`
60+
IgnoreWorkflowRuns []int64 `json:"ignore_workflow_runs,omitempty"`
61+
Timeout int `json:"timeout,omitempty"`
62+
MaxGitHubAPIRateLimit int `json:"max_github_api_rate_limit,omitempty"`
63+
MaxStorageMB int `json:"max_storage,omitempty"`
64+
PruneOlderRuns bool `json:"prune_older_runs,omitempty"`
6465
}
6566

6667
// WorkflowContinuation identifies a per-target cursor in a combined

pkg/cli/logs_timeout_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func TestBuildLogsCommandArgsIncludesResourceBudgets(t *testing.T) {
1616
MaxGitHubAPIRateLimit: -2000,
1717
MaxStorageMB: 10240,
1818
PruneOlderRuns: true,
19+
IgnoreWorkflowRuns: []int64{123, 456},
1920
})
2021
command := strings.Join(cmdArgs, " ")
2122

@@ -28,6 +29,9 @@ func TestBuildLogsCommandArgsIncludesResourceBudgets(t *testing.T) {
2829
if !strings.Contains(command, "--prune-older-runs") {
2930
t.Fatalf("command args do not include older-run pruning mode: %s", command)
3031
}
32+
if !strings.Contains(command, "--ignore-workflow-runs 123,456") {
33+
t.Fatalf("command args do not include ignored workflow runs: %s", command)
34+
}
3135
}
3236

3337
// TestTimeoutFlagParsing tests that the timeout flag is properly parsed

0 commit comments

Comments
 (0)