Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e7f8779
feat(sdk): managed_vps + static_hosting server types and create shape
thdurante Jun 9, 2026
4b810a5
feat(sdk): beta-enrollment, capability and managed-hosting methods
thdurante Jun 9, 2026
70ef1f1
feat(cli): managed_vps + static_hosting on servers create; signup att…
thdurante Jun 9, 2026
7b7ba5d
feat(detect): local framework detection for launch recommendations
thdurante Jun 9, 2026
2699d24
docs(cli): document managed offerings (README, SKILL, servers reference)
thdurante Jun 9, 2026
76b2bb7
feat(cli): dhq launch — one-command provision + deploy
thdurante Jun 9, 2026
5bbce1b
test(cli): dhq launch — config resolution, pre-flights, structured er…
thdurante Jun 9, 2026
a4eae76
feat(cli): agent metadata for dhq launch
thdurante Jun 9, 2026
83c7d1c
docs(cli): document dhq launch (README + agent skill guide)
thdurante Jun 9, 2026
fccc4b6
feat(config): add Server + Target fields for launch idempotency
thdurante Jun 9, 2026
b10e1ec
fix(launch): address Review Council findings — idempotency, dry-run, …
thdurante Jun 9, 2026
1b64a71
fix(servers): add --accept-cost guardrail to dhq servers create manag…
thdurante Jun 9, 2026
4d47bef
test(launch): add Review Council coverage — idempotency, dry-run, cos…
thdurante Jun 9, 2026
35b0b6a
fix(launch): clear golangci-lint findings (ineffassign + staticcheck)
thdurante Jun 10, 2026
8bb9ffc
test(launch): replace misnamed flag test with a real auth_required check
thdurante Jun 10, 2026
1ee1440
feat(launch): gate managed-resource copy behind a single beta switch
thdurante Jun 10, 2026
4d08017
docs(launch): reflect free-during-beta pricing for managed resources
thdurante Jun 10, 2026
868f6f6
feat(sdk): recognise 429 provisioning rate limit with Retry-After
thdurante Jun 10, 2026
3885947
feat(launch): surface 429 as a retryable rate_limited reason
thdurante Jun 10, 2026
8cd5056
feat(launch): friendlier VPS size labels + post-launch rollback guidance
thdurante Jun 10, 2026
21384ef
fix(launch): create the detected build command via the correct endpoint
thdurante Jun 11, 2026
3d48300
test(launch): validate SDK requests against the backend OpenAPI spec
thdurante Jun 11, 2026
7d0bf5f
ci: scheduled OpenAPI drift check against the live backend spec
thdurante Jun 11, 2026
48d5e52
feat(detect): SDK client + manifest collector for the backend detecti…
thdurante Jun 11, 2026
5622844
feat(launch): detect framework via the backend, fall back to local
thdurante Jun 11, 2026
bc131b6
docs(detect): reframe local detection as the offline fallback
thdurante Jun 11, 2026
b6f6dd7
fix(launch): correct branch revision and region selection (review)
thdurante Jun 11, 2026
4ed00ab
chore: strip internal labels and stale comments for external review
thdurante Jun 11, 2026
9f622da
Shrink local launch detection to a coarse target heuristic
thdurante Jun 16, 2026
2995ee5
launch: surface the project deploy key after connecting a repository
thdurante Jun 16, 2026
737e0a0
sdk: add optional ai_assisted field to DetectionResponse
thdurante Jun 16, 2026
fc1c240
launch: subdirectory prompt, detected-SPA preselect, apply detected e…
thdurante Jun 17, 2026
8f2d653
watch: de-duplicate repeated lines when rendering failure logs
thdurante Jun 17, 2026
44ea568
launch: local-AI diagnosis of a failed deploy (reuses dhq assist / Ol…
thdurante Jun 17, 2026
f346c7c
launch: auto-install the deploy key via the gh CLI for GitHub repos
thdurante Jun 17, 2026
a6818ed
launch: detect a stale persisted project and recover, not a misleadin…
thdurante Jun 17, 2026
7bb5a1d
launch: apply detected build commands as separate, catalog-faithful s…
thdurante Jun 17, 2026
c5e6e67
launch: handle unverified email gracefully (wait/retry or clean fail)
thdurante Jun 18, 2026
3cbae61
Docs: add TESTING-LOCAL.md — how to review/test dhq launch locally
thdurante Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/openapi-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: OpenAPI drift check

# Guards the committed OpenAPI fixture (internal/commands/testdata/openapi.json)
# against silent contract drift: fetches the backend's LIVE spec on a schedule
# and re-runs the spec-validating contract tests against it. A failure means the
# backend changed in a way that breaks the requests this CLI sends — refresh the
# fixture (script/update-openapi-fixture.sh) and adapt the SDK in one PR.
#
# Requires the repository variable DHQ_SPEC_URL (e.g. the staging /docs.json).
# When unset, the job no-ops so forks/CI stay green.

on:
schedule:
- cron: "0 6 * * 1-5" # weekday mornings UTC
workflow_dispatch:

permissions:
contents: read

jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Fetch live OpenAPI spec
id: fetch
env:
SPEC_URL: ${{ vars.DHQ_SPEC_URL }}
run: |
if [ -z "$SPEC_URL" ]; then
echo "DHQ_SPEC_URL repository variable not set — skipping drift check."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
curl -sf --max-time 180 "$SPEC_URL" -o /tmp/live-openapi.json
head -c 100 /tmp/live-openapi.json | grep -q '"openapi"' || {
echo "::error::Response from $SPEC_URL does not look like an OpenAPI document"; exit 1; }

- uses: actions/setup-go@v5
if: steps.fetch.outputs.skip != 'true'
with:
go-version: "1.25"

- name: Run contract tests against the live spec
if: steps.fetch.outputs.skip != 'true'
run: |
# Informational: how far has the spec moved since the pinned fixture?
if ! cmp -s /tmp/live-openapi.json internal/commands/testdata/openapi.json; then
echo "::notice::Live spec differs from the committed fixture (additive drift is fine; this job fails only on breaking drift)."
fi
cp /tmp/live-openapi.json internal/commands/testdata/openapi.json
go test ./internal/commands/

- name: Explain failure
if: failure()
run: |
echo "::error::The CLI's requests no longer conform to the backend's live OpenAPI spec." \
"Refresh the fixture with script/update-openapi-fixture.sh, fix the SDK, and ship both in one PR."
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ dhq update
## Quick Start

```bash
# One command: detect the framework, provision DeployHQ hosting
# (Static Hosting or a Managed VPS) and deploy — to a live URL.
dhq launch

# Guided setup (login or signup, pick a project, optional first deploy)
dhq hello

Expand All @@ -62,6 +66,30 @@ dhq deployments logs <id> -p my-app
dhq open my-app
```

## One-command deploy (`dhq launch`)

`dhq launch` takes a project folder to a live URL on DeployHQ's own
infrastructure — **Static Hosting** (global CDN, Cloudflare-backed) or a
**Managed VPS** (DeployHQ-provisioned) — in a single command. It detects your
framework, provisions the target, deploys, and prints the URL.

```bash
dhq launch # interactive: detect, pick a target, deploy
dhq launch --static --subdomain my-app
dhq launch --vps --accept-cost --region lon1 --size s-1vcpu-1gb

# Agents / CI — structured JSON, never prompts:
dhq launch --static --json
dhq launch --vps --dry-run --json # preview cost + actions, no side effects
```

A Managed VPS is a managed resource — free for early customers during the beta,
billed monthly afterwards — so `--accept-cost` is required for non-interactive VPS
provisioning (`--yes` alone never provisions one). After the first run, `launch`
writes `.deployhq.toml` so subsequent deploys are just `dhq deploy`. See the
[agent guide](skills/deployhq/references/launch.md) for the full flag set and the
structured-error reasons agents can branch on.

## Authentication

```bash
Expand Down Expand Up @@ -104,6 +132,9 @@ See `examples/github-actions/` for complete workflows:
```
dhq projects list | show | create | update | delete | star | insights | upload-key | badge
dhq servers list | show | create | update | delete | reset-host-key
protocols: ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean,
hetzner_cloud, heroku, netlify, shopify,
static_hosting (beta), managed_vps (beta)
dhq server-groups list | show | create | update | delete
dhq deployments list | show | create | abort | rollback | logs | watch
dhq repos show | create | update | branches | commits | commit-info | latest-revision
Expand Down
185 changes: 185 additions & 0 deletions TESTING-LOCAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Testing `dhq launch` locally (one-command deploy)

How to review/test the one-command deploy work end-to-end against your **local**
DeployHQ. ~10 minutes. Two branches, released together:

| Repo | Branch | PR |
|---|---|---|
| `deployhq` (backend) | `feat/cli-managed-resources-api` | #926 |
| `deployhq-cli` | `feat/one-command-deploy` | #25 |

---

## Prerequisites

- A working **local DeployHQ dev environment** that serves `https://deploy.localhost`
(the standard `./bin/dev` setup).
- One of: **Go 1.25**, or **Docker** (to build the CLI without installing Go).
- **`gh` CLI authenticated** (`gh auth status`) — exercises the GitHub deploy-key
auto-install path during launch.
- **A git repo you own** to launch against (the deploy-key step needs a repo you
control). See [Test repos](#test-repos) to scaffold one.

---

## 1. Check out both branches

Backend:
```bash
cd path/to/deployhq
git fetch origin && git checkout feat/cli-managed-resources-api
bin/rails db:migrate # pick up any migrations on the branch
```

CLI:
```bash
cd path/to/deployhq-cli
git fetch origin && git checkout feat/one-command-deploy
```

## 2. Start the local backend

```bash
cd path/to/deployhq
./bin/dev
# sanity check (should be a 308 redirect to https://deploy.localhost/):
curl -sI http://deploy.localhost | head -1
```

## 3. Build the CLI dev binary

**With Go 1.25:**
```bash
cd path/to/deployhq-cli
go build -o dhq ./cmd/dhq
```

**Without Go (Docker):** set `GOOS`/`GOARCH` for your machine — Apple Silicon
`darwin/arm64`, Intel mac `darwin/amd64`, Linux `linux/amd64`.
```bash
cd path/to/deployhq-cli
docker run --rm -v "$PWD":/src -v dhqcli-gocache:/go -w /src \
-e CGO_ENABLED=0 -e GOOS=darwin -e GOARCH=arm64 \
golang:1.25 go build -o dhq ./cmd/dhq
```

**Verify the build:**
```bash
./dhq version # MUST print: dhq version dev
```
If it prints `0.x.y`, you're running the released binary, not your build — fix the
alias in the next step.

## 4. Point the CLI at local + alias it

```bash
export DEPLOYHQ_HOST=deploy.localhost
alias dhq="$(pwd)/dhq" # run from deployhq-cli, or use the absolute path
```

Verify both:
```bash
printenv | grep DEPLOYHQ # DEPLOYHQ_HOST=deploy.localhost
type dhq # dhq is aliased to .../deployhq-cli/dhq
dhq version # dhq version dev
```

Why each matters:
- **`DEPLOYHQ_HOST=deploy.localhost`** → the CLI builds its API base URL as
`https://<account>.deploy.localhost`, i.e. your local backend instead of
production. Without it, the CLI talks to the hosted product.
- **the alias** → `dhq` runs the dev binary you just built. Any globally-installed
`dhq` is an old release that doesn't even have a `launch` command.

> Both are **per-shell** — a new terminal needs the `export` + `alias` again (or
> add them to your shell rc).

## 5. Run a launch

From inside a repo you own, with no `.deployhq.toml` yet:

```bash
cd /path/to/some-app
dhq launch
```

**Safe first pass — `--dry-run` (no side effects):**
```bash
dhq launch --dry-run
```
Prints the detected stack, the chosen target (Static Hosting vs Managed VPS), and
— for VPS — the cost, **without creating anything**. Best way to eyeball
detection/steering before touching real infra.

> A real `dhq launch` on a VPS-detected repo **provisions a real DigitalOcean
> droplet** (it costs money and you must delete it afterward). Use `--dry-run`
> first, and `--cleanup-on-failure` on real runs.

---

## What to test

| Scenario | Repo to use | Expected |
|---|---|---|
| **Static steering** | Next.js `output: 'export'`, or a Vite SPA | detected → **Static Hosting** |
| **VPS steering** | Laravel / Rails / generic PHP | detected → **Managed VPS** (`--dry-run` shows VPS + cost) |
| **Signup-in-launch** | logged out / brand-new account | launch walks you through signup, creates the account |
| **Email verification** | account with unverified email | **interactive:** "your email needs to be verified…", waits for you to verify then retries. **non-interactive:** fails cleanly telling you to verify |
| **GitHub deploy key** | a GitHub repo, `gh` authenticated | deploy key auto-installed via `gh` (no manual copy/paste) |
| **Stale config** | repo whose `.deployhq.toml` points at a deleted project/server | CLI detects stale, warns/clears, re-creates |
| **Non-interactive** | `dhq launch --yes` (or piped) | never prompts; VPS requires `--accept-cost`; ambiguity → structured error, never a browser redirect |

### Non-interactive mode

Auto-detected when stdout/stdin aren't a TTY (piped/CI), or forced with
`--non-interactive`:
```bash
dhq launch --non-interactive # alias: --yes
dhq launch --static --yes # force Static, no prompts
dhq launch --vps --accept-cost --yes # provision a VPS, no prompts (cost ack required)
```
In non-interactive mode the CLI never redirects you to the browser — it fails with
an actionable error instead.

### Useful flags

- `--dry-run` — show intent + cost, no changes
- `--static` / `--vps` — force the target (override detection's choice)
- `--accept-cost` — acknowledge VPS cost (required for non-interactive VPS)
- `--region lon1 --size s-1vcpu-1gb` — VPS placement
- `--subdomain <name>` — Static Hosting subdomain
- `--project <permalink>` — reuse an existing project (skip creation)
- `--branch <name>` — deploy a non-default branch
- `--cleanup-on-failure` — delete the provisioned droplet if the deploy fails

---

## Resetting between runs

- Remove the launch-written config: `rm .deployhq.toml`
- Switch / clear account: edit the `account = …` line in `~/.deployhq/config.toml`,
or pass `--account <name>`.
- Provisioned a VPS you don't want? **Delete the droplet** — it's real infra.

## Test repos

Detection only needs the manifest, but the deploy-key step needs a repo you own.
Quick scaffolds:

- **Static (Vite SPA):** `npm create vite@latest my-spa -- --template react-ts`
- **VPS (Laravel, no local PHP needed):**
`docker run --rm -v "$PWD":/app -w /app composer:latest create-project laravel/laravel my-laravel`

Then `git init`, push to a private repo you own
(`gh repo create <you>/my-app --private --source=. --push`), and `dhq launch`
from the checkout.

---

## Known limitation (WIP — not a bug to file)

Managed VPS currently provisions a **bare Ubuntu droplet** and the deploy is a
plain file copy — no web server / PHP runtime is installed yet, so the droplet's
IP will refuse connections after a VPS launch. That runtime-setup step is still in
progress. For reviewing the CLI flow, prefer **`--dry-run`** for the VPS path and
real launches for the **Static Hosting** path.
38 changes: 38 additions & 0 deletions docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,44 @@ dhq api POST /projects/<id>/config_files --body '{"config_file":{...}}'
5. `dhq deployment-checks list -p <project> --json` → pre_build / post_deploy gates
6. `dhq servers list -p <project> --json` → servers

### "Check account beta/managed-offerings eligibility"
```
dhq api GET /account/capabilities --json
```
Returns `beta_features`, `static_hosting_eligible`, `managed_vps_eligible`.

### "Enable managed-resources beta from CLI"
```
dhq api POST /beta/enrollments --body '{"protocol":"static_hosting"}'
```
Admin required for first enrollment; already-enrolled accounts are idempotent.

### "List Managed VPS regions and sizes"
```
dhq api GET /managed_hosting/regions --json
dhq api GET /managed_hosting/sizes --json
```
Requires beta_features enabled.

### "Create a Static Hosting server"
```
dhq servers create -p <project> --name "My Site" --protocol-type static_hosting \
--subdomain <name> --subdirectory dist --json
```

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

### "Poll provisioning status"
```
dhq servers show <server-id> -p <project> --json
```
Check `provisioning_status` ("provisioning" / "active" / "error"), `ip_address` (VPS),
and `static_hosting.url` (static) in the response.

## Invariants
- Always use `--json` for machine-readable output
- JSON responses include `breadcrumbs` with suggested next commands
Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/fatih/color v1.19.0
github.com/getkin/kin-openapi v0.140.0
github.com/manifoldco/promptui v0.9.0
github.com/mixpanel/mixpanel-go v1.2.1
github.com/spf13/cobra v1.10.2
Expand All @@ -27,8 +28,11 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
Expand All @@ -38,10 +42,13 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/oasdiff/yaml v0.1.0 // indirect
github.com/oasdiff/yaml3 v0.0.13 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
Expand Down
Loading
Loading