Skip to content

feat: dhq launch — one-command deploy to Static Hosting & Managed VPS#25

Open
thdurante wants to merge 23 commits into
mainfrom
feat/one-command-deploy
Open

feat: dhq launch — one-command deploy to Static Hosting & Managed VPS#25
thdurante wants to merge 23 commits into
mainfrom
feat/one-command-deploy

Conversation

@thdurante

@thdurante thdurante commented Jun 9, 2026

Copy link
Copy Markdown

Adds dhq launch — take a project folder to a live URL on DeployHQ's own infrastructure (Static Hosting or a Managed VPS) in one command. Plus the managed_vps / static_hosting protocols, SDK methods, framework detection, and agent docs.

How dhq launch works

One command, end to end:

  1. Auth — picks up credentials from flags, DEPLOYHQ_* env vars, or the keyring; offers signup/login when interactive.
  2. Detect — inspects the project folder (package.json, Gemfile, composer.json, …) and suggests a target: static frameworks (Vite, Next, Hugo, …) → Static Hosting; server runtimes (Rails, Django, Express, …) → Managed VPS. Detection also pre-fills the build command and output directory. It runs locally on purpose: the backend's detector (Detection::FromFileList, used by the web onboarding wizard) inspects a repo already connected via a provider, which doesn't exist yet at this point in the flow — the CLI mirrors that wizard's mapping. (Possible follow-up: a public API endpoint accepting a filename list so the CLI reuses backend detection once authed.)
  3. Beta + plan checks — enrolls the account in the managed-resources beta (admin-only, idempotent) and pre-flights plan limits before touching anything.
  4. Project + repo — creates the project (or reuses the one in .deployhq.toml) and connects the git remote.
  5. Provision — Static Hosting: creates the site under the chosen subdomain. Managed VPS: shows region/size/cost and requires acknowledgement (--accept-cost non-interactively), then provisions. Both poll the server until it reports active.
  6. Build + deploy — sets the detected build command on the project (skipped if build commands already exist), deploys, and watches to completion.
  7. Done — prints the live URL (last stdout line, scriptable) and writes .deployhq.toml, so the next deploy is just dhq deploy. Re-running launch resolves the existing project/server instead of provisioning twice.

On failure, the error carries a stable reason + retryable flag (see below), and --cleanup-on-failure tears down a server whose deploy failed.

launch is the funnel for DeployHQ's two managed offerings only. The interactive target menu's third option — "Use my own server (SSH/FTP/…)" — hands off to dhq init, and every existing protocol (SSH, FTP/FTPS, rsync, S3, DigitalOcean, Hetzner, Heroku, Netlify, Shopify, …) keeps working through dhq servers create / dhq init exactly as before.

What this adds

  • dhq launch — the flow above, non-interactive-first: prompts only fill missing values when a TTY is present; agents/CI get --json, --dry-run (no side effects), and fail-fast structured errors.
  • Structured-error taxonomy agents can branch on: auth_required, beta_enroll_required, accept_cost_required, repo_unreachable, plan_limit_reached, subdomain_taken, rate_limited, provision_failed, deploy_failed — each with a retryable boolean so agents know to back off vs. stop.
  • managed_vps / static_hosting join the existing protocol list on dhq servers create (SSH, FTP/FTPS, rsync, S3, and the rest are untouched), plus the SDK methods, internal/detect framework detection, and agent docs (README, SKILL guide, dhq commands metadata).

Depends on (deploy-order blocker)

Requires the deployhq backend PR deployhq/deployhq#926 (beta-enroll + capability + server-show provisioning endpoints) to be live in production. Gate the public CLI release until that ships to prod.

Pricing copy — free during beta

Managed VPS and Static Hosting are free for early customers while in beta; the listed monthly rate applies once the beta ends. All runtime copy derives from a single switch — meteredResourcesInBeta in internal/commands/metered.go — so flipping it to false at GA updates every cost/acknowledgement string (flag help, cost lines, the non-interactive gate, the failure warning, the target menu). The markdown docs repeat the wording and are flagged to update in the same change.

Safety / review

  • A Managed VPS is never provisioned non-interactively without --accept-cost — enforced in both dhq launch and dhq servers create (--yes alone is not enough).
  • Orphaned-resource protection: cleanup / teardown-hint on provision and deploy failure (--cleanup-on-failure).
  • Rate-limit aware: a 429 from the metered-provisioning rate limit (deployhq#933) maps to a retryable rate_limited reason carrying the Retry-After backoff — distinct from the 422 cap (plan_limit_reached), so CI backs off instead of failing hard.
  • A Review Council (Claude/Codex/Gemini) reviewed this; all findings folded in — most importantly: re-runs resolve the existing server instead of double-provisioning, --dry-run has no side effects, and the detected build command is created through the real /build_commands endpoint (it previously went to a nonexistent path and silently never landed).

Testing

Three layers, all hermetic — no live backend in CI:

  • Unit + flow tests against httptest fakes: target resolution, cost gate, idempotent re-runs, failure cleanup, build-command creation, and the provisioning poll (provisioning → active terminates; error stops with provision_failed instead of spinning).
  • Contract layernewSpecValidatingClient wraps the test client with a transport that validates every outgoing request against a committed snapshot of the backend's generated OpenAPI doc (internal/commands/testdata/openapi.json, refreshed via script/update-openapi-fixture.sh). A request to an undocumented path or with a schema-violating body fails the test — this is exactly the bug class review caught (the build command was POSTed to a path that doesn't exist), and it can't recur silently now.
  • Scheduled drift check (.github/workflows/openapi-drift.yml) — the fixture pins the contract this CLI was built against (lockfile model; additive backend changes stay green). To catch breaking backend drift, a weekday-morning job fetches the live spec and re-runs the contract tests against it. It no-ops until the DHQ_SPEC_URL repository variable is set — point it at staging's /docs.json once #926 deploys there.

Existing CI needed no changes — everything runs under the normal go test ./... + golangci-lint jobs (all green, 0 lint issues).

Summary by CodeRabbit

  • New Features

    • New dhq launch: one-command detect → provision → deploy flow (writes .deployhq.toml, prints live URL). Framework auto-detection suggests Static Hosting vs Managed VPS. Supports --dry-run, --json, non-interactive and interactive modes, and optional cleanup-on-failure.
    • Beta: Static Hosting and Managed VPS provisioning (subdomain/SPA/subdirectory and region/size/OS options) with non-interactive billing guard (--accept-cost).
  • Documentation

    • Quick Start, command references and skill guides updated with launch usage, flags, examples, and beta enrollment flow.

thdurante and others added 13 commits June 9, 2026 09:12
Add Server nested provisioning blocks (static_hosting, managed_vps) and the
ServerCreateRequest managed params. Those params (hosted_website_attributes,
region, size, os_image) are hoisted by CreateServer to top-level siblings of
`server` — the backend reads params[:region] / params[:hosted_website_attributes],
not params[:server][:...]. Includes create-shape contract tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EnrollBeta (POST /beta/enrollments), GetAccountCapabilities (GET /profile —
non-admin-readable), ListManagedHostingRegions/Sizes, GetServerProvisioningState
plus LiveURL/ProvisioningStatus/IsProvisioning helpers reading the nested blocks.
Signup now sends terms_accepted + client, models email_verified and the 2FA 422
fallback (TwoFactorError).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ribution

