Skip to content

[CFX-7945] refactor(api): unify next-link pagination behind internal/paginate - #861

Draft
ajalon1 wants to merge 5 commits into
mainfrom
aj/CFX-7834-list-pagination
Draft

[CFX-7945] refactor(api): unify next-link pagination behind internal/paginate#861
ajalon1 wants to merge 5 commits into
mainfrom
aj/CFX-7834-list-pagination

Conversation

@ajalon1

@ajalon1 ajalon1 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Split out of CFX-7834 and PR stack 858/859/860. While working on that ticket, found two problems with list-API pagination:

  1. The pipelines-api list client decodes a single DataPage and returns it (ListPipelines, internal/pipeline/pipeline.go:144), so --limit 500 silently returns one page — the server enforces a 1..100 page-size ceiling (maxPageSize = 100, internal/workload/workload.go:706) and larger totals are only reachable via next-links, which the pipeline family never follows.
  2. The next-link walk is copy-pasted per endpoint across the workload family (paginateWorkloads, paginateArtifacts, inline loops in build.go, credential.go, execenv.go, and in internal/drapi for templates and llms) instead of sharing one helper.

Scope

  • Add internal/paginate.Walk: the one generic next-link loop (accumulate rows, stop at limit or end of pages, return at most limit rows), guarded by the existing drapi.AssertNextOnSameHost.
  • Convert ListWorkloads, ListArtifacts, and ListArtifactBuilds to share that loop, replacing the per-endpoint paginate* copies.
  • Pipeline family: pass offset/limit through unclamped as today, and either adopt the walk or leave it single-page by design — current behavior kept, flagged for the API team re: the 422-on-limit>100 contract.
  • Not adopted: llm-gateway list and ListTaskExecutions (fetch-all by design).

Verification

  • task lint (all GOOS legs) and task test (race + coverage) pass.
  • Multi-page walk tests cover both the DataPage and rows paths; tests pin that out-of-range numerics error instead of clamping.

…tflags

Adds internal/countflags, a pflag.Value pair for count-like int flags:
PositiveInt rejects zero and negative values (a page size of 0 would fetch
nothing and read as an empty result), and NonNegativeInt allows zero while
rejecting negatives (offset 0 means "from the start"). Set keeps pflag's
base-0 ParseInt semantics and reports type "int", so Flags().GetInt reads
in telemetry closures are unchanged; only validation is new.

Migrates --limit (and --offset where present) on dr workload list,
dr workload logs, dr artifact list, dr artifact build list, and
dr artifact code versions to the new values and deletes the runtime
checks from their RunE bodies. Invalid input now fails at parse time as
`invalid argument "0" for "--limit" flag: must be a positive integer`
instead of `invalid --limit 0: must be positive` after the command starts.

Test assertions updated to the parse-time messages; the artifact code
versions invalid-limit case now asserts the Flags().Set error directly
since rejection happens before Execute. Documented the pattern in
docs/development/flags.md alongside the pollflags precedent.

Excluded on purpose: workload config --port/--replicas (0 means "use
default") and selector flags like --node-id. Library-level limit checks
in internal/workload stay as defense in depth for non-CLI callers.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@datarobot-pr-review-router

Copy link
Copy Markdown

🎫 Jira: CFX-7834 — Refactor --limit/--offset flags to custom pflag.Value (move validation to parsetime)

ajalon1 and others added 4 commits August 27, 2026 13:49
CodeQL (incorrect-conversion-between-integer-types) flags the int64 -> int
conversion of the strconv.ParseInt result: int is 32-bit on some
architectures, so a value in [2^31, 2^63) would silently truncate there.
Set now also rejects values above math.MaxInt, satisfying the upper-bound
check; on 64-bit builds the bound is unreachable and the check only
documents intent. Adds a parse-level test pinning that out-of-range
numerics error instead of clamping.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Migrates the pipeline family's count flags onto internal/countflags from
the base stack:

- --limit -> countflags.PositiveInt on pipeline list (default 50),
  version/run/image/input/schedule list (default 100)
- --offset -> countflags.NonNegativeInt on all six list commands
- --tail on pipeline run task logs -> countflags.NonNegativeInt, keeping
  0 as "no limit"

These commands previously did no validation at all, so --limit 0/-1 and
negative offsets went straight to the API; they are now rejected during
flag parsing with cobra's standard "invalid argument" message, before
any request is made.

Adds invalid-limit/invalid-offset reject tests per list command and an
invalid-tail test for task logs, in the existing runCmd style.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Migrates dr task run --concurrency onto internal/countflags.PositiveInt
(default 2 unchanged). Zero or negative values previously reached the
task runner and ran nothing or misbehaved; they are now rejected during
flag parsing with cobra's standard "invalid argument" message. Adds a
reject test in the existing command-test style.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…paginate

Introduces internal/paginate.Walk, the one generic loop for list endpoints
that answer one page per request plus a next link: accumulate rows, stop
once limit rows are collected or the pages run out, and return at most
limit rows.

internal/workload: ListWorkloads, ListArtifacts, and ListArtifactBuilds
now share one listWalk helper over their identical {data,next} envelope,
replacing the three hand-rolled loop copies (and the per-endpoint
WorkloadList/ArtifactList/BuildList envelopes they decoded into, whose
count fields were never read). The builds endpoint additionally clamps
its page size to the shared maxPageSize ceiling instead of sending the
full limit per request; next-links satisfy larger totals either way.

internal/pipeline: the six List* functions previously decoded a single
DataPage and returned it, so --limit values beyond one page were silently
truncated. They now clamp the per-request page size to 200 (the documented
1-200 ceiling) and walk next-links until the limit is met or the pages run
out. ListPipelines keeps its *DataPage return, restating Count over the
returned rows while preserving the server's TotalCount from the last page
so "Showing N of <total>" rendering stays truthful. Argument checks
(limit positive, offset non-negative) now mirror the workload family;
TestsListInputs/ListImages that asserted the old "zero limit omits the
param" contract are updated, and multi-page walk tests cover both the
DataPage and rows paths.

llm-gateway list already walks pages internally and ListTaskExecutions
fetches all rows by design, so neither adopts the walker.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@ajalon1
ajalon1 force-pushed the aj/CFX-7834-list-pagination branch from bbe797a to 596b00f Compare August 27, 2026 20:49
@github-actions github-actions Bot added the go Pull requests that update go code label Aug 27, 2026
@ajalon1

ajalon1 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Leaving this in draft because I'm not 100% sure on the changes here, unlike for the others in the stack. Still looking for reviews from workload and pipeline, though.

Base automatically changed from aj/CFX-7834-countflags-task to main August 31, 2026 18:02
@ajalon1 ajalon1 changed the title [CFX-7834] refactor(api): unify next-link pagination behind internal/paginate [CFX-7945] refactor(api): unify next-link pagination behind internal/paginate Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant