feat(servers): expose branch, auto-deploy and atomic deployment flags - #38
Conversation
Add five first-class deployment-configuration flags to `dhq servers create`
and `dhq servers update`, so automation no longer has to drop down to the
generic `dhq api` escape hatch:
--branch, --auto-deploy, --atomic, --atomic-strategy, --atomic-retention
A shared `serverDeploymentFlags` helper backs both commands so they cannot
drift apart. Only flags the operator explicitly supplied are sent, so an
update never disturbs a setting that was not named. Booleans and retention
reach the SDK as pointers: an omitted flag stays off the wire while an
explicit `--auto-deploy=false` still serialises as `false`, and a zero
retention is never sent into the backend's `>= 1` validation.
Validation of `--atomic-strategy` and `--atomic-retention` runs before the
project is resolved and before the API client is built, so a malformed
invocation fails offline with no credentials touched.
Branch handling, verified against the DeployHQ Rails source:
* `--branch ""` unpins a server so it falls back to the repository default.
`IGNORE_PARAMS_ON_BLANK` covers only credential params, so the backend
accepts and persists a blank branch; every consumer resolves it with
`.presence`. `Branch` is therefore a `*string` — as a plain string with
omitempty the empty value was dropped and the command reported success
having changed nothing. Note the API echoes it back as `""`, not null.
* Setting `--branch` on a server in a server group now warns on stderr.
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. The write succeeds and is echoed back,
making it a silent no-op; Rails hides the field in its UI, the API does
not. stdout stays pure data.
Tests: adds a capturing transport that pins the command -> request seam end
to end. Both call sites could previously be deleted with the entire suite
still green; the new tests fail when the wiring is removed (mutation-tested).
Suite goes 853 -> 863, clean under -race and go vet.
Refs DHQ-691
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
… fixture Follow-up to the DHQ-691 review. Four issues, all verified against the DeployHQ Rails source or by control test. Report the silent atomic strip instead of documenting it. An account without atomic deployments enabled has `atomic`, `atomic_strategy` and `atomic_retention` removed by the backend's permit list before any validation runs, so the call returns 2xx with atomic off and no error. The CLI already holds the create/update response — which is exactly the read-back the docs told operators to perform — so it now compares intent against the returned server and warns on stderr. No extra request; stdout stays pure data. The three params are permitted as a group, so checking `atomic` covers all of them; the other two atomic failure modes (unsupported protocol, change after the first deployment) return real validation errors and need no client check. Pin the Managed VPS billing guard with a regression test. The `--accept-cost` gate is the only thing between a non-interactive invocation and a billable provisioning call, and this branch inserted statements on both sides of it. The pre-existing test only asserted the flag was registered and never ran the command. The new test drives the real command with a network tripwire, so it fails if the guard is ever moved below the API call (mutation-verified). Make the atomic-before-first-deployment eval able to fail. run-evals.sh only checks that a response contains the expected command, so a response emitting both the deployments-list check and the forbidden `servers update --atomic` scored as a pass — the eval could not fail for the footgun it was added to catch. Adds the matching must_not_contain. Correct a fabricated value in the golden fixture. integration_test.go carried `"atomic_strategy": "symlink"` in two places, but no such strategy exists in the backend: the column defaults to copy_release, the only behavioural branch tests for copy_cache, the UI offers just those two, and no migration mentions symlink. CLAUDE.md advertises that file as validating against real API JSON, so the fixture was actively misleading. Suite goes 863 -> 871, clean under -race and go vet. Refs DHQ-691 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
WalkthroughChangesServer deployment configuration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CLI as servers create/update
participant Validation as deployment flag validation
participant Request as ServerCreateRequest/ServerUpdateRequest
participant DeployHQAPI
CLI->>Validation: validate supplied deployment flags
Validation->>Request: apply explicit deployment settings
Request->>DeployHQAPI: create or update server
DeployHQAPI-->>CLI: return server response
CLI->>CLI: emit dormant or unapplied setting warnings
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@codex review this pr |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
pkg/sdk/server_deployment_settings_test.go (1)
16-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove handler assertions to the test goroutine.
requirecallsFailNow, which must run in the test goroutine. Record the method and decode error in each handler, then assert them after the client call. Replacingrequirewithassertdoes not move the assertions out of the handler goroutine.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/sdk/server_deployment_settings_test.go` around lines 16 - 34, Update captureCreateBody and captureUpdateBody so their HTTP handlers only record the request method and JSON decode error, without calling require or assert. After each client call in the associated tests, assert the recorded method and decode error from the test goroutine, preserving the existing request validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/commands/servers_test.go`:
- Around line 28-39: Update blockNetwork to use sync/atomic.Bool for called,
storing true atomically in the transport callback and returning the atomic flag
rather than *bool. Adjust every called assertion to use Load(), add the
sync/atomic import, and preserve go vet and race-test compatibility.
In `@skills/deployhq/references/servers.md`:
- Around line 73-75: Fix the MD028 violation between the two blockquotes by
replacing the blank line with a non-blockquote separator, such as a normal
paragraph separator, so the validation-error and --branch warning blocks remain
distinct.
---
Nitpick comments:
In `@pkg/sdk/server_deployment_settings_test.go`:
- Around line 16-34: Update captureCreateBody and captureUpdateBody so their
HTTP handlers only record the request method and JSON decode error, without
calling require or assert. After each client call in the associated tests,
assert the recorded method and decode error from the test goroutine, preserving
the existing request validation.
🪄 Autofix
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: da085057-1fae-4cc6-a120-65f75fb8c7fa
📒 Files selected for processing (9)
CHANGELOG.mdinternal/commands/servers.gointernal/commands/servers_test.gopkg/sdk/integration_test.gopkg/sdk/server_deployment_settings_test.gopkg/sdk/types.goskill-evals/deployhq/evals.jsonskills/deployhq/SKILL.mdskills/deployhq/references/servers.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d816eaba8b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Every `dhq servers create --protocol-type managed_vps` example in the servers reference passed `--json` but omitted `--accept-cost`, so each one fails before creating anything: "Managed VPS creation requires --accept-cost (free for early customers during beta, billed monthly afterwards)" The guard at internal/commands/servers.go:514-519 fires whenever the session is non-TTY, non-interactive, or in JSON mode, so the examples break in any agent or CI context, not only when --json is present. Verified against the built binary with credentials resolvable: without the flag the command stops at the gate, with it the request reaches the API. Fixes the two examples added by this branch's worked two-environment section, plus the pre-existing "My VPS" example above them, which carried the same defect on main and would have been left broken next to the corrected ones. Caught by Codex on #38. Refs DHQ-691 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
Summary
Adds five first-class deployment-configuration flags to
dhq servers createanddhq servers update, so automation no longer has to drop to the genericdhq apiescape hatch:A shared
serverDeploymentFlagshelper backs both commands so they cannot drift apart. Only explicitly-supplied flags are sent, so an update never disturbs a setting the operator did not name. Booleans and retention reach the SDK as pointers: an omitted flag stays off the wire, an explicit--auto-deploy=falsestill serialises asfalse, and a zero retention never reaches the backend's>= 1validation. Strategy and retention are validated locally before the project is resolved and the API client is built, so a malformed invocation fails offline with no credentials touched.Companion API PR: deployhq/deployhq#1106 — corrects the published request schema this client depends on. Worth landing together: the spec currently types
auto_deployasstringand omits the atomic fields entirely, so the CLI's (correct) request shape fails validation against the published document.Resolves DHQ-691.
Behaviour verified against the Rails source
Rather than assume, each rule was traced to backend code:
--branch ""unpins a serverIGNORE_PARAMS_ON_BLANKis credential-only (server_params.rb:7), so a blank branch is permitted and persisted; every consumer resolves it with.presence--atomiclocked after first deployServerConcerns::Atomic#can_update_atomic?→deployments.first.nil?>= 1server.rb:103numericality validationcopy_release/copy_cacheservers_helper.rb:19-20; column defaults tocopy_releaseservers_controller.rb:683,698— permitted only whenatomic_deployments_allowed?;validate_atomic_allowedis guarded byif: :atomic_changed?, so it never firesTwo silent-failure modes now surfaced
Both were previously "succeeds with exit 0, changed nothing". Warnings go to stderr; stdout stays pure data.
--branchon a grouped server. The backend resolvesserver_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. Rails hides the field in its UI; the API does not.Test plan
go build ./cmd/dhq/go vet ./...go test ./...— 871 passing (was 853 onmain)go test -race ./...— 871 passing0/-1return structureduser_error/exit_code: 1with recovery hintsMutation-tested, not just green. Two regressions were introduced deliberately to confirm the new tests actually catch them:
applyToCreate/applyToUpdatecall sites previously left the entire suite green — it now fails--accept-costbilling gate now failsReview round
Addressed on-PR feedback; all three threads resolved.
servers.mdpassed--jsonbut omitted--accept-cost, so they hard-fail before creating anything681f3bc— also corrected the pre-existing "My VPS" example carrying the same defect onmainblockNetworkwritescalledwithout synchronisation — data raceSendTelemetry, invoked aftercmd.Execute()returns; and every caller assertsrequire.Error, so cobra returns beforePersistentPostRunandcalledis never written.-race -count=3→ 57 passed, no raceci.ymlrunsgolangci-lintonly), and the two warnings are meant to render as separate blocksThe
--accept-costfix was verified against the built binary with credentials resolvable, so the run actually reaches the gate: without the flag it stops at"Managed VPS creation requires --accept-cost"; with it the request reaches the API.Backward compatibility
pkg/sdkchange is additive — five new fields onServerCreateRequest/ServerUpdateRequest; no existing field alteredDocumentation
skills/deployhq/references/servers.md— flag table, both silent-failure warnings, unpinning, two-environment worked exampleskills/deployhq/SKILL.md— atomic gotchas in the always-loaded entry point, so an agent that never opens the reference still gets the irreversibility warningskill-evals/— 5 new cases; also fixed an existing eval that could not fail for the footgun it guardedCHANGELOG.md🤖 Generated with Claude Code
https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
Summary by CodeRabbit
New Features
Documentation