Skip to content

fix: paginate sem get tasks to return all tasks for a project#249

Open
cchristous wants to merge 3 commits intosemaphoreci:masterfrom
cchristous:feat/task-list-pagination
Open

fix: paginate sem get tasks to return all tasks for a project#249
cchristous wants to merge 3 commits intosemaphoreci:masterfrom
cchristous:feat/task-list-pagination

Conversation

@cchristous
Copy link
Copy Markdown
Contributor

@cchristous cchristous commented Apr 21, 2026

Summary

Fixes a pagination bug in sem get tasks -p <project>: it was silently truncating results to the first 30 tasks (one page) regardless of how many tasks the project had. A project with more than 30 tasks only showed the first 30; the rest were invisible from the CLI even though the UI showed them all.

Root cause

ListTasks in api/client/tasks_v1_alpha.go issued a single GET /api/v1alpha/tasks?project_id=… and returned whatever came back, discarding the pagination headers. The v1alpha API paginates via Link: rel="next" — the headers are returned in every response (e.g. Total: N Per-Page: 30 Link: …?page=2…; rel="next"), just unused.

Fix

Rewrite ListTasks to loop with page_size=200, follow Link: rel="next" until exhausted, and aggregate every page into a single TaskListV1Alpha.

This mirrors the pattern established for sem get pipelines in #247 (now merged):

  • retry.RetryWithMaxFailures with exponential backoff for transient failures (5xx, 429, transport errors)
  • retry.NonRetryable for 3xx/4xx (except 429) and deserialization failures — no wasted retries
  • Truncated error bodies (200 chars max) to keep CLI output readable
  • Mid-pagination error messages include the page number and accumulated task count for debuggability
  • Safety cap of 500 pages (100k tasks) to prevent runaway loops, with a loud error if hit
  • Nil-header guard, checked before appending the page so partial state is not mutated on error

The CLI surface is unchanged — no new flags, same command, just works.

Why this is safe

  • No changes to DescribeTask or RunTask; pagination applies only to the list endpoint.
  • The hasNextPage helper and retry.RetryWithMaxFailures / NonRetryable are reused unchanged from feat: paginate sem get pipelines to return all results within age window #247 — they are already unit-tested in api/client/pipelines_v1_alpha_test.go (TestHasNextPage) and api/retry/with_max_failures_test.go (TestRetryWithMaxFailures, ..._NonRetryable, ..._Backoff, ..._MixedRetryableAndNon, ..._ErrorWrapsAttemptCount).
  • The three existing Test__ListTasks__* httpmock responders that matched task URLs exactly are switched to regexp responders that match on the project-id query param. This was needed because requests now also carry page and page_size parameters; an exact-string responder would no longer match.
  • A new Test__ListTasks__MultiPage covers the new behavior. It uses anchored regexp patterns ([?&]page=N(?:&|$)) so the page-1 responder cannot accidentally match page=10/page=11/etc. The test asserts both responders fire, the call returns no error, the result has length 2, and both task names from the two pages are present in order.
  • go build ./... passes; all Test__ListTasks__*, Test__DescribeTask__*, and Test__GetTasks__* tests pass.

Test plan

  • Unit tests pass for single-page (existing tests, after URL-matcher swap), multi-page aggregation, and the unchanged describe/run paths
  • go build ./... succeeds
  • Manual smoke: run sem get tasks -p <project> against a project with 100+ tasks confirmed all are returned (locally-built binary returned the full task count matching the API total header; the released CLI returned 30)
  • Manual smoke: confirmed sem get tasks <task-id> (describe) is byte-identical to the released CLI, and sem run task <task-id> still triggers a workflow successfully

loadez
loadez previously approved these changes Apr 24, 2026
Copy link
Copy Markdown
Contributor

@loadez loadez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cchristous cchristous dismissed loadez’s stale review April 24, 2026 11:37

The merge-base changed after approval.

ListTasks previously issued a single GET /api/v1alpha/tasks and returned
whatever the first page contained (~30 tasks), silently discarding the
rest. Projects with >30 tasks were invisible beyond the first page.

Rewrite ListTasks to loop with page_size=200, following the API's
Link: rel="next" header until exhausted, and aggregating all pages.
Mirrors the pattern established for pipelines in semaphoreci#247, reusing
retry.RetryWithMaxFailures, retry.NonRetryable, and hasNextPage.

Includes a safety cap of 500 pages (100k tasks) and nil-header guard.
No CLI surface changes -- sem get tasks -p <project> just works now.

Existing httpmock responders that matched task URLs exactly are
switched to regexp matchers to accommodate the new page/page_size
query params. A new Test__ListTasks__MultiPage verifies both pages
are fetched via Link: rel=next and aggregated into the output.

Reported by Kumar Utkarsh against semaphoreci#246.
- maxTaskPages is now a local const inside ListTasks (matches pipelines
  pattern); the previous package-level var was silently mutable and
  unnecessary since no test overrides it.
- Nil-header guard now fires before the page is appended to allTasks,
  so on error we don't discard partial state that was already mutated.
  The error message now includes the accumulated task count for
  debuggability.
- Multi-page test regexps tightened: `page=1.*` would also match
  page=10, page=11, etc. Replaced with anchored boundary patterns
  `[?&]page=N(?:&|$)` so the regex cannot accidentally match a later
  page number if the test is extended.
@cchristous cchristous force-pushed the feat/task-list-pagination branch from 60d627e to 56606da Compare April 25, 2026 16:58
Without this assertion, a regression in hasNextPage or the break
condition could let the loop run extra requests; the existing
page1Received/page2Received flags only verify lower bounds. With
GetTotalCallCount we explicitly assert the loop terminated when
Link: rel=next was absent on page 2.
@cchristous cchristous marked this pull request as ready for review April 25, 2026 17:12
@cchristous
Copy link
Copy Markdown
Contributor Author

Tests are failing, but I can't see why. Can you take a look?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants