Skip to content

gh aw logs date-range filtering can return runs outside the requested window #31513

@rabo-unumed

Description

@rabo-unumed

I have tried to verify manually that this is indeed an issue on my own data, but reproduction requires an instance with runs.


Summary

gh aw logs appears to return workflow runs whose created_at values fall outside the
requested --start-date / --end-date window.

The core issue appears to be that gh aw logs translates a bounded date range into multiple
gh run list --created ... flags instead of a single --created "start..end" range.
That split form does not reliably enforce both bounds. Pagination then adds a third
--created flag and can worsen the problem, but the two-boundary case already appears broken.

Expected behavior

When running:

gh aw logs <workflow> --start-date <start> --end-date <end> --count <n> --json

every returned run should satisfy:

  • created_at >= start
  • created_at <= end

and the implementation should preserve that bounded range exactly.

Actual behavior

gh aw logs can return runs whose created_at values are earlier than the requested
--start-date or otherwise outside the intended range.

This makes date-bounded queries unreliable for exact analysis.

Evidence

Where gh aw logs constructs the split form

In pkg/cli/logs_github_api.go, listWorkflowRunsWithPagination builds the gh run list
command arguments like this:

if opts.StartDate != "" {
  args = append(args, "--created", ">="+opts.StartDate)
}
if opts.EndDate != "" {
  args = append(args, "--created", "<="+opts.EndDate)
}
if opts.BeforeDate != "" {
  args = append(args, "--created", "<"+opts.BeforeDate)
}

In pkg/cli/logs_orchestrator.go, DownloadWorkflowLogs passes StartDate, EndDate, and
BeforeDate into that function:

runs, totalFetched, err := listWorkflowRunsWithPagination(ListWorkflowRunsOptions{
  WorkflowName: workflowName,
  Limit:        batchSize,
  StartDate:    startDate,
  EndDate:      endDate,
  BeforeDate:   beforeDate,
  ...
})

and the pagination cursor is derived from the oldest run in the previous batch:

oldestRun := runs[len(runs)-1]
beforeDate = oldestRun.CreatedAt.Format(time.RFC3339)

So gh aw logs is concretely constructing bounded date queries as repeated --created
arguments:

gh run list ... \
  --created ">=START" \
  --created "<=END"

and with pagination:

gh run list ... \
  --created ">=START" \
  --created "<=END" \
  --created "<BEFORE_DATE"

Why that shape is unsafe

In GitHub CLI, gh run list defines --created as a single string flag, not a multi-value
slice:

type ListOptions struct {
  ...
  Created string
  ...
}

cmd.Flags().StringVarP(&opts.Created, "created", "", "", "Filter runs by the `date` it was created")

and later passes one created filter value through:

filters := &shared.FilterOptions{
  ...
  Created: opts.Created,
  ...
}

Minimal reproduction

The reproduction below uses only public CLI behavior.

1. Choose a repo and workflow with runs around a UTC date boundary

You need a workflow with runs near the beginning or end of a UTC day.

Set placeholders:

REPO=OWNER/REPO
WORKFLOW=workflow-name
DAY=2026-04-17

2. Verify the strict expected window with gh run list using range syntax

gh run list -R "$REPO" --workflow "$WORKFLOW" \
  --limit 250 \
  --json databaseId,createdAt \
  --created "$DAY..${DAY}T23:59:59Z"

Check that all returned createdAt values are within the UTC day.

3. Show that the split --created form is unsafe

This mirrors the way gh aw logs constructs a bounded range:

gh run list -R "$REPO" --workflow "$WORKFLOW" \
  --limit 250 \
  --json databaseId,createdAt \
  --created ">=$DAY" \
  --created "<=$DAY"

If the bug reproduces, this split form returns runs outside the intended day while the single
range form stays correctly bounded.

4. Run the equivalent gh aw logs query

gh aw logs "$WORKFLOW" \
  --repo "$REPO" \
  --start-date "$DAY" \
  --end-date "$DAY" \
  --count 250 \
  --json > aw.json

5. Inspect the returned date bounds

jq '{
  runs: (.runs | length),
  min_created_at: ([.runs[].created_at] | min),
  max_created_at: ([.runs[].created_at] | max)
}' aw.json

6. Observe the discrepancy

If the bug reproduces, min_created_at from gh aw logs will be earlier than the requested
day even though the strict gh run list --created "start..end" query stays inside the day.

Minimal local proof

A small direct probe against gh run list shows the difference clearly.

Single range expression:

gh run list -R "$REPO" --workflow "$WORKFLOW" \
  --limit 1000 \
  --json databaseId,createdAt \
  --created '2026-04-17T23:20:00Z..2026-04-17T23:45:00Z'

This returned only runs inside the interval.

Equivalent split form:

gh run list -R "$REPO" --workflow "$WORKFLOW" \
  --limit 1000 \
  --json databaseId,createdAt \
  --created '>=2026-04-17T23:20:00Z' \
  --created '<=2026-04-17T23:45:00Z'

This returned many runs from much earlier timestamps, showing that the lower bound was not
enforced.

An exact-point test showed the same issue:

gh run list ... --created '2026-04-17T23:30:42Z..2026-04-17T23:30:42Z'

correctly returned zero results, while:

gh run list ... \
  --created '>=2026-04-17T23:30:42Z' \
  --created '<=2026-04-17T23:30:42Z'

returned many earlier runs, again showing that the split form does not enforce both bounds.

Why this matters

  • Date-bounded log analysis becomes inaccurate.
  • Exact per-day or per-window token/cost analysis can be polluted by out-of-window runs.
  • Increasing --count can change which out-of-window runs appear, making the behavior hard to
    reason about.

Proposed fix

Use a single --created range expression instead of appending multiple --created flags.

For example:

  • first page: --created "START..END"
  • next page: --created "START..BEFORE_DATE"

or equivalent logic that maintains one bounded range per request.

In other words, gh aw logs should represent the date window as one --created value, and if
pagination is needed it should update the upper bound inside that same range expression instead
of adding another --created flag.

Relevant code paths

  • pkg/cli/logs_github_api.go
  • pkg/cli/logs_orchestrator.go

Notes

This report is about the run-discovery path used by gh aw logs --start-date/--end-date.
It does not affect workflows that bypass date discovery by supplying explicit run IDs.

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions