Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ internal/version/ Update checker (GitHub releases API)
- **`dhq api`**: Escape hatch covering all 144+ endpoints not in the command tree.
- **Breadcrumbs**: JSON responses include suggested next commands.
- **No login in CI**: `DEPLOYHQ_ACCOUNT` + `DEPLOYHQ_EMAIL` + `DEPLOYHQ_API_KEY` env vars.
- **Pagination**: `--page` and `--per-page` on paginated list commands (deployments, servers, server-groups). JSON output includes `pagination` metadata when available. Non-paginated endpoints don't have these flags.
- **Dry-run**: `dhq deploy --dry-run` creates a preview deployment without executing. Returns `preview_pending` status. Mutually exclusive with `--wait`.
- **API spec**: `https://api.deployhq.com/docs` (OpenAPI 3.1.0, machine-readable JSON at `/docs.json`)

## Testing

Expand Down
4 changes: 2 additions & 2 deletions internal/assist/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func GatherContext(ctx context.Context, client *sdk.Client, projectID string) (*
ac := &AssistContext{Project: projectID}

// Fetch recent deployments
result, err := client.ListDeployments(ctx, projectID)
result, err := client.ListDeployments(ctx, projectID, nil)
if err != nil {
return ac, nil // non-fatal
}
Expand Down Expand Up @@ -121,7 +121,7 @@ func GatherContext(ctx context.Context, client *sdk.Client, projectID string) (*
}

// Fetch servers
servers, err := client.ListServers(ctx, projectID)
servers, err := client.ListServers(ctx, projectID, nil)
if err == nil {
for _, s := range servers {
ac.Servers = append(ac.Servers, serverSummary{
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func newActivityListCmd() *cobra.Command {
if err != nil {
return err
}
events, err := client.ListActivity(cliCtx.Background())
events, err := client.ListActivity(cliCtx.Background(), nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -71,7 +71,7 @@ func newActivityStatsCmd() *cobra.Command {
if err != nil {
return err
}
result, err := client.ListActivityWithStats(cliCtx.Background())
result, err := client.ListActivityWithStats(cliCtx.Background(), nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func runAuthLogin(opts *AuthLoginOptions) error {
return &output.UserError{Message: err.Error()}
}

_, err = client.ListProjects(cliCtx.Background())
_, err = client.ListProjects(cliCtx.Background(), nil)
if err != nil {
if sdk.IsUnauthorized(err) {
return &output.AuthError{
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/auto_deploys.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func newAutoDeploysCmd() *cobra.Command {
if err != nil {
return err
}
result, err := client.ListAutoDeployments(cliCtx.Background(), projectID)
result, err := client.ListAutoDeployments(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/build_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func newBuildCommandsCmd() *cobra.Command {
if err != nil {
return err
}
cmds, err := client.ListBuildCommands(cliCtx.Background(), projectID)
cmds, err := client.ListBuildCommands(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -112,7 +112,7 @@ func newBuildCommandsUpdateCmd() *cobra.Command {

// resolveBuildCommandID resolves a numeric ID to a UUID identifier if needed.
func resolveBuildCommandID(ctx context.Context, client *sdk.Client, projectID, arg string) (string, error) {
cmds, err := client.ListBuildCommands(ctx, projectID)
cmds, err := client.ListBuildCommands(ctx, projectID, nil)
if err != nil {
return arg, nil // fall through — let the API report the real error
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/build_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func newBuildConfigsCmd() *cobra.Command {
if err != nil {
return err
}
configs, err := client.ListBuildConfigs(cliCtx.Background(), projectID)
configs, err := client.ListBuildConfigs(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/completions_dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func completeProjectNames(cmd *cobra.Command, args []string, toComplete string)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
projects, err := client.ListProjects(cliCtx.Background())
projects, err := client.ListProjects(cliCtx.Background(), nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
Expand All @@ -31,7 +31,7 @@ func completeServerNames(cmd *cobra.Command, args []string, toComplete string) (
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
servers, err := client.ListServers(cliCtx.Background(), projectID)
servers, err := client.ListServers(cliCtx.Background(), projectID, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/config_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func newConfigFilesListCmd() *cobra.Command {
if err != nil {
return err
}
files, err := client.ListConfigFiles(cliCtx.Background(), projectID)
files, err := client.ListConfigFiles(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func newConfigureCmd() *cobra.Command {
}

// Step 2: Pick a default project
projects, err := client.ListProjects(cliCtx.Background())
projects, err := client.ListProjects(cliCtx.Background(), nil)
if err != nil || len(projects) == 0 {
env.Status("No projects found. Create one with 'dhq projects create --name my-app'")
return nil
Expand Down
41 changes: 38 additions & 3 deletions internal/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func normalize(s string) string {

func newDeployCmd() *cobra.Command {
var branch, server, revision string
var wait bool
var dryRun, wait bool
var timeout int

cmd := &cobra.Command{
Expand All @@ -85,6 +85,10 @@ func newDeployCmd() *cobra.Command {
return err
}

if dryRun && wait {
return &output.UserError{Message: "--dry-run and --wait are mutually exclusive"}
}

client, err := cliCtx.Client()
if err != nil {
return err
Expand All @@ -94,7 +98,7 @@ func newDeployCmd() *cobra.Command {

// Auto-select server if not specified
if server == "" {
servers, err := client.ListServers(cliCtx.Background(), projectID)
servers, err := client.ListServers(cliCtx.Background(), projectID, nil)
if err == nil && len(servers) == 1 {
server = servers[0].Identifier
env.Status("Auto-selected server: %s", servers[0].Name)
Expand Down Expand Up @@ -126,7 +130,7 @@ func newDeployCmd() *cobra.Command {

// Resolve server name to identifier if needed
if server != "" && !isUUID(server) {
servers, err := client.ListServers(cliCtx.Background(), projectID)
servers, err := client.ListServers(cliCtx.Background(), projectID, nil)
if err == nil {
resolved, candidates := resolveServerName(server, servers)
if resolved != "" {
Expand Down Expand Up @@ -173,6 +177,28 @@ func newDeployCmd() *cobra.Command {
ParentIdentifier: server,
}

if dryRun {
preview, err := client.PreviewDeployment(cliCtx.Background(), projectID, req)
if err != nil {
return err
}

if env.JSONMode || !env.IsTTY {
return env.WriteJSON(output.NewResponse(preview,
fmt.Sprintf("Preview %s created (status: %s)", preview.Identifier, preview.Status),
output.Breadcrumb{Action: "execute", Cmd: deployExecuteCmd(projectID, server, branch)},
))
Comment thread
MartaKar marked this conversation as resolved.
}

env.Status("DRY RUN — preview created, deployment will NOT execute\n")
env.Status(" Identifier: %s", preview.Identifier)
env.Status(" Status: %s", output.ColorStatus(preview.Status))
env.Status("\nThe preview will be processed asynchronously.")
env.Status("Preview deployments are excluded from 'dhq deployments list'.")
env.Status("\nUse 'dhq deploy' (without --dry-run) to execute.")
return nil
}

dep, err := client.CreateDeployment(cliCtx.Background(), projectID, req)
if err != nil {
return err
Expand Down Expand Up @@ -219,11 +245,20 @@ func newDeployCmd() *cobra.Command {
cmd.Flags().StringVarP(&branch, "branch", "b", "", "Branch to deploy")
cmd.Flags().StringVarP(&server, "server", "s", "", "Server or group identifier")
cmd.Flags().StringVarP(&revision, "revision", "r", "", "End revision (default: latest)")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview what would be deployed without executing")
cmd.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for deployment to complete")
cmd.Flags().IntVar(&timeout, "timeout", 0, "Timeout in seconds when using --wait (0 = no timeout)")
return cmd
}

func deployExecuteCmd(projectID, server, branch string) string {
cmd := fmt.Sprintf("dhq deploy -p %s -s %s", projectID, server)
if branch != "" {
cmd += " -b " + branch
}
return cmd
}

func newRetryCmd() *cobra.Command {
return &cobra.Command{
Use: "retry <deployment-id>",
Expand Down
22 changes: 22 additions & 0 deletions internal/commands/deploy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package commands

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDeployDryRunFlag(t *testing.T) {
cmd := NewRootCmd("test")
deployCmd, _, _ := cmd.Find([]string{"deploy"})
assert.NotNil(t, deployCmd)
assert.NotNil(t, deployCmd.Flags().Lookup("dry-run"))
}

func TestDeployDryRunMutuallyExclusiveWithWait(t *testing.T) {
cmd := NewRootCmd("test")
cmd.SetArgs([]string{"deploy", "--dry-run", "--wait", "-p", "test-project"})
err := cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "mutually exclusive")
}
Comment thread
MartaKar marked this conversation as resolved.
24 changes: 21 additions & 3 deletions internal/commands/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ func newDeploymentsCmd() *cobra.Command {
}

func newDeploymentsListCmd() *cobra.Command {
return &cobra.Command{
var page, perPage int

cmd := &cobra.Command{
Use: "list",
Short: "List deployments",
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -44,14 +46,20 @@ func newDeploymentsListCmd() *cobra.Command {
return err
}

result, err := client.ListDeployments(cliCtx.Background(), projectID)
result, err := client.ListDeployments(cliCtx.Background(), projectID, listOptsFromFlags(page, perPage))
if err != nil {
return err
}

env := cliCtx.Envelope
if env.JSONMode || !env.IsTTY {
return env.WriteJSON(output.NewResponse(result,
return env.WriteJSON(output.NewPaginatedResponse(result.Records,
output.Pagination{
CurrentPage: result.Pagination.CurrentPage,
TotalPages: result.Pagination.TotalPages,
TotalRecords: result.Pagination.TotalRecords,
Offset: result.Pagination.Offset,
},
fmt.Sprintf("%d deployments (page %d/%d)", len(result.Records), result.Pagination.CurrentPage, result.Pagination.TotalPages),
output.Breadcrumb{Action: "show", Cmd: fmt.Sprintf("dhq deployments show <id> -p %s", projectID)},
output.Breadcrumb{Action: "deploy", Cmd: fmt.Sprintf("dhq deploy -p %s", projectID)},
Expand All @@ -73,12 +81,22 @@ func newDeploymentsListCmd() *cobra.Command {
}
env.WriteTable(columns, rows)

if result.Pagination.TotalPages > 1 {
env.Status("\nPage %d/%d (%d total) — use --page to navigate",
result.Pagination.CurrentPage,
result.Pagination.TotalPages,
result.Pagination.TotalRecords)
}

if len(result.Records) > 0 {
env.Status("\nTip: dhq deployments show %s -p %s", result.Records[0].Identifier, projectID)
}
return nil
},
}

addPaginationFlags(cmd, &page, &perPage)
return cmd
}

func newDeploymentsShowCmd() *cobra.Command {
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func newDoctorCmd() *cobra.Command {
if err != nil {
checks = append(checks, doctorCheck{"API connectivity", "fail", err.Error()})
} else {
_, err := client.ListProjects(cliCtx.Background())
_, err := client.ListProjects(cliCtx.Background(), nil)
if err != nil {
checks = append(checks, doctorCheck{"API connectivity", "fail", err.Error()})
} else {
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/env_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func newEnvVarsListCmd() *cobra.Command {
if err != nil {
return err
}
vars, err := client.ListEnvVars(cliCtx.Background(), projectID)
vars, err := client.ListEnvVars(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -219,7 +219,7 @@ func newGlobalEnvVarsCmd() *cobra.Command {
if err != nil {
return err
}
vars, err := client.ListGlobalEnvVars(cliCtx.Background())
vars, err := client.ListGlobalEnvVars(cliCtx.Background(), nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/excluded_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func newExcludedFilesCmd() *cobra.Command {
if err != nil {
return err
}
files, err := client.ListExcludedFiles(cliCtx.Background(), projectID)
files, err := client.ListExcludedFiles(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/global_servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func newGlobalServersCmd() *cobra.Command {
if err != nil {
return err
}
servers, err := client.ListGlobalServers(cliCtx.Background())
servers, err := client.ListGlobalServers(cliCtx.Background(), nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -171,7 +171,7 @@ func newIntegrationsCmd() *cobra.Command {
if err != nil {
return err
}
integrations, err := client.ListIntegrations(cliCtx.Background(), projectID)
integrations, err := client.ListIntegrations(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/hello.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func newHelloCmd() *cobra.Command {
return &output.InternalError{Message: "create client", Cause: err}
}

projects, err := client.ListProjects(cliCtx.Background())
projects, err := client.ListProjects(cliCtx.Background(), nil)
if err != nil {
env.Warn("Could not fetch projects: %v", err)
env.Status("")
Expand Down Expand Up @@ -196,7 +196,7 @@ func helloLogin(env *output.Envelope, reader *bufio.Reader) (*auth.Credentials,
return nil, &output.UserError{Message: err.Error()}
}

if _, err := client.ListProjects(cliCtx.Background()); err != nil {
if _, err := client.ListProjects(cliCtx.Background(), nil); err != nil {
if sdk.IsUnauthorized(err) {
return nil, &output.AuthError{
Message: "Invalid credentials",
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ func (m *initModel) createRepo() tea.Msg {

func (m *initModel) fetchSSHKeys() tea.Msg {
ctx := context.Background()
keys, err := m.client.ListSSHKeys(ctx)
keys, err := m.client.ListSSHKeys(ctx, nil)
if err != nil {
return sshKeysResultMsg{keys: nil, err: err}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/language_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func newLanguageVersionsCmd() *cobra.Command {
if err != nil {
return err
}
versions, err := client.ListLanguageVersions(cliCtx.Background(), projectID)
versions, err := client.ListLanguageVersions(cliCtx.Background(), projectID, nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/network_agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func newAgentsCmd() *cobra.Command {
if err != nil {
return err
}
agents, err := client.ListAgents(cliCtx.Background())
agents, err := client.ListAgents(cliCtx.Background(), nil)
if err != nil {
return err
}
Expand Down
Loading
Loading