Skip to content

(DHQ-540) Add dry-run deployments and pagination control#2

Merged
facundofarias merged 5 commits intomainfrom
feat/dry-run-pagination
Apr 17, 2026
Merged

(DHQ-540) Add dry-run deployments and pagination control#2
facundofarias merged 5 commits intomainfrom
feat/dry-run-pagination

Conversation

@MartaKar
Copy link
Copy Markdown
Contributor

@MartaKar MartaKar commented Apr 9, 2026

Linear: https://linear.app/deployhq/issue/DHQ-540/cli-add-dry-run-and-pagination-support-api-dependent

Summary

  • --dry-run on dhq deploy: Creates a preview deployment via the API (mode: "preview") without executing. Returns preview_pending status. Mutually exclusive with --wait.
  • --page / --per-page on paginated list commands: Added to deployments list, servers list, and server-groups list (the three endpoints with server-side pagination). JSON output includes pagination metadata.
  • SDK ListOptions: All 24 List* methods now accept *ListOptions for pagination params. Passing nil preserves current behavior.
  • Pagination struct fix: Aligned field names with actual API response (total_records/offset instead of total_count/per_page).

Tested against staging

Test Result
deployments list --page 1 --json Pagination metadata correct (total_records: 5)
servers list --page 1 Flags accepted, params sent
deploy --dry-run --json Returns {"status": "preview_pending", "identifier": "..."}
deploy --dry-run --wait Error: "mutually exclusive"
deploy --dry-run (TTY) Human-readable preview output
All unit tests (102) Pass

Test plan

  • Verify dhq deploy --dry-run -p <project> -s <server> creates preview without deploying
  • Verify dhq deployments list -p <project> --page 1 --json includes pagination metadata
  • Verify dhq servers list -p <project> --page 1 --per-page 1 limits results
  • Verify dhq deploy --dry-run --wait errors with mutual exclusivity message
  • Run go test ./... — all 102 tests pass

MartaKar added 3 commits April 9, 2026 12:57
Add ListOptions struct with Page/PerPage fields and appendListParams
helper to build query strings. All 24 List* methods now accept an
optional *ListOptions parameter (nil preserves current behavior).

Fix Pagination struct field names to match actual API response
(total_records/offset instead of total_count/per_page).

Update all callers to pass nil for the new parameter.
…ands

Dry-run: dhq deploy --dry-run creates a preview deployment via the API
(mode: "preview") without executing. Returns preview_pending status.
Mutually exclusive with --wait.

Pagination: --page and --per-page flags on deployments list, servers list,
and server-groups list (the three endpoints that support server-side
pagination). JSON output includes pagination metadata via new
NewPaginatedResponse envelope. TTY output shows page footer when
multiple pages exist.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 88a66f7d-da89-4bdd-9afa-276a9001bdc7

📥 Commits

Reviewing files that changed from the base of the PR and between 140a34f and 3b1edb1.

📒 Files selected for processing (2)
  • internal/commands/pagination_test.go
  • pkg/sdk/deployments.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/commands/pagination_test.go

Walkthrough

Adds SDK list pagination support and CLI flags (--page, --per-page), extends response schema with pagination metadata, adds dhq deploy --dry-run preview flow (uses a new PreviewDeployment SDK call), and updates call sites and tests to the new SDK signatures.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md
Documented pagination flags, JSON pagination metadata, dhq deploy --dry-run preview behavior, and API docs location.
SDK core types & helper
pkg/sdk/client.go, pkg/sdk/types.go
Added appendListParams helper and ListOptions; changed Pagination shape; added DeploymentPreview.
SDK list methods (signatures & query param support)
pkg/sdk/...
activity.go, agents.go, auto_deployments.go, build_commands.go, build_configs.go, config_files.go, env_vars.go, excluded_files.go, global_servers.go, integrations.go, language_versions.go, projects.go, repositories.go, scheduled_deployments.go, server_groups.go, servers.go, ssh_commands.go, ssh_keys.go, templates.go, zones.go
Updated list method signatures to accept opts *ListOptions and use appendListParams(...) to include pagination query params.
SDK deployments
pkg/sdk/deployments.go
ListDeployments now accepts *ListOptions; added PreviewDeployment(ctx, projectID, req) which POSTs a preview (Mode="preview") and returns *DeploymentPreview.
CLI pagination infra & tests
internal/commands/pagination.go, internal/commands/pagination_test.go
Added addPaginationFlags and listOptsFromFlags plus tests for flag-to-opts conversion and flag presence.
CLI pagination wiring
internal/commands/deployments.go, internal/commands/servers.go, internal/commands/server_groups.go
Registered --page/--per-page flags, pass listOptsFromFlags(page, perPage) into SDK calls, and render page status for multi-page results.
CLI deploy --dry-run
internal/commands/deploy.go, internal/commands/deploy_test.go
Added --dry-run flag (mutually exclusive with --wait), uses PreviewDeployment for dry-run path, returns preview JSON/breadcrumb or TTY preview text; added deployExecuteCmd helper and tests.
CLI list call-site updates
internal/...
multiple files (e.g., activity.go, auth.go, auto_deploys.go, build_commands.go, build_configs.go, completions_dynamic.go, config_files.go, configure.go, doctor.go, env_vars.go, excluded_files.go, global_servers.go, hello.go, init.go, language_versions.go, network_agents.go, projects.go, repos.go, scheduled_deploys.go, server_groups.go, servers.go, ssh_commands.go, ssh_keys.go, status.go, templates.go, test_access.go, url.go, zones.go, assist/context.go)
Updated client list call sites to pass explicit nil or listOptsFromFlags(...) to match new SDK signatures.
Output response & tests
internal/output/breadcrumbs.go, internal/output/breadcrumbs_test.go
Added exported Pagination type and Pagination *Pagination on Response; added NewPaginatedResponse and tests validating JSON marshaling.
SDK & client tests updated
pkg/sdk/client_test.go, pkg/sdk/deployments_test.go, pkg/sdk/projects_test.go, pkg/sdk/repositories_test.go, pkg/sdk/server_groups_test.go, pkg/sdk/servers_test.go, pkg/sdk/templates_test.go
Updated test call sites to pass nil opts; added TestAppendListParams; adjusted mock pagination payloads to the new shape.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as "User"
rect rgba(200,200,255,0.5)
participant CLI as "dhq CLI"
end
rect rgba(200,255,200,0.5)
participant SDK as "pkg/sdk Client"
end
rect rgba(255,200,200,0.5)
participant API as "DeployHQ API"
end

User->>CLI: run dhq deploy --dry-run -p project
CLI->>SDK: PreviewDeployment(ctx, projectID, DeploymentCreateRequest{Mode: "preview"})
SDK->>API: POST /projects/{id}/deployments (body with Mode=preview)
API-->>SDK: 202 Accepted with DeploymentPreview{status, identifier}
SDK-->>CLI: return DeploymentPreview
CLI-->>User: print preview JSON or TTY preview + suggested execute command

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the two main changes: adding dry-run deployment support and pagination control to list commands.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dry-run-pagination

Comment @coderabbitai help to get the list of available commands and usage tips.

@MartaKar MartaKar changed the title feat: add dry-run deployments and pagination control (DHQ-540) Add dry-run deployments and pagination control Apr 9, 2026
@linear
Copy link
Copy Markdown

linear bot commented Apr 9, 2026

@MartaKar MartaKar self-assigned this Apr 9, 2026
@MartaKar MartaKar requested a review from facundofarias April 9, 2026 11:15
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/commands/servers.go (1)

34-83: ⚠️ Potential issue | 🟠 Major

servers list applies pagination inputs but drops pagination outputs.

Line 50 passes page/per-page options into the SDK call, but Lines 57-73 still render a plain list response (output.NewResponse) with no pagination metadata or page footer. This makes the pagination feature incomplete for this command’s JSON and TTY outputs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/commands/servers.go` around lines 34 - 83, The List command passes
page/perPage into listOptsFromFlags and client.ListServers but then drops
pagination metadata when rendering; update the rendering to surface pagination
for both JSON and TTY: have the code capture pagination info returned or
obtainable (page, perPage, total/hasMore) and pass it into output.NewResponse
(so JSON includes pagination fields) and append a footer via env.Status for TTY
(e.g., "Page X of Y — use --page/--per-page" or next/prev hints). Modify the
block using output.NewResponse, env.WriteJSON, env.WriteTable and env.Status
accordingly and ensure addPaginationFlags, listOptsFromFlags, and ListServers
usage remain consistent.
🧹 Nitpick comments (2)
internal/commands/test_access.go (1)

37-45: Server name resolution currently checks only the first page of servers.

For accounts with many servers, a valid --server <name> may fail to resolve if it’s not on page 1. Consider paging through results during resolution.

♻️ Suggested fix
-				servers, err := client.ListServers(cliCtx.Background(), projectID, nil)
+				servers, err := listAllServers(cliCtx.Background(), client, projectID)
func listAllServers(ctx context.Context, client *sdk.Client, projectID string) ([]sdk.Server, error) {
	const perPage = 100
	var all []sdk.Server
	for page := 1; ; page++ {
		chunk, err := client.ListServers(ctx, projectID, &sdk.ListOptions{
			Page:    page,
			PerPage: perPage,
		})
		if err != nil {
			return nil, err
		}
		all = append(all, chunk...)
		if len(chunk) < perPage {
			break
		}
	}
	return all, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/commands/test_access.go` around lines 37 - 45, The current
server-name resolution in the test access command only calls client.ListServers
once (in the block around resolveServerName) so names not on the first page are
missed; change this to iterate pages (e.g., implement a helper like
listAllServers that repeatedly calls client.ListServers with ListOptions.Page
and PerPage until fewer than perPage results are returned) and use the combined
slice when calling resolveServerName; update the code path that currently does
client.ListServers(cliCtx.Background(), projectID, nil) to call the new paging
helper and propagate any errors appropriately.
internal/commands/completions_dynamic.go (1)

34-34: Server completions may be truncated to page 1.

Using nil options against a paginated servers endpoint can hide valid completion entries on later pages.

♻️ Suggested fix
-	servers, err := client.ListServers(cliCtx.Background(), projectID, nil)
+	servers, err := listAllServers(cliCtx.Background(), client, projectID)

Use the same paged helper approach as in other server-name resolution paths to return full completion candidates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/commands/completions_dynamic.go` at line 34, The call using
client.ListServers(cliCtx.Background(), projectID, nil) can miss servers on
subsequent pages; replace this single-page call with the paged helper used
elsewhere for server-name resolution (iterate across pages and append results
into the servers slice) so all completion candidates are returned — locate the
ListServers usage in completions_dynamic.go and swap it for the same pagination
logic/paged helper used by other server-name resolution code paths (collect into
the existing servers variable using projectID and cliCtx.Background()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/commands/deploy_test.go`:
- Around line 17-22: The test fails early due to authentication checks, so make
it deterministic by ensuring the CLI is authenticated (or auth checks are
stubbed) before calling NewRootCmd("test"). Update the test to establish an
authenticated context—e.g., call the existing test helper or function that
creates a logged-in session, set the auth env/token used by the CLI, or mock the
auth layer—so that when cmd := NewRootCmd("test"); cmd.SetArgs(...); err :=
cmd.Execute() runs it reaches the mutual-exclusivity validation and returns the
expected error containing "mutually exclusive" from the deploy command logic.

In `@internal/commands/deploy.go`:
- Around line 189-190: The breadcrumb command builder deployExecuteCmd is
currently always appending "-s" and omits the resolved revision used for
preview, which can yield an invalid "-s" with empty value or run the wrong
revision; update the code that constructs the breadcrumb (where
output.Breadcrumb{Action: "execute", Cmd: deployExecuteCmd(...) } is used) to
pass the resolved revision string into deployExecuteCmd and change
deployExecuteCmd to only include the "-s <revision>" flag when the revision is
non-empty, ensuring the breadcrumb command exactly matches the
previewed/resolved revision; make the same change for the other occurrences of
deployExecuteCmd in the file (the second usage block).

In `@internal/commands/pagination_test.go`:
- Around line 24-27: The test currently only checks deploymentsCmd is non-nil
after calling cmd.Find, which can mask errors because cmd.Find returns a (cmd,
args, error); update the assertions to also assert that the error is nil for
each cmd.Find call (e.g., when locating "deployments list" and the other
occurrences at lines 32-35 and 40-43) before checking
deploymentsCmd.Flags().Lookup("page") and ("per-page"), so replace or add checks
using assert.NoError(t, err) (or require.NoError) for the returned error from
cmd.Find to avoid false positives.

In `@pkg/sdk/deployments.go`:
- Around line 48-49: The function currently returns &preview even when c.post
returns an error, which can expose a zero-value DeploymentPreview; update the
return logic around the c.post call so that if err from c.post (the call to
c.post(ctx, fmt.Sprintf("/projects/%s/deployments", projectID), body, &preview))
is non-nil you return nil, err, otherwise return &preview, nil—ensuring callers
receive a nil *DeploymentPreview when an API error occurs.

---

Outside diff comments:
In `@internal/commands/servers.go`:
- Around line 34-83: The List command passes page/perPage into listOptsFromFlags
and client.ListServers but then drops pagination metadata when rendering; update
the rendering to surface pagination for both JSON and TTY: have the code capture
pagination info returned or obtainable (page, perPage, total/hasMore) and pass
it into output.NewResponse (so JSON includes pagination fields) and append a
footer via env.Status for TTY (e.g., "Page X of Y — use --page/--per-page" or
next/prev hints). Modify the block using output.NewResponse, env.WriteJSON,
env.WriteTable and env.Status accordingly and ensure addPaginationFlags,
listOptsFromFlags, and ListServers usage remain consistent.

---

Nitpick comments:
In `@internal/commands/completions_dynamic.go`:
- Line 34: The call using client.ListServers(cliCtx.Background(), projectID,
nil) can miss servers on subsequent pages; replace this single-page call with
the paged helper used elsewhere for server-name resolution (iterate across pages
and append results into the servers slice) so all completion candidates are
returned — locate the ListServers usage in completions_dynamic.go and swap it
for the same pagination logic/paged helper used by other server-name resolution
code paths (collect into the existing servers variable using projectID and
cliCtx.Background()).

In `@internal/commands/test_access.go`:
- Around line 37-45: The current server-name resolution in the test access
command only calls client.ListServers once (in the block around
resolveServerName) so names not on the first page are missed; change this to
iterate pages (e.g., implement a helper like listAllServers that repeatedly
calls client.ListServers with ListOptions.Page and PerPage until fewer than
perPage results are returned) and use the combined slice when calling
resolveServerName; update the code path that currently does
client.ListServers(cliCtx.Background(), projectID, nil) to call the new paging
helper and propagate any errors appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1271129-e56f-4588-8831-9a5173bd6b19

📥 Commits

Reviewing files that changed from the base of the PR and between c577edb and a65b605.

📒 Files selected for processing (67)
  • CLAUDE.md
  • internal/assist/context.go
  • internal/commands/activity.go
  • internal/commands/auth.go
  • internal/commands/auto_deploys.go
  • internal/commands/build_commands.go
  • internal/commands/build_configs.go
  • internal/commands/completions_dynamic.go
  • internal/commands/config_files.go
  • internal/commands/configure.go
  • internal/commands/deploy.go
  • internal/commands/deploy_test.go
  • internal/commands/deployments.go
  • internal/commands/doctor.go
  • internal/commands/env_vars.go
  • internal/commands/excluded_files.go
  • internal/commands/global_servers.go
  • internal/commands/hello.go
  • internal/commands/init.go
  • internal/commands/language_versions.go
  • internal/commands/network_agents.go
  • internal/commands/pagination.go
  • internal/commands/pagination_test.go
  • internal/commands/projects.go
  • internal/commands/repos.go
  • internal/commands/scheduled_deploys.go
  • internal/commands/server_groups.go
  • internal/commands/servers.go
  • internal/commands/ssh_commands.go
  • internal/commands/ssh_keys.go
  • internal/commands/status.go
  • internal/commands/templates.go
  • internal/commands/test_access.go
  • internal/commands/url.go
  • internal/commands/zones.go
  • internal/output/breadcrumbs.go
  • internal/output/breadcrumbs_test.go
  • pkg/sdk/activity.go
  • pkg/sdk/agents.go
  • pkg/sdk/auto_deployments.go
  • pkg/sdk/build_commands.go
  • pkg/sdk/build_configs.go
  • pkg/sdk/client.go
  • pkg/sdk/client_test.go
  • pkg/sdk/config_files.go
  • pkg/sdk/deployments.go
  • pkg/sdk/deployments_test.go
  • pkg/sdk/env_vars.go
  • pkg/sdk/excluded_files.go
  • pkg/sdk/global_servers.go
  • pkg/sdk/integrations.go
  • pkg/sdk/language_versions.go
  • pkg/sdk/projects.go
  • pkg/sdk/projects_test.go
  • pkg/sdk/repositories.go
  • pkg/sdk/repositories_test.go
  • pkg/sdk/scheduled_deployments.go
  • pkg/sdk/server_groups.go
  • pkg/sdk/server_groups_test.go
  • pkg/sdk/servers.go
  • pkg/sdk/servers_test.go
  • pkg/sdk/ssh_commands.go
  • pkg/sdk/ssh_keys.go
  • pkg/sdk/templates.go
  • pkg/sdk/templates_test.go
  • pkg/sdk/types.go
  • pkg/sdk/zones.go

Comment thread internal/commands/deploy_test.go
Comment thread internal/commands/deploy.go
Comment thread internal/commands/pagination_test.go Outdated
Comment thread pkg/sdk/deployments.go Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

MartaKar added 2 commits April 9, 2026 13:24
The mutual exclusivity check was after cliCtx.Client() which requires
auth. In CI (no keyring), the test hit "Not logged in" before reaching
the flag validation. Move the check earlier.
- Return nil preview on API error in PreviewDeployment (avoid zero-value use)
- Use require.NoError on cmd.Find in pagination flag tests (stricter assertions)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
internal/commands/deploy.go (1)

186-190: ⚠️ Potential issue | 🟠 Major

Fix execute breadcrumb to preserve preview context and avoid invalid -s.

The generated execute command can be wrong: it always emits -s (even when empty) and omits the resolved revision, so it may execute a different commit than the previewed one.

💡 Proposed fix
- output.Breadcrumb{Action: "execute", Cmd: deployExecuteCmd(projectID, server, branch)},
+ output.Breadcrumb{Action: "execute", Cmd: deployExecuteCmd(projectID, server, branch, revision)},
-func deployExecuteCmd(projectID, server, branch string) string {
-	cmd := fmt.Sprintf("dhq deploy -p %s -s %s", projectID, server)
+func deployExecuteCmd(projectID, server, branch, revision string) string {
+	cmd := fmt.Sprintf("dhq deploy -p %s", projectID)
+	if server != "" {
+		cmd += " -s " + server
+	}
 	if branch != "" {
 		cmd += " -b " + branch
 	}
+	if revision != "" {
+		cmd += " -r " + revision
+	}
 	return cmd
 }

Also applies to: 254-260

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/commands/deploy.go` around lines 186 - 190, The execute breadcrumb
is built with deployExecuteCmd(projectID, server, branch) which always emits a
"-s" flag even when service is empty and omits the preview's resolved revision;
update the breadcrumb construction to call deployExecuteCmd with the preview's
resolved revision and only include the service flag when preview.Service (or the
equivalent preview.ServiceName) is non-empty so "-s" is not emitted as an empty
flag; change the env.WriteJSON/new output.Breadcrumb usage around preview
handling (references: preview.Identifier, preview.Status, preview.Revision,
preview.Service and deployExecuteCmd) and apply the same fix to the other
JSON/TTY block that builds the execute breadcrumb.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/commands/deploy.go`:
- Around line 186-190: The execute breadcrumb is built with
deployExecuteCmd(projectID, server, branch) which always emits a "-s" flag even
when service is empty and omits the preview's resolved revision; update the
breadcrumb construction to call deployExecuteCmd with the preview's resolved
revision and only include the service flag when preview.Service (or the
equivalent preview.ServiceName) is non-empty so "-s" is not emitted as an empty
flag; change the env.WriteJSON/new output.Breadcrumb usage around preview
handling (references: preview.Identifier, preview.Status, preview.Revision,
preview.Service and deployExecuteCmd) and apply the same fix to the other
JSON/TTY block that builds the execute breadcrumb.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7ce6195-5f68-4779-998e-14aa551ac787

📥 Commits

Reviewing files that changed from the base of the PR and between a65b605 and 140a34f.

📒 Files selected for processing (1)
  • internal/commands/deploy.go

@facundofarias facundofarias merged commit 090d8cc into main Apr 17, 2026
3 checks passed
@facundofarias facundofarias deleted the feat/dry-run-pagination branch April 17, 2026 11:03
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