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
35 changes: 27 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,11 @@ While the CLI is pre-1.0, minor versions may carry breaking changes to the publi
any request, for a non-`managed_vps` protocol or alongside
`--global-key-pair-id` (the ssh/rsync equivalent). Requires the matching API
change (DHQ-692).

- **SDK**: `ServerCreateRequest.KeyPairIdentifier` (hoisted, `json:"-"`), and
`ManagedVPSInfo.SSHKey` (`*ManagedVPSSSHKey` — identifier, title, fingerprint)
for the read-back. Purely additive.

### Fixed

- **Agent skill**: `references/global-resources.md` documented
`dhq ssh-keys create --name --public-key`; neither flag exists. Keys are
generated server-side and the flags are `--title` (required) and `--type`
(`ED25519` default, or `RSA`). Also documents `ssh-keys download -o` and
`ssh-keys delete`.

- **`dhq servers create` / `dhq servers update`**: five deployment-configuration
flags — `--branch` (the server's preferred branch), `--auto-deploy`
(DeployHQ's native repository auto-deployment), `--atomic` (zero-downtime
Expand All @@ -46,29 +39,55 @@ While the CLI is pre-1.0, minor versions may carry breaking changes to the publi
request still succeeds, so verify with
`dhq servers show <id> -p <project> --json atomic,atomic_strategy,atomic_retention`
rather than trusting the exit code (DHQ-691).

- **`dhq servers create` / `dhq servers update`**: `--branch ""` now unpins a
server so it falls back to the repository default. Previously the empty value
was dropped by `omitempty`, and the command reported success having changed
nothing. The backend accepts and persists a blank branch (it echoes it back as
`""`, not `null`).

- **`dhq servers create` / `dhq servers update`**: warn on stderr when `--atomic`
was requested but the server comes back with atomic off. On accounts without
atomic deployments enabled the backend strips the atomic params before
validation and returns 2xx, so this was previously a silent success. The
create/update response is the read-back the docs asked operators to perform,
so no extra request is made. stdout stays pure data.

- **`dhq servers create` / `dhq servers update`**: warn on stderr when
`--branch` is set on a server that belongs to a server group. The backend
resolves the branch as `server_group.branch || server.branch ||
repository.branch`, and grouped servers are excluded from auto-deployment
entirely, so a branch stored on a grouped server never deploys — previously a
silent no-op. stdout stays pure data.

- **SDK**: `ServerCreateRequest` and `ServerUpdateRequest` gained matching
`Branch`, `AutoDeploy`, `Atomic`, `AtomicStrategy` and `AtomicRetention`
fields. Purely additive — no existing field changed, so this is not a breaking
change for importers of `github.com/deployhq/deployhq-cli/pkg/sdk`. `Branch`
is a `*string` so an explicitly-cleared branch survives serialisation.

### Fixed

- **Agent skill**: corrected commands and semantics that did not match the CLI.
`dhq deploy --wait` was documented as blocking, but output auto-switches to
JSON whenever stdout is not a TTY and the JSON path returns as soon as the
deployment is queued — so in any pipe, CI job or agent it exits 0 immediately
without waiting. The skill now documents the create → `deployments watch` →
`deployments show` composition and requires asserting `status == "completed"`.
Also: a cancelled deployment exits 0 (only `failed` is non-zero), so the
blanket "non-zero = failure" claim is now qualified; `ssh-commands create`
cannot set callback phase, server targeting, timeout or halt-on-error, so a
`post_deploy` SSH deployment check is documented as the first-class
alternative; and `--json=<fields>` unwraps the response envelope, so selected
fields are read as `.<field>` rather than `.data.<field>`. Reference-file and
eval-suite counts in `CLAUDE.md` corrected (DHQ-695).

- **Agent skill**: `references/global-resources.md` documented
`dhq ssh-keys create --name --public-key`; neither flag exists. Keys are
generated server-side and the flags are `--title` (required) and `--type`
(`ED25519` default, or `RSA`). Also documents `ssh-keys download -o` and
`ssh-keys delete`.

## [0.20.1] - 2026-07-24

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ internal/skillinstaller/ Detects installed AI agents (12 supported) and
write into the repo (opt-in via --agent). Powers
`dhq skills` and the `dhq hello` onboarding step.

skills/deployhq/ Agent skill guide + per-domain reference docs (8 files)
skills/deployhq/ Agent skill guide + per-domain reference docs (9 files)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
SKILL.md is the entry point for any AI agent.

skill-evals/deployhq/ Eval suite (49 cases) testing LLM → CLI translation
skill-evals/deployhq/ Eval suite (69 cases) testing LLM → CLI translation
run-evals.sh drives Claude API, checks command accuracy.
```

Expand Down
32 changes: 32 additions & 0 deletions skill-evals/deployhq/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,38 @@
"flags": ["-p", "my-app", "managed_vps"]
},
"notes": "The key is under managed_vps.ssh_key in the read-back."
},
{
"id": "deploy-and-verify-in-automation",
"category": "gotchas",
"prompt": "In a CI pipeline, deploy my-app to Production and make the job fail if the deployment doesn't succeed",
"expected": {
"command": "dhq deploy",
"flags": ["-p", "my-app", "watch", "show", "completed"]
},
"must_not_contain": ["--wait"],
"notes": "check_eval word-splits expected.flags and substring-matches each token, so single words are used: watch + show + completed together force the whole create -> watch -> assert sequence rather than any one step. --wait is inert outside a TTY: output auto-switches to JSON and the JSON path returns as soon as the deploy is queued, exiting 0."
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
"id": "cancelled-deploy-not-success",
"category": "gotchas",
"prompt": "My deploy script exits 0 but nothing was deployed. How do I detect a cancelled deployment?",
"expected": {
"command": "dhq deployments show",
"flags": ["status"]
},
"notes": "A cancelled deployment exits 0 from the watcher; only failed is non-zero. The status must be asserted explicitly."
},
{
"id": "post-deploy-reconcile-check",
"category": "configuration",
"prompt": "Run a reconcile script on my-app's server srv-abc123 after each release, and fail the deployment if it errors",
"expected": {
"command": "dhq deployment-checks create",
"flags": ["-p", "my-app", "--stage", "post_deploy", "--check-type", "ssh", "--servers", "srv-abc123"]
},
"must_not_contain": ["ssh-commands create"],
"notes": "ssh-commands create cannot set callback phase, targeting, timeout or halt-on-error; a post_deploy ssh deployment check can, and fails the deployment on non-zero exit."
}
]
}
4 changes: 2 additions & 2 deletions skills/deployhq/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Verify with: `dhq auth status`
- **Piped/non-TTY**: Auto-switches to JSON
- **`--json`**: Force JSON output. Optionally select fields: `--json=name,status,identifier`
- **Breadcrumbs**: JSON responses include `breadcrumbs` array with suggested next commands
- **Exit codes**: 0 = success, non-zero = failure
- **Exit codes**: 0 = success, non-zero = failure — with one exception: a **cancelled** deployment also exits 0. Never infer deployment success from the exit code alone; assert `status == "completed"` via `dhq deployments show <id> --json=status`

## Non-Interactive Mode

Expand Down Expand Up @@ -131,7 +131,7 @@ dhq api POST /projects/<permalink>/deployments --body '{"deployment":{...}}'
- Some API fields return strings OR numbers inconsistently (handled internally by `FlexString`)
- `dhq deploy` auto-fetches latest revision if `--revision` is omitted
- `dhq deploy` is **incremental by default** — it picks up from the server's last successful deploy. Use `--full` for a full-branch deploy or `--start-revision <sha>` to pin a specific start commit
- `dhq deploy --wait` blocks until deployment completes (use `--timeout` to cap)
- `dhq deploy --wait` blocks **only in an interactive terminal**. Output auto-switches to JSON whenever stdout is not a TTY, and the JSON path returns as soon as the deployment is queued — so in any pipe, CI job or agent, `--wait` does nothing and the command exits 0 immediately. In automation use create → `dhq deployments watch <id>` → `dhq deployments show <id> --json=status` and require `completed` (see [deployments.md](references/deployments.md))
- Deployment `watch` uses TUI in TTY mode, append-only in pipes
- `dhq env-vars create` prompts for value if `--value` is omitted (not agent-friendly — always pass `--value`)
- `dhq servers create` / `dhq servers update` configure deployment behaviour with `--branch` (preferred branch), `--auto-deploy` (DeployHQ's native auto-deployment), `--atomic`, `--atomic-strategy` and `--atomic-retention`. On `update`, only flags you explicitly pass are sent — omitted flags never disturb existing settings
Expand Down
39 changes: 39 additions & 0 deletions skills/deployhq/references/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,45 @@ dhq ssh-commands list -p my-app --json
dhq ssh-commands create -p my-app --command "sudo systemctl restart app" --description "Restart" --json
```

> **Limitation — `ssh-commands create` cannot express a complete SSH command.**
> The CLI sends only `command`, `description` and `timing`. It cannot set the
> callback phase, server targeting, timeout, or halt-on-error, and the backend
> requires a valid timeout and description while defaulting the callback to
> `before_changes` and halt-on-error to `false`. So a command created this way
> runs **before changes are uploaded, on every server, and does not fail the
> deployment when it errors** — rarely what a release hook wants.
>
> **For post-release reconciliation or health gates, use a deployment check
> instead** — it is first-class in the CLI and expresses everything the hook
> needs:

```bash
dhq deployment-checks create \
-p my-app \
--name "Reconcile release" \
--stage post_deploy \
--check-type ssh \
--command 'cd %current_path% && ./scripts/deploy-release.sh %endrev%' \
--servers srv-abc123 \
--timeout 600 \
--json
```

Post-deploy SSH checks:

| Property | Behaviour |
|---|---|
| Ordering | Run **after** the atomic `symlink_release` step, so `%current_path%` is the new release |
| Variables | `%current_path%` and `%endrev%` are expanded |
| Failure | A non-zero exit **fails the deployment** |
| Timeout | Maximum **600 seconds** |
| Targeting | `--servers` takes exact server identifiers (not fuzzy names) |
| Availability | Beta-feature gated |

If first-time bootstrap on a host can exceed 600 seconds, do that installation
separately and enrol the check afterwards, so the timeout applies only to the
steady-state reconcile.

### `dhq ssh-commands update <id>`

Update fields on an existing SSH command. Only flags you pass are sent — omit `--timing` to leave the current value untouched.
Expand Down
61 changes: 55 additions & 6 deletions skills/deployhq/references/deployments.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Shortcut for creating a deployment with smart defaults.
| `--revision` | `-r` | End revision SHA (auto-fetches latest if omitted) |
| `--start-revision` | | Start revision SHA (default: server's last deployed commit) |
| `--full` | | Deploy entire branch from the first commit (overrides incremental default) |
| `--wait` | `-w` | Block until deployment completes |
| `--wait` | `-w` | Block until the deployment completes. **Interactive terminals only** — see the warning below. |
| `--timeout` | | Timeout in seconds for `--wait` (0 = no timeout) |

```bash
Expand All @@ -32,11 +32,13 @@ dhq deploy -p my-app --json
# Deploy specific branch to specific server
dhq deploy -p my-app -b staging -s "Staging Server" --json

# Deploy and wait for completion
dhq deploy -p my-app -s Production --wait --json
# Deploy and wait for completion — INTERACTIVE TERMINAL ONLY.
# In a pipe, CI job or agent, --wait does nothing (see the warning below);
# use the create -> watch -> show composition instead.
dhq deploy -p my-app -s Production --wait

# Deploy with timeout
dhq deploy -p my-app --wait --timeout 300 --json
# Deploy with timeout (also interactive-only)
dhq deploy -p my-app --wait --timeout 300

# Deploy a specific commit range (e.g. a hotfix)
dhq deploy -p my-app --start-revision a1b2c3d --revision e4f5g6h --json
Expand All @@ -52,7 +54,54 @@ dhq deploy -p my-app --full --json
- **Start revision resolution (incremental by default):** `--full` forces an empty start (full-branch deploy). Otherwise `--start-revision` is used if set. Otherwise the resolved server's `last_revision` (last successful deploy) is used — this is what makes deploys incremental. Servers with no prior deploy and server-group identifiers fall through to a full deploy because there's no single baseline to start from.
- An unknown `--branch` errors out instead of silently deploying the wrong branch.
- `--full` and `--start-revision` are mutually exclusive.
- `--wait` shows TUI progress in TTY, append-only in pipes
- `--wait` shows TUI progress in a TTY. It has **no effect** outside one — see the warning below.

> **Warning — `--wait` does not wait unless stdout is a terminal.**
> Output auto-switches to JSON whenever stdout is not a TTY (`WantsJSON()` is
> `JSONMode || !IsTTY`), and the JSON path returns as soon as the deployment is
> **queued**, before the wait begins. So in any pipe, CI job, agent invocation,
> or redirect, `dhq deploy --wait` prints the queued-deployment JSON and exits
> **0 immediately** — the deployment may still be running, and may still fail.
> Passing `--json` explicitly does the same thing; the trigger is the missing
> TTY, not the flag.
>
> **Never treat a zero exit from `dhq deploy` as "the deployment succeeded".**
> Use the composition below, which is safe everywhere.

**Deploy and verify (the safe composition — use this in automation):**

```bash
# 1. create, and capture the identifier
id=$(dhq deploy -p my-app -s Production --json | jq -r '.data.identifier')

# 2. follow it to a terminal state (this genuinely blocks)
dhq deployments watch "$id" -p my-app

# 3. assert the final status — this is the step that actually decides success
status=$(dhq deployments show "$id" -p my-app --json=status | jq -r '.status')
[ "$status" = "completed" ] || { echo "deployment $id ended as: $status"; exit 1; }
```

> **Note — field selection changes the JSON shape.** With `--json` alone the
> response is the full envelope (`{"ok":…, "data":{…}}`), so read values with
> `.data.<field>`. With `--json=<fields>` the envelope is unwrapped and only the
> selected fields are emitted at the **top level** (`{"status":"completed"}`), so
> read them with `.<field>`. Using `.data.status` after `--json=status` yields
> `null`, which would fail the check above on every successful deployment.

Verify the deployed revision and server too when it matters:

```bash
dhq deployments show "$id" -p my-app --json=status,end_revision,servers
```

> **Warning — a cancelled deployment exits 0.**
> `dhq deployments watch` returns success for a cancelled deployment
> (`watch.go` treats `cancelled` as a clean terminal state), and `dhq rollback`
> shares the same watcher. Only `failed` produces a non-zero exit. A cancelled
> deployment has **not** deployed your code, so the explicit
> `status == "completed"` assertion in step 3 is required — for rollbacks as
> well as deploys. Do not rely on exit codes alone.

### `dhq deployments list`
List recent deployments with pagination.
Expand Down
11 changes: 8 additions & 3 deletions skills/deployhq/references/servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,16 @@ dhq servers show <production-identifier> -p my-app \

# Staging has no native auto-deployment, so ship it explicitly when you want to.
# No -b needed: the server's preferred branch (`staging`) is used.
dhq deploy -p my-app -s Staging --wait --json
# --wait is omitted deliberately: it has no effect outside a terminal, so this
# creates, follows, then asserts the final status. See deployments.md.
id=$(dhq deploy -p my-app -s Staging --json | jq -r '.data.identifier')
dhq deployments watch "$id" -p my-app
# NB: --json=<fields> unwraps the envelope, so it is .status here, not .data.status
[ "$(dhq deployments show "$id" -p my-app --json=status | jq -r '.status')" = "completed" ] || exit 1

# Production needs no CLI deploy — DeployHQ auto-deploys `main` on push.
# Deploy it manually only when you want an out-of-band release:
dhq deploy -p my-app -s Production --wait --json
# Deploy it manually only when you want an out-of-band release (same pattern).
dhq deploy -p my-app -s Production --json
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

### `dhq servers update <identifier>`
Expand Down
Loading