From f5dbe41bb781de0fca399e60338f824040dac8cf Mon Sep 17 00:00:00 2001 From: Thiago Durante Date: Wed, 5 Aug 2026 10:07:29 +0200 Subject: [PATCH 1/2] feat(servers): select an existing account SSH key for Managed VPS creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds --key-pair-identifier to `dhq servers create`, so a Managed VPS can be provisioned with an SSH key the operator already has instead of the shared key DeployHQ auto-creates. That matters for deterministic recovery access: without it there is no way to guarantee you hold the private key for a droplet. The value is the PUBLIC identifier from `dhq ssh-keys list`. The backend resolves it against the account's own keys and rejects an unknown or foreign identifier with 422, creating neither a server nor a hosted resource. The internal database id is never accepted from a client. Sent as a top-level provisioning param, a sibling of `server`, next to region/size/os_image — the backend reads params[:key_pair_identifier], not params[:server][...]. Nesting it would be a silent no-op, since Rails' permit list for Servers::ManagedVps drops it and still returns 2xx. Note this deliberately does NOT reuse --global-key-pair-id as the ticket originally proposed. That flag serialises inside `server` and is not permitted for Servers::ManagedVps, so it is a silent no-op there today; making it work would need one field serialised into two different envelope positions depending on protocol, which the SDK has no precedent for. The two are mutually exclusive and both misuses are rejected locally, before any request. Read-back adds ManagedVPSInfo.SSHKey so `dhq servers show --json managed_vps` confirms which key was applied — identifier, title and fingerprint only, never key material or an internal id. Also corrects the agent skill: global-resources.md documented `ssh-keys create --name --public-key`, and neither flag exists. Keys are generated server-side; the flags are --title and --type. Documents `ssh-keys download -o` and `ssh-keys delete` too, and warns against writing a private key to stdout. Requires the matching API change (deployhq/deployhq). Suite 871 -> 879, clean under -race and go vet. Refs DHQ-692 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz --- CHANGELOG.md | 20 +++++ internal/commands/servers.go | 25 +++++- internal/commands/servers_test.go | 62 +++++++++++++ pkg/sdk/managed_vps_key_test.go | 88 +++++++++++++++++++ pkg/sdk/servers.go | 3 + pkg/sdk/types.go | 22 +++++ skill-evals/deployhq/evals.json | 43 +++++++++ .../deployhq/references/global-resources.md | 37 +++++++- skills/deployhq/references/servers.md | 36 ++++++++ 9 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 pkg/sdk/managed_vps_key_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e98934a..bdcf289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,26 @@ While the CLI is pre-1.0, minor versions may carry breaking changes to the publi ### Added +- **`dhq servers create`**: `--key-pair-identifier` provisions a Managed VPS with + an existing account SSH key, selected by the public identifier that + `dhq ssh-keys list` prints. Sent as a top-level provisioning param alongside + `--region`/`--size`/`--os-image`. Omit it to keep the current behaviour, where + DeployHQ creates and reuses one shared managed key. Rejected locally, before + 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 diff --git a/internal/commands/servers.go b/internal/commands/servers.go index b72280a..53a6168 100644 --- a/internal/commands/servers.go +++ b/internal/commands/servers.go @@ -386,7 +386,7 @@ func newServersCreateCmd() *cobra.Command { var spaMode bool var subdirectory string // Managed VPS (beta) - var region, size, osImage string + var region, size, osImage, keyPairIdentifier string // Billing guardrail (mirrors the gate in `dhq launch`) var acceptCost bool // Deployment configuration shared with `dhq servers update` @@ -440,6 +440,23 @@ func newServersCreateCmd() *cobra.Command { if err := deployFlags.validate(); err != nil { return err } + if keyPairIdentifier != "" { + // The identifier is hoisted to the Managed VPS provisioning + // boundary; no other protocol has one, and the backend would + // silently drop it rather than error. + if protocolType != "managed_vps" { + return &output.UserError{ + Message: fmt.Sprintf("--key-pair-identifier is only valid with --protocol-type managed_vps (got %q)", protocolType), + Hint: "For ssh and rsync servers use --global-key-pair-id instead.", + } + } + if globalKeyPairID != "" { + return &output.UserError{ + Message: "--key-pair-identifier and --global-key-pair-id are mutually exclusive", + Hint: "Use --key-pair-identifier for managed_vps; --global-key-pair-id applies to ssh and rsync servers.", + } + } + } projectID, err := cliCtx.RequireProject() if err != nil { @@ -485,6 +502,8 @@ func newServersCreateCmd() *cobra.Command { Region: region, Size: size, OSImage: osImage, + // Hoisted to a top-level sibling of `server` by CreateServer. + KeyPairIdentifier: keyPairIdentifier, } deployFlags.applyToCreate(&req) // Static Hosting (beta) — nested attributes @@ -665,6 +684,10 @@ func newServersCreateCmd() *cobra.Command { cmd.Flags().StringVar(®ion, "region", "", "DigitalOcean region slug, e.g. lon1, nyc3 (managed_vps)") cmd.Flags().StringVar(&size, "size", "", "DigitalOcean droplet size slug, e.g. s-1vcpu-1gb (managed_vps)") cmd.Flags().StringVar(&osImage, "os-image", "", "OS image slug (managed_vps, default: ubuntu-24-04-x64)") + cmd.Flags().StringVar(&keyPairIdentifier, "key-pair-identifier", "", + "Public identifier of an existing account SSH key to provision the droplet with (managed_vps). "+ + "Use the Identifier column from `dhq ssh-keys list`. "+ + "Omit to let DeployHQ create and reuse its shared managed key") // Cost-acknowledgement guardrail — required for managed_vps in non-interactive mode cmd.Flags().BoolVar(&acceptCost, "accept-cost", false, "Acknowledge Managed VPS provisioning — "+managedVPSAcknowledgePhrase()+" (required for non-interactive managed_vps creation)") diff --git a/internal/commands/servers_test.go b/internal/commands/servers_test.go index 3ce1267..0f1be67 100644 --- a/internal/commands/servers_test.go +++ b/internal/commands/servers_test.go @@ -593,3 +593,65 @@ func TestServersCreate_ManagedVPSWithAcceptCost_Proceeds(t *testing.T) { assert.Equal(t, "lon1", cap.body["region"]) assert.Equal(t, "s-1vcpu-1gb", cap.body["size"]) } + +// ── Managed VPS SSH key selection (DHQ-692) ────────────────────────────────── + +func TestServersCreate_RegistersKeyPairIdentifierFlag(t *testing.T) { + root := NewRootCmd("test") + createCmd, _, err := root.Find([]string{"servers", "create"}) + require.NoError(t, err) + require.NotNil(t, createCmd.Flags().Lookup("key-pair-identifier")) + + // It is Managed-VPS-only; update has no provisioning boundary to hoist to. + updateCmd, _, err := root.Find([]string{"servers", "update"}) + require.NoError(t, err) + assert.Nil(t, updateCmd.Flags().Lookup("key-pair-identifier"), + "--key-pair-identifier applies to Managed VPS provisioning, not update") +} + +func TestServersCreate_KeyPairIdentifierRejectedForNonVPS_NoHTTP(t *testing.T) { + withResolvableContext(t) + called := blockNetwork(t) + + err := runServersCmd(t, "servers", "create", + "--name", "web", "--protocol-type", "ssh", "--hostname", "h", "--username", "u", + "--key-pair-identifier", "key-uuid-123", + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "managed_vps") + assert.False(t, *called, "must fail locally, before any request") +} + +func TestServersCreate_KeyPairIdentifierConflictsWithGlobalKeyPairID_NoHTTP(t *testing.T) { + withResolvableContext(t) + called := blockNetwork(t) + + err := runServersCmd(t, "servers", "create", + "--name", "vps", "--protocol-type", "managed_vps", + "--region", "lon1", "--size", "s-1vcpu-1gb", "--accept-cost", + "--key-pair-identifier", "key-uuid-123", + "--global-key-pair-id", "other-key", + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "mutually exclusive") + assert.False(t, *called, "must fail locally, before any request") +} + +func TestServersCreate_ManagedVPSSendsKeyPairIdentifierTopLevel(t *testing.T) { + withResolvableContext(t) + cap := captureRequest(t) + + err := runServersCmd(t, "servers", "create", + "--name", "vps", "--protocol-type", "managed_vps", + "--region", "lon1", "--size", "s-1vcpu-1gb", "--accept-cost", + "--key-pair-identifier", "key-uuid-123", + ) + require.NoError(t, err) + + srv := cap.server(t) + assert.NotContains(t, srv, "key_pair_identifier", "must not nest inside server") + + cap.mu.Lock() + defer cap.mu.Unlock() + assert.Equal(t, "key-uuid-123", cap.body["key_pair_identifier"]) +} diff --git a/pkg/sdk/managed_vps_key_test.go b/pkg/sdk/managed_vps_key_test.go new file mode 100644 index 0000000..65fd1d1 --- /dev/null +++ b/pkg/sdk/managed_vps_key_test.go @@ -0,0 +1,88 @@ +package sdk + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The key identifier is a TOP-LEVEL provisioning param, a sibling of `server` — +// the backend reads params[:key_pair_identifier], not params[:server][...]. +// Sending it nested is a silent no-op: Rails' permit list for Servers::ManagedVps +// drops unknown nested keys and still returns 2xx. +func TestCreateServer_ManagedVPS_KeyPairIdentifierIsTopLevel(t *testing.T) { + var body map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(Server{Identifier: "srv-1", Name: "vps"}) + })) + defer srv.Close() + + c := newTestClient(t, srv) + _, err := c.CreateServer(context.Background(), "my-app", ServerCreateRequest{ + Name: "vps", + ProtocolType: "managed_vps", + Region: "lon1", + Size: "s-1vcpu-1gb", + KeyPairIdentifier: "key-uuid-123", + }) + require.NoError(t, err) + + assert.Equal(t, "key-uuid-123", body["key_pair_identifier"], "must be a top-level sibling of server") + + server := body["server"].(map[string]any) + assert.NotContains(t, server, "key_pair_identifier", "must NOT be nested inside server") + assert.NotContains(t, server, "key_pair_id", "the internal id is never sent") +} + +func TestCreateServer_OmittedKeyPairIdentifierIsAbsent(t *testing.T) { + var body map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(Server{Identifier: "srv-1"}) + })) + defer srv.Close() + + c := newTestClient(t, srv) + _, err := c.CreateServer(context.Background(), "my-app", ServerCreateRequest{ + Name: "vps", ProtocolType: "managed_vps", Region: "lon1", Size: "s-1vcpu-1gb", + }) + require.NoError(t, err) + assert.NotContains(t, body, "key_pair_identifier") +} + +// Read-back: the CLI must surface which key was applied, and never key material. +func TestServer_ManagedVPS_SSHKeyReadBack(t *testing.T) { + raw := `{ + "identifier": "srv-1", + "name": "vps", + "managed_vps": { + "hosted_resource_identifier": "hr-1", + "status": "active", + "region": "lon1", + "size": "s-1vcpu-1gb", + "ssh_key": {"identifier": "key-uuid-123", "title": "ops key", "fingerprint": "SHA256:abc"} + } + }` + var s Server + require.NoError(t, json.Unmarshal([]byte(raw), &s)) + require.NotNil(t, s.ManagedVPS) + require.NotNil(t, s.ManagedVPS.SSHKey) + assert.Equal(t, "key-uuid-123", s.ManagedVPS.SSHKey.Identifier) + assert.Equal(t, "ops key", s.ManagedVPS.SSHKey.Title) + assert.Equal(t, "SHA256:abc", s.ManagedVPS.SSHKey.Fingerprint) +} + +func TestServer_ManagedVPS_SSHKeyAbsent(t *testing.T) { + var s Server + require.NoError(t, json.Unmarshal([]byte(`{"identifier":"srv-1","managed_vps":{"status":"provisioning"}}`), &s)) + require.NotNil(t, s.ManagedVPS) + assert.Nil(t, s.ManagedVPS.SSHKey, "absent ssh_key must stay nil, not a zero-valued struct") +} diff --git a/pkg/sdk/servers.go b/pkg/sdk/servers.go index d9e68b8..5ab12d3 100644 --- a/pkg/sdk/servers.go +++ b/pkg/sdk/servers.go @@ -44,6 +44,9 @@ func (c *Client) CreateServer(ctx context.Context, projectID string, req ServerC if req.OSImage != "" { body["os_image"] = req.OSImage } + if req.KeyPairIdentifier != "" { + body["key_pair_identifier"] = req.KeyPairIdentifier + } var server Server if err := c.post(ctx, fmt.Sprintf("/projects/%s/servers", projectID), body, &server); err != nil { return nil, err diff --git a/pkg/sdk/types.go b/pkg/sdk/types.go index f4a6a57..63fb451 100644 --- a/pkg/sdk/types.go +++ b/pkg/sdk/types.go @@ -213,6 +213,13 @@ type ServerCreateRequest struct { Size string `json:"-"` // OSImage is the DigitalOcean image slug (defaults to "ubuntu-24-04-x64" when empty). OSImage string `json:"-"` + // KeyPairIdentifier selects an existing account SSH key for a Managed VPS by + // its PUBLIC identifier — the one `dhq ssh-keys list` prints. The backend + // resolves it against the account's own keys and rejects an unknown or + // foreign identifier with 422, creating neither a server nor a hosted + // resource. Leave empty to let DeployHQ create and reuse its shared managed + // key. Not the internal database id, which the API never accepts from a client. + KeyPairIdentifier string `json:"-"` } // ServerUpdateRequest is the payload for updating a server. @@ -531,6 +538,21 @@ type ManagedVPSInfo struct { Size string `json:"size,omitempty"` // MonthlyCost is the droplet's monthly cost (string or number on the wire). MonthlyCost FlexString `json:"monthly_cost,omitempty"` + // SSHKey identifies the account SSH key the droplet was provisioned with. + // Nil until a key has been assigned. Deliberately narrow — the API exposes + // no key material and no internal id on this endpoint. + SSHKey *ManagedVPSSSHKey `json:"ssh_key,omitempty"` +} + +// ManagedVPSSSHKey is the account SSH key a Managed VPS was provisioned with, +// as reported by the server read-back. +type ManagedVPSSSHKey struct { + // Identifier is the key's public identifier, matching `dhq ssh-keys list`. + Identifier string `json:"identifier"` + // Title is the key's human-readable name. + Title string `json:"title,omitempty"` + // Fingerprint is the key's fingerprint. + Fingerprint string `json:"fingerprint,omitempty"` } // StaticHostingInfo is the nested `static_hosting` object within a server response for static_hosting servers. diff --git a/skill-evals/deployhq/evals.json b/skill-evals/deployhq/evals.json index ca9a3c9..e63fea9 100644 --- a/skill-evals/deployhq/evals.json +++ b/skill-evals/deployhq/evals.json @@ -607,6 +607,49 @@ }, "must_not_contain": ["servers update srv-001 --atomic"], "notes": "Atomic cannot be changed once a server has any deployment. The agent must check for existing deployments or warn the user rather than blindly issuing 'dhq servers update --atomic', which the backend rejects." + }, + { + "id": "list-ssh-keys", + "category": "global", + "prompt": "Show me the SSH keys on my account", + "expected": { + "command": "dhq ssh-keys list", + "flags": ["--json"] + }, + "notes": "The Identifier column is the public identifier used everywhere else." + }, + { + "id": "create-ssh-key", + "category": "global", + "prompt": "Create a new SSH key called 'Ops Recovery Key'", + "expected": { + "command": "dhq ssh-keys create", + "flags": ["--title", "Ops Recovery Key"] + }, + "must_not_contain": ["--name", "--public-key"], + "notes": "Keys are generated server-side; --title is the flag, not --name, and there is no --public-key." + }, + { + "id": "create-managed-vps-with-existing-key", + "category": "servers", + "prompt": "Create a Managed VPS called ops in my-app using my existing SSH key key-abc123, in lon1 at s-1vcpu-1gb", + "expected": { + "command": "dhq servers create", + "flags": ["-p", "my-app", "--protocol-type", "managed_vps", "--key-pair-identifier", "key-abc123", "--accept-cost"] + }, + "must_not_contain": ["--global-key-pair-id", "api POST"], + "notes": "managed_vps uses --key-pair-identifier with the public identifier; --global-key-pair-id is the ssh/rsync flag and is rejected here. --accept-cost is required non-interactively." + }, + { + "id": "verify-managed-vps-key", + "category": "servers", + "prompt": "Which SSH key was my Managed VPS srv-001 in my-app provisioned with?", + "expected": { + "command": "dhq servers show", + "args": ["srv-001"], + "flags": ["-p", "my-app", "managed_vps"] + }, + "notes": "The key is under managed_vps.ssh_key in the read-back." } ] } diff --git a/skills/deployhq/references/global-resources.md b/skills/deployhq/references/global-resources.md index d076d17..c80eaf9 100644 --- a/skills/deployhq/references/global-resources.md +++ b/skills/deployhq/references/global-resources.md @@ -107,16 +107,51 @@ dhq global-config-files delete 12345 ## SSH Keys +Account-level SSH keys. These are the same keys a Managed VPS can be provisioned +with — see `--key-pair-identifier` in [servers.md](servers.md). + ### `dhq ssh-keys list` +Columns: `Title | Identifier | Type | Fingerprint`. **`Identifier` is the public +identifier** — the value every other command takes. There is no way to obtain or +pass an internal database id, by design. + ```bash dhq ssh-keys list --json +dhq ssh-keys list --json title,identifier,fingerprint ``` ### `dhq ssh-keys create` +Keys are **generated server-side** — you do not supply your own public key. + +| Flag | Required | Description | +|------|----------|-------------| +| `--title` | yes | Key title | +| `--type` | no | `ED25519` (default) or `RSA` | + ```bash -dhq ssh-keys create --name "Deploy Key" --public-key "ssh-rsa AAAA..." --json +dhq ssh-keys create --title "Ops Recovery Key" --json +dhq ssh-keys create --title "Legacy Host Key" --type RSA --json ``` +### `dhq ssh-keys download ` +Writes the **private** key. Without `-o/--output` it goes to stdout, which for a +secret is almost never what you want in an agent or CI context — prefer a file. + +```bash +dhq ssh-keys download key-abc123 -o ~/.ssh/deployhq_ops +chmod 600 ~/.ssh/deployhq_ops +``` + +Never paste the output into a log, a PR, an issue, or a chat transcript. + +### `dhq ssh-keys delete ` +```bash +dhq ssh-keys delete key-abc123 +``` + +A key still attached to a Managed VPS cannot be deleted — the backend rejects it +and names the resources using it. Delete or re-key the resource first. + ## Templates ### `dhq templates list` diff --git a/skills/deployhq/references/servers.md b/skills/deployhq/references/servers.md index 1cf2134..e626c75 100644 --- a/skills/deployhq/references/servers.md +++ b/skills/deployhq/references/servers.md @@ -117,6 +117,42 @@ dhq servers show srv-001 -p my-app --json branch,preferred_branch | `--region` | DigitalOcean region slug (e.g. lon1, nyc3). Use `dhq api GET /managed_hosting/regions` to list. | | `--size` | DigitalOcean droplet size slug (e.g. s-1vcpu-1gb). Use `dhq api GET /managed_hosting/sizes` to list. | | `--os-image` | OS image slug (default: ubuntu-24-04-x64) | +| `--key-pair-identifier` | Public identifier of an existing account SSH key to provision the droplet with. Use the `Identifier` column from `dhq ssh-keys list`. Omit to let DeployHQ create and reuse its shared managed key. | + +**Choosing the SSH key for a Managed VPS** + +By default DeployHQ creates one shared account key (titled `Managed VPS Key`) on +first use and reuses it for every subsequent droplet. Pass +`--key-pair-identifier` to provision with a specific existing account key +instead — which is what you want for deterministic operator recovery access. + +Use the **public identifier** from `dhq ssh-keys list`. The internal database id +is never accepted from a client. + +```bash +# 1. find the key +dhq ssh-keys list --json title,identifier,fingerprint + +# 2. provision with it +dhq servers create -p my-app --name ops --protocol-type managed_vps \ + --region lon1 --size s-1vcpu-1gb --accept-cost \ + --key-pair-identifier key-abc123 --json + +# 3. verify which key was actually applied +dhq servers show -p my-app --json managed_vps +``` + +The read-back returns the key under `managed_vps.ssh_key` as +`{identifier, title, fingerprint}` — enough to confirm the selection, and +deliberately no key material and no internal id. + +An identifier that does not exist, or belongs to a different account, is +rejected with **422** and creates neither a server nor a hosted resource. Unlike +the atomic account-permission case, this one fails loudly. + +`--key-pair-identifier` is Managed-VPS-only and is rejected locally, before any +request, if paired with another protocol or with `--global-key-pair-id` (which +is the ssh/rsync equivalent). **SSH/FTP/FTPS/Rsync flags:** From c7b4792056295fe3045fa14c6108c9c491b08c18 Mon Sep 17 00:00:00 2001 From: Thiago Durante Date: Wed, 5 Aug 2026 10:45:06 +0200 Subject: [PATCH 2/2] docs(skill): use --json=, the only form that parses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--json` has NoOptDefVal="true" (internal/commands/root.go:160), so pflag never consumes a following token. The space form therefore leaves the field list as a positional argument: dhq servers show srv-001 -p my-app --json managed_vps → "error": "accepts 1 arg(s), received 2" (servers show is ExactArgs(1)) dhq servers show srv-001 -p my-app --json=managed_vps → reaches the API Verified against a built binary, both forms. Codex flagged the one line this PR added, but the same broken form shipped in six other places in the previous commit, and on commands without an Args validator it fails more quietly still — the field list is silently discarded and every field is returned. SKILL.md:44 was teaching the wrong form repo-wide. Fixes all 11 occurrences in the three skill files this PR already touches: 8 in references/servers.md, 2 in SKILL.md, 1 in references/global-resources.md. Bare `--json` is untouched. Two occurrences remain in references/deployments.md:62 and references/projects.md:10 — both the silent-drop class, both pre-existing, and both in files outside this PR. Left for a follow-up rather than widening the diff. Caught by Codex on #39. Refs DHQ-692 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz --- skills/deployhq/SKILL.md | 4 ++-- skills/deployhq/references/global-resources.md | 2 +- skills/deployhq/references/servers.md | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/skills/deployhq/SKILL.md b/skills/deployhq/SKILL.md index 326d010..00d0455 100644 --- a/skills/deployhq/SKILL.md +++ b/skills/deployhq/SKILL.md @@ -41,7 +41,7 @@ Verify with: `dhq auth status` - **TTY mode**: Table output with headers - **Piped/non-TTY**: Auto-switches to JSON -- **`--json`**: Force JSON output. Optionally select fields: `--json name,status,identifier` +- **`--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 @@ -136,7 +136,7 @@ dhq api POST /projects//deployments --body '{"deployment":{...}}' - `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 - **`--atomic` must be set before the server's first deployment.** The backend refuses to change it once any deployment exists ("cannot be changed after a deployment has been made to this server") and there is no override — the only remedy is a new server. Set it at `create` time; check `dhq deployments list -p --json` before ever putting `--atomic` on an `update` -- `--atomic` fails **silently** on accounts without atomic deployments enabled: the fields are stripped server-side and the request still returns 2xx with atomic off. A zero exit code does not prove it applied — read it back with `dhq servers show -p --json atomic,atomic_strategy,atomic_retention` +- `--atomic` fails **silently** on accounts without atomic deployments enabled: the fields are stripped server-side and the request still returns 2xx with atomic off. A zero exit code does not prove it applied — read it back with `dhq servers show -p --json=atomic,atomic_strategy,atomic_retention` ## Triggers diff --git a/skills/deployhq/references/global-resources.md b/skills/deployhq/references/global-resources.md index c80eaf9..62e9838 100644 --- a/skills/deployhq/references/global-resources.md +++ b/skills/deployhq/references/global-resources.md @@ -117,7 +117,7 @@ pass an internal database id, by design. ```bash dhq ssh-keys list --json -dhq ssh-keys list --json title,identifier,fingerprint +dhq ssh-keys list --json=title,identifier,fingerprint ``` ### `dhq ssh-keys create` diff --git a/skills/deployhq/references/servers.md b/skills/deployhq/references/servers.md index e626c75..3fa70ae 100644 --- a/skills/deployhq/references/servers.md +++ b/skills/deployhq/references/servers.md @@ -7,7 +7,7 @@ List servers in project. ```bash dhq servers list -p my-app --json -dhq servers list -p my-app --json name,identifier,protocol_type +dhq servers list -p my-app --json=name,identifier,protocol_type ``` ### `dhq servers show ` @@ -64,7 +64,7 @@ Two further backend constraints on `--atomic` — note that they fail *different > the setting back: > > ```bash -> dhq servers show srv-001 -p my-app --json atomic,atomic_strategy,atomic_retention +> dhq servers show srv-001 -p my-app --json=atomic,atomic_strategy,atomic_retention > ``` > > If `atomic` is `false` after a command that exited 0, the account does not @@ -99,7 +99,7 @@ treat the two as equivalent. ```bash # Unpin a server from its branch; it reverts to the repository default dhq servers update srv-001 -p my-app --branch "" --json -dhq servers show srv-001 -p my-app --json branch,preferred_branch +dhq servers show srv-001 -p my-app --json=branch,preferred_branch ``` **Static Hosting flags (beta, requires managed-resources beta on account):** @@ -131,7 +131,7 @@ is never accepted from a client. ```bash # 1. find the key -dhq ssh-keys list --json title,identifier,fingerprint +dhq ssh-keys list --json=title,identifier,fingerprint # 2. provision with it dhq servers create -p my-app --name ops --protocol-type managed_vps \ @@ -139,7 +139,7 @@ dhq servers create -p my-app --name ops --protocol-type managed_vps \ --key-pair-identifier key-abc123 --json # 3. verify which key was actually applied -dhq servers show -p my-app --json managed_vps +dhq servers show -p my-app --json=managed_vps ``` The read-back returns the key under `managed_vps.ssh_key` as @@ -249,9 +249,9 @@ dhq servers create -p my-app --name Production --protocol-type managed_vps \ # (accounts without atomic deployments enabled have the fields stripped silently). # Use the identifiers returned by the two create calls above. dhq servers show -p my-app \ - --json atomic,atomic_strategy,atomic_retention + --json=atomic,atomic_strategy,atomic_retention dhq servers show -p my-app \ - --json atomic,atomic_strategy,atomic_retention + --json=atomic,atomic_strategy,atomic_retention # 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. @@ -295,7 +295,7 @@ dhq servers update srv-001 -p my-app --atomic-retention 10 --json # Enabling atomic — only valid while the server has NO deployments yet, # and it can succeed with a 2xx without applying, so read the value back dhq servers update srv-002 -p my-app --atomic --json -dhq servers show srv-002 -p my-app --json atomic,atomic_strategy,atomic_retention +dhq servers show srv-002 -p my-app --json=atomic,atomic_strategy,atomic_retention ``` ### `dhq servers delete `