Adds the two protocols to the create wizard/flags (--subdomain/--spa-mode/
--subdirectory, --region/--size/--os-image) and protocol lists. The signup
command sends terms_accepted and client=dhq-cli for attribution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
internal/detect reads root project files and returns {Framework, SuggestedProtocol,
BuildCommand, OutputDir, SPA}, mirroring the web funnel's coarse mapping
(node-app -> static_hosting, server-runtime -> managed_vps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New `dhq launch` command, non-interactive-first (D7): auth/bootstrap →
framework detect → capability + beta-enroll pre-flight → repo-deployability
check → target select → project/repo → plan-limit pre-flight → provision
(static or managed VPS, polling to active) → build config → deploy → print
the live URL → persist to .deployhq.toml. Agent/CI surface: --json, --dry-run
(no side effects, shows cost), --accept-cost (billable VPS guardrail — never
provisions under --yes alone), --non-interactive/--interactive, harness
auto-detection, and deterministic structured errors {error,next_step,details}
(auth_required, beta_enroll_required, accept_cost_required, repo_unreachable,
plan_limit_reached, subdomain_taken, provision_failed, deploy_failed). In
non-interactive mode beta enrollment is attempted directly (idempotent +
admin-gated server-side). Registered in root; deploy's no-project hint points
to launch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rors, dry-run

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the dhq launch entry to the agent-metadata table consumed by
`dhq commands --json` (and by agents): interactive, idempotent re-runs,
requires_confirmation (provisions billable resources), supports_json,
safe_for_automation, resource_types project/server/deployment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README feature section + Quick Start entry, and the agent skill guide
(SKILL.md command table, a one-command decision tree, a trigger) plus a new
references/launch.md covering the flag set, the non-interactive/--json/--dry-run
contract, the --accept-cost billable guardrail, and the structured-error
reasons agents can branch on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Server (string) and Target (string) fields to config.Config with
mapstructure + json tags so `dhq launch` can persist and re-read the
provisioned server identifier across runs.

Also add "server" and "target" to the Keys slice so the source-tracking
and env-binding logic covers them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…beta, caps, branch, detection, cleanup

Fix 1 (idempotency): add serverID to launchConfig, read from .deployhq.toml via
resolveLaunchConfig; in runLaunch call GetServerProvisioningState before provisioning
— skip CreateServer when an existing server resolves.

Fix 2 (dry-run side effects): move dry-run exit to BEFORE beta enrollment and any
mutation; pass command ctx to launchDryRun instead of context.Background().

Fix 3 (interactive target → 403): evaluate isManagedTarget and run beta enrollment
AFTER target selection so interactive picks are covered before the enroll attempt.

Fix 5 (provision failure cleanup): on provision error in runLaunch, run
launchDeployFailureCleanup (same path as deploy failures) so --cleanup-on-failure
and named-resource hint apply to provision timeouts/failures too.

Fix 6 (repo errors swallowed): treat repo-connect failures as terminal launchErrors
(reasonRepoUnreachable) on the project-reuse, auto-select, and interactive-pick paths
— not just the new-project path.

Fix 7 (detection defaults): seed cfg.subdirectory from detection.OutputDir and
cfg.spaMode from detection.SPA before calling launchProvisionStatic when flags unset.

Fix 8 (caps 404 → plan_limit_reached): introduce capsKnown bool; when caps endpoint
returns 404 set capsKnown=false and skip the eligibility/plan-limit gate so the
backend's CreateServer is the authority.

Fix 9 (branch hardcoded): replace `"main"` fallback in launchEnsureRepo with
detectDefaultBranch() which tries git symbolic-ref then origin/HEAD; returns "" so
the API resolves the repo default when local git provides nothing.

Cheap fixes:
- Use errors.As in writeLaunchError (not bare type assertion) for wrapped errors.
- Terminate pollProvisioningState immediately on 401/403/404 (non-retryable).
- Add runGitCommand helper for git sub-commands.
- Extract launchGetCaps helper to avoid duplicated caps-fetch logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed_vps

A billable Managed VPS must never be created non-interactively without explicit
cost acknowledgement. Mirror the gate from `dhq launch --vps`:

- Add --accept-cost bool flag to newServersCreateCmd.
- In non-interactive mode (non-TTY, --non-interactive, --json) require the flag
  or return a UserError before any API call.
- In interactive mode, prompt to confirm when --accept-cost is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t gate, cleanup, repo errors

New tests:
- TestLaunchIdempotent_SecondRunSkipsProvision: re-run with a persisted
  serverID must call GET /servers/:id and NOT POST /servers.
- TestLaunchDryRun_NoBetaEnroll: --dry-run must never POST /beta/enrollments
  even when caps.BetaFeatures is false.
- TestServersCreate_ManagedVPS_RequiresAcceptCost_NonInteractive: --accept-cost
  flag is registered; non-interactive managed_vps create without it returns error.
- TestLaunchProvisionFailure_CleanupOnFailure_DeletesCalled: provision failure
  with --cleanup-on-failure issues DELETE /servers/:id.
- TestLaunchEnsureProject_RepoConnectFailure_IsTerminal: CreateRepository 422
  returns a repo_unreachable launchError before provision.

Also:
- Update isLaunchErr helper to use errors.As (wrapped errors).
- Add "errors" import.
- Update launchDryRun calls to pass t.Context() (new signature).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new dhq launch command implementing detect → provision → deploy for DeployHQ-managed Static Hosting and Managed VPS, plus framework detection, SDK managed-hosting helpers, servers CLI flags/guardrails, config persistence, extensive tests, and documentation updates.

Changes

Managed Hosting Launch Feature

Layer / File(s) Summary
README Quick Start & protocol list
README.md
Adds Quick Start snippet and detailed dhq launch docs; adds static_hosting (beta) and managed_vps (beta) to server protocols.
Skill & reference docs
skills/deployhq/*, docs/SKILL.md
Adds launch command group and launch.md reference; documents managed-resources beta workflow, regions/sizes, servers create examples, and provisioning polling.
Agent metadata & root command
internal/commands/agent_metadata.go, internal/commands/root.go
Registers dhq launch in root command and agent metadata table with interactive/idempotent/confirmation/JSON/automation-safe flags.
Config persistence & init mapping
internal/config/config.go, internal/commands/init.go
Adds Server and Target config fields persisted to .deployhq.toml; extends init protocol display-to-API mapping with Static Hosting and Managed VPS beta labels.
servers create CLI & guardrails
internal/commands/servers.go
Adds static-hosting and managed-VPS flags (--subdomain, --spa-mode, --subdirectory, --region, --size, --os-image, --accept-cost), wires HostedWebsiteAttributes, and enforces --accept-cost guard in non-interactive runs.
SDK types & server payload shaping
pkg/sdk/types.go, pkg/sdk/servers.go, pkg/sdk/server_protocols_test.go
Adds managed-hosting types (AccountCapabilities, BetaEnrollment*, ManagedVPSInfo, StaticHostingInfo, HostedWebsiteAttributes, TwoFactorError) and changes CreateServer to hoist provisioning fields to top-level JSON with contract tests.
Signup changes & 2FA mapping
pkg/sdk/signup.go, pkg/sdk/signup_test.go, internal/commands/signup.go
Adds Client and TermsAccepted to signup payload, exposes EmailVerified, detects 2FA-related 422 responses and returns TwoFactorError, and sets Client to "dhq-cli" in CLI signup.
SDK managed-hosting methods
pkg/sdk/managed_hosting.go, pkg/sdk/managed_hosting_test.go
Adds GetAccountCapabilities, EnrollBeta, ListManagedHostingRegions/Sizes, GetServerProvisioningState, LiveURL/ProvisioningStatus/IsProvisioning helpers with tests.
Framework detection
internal/detect/detect.go, internal/detect/detect_test.go
Adds Detect(dir) with extensive Node/framework heuristics and fileContains helpers; many fixture-based tests covering static frameworks, generators, server runtimes, precedence, and edge cases.
dhq launch implementation
internal/commands/launch.go
Implements newLaunchCmd, resolveLaunchConfig, runLaunch orchestration (auth, detect, dry-run, capabilities, beta enrollment, project/repo ensure, provision static/vps with polling, deploy, optional cleanup, persistence), structured launchError taxonomy, and JSON error output.
Launch tests & regressions
internal/commands/launch_test.go
Adds comprehensive unit/integration/regression tests for resolveLaunchConfig, detection, error contracts, rate-limit mapping, accept-cost gating, beta enrollment, dry-run isolation, idempotency, cleanup-on-failure, polling fast paths, and CLI wiring.
Metered copy helpers
internal/commands/metered.go
Adds a beta gate and helper functions for consistent Managed VPS cost/acknowledgement text used by CLI help and prompts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • facundofarias
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and clearly describes the primary change: introducing 'dhq launch' as a one-command deployment feature for Static Hosting and Managed VPS, matching the changeset scope perfectly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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/one-command-deploy

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
internal/commands/launch_test.go (2)

27-27: 💤 Low value

Consider configuring Logger writer destination.

The Logger is initialized as an empty struct. Based on the comment at lines 832-834, tests don't need a real log file. Consider explicitly setting the Logger's writer to io.Discard or one of the test buffers to ensure log writes don't fail or panic if the Logger implementation expects a configured writer.

📝 Suggested fix
 	env := &output.Envelope{
 		Stdout:         &stdout,
 		Stderr:         &stderr,
-		Logger:         &output.Logger{},
+		Logger:         &output.Logger{Writer: io.Discard},
 		IsTTY:          false,
 		NonInteractive: true,
 		JSONMode:       false,
🤖 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 `@internal/commands/launch_test.go` at line 27, The Logger in the test setup is
created as an empty output.Logger{} which may lack a configured writer and cause
panics on write; update the test initialization that sets Logger to a configured
instance by assigning its writer (e.g., set the Logger.Writer field to
io.Discard or a test buffer) so log writes in the launch_test.go test use a safe
destination; locate the logger setup where Logger: &output.Logger{} is created
and replace it with an instance whose writer is explicitly set.

711-738: ⚡ Quick win

Test validates flag presence but not the cost-acceptance guard behavior.

The test name TestServersCreate_ManagedVPS_RequiresAcceptCost_NonInteractive suggests it validates that creating a managed_vps server without --accept-cost returns an error in non-interactive mode. However, the test only verifies that the --accept-cost flag is registered (line 735) and that no API calls were made before the flag check (line 737).

Consider adding an actual command execution test that:

  1. Calls servers create with managed_vps protocol
  2. Without --accept-cost in non-interactive mode
  3. Expects a UserError before any API call
🤖 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 `@internal/commands/launch_test.go` around lines 711 - 738, The test currently
only checks flag registration; update
TestServersCreate_ManagedVPS_RequiresAcceptCost_NonInteractive to actually
execute the servers create command in non-interactive mode and assert the
accept-cost guard fires: set cliCtx to a non-interactive context (use the shared
cliCtx variable), build the root command via NewRootCmd("test"), set args to run
"servers create" with --protocol=managed_vps but without --accept-cost, execute
the command (via ExecuteC/Execute) and assert it returns a UserError and that
apiCalled remains false (no API requests were made). Ensure you reset cliCtx
after the test and keep the existing apiCalled/http test server setup and
assertions.
docs/SKILL.md (1)

110-145: ⚡ Quick win

Add language identifiers to fenced code blocks.

The fenced code blocks at lines 110, 116, 122, 129, 135, and 141 are missing language identifiers, which prevents proper syntax highlighting. Add bash or shell identifiers to these blocks.

📝 Suggested fix
 ### "Check account beta/managed-offerings eligibility"
-```
+```bash
 dhq api GET /account/capabilities --json

"Enable managed-resources beta from CLI"

- +bash
dhq api POST /beta/enrollments --body '{"protocol":"static_hosting"}'


### "List Managed VPS regions and sizes"
-```
+```bash
dhq api GET /managed_hosting/regions --json
dhq api GET /managed_hosting/sizes --json

"Create a Static Hosting server"

- +bash
dhq servers create -p --name "My Site" --protocol-type static_hosting
--subdomain --subdirectory dist --json


### "Create a Managed VPS server"
-```
+```bash
dhq servers create -p <project> --name "My VPS" --protocol-type managed_vps \
  --region lon1 --size s-1vcpu-1gb --json

"Poll provisioning status"

- +bash
dhq servers show -p --json

🤖 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 `@docs/SKILL.md` around lines 110 - 145, Add bash language identifiers to the
fenced code blocks that contain the dhq CLI examples so they get syntax
highlighting: locate the blocks starting with the commands "dhq api GET
/account/capabilities --json", "dhq api POST /beta/enrollments --body
'{\"protocol\":\"static_hosting\"}'", the two-line block with "dhq api GET
/managed_hosting/regions --json" and "dhq api GET /managed_hosting/sizes
--json", the static hosting create block starting "dhq servers create -p
<project> --name \"My Site\" --protocol-type static_hosting ...", the managed
VPS create block starting "dhq servers create -p <project> --name \"My VPS\"
--protocol-type managed_vps ...", and the polling block "dhq servers show
<server-id> -p <project> --json"; for each, change the opening triple backticks
from ``` to ```bash (or ```shell) so the blocks render with shell highlighting.

Source: Linters/SAST tools

internal/commands/launch.go (1)

182-194: 💤 Low value

Consider adding explicit SPA mode flags for non-interactive override.

Detection automatically sets spaMode when a framework is recognized as an SPA (line 354), and the interactive flow prompts for it (lines 1005-1011). However, non-interactive users cannot override the detection result if they need to force SPA or non-SPA routing.

Adding --spa and --no-spa flags would allow explicit control in CI/agent contexts when detection guesses incorrectly.

🤖 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 `@internal/commands/launch.go` around lines 182 - 194, Add explicit SPA
override flags to the command by declaring new boolean flag variables (e.g.,
flagSPA and flagNoSPA) via cmd.Flags().BoolVar calls alongside the existing
flags (matching the style used for flagStatic, flagVPS, flagNonInteract, etc.);
then make the SPA detection/assignment logic (the spaMode decision around the
detection at the spa detection location and the interactive prompt handling at
the interactive prompt location) respect these flags: if flagSPA is true force
spaMode=true, if flagNoSPA is true force spaMode=false, and have these explicit
flags take precedence over automatic detection and interactive prompts
(including in non-interactive/--yes flows). Ensure mutual exclusion/validation
so both flags cannot be set simultaneously and update any help text to document
--spa and --no-spa behavior.
internal/detect/detect.go (1)

155-155: ⚡ Quick win

Overly broad Hugo detection heuristic may cause false positives.

The baseURL substring check in a generic config.toml could match non-Hugo projects (e.g., a JSON config for a custom SSG). Per the package's design principle (line 13), false negatives are preferred over false positives.

Consider requiring a Hugo-specific config file (hugo.toml, hugo.yaml, hugo.json) OR tightening the config.toml check to verify additional Hugo-specific keys (e.g., theme, languageCode) to reduce false-positive risk.

🤖 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 `@internal/detect/detect.go` at line 155, The current Hugo detection in
detect.go uses has(...) or checks config.toml for the substring "baseURL", which
is too broad and causes false positives; update the conditional in the Hugo
detection (the has, readFile, and fileContains usage) to either only accept
explicit Hugo config files (hugo.toml, hugo.yaml, hugo.json) or, if config.toml
must be supported, require additional Hugo-specific keys (e.g., "theme",
"languageCode", or "baseURL =" with TOML syntax) by checking for multiple key
substrings in the fileContents returned by readFile before returning true;
ensure you modify the same conditional that currently reads has("hugo.toml",
"hugo.yaml", "hugo.json") || (has("config.toml") &&
fileContains(readFile("config.toml"), "baseURL")) so false positives are
avoided.
🤖 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/launch_test.go`:
- Around line 104-130: TestLaunchErrorAuthRequired_NonInteractive currently only
checks flag registration; either rename it to TestLaunchCommandFlagRegistration
or implement the promised auth_required behavior: set up cliCtx to a minimal
stub that simulates "no credentials" (override cliCtx), create the root command
via NewRootCmd("test"), set args ["launch","--non-interactive",...], execute the
command (cmd.Execute() or ExecuteC()) and assert the returned error is a
*launchError whose reason equals reasonAuthRequired; reference
TestLaunchErrorAuthRequired_NonInteractive, cliCtx, NewRootCmd,
cmd.Execute/ExecuteC, launchError and reasonAuthRequired when making the
changes.

In `@internal/commands/launch.go`:
- Around line 1399-1419: The function launchDeployFailureCleanup computes
liveURL and urlDisplay but never uses urlDisplay (and liveURL is only used to
set urlDisplay), causing a linter error; remove the unused urlDisplay variable
and its assignment (and if liveURL is not used elsewhere in
launchDeployFailureCleanup remove its assignment too) or alternatively
incorporate liveURL/urlDisplay into the user-facing messages (e.g., replace
server.Identifier or server.Name with urlDisplay) — update the code in
launchDeployFailureCleanup to either delete the urlDisplay and liveURL lines or
to use urlDisplay in the env.Warn / env.Status messages so no unused variable
remains.

In `@internal/detect/detect.go`:
- Around line 119-130: Remove the ineffectual initial assignment to
FrameworkDjango and set the framework only once based on the actual condition:
call has("manage.py") and fileContains(readFile("requirements.txt"), "django")
to pick FrameworkDjango otherwise use FrameworkUnknown, then return
Result{Framework: framework, SuggestedProtocol: ProtocolManagedVPS}; update the
block around the has("requirements.txt", "Pipfile", "pyproject.toml") check to
avoid assigning framework twice (refer to symbols: has, fileContains, readFile,
FrameworkDjango, FrameworkUnknown, Result, ProtocolManagedVPS).

---

Nitpick comments:
In `@docs/SKILL.md`:
- Around line 110-145: Add bash language identifiers to the fenced code blocks
that contain the dhq CLI examples so they get syntax highlighting: locate the
blocks starting with the commands "dhq api GET /account/capabilities --json",
"dhq api POST /beta/enrollments --body '{\"protocol\":\"static_hosting\"}'", the
two-line block with "dhq api GET /managed_hosting/regions --json" and "dhq api
GET /managed_hosting/sizes --json", the static hosting create block starting
"dhq servers create -p <project> --name \"My Site\" --protocol-type
static_hosting ...", the managed VPS create block starting "dhq servers create
-p <project> --name \"My VPS\" --protocol-type managed_vps ...", and the polling
block "dhq servers show <server-id> -p <project> --json"; for each, change the
opening triple backticks from ``` to ```bash (or ```shell) so the blocks render
with shell highlighting.

In `@internal/commands/launch_test.go`:
- Line 27: The Logger in the test setup is created as an empty output.Logger{}
which may lack a configured writer and cause panics on write; update the test
initialization that sets Logger to a configured instance by assigning its writer
(e.g., set the Logger.Writer field to io.Discard or a test buffer) so log writes
in the launch_test.go test use a safe destination; locate the logger setup where
Logger: &output.Logger{} is created and replace it with an instance whose writer
is explicitly set.
- Around line 711-738: The test currently only checks flag registration; update
TestServersCreate_ManagedVPS_RequiresAcceptCost_NonInteractive to actually
execute the servers create command in non-interactive mode and assert the
accept-cost guard fires: set cliCtx to a non-interactive context (use the shared
cliCtx variable), build the root command via NewRootCmd("test"), set args to run
"servers create" with --protocol=managed_vps but without --accept-cost, execute
the command (via ExecuteC/Execute) and assert it returns a UserError and that
apiCalled remains false (no API requests were made). Ensure you reset cliCtx
after the test and keep the existing apiCalled/http test server setup and
assertions.

In `@internal/commands/launch.go`:
- Around line 182-194: Add explicit SPA override flags to the command by
declaring new boolean flag variables (e.g., flagSPA and flagNoSPA) via
cmd.Flags().BoolVar calls alongside the existing flags (matching the style used
for flagStatic, flagVPS, flagNonInteract, etc.); then make the SPA
detection/assignment logic (the spaMode decision around the detection at the spa
detection location and the interactive prompt handling at the interactive prompt
location) respect these flags: if flagSPA is true force spaMode=true, if
flagNoSPA is true force spaMode=false, and have these explicit flags take
precedence over automatic detection and interactive prompts (including in
non-interactive/--yes flows). Ensure mutual exclusion/validation so both flags
cannot be set simultaneously and update any help text to document --spa and
--no-spa behavior.

In `@internal/detect/detect.go`:
- Line 155: The current Hugo detection in detect.go uses has(...) or checks
config.toml for the substring "baseURL", which is too broad and causes false
positives; update the conditional in the Hugo detection (the has, readFile, and
fileContains usage) to either only accept explicit Hugo config files (hugo.toml,
hugo.yaml, hugo.json) or, if config.toml must be supported, require additional
Hugo-specific keys (e.g., "theme", "languageCode", or "baseURL =" with TOML
syntax) by checking for multiple key substrings in the fileContents returned by
readFile before returning true; ensure you modify the same conditional that
currently reads has("hugo.toml", "hugo.yaml", "hugo.json") ||
(has("config.toml") && fileContains(readFile("config.toml"), "baseURL")) so
false positives are avoided.
🪄 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: 8d315b8c-7917-4638-a5bd-657d4183c889

📥 Commits

Reviewing files that changed from the base of the PR and between a6480be and 4d47bef.

📒 Files selected for processing (24)
  • README.md
  • docs/SKILL.md
  • internal/commands/agent_metadata.go
  • internal/commands/agent_metadata_test.go
  • internal/commands/deploy.go
  • internal/commands/init.go
  • internal/commands/launch.go
  • internal/commands/launch_test.go
  • internal/commands/root.go
  • internal/commands/servers.go
  • internal/commands/signup.go
  • internal/config/config.go
  • internal/detect/detect.go
  • internal/detect/detect_test.go
  • pkg/sdk/managed_hosting.go
  • pkg/sdk/managed_hosting_test.go
  • pkg/sdk/server_protocols_test.go
  • pkg/sdk/servers.go
  • pkg/sdk/signup.go
  • pkg/sdk/signup_test.go
  • pkg/sdk/types.go
  • skills/deployhq/SKILL.md
  • skills/deployhq/references/launch.md
  • skills/deployhq/references/servers.md

Comment thread internal/commands/launch_test.go Outdated
Comment thread internal/commands/launch.go
Comment thread internal/detect/detect.go
@thdurante thdurante added the WIP Work in Progress label Jun 9, 2026
thdurante and others added 2 commits June 10, 2026 12:35
CI runs golangci-lint (stricter than go vet). Remove the dead liveURL/urlDisplay
in launchDeployFailureCleanup (the failure message uses server.Name/Identifier),
simplify the Django detection branch (drop the always-overwritten initial
assignment), and convert the dry-run target if/else to a tagged switch (QF1003).
No behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit: TestLaunchErrorAuthRequired_NonInteractive only asserted flag
registration (already covered by TestLaunchCommandFlagSet) despite its name.
Remove it and add TestLaunchAuthRequired_JSONReason, which verifies the
auth_required structured-error contract. The full no-creds flow can't be
unit-tested deterministically because Credentials() falls back to the OS keyring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

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

1161-1172: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Region selection index mismatch causes wrong region to be selected.

regionItems contains only available regions, so rIdx is an index into that filtered subset. However, the lookup loop compares rIdx against i (the index into the full regions slice). When unavailable regions precede available ones, the indices diverge and the wrong region is selected—or no region matches at all.

🐛 Proposed fix
 if rIdx, _, rErr := regionPrompt.Run(); rErr == nil {
-  // find slug from items
-  for i, r := range regions {
-    if r.Available {
-      if i == rIdx {
-        region = r.Slug
-        selectedRegion = r
-        break
-      }
-    }
-  }
+  // Map rIdx (index into filtered available-only list) to the actual region.
+  availIdx := 0
+  for _, r := range regions {
+    if r.Available {
+      if availIdx == rIdx {
+        region = r.Slug
+        selectedRegion = r
+        break
+      }
+      availIdx++
+    }
+  }
 }
🤖 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 `@internal/commands/launch.go` around lines 1161 - 1172, The region selection
uses rIdx from regionPrompt.Run() which indexes into the filtered list of
available regions (regionItems) but the code compares rIdx against i over the
full regions slice, causing mismatches; fix by mapping rIdx to the available
region directly—either index into the filtered slice (regionItems or a new
availableRegions slice) to set region and selectedRegion, or change the loop
over regions to maintain a separate available counter that increments only for
r.Available and matches rIdx before assigning region/selectedRegion.
🤖 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.

Outside diff comments:
In `@internal/commands/launch.go`:
- Around line 1161-1172: The region selection uses rIdx from regionPrompt.Run()
which indexes into the filtered list of available regions (regionItems) but the
code compares rIdx against i over the full regions slice, causing mismatches;
fix by mapping rIdx to the available region directly—either index into the
filtered slice (regionItems or a new availableRegions slice) to set region and
selectedRegion, or change the loop over regions to maintain a separate available
counter that increments only for r.Available and matches rIdx before assigning
region/selectedRegion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2cb3d0d-8ab8-4299-8a07-6064a17ec2d1

📥 Commits

Reviewing files that changed from the base of the PR and between 4d47bef and 8bb9ffc.

📒 Files selected for processing (3)
  • internal/commands/launch.go
  • internal/commands/launch_test.go
  • internal/detect/detect.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/detect/detect.go
  • internal/commands/launch_test.go

thdurante and others added 2 commits June 10, 2026 16:04
Introduce internal/commands/metered.go with meteredResourcesInBeta and a
small set of helpers (managedVPSAcknowledgePhrase, managedVPSCostDescription,
managedRunningCostTail, betaFreeSuffix). While the metered managed resources
(Managed VPS, Static Hosting) are in beta, the CLI presents them as free for
early customers and frames the listed monthly rate as the post-beta price.

Route every piece of runtime cost/acknowledgement copy in `dhq launch` and
`dhq servers create` through these helpers, plus the target-selection menu and
the agent metadata comment, so flipping one variable updates all of it. Update
the affected tests to assert on the stable `--accept-cost` substring rather
than the word "billable".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update README, the deployhq skill guide, and the launch reference so the
Managed VPS / Static Hosting copy says "free for early customers during beta,
billed monthly afterwards" instead of "billable". Add a Pricing note in
launch.md pointing at the meteredResourcesInBeta switch so the docs are
updated in the same change when the resources go GA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thdurante

Copy link
Copy Markdown
Author

@claude review

@thdurante

Copy link
Copy Markdown
Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
skills/deployhq/references/launch.md (1)

71-76: ⚡ Quick win

Consider making the maintenance instruction more specific.

Line 75 correctly identifies the meteredResourcesInBeta switch and notes that "this beta wording" needs updating. However, the instruction doesn't specify exactly which lines contain beta pricing copy (lines 39, 51, and 75 itself). When a maintainer flips the switch, they'll need to search for mentions.

Consider adding explicit line references or a more specific instruction, for example: "update the beta wording on lines 39, 51, and this note itself."

🤖 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 `@skills/deployhq/references/launch.md` around lines 71 - 76, The note about
flipping the meteredResourcesInBeta flag is vague; when you toggle the boolean
named meteredResourcesInBeta in internal/commands/metered.go, also update the
beta-pricing copy in this document—specifically replace the beta wording at the
three occurrences: the two earlier pricing lines (the copies currently at lines
referenced in the review: the pricing mentions near the start of the doc and the
mid-doc paragraph) and the maintenance note itself—so search the file for the
beta pricing sentences and update those three locations together with the switch
flip to remove/replace "beta" wording and adjust the stated monthly rate.
🤖 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.

Nitpick comments:
In `@skills/deployhq/references/launch.md`:
- Around line 71-76: The note about flipping the meteredResourcesInBeta flag is
vague; when you toggle the boolean named meteredResourcesInBeta in
internal/commands/metered.go, also update the beta-pricing copy in this
document—specifically replace the beta wording at the three occurrences: the two
earlier pricing lines (the copies currently at lines referenced in the review:
the pricing mentions near the start of the doc and the mid-doc paragraph) and
the maintenance note itself—so search the file for the beta pricing sentences
and update those three locations together with the switch flip to remove/replace
"beta" wording and adjust the stated monthly rate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07f33b7f-b77b-49bd-8bcd-21eb7c9c6a79

📥 Commits

Reviewing files that changed from the base of the PR and between 8bb9ffc and 4d08017.

📒 Files selected for processing (9)
  • README.md
  • internal/commands/agent_metadata.go
  • internal/commands/agent_metadata_test.go
  • internal/commands/launch.go
  • internal/commands/launch_test.go
  • internal/commands/metered.go
  • internal/commands/servers.go
  • skills/deployhq/SKILL.md
  • skills/deployhq/references/launch.md
✅ Files skipped from review due to trivial changes (3)
  • internal/commands/metered.go
  • README.md
  • skills/deployhq/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/commands/agent_metadata.go
  • internal/commands/agent_metadata_test.go
  • internal/commands/servers.go
  • internal/commands/launch_test.go
  • internal/commands/launch.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d08017925

ℹ️ 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".

Comment thread internal/commands/launch.go Outdated
thdurante and others added 3 commits June 10, 2026 16:35
Add APIError.IsRateLimited() (HTTP 429) and an IsRateLimited(err) helper,
plus an APIError.RetryAfter field parsed from the Retry-After response header
(integer-seconds form) in parseAPIError. This lets callers distinguish the
metered-resource provisioning rate limit (429, retryable) from the cap /
kill-switch (422, hard wall) — see deployhq/deployhq#933.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Map a 429 from POST /servers (provisioning rate limit) to a new
rate_limited launch reason instead of letting it fall through to
provision_failed. The structured error is marked retryable and carries the
Retry-After backoff in details.retry_after, so an agent/CI backs off and
re-runs rather than failing hard.

Also make writeLaunchError treat the wrapped launchError's own Reason as
authoritative (a rate_limited / subdomain_taken error surfaced through a
generic call site keeps its true reason), and add a retryable boolean to the
--json error payload. Document the reason in the launch reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Managed VPS size selection now shows readable, tier-named labels instead of
the raw droplet slug: a price-ranked tier (Starter/Standard/Plus/Pro) plus
specs and price, e.g. "Starter · 1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
(s-1vcpu-1gb)" — the slug stays visible so the equivalent --size flag is
discoverable. The configuration summary's Size line shows specs + slug too,
and falls back to the API Description / bare slug when structured fields are
absent. Tiers rank by price (not list position), so ordering is stable.

Also close the post-launch rollback guidance gap: a successful launch now
prints how to roll back, and the launch reference clarifies that both targets
roll back identically via `dhq rollback <deployment>` (redeploys the previous
revision) — there's no separate static-vs-VPS command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thdurante thdurante self-assigned this Jun 11, 2026
thdurante and others added 3 commits June 11, 2026 08:23
dhq launch was POSTing the detected static build command to
/projects/:id/build_configs with a {build_config:{build_commands}} body — a
path/shape that doesn't exist (the SDK's build-command path is
/projects/:id/build_commands with {build_command:{command}}). Because the
error was only warned, the build command silently never got set, so the first
Static Hosting deploy could publish unbuilt sources instead of the generated
dist/public output. (P1 from review.)

Rewrite launchApplyBuildConfig → launchApplyBuildCommand to use the SDK's
CreateBuildCommand, mirroring the web onboarding wizard
(Onboarding::ProjectCreator), which creates project build commands and needs no
separate build environment. Skip when the project already has build commands
(idempotent re-runs / reused --project don't duplicate), keep failure
non-fatal but now surface a discoverable manual-fix hint, and cap the
description at 100 runes like the wizard.

Tests: full branch coverage for launchApplyBuildCommand (correct endpoint+body,
skip-when-exists, empty no-op, list-error-still-creates, create-error-warns,
description truncation) plus a static provision→build-command integration test
proving the command lands on /build_commands during a real static launch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a spec-validating test client (newSpecValidatingClient) whose transport
checks every outgoing request against a committed snapshot of the backend's
generated OpenAPI document (/docs.json → internal/commands/testdata/
openapi.json, refreshed via script/update-openapi-fixture.sh). A request to an
undocumented path, or with a body violating the documented schema, fails the
SDK call with an "openapi spec violation" error — so tests prove the CLI
speaks the backend's actual contract, not just what the hand-written fakes
accept. Harness self-tests recreate the build-command P1 (POST /build_configs
→ rejected as undocumented) and a missing required `server` key.

Swap the launch build-command and static provision→build-command integration
tests onto the validating client, and add real provisioning-poll coverage:
provisioning→active terminates (and stops on the first active), and an
"error" status stops polling with a structured provision_failed reason.
pollProvisioningState's initial backoff becomes a package var as a test seam
so the sequence runs without real waits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The committed OpenAPI fixture pins the contract this CLI was built against
(lockfile model) — additive backend changes keep passing, but a breaking
backend change would only surface when the fixture is refreshed. This
scheduled job closes that gap: it fetches the live spec (repository variable
DHQ_SPEC_URL, no-op when unset), substitutes it for the fixture, and re-runs
the contract tests. A failure means the backend drifted in a way that breaks
the requests this CLI sends — refresh the fixture and adapt the SDK in one PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

WIP Work in Progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant