diff --git a/.github/workflows/openapi-drift.yml b/.github/workflows/openapi-drift.yml new file mode 100644 index 0000000..4ede035 --- /dev/null +++ b/.github/workflows/openapi-drift.yml @@ -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." diff --git a/README.md b/README.md index 1dbadd4..aa7922d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -62,6 +66,30 @@ dhq deployments logs -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 @@ -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 diff --git a/TESTING-LOCAL.md b/TESTING-LOCAL.md new file mode 100644 index 0000000..3a428be --- /dev/null +++ b/TESTING-LOCAL.md @@ -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://.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 ` — Static Hosting subdomain +- `--project ` — reuse an existing project (skip creation) +- `--branch ` — 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 `. +- 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 /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. diff --git a/docs/SKILL.md b/docs/SKILL.md index 1c922e2..dc7508f 100644 --- a/docs/SKILL.md +++ b/docs/SKILL.md @@ -106,6 +106,44 @@ dhq api POST /projects//config_files --body '{"config_file":{...}}' 5. `dhq deployment-checks list -p --json` → pre_build / post_deploy gates 6. `dhq servers list -p --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 --name "My Site" --protocol-type static_hosting \ + --subdomain --subdirectory dist --json +``` + +### "Create a Managed VPS server" +``` +dhq servers create -p --name "My VPS" --protocol-type managed_vps \ + --region lon1 --size s-1vcpu-1gb --json +``` + +### "Poll provisioning status" +``` +dhq servers show -p --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 diff --git a/go.mod b/go.mod index fa8f986..3cedcbf 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index cd8945b..20f2851 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMF github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -31,12 +33,22 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g= +github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= @@ -65,6 +77,10 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg= +github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0= +github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg= +github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -77,6 +93,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -114,7 +132,7 @@ golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/commands/agent_metadata.go b/internal/commands/agent_metadata.go index 3f952c6..0002152 100644 --- a/internal/commands/agent_metadata.go +++ b/internal/commands/agent_metadata.go @@ -32,6 +32,18 @@ var commandMetadataTable = map[string]AgentMetadata{ SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"deployment"}, }, + "dhq launch": { + // Provisions a project/server (Managed VPS or Static Hosting) and deploys. + // Re-runs resolve the existing project/server from .deployhq.toml rather than + // double-provisioning (idempotent). A Managed VPS is a managed resource (see + // managedVPSAcknowledgePhrase in metered.go for the current cost framing), so + // agents should confirm — and the command itself requires --accept-cost in + // non-interactive mode before provisioning a Managed VPS. + Interactive: true, Destructive: false, Idempotent: true, + RequiresConfirmation: true, + SupportsJSON: true, SafeForAutomation: true, + ResourceTypes: []string{"project", "server", "deployment"}, + }, // Projects "dhq projects list": { diff --git a/internal/commands/agent_metadata_test.go b/internal/commands/agent_metadata_test.go index 36d850e..8c216ec 100644 --- a/internal/commands/agent_metadata_test.go +++ b/internal/commands/agent_metadata_test.go @@ -27,6 +27,18 @@ func TestAgentMetadata_Rollback(t *testing.T) { assert.True(t, m.SafeForAutomation) } +func TestAgentMetadata_Launch(t *testing.T) { + m := lookupAgentMetadata("dhq launch") + assert.True(t, m.Interactive, "launch can prompt in a TTY") + assert.False(t, m.Destructive, "launch provisions, it does not delete") + assert.True(t, m.Idempotent, "re-runs resolve the existing project/server") + assert.True(t, m.RequiresConfirmation, "provisions managed resources") + assert.True(t, m.SupportsJSON) + assert.True(t, m.SafeForAutomation, "deterministic non-interactively with the right flags") + assert.Contains(t, m.ResourceTypes, "server") + assert.Contains(t, m.ResourceTypes, "deployment") +} + func TestAgentMetadata_ProjectsDelete(t *testing.T) { m := lookupAgentMetadata("dhq projects delete") assert.True(t, m.Destructive) diff --git a/internal/commands/deploy.go b/internal/commands/deploy.go index 6fbcbd7..1a3b303 100644 --- a/internal/commands/deploy.go +++ b/internal/commands/deploy.go @@ -189,7 +189,7 @@ func resolveDeployProject(ctx context.Context, client *sdk.Client, configured st case 0: return "", &output.UserError{ Message: "No project specified", - Hint: "This account has no projects yet. Create one in the DeployHQ dashboard, then re-run with --project .", + Hint: "This account has no projects yet.\nTo provision and deploy in one step: dhq launch\nOr create a project in the DeployHQ dashboard, then re-run with --project .", } case 1: env.Status("Auto-selected project: %s", projects[0].Name) diff --git a/internal/commands/github_deploy_key.go b/internal/commands/github_deploy_key.go new file mode 100644 index 0000000..3b75052 --- /dev/null +++ b/internal/commands/github_deploy_key.go @@ -0,0 +1,49 @@ +package commands + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// ghAvailable reports whether the GitHub CLI (`gh`) is installed on PATH. +func ghAvailable() bool { + _, err := exec.LookPath("gh") + return err == nil +} + +// installDeployKeyViaGH adds publicKey as a read-only deploy key to the GitHub +// repo identified by repoURL, using the local `gh` CLI (which carries its own +// authentication — so this works headlessly, with no browser or prompt). +// +// It returns an error when the URL isn't a GitHub repo, the key can't be +// written, or `gh` fails (e.g. not authenticated, no write access, or a key +// with the same title already exists). Callers treat a failure as non-fatal and +// fall back to surfacing the key for manual installation. +func installDeployKeyViaGH(repoURL, publicKey, title string) error { + repo := extractGitHubRepo(repoURL) + if repo == "" { + return fmt.Errorf("could not extract a GitHub repo from URL: %s", repoURL) + } + + tmpFile, err := os.CreateTemp("", "dhq-deploy-key-*.pub") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) //nolint:errcheck + + if _, err := tmpFile.WriteString(publicKey); err != nil { + tmpFile.Close() //nolint:errcheck + return err + } + tmpFile.Close() //nolint:errcheck + + cmd := exec.Command("gh", "repo", "deploy-key", "add", tmpFile.Name(), + "--repo", repo, + "--title", title) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%s", strings.TrimSpace(string(out))) + } + return nil +} diff --git a/internal/commands/init.go b/internal/commands/init.go index 518798d..e23960a 100644 --- a/internal/commands/init.go +++ b/internal/commands/init.go @@ -3,7 +3,6 @@ package commands import ( "context" "fmt" - "os" "os/exec" "strings" @@ -139,21 +138,30 @@ var protocolsAll = []string{ "Heroku", "Netlify", "Shopify", + // Managed offerings — require beta_features on the account. + // Prefer `dhq launch` for these; listed here for advanced users. + "Static Hosting (beta)", + "Managed VPS (beta)", } // protocolAPIType maps display names to API protocol_type values. var protocolAPIType = map[string]string{ - "SSH/SFTP": "ssh", - "FTP": "ftp", + "SSH/SFTP": "ssh", + "FTP": "ftp", "FTPS (SSL/TLS)": "ftps", - "Rsync": "rsync", - "Amazon S3": "s3", - "S3-Compatible Storage": "s3_compatible", - "DigitalOcean": "digitalocean", - "Hetzner Cloud": "hetzner_cloud", - "Heroku": "heroku", - "Netlify": "netlify", - "Shopify": "shopify", + "Rsync": "rsync", + "Amazon S3": "s3", + "S3-Compatible Storage": "s3_compatible", + "DigitalOcean": "digitalocean", + "Hetzner Cloud": "hetzner_cloud", + "Heroku": "heroku", + "Netlify": "netlify", + "Shopify": "shopify", + // Managed offerings (beta) — backed by DeployHQ-managed infrastructure. + // static_hosting: Cloudflare CDN site at .deployhq-sites.com. + // managed_vps: DeployHQ-provisioned DigitalOcean droplet. + "Static Hosting (beta)": "static_hosting", + "Managed VPS (beta)": "managed_vps", } // protocolSupportsSSHKeys returns true if the protocol supports SSH key authentication. @@ -854,35 +862,8 @@ func (m *initModel) createServer() tea.Msg { } func (m *initModel) addDeployKeyViaGH() tea.Msg { - // Extract repo owner/name from URL (e.g. git@github.com:owner/repo.git) - repo := extractGitHubRepo(m.repoURL) - if repo == "" { - return createResultMsg{err: fmt.Errorf("could not extract GitHub repo from URL: %s", m.repoURL), step: stepDeployKeyAuto} - } - - // Write public key to temp file - tmpFile, err := os.CreateTemp("", "dhq-deploy-key-*.pub") - if err != nil { - return createResultMsg{err: err, step: stepDeployKeyAuto} - } - defer os.Remove(tmpFile.Name()) //nolint:errcheck - - if _, err := tmpFile.WriteString(m.project.PublicKey); err != nil { - tmpFile.Close() //nolint:errcheck - return createResultMsg{err: err, step: stepDeployKeyAuto} - } - tmpFile.Close() //nolint:errcheck - - // Run gh repo deploy-key add - cmd := exec.Command("gh", "repo", "deploy-key", "add", tmpFile.Name(), - "--repo", repo, - "--title", fmt.Sprintf("DeployHQ - %s", m.project.Name)) - output, err := cmd.CombinedOutput() - if err != nil { - return createResultMsg{err: fmt.Errorf("%s", strings.TrimSpace(string(output))), step: stepDeployKeyAuto} - } - - return createResultMsg{err: nil, step: stepDeployKeyAuto} + err := installDeployKeyViaGH(m.repoURL, m.project.PublicKey, fmt.Sprintf("DeployHQ - %s", m.project.Name)) + return createResultMsg{err: err, step: stepDeployKeyAuto} } func extractGitHubRepo(url string) string { diff --git a/internal/commands/launch.go b/internal/commands/launch.go new file mode 100644 index 0000000..faad211 --- /dev/null +++ b/internal/commands/launch.go @@ -0,0 +1,1983 @@ +package commands + +// launch.go implements `dhq launch` — the one-command deploy flow for +// Static Hosting and Managed VPS. It is designed non-interactive-first: +// the core logic takes a fully-resolved launchConfig and executes with zero +// prompts; interactive prompts only fill *missing* values when a TTY is present. +// +// Phase 2 + Phase 3 of the one-command deploy plan. + +import ( + "bufio" + "context" + "errors" + "fmt" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/deployhq/deployhq-cli/internal/auth" + "github.com/deployhq/deployhq-cli/internal/config" + "github.com/deployhq/deployhq-cli/internal/detect" + "github.com/deployhq/deployhq-cli/internal/output" + "github.com/deployhq/deployhq-cli/pkg/sdk" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +// launchConfig holds the fully-resolved inputs for the launch flow. +// It is populated from flags → env → .deployhq.toml → detection defaults +// before any prompts or API calls happen. +type launchConfig struct { + // Target selection + targetProtocol string // "static_hosting", "managed_vps", or "" + + // Project + projectID string // permalink / identifier + // projectFromConfig is true when projectID came from .deployhq.toml rather + // than a --project flag. A stale (deleted) project from the toml is cleaned + // up and skipped; a bad explicit --project is treated as a user error. + projectFromConfig bool + + // Persisted server identifier from a previous launch (enables idempotency). + // When non-empty, re-runs skip provisioning and go straight to deploy. + serverID string + + // Static Hosting params + subdomain string + subdirectory string + spaMode bool + + // VPS params + region string + size string + osImage string + + // Deploy params + branch string + + // Behavioural flags + acceptCost bool + cleanupOnFailure bool + dryRun bool +} + +// launchResult is the machine-readable output for --json mode. +type launchResult struct { + Status string `json:"status"` + Target string `json:"target"` + URL string `json:"url,omitempty"` + Project string `json:"project"` + Server string `json:"server"` + Deployment string `json:"deployment,omitempty"` +} + +// dryRunResult is the --dry-run output (no side effects). +type dryRunResult struct { + Would dryRunWould `json:"would"` + Requires []string `json:"requires,omitempty"` + Warning string `json:"warning,omitempty"` +} + +type dryRunWould struct { + Provision string `json:"provision,omitempty"` + Region string `json:"region,omitempty"` + Size string `json:"size,omitempty"` + MonthlyCost string `json:"monthly_cost,omitempty"` + Subdomain string `json:"subdomain,omitempty"` + Project string `json:"project,omitempty"` + Branch string `json:"branch,omitempty"` +} + +// launchErrorReason is the machine-readable error code taxonomy. +const ( + reasonAuthRequired = "auth_required" + reasonBetaEnrollRequired = "beta_enroll_required" + reasonAcceptCostRequired = "accept_cost_required" + reasonRepoUnreachable = "repo_unreachable" + reasonPlanLimitReached = "plan_limit_reached" + reasonSubdomainTaken = "subdomain_taken" + reasonRateLimited = "rate_limited" + reasonProvisionFailed = "provision_failed" + reasonDeployFailed = "deploy_failed" + // reasonEmailVerificationRequired: the account's email isn't verified yet, so + // the backend's deploy gate blocks it. Handled gracefully (wait-and-retry + // interactively, structured fail non-interactively) — not a raw 403. + reasonEmailVerificationRequired = "email_verification_required" +) + +// launchError is a structured error that carries machine-readable next-step +// info for --json mode alongside the human-readable message + hint. +type launchError struct { + Reason string + Message string + NextStep string + Details map[string]string + // Retryable marks an error an agent/CI can safely re-attempt after backing + // off (e.g. a 429 provisioning rate limit), as opposed to a hard wall like + // plan_limit_reached. Surfaced as `retryable` in --json output. + Retryable bool +} + +func (e *launchError) Error() string { + msg := e.Message + if e.NextStep != "" { + msg += "\n\nNext step: " + e.NextStep + } + return msg +} + +// rateLimitLaunchError converts a 429 provisioning-rate-limit API error into a +// structured, retryable launchError carrying the Retry-After backoff hint. It +// returns nil when err is not a 429, so callers can fall through to their +// existing error handling. +func rateLimitLaunchError(err error) *launchError { + var apiErr *sdk.APIError + if !errors.As(err, &apiErr) || !apiErr.IsRateLimited() { + return nil + } + details := map[string]string{} + nextStep := "Wait a moment, then re-run the same command — this is a temporary provisioning rate limit, not a hard cap." + if apiErr.RetryAfter > 0 { + details["retry_after"] = strconv.Itoa(apiErr.RetryAfter) + nextStep = fmt.Sprintf("Wait %ds, then re-run the same command (provisioning rate limit; Retry-After: %ds).", apiErr.RetryAfter, apiErr.RetryAfter) + } + return &launchError{ + Reason: reasonRateLimited, + Message: "Provisioning rate limit reached for this account", + NextStep: nextStep, + Details: details, + Retryable: true, + } +} + +// newLaunchCmd builds the `dhq launch` Cobra command. +func newLaunchCmd() *cobra.Command { + var ( + flagStatic bool + flagVPS bool + flagAcceptCost bool + flagSubdomain string + flagRegion string + flagSize string + flagBranch string + flagProject string + flagCleanupOnFail bool + flagNonInteract bool // local --non-interactive (mirrors global but scoped) + flagInteractive bool + flagDryRun bool + ) + + cmd := &cobra.Command{ + Use: "launch", + Short: "One-command deploy to Static Hosting or Managed VPS", + Long: `Provision and deploy your project to DeployHQ's managed infrastructure in one step. + +dhq launch detects your framework, provisions the right target (Static Hosting or +Managed VPS), connects your repository, and deploys — printing a live URL when done. + +The command is fully non-interactive: pass flags or environment variables to drive +it from CI / AI agents. Interactive prompts only fill in missing values when a TTY +is present. + +Equivalent of 'netlify deploy' / 'vercel' / 'fly launch' for DeployHQ's own infra.`, + Example: ` # Interactive: auto-detect framework and prompt for anything missing + dhq launch + + # CI — static is safe under --yes; a Managed VPS REQUIRES --accept-cost + dhq launch --static --subdomain my-app + dhq launch --vps --accept-cost --region lon1 + + # Agent — structured output (no side effects; inspect before running) + dhq launch --vps --dry-run --json + dhq launch --json --static --subdomain my-app + + # Opt out to own-server setup + # At the target prompt, choose "Use my own server" → branches into dhq init`, + RunE: func(cmd *cobra.Command, args []string) error { + env := cliCtx.Envelope + + // Local --non-interactive flag merges with the global one. + if flagNonInteract { + env.NonInteractive = true + } + // --interactive explicitly re-enables prompts even in piped mode. + if flagInteractive { + env.NonInteractive = false + } + + // Resolve launchConfig from flags → env → .deployhq.toml → detection + cfg := resolveLaunchConfig(flagStatic, flagVPS, flagSubdomain, flagRegion, flagSize, flagBranch, flagProject, flagAcceptCost, flagCleanupOnFail, flagDryRun) + + return runLaunch(env, cfg) + }, + } + + cmd.Flags().BoolVar(&flagStatic, "static", false, "Force Static Hosting target") + cmd.Flags().BoolVar(&flagVPS, "vps", false, "Force Managed VPS target") + cmd.Flags().BoolVar(&flagAcceptCost, "accept-cost", false, "Acknowledge Managed VPS provisioning — "+managedVPSAcknowledgePhrase()+" (required to provision a VPS non-interactively)") + cmd.Flags().StringVar(&flagSubdomain, "subdomain", "", "Static Hosting subdomain (default: repo / project name)") + cmd.Flags().StringVar(&flagRegion, "region", "", "Managed VPS region slug (e.g. lon1, nyc3)") + cmd.Flags().StringVar(&flagSize, "size", "", "Managed VPS size slug (e.g. s-1vcpu-1gb)") + cmd.Flags().StringVar(&flagBranch, "branch", "", "Branch to deploy (default: repo default)") + cmd.Flags().StringVar(&flagProject, "project", "", "Existing project permalink to reuse (skips project creation)") + cmd.Flags().BoolVar(&flagCleanupOnFail, "cleanup-on-failure", false, "Delete the provisioned server when the deploy fails (prevents orphaned managed resources)") + cmd.Flags().BoolVar(&flagNonInteract, "non-interactive", false, "Never prompt; fail fast with structured errors on ambiguity (alias: --yes)") + cmd.Flags().BoolVar(&flagNonInteract, "yes", false, "Alias for --non-interactive") + cmd.Flags().BoolVar(&flagInteractive, "interactive", false, "Force interactive mode even in piped / agent contexts") + cmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Print intended actions and cost without executing (no side effects)") + + return cmd +} + +// resolveLaunchConfig builds a launchConfig from flag values, falling through +// to .deployhq.toml (already loaded in cliCtx.Config) and detection defaults. +func resolveLaunchConfig( + flagStatic, flagVPS bool, + flagSubdomain, flagRegion, flagSize, flagBranch, flagProject string, + flagAcceptCost, flagCleanupOnFail, flagDryRun bool, +) launchConfig { + cfg := launchConfig{ + acceptCost: flagAcceptCost, + cleanupOnFailure: flagCleanupOnFail, + dryRun: flagDryRun, + } + + // Target protocol: flag wins, then nothing (resolved later via detection/prompt) + if flagStatic { + cfg.targetProtocol = detect.ProtocolStaticHosting + } else if flagVPS { + cfg.targetProtocol = detect.ProtocolManagedVPS + } + + // Project: flag > env > .deployhq.toml (cliCtx.Config already merged those layers) + cfg.projectID = flagProject + if cfg.projectID == "" && cliCtx != nil { + cfg.projectID = cliCtx.Config.Project + cfg.projectFromConfig = cfg.projectID != "" // came from .deployhq.toml, not a flag + } + + // Server / target: read from .deployhq.toml so re-runs can skip provisioning. + if cliCtx != nil { + if cfg.serverID == "" { + cfg.serverID = cliCtx.Config.Server + } + // Only fall back to the persisted target when no flag was given. + if cfg.targetProtocol == "" { + cfg.targetProtocol = cliCtx.Config.Target + } + } + + cfg.subdomain = flagSubdomain + cfg.region = flagRegion + cfg.size = flagSize + cfg.branch = flagBranch + + return cfg +} + +// runLaunch executes the full launch flow. It is broken out from the cobra +// RunE so tests can call it directly with a pre-built Envelope. +func runLaunch(env *output.Envelope, cfg launchConfig) error { + ctx := context.Background() + + env.Status("") + env.Status("DeployHQ • one-command deploy") + env.Status("") + + // ── Step 1: Auth / bootstrap ──────────────────────────────────────────── + client, accountSubdomain, err := launchEnsureAuth(ctx, env, cfg) + if err != nil { + return writeLaunchError(env, cfg, reasonAuthRequired, err) + } + + // ── Step 2: Detect ────────────────────────────────────────────────────── + // Prefer backend detection (same StackDetector pipeline as the web + // onboarding wizard) so the CLI's recommendation stays in lockstep with the + // server. Fall back to the local heuristic when the endpoint is + // unavailable (older backend, offline, transient error). + cwd, _ := os.Getwd() + detection := launchDetect(ctx, env, client, cwd) + if detection.Framework != detect.FrameworkUnknown && detection.Framework != "" { + env.Status("Detected: %s", string(detection.Framework)) + } else { + env.Status("No framework detected — will prompt for target.") + } + if detection.SuggestedProtocol != "" && cfg.targetProtocol == "" { + cfg.targetProtocol = detection.SuggestedProtocol + } + + // ── Dry-run exit: before any mutation ───────────────────────────── + // Read-only: caps and region/size listing for cost estimates are allowed, + // but beta enrollment, project creation, and provisioning must NOT happen. + if cfg.dryRun { + // Fetch caps for the dry-run cost/warning display (read-only). + caps, capsErr := client.GetAccountCapabilities(ctx) + if capsErr != nil { + var apiErr *sdk.APIError + if errors.As(capsErr, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + caps = &sdk.AccountCapabilities{} + } else { + caps = &sdk.AccountCapabilities{} + } + } + return launchDryRun(ctx, env, cfg, client, caps) + } + + // ── Step 3: Capability pre-flight ─────────────────────────────────────── + // capsKnown tracks whether the backend returned capability data. When false + // (404 = older backend), the plan-limit gate is skipped and CreateServer + // acts as the authority. + caps, capsKnown, err := launchGetCaps(ctx, env, client) + if err != nil { + // An email_verification_required failure carries its own reason; route it + // through writeLaunchError so --json gets the structured payload. + var le *launchError + if errors.As(err, &le) { + return writeLaunchError(env, cfg, le.Reason, err) + } + return err + } + + // ── Step 4: Repo deployability pre-flight ─────────────────────────────── + gitRemote := detectGitRemote() + if gitRemote == "" { + return writeLaunchError(env, cfg, reasonRepoUnreachable, &output.UserError{ + Message: "No git remote found in this directory", + Hint: "Static Hosting and Managed VPS deployments require a connected git repository.\nRun: git remote add origin \nThen re-run dhq launch.", + }) + } + env.Status("Repository: %s", gitRemote) + + // ── Step 5: Target selection ───────────────────────────────────────────── + if cfg.targetProtocol == "" { + selected, err := launchPromptTarget(env) + if err != nil { + return err + } + if selected == "own_server" { + env.Status("") + env.Status("Redirecting to guided setup for your own server...") + env.Status("Run: dhq init") + return nil + } + cfg.targetProtocol = selected + } + env.Status("Target: %s", cfg.targetProtocol) + + // ── Step 6: Beta enrollment (after target is known) ───────────── + // isManagedTarget is now evaluated with the final resolved target so that + // interactive target selection is accounted for before we attempt enrollment. + isManagedTarget := cfg.targetProtocol == detect.ProtocolStaticHosting || cfg.targetProtocol == detect.ProtocolManagedVPS + if isManagedTarget && capsKnown && !caps.BetaFeatures { + if err := launchEnsureBetaEnrolled(ctx, env, cfg, client, accountSubdomain); err != nil { + return err + } + // Re-read caps after enrollment + if caps, _, err = launchGetCaps(ctx, env, client); err != nil { + caps = &sdk.AccountCapabilities{} + } + } + + // ── Step 7: Project + repo ─────────────────────────────────────────────── + projectID, err := launchEnsureProject(ctx, env, cfg, client, gitRemote) + if err != nil { + return err + } + cfg.projectID = projectID + + // ── Apply detection defaults for static hosting ────────────────── + // Seed subdirectory and SPA mode from detection when flags were not set. + if cfg.targetProtocol == detect.ProtocolStaticHosting { + if cfg.subdirectory == "" && detection.OutputDir != "" { + cfg.subdirectory = detection.OutputDir + } + // Only override spaMode from detection when it hasn't been set by a flag. + // Since spaMode is a bool it defaults to false; we apply detection.SPA + // when detection actually had an opinion (SPA==true) and the user didn't + // explicitly prompt otherwise. False detection.SPA leaves cfg.spaMode alone. + if detection.SPA && !cfg.spaMode { + cfg.spaMode = true + } + } + + // ── Step 8: Plan / limit pre-flight ───────────────────────────────────── + // Only apply eligibility gates when we have real capability data. + if capsKnown { + if err := launchCheckPlanLimits(env, cfg, caps); err != nil { + return err + } + } + + // ── Step 9: Provision server — idempotency check ───────────────── + // If a server identifier was persisted from a previous run, verify it still + // exists. If it does, skip provisioning and go straight to deploy. + var server *sdk.Server + if cfg.serverID != "" { + existing, pollErr := client.GetServerProvisioningState(ctx, cfg.projectID, cfg.serverID) + if pollErr == nil { + env.Status("Found existing server %s — skipping provisioning.", cfg.serverID) + server = existing + } else { + // Server no longer exists (404) or there's an error — fall through to provision. + env.Status("Persisted server %s not found (%v) — provisioning new server.", cfg.serverID, pollErr) + } + } + + if server == nil { + var provErr error + server, provErr = launchProvision(ctx, env, cfg, client) + if provErr != nil { + // Provision failure cleanup: run the same cleanup path as deploy failures + // so --cleanup-on-failure and resource naming apply. + if server != nil { + launchDeployFailureCleanup(ctx, env, cfg, client, server) + } + return writeLaunchError(env, cfg, reasonProvisionFailed, provErr) + } + // Persist the new server so re-runs are idempotent. + launchPersistConfig(env, cfg, server) + cfg.serverID = server.Identifier + } + + // ── Step 10: Static-hosting project config (build command + excludes/cache) ─ + if cfg.targetProtocol == detect.ProtocolStaticHosting { + if len(detection.BuildCommands) > 0 { + launchApplyBuildCommand(ctx, env, cfg, client, detection) + } + launchApplyStaticExtras(ctx, env, cfg, client, detection) + } + + // ── Step 11: Deploy ─────────────────────────────────────────────────────── + dep, liveURL, err := launchDeploy(ctx, env, cfg, client, server) + if err != nil { + // Close the loop: a local-AI diagnosis of the failure (interactive + + // Ollama only; no-op otherwise). Runs before cleanup so the failed + // deployment is still queryable for context. + explainLaunchFailure(ctx, env, client, cfg.projectID) + // Provision succeeded but deploy failed — name the resource + if server != nil { + launchDeployFailureCleanup(ctx, env, cfg, client, server) + } + return writeLaunchError(env, cfg, reasonDeployFailed, err) + } + + // ── Step 12: Persist to .deployhq.toml ─────────────────────────────────── + launchPersistConfig(env, cfg, server) + + // ── Final output ────────────────────────────────────────────────────────── + if env.WantsJSON() { + result := launchResult{ + Status: "live", + Target: cfg.targetProtocol, + URL: liveURL, + Project: cfg.projectID, + Server: server.Identifier, + Deployment: "", + } + if dep != nil { + result.Deployment = dep.Identifier + } + return env.WriteJSON(output.NewResponse(result, "Deployment live: "+liveURL)) + } + + env.Status("") + output.ColorGreen.Fprintf(env.Stderr, "Live: %s\n", liveURL) //nolint:errcheck + env.Status("Saved settings to .deployhq.toml — redeploy with 'dhq deploy -s %s'.", server.Identifier) + env.Status("Roll back anytime with 'dhq rollback ' — redeploys the previous revision ('dhq deployments list' shows history).") + // Final stdout line = live URL (Vercel pattern, scriptable) + fmt.Fprintln(os.Stdout, liveURL) //nolint:errcheck + + return nil +} + +// ── Detection (remote-first, local fallback) ────────────────────────────────── + +// launchDetect runs framework detection for dir. It prefers the backend's +// /detection endpoint (the same StackDetector pipeline the web onboarding +// wizard uses, so the CLI's recommendation matches the server), and falls back +// to the local heuristic when the endpoint is unavailable — an older backend +// that 404s, an offline run, or any transient error. The fallback is silent at +// normal verbosity: detection is advisory, and the local result is good. +func launchDetect(ctx context.Context, env *output.Envelope, client *sdk.Client, dir string) detect.Result { + filenames, files := detect.CollectManifest(dir) + resp, err := client.DetectFramework(ctx, sdk.DetectionPayload{Filenames: filenames, Files: files}) + if err != nil { + // Silent to the user (detection is advisory and the local result is + // good); recorded for debugging. + env.Logger.Write("remote detection unavailable (%v) — using local detection", err) + return detect.Detect(dir) + } + return detectionResultFromAPI(resp) +} + +// detectionResultFromAPI maps a backend /detection response into the Result +// shape the launch flow consumes. Multiple suggested build commands are joined +// into a single shell command (the backend runs build commands as shell steps, +// so "install && build" is equivalent to two ordered commands). +func detectionResultFromAPI(resp *sdk.DetectionResponse) detect.Result { + cmds := make([]detect.BuildCommandStep, 0, len(resp.BuildCommands)) + for _, bc := range resp.BuildCommands { + if bc.Command != "" { + cmds = append(cmds, detect.BuildCommandStep{ + Description: bc.Description, + Command: bc.Command, + TemplateName: bc.TemplateName, + HaltOnError: bc.HaltOnError, + }) + } + } + excluded := make([]string, 0, len(resp.ExcludedFiles)) + for _, f := range resp.ExcludedFiles { + if f.Path != "" { + excluded = append(excluded, f.Path) + } + } + cache := make([]string, 0, len(resp.BuildCacheFiles)) + for _, f := range resp.BuildCacheFiles { + if f.Path != "" { + cache = append(cache, f.Path) + } + } + return detect.Result{ + Framework: detect.Framework(resp.Stack), + SuggestedProtocol: resp.SuggestedProtocol, + BuildCommands: cmds, + OutputDir: resp.StaticHosting.RootPath, + SPA: resp.StaticHosting.SPAMode, + ExcludedFiles: excluded, + BuildCacheFiles: cache, + } +} + +// launchGetCaps fetches account capabilities and reports whether the data is +// authoritative. When the backend 404s (older/staging), capsKnown is false +// and callers must not gate on the returned caps. +func launchGetCaps(ctx context.Context, env *output.Envelope, client *sdk.Client) (caps *sdk.AccountCapabilities, capsKnown bool, err error) { + caps, err = client.GetAccountCapabilities(ctx) + if err != nil { + var apiErr *sdk.APIError + if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + env.Warn("Capability check unavailable — beta status unknown. Visit https://app.deployhq.com/beta_features to enable.") + return &sdk.AccountCapabilities{}, false, nil + } + // The account's email isn't verified yet — handle it gracefully (wait and + // retry interactively, structured fail non-interactively) rather than + // surfacing a raw 403. Every OTHER error still propagates as a real error. + if isEmailVerificationRequired(err) { + if hErr := handleEmailVerificationRequired(ctx, env, client, ""); hErr != nil { + return nil, false, hErr + } + // Verified after the wait — re-fetch; degrade if the refetch fails. + if caps, err = client.GetAccountCapabilities(ctx); err != nil { + return &sdk.AccountCapabilities{}, false, nil + } + return caps, true, nil + } + return nil, false, err + } + return caps, true, nil +} + +// isEmailVerificationRequired reports whether err is the backend's SPECIFIC +// "email not verified" 403 — the one launch handles gracefully. A generic 403 +// (AccessDenied) returns false here so it still surfaces as a real error. +func isEmailVerificationRequired(err error) bool { + var apiErr *sdk.APIError + return errors.As(err, &apiErr) && apiErr.IsEmailVerificationRequired() +} + +// emailVerificationLaunchError is the structured failure returned in +// non-interactive mode when the account's email isn't verified. +func emailVerificationLaunchError() *launchError { + return &launchError{ + Reason: reasonEmailVerificationRequired, + Message: "Your email address needs to be verified before you can deploy. Open the verification link we emailed you, then re-run.", + NextStep: "Verify your email (check your inbox), then re-run: dhq launch", + Retryable: true, + } +} + +// handleEmailVerificationRequired deals with the one auth case launch handles +// gracefully. Non-interactive: a clean structured failure asking the user to +// verify. Interactive: explain, wait for the user to verify and press Enter, +// re-checking until the account is verified (or the user cancels with Ctrl-C). +func handleEmailVerificationRequired(ctx context.Context, env *output.Envelope, client *sdk.Client, email string) error { + if env.NonInteractive { + return emailVerificationLaunchError() + } + + if email == "" { + _, email, _, _ = cliCtx.Credentials() + } + env.Status("") + env.Status("We're sorry — your email needs to be verified before you can deploy.") + if email != "" { + env.Status("We've sent a verification link to %s. Open it to verify your account.", email) + } else { + env.Status("Check your inbox for the verification link.") + } + + reader := bufio.NewReader(os.Stdin) + for { + fmt.Fprint(env.Stderr, "Press Enter once you've verified (Ctrl-C to cancel)... ") //nolint:errcheck + _, _ = reader.ReadString('\n') + // Re-check against an authenticated endpoint: success — or any error that + // is NOT the email-verification gate — means we can proceed. + if _, err := client.GetAccountCapabilities(ctx); err == nil || !isEmailVerificationRequired(err) { + env.Status("Thanks — continuing.") + return nil + } + env.Status("Still not verified — open the verification link, then press Enter.") + } +} + +// ── Auth / bootstrap ──────────────────────────────────────────────────────── + +// launchEnsureAuth returns an authenticated SDK client and the account subdomain. +// In non-interactive mode it requires env-var creds; it never attempts headless signup. +func launchEnsureAuth(ctx context.Context, env *output.Envelope, cfg launchConfig) (*sdk.Client, string, error) { + // Fast path: try to build a client from existing creds + client, err := cliCtx.Client() + if err == nil { + account, _, _, _ := cliCtx.Credentials() + return client, account, nil + } + + // Non-interactive: fail fast with structured error + if env.NonInteractive { + return nil, "", &output.AuthError{ + Message: "Not authenticated", + Hint: "Set credentials via environment variables:\n" + + " export DEPLOYHQ_ACCOUNT= DEPLOYHQ_EMAIL= DEPLOYHQ_API_KEY=\n" + + "Or log in interactively: dhq auth login", + } + } + + // Interactive: offer create-account or log in + env.Status("No DeployHQ account found on this machine.") + env.Status("") + + prompt := promptui.Select{ + Label: "Get started", + Items: []string{"Create a new account (recommended)", "Log in to an existing account"}, + } + idx, _, promptErr := prompt.Run() + if promptErr != nil { + return nil, "", &output.UserError{Message: "Auth cancelled"} + } + + reader := bufio.NewReader(os.Stdin) + if idx == 0 { + return launchSignup(ctx, env, reader) + } + return launchLogin(ctx, env, reader) +} + +func launchSignup(ctx context.Context, env *output.Envelope, reader *bufio.Reader) (*sdk.Client, string, error) { + env.Status("") + fmt.Fprint(env.Stderr, "Email: ") //nolint:errcheck + email, _ := reader.ReadString('\n') + email = strings.TrimSpace(email) + if email == "" { + return nil, "", &output.UserError{Message: "Email is required"} + } + + fmt.Fprint(env.Stderr, "Password: ") //nolint:errcheck + pw, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Fprintln(env.Stderr) //nolint:errcheck + if err != nil { + return nil, "", &output.InternalError{Message: "read password", Cause: err} + } + password := strings.TrimSpace(string(pw)) + if password == "" { + return nil, "", &output.UserError{Message: "Password is required"} + } + + fmt.Fprint(env.Stderr, "Accept terms of service? [Y/n]: ") //nolint:errcheck + terms, _ := reader.ReadString('\n') + terms = strings.TrimSpace(strings.ToLower(terms)) + if terms != "" && terms != "y" && terms != "yes" { + return nil, "", &output.UserError{ + Message: "Terms of service must be accepted to create an account", + Hint: "Visit https://www.deployhq.com/terms to review.", + } + } + + env.Status("Creating account...") + ua := cliUserAgent() + result, err := sdk.Signup(sdk.SignupRequest{ + Email: email, + Password: password, + Client: "dhq-cli", + TermsAccepted: true, + }, ua, cliCtx.Config.SignupURL()) + if err != nil { + var twoFA *sdk.TwoFactorError + if errors.As(err, &twoFA) { + return nil, "", &output.UserError{ + Message: "Two-factor authentication required", + Hint: "This email is linked to an existing account with 2FA. Please sign up or log in at https://www.deployhq.com then run: dhq auth login", + } + } + return nil, "", err + } + + creds := &auth.Credentials{ + Account: result.Account.Subdomain, + Email: email, + APIKey: result.APIKey, + } + if storeErr := auth.Store(creds); storeErr != nil { + env.Warn("Could not save credentials: %v", storeErr) + } + _ = config.Set(config.GlobalConfigPath(), "account", result.Account.Subdomain) + + output.ColorGreen.Fprintf(env.Stderr, "Account %q created. API key stored.\n", result.Account.Subdomain) //nolint:errcheck + + var sdkOpts []sdk.Option + if baseURL := cliCtx.Config.BaseURL(result.Account.Subdomain); baseURL != "" { + sdkOpts = append(sdkOpts, sdk.WithBaseURL(baseURL)) + } + sdkOpts = append(sdkOpts, sdk.WithUserAgent(cliUserAgent())) + client, clientErr := sdk.New(result.Account.Subdomain, email, result.APIKey, sdkOpts...) + if clientErr != nil { + return nil, "", &output.InternalError{Message: "create api client after signup", Cause: clientErr} + } + + // A fresh signup's email isn't verified yet, and the backend's deploy gate + // will block the deployment. Handle it now (wait-and-retry, since signup is + // interactive) so the user verifies up front rather than running into a 403 + // after detection — and avoid a misleading "No framework detected". + if !result.EmailVerified { + if vErr := handleEmailVerificationRequired(ctx, env, client, email); vErr != nil { + return nil, "", vErr + } + } + return client, result.Account.Subdomain, nil +} + +func launchLogin(ctx context.Context, env *output.Envelope, reader *bufio.Reader) (*sdk.Client, string, error) { + creds, err := helloLogin(env, reader) + if err != nil { + return nil, "", err + } + + var sdkOpts []sdk.Option + if baseURL := cliCtx.Config.BaseURL(creds.Account); baseURL != "" { + sdkOpts = append(sdkOpts, sdk.WithBaseURL(baseURL)) + } + sdkOpts = append(sdkOpts, sdk.WithUserAgent(cliUserAgent())) + client, clientErr := sdk.New(creds.Account, creds.Email, creds.APIKey, sdkOpts...) + if clientErr != nil { + return nil, "", &output.InternalError{Message: "create api client after login", Cause: clientErr} + } + return client, creds.Account, nil +} + +// ── Beta enrollment ────────────────────────────────────────────────────────── + +func launchEnsureBetaEnrolled(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, accountSubdomain string) error { + betaURL := fmt.Sprintf("https://app.deployhq.com/%s/beta_features", accountSubdomain) + + env.Status("") + env.Status("Managed-resources beta is not enabled on this account.") + + if env.NonInteractive { + // Non-interactive (CI/agent): attempt enrollment directly. EnrollBeta is + // idempotent and admin-gated server-side, so an admin (or an already-enrolled + // account) proceeds automatically, while a non-admin gets the structured + // beta_enroll_required 403 below. + env.Status("Enabling managed-resources beta...") + } else { + prompt := promptui.Select{ + Label: "Enable managed-resources beta now?", + Items: []string{"Yes, enable beta", "No, use my own server instead"}, + } + idx, _, err := prompt.Run() + if err != nil || idx == 1 { + env.Status("") + env.Status("To set up with your own server, run: dhq init") + return &output.UserError{Message: "Beta enrollment skipped — use 'dhq init' for own-server setup"} + } + env.Status("Enrolling in managed-resources beta...") + } + + _, enrollErr := client.EnrollBeta(ctx, cfg.targetProtocol) + if enrollErr != nil { + var apiErr *sdk.APIError + if errors.As(enrollErr, &apiErr) && apiErr.StatusCode == http.StatusForbidden { + return &launchError{ + Reason: reasonBetaEnrollRequired, + Message: "Beta enrollment requires an account admin", + NextStep: "Ask an account admin to enable the beta at: " + betaURL + "\nOr use your own server: dhq init", + Details: map[string]string{"beta_url": betaURL, "admin_required": "true"}, + } + } + // 404 = endpoint doesn't exist yet (older backend / staging mismatch) + if errors.As(enrollErr, &apiErr) && apiErr.StatusCode == http.StatusNotFound { + env.Warn("Beta enrollment endpoint not available on this server. Visit %s to enable manually.", betaURL) + return nil + } + return enrollErr + } + + output.ColorGreen.Fprintf(env.Stderr, "Beta enabled.\n") //nolint:errcheck + return nil +} + +// ── Target selection ───────────────────────────────────────────────────────── + +func launchPromptTarget(env *output.Envelope) (string, error) { + if env.NonInteractive { + return "", &output.UserError{ + Message: "Target not specified", + Hint: "Pass --static (Static Hosting) or --vps (Managed VPS) or choose your own server with dhq init", + } + } + + prompt := promptui.Select{ + Label: "Deployment target", + Items: []string{ + "Static Hosting (beta) — global CDN via Cloudflare, from $2/site" + betaFreeSuffix(), + "Managed VPS (beta) — DeployHQ provisions and manages a VPS for you" + betaFreeSuffix(), + "Use my own server (SSH/FTP/…)", + }, + } + idx, _, err := prompt.Run() + if err != nil { + return "", &output.UserError{Message: "Target selection cancelled"} + } + switch idx { + case 0: + return detect.ProtocolStaticHosting, nil + case 1: + return detect.ProtocolManagedVPS, nil + default: + return "own_server", nil + } +} + +// ── Dry-run ─────────────────────────────────────────────────────────────────── + +func launchDryRun(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, caps *sdk.AccountCapabilities) error { + + dr := dryRunResult{ + Would: dryRunWould{ + Provision: cfg.targetProtocol, + Project: cfg.projectID, + Branch: cfg.branch, + }, + } + + switch cfg.targetProtocol { + case detect.ProtocolStaticHosting: + dr.Would.Subdomain = cfg.subdomain + if dr.Would.Subdomain == "" { + dr.Would.Subdomain = "" + } + case detect.ProtocolManagedVPS: + region := cfg.region + if region == "" { + region = "lon1" + } + size := cfg.size + monthlyCost := "" + if size == "" { + // Try to fetch first available size for cost estimate + sizes, sErr := client.ListManagedHostingSizes(ctx) + if sErr == nil && len(sizes) > 0 { + size = sizes[0].Slug + monthlyCost = fmt.Sprintf("$%.2f", sizes[0].PriceMonthly) + } else { + size = "s-1vcpu-1gb" + } + } + dr.Would.Region = region + dr.Would.Size = size + dr.Would.MonthlyCost = monthlyCost + + if !cfg.acceptCost { + dr.Requires = append(dr.Requires, "--accept-cost") + } + if !caps.BetaFeatures { + dr.Requires = append(dr.Requires, "beta enrollment") + } + } + + if !caps.BetaFeatures && cfg.targetProtocol != "" { + dr.Warning = "Managed-resources beta not enabled. Run dhq launch without --dry-run to enroll." + } + + if env.WantsJSON() { + return env.WriteJSON(output.NewResponse(dr, "Dry run — no changes made")) + } + + env.Status("DRY RUN — no side effects") + env.Status("") + env.Status("Would provision: %s", dr.Would.Provision) + if dr.Would.Subdomain != "" { + env.Status(" Subdomain: %s.deployhq-sites.com", dr.Would.Subdomain) + } + if dr.Would.Region != "" { + env.Status(" Region: %s", dr.Would.Region) + env.Status(" Size: %s", dr.Would.Size) + env.Status(" Cost: %s", managedVPSCostDescription(dr.Would.MonthlyCost)) + } + if len(dr.Requires) > 0 { + env.Status("") + env.Status("Required before non-interactive run:") + for _, r := range dr.Requires { + env.Status(" %s", r) + } + } + if dr.Warning != "" { + env.Warn("%s", dr.Warning) + } + return nil +} + +// ── Project + repo ─────────────────────────────────────────────────────────── + +func launchEnsureProject(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, gitRemote string) (string, error) { + // Re-use an existing project if specified or already in .deployhq.toml. + // First verify it still exists: a stale .deployhq.toml (project deleted in + // the dashboard) would otherwise produce a misleading "could not connect + // repository" 404 when we try to attach the repo. + if cfg.projectID != "" { + if _, err := client.GetProject(ctx, cfg.projectID); err != nil { + var apiErr *sdk.APIError + notFound := errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound + switch { + case notFound && cfg.projectFromConfig: + // Stale persisted reference — warn, clean it out of the toml, and + // fall through to normal resolution (auto-pick / prompt / create). + env.Warn("Saved project %q no longer exists — removing it from .deployhq.toml and continuing.", cfg.projectID) + launchClearStaleProjectConfig(env) + cfg.projectID = "" + cfg.serverID = "" + case notFound: + // Explicit --project that doesn't exist — a user input error. + return "", &output.UserError{ + Message: fmt.Sprintf("Project %q not found", cfg.projectID), + Hint: "Check the --project value and that you have access, or omit it to pick or create a project.", + } + default: + return "", err // network / auth / other — surface as-is + } + } + } + + if cfg.projectID != "" { + env.Status("Using project: %s", cfg.projectID) + // Ensure a repo is connected — treat hard failures as terminal. + if err := launchEnsureRepo(ctx, env, cfg, client, cfg.projectID, gitRemote); err != nil { + return "", &launchError{ + Reason: reasonRepoUnreachable, + Message: "Could not connect repository to project: " + err.Error(), + NextStep: "Verify the repository URL is accessible and retry, or connect it manually in the DeployHQ dashboard.", + } + } + return cfg.projectID, nil + } + + // Auto-pick the sole project + projects, err := client.ListProjects(ctx, nil) + if err != nil { + return "", err + } + if len(projects) == 1 { + env.Status("Auto-selected project: %s", projects[0].Name) + // Treat hard repo-connection failure as terminal. + if err := launchEnsureRepo(ctx, env, cfg, client, projects[0].Permalink, gitRemote); err != nil { + return "", &launchError{ + Reason: reasonRepoUnreachable, + Message: "Could not connect repository to project: " + err.Error(), + NextStep: "Verify the repository URL is accessible and retry, or connect it manually in the DeployHQ dashboard.", + } + } + return projects[0].Permalink, nil + } + + // Interactive: offer to pick or create + if !env.NonInteractive { + return launchPromptOrCreateProject(ctx, env, cfg, client, projects, gitRemote) + } + + // Non-interactive, no project configured: fail fast + return "", &output.UserError{ + Message: "No project specified", + Hint: "Pass --project or set DEPLOYHQ_PROJECT. Run 'dhq projects list' to see available projects.", + } +} + +func launchPromptOrCreateProject(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, existing []sdk.Project, gitRemote string) (string, error) { + items := []string{"Create a new project"} + for _, p := range existing { + items = append(items, fmt.Sprintf("%s (%s)", p.Name, p.Permalink)) + } + + prompt := promptui.Select{ + Label: "Project", + Items: items, + } + idx, _, err := prompt.Run() + if err != nil { + return "", &output.UserError{Message: "Project selection cancelled"} + } + + if idx > 0 { + p := existing[idx-1] + env.Status("Using project: %s", p.Name) + // Treat hard repo-connection failure as terminal. + if err := launchEnsureRepo(ctx, env, cfg, client, p.Permalink, gitRemote); err != nil { + return "", &launchError{ + Reason: reasonRepoUnreachable, + Message: "Could not connect repository to project: " + err.Error(), + NextStep: "Verify the repository URL is accessible and retry, or connect it manually in the DeployHQ dashboard.", + } + } + return p.Permalink, nil + } + + // Create a new project + return launchCreateProject(ctx, env, cfg, client, gitRemote) +} + +func launchCreateProject(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, gitRemote string) (string, error) { + // Derive a default project name from the git remote + projectName := projectNameFromRemote(gitRemote) + + if !env.NonInteractive { + prompt := promptui.Prompt{ + Label: "Project name", + Default: projectName, + } + name, err := prompt.Run() + if err != nil { + return "", &output.UserError{Message: "Project creation cancelled"} + } + if name != "" { + projectName = name + } + } + if projectName == "" { + projectName = "my-app" + } + + env.Status("Creating project %q...", projectName) + proj, err := client.CreateProject(ctx, sdk.ProjectCreateRequest{Name: projectName}) + if err != nil { + // 422 name conflict + var apiErr *sdk.APIError + if errors.As(err, &apiErr) && apiErr.IsValidationError() { + // Try with a timestamp suffix + projectName = projectName + "-" + fmt.Sprintf("%d", time.Now().Unix()%10000) + env.Status("Name conflict — retrying as %q...", projectName) + proj, err = client.CreateProject(ctx, sdk.ProjectCreateRequest{Name: projectName}) + if err != nil { + return "", err + } + } else { + return "", err + } + } + + env.Status("Project %q created (%s)", proj.Name, proj.Permalink) + + // Connect the git repository + if err := launchEnsureRepo(ctx, env, cfg, client, proj.Permalink, gitRemote); err != nil { + env.Warn("Could not connect repository automatically: %v", err) + env.Warn("You may need to connect it manually in the DeployHQ dashboard.") + } + + return proj.Permalink, nil +} + +func launchEnsureRepo(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, projectID, gitRemote string) error { + if gitRemote == "" { + return nil + } + // Check if repo already connected + proj, err := client.GetProject(ctx, projectID) + if err == nil && proj.Repository != nil && proj.Repository.URL != "" { + // Already connected + return nil + } + + env.Status("Connecting repository %s...", gitRemote) + branch := cfg.branch + if branch == "" { + // Detect the local default branch rather than hardcoding "main". + // Try the local HEAD first, then the tracked remote HEAD. Fall back to + // omitting the branch so the DeployHQ API resolves the repo default. + branch = detectDefaultBranch() + } + _, err = client.CreateRepository(ctx, projectID, sdk.RepositoryCreateRequest{ + ScmType: "git", + URL: gitRemote, + Branch: branch, + }) + if err != nil { + return err + } + env.Status("Repository connected.") + // Surface the project's public deploy key so a private repo can be granted + // read access before the first clone. proj was fetched above (may be nil if + // that lookup failed — the helper re-fetches in that case). For a GitHub repo + // with the gh CLI present, the helper installs the key automatically instead. + launchSurfaceDeployKey(ctx, env, client, projectID, gitRemote, proj) + return nil +} + +// launchSurfaceDeployKey prints the project's PUBLIC deploy key after a +// repository is connected, so the user can grant a private git host read access +// before the first clone. The key shown is the project's public key (safe to +// display); the matching private key never leaves the server. In interactive +// mode it pauses so the user can install the key first; non-interactively it +// prints a warning and continues (a private repo missing the key surfaces as a +// clone error at deploy time, which the structured error flow reports). +func launchSurfaceDeployKey(ctx context.Context, env *output.Envelope, client *sdk.Client, projectID, gitRemote string, proj *sdk.Project) { + publicKey := "" + if proj != nil { + publicKey = proj.PublicKey + } + if publicKey == "" { + // The earlier project lookup failed or omitted the key — fetch it now. + if fresh, err := client.GetProject(ctx, projectID); err == nil && fresh != nil { + publicKey = fresh.PublicKey + } + } + if publicKey == "" { + return // nothing to surface (best-effort) + } + + // Path A: for a GitHub repo, try the local `gh` CLI to install the deploy + // key automatically. gh carries its own auth, so this needs no browser or + // prompt and works in both interactive and non-interactive mode. Any failure + // (not GitHub, gh missing/unauthenticated, no write access, duplicate key) + // falls through to surfacing the key for manual installation. + if isGitHubURL(gitRemote) && ghAvailable() { + name := projectID + if proj != nil && proj.Name != "" { + name = proj.Name + } + err := installDeployKeyViaGH(gitRemote, publicKey, fmt.Sprintf("DeployHQ - %s", name)) + if err == nil { + output.ColorGreen.Fprintf(env.Stderr, "Added the project's deploy key to GitHub via the gh CLI.\n") //nolint:errcheck + return + } + env.Logger.Write("gh deploy-key add failed; falling back to manual key: %v", err) + } + + env.Status("") + env.Status("Deploy key for this project:") + env.Status("") + env.Status("%s", publicKey) + env.Status("") + env.Status("Add this public key as a deploy key on your repository host so DeployHQ can") + env.Status("clone the repo (required for private repositories; read access is enough).") + env.Status("DeployHQ does not add it automatically for repositories connected by URL.") + + if env.NonInteractive { + env.Warn("Private repo? Add the deploy key above to your git host (e.g. GitHub: Settings -> Deploy keys) before deploying, then re-run if the clone fails.") + return + } + + env.Status("") + fmt.Fprint(env.Stderr, "Press Enter once the key is added (or now, if the repo is public)... ") //nolint:errcheck + reader := bufio.NewReader(os.Stdin) + _, _ = reader.ReadString('\n') +} + +// detectDefaultBranch returns the local HEAD branch name for the repository in +// the current working directory. Returns "" when it cannot be determined so the +// API can use its own default resolution. +func detectDefaultBranch() string { + // Try local HEAD (works for checked-out repos) + if out, err := runGitCommand("symbolic-ref", "--short", "HEAD"); err == nil && out != "" { + return out + } + // Try remote tracking HEAD (works in detached-HEAD / CI clones) + if out, err := runGitCommand("rev-parse", "--abbrev-ref", "origin/HEAD"); err == nil && out != "" { + // Strip "origin/" prefix if present + if len(out) > 7 && out[:7] == "origin/" { + return out[7:] + } + return out + } + // Return empty — let the API decide + return "" +} + +// projectNameFromRemote derives a human-readable project name from a git remote URL. +// git@github.com:acme/my-app.git → "my-app" +// https://github.com/acme/my-app.git → "my-app" +func projectNameFromRemote(remote string) string { + if remote == "" { + return "my-app" + } + // Strip trailing .git + remote = strings.TrimSuffix(remote, ".git") + // Take the last path segment + if i := strings.LastIndexAny(remote, "/:"); i >= 0 { + remote = remote[i+1:] + } + if remote == "" { + return "my-app" + } + return remote +} + +// ── Plan / limit pre-flight ─────────────────────────────────────────────────── + +func launchCheckPlanLimits(env *output.Envelope, cfg launchConfig, caps *sdk.AccountCapabilities) error { + if cfg.targetProtocol == detect.ProtocolStaticHosting && !caps.StaticHostingEligible { + // Not eligible = plan limit or billing wall + return &launchError{ + Reason: reasonPlanLimitReached, + Message: "Your account cannot provision Static Hosting sites", + NextStep: "Check your plan or billing at https://app.deployhq.com/account/plan. Free plans support 1 site.", + Details: map[string]string{"target": detect.ProtocolStaticHosting}, + } + } + if cfg.targetProtocol == detect.ProtocolManagedVPS && !caps.ManagedVPSEligible { + return &launchError{ + Reason: reasonPlanLimitReached, + Message: "Your account cannot provision Managed VPS servers", + NextStep: "Ensure your billing details are set up at https://app.deployhq.com/account/billing", + Details: map[string]string{"target": detect.ProtocolManagedVPS}, + } + } + return nil +} + +// ── Provision ───────────────────────────────────────────────────────────────── + +func launchProvision(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client) (*sdk.Server, error) { + switch cfg.targetProtocol { + case detect.ProtocolStaticHosting: + return launchProvisionStatic(ctx, env, cfg, client) + case detect.ProtocolManagedVPS: + return launchProvisionVPS(ctx, env, cfg, client) + default: + return nil, &output.UserError{Message: "Unknown target protocol: " + cfg.targetProtocol} + } +} + +func launchProvisionStatic(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client) (*sdk.Server, error) { + subdomain := cfg.subdomain + if subdomain == "" { + subdomain = projectNameFromRemote(detectGitRemote()) + } + + if !env.NonInteractive { + // Prompt for subdomain with default + subPrompt := promptui.Prompt{Label: "Subdomain", Default: subdomain} + if s, err := subPrompt.Run(); err == nil && s != "" { + subdomain = s + } + + // Subdirectory (build output dir) prompt, pre-filled with the detected + // default (e.g. "dist" for Vite). The user can accept, override, or clear it. + subdirPrompt := promptui.Prompt{ + Label: "Subdirectory to deploy from (build output dir)", + Default: cfg.subdirectory, + } + if s, err := subdirPrompt.Run(); err == nil { + cfg.subdirectory = strings.TrimSpace(s) + } + + // SPA routing prompt — preselect the detected value. cfg.spaMode is + // seeded from detection before provisioning, so a detected SPA defaults + // the cursor to "Yes" (the user can still change it). + spaCursor := 0 + spaLabel := "SPA routing (rewrite all paths to index.html)?" + if cfg.spaMode { + spaCursor = 1 + spaLabel += " [detected]" + } + spaPrompt := promptui.Select{ + Label: spaLabel, + Items: []string{"No", "Yes"}, + CursorPos: spaCursor, + } + if idx, _, err := spaPrompt.Run(); err == nil { + cfg.spaMode = idx == 1 + } + } else if subdomain == "" { + return nil, &output.UserError{ + Message: "Subdomain not specified", + Hint: "Pass --subdomain ", + } + } + + env.Status("Provisioning Static Hosting site: %s.deployhq-sites.com...", subdomain) + + req := sdk.ServerCreateRequest{ + Name: subdomain, + ProtocolType: detect.ProtocolStaticHosting, + HostedWebsiteAttributes: &sdk.HostedWebsiteAttributes{ + Subdomain: subdomain, + SPAMode: cfg.spaMode, + Subdirectory: cfg.subdirectory, + }, + } + + server, err := client.CreateServer(ctx, cfg.projectID, req) + if err != nil { + // 429 provisioning rate limit — retryable, distinct from the 422 cap. + if rl := rateLimitLaunchError(err); rl != nil { + return nil, rl + } + var apiErr *sdk.APIError + if errors.As(err, &apiErr) && apiErr.IsValidationError() { + msg := apiErr.Error() + if strings.Contains(strings.ToLower(msg), "subdomain") { + if env.NonInteractive { + return nil, &launchError{ + Reason: reasonSubdomainTaken, + Message: "Subdomain already taken: " + subdomain, + NextStep: "Pass --subdomain to choose a different subdomain", + Details: map[string]string{"subdomain": subdomain}, + } + } + // Interactive: re-prompt for a different subdomain + env.Warn("Subdomain %q is taken. Please choose another.", subdomain) + subPrompt := promptui.Prompt{Label: "Subdomain", Default: subdomain + "-app"} + newSub, promptErr := subPrompt.Run() + if promptErr != nil || newSub == "" { + return nil, &output.UserError{Message: "Subdomain selection cancelled"} + } + cfg.subdomain = newSub + return launchProvisionStatic(ctx, env, cfg, client) + } + } + return nil, err + } + + // Poll until active + server, err = pollProvisioningState(ctx, env, client, cfg.projectID, server, 10*time.Minute) + if err != nil { + return server, err + } + + liveURL := sdk.LiveURL(server) + output.ColorGreen.Fprintf(env.Stderr, "Static Hosting site provisioned: %s\n", liveURL) //nolint:errcheck + return server, nil +} + +// ── Managed VPS size presentation ───────────────────────────────────────────── + +// humanMB renders a RAM figure (in MB) as a friendly "1 GB" / "512 MB" string. +func humanMB(mb int) string { + switch { + case mb >= 1024 && mb%1024 == 0: + return fmt.Sprintf("%d GB", mb/1024) + case mb >= 1024: + return fmt.Sprintf("%.1f GB", float64(mb)/1024) + default: + return fmt.Sprintf("%d MB", mb) + } +} + +// managedSizeTier returns a friendly tier name for a size given its zero-based +// rank among the offered sizes ordered by price (cheapest first), or "" when the +// rank is beyond the named tiers — those fall back to a spec-only label. +func managedSizeTier(rank int) string { + switch rank { + case 0: + return "Starter" + case 1: + return "Standard" + case 2: + return "Plus" + case 3: + return "Pro" + default: + return "" + } +} + +// managedSizeRanks returns the price-rank of each size aligned to the input +// order (0 = cheapest). Ties are broken by original index for stability. O(n²) +// but n is a handful of sizes, so no sort import is warranted. +func managedSizeRanks(sizes []sdk.ManagedHostingSize) []int { + ranks := make([]int, len(sizes)) + for i := range sizes { + rank := 0 + for j := range sizes { + if j == i { + continue + } + if sizes[j].PriceMonthly < sizes[i].PriceMonthly || + (sizes[j].PriceMonthly == sizes[i].PriceMonthly && j < i) { + rank++ + } + } + ranks[i] = rank + } + return ranks +} + +// managedSizeSpecs renders the hardware line for a size, e.g. +// "1 vCPU · 1 GB RAM · 25 GB SSD". Falls back to the API's Description when the +// structured fields are absent. +func managedSizeSpecs(s sdk.ManagedHostingSize) string { + if s.VCPUs > 0 { + return fmt.Sprintf("%d vCPU · %s RAM · %d GB SSD", s.VCPUs, humanMB(s.Memory), s.Disk) + } + return s.Description +} + +// managedSizeLabel renders a human-friendly one-line label for a size in the +// interactive picker, with an optional tier prefix derived from its price rank: +// +// 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. +func managedSizeLabel(s sdk.ManagedHostingSize, rank int) string { + parts := make([]string, 0, 3) + if tier := managedSizeTier(rank); tier != "" { + parts = append(parts, tier) + } + if specs := managedSizeSpecs(s); specs != "" { + parts = append(parts, specs) + } + parts = append(parts, fmt.Sprintf("$%.2f/mo", s.PriceMonthly)) + return fmt.Sprintf("%s (%s)", strings.Join(parts, " · "), s.Slug) +} + +func launchProvisionVPS(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client) (*sdk.Server, error) { + // Resolve region and size defaults + region := cfg.region + size := cfg.size + osImage := cfg.osImage + if osImage == "" { + osImage = "ubuntu-24-04-x64" + } + + var selectedRegion sdk.ManagedHostingRegion + var selectedSize sdk.ManagedHostingSize + var monthlyCostStr string + + // Fetch available regions and sizes + regions, err := client.ListManagedHostingRegions(ctx) + if err != nil { + // Fall back to hardcoded defaults if endpoint not available + if region == "" { + region = "lon1" + } + if size == "" { + size = "s-1vcpu-1gb" + } + monthlyCostStr = "contact support for pricing" + } else { + // Pick region + if region == "" { + if len(regions) > 0 { + // Pick first available region as default + for _, r := range regions { + if r.Available { + selectedRegion = r + region = r.Slug + break + } + } + if region == "" { + region = regions[0].Slug + selectedRegion = regions[0] + } + } else { + region = "lon1" + } + } else { + for _, r := range regions { + if r.Slug == region { + selectedRegion = r + break + } + } + } + + // Fetch sizes + sizes, sErr := client.ListManagedHostingSizes(ctx) + if sErr == nil && len(sizes) > 0 { + if size == "" { + selectedSize = sizes[0] + size = sizes[0].Slug + } else { + for _, s := range sizes { + if s.Slug == size { + selectedSize = s + break + } + } + if selectedSize.Slug == "" { + selectedSize = sizes[0] + } + } + monthlyCostStr = fmt.Sprintf("$%.2f/month", selectedSize.PriceMonthly) + } else { + if size == "" { + size = "s-1vcpu-1gb" + } + monthlyCostStr = "see dashboard for pricing" + } + + // Interactive: allow region/size selection + if !env.NonInteractive { + if len(regions) > 1 { + // Keep a parallel slice of the available regions: the prompt only + // lists those, so the selected index must map back through this + // filtered slice — indexing the full `regions` slice would pick the + // wrong region whenever an unavailable one precedes an available one. + availableRegions := make([]sdk.ManagedHostingRegion, 0, len(regions)) + regionItems := make([]string, 0, len(regions)) + for _, r := range regions { + if r.Available { + availableRegions = append(availableRegions, r) + regionItems = append(regionItems, fmt.Sprintf("%s (%s)", r.Name, r.Slug)) + } + } + regionPrompt := promptui.Select{ + Label: fmt.Sprintf("Region [%s]", region), + Items: regionItems, + } + if rIdx, _, rErr := regionPrompt.Run(); rErr == nil && rIdx < len(availableRegions) { + region = availableRegions[rIdx].Slug + selectedRegion = availableRegions[rIdx] + } + } + + sizes, sErr := client.ListManagedHostingSizes(ctx) + if sErr == nil && len(sizes) > 1 { + ranks := managedSizeRanks(sizes) + sizeItems := make([]string, len(sizes)) + for i, s := range sizes { + sizeItems[i] = managedSizeLabel(s, ranks[i]) + } + sizePrompt := promptui.Select{ + Label: fmt.Sprintf("Size [%s]", size), + Items: sizeItems, + } + if sIdx, _, sErr2 := sizePrompt.Run(); sErr2 == nil { + selectedSize = sizes[sIdx] + size = selectedSize.Slug + monthlyCostStr = fmt.Sprintf("$%.2f/month", selectedSize.PriceMonthly) + } + } + } + } + + // Cost confirmation gate + regionName := selectedRegion.Name + if regionName == "" { + regionName = region + } + sizeDisplay := size + if selectedSize.Slug != "" { + if specs := managedSizeSpecs(selectedSize); specs != "" { + sizeDisplay = fmt.Sprintf("%s (%s)", specs, selectedSize.Slug) + } + } + env.Status("") + env.Status("Managed VPS configuration:") + env.Status(" Region: %s", regionName) + env.Status(" Size: %s", sizeDisplay) + env.Status(" OS: %s", osImage) + env.Status(" Cost: %s", managedVPSCostDescription(monthlyCostStr)) + env.Status("") + + if !cfg.acceptCost { + if env.NonInteractive { + return nil, &launchError{ + Reason: reasonAcceptCostRequired, + Message: "Provisioning a Managed VPS requires --accept-cost (" + managedVPSAcknowledgePhrase() + ")", + NextStep: "Add --accept-cost to acknowledge that a Managed VPS is " + managedVPSAcknowledgePhrase(), + Details: map[string]string{ + "monthly_cost": monthlyCostStr, + "region": region, + "size": size, + }, + } + } + + // Interactive cost confirm + confirmPrompt := promptui.Select{ + Label: fmt.Sprintf("Provision a Managed VPS (%s)? Continue?", managedVPSCostDescription(monthlyCostStr)), + Items: []string{"No", "Yes, provision VPS"}, + } + idx, _, cErr := confirmPrompt.Run() + if cErr != nil || idx == 0 { + return nil, &output.UserError{Message: "VPS provisioning cancelled"} + } + } + + serverName := size + "-" + region + if cfg.projectID != "" { + serverName = cfg.projectID + "-vps" + } + + env.Status("Provisioning Managed VPS...") + req := sdk.ServerCreateRequest{ + Name: serverName, + ProtocolType: detect.ProtocolManagedVPS, + Region: region, + Size: size, + OSImage: osImage, + } + + server, err := client.CreateServer(ctx, cfg.projectID, req) + if err != nil { + // 429 provisioning rate limit — retryable, distinct from the 422 cap. + if rl := rateLimitLaunchError(err); rl != nil { + return nil, rl + } + return nil, err + } + + // Poll until active + server, err = pollProvisioningState(ctx, env, client, cfg.projectID, server, 15*time.Minute) + if err != nil { + return server, err + } + + ipAddr := "" + if server.ManagedVPS != nil { + ipAddr = server.ManagedVPS.IPAddress + } + output.ColorGreen.Fprintf(env.Stderr, "Managed VPS provisioned: IP %s\n", ipAddr) //nolint:errcheck + return server, nil +} + +// provisionPollInitialBackoff is the first poll delay in pollProvisioningState. +// It is a variable (not a constant) only as a test seam — tests shrink it to +// ~1ms so the provisioning→active sequence runs without real waits. +var provisionPollInitialBackoff = 5 * time.Second + +// pollProvisioningState polls GET /projects/:id/servers/:id until the managed +// resource reaches "active" or "error". It prints a progress message on the +// first call and an "async, safe to Ctrl-C" hint. +func pollProvisioningState(ctx context.Context, env *output.Envelope, client *sdk.Client, projectID string, server *sdk.Server, maxWait time.Duration) (*sdk.Server, error) { + if sdk.IsProvisioningActive(server) { + return server, nil + } + + env.Status("Provisioning is async — safe to Ctrl-C (resource will continue provisioning).") + env.Status("Waiting for active status...") + + deadline := time.Now().Add(maxWait) + backoff := provisionPollInitialBackoff + const maxBackoff = 30 * time.Second + + for { + select { + case <-ctx.Done(): + return server, ctx.Err() + case <-time.After(backoff): + } + + updated, err := client.GetServerProvisioningState(ctx, projectID, server.Identifier) + if err != nil { + // Terminate immediately on non-retryable errors (401/403/404) to + // avoid burning the full timeout on a permanent failure. + var apiErr *sdk.APIError + if errors.As(err, &apiErr) { + switch apiErr.StatusCode { + case http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound: + return server, &launchError{ + Reason: reasonProvisionFailed, + Message: fmt.Sprintf("Provisioning poll failed (status %d): %s", apiErr.StatusCode, apiErr.Error()), + NextStep: "Check your credentials and project access, then retry.", + } + } + } + // Transient error: keep polling + env.Warn("Poll error (will retry): %v", err) + continue + } + server = updated + + status := sdk.ProvisioningStatus(server) + env.Status(" Provisioning status: %s", status) + + if sdk.IsProvisioningActive(server) { + return server, nil + } + if strings.EqualFold(status, "error") { + return server, &launchError{ + Reason: reasonProvisionFailed, + Message: "Provisioning failed (status: error)", + NextStep: "Check the DeployHQ dashboard for error details, or run 'dhq servers delete' to remove the failed resource", + } + } + + if time.Now().After(deadline) { + return server, &launchError{ + Reason: reasonProvisionFailed, + Message: fmt.Sprintf("Provisioning timed out after %s", maxWait), + NextStep: "The resource may still be provisioning. Check status with: dhq servers show -p " + projectID + " " + server.Identifier, + } + } + + // Exponential backoff capped at maxBackoff + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + } +} + +// ── Build command ───────────────────────────────────────────────────────────── + +// launchApplyBuildCommand sets the detected build command on the project so the +// first Static Hosting deploy publishes the built output (dist/public/…) rather +// than unbuilt sources. It mirrors the web onboarding wizard +// (Onboarding::ProjectCreator), which creates project build commands via +// POST /projects/:id/build_commands — no separate build environment is needed. +func launchApplyBuildCommand(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, detection detect.Result) { + if len(detection.BuildCommands) == 0 { + return + } + // Don't clobber or duplicate: if the project already has build commands + // (an idempotent re-run, or a reused --project), leave them as-is. + if existing, err := client.ListBuildCommands(ctx, cfg.projectID, nil); err == nil && len(existing) > 0 { + return + } + + // Create each step as its own command (e.g. "Install dependencies", then + // "Build") so the project's build pipeline matches the web wizard rather + // than a single collapsed shell line. + for _, step := range detection.BuildCommands { + if step.Command == "" { + continue + } + desc := step.Description + if desc == "" { + desc = step.Command + } + if r := []rune(desc); len(r) > 100 { + desc = string(r[:100]) + } + env.Status("Setting build command from detection: %s", step.Command) + if _, err := client.CreateBuildCommand(ctx, cfg.projectID, sdk.BuildCommandCreateRequest{ + Command: step.Command, + Description: desc, + TemplateName: step.TemplateName, + HaltOnError: step.HaltOnError, + }); err != nil { + // Non-fatal: the site still provisions, but without a build step the + // first deploy may publish unbuilt sources — make the fix discoverable. + env.Warn("Could not set the detected build command (%q): %v", step.Command, err) + env.Warn("Add it manually: dhq build-commands create -p %s --command %q", cfg.projectID, step.Command) + } + } +} + +// launchApplyStaticExtras applies the detected excluded-file and build-cache +// patterns to the project, mirroring the web onboarding wizard's suggested +// config so the first static deploy is clean and fast. Best-effort and +// idempotent: it skips patterns that already exist, and any failure is +// non-fatal (the deploy still works, just without the optimisation). +func launchApplyStaticExtras(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, detection detect.Result) { + if len(detection.ExcludedFiles) > 0 { + existing := map[string]bool{} + if list, err := client.ListExcludedFiles(ctx, cfg.projectID, nil); err == nil { + for _, e := range list { + existing[e.Path] = true + } + } + for _, path := range detection.ExcludedFiles { + if path == "" || existing[path] { + continue + } + if _, err := client.CreateExcludedFile(ctx, cfg.projectID, sdk.ExcludedFileCreateRequest{Path: path}); err != nil { + env.Logger.Write("could not add excluded file %q: %v", path, err) + } + } + } + + if len(detection.BuildCacheFiles) > 0 { + existing := map[string]bool{} + if list, err := client.ListBuildCacheFiles(ctx, cfg.projectID); err == nil { + for _, b := range list { + existing[b.Path] = true + } + } + for _, path := range detection.BuildCacheFiles { + if path == "" || existing[path] { + continue + } + if _, err := client.CreateBuildCacheFile(ctx, cfg.projectID, sdk.BuildCacheFileCreateRequest{Path: path}); err != nil { + env.Logger.Write("could not add build cache file %q: %v", path, err) + } + } + } +} + +// ── Deploy ──────────────────────────────────────────────────────────────────── + +// resolveLaunchRevision picks the end_revision for the launch deploy. With a +// branch set it resolves THAT branch's tip — resolveLatestRevision only knows the +// repository default, so pairing its SHA with a different Branch would deploy the +// wrong commit. Returns "" to let the backend resolve the branch/repo HEAD from +// the Branch field (e.g. when the branch tip can't be looked up). +func resolveLaunchRevision(ctx context.Context, env *output.Envelope, client *sdk.Client, cfg launchConfig) string { + if cfg.branch != "" { + if branches, err := client.ListBranches(ctx, cfg.projectID, nil); err == nil { + return branches[cfg.branch] + } + return "" + } + rev, err := resolveLatestRevision(ctx, client, cfg.projectID) + if err != nil { + env.Warn("Could not fetch latest revision, deploying HEAD: %v", err) + return "" + } + return rev +} + +func launchDeploy(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, server *sdk.Server) (*sdk.Deployment, string, error) { + env.Status("Deploying...") + + req := sdk.DeploymentCreateRequest{ + ParentIdentifier: server.Identifier, + EndRevision: resolveLaunchRevision(ctx, env, client, cfg), + Branch: cfg.branch, + } + + dep, err := client.CreateDeployment(ctx, cfg.projectID, req) + if err != nil { + return nil, "", err + } + + env.Status("Deployment %s queued", dep.Identifier) + + // Watch the deployment + if err := watchDeployment(ctx, client, env, cfg.projectID, dep.Identifier); err != nil { + return dep, "", err + } + + liveURL := sdk.LiveURL(server) + return dep, liveURL, nil +} + +// ── Failure / cleanup ───────────────────────────────────────────────────────── + +func launchDeployFailureCleanup(ctx context.Context, env *output.Envelope, cfg launchConfig, client *sdk.Client, server *sdk.Server) { + env.Status("") + env.Warn("Deploy failed. Your %s %q is still running%s.", cfg.targetProtocol, server.Name, managedRunningCostTail()) + env.Status("To remove it: dhq servers delete -p %s %s", cfg.projectID, server.Identifier) + env.Status("To re-deploy: dhq deploy -p %s -s %s", cfg.projectID, server.Identifier) + + if cfg.cleanupOnFailure { + env.Status("--cleanup-on-failure set: deleting server %s...", server.Identifier) + if delErr := client.DeleteServer(ctx, cfg.projectID, server.Identifier); delErr != nil { + env.Warn("Could not delete server: %v", delErr) + } else { + env.Status("Server %s deleted.", server.Identifier) + } + } +} + +// ── Persist ─────────────────────────────────────────────────────────────────── + +func launchPersistConfig(env *output.Envelope, cfg launchConfig, server *sdk.Server) { + path := config.ProjectConfigPath() + _ = config.Set(path, "project", cfg.projectID) + if server != nil { + _ = config.Set(path, "server", server.Identifier) + } + _ = config.Set(path, "target", cfg.targetProtocol) + env.Status("Settings saved to %s", path) +} + +// launchClearStaleProjectConfig removes the launch-persisted project/server/ +// target from .deployhq.toml after the saved project is found to no longer +// exist, so subsequent runs — and other commands that read `project` — start +// clean rather than tripping over the dead reference. Best-effort. +func launchClearStaleProjectConfig(_ *output.Envelope) { + path := config.ProjectConfigPath() + _ = config.Unset(path, "project") + _ = config.Unset(path, "server") + _ = config.Unset(path, "target") +} + +// ── Git helpers ─────────────────────────────────────────────────────────────── + +// runGitCommand runs a git sub-command and returns the trimmed stdout output. +// Returns an error when the command fails or produces no output. +func runGitCommand(args ...string) (string, error) { + cmd := exec.Command("git", args...) + out, err := cmd.Output() + if err != nil { + return "", err + } + result := strings.TrimSpace(string(out)) + if result == "" { + return "", fmt.Errorf("git %s: empty output", strings.Join(args, " ")) + } + return result, nil +} + +// ── Error helpers ───────────────────────────────────────────────────────────── + +// writeLaunchError emits a structured JSON error (in --json mode) or a human +// error (plain mode) and returns the underlying error for exit-code purposes. +func writeLaunchError(env *output.Envelope, cfg launchConfig, reason string, err error) error { + if !env.WantsJSON() { + return err + } + + // Build structured error payload using errors.As so wrapped errors keep their + // metadata (cheap correctness fix). + var le *launchError + isLaunchErr := errors.As(err, &le) + + code := reason + message := err.Error() + nextStep := "" + details := map[string]string{} + retryable := false + + if isLaunchErr { + // The wrapped error's own Reason is authoritative — a rate_limited or + // subdomain_taken error surfaced through a generic call site (e.g. + // provision_failed) must keep its true reason and retryable flag. + if le.Reason != "" { + code = le.Reason + } + message = le.Message + nextStep = le.NextStep + retryable = le.Retryable + for k, v := range le.Details { + details[k] = v + } + } + + type structuredErr struct { + Error string `json:"error"` + Reason string `json:"reason"` + Retryable bool `json:"retryable"` + NextStep string `json:"next_step,omitempty"` + Details map[string]string `json:"details,omitempty"` + } + + resp := &output.Response{ + OK: false, + Data: structuredErr{ + Error: message, + Reason: code, + Retryable: retryable, + NextStep: nextStep, + Details: details, + }, + } + _ = env.WriteJSON(resp) + return err +} diff --git a/internal/commands/launch_detect_test.go b/internal/commands/launch_detect_test.go new file mode 100644 index 0000000..b18ebf4 --- /dev/null +++ b/internal/commands/launch_detect_test.go @@ -0,0 +1,125 @@ +package commands + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/deployhq/deployhq-cli/internal/detect" + "github.com/deployhq/deployhq-cli/pkg/sdk" + "github.com/stretchr/testify/assert" +) + +func TestResolveLaunchRevision_BranchResolvesBranchTip(t *testing.T) { + // With --branch set, the deploy must use THAT branch's tip — not the repo + // default from /latest_revision (which would deploy the wrong commit). + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.URL.Path, "/repository/branches"): + _, _ = w.Write([]byte(`{"main":"defaultsha","feature-x":"featuresha"}`)) + case strings.HasSuffix(r.URL.Path, "/latest_revision"): + t.Errorf("must not call latest_revision when a branch is set") + _, _ = w.Write([]byte(`{"ref":"defaultsha"}`)) + } + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + got := resolveLaunchRevision(t.Context(), env, newTestClient(t, srv), + launchConfig{projectID: "p", branch: "feature-x"}) + assert.Equal(t, "featuresha", got) +} + +func TestResolveLaunchRevision_NoBranchUsesLatest(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/latest_revision") { + _, _ = w.Write([]byte(`{"ref":"defaultsha"}`)) + return + } + _, _ = w.Write([]byte(`{}`)) + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + got := resolveLaunchRevision(t.Context(), env, newTestClient(t, srv), launchConfig{projectID: "p"}) + assert.Equal(t, "defaultsha", got) +} + +func TestResolveLaunchRevision_UnknownBranchOmitsRevision(t *testing.T) { + // Branch not in the list → return "" so the backend resolves the branch HEAD, + // rather than sending a stale/wrong SHA. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"main":"defaultsha"}`)) + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + got := resolveLaunchRevision(t.Context(), env, newTestClient(t, srv), + launchConfig{projectID: "p", branch: "does-not-exist"}) + assert.Equal(t, "", got) +} + +func TestDetectionResultFromAPI_MapsAllFields(t *testing.T) { + resp := &sdk.DetectionResponse{ + Stack: "spa_vite_react", + SuggestedProtocol: "static_hosting", + StaticHosting: sdk.DetectionStaticHosting{RootPath: "dist", SPAMode: true}, + BuildCommands: []sdk.DetectionBuildCommand{ + {Command: "npm install"}, + {Command: "npm run build"}, + }, + } + + r := detectionResultFromAPI(resp) + assert.Equal(t, detect.Framework("spa_vite_react"), r.Framework) + assert.Equal(t, "static_hosting", r.SuggestedProtocol) + assert.Equal(t, "dist", r.OutputDir) + assert.True(t, r.SPA) + // Build steps are preserved individually, not collapsed into one command. + if assert.Len(t, r.BuildCommands, 2) { + assert.Equal(t, "npm install", r.BuildCommands[0].Command) + assert.Equal(t, "npm run build", r.BuildCommands[1].Command) + } +} + +func TestLaunchDetect_UsesRemoteWhenAvailable(t *testing.T) { + // Spec-validating client: the request the CLI sends must conform to the + // /detection contract, and the mapped response drives the recommendation. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/detection", r.URL.Path) + _, _ = w.Write([]byte(`{"stack":"rails","suggested_protocol":"managed_vps",` + + `"static_hosting":{"eligibility":"requires_runtime","confidence":"none"},"build_commands":[]}`)) + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + client := newSpecValidatingClient(t, srv) + got := launchDetect(t.Context(), env, client, t.TempDir()) + + assert.Equal(t, detect.ProtocolManagedVPS, got.SuggestedProtocol) + assert.Equal(t, detect.Framework("rails"), got.Framework) +} + +func TestLaunchDetect_FallsBackToLocalOnRemoteError(t *testing.T) { + // When the endpoint errors, detection falls back to the local heuristic + // silently. A local Gemfile must still yield the managed_vps suggestion. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "Gemfile"), []byte("gem 'rails'\n"), 0o600); err != nil { + t.Fatal(err) + } + + env, _, _ := testLaunchEnvelope() + client := newTestClient(t, srv) // plain client: the 500 reaches the fallback path + got := launchDetect(t.Context(), env, client, dir) + + assert.Equal(t, detect.ProtocolManagedVPS, got.SuggestedProtocol, + "must fall back to local detection (Gemfile → managed_vps)") +} diff --git a/internal/commands/launch_explain.go b/internal/commands/launch_explain.go new file mode 100644 index 0000000..f2372c3 --- /dev/null +++ b/internal/commands/launch_explain.go @@ -0,0 +1,50 @@ +package commands + +import ( + "context" + + "github.com/deployhq/deployhq-cli/internal/assist" + "github.com/deployhq/deployhq-cli/internal/output" + "github.com/deployhq/deployhq-cli/pkg/sdk" +) + +// explainLaunchFailure offers a local-AI diagnosis of a just-failed deployment, +// reusing the same Ollama-backed assistant as `dhq assist` (which already +// gathers the failed step's logs as context). It closes the failure loop without +// a round-trip to the DeployHQ API — nothing leaves the machine. +// +// It is INTERACTIVE-ONLY and best-effort: +// - In non-interactive / --json mode it does nothing; the structured +// launchError is the machine-readable output and must not be cluttered. +// - When Ollama isn't running it prints a one-line tip pointing at `dhq assist`. +// - Any error (context gathering, streaming) is swallowed — the diagnosis is +// an aid, never a gate. +func explainLaunchFailure(ctx context.Context, env *output.Envelope, client *sdk.Client, projectID string) { + if projectID == "" || env == nil || env.NonInteractive || env.WantsJSON() || !env.IsTTY { + return + } + + ollama := assist.NewOllamaClient() + if !ollama.IsAvailable(ctx) { + env.Status("") + env.Status("Tip: run 'dhq assist -p %s' for a local AI diagnosis of this failure (requires Ollama).", projectID) + return + } + + ac, err := assist.GatherContext(ctx, client, projectID) + if err != nil { + return + } + + const question = "The most recent deployment just failed. Using the failed step's logs, " + + "explain the most likely root cause in 2-3 sentences, then give the exact fix " + + "(the commands to run or the DeployHQ setting to change). Be concise and specific." + messages := assist.BuildMessages(ac.FormatContext(), question) + + env.Status("") + output.ColorCyan.Fprint(env.Stderr, "AI diagnosis (local Ollama):\n") //nolint:errcheck + if streamErr := ollama.ChatStream(ctx, messages, env.Stderr); streamErr != nil { + env.Logger.Write("ollama diagnosis failed: %v", streamErr) + } + env.Status("") +} diff --git a/internal/commands/launch_test.go b/internal/commands/launch_test.go new file mode 100644 index 0000000..bb0c9c1 --- /dev/null +++ b/internal/commands/launch_test.go @@ -0,0 +1,1398 @@ +package commands + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/deployhq/deployhq-cli/internal/detect" + "github.com/deployhq/deployhq-cli/internal/output" + "github.com/deployhq/deployhq-cli/pkg/sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ── helpers ─────────────────────────────────────────────────────────────────── + +// testLaunchEnvelope creates a non-TTY, non-interactive Envelope backed by buffers. +func testLaunchEnvelope() (*output.Envelope, *bytes.Buffer, *bytes.Buffer) { + var stdout, stderr bytes.Buffer + env := &output.Envelope{ + Stdout: &stdout, + Stderr: &stderr, + Logger: &output.Logger{}, + IsTTY: false, + NonInteractive: true, + JSONMode: false, + } + return env, &stdout, &stderr +} + +// testLaunchEnvelopeJSON creates a JSON-mode Envelope for --json tests. +func testLaunchEnvelopeJSON() (*output.Envelope, *bytes.Buffer, *bytes.Buffer) { + env, stdout, stderr := testLaunchEnvelope() + env.JSONMode = true + return env, stdout, stderr +} + +// newTestClient returns an SDK client wired to the given httptest server. +func newTestClient(t *testing.T, srv *httptest.Server) *sdk.Client { + t.Helper() + c, err := sdk.New("test", "u@e.com", "k", + sdk.WithBaseURL(srv.URL), + sdk.WithHTTPClient(srv.Client()), + ) + require.NoError(t, err) + return c +} + +// ── Unit: resolveLaunchConfig ───────────────────────────────────────────────── + +func TestResolveLaunchConfig_FlagStaticSetsProtocol(t *testing.T) { + // --static must set targetProtocol without cliCtx involvement + cfg := resolveLaunchConfig(true, false, "myapp", "", "", "", "", false, false, false) + assert.Equal(t, "static_hosting", cfg.targetProtocol) + assert.Equal(t, "myapp", cfg.subdomain) +} + +func TestResolveLaunchConfig_FlagVPSSetsProtocol(t *testing.T) { + cfg := resolveLaunchConfig(false, true, "", "lon1", "s-1vcpu-1gb", "main", "", true, false, false) + assert.Equal(t, "managed_vps", cfg.targetProtocol) + assert.Equal(t, "lon1", cfg.region) + assert.Equal(t, "s-1vcpu-1gb", cfg.size) + assert.True(t, cfg.acceptCost) +} + +func TestResolveLaunchConfig_NoFlagsMeansEmptyProtocol(t *testing.T) { + cfg := resolveLaunchConfig(false, false, "", "", "", "", "", false, false, false) + assert.Equal(t, "", cfg.targetProtocol, "no flag → protocol empty, resolved later via detection/prompt") +} + +func TestResolveLaunchConfig_DryRunFlag(t *testing.T) { + cfg := resolveLaunchConfig(false, true, "", "", "", "", "", false, false, true) + assert.True(t, cfg.dryRun) +} + +// ── Unit: projectNameFromRemote ─────────────────────────────────────────────── + +func TestProjectNameFromRemote_SSHStyle(t *testing.T) { + name := projectNameFromRemote("git@github.com:acme/my-app.git") + assert.Equal(t, "my-app", name) +} + +func TestProjectNameFromRemote_HTTPS(t *testing.T) { + name := projectNameFromRemote("https://github.com/acme/example-repo.git") + assert.Equal(t, "example-repo", name) +} + +func TestProjectNameFromRemote_Empty(t *testing.T) { + name := projectNameFromRemote("") + assert.Equal(t, "my-app", name) +} + +func TestProjectNameFromRemote_NoExtension(t *testing.T) { + name := projectNameFromRemote("https://bitbucket.org/team/service") + assert.Equal(t, "service", name) +} + +// ── auth_required structured error ──────────────────────────────────────────── +// +// The full no-credentials flow in launchEnsureAuth depends on the OS keyring +// (auth.LoadByAccount), so it cannot be unit-tested deterministically across +// machines. We verify the observable contract instead: a non-interactive auth +// failure surfaces as a structured auth_required error (and launchEnsureAuth +// never attempts a headless signup in non-interactive mode — see launch.go). +// Flag registration is covered separately by TestLaunchCommandFlagSet. + +func TestLaunchAuthRequired_JSONReason(t *testing.T) { + env, stdout, _ := testLaunchEnvelopeJSON() + authErr := &output.AuthError{Message: "Not authenticated"} + + result := writeLaunchError(env, launchConfig{}, reasonAuthRequired, authErr) + assert.Equal(t, authErr, result, "must return the original error for exit-code purposes") + + var resp map[string]interface{} + require.NoError(t, json.NewDecoder(stdout).Decode(&resp)) + assert.Equal(t, false, resp["ok"]) + data := resp["data"].(map[string]interface{}) + assert.Equal(t, reasonAuthRequired, data["reason"]) + assert.Contains(t, data["error"].(string), "Not authenticated") +} + +// ── Build command application (static) ─────────────────────────────────────── + +func TestLaunchApplyBuildCommand_CreatesViaBuildCommandsEndpoint(t *testing.T) { + // The detected build command must be created through POST /build_commands + // with a {build_command: {command}} body — not the old /build_configs path — + // so the first static deploy publishes built output, not unbuilt sources. + var postPath, postBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/build_commands"): + _, _ = w.Write([]byte("[]")) // no existing build commands + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/build_commands"): + postPath = r.URL.Path + b, _ := io.ReadAll(r.Body) + postBody = string(b) + _, _ = w.Write([]byte(`{"identifier":"bc-1","command":"npm run build"}`)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + client := newSpecValidatingClient(t, srv) + cfg := launchConfig{projectID: "my-app", targetProtocol: "static_hosting"} + detection := detect.Result{BuildCommands: []detect.BuildCommandStep{{Command: "npm run build"}}, OutputDir: "dist"} + + launchApplyBuildCommand(t.Context(), env, cfg, client, detection) + + assert.Contains(t, postPath, "/projects/my-app/build_commands") + assert.Contains(t, postBody, `"build_command"`) + assert.Contains(t, postBody, `"command":"npm run build"`) + assert.NotContains(t, postBody, "build_config", "must not use the wrong build_config payload") +} + +func TestLaunchApplyBuildCommand_SkipsWhenBuildCommandsExist(t *testing.T) { + // An idempotent re-run / reused --project must not duplicate build commands. + postCalled := false + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/build_commands"): + _, _ = w.Write([]byte(`[{"identifier":"bc-existing","command":"npm run build"}]`)) + case r.Method == http.MethodPost: + postCalled = true + t.Errorf("must not POST when build commands already exist") + } + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + client := newSpecValidatingClient(t, srv) + cfg := launchConfig{projectID: "my-app"} + launchApplyBuildCommand(t.Context(), env, cfg, client, detect.Result{BuildCommands: []detect.BuildCommandStep{{Command: "npm run build"}}}) + + assert.False(t, postCalled) +} + +func TestLaunchApplyBuildCommand_EmptyCommandIsNoop(t *testing.T) { + // No detected build command → no HTTP calls at all. + called := false + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + t.Errorf("no request expected for an empty build command: %s %s", r.Method, r.URL.Path) + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + launchApplyBuildCommand(t.Context(), env, launchConfig{projectID: "p"}, newSpecValidatingClient(t, srv), detect.Result{}) + assert.False(t, called) +} + +func TestLaunchApplyBuildCommand_ListErrorStillCreates(t *testing.T) { + // If listing existing build commands fails, the guard must NOT skip — it's + // best-effort, so we still attempt to create the detected command. + postCalled := false + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/build_commands"): + w.WriteHeader(http.StatusInternalServerError) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/build_commands"): + postCalled = true + _, _ = w.Write([]byte(`{"identifier":"bc-1","command":"npm run build"}`)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + launchApplyBuildCommand(t.Context(), env, launchConfig{projectID: "p"}, newSpecValidatingClient(t, srv), detect.Result{BuildCommands: []detect.BuildCommandStep{{Command: "npm run build"}}}) + assert.True(t, postCalled, "list error must not prevent the create attempt") +} + +func TestLaunchApplyBuildCommand_CreateErrorWarnsWithHint(t *testing.T) { + // A failed create is non-fatal but must surface a discoverable manual hint — + // otherwise the static site silently deploys unbuilt sources. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/build_commands"): + _, _ = w.Write([]byte("[]")) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/build_commands"): + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"error":"command is invalid"}`)) + } + })) + defer srv.Close() + + env, _, stderr := testLaunchEnvelope() + // Must not panic / must return normally despite the API error. + launchApplyBuildCommand(t.Context(), env, launchConfig{projectID: "my-app"}, newSpecValidatingClient(t, srv), + detect.Result{BuildCommands: []detect.BuildCommandStep{{Command: "npm run build"}}}) + + warn := stderr.String() + assert.Contains(t, warn, "dhq build-commands create", "must point the user at the manual fix") + assert.Contains(t, warn, "npm run build") +} + +func TestLaunchApplyBuildCommand_TruncatesLongDescription(t *testing.T) { + // Description is capped at 100 runes (mirrors the web wizard) while the full + // command is preserved. + longCmd := strings.Repeat("a", 150) + var body string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/build_commands"): + _, _ = w.Write([]byte("[]")) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/build_commands"): + b, _ := io.ReadAll(r.Body) + body = string(b) + _, _ = w.Write([]byte(`{"identifier":"bc-1"}`)) + } + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + launchApplyBuildCommand(t.Context(), env, launchConfig{projectID: "p"}, newSpecValidatingClient(t, srv), + detect.Result{BuildCommands: []detect.BuildCommandStep{{Command: longCmd}}}) + + var parsed struct { + BuildCommand struct { + Command string `json:"command"` + Description string `json:"description"` + } `json:"build_command"` + } + require.NoError(t, json.Unmarshal([]byte(body), &parsed)) + assert.Equal(t, 150, len([]rune(parsed.BuildCommand.Command)), "command preserved in full") + assert.Equal(t, 100, len([]rune(parsed.BuildCommand.Description)), "description capped at 100 runes") +} + +// TestLaunchStatic_ProvisionThenBuildCommand_Integration exercises the static +// branch as the orchestrator runs it: provision the Static Hosting server (Step +// 8), then apply the detected build command (Step 9), against one routed server. +// It proves the build command lands on /build_commands during a real static +// launch — the core regression the P1 finding flagged. +func TestLaunchStatic_ProvisionThenBuildCommand_Integration(t *testing.T) { + var serverCreated, buildCmdPath, buildCmdBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/servers"): + b, _ := io.ReadAll(r.Body) + serverCreated = string(b) + // Return an already-active static server so provisioning doesn't poll. + _, _ = w.Write([]byte(`{"identifier":"srv-1","protocol_type":"static_hosting",` + + `"static_hosting":{"url":"https://my-app.deployhq-sites.com","subdomain":"my-app","status":"active"}}`)) + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/build_commands"): + _, _ = w.Write([]byte("[]")) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/build_commands"): + buildCmdPath = r.URL.Path + b, _ := io.ReadAll(r.Body) + buildCmdBody = string(b) + _, _ = w.Write([]byte(`{"identifier":"bc-1","command":"npm run build"}`)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + client := newSpecValidatingClient(t, srv) + cfg := launchConfig{ + projectID: "my-app", + targetProtocol: "static_hosting", + subdomain: "my-app", + subdirectory: "dist", + } + detection := detect.Result{BuildCommands: []detect.BuildCommandStep{{Command: "npm run build"}}, OutputDir: "dist"} + + // Step 8: provision the static server. + server, err := launchProvisionStatic(t.Context(), env, cfg, client) + require.NoError(t, err) + require.NotNil(t, server) + assert.Equal(t, "srv-1", server.Identifier) + assert.Contains(t, serverCreated, "static_hosting") + assert.Contains(t, serverCreated, `"subdomain":"my-app"`) + + // Step 9: apply the detected build command (the static-only branch). + launchApplyBuildCommand(t.Context(), env, cfg, client, detection) + assert.Contains(t, buildCmdPath, "/projects/my-app/build_commands") + assert.Contains(t, buildCmdBody, `"command":"npm run build"`) + assert.NotContains(t, buildCmdBody, "build_config") +} + +// ── Provisioning poll (provisioning → active / error) ──────────────────────── + +// shrinkPollBackoff makes pollProvisioningState's first delay ~instant for the +// duration of the test, restoring the production value afterwards. +func shrinkPollBackoff(t *testing.T) { + t.Helper() + old := provisionPollInitialBackoff + provisionPollInitialBackoff = time.Millisecond + t.Cleanup(func() { provisionPollInitialBackoff = old }) +} + +// pollStateServer returns an httptest server whose GET /servers/:id responses +// walk the given status sequence (clamping on the last one), plus a counter. +func pollStateServer(t *testing.T, statuses []string) (*httptest.Server, *int) { + t.Helper() + polls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || !strings.Contains(r.URL.Path, "/servers/") { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + return + } + idx := polls + if idx >= len(statuses) { + idx = len(statuses) - 1 + } + polls++ + _, _ = fmt.Fprintf(w, `{"identifier":"srv-1","protocol_type":"static_hosting",`+ + `"static_hosting":{"url":"https://my-app.deployhq-sites.com","subdomain":"my-app","status":%q}}`, statuses[idx]) + })) + t.Cleanup(srv.Close) + return srv, &polls +} + +func TestPollProvisioningState_ProvisioningThenActive(t *testing.T) { + // The poll loop must keep polling through "provisioning" and terminate as + // soon as the resource reports "active" — the core async-provisioning path + // `dhq launch` relies on for both Static Hosting and Managed VPS. + shrinkPollBackoff(t) + srv, polls := pollStateServer(t, []string{"provisioning", "provisioning", "active"}) + + env, _, _ := testLaunchEnvelope() + client := newSpecValidatingClient(t, srv) + pending := &sdk.Server{ + Identifier: "srv-1", + ProtocolType: "static_hosting", + StaticHosting: &sdk.StaticHostingInfo{Status: "provisioning"}, + } + + got, err := pollProvisioningState(t.Context(), env, client, "my-app", pending, 10*time.Second) + require.NoError(t, err) + assert.True(t, sdk.IsProvisioningActive(got), "must return the active server") + assert.Equal(t, 3, *polls, "must poll exactly until the first active status") +} + +// (The already-active short-circuit is covered by the pre-existing +// TestPollProvisioningState_AlreadyActive_NoRequests further down.) + +func TestPollProvisioningState_ErrorStatusFailsWithProvisionReason(t *testing.T) { + // A resource that lands in "error" must stop polling and surface a + // structured provision_failed error — not spin until the timeout. + shrinkPollBackoff(t) + srv, polls := pollStateServer(t, []string{"provisioning", "error"}) + + env, _, _ := testLaunchEnvelope() + client := newSpecValidatingClient(t, srv) + pending := &sdk.Server{ + Identifier: "srv-1", + ProtocolType: "static_hosting", + StaticHosting: &sdk.StaticHostingInfo{Status: "provisioning"}, + } + + _, err := pollProvisioningState(t.Context(), env, client, "my-app", pending, 10*time.Second) + require.Error(t, err) + var le *launchError + require.ErrorAs(t, err, &le) + assert.Equal(t, reasonProvisionFailed, le.Reason) + assert.Equal(t, 2, *polls, "must stop on the first error status") +} + +// ── Managed VPS size presentation ──────────────────────────────────────────── + +func TestHumanMB(t *testing.T) { + assert.Equal(t, "1 GB", humanMB(1024)) + assert.Equal(t, "2 GB", humanMB(2048)) + assert.Equal(t, "512 MB", humanMB(512)) + assert.Equal(t, "1.5 GB", humanMB(1536)) +} + +func TestManagedSizeRanksAndTiers(t *testing.T) { + // Deliberately out of price order to prove ranking is by price, not position. + sizes := []sdk.ManagedHostingSize{ + {Slug: "mid", PriceMonthly: 12}, + {Slug: "cheap", PriceMonthly: 6}, + {Slug: "dear", PriceMonthly: 24}, + } + ranks := managedSizeRanks(sizes) + assert.Equal(t, []int{1, 0, 2}, ranks) + + assert.Equal(t, "Starter", managedSizeTier(0)) + assert.Equal(t, "Standard", managedSizeTier(1)) + assert.Equal(t, "Plus", managedSizeTier(2)) + assert.Equal(t, "Pro", managedSizeTier(3)) + assert.Equal(t, "", managedSizeTier(4), "ranks beyond named tiers fall back to spec-only") +} + +func TestManagedSizeLabel(t *testing.T) { + s := sdk.ManagedHostingSize{Slug: "s-1vcpu-1gb", VCPUs: 1, Memory: 1024, Disk: 25, PriceMonthly: 6} + label := managedSizeLabel(s, 0) + assert.Contains(t, label, "Starter") + assert.Contains(t, label, "1 vCPU") + assert.Contains(t, label, "1 GB RAM") + assert.Contains(t, label, "25 GB SSD") + assert.Contains(t, label, "$6.00/mo") + assert.Contains(t, label, "(s-1vcpu-1gb)", "slug stays visible for --size discoverability") + + // Missing structured specs → fall back to the API Description, no tier crash. + bare := sdk.ManagedHostingSize{Slug: "x", Description: "custom", PriceMonthly: 9} + assert.Contains(t, managedSizeLabel(bare, 9), "custom") +} + +// ── rate_limited (429 provisioning rate limit) ─────────────────────────────── + +func TestRateLimitLaunchError_Mapping(t *testing.T) { + // 429 with Retry-After → retryable rate_limited error carrying the backoff. + withRA := rateLimitLaunchError(&sdk.APIError{StatusCode: http.StatusTooManyRequests, RetryAfter: 30}) + require.NotNil(t, withRA) + assert.Equal(t, reasonRateLimited, withRA.Reason) + assert.True(t, withRA.Retryable) + assert.Equal(t, "30", withRA.Details["retry_after"]) + assert.Contains(t, withRA.NextStep, "30s") + + // 429 without Retry-After → still retryable, no retry_after detail. + noRA := rateLimitLaunchError(&sdk.APIError{StatusCode: http.StatusTooManyRequests}) + require.NotNil(t, noRA) + assert.True(t, noRA.Retryable) + _, hasRA := noRA.Details["retry_after"] + assert.False(t, hasRA) + + // A 422 cap is NOT a rate limit — must fall through (nil). + assert.Nil(t, rateLimitLaunchError(&sdk.APIError{StatusCode: http.StatusUnprocessableEntity})) + // A non-API error is not a rate limit either. + assert.Nil(t, rateLimitLaunchError(errors.New("boom"))) +} + +func TestLaunchRateLimited_JSONReason(t *testing.T) { + // A rate_limited error surfaced through the generic provision_failed call + // site must keep its true reason and retryable flag in --json output — + // agents branch on `reason` + `retryable`, not the call site. + env, stdout, _ := testLaunchEnvelopeJSON() + rl := rateLimitLaunchError(&sdk.APIError{StatusCode: http.StatusTooManyRequests, RetryAfter: 12}) + require.NotNil(t, rl) + + _ = writeLaunchError(env, launchConfig{}, reasonProvisionFailed, rl) + + var resp map[string]interface{} + require.NoError(t, json.NewDecoder(stdout).Decode(&resp)) + data := resp["data"].(map[string]interface{}) + assert.Equal(t, reasonRateLimited, data["reason"], "reason must be rate_limited, not the provision_failed call site") + assert.Equal(t, true, data["retryable"]) + details := data["details"].(map[string]interface{}) + assert.Equal(t, "12", details["retry_after"]) +} + +// ── Integration: accept_cost_required ──────────────────────────────────────── + +func TestLaunchVPS_AcceptCostRequired_NonInteractive(t *testing.T) { + // Non-interactive VPS provisioning without --accept-cost must return + // accept_cost_required — never silently charge the user. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/profile": + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "account": map[string]interface{}{ + "beta_features": true, + "static_hosting_eligible": true, + "managed_vps_eligible": true, + }, + }) + case "/managed_hosting/regions": + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"slug": "lon1", "name": "London", "available": true}, + }) + case "/managed_hosting/sizes": + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"slug": "s-1vcpu-1gb", "description": "1 vCPU / 1 GB", "price_monthly": 6.0}, + }) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{ + targetProtocol: "managed_vps", + projectID: "test-proj", + acceptCost: false, // deliberately absent + } + + server, err := launchProvisionVPS(t.Context(), env, cfg, client) + require.Error(t, err) + assert.Nil(t, server) + + var le *launchError + require.True(t, isLaunchErr(err, &le), "must be a launchError") + assert.Equal(t, reasonAcceptCostRequired, le.Reason) + assert.Contains(t, le.Message, "--accept-cost") + assert.Contains(t, le.NextStep, "--accept-cost") +} + +// ── Integration: accept_cost passes with --accept-cost ──────────────────────── + +func TestLaunchVPS_AcceptCostPresent_ProvisionsCalled(t *testing.T) { + // With --accept-cost the flow should proceed past the cost gate and + // call POST /projects/:id/servers. + provisionCalled := false + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/managed_hosting/regions": + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"slug": "lon1", "name": "London", "available": true}, + }) + case r.Method == http.MethodGet && r.URL.Path == "/managed_hosting/sizes": + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"slug": "s-1vcpu-1gb", "description": "1 vCPU", "price_monthly": 6.0}, + }) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/servers"): + provisionCalled = true + // Return a server in "active" state so polling is skipped + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "identifier": "srv-abc", + "name": "s-1vcpu-1gb-lon1", + "protocol_type": "managed_vps", + "managed_vps": map[string]interface{}{ + "status": "active", + "ip_address": "203.0.113.10", + "region": "lon1", + "size": "s-1vcpu-1gb", + }, + }) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{ + targetProtocol: "managed_vps", + projectID: "test-proj", + acceptCost: true, + region: "lon1", + size: "s-1vcpu-1gb", + } + + server, err := launchProvisionVPS(t.Context(), env, cfg, client) + require.NoError(t, err) + assert.True(t, provisionCalled, "POST /servers must be called when --accept-cost is set") + require.NotNil(t, server) + assert.Equal(t, "srv-abc", server.Identifier) +} + +// ── Integration: repo_unreachable ──────────────────────────────────────────── + +func TestLaunchError_RepoUnreachable(t *testing.T) { + // writeLaunchError with reason=repo_unreachable must surface the reason. + env, _, stderr := testLaunchEnvelope() + err := &output.UserError{ + Message: "No git remote found in this directory", + Hint: "Run: git remote add origin ", + } + result := writeLaunchError(env, launchConfig{}, reasonRepoUnreachable, err) + require.Error(t, result) + assert.Contains(t, stderr.String(), "", "plain mode: just return the error, no JSON emitted") + assert.Equal(t, err, result, "error must pass through unchanged in plain mode") +} + +func TestLaunchError_RepoUnreachable_JSON(t *testing.T) { + env, stdout, _ := testLaunchEnvelopeJSON() + err := &launchError{ + Reason: reasonRepoUnreachable, + Message: "No git remote found", + NextStep: "Run: git remote add origin ", + } + result := writeLaunchError(env, launchConfig{}, reasonRepoUnreachable, err) + require.Error(t, result) + + // JSON must contain the reason field + var resp map[string]interface{} + require.NoError(t, json.NewDecoder(stdout).Decode(&resp)) + assert.Equal(t, false, resp["ok"]) + data, ok := resp["data"].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, reasonRepoUnreachable, data["reason"]) + assert.Contains(t, data["next_step"], "git remote") +} + +// ── Integration: beta_enroll_required ──────────────────────────────────────── + +func TestLaunchBetaEnroll_NonAdminForbidden(t *testing.T) { + // POST /beta/enrollments returns 403 → launchError with beta_enroll_required. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.URL.Path == "/beta/enrollments" { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"error":"admin_required"}`)) //nolint:errcheck + return + } + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{targetProtocol: "static_hosting"} + err := launchEnsureBetaEnrolled(t.Context(), env, cfg, client, "myaccount") + require.Error(t, err) + + var le *launchError + require.True(t, isLaunchErr(err, &le)) + assert.Equal(t, reasonBetaEnrollRequired, le.Reason) + assert.Contains(t, le.NextStep, "admin") + assert.Contains(t, le.Details["admin_required"], "true") +} + +func TestLaunchBetaEnroll_AlreadyEnrolled_Idempotent(t *testing.T) { + // POST /beta/enrollments returns 200 with enrolled:true → no error. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.URL.Path == "/beta/enrollments" { + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "enrolled": true, + "beta_features": true, + }) + return + } + t.Errorf("unexpected: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newTestClient(t, srv) + // An already-enrolled account must succeed idempotently on EnrollBeta, + // regardless of admin status (admin is required only for the first + // not-enrolled→enrolled flip). Verify that round-trip directly. + _, enrollErr := client.EnrollBeta(t.Context(), "static_hosting") + require.NoError(t, enrollErr, "already-enrolled account must succeed idempotently") +} + +// ── Integration: subdomain_taken 422 handling ───────────────────────────────── + +func TestLaunchStatic_SubdomainTaken_NonInteractive(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/servers") { + w.WriteHeader(http.StatusUnprocessableEntity) + w.Write([]byte(`{"errors":["subdomain has already been taken"]}`)) //nolint:errcheck + return + } + t.Errorf("unexpected: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() // NonInteractive=true + + cfg := launchConfig{ + targetProtocol: "static_hosting", + projectID: "test-proj", + subdomain: "taken-subdomain", + } + + server, err := launchProvisionStatic(t.Context(), env, cfg, client) + require.Error(t, err) + assert.Nil(t, server) + + var le *launchError + require.True(t, isLaunchErr(err, &le)) + assert.Equal(t, reasonSubdomainTaken, le.Reason) + assert.Contains(t, le.NextStep, "--subdomain") +} + +// ── Integration: dry-run output ─────────────────────────────────────────────── + +func TestLaunchDryRun_Static_JSON(t *testing.T) { + // --dry-run --static --json must emit the intended action with no side effects. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // No requests should reach the server for a static dry run (no size/region calls needed) + // We may receive the capabilities request but dry run should not provision. + if r.Method == http.MethodGet && r.URL.Path == "/profile" { + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "account": map[string]interface{}{ + "beta_features": true, + "static_hosting_eligible": true, + "managed_vps_eligible": true, + }, + }) + return + } + // POST to any endpoint in dry-run must not happen + if r.Method == http.MethodPost { + t.Errorf("dry-run must not make POST requests: %s %s", r.Method, r.URL.Path) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, stdout, _ := testLaunchEnvelopeJSON() + + caps := &sdk.AccountCapabilities{ + BetaFeatures: true, + StaticHostingEligible: true, + ManagedVPSEligible: true, + } + cfg := launchConfig{ + targetProtocol: "static_hosting", + subdomain: "my-app", + dryRun: true, + } + + err := launchDryRun(t.Context(), env, cfg, client, caps) + require.NoError(t, err) + + var resp map[string]interface{} + require.NoError(t, json.NewDecoder(stdout).Decode(&resp)) + assert.Equal(t, true, resp["ok"]) + data, ok := resp["data"].(map[string]interface{}) + require.True(t, ok) + would, ok := data["would"].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "static_hosting", would["provision"]) + assert.Contains(t, would["subdomain"], "my-app") +} + +func TestLaunchDryRun_VPS_JSON_RequiresAcceptCost(t *testing.T) { + // VPS dry-run without --accept-cost must list it in "requires". + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/managed_hosting/sizes" { + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"slug": "s-1vcpu-1gb", "description": "1 vCPU", "price_monthly": 6.0}, + }) + return + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, stdout, _ := testLaunchEnvelopeJSON() + + caps := &sdk.AccountCapabilities{ + BetaFeatures: true, + ManagedVPSEligible: true, + } + cfg := launchConfig{ + targetProtocol: "managed_vps", + acceptCost: false, // not set + dryRun: true, + } + + err := launchDryRun(t.Context(), env, cfg, client, caps) + require.NoError(t, err) + + var resp map[string]interface{} + require.NoError(t, json.NewDecoder(stdout).Decode(&resp)) + data := resp["data"].(map[string]interface{}) + requires, ok := data["requires"].([]interface{}) + require.True(t, ok) + var requiresStrs []string + for _, r := range requires { + requiresStrs = append(requiresStrs, r.(string)) + } + assert.Contains(t, requiresStrs, "--accept-cost") +} + +// ── Unit: launchError.Error() ───────────────────────────────────────────────── + +func TestLaunchError_ErrorMethod(t *testing.T) { + le := &launchError{ + Reason: reasonAuthRequired, + Message: "Not authenticated", + NextStep: "Set DEPLOYHQ_API_KEY", + } + msg := le.Error() + assert.Contains(t, msg, "Not authenticated") + assert.Contains(t, msg, "Set DEPLOYHQ_API_KEY") +} + +func TestLaunchError_ErrorMethodNoNextStep(t *testing.T) { + le := &launchError{ + Reason: reasonProvisionFailed, + Message: "Provisioning failed", + } + assert.Equal(t, "Provisioning failed", le.Error()) +} + +// ── Integration: plan_limit_reached ────────────────────────────────────────── + +func TestLaunchCheckPlanLimits_StaticIneligible(t *testing.T) { + env, _, _ := testLaunchEnvelope() + caps := &sdk.AccountCapabilities{ + BetaFeatures: true, + StaticHostingEligible: false, // not eligible + ManagedVPSEligible: true, + } + cfg := launchConfig{targetProtocol: "static_hosting"} + err := launchCheckPlanLimits(env, cfg, caps) + require.Error(t, err) + var le *launchError + require.True(t, isLaunchErr(err, &le)) + assert.Equal(t, reasonPlanLimitReached, le.Reason) +} + +func TestLaunchCheckPlanLimits_VPSIneligible(t *testing.T) { + env, _, _ := testLaunchEnvelope() + caps := &sdk.AccountCapabilities{ + BetaFeatures: true, + ManagedVPSEligible: false, + } + cfg := launchConfig{targetProtocol: "managed_vps"} + err := launchCheckPlanLimits(env, cfg, caps) + require.Error(t, err) + var le *launchError + require.True(t, isLaunchErr(err, &le)) + assert.Equal(t, reasonPlanLimitReached, le.Reason) +} + +func TestLaunchCheckPlanLimits_BothEligible_NoError(t *testing.T) { + env, _, _ := testLaunchEnvelope() + caps := &sdk.AccountCapabilities{ + BetaFeatures: true, + StaticHostingEligible: true, + ManagedVPSEligible: true, + } + for _, proto := range []string{"static_hosting", "managed_vps"} { + cfg := launchConfig{targetProtocol: proto} + assert.NoError(t, launchCheckPlanLimits(env, cfg, caps)) + } +} + +// ── Integration: pollProvisioningState already-active fast-path ─────────────── + +func TestPollProvisioningState_AlreadyActive_NoRequests(t *testing.T) { + // If server is already active, no requests should be made. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("unexpected request: %s %s — should not poll when already active", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + server := &sdk.Server{ + Identifier: "srv-active", + ProtocolType: "static_hosting", + StaticHosting: &sdk.StaticHostingInfo{ + Status: "active", + URL: "https://my-app.deployhq-sites.com", + }, + } + + result, err := pollProvisioningState(t.Context(), env, client, "proj", server, 0) + require.NoError(t, err) + assert.Equal(t, "active", sdk.ProvisioningStatus(result)) +} + +// ── Integration: writeLaunchError structured output ─────────────────────────── + +func TestWriteLaunchError_PlainMode_PassesThroughError(t *testing.T) { + env, _, _ := testLaunchEnvelope() // NonInteractive, NOT json + orig := &output.UserError{Message: "something bad"} + result := writeLaunchError(env, launchConfig{}, reasonRepoUnreachable, orig) + assert.Equal(t, orig, result, "plain mode must return the original error unchanged") +} + +func TestWriteLaunchError_JSONMode_EmitsStructuredPayload(t *testing.T) { + env, stdout, _ := testLaunchEnvelopeJSON() + le := &launchError{ + Reason: reasonDeployFailed, + Message: "deploy failed", + NextStep: "check logs", + Details: map[string]string{"server": "srv-123"}, + } + result := writeLaunchError(env, launchConfig{}, reasonDeployFailed, le) + assert.Equal(t, le, result, "JSON mode must still return the original error for exit-code purposes") + + var resp map[string]interface{} + require.NoError(t, json.NewDecoder(stdout).Decode(&resp)) + assert.Equal(t, false, resp["ok"]) + data := resp["data"].(map[string]interface{}) + assert.Equal(t, reasonDeployFailed, data["reason"]) + assert.Equal(t, "deploy failed", data["error"]) + assert.Equal(t, "check logs", data["next_step"]) +} + +// ── Command registration ────────────────────────────────────────────────────── + +func TestLaunchCommandRegistered(t *testing.T) { + cmd := NewRootCmd("test") + launchCmd, _, _ := cmd.Find([]string{"launch"}) + require.NotNil(t, launchCmd, "dhq launch must be registered in root command") + assert.Equal(t, "launch", launchCmd.Name()) +} + +func TestLaunchCommandFlagSet(t *testing.T) { + cmd := NewRootCmd("test") + launchCmd, _, _ := cmd.Find([]string{"launch"}) + require.NotNil(t, launchCmd) + + expectedFlags := []string{ + "static", "vps", "accept-cost", "subdomain", "region", "size", + "branch", "project", "cleanup-on-failure", "non-interactive", "yes", + "interactive", "dry-run", + } + for _, f := range expectedFlags { + assert.NotNil(t, launchCmd.Flags().Lookup(f), "expected flag --%s to be registered", f) + } +} + +// ── Helper ──────────────────────────────────────────────────────────────────── + +// isLaunchErr tests whether err (or any wrapped error) is a *launchError and +// assigns it to le if so. Uses errors.As so wrapped launchErrors are found too. +func isLaunchErr(err error, le **launchError) bool { + if err == nil { + return false + } + return errors.As(err, le) +} + +// ── Idempotent re-run — two launches → exactly ONE POST /servers ─────── + +func TestLaunchIdempotent_SecondRunSkipsProvision(t *testing.T) { + // Simulate a re-run where the server was already persisted in cfg.serverID. + // The flow must call GET /projects/:id/servers/:id to verify the existing + // server and skip POST /servers entirely. + + provisionCalls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/servers/srv-existing"): + // Server exists and is active. + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "identifier": "srv-existing", + "name": "my-static-site", + "protocol_type": "static_hosting", + "static_hosting": map[string]interface{}{ + "status": "active", + "url": "https://my-app.deployhq-sites.com", + "subdomain": "my-app", + }, + }) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/servers"): + provisionCalls++ + t.Errorf("POST /servers must NOT be called on a re-run with an existing server") + w.WriteHeader(http.StatusInternalServerError) + default: + // Allow any other reads (project, deploy, etc.) — they are not under test here. + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{ + targetProtocol: "static_hosting", + projectID: "proj-abc", + serverID: "srv-existing", // already persisted + } + + // Call GetServerProvisioningState directly — this is what runLaunch does in the + // idempotency check. Verify it returns successfully without calling CreateServer. + existing, err := client.GetServerProvisioningState(t.Context(), cfg.projectID, cfg.serverID) + require.NoError(t, err) + assert.Equal(t, "srv-existing", existing.Identifier) + assert.Equal(t, 0, provisionCalls, "no POST /servers should have been made") + _ = env // env is wired but not exercised in this unit test +} + +// ── --dry-run must not call POST /beta/enrollments ───────────────────── + +func TestLaunchDryRun_NoBetaEnroll(t *testing.T) { + // When --dry-run is set, NO POST to /beta/enrollments must happen even if + // caps.BetaFeatures is false. + enrollCalled := false + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.URL.Path == "/beta/enrollments" { + enrollCalled = true + t.Errorf("--dry-run must NOT POST /beta/enrollments (no side effects)") + w.WriteHeader(http.StatusOK) + return + } + // Allow caps reads + if r.Method == http.MethodGet && r.URL.Path == "/profile" { + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "account": map[string]interface{}{ + "beta_features": false, // not enrolled + "static_hosting_eligible": false, + "managed_vps_eligible": false, + }, + }) + return + } + // Sizes endpoint for cost estimate + if r.Method == http.MethodGet && r.URL.Path == "/managed_hosting/sizes" { + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"slug": "s-1vcpu-1gb", "description": "1 vCPU", "price_monthly": 6.0}, + }) + return + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelopeJSON() + + // Dry-run calls launchDryRun directly — beta enrollment is in runLaunch, not in + // launchDryRun, so calling launchDryRun directly with caps.BetaFeatures=false + // must never trigger enrollment. + caps := &sdk.AccountCapabilities{BetaFeatures: false} + cfg := launchConfig{ + targetProtocol: "managed_vps", + dryRun: true, + acceptCost: true, + } + + err := launchDryRun(t.Context(), env, cfg, client, caps) + require.NoError(t, err) + assert.False(t, enrollCalled, "--dry-run must not POST /beta/enrollments") +} + +// ── dhq servers create managed_vps without --accept-cost → error ─────── + +func TestServersCreate_ManagedVPS_RequiresAcceptCost_NonInteractive(t *testing.T) { + // In non-interactive / non-TTY mode, creating a managed_vps without + // --accept-cost must return a UserError before any API call is made. + + apiCalled := false + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + apiCalled = true + t.Errorf("no API calls should be made when accept-cost guard fires: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + // Build a minimal cliCtx backed by the test server. + origCtx := cliCtx + defer func() { cliCtx = origCtx }() + + cmd := NewRootCmd("test") + // Look up the servers create command + serversCmd, _, _ := cmd.Find([]string{"servers"}) + require.NotNil(t, serversCmd) + createCmd, _, _ := serversCmd.Find([]string{"create"}) + require.NotNil(t, createCmd) + + // Verify the --accept-cost flag is registered + assert.NotNil(t, createCmd.Flags().Lookup("accept-cost"), + "--accept-cost flag must be registered on servers create") + assert.False(t, apiCalled, "no API calls expected before flag check") +} + +// ── Provision failure with --cleanup-on-failure → issues a DELETE ────── + +func TestLaunchProvisionFailure_CleanupOnFailure_DeletesCalled(t *testing.T) { + // When a server is partially provisioned (returned by CreateServer) but + // pollProvisioningState returns an error, and --cleanup-on-failure is set, + // launchDeployFailureCleanup must call DELETE /projects/:id/servers/:id. + deleteCalled := false + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodDelete && strings.HasSuffix(r.URL.Path, "/servers/srv-partial"): + deleteCalled = true + w.WriteHeader(http.StatusNoContent) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{ + targetProtocol: "managed_vps", + projectID: "proj-abc", + cleanupOnFailure: true, + } + + // Simulate a partially-created server (e.g. API returned a server object + // but provisioning timed out). + partialServer := &sdk.Server{ + Identifier: "srv-partial", + ProtocolType: "managed_vps", + Name: "my-vps", + ManagedVPS: &sdk.ManagedVPSInfo{ + Status: "provisioning", + }, + } + + // Call the cleanup function directly — this is the path runLaunch takes on + // provision failure when --cleanup-on-failure is set. + launchDeployFailureCleanup(t.Context(), env, cfg, client, partialServer) + assert.True(t, deleteCalled, "DELETE /servers/:id must be called when --cleanup-on-failure is set") +} + +// ── Repo-connect failure is terminal before provision ────────────────── + +func TestLaunchEnsureProject_RepoConnectFailure_IsTerminal(t *testing.T) { + // When the only project exists but CreateRepository returns an error, + // launchEnsureProject must return a repo_unreachable launchError — never + // proceed to provision. + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/projects": + // Return one project so it gets auto-selected + json.NewEncoder(w).Encode([]map[string]interface{}{ //nolint:errcheck + {"name": "my-app", "permalink": "my-app"}, + }) + case r.Method == http.MethodGet && r.URL.Path == "/projects/my-app": + // Project has no repo connected + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "name": "my-app", + "permalink": "my-app", + }) + case r.Method == http.MethodPost && r.URL.Path == "/projects/my-app/repository": + // Simulate repo connectivity failure + w.WriteHeader(http.StatusUnprocessableEntity) + w.Write([]byte(`{"errors":["repository not accessible"]}`)) //nolint:errcheck + default: + t.Logf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{ + targetProtocol: "static_hosting", + branch: "main", + } + + _, err := launchEnsureProject(t.Context(), env, cfg, client, "git@github.com:acme/my-app.git") + require.Error(t, err) + + var le *launchError + require.True(t, isLaunchErr(err, &le), "must be a launchError") + assert.Equal(t, reasonRepoUnreachable, le.Reason) +} + +// ── Public deploy key surfaced after repo connect (private-repo support) ────── + +func TestLaunchEnsureRepo_SurfacesPublicDeployKey(t *testing.T) { + // After connecting a repository, the project's PUBLIC deploy key must be + // surfaced so a private repo can be granted read access before the clone. + const pubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABexamplekey deployhq-my-app" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/projects/my-app": + // Project with no repo connected yet, carrying its public deploy key. + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "name": "my-app", + "permalink": "my-app", + "public_key": pubKey, + }) + case r.Method == http.MethodPost && r.URL.Path == "/projects/my-app/repository": + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "scm_type": "git", "url": "git@github.com:acme/my-app.git", "branch": "main", + }) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, stderr := testLaunchEnvelope() // NonInteractive: surfaces without pausing + + cfg := launchConfig{targetProtocol: "static_hosting", branch: "main"} + // Non-GitHub host so the gh-CLI auto-install path is skipped and we + // deterministically exercise the surface-the-key fallback. + err := launchEnsureRepo(t.Context(), env, cfg, client, "my-app", "git@git.example.com:acme/my-app.git") + require.NoError(t, err) + assert.Contains(t, stderr.String(), pubKey, "public deploy key must be surfaced after connecting the repo") +} + +func TestLaunchEnsureRepo_AlreadyConnected_NoKeyNoise(t *testing.T) { + // When the repo is already connected, launchEnsureRepo returns early and must + // NOT re-print the deploy key (avoids noise on idempotent re-runs). + const pubKey = "ssh-rsa AAAAB3already-connected-key" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/projects/my-app" { + json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck + "name": "my-app", "permalink": "my-app", "public_key": pubKey, + "repository": map[string]interface{}{ + "scm_type": "git", "url": "git@github.com:acme/my-app.git", "branch": "main", + }, + }) + return + } + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, stderr := testLaunchEnvelope() + + err := launchEnsureRepo(t.Context(), env, launchConfig{}, client, "my-app", "git@github.com:acme/my-app.git") + require.NoError(t, err) + assert.NotContains(t, stderr.String(), pubKey, "no deploy-key output when the repo is already connected") +} + +// ── Stale persisted project (.deployhq.toml) handling ───────────────────────── + +func TestLaunchEnsureProject_StaleConfigProjectFallsThrough(t *testing.T) { + // A project persisted in .deployhq.toml that no longer exists (404) must be + // detected and skipped — NOT surfaced as the misleading "could not connect + // repository" error — and the flow must fall through to normal resolution. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/projects/gone": + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"status":"not_found","error_code":"record_not_found"}`)) //nolint:errcheck + case r.Method == http.MethodGet && r.URL.Path == "/projects": + w.Write([]byte("[]")) //nolint:errcheck // no projects → falls through to "no project specified" + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, stderr := testLaunchEnvelope() // NonInteractive + + cfg := launchConfig{projectID: "gone", projectFromConfig: true, targetProtocol: "static_hosting"} + _, err := launchEnsureProject(t.Context(), env, cfg, client, "git@git.example.com:acme/app.git") + + require.Error(t, err) + assert.Contains(t, stderr.String(), "no longer exists", "must warn about the stale saved project") + assert.NotContains(t, err.Error(), "Could not connect repository", "must not surface the misleading repo error") +} + +func TestLaunchEnsureProject_StaleFlagProjectErrorsClearly(t *testing.T) { + // An explicit --project that doesn't exist is a user error: fail clearly, + // don't silently fall through to creating a new project. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/projects/typo" { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"status":"not_found"}`)) //nolint:errcheck + return + } + t.Errorf("unexpected request (must not list/create): %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newTestClient(t, srv) + env, _, _ := testLaunchEnvelope() + + cfg := launchConfig{projectID: "typo", projectFromConfig: false} // came from --project + _, err := launchEnsureProject(t.Context(), env, cfg, client, "git@git.example.com:acme/app.git") + + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +// ── Failure diagnosis (explainLaunchFailure) — interactive-only ─────────────── + +func TestExplainLaunchFailure_NonInteractiveIsNoOp(t *testing.T) { + // Non-interactive must never auto-diagnose, prompt, or call Ollama — the + // structured launchError is the machine-readable output. Expect no output. + env, stdout, stderr := testLaunchEnvelope() // NonInteractive=true, IsTTY=false + explainLaunchFailure(t.Context(), env, nil, "my-app") + assert.Empty(t, stdout.String()) + assert.Empty(t, stderr.String()) +} + +func TestExplainLaunchFailure_EmptyProjectIsNoOp(t *testing.T) { + env, _, stderr := testLaunchEnvelope() + explainLaunchFailure(t.Context(), env, nil, "") + assert.Empty(t, stderr.String()) +} + +func TestInstallDeployKeyViaGH_RejectsNonGitHubURL(t *testing.T) { + // A non-GitHub URL is rejected before any `gh` invocation, so callers fall + // back to surfacing the key for manual installation. + err := installDeployKeyViaGH("git@gitlab.com:acme/app.git", "ssh-ed25519 AAAA key", "DeployHQ - app") + require.Error(t, err) + assert.Contains(t, err.Error(), "GitHub") +} + +// ── Email-verification gate handling ────────────────────────────────────────── + +func TestIsEmailVerificationRequired(t *testing.T) { + assert.True(t, isEmailVerificationRequired(&sdk.APIError{StatusCode: http.StatusForbidden, Message: "email_verification_required"})) + // Critical: a generic 403 must NOT be treated as the email-verification case. + assert.False(t, isEmailVerificationRequired(&sdk.APIError{StatusCode: http.StatusForbidden, Message: "AccessDenied"})) + assert.False(t, isEmailVerificationRequired(&sdk.APIError{StatusCode: http.StatusNotFound, Message: "email_verification_required"})) + assert.False(t, isEmailVerificationRequired(errors.New("boom"))) +} + +func TestHandleEmailVerificationRequired_NonInteractiveFailsCleanly(t *testing.T) { + env, _, _ := testLaunchEnvelope() // NonInteractive + err := handleEmailVerificationRequired(t.Context(), env, nil, "user@example.com") + require.Error(t, err) + var le *launchError + require.True(t, errors.As(err, &le)) + assert.Equal(t, reasonEmailVerificationRequired, le.Reason) + assert.True(t, le.Retryable, "the user can retry after verifying") +} + +func TestLaunchGetCaps_EmailVerificationRequired_StructuredFail(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"error":"email_verification_required"}`)) //nolint:errcheck + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() // NonInteractive → no wait, clean structured fail + _, capsKnown, err := launchGetCaps(t.Context(), env, newTestClient(t, srv)) + require.Error(t, err) + assert.False(t, capsKnown) + var le *launchError + require.True(t, errors.As(err, &le)) + assert.Equal(t, reasonEmailVerificationRequired, le.Reason) +} + +func TestLaunchGetCaps_GenericForbidden_Propagates(t *testing.T) { + // Anti-regression: a real authorization 403 must surface as a real error — + // NOT be swallowed or mistaken for the email-verification case. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"error":"AccessDenied"}`)) //nolint:errcheck + })) + defer srv.Close() + + env, _, _ := testLaunchEnvelope() + _, capsKnown, err := launchGetCaps(t.Context(), env, newTestClient(t, srv)) + require.Error(t, err) + assert.False(t, capsKnown) + var le *launchError + assert.False(t, errors.As(err, &le), "a generic 403 must not become an email_verification launchError") +} + +// testLaunchEnvelope uses io.Discard for the Logger so tests don't need a +// real log file. We define a Logger inline since output.Logger has exported +// fields but the constructor creates a file. +var _ io.Writer = (*bytes.Buffer)(nil) // compile-time check diff --git a/internal/commands/metered.go b/internal/commands/metered.go new file mode 100644 index 0000000..ffa62c2 --- /dev/null +++ b/internal/commands/metered.go @@ -0,0 +1,62 @@ +package commands + +import "strings" + +// meteredResourcesInBeta is the single switch that gates the "free during beta" +// messaging for DeployHQ's metered managed resources (Managed VPS and Static +// Hosting). While true, the CLI presents these resources as free for early +// customers during the beta and frames the listed monthly price as the rate +// that applies once the beta ends. +// +// Flip this to false when managed resources leave beta — every piece of runtime +// CLI copy keyed off it (via the helpers below) updates automatically. +// +// NOTE: the markdown docs (README.md, skills/deployhq/SKILL.md, +// skills/deployhq/references/launch.md) repeat this wording and cannot read this +// variable — update them in the same change when you flip it. +var meteredResourcesInBeta = true + +// managedVPSAcknowledgePhrase describes — without a specific rate — what a user +// is acknowledging when they provision a Managed VPS. Used in flag help and the +// non-interactive cost gate. +func managedVPSAcknowledgePhrase() string { + if meteredResourcesInBeta { + return "free for early customers during beta, billed monthly afterwards" + } + return "billed monthly" +} + +// managedVPSCostDescription renders a Managed VPS's cost given its monthly rate +// (e.g. "$6.00/month"). A rate that does not start with "$" is treated as an +// unknown/fallback phrase. During the beta the description leads with "free +// during beta". +func managedVPSCostDescription(rate string) string { + hasPrice := strings.HasPrefix(rate, "$") + switch { + case meteredResourcesInBeta && hasPrice: + return "free during beta, then " + rate + case meteredResourcesInBeta: + return "free for early customers during beta" + default: + return rate + } +} + +// managedRunningCostTail is appended when warning that a provisioned (but +// not-yet-deployed) Managed VPS is still running and should be cleaned up. +func managedRunningCostTail() string { + if meteredResourcesInBeta { + return " (free during beta)" + } + return " and billable" +} + +// betaFreeSuffix is a light-touch "(free during beta)" qualifier for managed- +// resource copy (Static Hosting or Managed VPS), or "" once metered resources +// are generally available. +func betaFreeSuffix() string { + if meteredResourcesInBeta { + return " (free during beta)" + } + return "" +} diff --git a/internal/commands/openapi_validation_test.go b/internal/commands/openapi_validation_test.go new file mode 100644 index 0000000..a65f84f --- /dev/null +++ b/internal/commands/openapi_validation_test.go @@ -0,0 +1,177 @@ +package commands + +// openapi_validation_test.go — a request-validating http.RoundTripper for tests. +// +// newSpecValidatingClient wraps the usual httptest-backed SDK client with a +// transport that validates every outgoing request against the backend's +// OpenAPI document (testdata/openapi.json, a committed snapshot of the +// backend's /docs.json — refresh with script/update-openapi-fixture.sh). +// +// A request to an undocumented path, or with a body that violates the +// documented schema, fails the SDK call with an "openapi spec violation" +// error — so tests using this client prove the CLI speaks the backend's +// actual contract, not just whatever the hand-written test fake accepts. +// This is exactly the bug class behind the launch build-command P1 (the CLI +// POSTed to /build_configs, a path that does not exist in the API). + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "path/filepath" + "sync" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/deployhq/deployhq-cli/pkg/sdk" +) + +var ( + specRouterOnce sync.Once + specRouter routers.Router + specRouterErr error +) + +// loadSpecRouter loads the committed OpenAPI fixture once per test binary and +// builds a route matcher from it. +func loadSpecRouter() (routers.Router, error) { + specRouterOnce.Do(func() { + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile(filepath.Join("testdata", "openapi.json")) + if err != nil { + specRouterErr = fmt.Errorf("load OpenAPI fixture: %w", err) + return + } + // Tests run against httptest hosts, not the documented server URLs — + // match on path only. + doc.Servers = nil + specRouter, specRouterErr = gorillamux.NewRouter(doc) + }) + return specRouter, specRouterErr +} + +// specValidatingTransport validates each request against the OpenAPI document +// before forwarding it to the wrapped transport. Violations surface as +// transport errors, failing the SDK call loudly. +type specValidatingTransport struct { + wrapped http.RoundTripper + router routers.Router +} + +func (s *specValidatingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Buffer the body: validation consumes it, and the real request still + // needs to send it. + var body []byte + if req.Body != nil { + b, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + body = b + req.Body = io.NopCloser(bytes.NewReader(body)) + } + + route, pathParams, err := s.router.FindRoute(req) + if err != nil { + return nil, fmt.Errorf("openapi spec violation: %s %s is not a documented endpoint: %w", req.Method, req.URL.Path, err) + } + if err := openapi3filter.ValidateRequest(req.Context(), &openapi3filter.RequestValidationInput{ + Request: req, + Route: route, + PathParams: pathParams, + Options: &openapi3filter.Options{ + // Auth is enforced by the backend, not the schema check. + AuthenticationFunc: openapi3filter.NoopAuthenticationFunc, + }, + }); err != nil { + return nil, fmt.Errorf("openapi spec violation: %s %s: %w", req.Method, req.URL.Path, err) + } + + req.Body = io.NopCloser(bytes.NewReader(body)) + return s.wrapped.RoundTrip(req) +} + +// newSpecValidatingClient returns an SDK client wired to the given httptest +// server whose every request is validated against the OpenAPI fixture. +// Prefer this over newTestClient for tests that exercise real API flows. +func newSpecValidatingClient(t *testing.T, srv *httptest.Server) *sdk.Client { + t.Helper() + router, err := loadSpecRouter() + require.NoError(t, err, "OpenAPI fixture must load (refresh with script/update-openapi-fixture.sh)") + + hc := srv.Client() + base := hc.Transport + if base == nil { + base = http.DefaultTransport + } + hc.Transport = &specValidatingTransport{wrapped: base, router: router} + + c, err := sdk.New("test", "u@e.com", "k", + sdk.WithBaseURL(srv.URL), + sdk.WithHTTPClient(hc), + ) + require.NoError(t, err) + return c +} + +// ── Harness self-tests ──────────────────────────────────────────────────────── + +func TestSpecValidatingClient_RejectsUndocumentedEndpoint(t *testing.T) { + // Recreates the original launch build-command P1: POSTing to + // /build_configs (a path that does not exist in the API) must fail at the + // validation layer — the backend is never reached. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("request must be rejected before reaching the server: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newSpecValidatingClient(t, srv) + err := client.Do(t.Context(), "POST", "/projects/my-app/build_configs", + map[string]any{"build_config": map[string]any{"build_commands": "npm run build"}}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "openapi spec violation") + assert.Contains(t, err.Error(), "not a documented endpoint") +} + +func TestSpecValidatingClient_RejectsSchemaViolation(t *testing.T) { + // A documented path with a body missing its required key must also fail: + // POST /projects/:id/servers requires a top-level `server` object. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("request must be rejected before reaching the server: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + client := newSpecValidatingClient(t, srv) + err := client.Do(t.Context(), "POST", "/projects/my-app/servers", + map[string]any{"name": "missing-server-wrapper"}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "openapi spec violation") +} + +func TestSpecValidatingClient_AcceptsDocumentedRequest(t *testing.T) { + // Sanity: a well-formed request passes validation and reaches the server. + reached := false + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reached = true + _, _ = w.Write([]byte(`{"identifier":"bc-1","command":"npm run build"}`)) + })) + defer srv.Close() + + client := newSpecValidatingClient(t, srv) + _, err := client.CreateBuildCommand(t.Context(), "my-app", sdk.BuildCommandCreateRequest{ + Command: "npm run build", + Description: "npm run build", + }) + require.NoError(t, err) + assert.True(t, reached) +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 3dac90c..b16c9bc 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -201,6 +201,7 @@ Support: support@deployhq.com`, newTestAccessCmd(), // Shortcuts + newLaunchCmd(), newDeployCmd(), newRetryCmd(), newRollbackCmd(), diff --git a/internal/commands/servers.go b/internal/commands/servers.go index 8376b12..4f9aa83 100644 --- a/internal/commands/servers.go +++ b/internal/commands/servers.go @@ -16,7 +16,9 @@ func newServersCmd() *cobra.Command { Use: "servers", Aliases: []string{"server", "srv"}, Short: "Manage servers", - Long: `Servers are the deployment targets for a project — the destinations where your code lands. DeployHQ supports many protocols: SSH, FTP/FTPS, Rsync, S3 (and S3-compatible), DigitalOcean, Hetzner Cloud, Heroku, Netlify, and Shopify. Each server pins exactly one protocol. + Long: `Servers are the deployment targets for a project — the destinations where your code lands. DeployHQ supports many protocols: SSH, FTP/FTPS, Rsync, S3 (and S3-compatible), DigitalOcean, Hetzner Cloud, Heroku, Netlify, Shopify, Static Hosting, and Managed VPS. Each server pins exactly one protocol. + +Static Hosting and Managed VPS are managed offerings backed by DeployHQ infrastructure (beta). They require the managed-resources beta to be enabled on your account. Use "dhq launch" for guided one-command provisioning of these offerings. Use these commands to create, configure, and list the servers attached to a project. To deploy to one, see "dhq deploy" or "dhq deployments create".`, } @@ -169,6 +171,14 @@ func newServersCreateCmd() *cobra.Command { var siteID, accessToken string // Shopify var storeURL, themeName string + // Static Hosting (beta) + var subdomain string + var spaMode bool + var subdirectory string + // Managed VPS (beta) + var region, size, osImage string + // Billing guardrail (mirrors the gate in `dhq launch`) + var acceptCost bool cmd := &cobra.Command{ Use: "create", @@ -189,7 +199,15 @@ func newServersCreateCmd() *cobra.Command { # Heroku app dhq servers create -p my-app --name staging --protocol-type heroku \ - --app-name my-app-staging --api-key ''`, + --app-name my-app-staging --api-key '' + + # Static Hosting site (beta — requires managed-resources beta) + dhq servers create -p my-app --name site --protocol-type static_hosting \ + --subdomain my-app --subdirectory dist + + # Managed VPS droplet (beta — requires managed-resources beta) + dhq servers create -p my-app --name vps --protocol-type managed_vps \ + --region lon1 --size s-1vcpu-1gb`, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return &output.UserError{Message: "Server name is required", Hint: "Use --name flag"} @@ -197,7 +215,7 @@ func newServersCreateCmd() *cobra.Command { if protocolType == "" { return &output.UserError{ Message: "Protocol type is required", - Hint: "Use --protocol-type with one of: ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean, hetzner_cloud, heroku, netlify, shopify", + Hint: "Use --protocol-type with one of: ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean, hetzner_cloud, heroku, netlify, shopify, static_hosting, managed_vps", } } @@ -221,8 +239,8 @@ func newServersCreateCmd() *cobra.Command { Username: username, Password: password, // S3 - BucketName: bucketName, - AccessKeyID: accessKeyID, + BucketName: bucketName, + AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, // S3-Compatible CustomEndpoint: customEndpoint, @@ -241,6 +259,18 @@ func newServersCreateCmd() *cobra.Command { // Shopify StoreURL: storeURL, ThemeName: themeName, + // Managed VPS (beta) + Region: region, + Size: size, + OSImage: osImage, + } + // Static Hosting (beta) — nested attributes + if protocolType == "static_hosting" && subdomain != "" { + req.HostedWebsiteAttributes = &sdk.HostedWebsiteAttributes{ + Subdomain: subdomain, + SPAMode: spaMode, + Subdirectory: subdirectory, + } } if cmd.Flags().Changed("port") { req.Port = &port @@ -254,6 +284,27 @@ func newServersCreateCmd() *cobra.Command { env := cliCtx.Envelope + // Cost-acknowledgement guardrail: managed_vps MUST be acknowledged before + // creation (mirrors the gate in `dhq launch --vps`) (Fix 4). The exact + // wording is sourced from managedVPSAcknowledgePhrase() so it tracks the + // beta-vs-GA switch in metered.go. + if protocolType == "managed_vps" && !acceptCost { + if !env.IsTTY || env.NonInteractive || env.JSONMode { + return &output.UserError{ + Message: "Managed VPS creation requires --accept-cost (" + managedVPSAcknowledgePhrase() + ")", + Hint: "Add --accept-cost to acknowledge that a Managed VPS is " + managedVPSAcknowledgePhrase() + ".", + } + } + // Interactive: prompt to confirm + fmt.Fprintf(env.Stderr, "Creating a Managed VPS — %s. Continue? [y/N]: ", managedVPSAcknowledgePhrase()) //nolint:errcheck + reader := bufio.NewReader(os.Stdin) + answer, _ := reader.ReadString('\n') + answer = strings.TrimSpace(strings.ToLower(answer)) + if answer != "y" && answer != "yes" { + return &output.UserError{Message: "Managed VPS creation cancelled"} + } + } + var server *sdk.Server for { var err error @@ -335,7 +386,7 @@ func newServersCreateCmd() *cobra.Command { // Common flags cmd.Flags().StringVar(&name, "name", "", "Server name (required)") - cmd.Flags().StringVar(&protocolType, "protocol-type", "", "Protocol (required): ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean, hetzner_cloud, heroku, netlify, shopify") + cmd.Flags().StringVar(&protocolType, "protocol-type", "", "Protocol (required): ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean, hetzner_cloud, heroku, netlify, shopify, static_hosting, managed_vps") cmd.Flags().StringVar(&serverPath, "path", "", "Server path") cmd.Flags().StringVar(&environment, "environment", "", "Environment name") @@ -376,6 +427,19 @@ func newServersCreateCmd() *cobra.Command { cmd.Flags().StringVar(&storeURL, "store-url", "", "Shopify store URL (shopify)") cmd.Flags().StringVar(&themeName, "theme-name", "", "Shopify theme name (shopify)") + // Static Hosting (beta) — requires managed-resources beta on the account + cmd.Flags().StringVar(&subdomain, "subdomain", "", "Globally unique subdomain under deployhq-sites.com (static_hosting)") + cmd.Flags().BoolVar(&spaMode, "spa-mode", false, "Enable SPA routing: all paths rewrite to index.html (static_hosting)") + cmd.Flags().StringVar(&subdirectory, "subdirectory", "", "Output subdirectory to publish, e.g. dist (static_hosting)") + + // Managed VPS (beta) — requires managed-resources beta on the account + 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)") + + // 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)") + return cmd } diff --git a/internal/commands/signup.go b/internal/commands/signup.go index af1e7cd..6df8145 100644 --- a/internal/commands/signup.go +++ b/internal/commands/signup.go @@ -69,11 +69,12 @@ func newSignupCmd() *cobra.Command { ua := cliUserAgent() req := sdk.SignupRequest{ - Email: email, - Password: password, - AccountName: accountName, - FullName: fullName, - Client: ua, + Email: email, + Password: password, + AccountName: accountName, + FullName: fullName, + Client: "dhq-cli", + TermsAccepted: true, } result, err := sdk.Signup(req, ua, cliCtx.Config.SignupURL()) diff --git a/internal/commands/testdata/openapi.json b/internal/commands/testdata/openapi.json new file mode 100644 index 0000000..bb453b9 --- /dev/null +++ b/internal/commands/testdata/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"DeployHQ API Interactive Documentation","summary":"DeployHQ API Interactive Documentation","description":"# DeployHQ API\n\nDeployHQ offers an API to allow interacting with our platform. You can find in this documentation a list of all the endpoints available and how to use them.\n\nPlease note we also offer a more user-centric API documentation available at [https://www.deployhq.com/support/api](https://www.deployhq.com/support/api).\n\n## Access & Authentication\n\nUsers are provided with an API key which can be found from the \"Security\" page within the \"Settings\" menu. It's a 40 character string which must be used with your username in order to authenticate. All API requests should be sent with HTTP Basic Authentication with your username (email address) and API key.\n\nAll requests should also send the following headers:\n\n- `Accept: application/json`\n- `Content-Type: application/json`\n\nThese requests should be in JSON and all responses received will be returned as JSON.\n\nRequests should be made to `https://.deployhq.com/` replacing `` with the name of your account.\n","termsOfService":"","contact":{"name":"DeployHQ","url":"https://www.deployhq.com","email":"support@deployhq.com"},"version":"1.0.0","favicon":"favicon-no-bg.png"},"servers":[{"url":"https://{account}.deploy.localhost","description":"DeployHQ API (enter your account's permalink)","variables":{"account":{"default":"sg","description":"Your account's permalink (e.g., sg, dhq, etc.)"}}},{"url":"https://{hostname}","description":"Dynamic Server (enter your host)","variables":{"hostname":{"default":"sg.deploy.localhost","description":"Your server host (e.g., sg.deploy.localhost)"}}}],"paths":{"/account":{"get":{"tags":["Account"],"summary":"View account details","description":"Returns the current account's settings including plan, status, and project usage.","operationId":"getAccount","responses":{"200":{"$ref":"#/components/responses/4cecb307e8754b34dda9884728d8e12e"},"404":{"$ref":"#/components/responses/a9f7a2eef628397bbcbe7035eb37a5f4"},"401":{"$ref":"#/components/responses/77bf90a0f411e4f1f1845ea1b605cbfd"},"403":{"$ref":"#/components/responses/c4aeb4c285b3219cd8e7c3666f608fbf"},"500":{"$ref":"#/components/responses/9bc5d36aa3015b1f64c0ca4666c8560f"}},"security":[{"basic":[]}]},"put":{"tags":["Account"],"summary":"Update account settings","description":"Updates account settings. Only account administrators can perform this action.","operationId":"replaceAccount","requestBody":{"$ref":"#/components/requestBodies/ba75be51071603ebe817ff07c7e7fbde"},"responses":{"200":{"$ref":"#/components/responses/de2d3701d04bbc262727cd4c2a9277f9"},"422":{"$ref":"#/components/responses/e0217d05a81378af962648a32fab1c8f"},"404":{"$ref":"#/components/responses/aa5604d80a442449a9492874f89abd13"},"401":{"$ref":"#/components/responses/b64b9dda5839d9d6fe818a046a263c79"},"403":{"$ref":"#/components/responses/04cf39ea1fd36a6073ab0dcc0e6c53ac"},"500":{"$ref":"#/components/responses/b3e3482bf2a2a8ef67b4b7d52c3851c7"}},"security":[{"basic":[]}]},"patch":{"tags":["Account"],"summary":"Update account settings","description":"Updates account settings. Only account administrators can perform this action.","operationId":"updateAccount","requestBody":{"$ref":"#/components/requestBodies/75ef393fb7b8bb0ab84a6a8ea29d77a8"},"responses":{"200":{"$ref":"#/components/responses/4e446e2fed16bfa261780848b4692622"},"422":{"$ref":"#/components/responses/898565f5e28c5fb8109d7948fcfd3e0f"},"404":{"$ref":"#/components/responses/b0ce86f69f822cb5b79fb0295e890a6f"},"401":{"$ref":"#/components/responses/df93cc716f999632872dbbacf3abf63e"},"403":{"$ref":"#/components/responses/110131ccfe6a6853f7c09b5d6bcb01f4"},"500":{"$ref":"#/components/responses/d519c6b94d481da62dddc3e127a19357"}},"security":[{"basic":[]}]}},"/account/billing_status":{"get":{"tags":["Account"],"summary":"Get billing status","description":"Returns billing and subscription status for the account.","operationId":"billingStatusAccountBillingStatus","responses":{"200":{"$ref":"#/components/responses/422869ab4c640e00cd1a469772b3074e"},"401":{"$ref":"#/components/responses/60f43035f1bfa7913cf55de5efcc1090"},"403":{"$ref":"#/components/responses/fb7452d83fe3381bcd79ef0bbdb435e8"},"500":{"$ref":"#/components/responses/458ac5450e3fe2f0efde543d6296f793"}},"security":[{"basic":[]}]}},"/agents":{"get":{"tags":["Network Agents"],"summary":"List network agents","description":"Returns all network agents registered to the current account.","operationId":"listAgents","responses":{"200":{"$ref":"#/components/responses/1e68ced21fa040c1dd867d9e6df28064"},"401":{"$ref":"#/components/responses/440208eaef46ab1dcd07db7d6e329fe1"},"403":{"$ref":"#/components/responses/da11793fbc235d1a85f699ce0803498d"},"500":{"$ref":"#/components/responses/2e963065819657428890fc4e249993fb"}},"security":[{"basic":[]}]},"post":{"tags":["Network Agents"],"summary":"Claim a network agent","description":"Claims an unclaimed network agent using its claim code and registers it to the current account.","operationId":"createAgent","requestBody":{"$ref":"#/components/requestBodies/4fb5a59fad4d7bb77a59fa15f4b1b35a"},"responses":{"200":{"$ref":"#/components/responses/d61e7aa260c32115ae3221df3a6f01ad"},"422":{"$ref":"#/components/responses/4062770b4d4d672e678d7db6064812f9"},"401":{"$ref":"#/components/responses/34bc186eea212b8f81b75e5881502447"},"403":{"$ref":"#/components/responses/4f42fac3e022f61e8fd4a0d5e2ae87a9"},"500":{"$ref":"#/components/responses/abad08ad7bc61525e808a81c0f13ded3"}},"security":[{"basic":[]}]}},"/agents/{id}":{"put":{"tags":["Network Agents"],"summary":"Update a network agent","description":"Updates the name of an existing network agent.","operationId":"replaceAgent","parameters":[{"$ref":"#/components/parameters/65b7c1ef9a672c9e027100beadc77403"}],"requestBody":{"$ref":"#/components/requestBodies/77f7326cbdf8e1ebed7815bfcc1d51c7"},"responses":{"200":{"$ref":"#/components/responses/44aa2f610b8f7b8421108c3f4c12cf5c"},"422":{"$ref":"#/components/responses/062ff3062e226097a9c0918da69f1e6d"},"404":{"$ref":"#/components/responses/b6d0d7107298022ed208958b012f9998"},"401":{"$ref":"#/components/responses/e9ba4acf743bb774c494f00b084ddbb0"},"403":{"$ref":"#/components/responses/2d7d879608f0eab9a63f91b1f89741ec"},"500":{"$ref":"#/components/responses/12a395a00f8c6d0e5d8172d524e5e7aa"}},"security":[{"basic":[]}]},"patch":{"tags":["Network Agents"],"summary":"Update a network agent","description":"Updates the name of an existing network agent.","operationId":"updateAgent","parameters":[{"$ref":"#/components/parameters/65b7c1ef9a672c9e027100beadc77403"}],"requestBody":{"$ref":"#/components/requestBodies/da2cbb8b30ba849e328e7e8a4a75ba93"},"responses":{"200":{"$ref":"#/components/responses/a09cbd3286eb779413bef09e65fe4893"},"422":{"$ref":"#/components/responses/51fd5d359a1194712d337f829f962b65"},"404":{"$ref":"#/components/responses/47316208d5f6fb55b85da2c849b5664b"},"401":{"$ref":"#/components/responses/e0799b05947d2483adfa555b681f0bdd"},"403":{"$ref":"#/components/responses/b626a4bb65dbb86f6c8a406afd4dc43d"},"500":{"$ref":"#/components/responses/a8203c0970525bb578494fb00d3ead2a"}},"security":[{"basic":[]}]},"delete":{"tags":["Network Agents"],"summary":"Delete a network agent","description":"Permanently deletes a network agent from the account.","operationId":"deleteAgent","parameters":[{"$ref":"#/components/parameters/cf3c2621f7f26a86e4f7b3aa67057676"}],"responses":{"200":{"$ref":"#/components/responses/37df2b0d0e8970f776cde5deacded9c8"},"500":{"$ref":"#/components/responses/b3fa34659ed201fe387327b842a20c01"},"404":{"$ref":"#/components/responses/3f8ace65c15b14e8019f391705fb146f"},"401":{"$ref":"#/components/responses/944c9aafa08b8d0393c583154dbbb936"},"403":{"$ref":"#/components/responses/800c5eed6ac88f8ba9cfbce750cd10ea"}},"security":[{"basic":[]}]}},"/agents/{id}/revoke":{"post":{"tags":["Network Agents"],"summary":"Revoke a network agent","description":"Revokes the agent's claim, releasing it from the account so it can be reclaimed.","operationId":"revokeAgent","parameters":[{"$ref":"#/components/parameters/298320f1616036082cc4bd56d72a6c09"}],"responses":{"200":{"$ref":"#/components/responses/98d6a705724146a63894ab044532ba68"},"500":{"$ref":"#/components/responses/0bb9b678ee0c69ad759d875e9e934b9a"},"401":{"$ref":"#/components/responses/d45c28b223f939f2a3d591327412a7bb"},"403":{"$ref":"#/components/responses/136e3c9f4bf7eda5c72882ee9155aaba"}},"security":[{"basic":[]}]}},"/api/v1/signup":{"post":{"tags":["Signup"],"summary":"Create a new DeployHQ account","description":"Registers a new DeployHQ account. No authentication required. Returns API credentials and SSH public key for immediate use. Rate-limited per IP, per email address, and globally; see the RATE_LIMITS constant for thresholds.","operationId":"createSignup","requestBody":{"$ref":"#/components/requestBodies/bfbfad7b4b3f2ac265d765fa45ef1278"},"responses":{"201":{"$ref":"#/components/responses/8cd70662d5b3a511cdc069e568de7519"},"400":{"$ref":"#/components/responses/96e0f2222ed448e80963c2be471bcaca"},"403":{"$ref":"#/components/responses/c7405448fc2cf7b98059648a1d56ac1d"},"409":{"$ref":"#/components/responses/6ad05c548e8d9e323708867f354cad59"},"422":{"$ref":"#/components/responses/7f550ef71b5972aa31712285241f6aaa"},"429":{"$ref":"#/components/responses/9a229ea5f52f2076eff061b748006baf"},"503":{"$ref":"#/components/responses/034eae2e693a0a10d9d9c2ff3e5c6a11"}},"security":[]}},"/beta/enrollments":{"post":{"tags":["Beta"],"summary":"Enroll in managed-resources beta","description":"Enroll the current account in the managed-resources beta.","operationId":"createBetumEnrollment","parameters":[{"$ref":"#/components/parameters/45521a405675361f0a85bc01c3ad4550"}],"requestBody":{"$ref":"#/components/requestBodies/c59cece82991d620e689791e614398d4"},"responses":{"200":{"$ref":"#/components/responses/c7096c3140be9a79a00e32ce6a7c7c82"},"403":{"$ref":"#/components/responses/c902f87ed72080a146ee6f9996f52622"},"422":{"$ref":"#/components/responses/e17f4fa1a958a199085f2dc1fdad83dd"},"500":{"$ref":"#/components/responses/c22d352c9d4fe9ec8b1cf035db82ed85"},"401":{"$ref":"#/components/responses/112315a520da27b3a9dfc07ae419af8e"}},"security":[{"basic":[]}]}},"/detection":{"post":{"tags":["Detection"],"summary":"Detect framework from a file manifest","description":"Detects the project's framework/stack from an uploaded filename listing plus the contents of key manifest files. Returns the canonical stack, a suggested protocol (static_hosting or managed_vps), the static-hosting preset (output directory and SPA mode), and suggested build commands — the same detection pipeline the web onboarding wizard uses. Files not uploaded degrade detection precision gracefully; they never error.","operationId":"createDetection","requestBody":{"$ref":"#/components/requestBodies/9b1548dae94f60bf68534c451ebd29cd"},"responses":{"200":{"$ref":"#/components/responses/139452f9a3f243958dbe303a23530a8e"},"422":{"$ref":"#/components/responses/9a64f6184295d730187ea618080729f8"},"401":{"$ref":"#/components/responses/de9b8ab82a149f778567eadb59f5f41c"},"403":{"$ref":"#/components/responses/4397d437356851d3f129a431acccb997"},"500":{"$ref":"#/components/responses/9f943f615d616f45117912ac838e97ca"}},"security":[{"basic":[]}]}},"/global_config_files":{"get":{"tags":["Global Config Files"],"summary":"List global config files","description":"","operationId":"listGlobalConfigFiles","responses":{"200":{"$ref":"#/components/responses/f7c3e110b62b70cb68cf29342e951d54"},"401":{"$ref":"#/components/responses/22078fb50c8b46789c8fc6305b0b38ca"},"403":{"$ref":"#/components/responses/e48223afa8931a7a3d64d068025f60c7"},"500":{"$ref":"#/components/responses/dbcfbf6009d0d9beee67e5c47b7b8ddd"}},"security":[{"basic":[]}]},"post":{"tags":["Global Config Files"],"summary":"Create new global config file","description":"","operationId":"createGlobalConfigFile","requestBody":{"$ref":"#/components/requestBodies/ebb3ec94049e9b0b61d534e338818fff"},"responses":{"201":{"$ref":"#/components/responses/88ad17e7870813a88be4a352ca057fd9"},"422":{"$ref":"#/components/responses/1a35561477bf2b192cdb0c9ce1a960b8"},"401":{"$ref":"#/components/responses/64c383836fb36c6c1129be83e4423b35"},"403":{"$ref":"#/components/responses/f7e2ba7e7c1be6e7f41d6b401dea3fe0"},"500":{"$ref":"#/components/responses/7bea5ec58c5459de41840d9bbbcbda21"}},"security":[{"basic":[]}]}},"/global_config_files/{id}":{"get":{"tags":["Global Config Files"],"summary":"View global config file","description":"","operationId":"getGlobalConfigFile","parameters":[{"$ref":"#/components/parameters/2e5bd103a88877e6bf0ce64ab017229d"}],"responses":{"200":{"$ref":"#/components/responses/972df02ba99c6e10e00354e9f6254019"},"404":{"$ref":"#/components/responses/0041dea174622a2ce65134553703efa8"},"401":{"$ref":"#/components/responses/acbc286a21d4dd7682f81742b7c31176"},"403":{"$ref":"#/components/responses/a156b4eb95265fc201a7e14ed0aed75f"},"500":{"$ref":"#/components/responses/5ae3acda2adf4fd33eef0b7d8d4a8819"}},"security":[{"basic":[]}]},"put":{"tags":["Global Config Files"],"summary":"Update global config file","description":"","operationId":"replaceGlobalConfigFile","parameters":[{"$ref":"#/components/parameters/2e5bd103a88877e6bf0ce64ab017229d"}],"requestBody":{"$ref":"#/components/requestBodies/d540ff1656b031da1934a828e0d9f9d4"},"responses":{"200":{"$ref":"#/components/responses/24610b5e5d89e6b632585d03fb79572d"},"422":{"$ref":"#/components/responses/e2e530f10f7496aba47df723677a60db"},"404":{"$ref":"#/components/responses/01bed04d50e50e47612e877e1b81a068"},"401":{"$ref":"#/components/responses/a7245ec2d4a466e46f784c0d6e61465d"},"403":{"$ref":"#/components/responses/fa9efe64e2cebb1951dfc144a132c924"},"500":{"$ref":"#/components/responses/8db4f5c702e45976c56c59b5018bf6bd"}},"security":[{"basic":[]}]},"patch":{"tags":["Global Config Files"],"summary":"Update global config file","description":"","operationId":"updateGlobalConfigFile","parameters":[{"$ref":"#/components/parameters/2e5bd103a88877e6bf0ce64ab017229d"}],"requestBody":{"$ref":"#/components/requestBodies/d0c6ef3d21c8923f00ed5f4e6b7dd5e2"},"responses":{"200":{"$ref":"#/components/responses/bb0103bcab8a3870d6f6690aeed10833"},"422":{"$ref":"#/components/responses/cc249b4d017f33853d9e75182a44cfaa"},"404":{"$ref":"#/components/responses/04ebbe0b2c0e1d33d81aa43a9e571027"},"401":{"$ref":"#/components/responses/aa8494a7852dc58c11f47c1bde8b4c3d"},"403":{"$ref":"#/components/responses/7acad67dc92b783a965dc39407b996ad"},"500":{"$ref":"#/components/responses/a3d7206d72f13f8095776ec5b36070c3"}},"security":[{"basic":[]}]},"delete":{"tags":["Global Config Files"],"summary":"Delete global config file","description":"","operationId":"deleteGlobalConfigFile","parameters":[{"$ref":"#/components/parameters/2e5bd103a88877e6bf0ce64ab017229d"}],"responses":{"200":{"$ref":"#/components/responses/dcf706deff01af633f456d3de442c410"},"404":{"$ref":"#/components/responses/f0217d02506c137f2365add167187b2f"},"401":{"$ref":"#/components/responses/b33ef77e9d3e9aaf5410d9a6ef0f2b99"},"403":{"$ref":"#/components/responses/a49852f39158f3e1e8dc5f1b348f0721"},"500":{"$ref":"#/components/responses/f29111476c43e7e874784af8ff035c54"}},"security":[{"basic":[]}]}},"/global_environment_variables":{"get":{"tags":["Global Environment Variables"],"summary":"List global environment variables","description":"Returns all account-level environment variables available to every project.","operationId":"listGlobalEnvironmentVariables","responses":{"200":{"$ref":"#/components/responses/21cef77e5f5f1a427ac5108a0cc015ba"},"401":{"$ref":"#/components/responses/005cd9a54340a236eda7676fa5ffc1b1"},"403":{"$ref":"#/components/responses/254045048ac8aa6e8d334b243ae1eda1"},"500":{"$ref":"#/components/responses/dd05ed70cb4b07c42a9bb55c28ab805b"}},"security":[{"basic":[]}]},"post":{"tags":["Global Environment Variables"],"summary":"Create a global environment variable","description":"Creates a new account-level environment variable. Locked variables cannot be read back after creation.","operationId":"createGlobalEnvironmentVariable","requestBody":{"$ref":"#/components/requestBodies/f5a6a323bfecca789b938f7e87a1d5c4"},"responses":{"201":{"$ref":"#/components/responses/c93e91305c0b06a1611d33f07f7327a2"},"422":{"$ref":"#/components/responses/ac1b06541eb59ebd0a73a832c35d4497"},"401":{"$ref":"#/components/responses/9ac2a5269aab16e2f9f8df9125224d61"},"403":{"$ref":"#/components/responses/765754ac3624e77bca9b473bea6488c6"},"500":{"$ref":"#/components/responses/d7442bdf72ed6e06547ae65e84d5827d"}},"security":[{"basic":[]}]}},"/global_environment_variables/{id}":{"get":{"tags":["Global Environment Variables"],"summary":"Get a global environment variable","description":"Returns the details of a single global environment variable.","operationId":"getGlobalEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"responses":{"200":{"$ref":"#/components/responses/27f03922d6d4db5df55b2b97be10c33b"},"404":{"$ref":"#/components/responses/6d493436b89681dbed187839c41bb8ea"},"401":{"$ref":"#/components/responses/0a4239c40216d5a2f353f4f037c04e35"},"403":{"$ref":"#/components/responses/a0371751682ae603255e8b72189b41b3"},"500":{"$ref":"#/components/responses/7badd6b1ef8cdf7c2c2a476151288b66"}},"security":[{"basic":[]}]},"put":{"tags":["Global Environment Variables"],"summary":"Update a global environment variable","description":"Updates an existing global environment variable's name, value, or locked status.","operationId":"replaceGlobalEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"requestBody":{"$ref":"#/components/requestBodies/3f5c4ba69ba867ecffceff738f82e31e"},"responses":{"200":{"$ref":"#/components/responses/73ce6437d5a29ce889cd12e133c70517"},"422":{"$ref":"#/components/responses/c3c56654a8c7e639ab02116074d76159"},"404":{"$ref":"#/components/responses/91140e610b2c97b3f28e1c2a8c3a4e8a"},"401":{"$ref":"#/components/responses/ba6b80a4f8ec434e6e667c7d6a2d88b9"},"403":{"$ref":"#/components/responses/cdd217cb599c15a555a9bd1c33cf901e"},"500":{"$ref":"#/components/responses/051332e78dda4ac37c4db05956548c17"}},"security":[{"basic":[]}]},"patch":{"tags":["Global Environment Variables"],"summary":"Update a global environment variable","description":"Updates an existing global environment variable's name, value, or locked status.","operationId":"updateGlobalEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"requestBody":{"$ref":"#/components/requestBodies/ee43434d10cecfc2fb93542c24e20746"},"responses":{"200":{"$ref":"#/components/responses/dd06ffe99ab5adac58c10c6ca61907fd"},"422":{"$ref":"#/components/responses/fb0becb80bf1b86e6b38a4abe439b464"},"404":{"$ref":"#/components/responses/6270d98592bfa240578aa3e841f5fab5"},"401":{"$ref":"#/components/responses/f3aa6ad2bcde4af4eb3bfd6d3462464d"},"403":{"$ref":"#/components/responses/7037373a588008f476609c12f3d10dc5"},"500":{"$ref":"#/components/responses/54062a22c232d9bd2628961a5c22f8eb"}},"security":[{"basic":[]}]},"delete":{"tags":["Global Environment Variables"],"summary":"Delete a global environment variable","description":"Permanently removes a global environment variable from the account.","operationId":"deleteGlobalEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"responses":{"204":{"$ref":"#/components/responses/68521ef8d89bf2b26f92bdcee641d000"},"404":{"$ref":"#/components/responses/31642a6088a356d24e689ec59817bc5f"},"401":{"$ref":"#/components/responses/e692b5389e7dac02ff3ba8f88b6d3f8a"},"403":{"$ref":"#/components/responses/8624ed5eb7712c7e665a1c24104988a7"},"500":{"$ref":"#/components/responses/80888d3250596f195b54ab43574f7346"}},"security":[{"basic":[]}]}},"/global_servers":{"get":{"tags":["Global Servers"],"summary":"List global servers","description":"","operationId":"listGlobalServers","responses":{"200":{"$ref":"#/components/responses/6863b278a39de2b027591706f7a16c62"},"401":{"$ref":"#/components/responses/148824641d85fe12340e0de7380308a8"},"403":{"$ref":"#/components/responses/4ff386d2f4a0a9a022e93050c6117166"},"500":{"$ref":"#/components/responses/4989446a8d644bfae1a84c9004fef9d0"}},"security":[{"basic":[]}]},"post":{"tags":["Global Servers"],"summary":"Create a global server","description":"","operationId":"createGlobalServer","requestBody":{"$ref":"#/components/requestBodies/31f971880e92be00ef2b8aad36a25ded"},"responses":{"201":{"$ref":"#/components/responses/c25d44b2c8e2a8c76643bcf1f3f4e1de"},"422":{"$ref":"#/components/responses/aa466175ee0ce28feadec5f3f027776b"},"401":{"$ref":"#/components/responses/6a40c698b55f18e601866db89e3b6966"},"403":{"$ref":"#/components/responses/eeb97b2c1f95075d78e50be037b0e0e5"},"500":{"$ref":"#/components/responses/62f8a79c78df10cc57eef5982faef987"}},"security":[{"basic":[]}]}},"/global_servers/{id}":{"get":{"tags":["Global Servers"],"summary":"View a global server","description":"","operationId":"getGlobalServer","parameters":[{"$ref":"#/components/parameters/63af325434e89dbb89f6fc1772268b53"}],"responses":{"200":{"$ref":"#/components/responses/2f64de8c5825eb2684bc318c474080b1"},"404":{"$ref":"#/components/responses/13bbe0dc031e3e20a9a0eca9031114db"},"401":{"$ref":"#/components/responses/07d608a597e647d0b21a4c80c2b873f8"},"403":{"$ref":"#/components/responses/b83df3066cab73f26ac99097280e5bc7"},"500":{"$ref":"#/components/responses/c4386751da79ca4dada23de8ea493493"}},"security":[{"basic":[]}]},"put":{"tags":["Global Servers"],"summary":"Update a global server","description":"","operationId":"replaceGlobalServer","parameters":[{"$ref":"#/components/parameters/63af325434e89dbb89f6fc1772268b53"}],"requestBody":{"$ref":"#/components/requestBodies/52da00d973fd08285674d3949177e04b"},"responses":{"200":{"$ref":"#/components/responses/b46e979e956f606587a7887ed8f0e3d3"},"422":{"$ref":"#/components/responses/b95398ac496e36061b2730c18293bfba"},"404":{"$ref":"#/components/responses/851f07e2f42527fbec700b887e631cc8"},"401":{"$ref":"#/components/responses/bd4a6677264d1e83f70685c994543d87"},"403":{"$ref":"#/components/responses/f834bbb0aba360645094e0f548990631"},"500":{"$ref":"#/components/responses/07f7ca158567e429b51bd00eb2e08a22"}},"security":[{"basic":[]}]},"patch":{"tags":["Global Servers"],"summary":"Update a global server","description":"","operationId":"updateGlobalServer","parameters":[{"$ref":"#/components/parameters/63af325434e89dbb89f6fc1772268b53"}],"requestBody":{"$ref":"#/components/requestBodies/69896c7191677d1ceb1e373f5508c59f"},"responses":{"200":{"$ref":"#/components/responses/670204086fa9eafdc19bbbacd0c97e95"},"422":{"$ref":"#/components/responses/b77c76fbfee15fd09c07cba3d4788c27"},"404":{"$ref":"#/components/responses/3c7c2cc397b42e5f138f2cab10df9094"},"401":{"$ref":"#/components/responses/d06ad690d7808a5962453150d5e1b7f4"},"403":{"$ref":"#/components/responses/cee78c7e628620bd3086ed191718433f"},"500":{"$ref":"#/components/responses/fc8c90805e8ea4ba6cc1d1369cca1086"}},"security":[{"basic":[]}]},"delete":{"tags":["Global Servers"],"summary":"Delete a global server","description":"","operationId":"deleteGlobalServer","parameters":[{"$ref":"#/components/parameters/63af325434e89dbb89f6fc1772268b53"}],"responses":{"200":{"$ref":"#/components/responses/26dd8e17f89b960c38496a4da9b3233f"},"422":{"$ref":"#/components/responses/55c18e9cf1124ac3783e8ff4a1a562a5"},"404":{"$ref":"#/components/responses/ec2c44ee386502c9a5c3587a5e1599de"},"401":{"$ref":"#/components/responses/0de51377d2776a353ad64646624fd3fd"},"403":{"$ref":"#/components/responses/19666948b8c70341a4398d6270f303c0"},"500":{"$ref":"#/components/responses/639db9f3957950158c8219b32f0523d6"}},"security":[{"basic":[]}]}},"/global_servers/{id}/copy_to_project":{"post":{"tags":["Global Servers"],"summary":"Copy a global server to a project","description":"","operationId":"copyToProjectGlobalServer","parameters":[{"$ref":"#/components/parameters/63af325434e89dbb89f6fc1772268b53"},{"$ref":"#/components/parameters/4f5fca8deca120935a640ae64413387f"}],"responses":{"201":{"$ref":"#/components/responses/af8b454269337bb3ca47bcf91531436d"},"422":{"$ref":"#/components/responses/6a158ae03e3452ab22cff57f419abc62"},"401":{"$ref":"#/components/responses/a60ed1d2212c40f10a39f42c0a72a538"},"403":{"$ref":"#/components/responses/dc58212c5315fa6541ec024807b9a995"},"500":{"$ref":"#/components/responses/1cddba3b906454b4e68d2ecbe9260805"}},"security":[{"basic":[]}]}},"/hosted_resources":{"get":{"tags":["Hosted Resources"],"summary":"List all hosted resources and hosted websites","description":"Returns both managed VPS resources (hosted_resource) and static hosting sites\n(hosted_website) for the authenticated account. The `kind` field discriminates;\ntype-specific fields are populated accordingly.","operationId":"listHostedResources","responses":{"200":{"$ref":"#/components/responses/e503a0299e8fc27de98a101b3bc04f9f"},"401":{"$ref":"#/components/responses/a5d5622d69aa76c4d6b1150c1fd72ed6"},"403":{"$ref":"#/components/responses/7501c12131a155d53e72c02271acfd19"},"500":{"$ref":"#/components/responses/d017964ba2409c8af6dd9567bb44030f"}},"security":[{"basic":[]}]}},"/hosted_resources/{id}":{"get":{"tags":["Hosted Resources"],"summary":"Show a hosted resource or hosted website","description":"Returns a hosted resource (managed VPS) or hosted website (static hosting)\nbased on the identifier. The `kind` field discriminates the response shape.","operationId":"getHostedResource","parameters":[{"$ref":"#/components/parameters/0bb858b19ca0cc95038712ae24f7ae1f"}],"responses":{"200":{"$ref":"#/components/responses/6aacc99e1d8d3f33ef5384a069674c9a"},"404":{"$ref":"#/components/responses/79311822e3af41ecd1fec7b22fd28450"},"401":{"$ref":"#/components/responses/9131337a6e7d318fecd4318f59e1c68b"},"403":{"$ref":"#/components/responses/f749441fc0112edf3d4ca68e0072a62d"},"500":{"$ref":"#/components/responses/aadcda2a77593c815b7892b06a4f905c"}},"security":[{"basic":[]}]}},"/hosted_resources/{id}/retry_provision":{"post":{"tags":["Hosted Resources"],"summary":"Retry provisioning a hosted resource","description":"Retries provisioning for a hosted resource that is in an error state. Resets the status to provisioning and enqueues a new provision job.","operationId":"retryProvisionHostedResourceRetryProvision","parameters":[{"$ref":"#/components/parameters/43beaabd70d1089bd81ac12e4c47beb8"}],"responses":{"200":{"$ref":"#/components/responses/58da531d399ef3aa368bb409a8e00c19"},"422":{"$ref":"#/components/responses/725b497c498ebc6c85626e2dc9dea818"},"401":{"$ref":"#/components/responses/7d37e53c9f6f2d92841901e01d795a3a"},"403":{"$ref":"#/components/responses/13cb46a7734ff7573ab1f7f9831ef530"},"500":{"$ref":"#/components/responses/25e6ba534fa74421fd7321a96f3f95ad"}},"security":[{"basic":[]}]}},"/hosted_resources/{id}/sync":{"post":{"tags":["Hosted Resources"],"summary":"Sync a hosted resource","description":"","operationId":"syncHostedResourceSync","parameters":[{"$ref":"#/components/parameters/3fc4d3fa60dd528d714e5558d68c3191"}],"responses":{"200":{"$ref":"#/components/responses/766544a5cebaff4d4459a2913614e6ee"},"401":{"$ref":"#/components/responses/de41a3b9f570ed3ae5ae21dedf743f7e"},"403":{"$ref":"#/components/responses/7f5097b55fd2c2ef930dc45635ef7428"},"500":{"$ref":"#/components/responses/86a8612b8245b719931252dfff717ec5"}},"security":[{"basic":[]}]}},"/language_versions":{"get":{"tags":["Language Versions"],"summary":"List available language versions","description":"Returns all available language/runtime versions from the build server.","operationId":"listLanguageVersions","responses":{"200":{"$ref":"#/components/responses/f982c6014d71d60e3550a9343849d64a"},"401":{"$ref":"#/components/responses/35bd80927d7138e6be9ba301185a44d4"},"403":{"$ref":"#/components/responses/221f5952ae476f9b1e2a15f79ad98a9e"},"500":{"$ref":"#/components/responses/dadfd016a8dd5bff54bd6bd823ab8512"}},"security":[{"basic":[]}]}},"/managed_hosting/regions":{"get":{"tags":["Managed Hosting"],"summary":"List available regions","description":"Returns available managed VPS regions grouped geographically.","operationId":"listManagedHostingRegions","responses":{"200":{"$ref":"#/components/responses/a9c0a30bf2cd4d11ee77f679f95ccce9"},"401":{"$ref":"#/components/responses/afc0f73097a4e5798b47a498dd5667e0"},"403":{"$ref":"#/components/responses/688c8565397e3d95dc13a0cb16208ae5"},"500":{"$ref":"#/components/responses/fc0775d143d68cf6c5b3d74be9f8bf64"}},"security":[{"basic":[]}]}},"/managed_hosting/sizes":{"get":{"tags":["Managed Hosting"],"summary":"List available sizes","description":"","operationId":"listManagedHostingSizes","responses":{"200":{"$ref":"#/components/responses/16d5bb262d48607dd204ee97a4996ea3"},"401":{"$ref":"#/components/responses/409fb6712a27355c86a96aa07250ad08"},"403":{"$ref":"#/components/responses/a762eb122b1e1c28c274c810ce83f133"},"500":{"$ref":"#/components/responses/b4cf97e43552fdd62376a0b039dd77e3"}},"security":[{"basic":[]}]}},"/profile":{"get":{"tags":["Profile"],"summary":"View your profile","description":"Returns the authenticated user's profile including account details and capability flags.\nThe capability fields (beta_features, static_hosting_eligible, managed_vps_eligible)\nallow any authenticated account member to detect managed-resource eligibility without\nrequiring admin access (GET /account is admin-only and cannot serve this purpose).","operationId":"getProfile","responses":{"200":{"$ref":"#/components/responses/d90c986c9d066985c56797d473730b4d"},"404":{"$ref":"#/components/responses/1d4f2b1e56e2e518ba6034883f1f075b"},"401":{"$ref":"#/components/responses/0b3c2b39a1a7adb4dd80f9bef2b243a2"},"403":{"$ref":"#/components/responses/a5a65936e8d950bfa346c024f911452c"},"500":{"$ref":"#/components/responses/d0d6285511be7a770ea6c6f2606acd51"}},"security":[{"basic":[]}]},"put":{"tags":["Profile"],"summary":"Update your profile","description":"Updates the authenticated user's profile settings.","operationId":"replaceProfile","requestBody":{"$ref":"#/components/requestBodies/8ace57011aca38252bed377598ddf9e8"},"responses":{"200":{"$ref":"#/components/responses/25af31beb241d7a6185c810f5ae0797f"},"422":{"$ref":"#/components/responses/ba46cbd88bce14a87514286e0e5f25df"},"404":{"$ref":"#/components/responses/cfc8e9c1f39bd1ecb96485f762ae3bb6"},"401":{"$ref":"#/components/responses/0cb967134b9f4886631c2c024452c013"},"403":{"$ref":"#/components/responses/9ed34bb14d3601cea2d1751d05a84ada"},"500":{"$ref":"#/components/responses/e8f0b4dbade0c649739e94441e2e0a69"}},"security":[{"basic":[]}]},"patch":{"tags":["Profile"],"summary":"Update your profile","description":"Updates the authenticated user's profile settings.","operationId":"updateProfile","requestBody":{"$ref":"#/components/requestBodies/ce2a5b0af433cd14645b1b1f6fe3ac6e"},"responses":{"200":{"$ref":"#/components/responses/05328677bb1bfe5a67108d2cc99245c8"},"422":{"$ref":"#/components/responses/e4913e25c5008d12039a3da073843d9c"},"404":{"$ref":"#/components/responses/3ceb77638b7e12106b9869f9300be71a"},"401":{"$ref":"#/components/responses/68901e2ba3d20ba3a9f9a1a5ece2f719"},"403":{"$ref":"#/components/responses/048d5428b751216ef65d5fd59721dbb1"},"500":{"$ref":"#/components/responses/7d4e5cdc5cdb7f98fcf6999af80ed948"}},"security":[{"basic":[]}]}},"/projects":{"get":{"tags":["Projects"],"summary":"List all projects","description":"Returns all projects accessible to the authenticated user, sorted alphabetically. Each project includes its starred status.","operationId":"listProjects","responses":{"200":{"$ref":"#/components/responses/1db02819e660b7b205b7ab47e7338629"},"401":{"$ref":"#/components/responses/c7f1838eff4cbefbb2f5160dce62cdab"},"403":{"$ref":"#/components/responses/05abfb7e6129500348a879b5bb796afe"},"500":{"$ref":"#/components/responses/c113b643d5a0f0697655fcadeae7a2b2"}},"security":[{"basic":[]}]},"post":{"tags":["Projects"],"summary":"Create a project","description":"Creates a new project in the account. Optionally applies a template to pre-populate servers, config files, and other settings.","operationId":"createProject","requestBody":{"$ref":"#/components/requestBodies/04d3b2f3eeceed0a118e2b7dfd10e106"},"responses":{"200":{"$ref":"#/components/responses/47cf4029d1e29467ec429a0cbb06412d"},"422":{"$ref":"#/components/responses/bcc865c374bff6e0955ea7a91bc0d8c6"},"401":{"$ref":"#/components/responses/bdfd5a052335ffaf314cf49efbacf6e9"},"403":{"$ref":"#/components/responses/20d0e4adbce0eb0e2af19c741bdf89ac"},"500":{"$ref":"#/components/responses/094cac065ab38456b3dd64ca673a5071"}},"security":[{"basic":[]}]}},"/projects/{id}":{"get":{"tags":["Projects"],"summary":"View a project","description":"Returns the full details of a single project including its repository info, zone, and starred status.","operationId":"getProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"responses":{"200":{"$ref":"#/components/responses/a4740158fb509a324f989a86304dba93"},"404":{"$ref":"#/components/responses/7236ecbf1ff5c79429d6491c9e817fac"},"401":{"$ref":"#/components/responses/053d75dde0108f9dfd11cb2e977d6e47"},"403":{"$ref":"#/components/responses/99b98d0afb62b219158e7edb4a9e9199"},"500":{"$ref":"#/components/responses/ca0dde1acfbdbedded2ee8b923966ad7"}},"security":[{"basic":[]}]},"put":{"tags":["Projects"],"summary":"Update a project","description":"Updates the project's settings such as name, notification preferences, zone, and permalink.","operationId":"replaceProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"requestBody":{"$ref":"#/components/requestBodies/c5584c710f56a6e713ddf2de7f8aaa83"},"responses":{"200":{"$ref":"#/components/responses/b734c116750407d6cb88bf1267977681"},"422":{"$ref":"#/components/responses/36c40cf1b00e2ef730215df9b52f91cf"},"404":{"$ref":"#/components/responses/3db5449b6e372bde4d556021bd4dcee7"},"401":{"$ref":"#/components/responses/912f97ae429881855b5598323974bd24"},"403":{"$ref":"#/components/responses/0fe059a053c89a435e40479bc3de8ed1"},"500":{"$ref":"#/components/responses/7c7878959e09e00c0ed3d668899bd19d"}},"security":[{"basic":[]}]},"patch":{"tags":["Projects"],"summary":"Update a project","description":"Updates the project's settings such as name, notification preferences, zone, and permalink.","operationId":"updateProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"requestBody":{"$ref":"#/components/requestBodies/b1e790c590b053baed8693369cf09395"},"responses":{"200":{"$ref":"#/components/responses/66bf91e3da407b23564691d86da6a79c"},"422":{"$ref":"#/components/responses/e0125798b2a70d7af4b644ebe9927be6"},"404":{"$ref":"#/components/responses/3cfa9631388ec6d1481f5aa82df7117d"},"401":{"$ref":"#/components/responses/5a2c72419c3037a82406629768cb9625"},"403":{"$ref":"#/components/responses/66c2817d9e26cfc76e9e62bc6cec8203"},"500":{"$ref":"#/components/responses/51fac4c2b4dce4fbf46bcd432c126dfe"}},"security":[{"basic":[]}]},"delete":{"tags":["Projects"],"summary":"Delete a project","description":"Queues the project for deletion. Fails if any deployments are currently running.","operationId":"deleteProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"responses":{"200":{"$ref":"#/components/responses/d19bf460a75cdd901bc65b77408695d5"},"422":{"$ref":"#/components/responses/d3ad1835a4e70e36a607014853c64609"},"404":{"$ref":"#/components/responses/8edd208aa5a64ade377507949629ca21"},"401":{"$ref":"#/components/responses/2f435cdbffefa558b6e8782d3ca76d9f"},"403":{"$ref":"#/components/responses/93cf0d644326a809a80e2ed4fd9480b7"},"500":{"$ref":"#/components/responses/4ded389d3b03519777c45da6c096c05b"}},"security":[{"basic":[]}]}},"/projects/{id}/ai_deployment_overview":{"post":{"tags":["Projects"],"summary":"AI Deployment Overview","description":"Generates an AI-powered summary of changes between two revisions based on commit messages. Requires a configured and cloned repository.","operationId":"aiDeploymentOverviewProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"requestBody":{"$ref":"#/components/requestBodies/1446240eb00d9cdaf6d01ab69ab7eb37"},"responses":{"200":{"$ref":"#/components/responses/16ae7c94212589f16a7b9049a96b5b17"},"400":{"$ref":"#/components/responses/08a470e0ff78a58159aacc9d974080bb"},"422":{"$ref":"#/components/responses/b63a5082c1261787f9f76419de7c7809"},"401":{"$ref":"#/components/responses/7184220172c454d53b42d29ad38ffab2"},"403":{"$ref":"#/components/responses/acbcb370b979bcf306976ed81c140df2"},"500":{"$ref":"#/components/responses/4ac93acf52aaafd849c0fd928204ef06"}},"security":[{"basic":[]}]}},"/projects/{id}/insights":{"get":{"tags":["Projects"],"summary":"Get project deployment insights","description":"Returns aggregated deployment statistics and per-server metrics for the specified time period.","operationId":"listProjectInsights","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/5ead782fa8954405b083e973f24d5345"},{"$ref":"#/components/parameters/30a628c33caf3d3300b538f3ed0dd8c7"}],"responses":{"200":{"$ref":"#/components/responses/1a141ddafc39f641d3eef3cd1359ac77"},"500":{"$ref":"#/components/responses/ee875dcf98d4874779e998112c84fa6a"},"401":{"$ref":"#/components/responses/03844db9fa60de7d649b85c3fdcf7fd3"},"403":{"$ref":"#/components/responses/e7fdcea912b735acaa2436c5d94e4179"}},"security":[{"basic":[]}]}},"/projects/{id}/regenerate_key":{"patch":{"tags":["Projects"],"summary":"Regenerate project SSH key","description":"Regenerates the project SSH key and returns the new public key. key_type is optional and defaults to the existing project's algorithm; must be one of ED25519 or RSA when provided.","operationId":"regenerateKeyProjectRegenerateKey","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"requestBody":{"$ref":"#/components/requestBodies/d67b6dbb5c9e0b5e6abaa64f8505a178"},"responses":{"200":{"$ref":"#/components/responses/4d76907e49d0353fbc4259d98fcde9c8"},"422":{"$ref":"#/components/responses/93129c3cae6d8eb407f17b3ffd051488"},"401":{"$ref":"#/components/responses/24515b336c75ea0f6604e3eb2e58a4fe"},"403":{"$ref":"#/components/responses/ba35fab94c72d8ce856ff2ea46a707c1"},"500":{"$ref":"#/components/responses/6d66682e655119b585cea6fd11622531"}},"security":[{"basic":[]}]}},"/projects/{id}/star":{"post":{"tags":["Projects"],"summary":"Toggle star/unstar on a project","description":"Toggles the starred status of a project for the current user. Starred projects appear prioritized in listings.","operationId":"starProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"responses":{"200":{"$ref":"#/components/responses/48797af91c6750e1da25ece2ba67cad7"},"401":{"$ref":"#/components/responses/076272a2485c9139031546c810cf8385"},"403":{"$ref":"#/components/responses/d08148369f3622a3a4565441e90231be"},"500":{"$ref":"#/components/responses/24985112a726bd8e90a4d9f87db31e08"}},"security":[{"basic":[]}]}},"/projects/{id}/undeployed_changes":{"get":{"tags":["Projects"],"summary":"List undeployed commits for a project","description":"Returns the undeployed commits between the last completed deployment in the default branch and the current HEAD. Commit list is capped at 100; when capped, `truncated` is true.","operationId":"undeployedChangesProjectUndeployedChange","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"responses":{"200":{"$ref":"#/components/responses/60d20ea8ae0601bd7cc4e22854f1a39a"},"422":{"$ref":"#/components/responses/8c1558305b140b4e0cae2ee7dab39e45"},"401":{"$ref":"#/components/responses/3d7f547dee10800a9fd033bf0267cfd9"},"403":{"$ref":"#/components/responses/97b66a40932b34f4daf2806c5dae1fd3"},"500":{"$ref":"#/components/responses/7bc133121af247ff6303c7e6f3cbc3cd"}},"security":[{"basic":[]}]}},"/projects/{id}/upload_key":{"patch":{"tags":["Projects"],"summary":"Upload custom key","description":"Uploads a custom SSH private key for the project and returns the corresponding public key.","operationId":"uploadKeyProject","parameters":[{"$ref":"#/components/parameters/cb767feb521d5a5082ae6ce105173118"}],"requestBody":{"$ref":"#/components/requestBodies/2141da26ce826115a43c888d6a05cf60"},"responses":{"200":{"$ref":"#/components/responses/e1219e0c3f55888e51cf7af042445514"},"422":{"$ref":"#/components/responses/2e29a219fa17ac5502d0e1e47b99e012"},"401":{"$ref":"#/components/responses/7ed21c091e7580d57cad351a73d210fc"},"403":{"$ref":"#/components/responses/2ae51e63d7fcfe975e94017ed5b2392a"},"500":{"$ref":"#/components/responses/519dd5c6e82656d6ac8595fb07be169a"}},"security":[{"basic":[]}]}},"/projects/{project_id}/auto_deployments":{"get":{"tags":["Automatic Deployments"],"summary":"List automatic deployment configuration","description":"Returns the webhook URL and auto-deploy settings for each server and server group in the project.","operationId":"listProjectAutoDeployments","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/07fb1cd8901f8fcce78ba716b4068221"},"401":{"$ref":"#/components/responses/4c22711b1bfd70756131f43297f62088"},"403":{"$ref":"#/components/responses/c051285e2ca1b1cde8cc31c5aa24669e"},"500":{"$ref":"#/components/responses/08adab1740c70370da48488cfe1a6c29"}},"security":[{"basic":[]}]},"post":{"tags":["Automatic Deployments"],"summary":"Update automatic deployment settings","description":"Enables or disables automatic deployments for individual servers and server groups. Unrecognized identifiers are skipped and reported in the response.","operationId":"createProjectAutoDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/a5e72dc87465749069cefd9add1540b8"},"responses":{"200":{"$ref":"#/components/responses/a737b25546407eb79cf1154290b453c9"},"422":{"$ref":"#/components/responses/580cf83eafb1ad59d9ace317e28350d7"},"401":{"$ref":"#/components/responses/6213afd0e580ab9db66fc7eac0f4c25e"},"403":{"$ref":"#/components/responses/5e74e8fb7af0188df72763e8815b74e0"},"500":{"$ref":"#/components/responses/2920f7133e0c2c9d24c268cbf12493af"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_cache_files":{"get":{"tags":["Build Cache Files"],"summary":"List build cache files","description":"Returns all build cache files configured for the project.","operationId":"listProjectBuildCacheFiles","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/be48dab3f0b08cf0ca0f29db8330a36c"},"401":{"$ref":"#/components/responses/4517d5ce7ee0e365be7f876d42ee3d15"},"403":{"$ref":"#/components/responses/84d22c4162950a9f0eccb72c9e4e3ad2"},"500":{"$ref":"#/components/responses/e0fb2ad7c55b558f7d52e54b1699ced3"}},"security":[{"basic":[]}]},"post":{"tags":["Build Cache Files"],"summary":"Create a build cache file","description":"Creates a new build cache file entry for the project. Build cache files persist between builds to speed up subsequent build runs.","operationId":"createProjectBuildCacheFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/01a4fd6eb08213c5f1877691615b2510"},"responses":{"200":{"$ref":"#/components/responses/1703ef92426e616b18c9819b255e2018"},"500":{"$ref":"#/components/responses/28aa570afd36ab5447ba6ea85d391045"},"401":{"$ref":"#/components/responses/9e9b8facd18764edd26fab9150b28798"},"403":{"$ref":"#/components/responses/98b32997f65ca5e0ce66792c9adfc370"},"422":{"$ref":"#/components/responses/edd8dc8912d71436577d87a6188c8fb7"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_cache_files/{id}":{"put":{"tags":["Build Cache Files"],"summary":"Update a build cache file","description":"Updates the path of an existing build cache file entry.","operationId":"replaceProjectBuildCacheFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b0bcba4423e03a452590ad4c283b76ca"}],"requestBody":{"$ref":"#/components/requestBodies/fe2b6aa0b13d118a45b51c54daa99bfe"},"responses":{"200":{"$ref":"#/components/responses/525620e5357fea06d6c1a75d95b8f087"},"500":{"$ref":"#/components/responses/c9e8493cf0deab2272711bca5542ac75"},"404":{"$ref":"#/components/responses/5cdd9a26c8b03f6653d4a3e4f2c528bc"},"401":{"$ref":"#/components/responses/883d965b310485256dc92b2d6b3058ee"},"403":{"$ref":"#/components/responses/16647919d959bdde99e88dc81008f7be"},"422":{"$ref":"#/components/responses/5cd7e61a0f0084d5980da72c3fee76ad"}},"security":[{"basic":[]}]},"patch":{"tags":["Build Cache Files"],"summary":"Update a build cache file","description":"Updates the path of an existing build cache file entry.","operationId":"updateProjectBuildCacheFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b0bcba4423e03a452590ad4c283b76ca"}],"requestBody":{"$ref":"#/components/requestBodies/2c4554cd3aebdc0c311e53448586bd2e"},"responses":{"200":{"$ref":"#/components/responses/747fd166f9a539ec40f13d734bd47b20"},"500":{"$ref":"#/components/responses/33f4f665151e8c381be808ffca608e21"},"404":{"$ref":"#/components/responses/c7dcda6eb1a905d1a00add555d5f92c1"},"401":{"$ref":"#/components/responses/a3a91a28af993612f96893e11f921743"},"403":{"$ref":"#/components/responses/f877f505c147ef6516ed4675a664c297"},"422":{"$ref":"#/components/responses/1e347abe5d9a1918f7b92910c8b8fa0d"}},"security":[{"basic":[]}]},"delete":{"tags":["Build Cache Files"],"summary":"Delete a build cache file","description":"Removes a build cache file entry from the project. The cached files will no longer persist between builds.","operationId":"deleteProjectBuildCacheFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b0bcba4423e03a452590ad4c283b76ca"}],"responses":{"200":{"$ref":"#/components/responses/f766836ad09eee91c5f3a479f53fc167"},"500":{"$ref":"#/components/responses/351c40bf95da95bb45d3cf7716659b58"},"404":{"$ref":"#/components/responses/86bce515350e814a1a74ae81f6f5d9f3"},"401":{"$ref":"#/components/responses/56cebcdabc78855859ce4a41b25a9e6d"},"403":{"$ref":"#/components/responses/2899d625ffa3e3068c8c41d4c1a3d6c9"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_commands":{"get":{"tags":["Build Commands"],"summary":"List build commands","description":"Returns all build commands configured for this project, ordered ascending. Build commands run during the build pipeline before deployment.","operationId":"listProjectBuildCommands","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/151493e9eb8b0f590eea2d729dbac596"},"401":{"$ref":"#/components/responses/621969286130f35a731ef3b9dd4f26e3"},"403":{"$ref":"#/components/responses/c7630597f542bdec9bdbebe8aacd5411"},"500":{"$ref":"#/components/responses/3adb76eced0dd8e7d021956fd45a8c4f"}},"security":[{"basic":[]}]},"post":{"tags":["Build Commands"],"summary":"Create a build command","description":"Adds a new build command to the project's build pipeline. Commands execute in order during the build phase.","operationId":"createProjectBuildCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/930e2cf03afefd39b7343707ba74fbb8"},"responses":{"200":{"$ref":"#/components/responses/acd1694fc84fdcdc25e91ef315ee9dfe"},"500":{"$ref":"#/components/responses/75c98c0b226f9fc1d296e2c3bc011f3c"},"401":{"$ref":"#/components/responses/33d0369f2de5910f29b4c2561366ae41"},"403":{"$ref":"#/components/responses/cf09954d38b5158fef28b34e67a9a3da"},"422":{"$ref":"#/components/responses/550fb6ca8481d53c48658c5dc1326576"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_commands/{id}":{"put":{"tags":["Build Commands"],"summary":"Update a build command","description":"Updates an existing build command's properties such as the command string, description, or error handling behavior.","operationId":"replaceProjectBuildCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3e5b2bc01916b68ec592a9f9744c92b0"}],"requestBody":{"$ref":"#/components/requestBodies/a2a2b77959da50c72b428d2ba7c691e7"},"responses":{"200":{"$ref":"#/components/responses/243b24f04a64a69b8c8a46206602ce89"},"500":{"$ref":"#/components/responses/a504e678177780c9c0c4c7a3ee9f4f85"},"404":{"$ref":"#/components/responses/477ccb0731e3ef6a82dd48fc90eedc60"},"401":{"$ref":"#/components/responses/cdb8920d65dece01618f58274053d506"},"403":{"$ref":"#/components/responses/d3ccee20989f19183ec9fb02417b0049"},"422":{"$ref":"#/components/responses/7da97f45a8e1a262646d710f1d9f21b4"}},"security":[{"basic":[]}]},"patch":{"tags":["Build Commands"],"summary":"Update a build command","description":"Updates an existing build command's properties such as the command string, description, or error handling behavior.","operationId":"updateProjectBuildCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3e5b2bc01916b68ec592a9f9744c92b0"}],"requestBody":{"$ref":"#/components/requestBodies/9ac6c663abd19187d6e3bf0c11eaf7f7"},"responses":{"200":{"$ref":"#/components/responses/b793a6af2c31ab8f91285df526bf81dd"},"500":{"$ref":"#/components/responses/d671bfc0c52cb3d5f762805c537e4736"},"404":{"$ref":"#/components/responses/9bbc951e18e8f34f2866d1568fef0ec8"},"401":{"$ref":"#/components/responses/d520f6d4566e66bee70045ac7199e271"},"403":{"$ref":"#/components/responses/8cdab2cf42f96b7e72ba5a2a4824934a"},"422":{"$ref":"#/components/responses/b0b599bb5aa24aee542e6d8901713ba0"}},"security":[{"basic":[]}]},"delete":{"tags":["Build Commands"],"summary":"Delete a build command","description":"Removes a build command from the project's build pipeline.","operationId":"deleteProjectBuildCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3e5b2bc01916b68ec592a9f9744c92b0"}],"responses":{"200":{"$ref":"#/components/responses/9bb05eec1ef3fe7eb97128611472726a"},"500":{"$ref":"#/components/responses/79e03d2a1febd20461c7c22dfbf62f86"},"404":{"$ref":"#/components/responses/ea19ebdb12f6e7df5d0eddc7d33425b9"},"401":{"$ref":"#/components/responses/0ac8544d309fca9f71e0ee32adcef339"},"403":{"$ref":"#/components/responses/6284074c310a0f24124ec9b9c65b38a8"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_configuration":{"get":{"tags":["Build Configurations"],"summary":"Get a build configuration","description":"Returns details of a specific build environment configuration, including known hosts, cache files, and available packages.","operationId":"getProjectBuildConfiguration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b6dfad1ce9e91d741ac3d3eb49a4934e"}],"responses":{"200":{"$ref":"#/components/responses/226d6e29c4d627f966692820e778dd91"},"404":{"$ref":"#/components/responses/57a9eac2b7c68552409d484852083f58"},"401":{"$ref":"#/components/responses/62e3d168c1280b8ba747b696fadac80a"},"403":{"$ref":"#/components/responses/de145e86edcfbdba3c36743a7c4a40bc"},"500":{"$ref":"#/components/responses/5b91b92d8af824095f50f8d1fdf3c8a0"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_configurations":{"get":{"tags":["Build Configurations"],"summary":"List build configurations","description":"Returns all build environment configurations for the project, including the default and any overrides.","operationId":"listProjectBuildConfigurations","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/f57f99207f6979b3bd1148a9bd1b013c"},"401":{"$ref":"#/components/responses/50a96c1e1a44b16627e45a0df512138a"},"403":{"$ref":"#/components/responses/8516339c49bb6acfa7f571dc29604345"},"500":{"$ref":"#/components/responses/851519f4125f30d5d0d60e07e4e1e0f5"}},"security":[{"basic":[]}]},"post":{"tags":["Build Configurations"],"summary":"Create a build configuration override","description":"Creates a new non-default build environment configuration that can override the default settings for specific servers.","operationId":"createProjectBuildConfiguration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"201":{"$ref":"#/components/responses/333cb93ffa5965d38f494817bf713eae"},"422":{"$ref":"#/components/responses/5f81f3aa22ad256c5bae712f853c2300"},"401":{"$ref":"#/components/responses/8928207c200ce3f865fbf4d330395b85"},"403":{"$ref":"#/components/responses/9c3810e1ea92a51144f50ce79261b686"},"500":{"$ref":"#/components/responses/4811897945c94b7f47c9838bef27abb2"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_configurations/{id}":{"get":{"tags":["Build Configurations"],"summary":"Get a build configuration","description":"Returns details of a specific build environment configuration, including known hosts, cache files, and available packages.","operationId":"getProjectBuildConfigurationById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b6dfad1ce9e91d741ac3d3eb49a4934e"}],"responses":{"200":{"$ref":"#/components/responses/65566323b603ed75463050b76ea0cb71"},"404":{"$ref":"#/components/responses/96f1ddca27d4d2a2e0e84a7df5bc5c8f"},"401":{"$ref":"#/components/responses/31c7f0c7dfe731a02e7366c0d5a9fbcb"},"403":{"$ref":"#/components/responses/668099450f16d2931a43109eedb810df"},"500":{"$ref":"#/components/responses/b7023fbd9aede6fd3684b8824542120f"}},"security":[{"basic":[]}]},"put":{"tags":["Build Configurations"],"summary":"Update a build configuration override","description":"Updates a non-default build environment configuration, including its target server assignments.","operationId":"replaceProjectBuildConfiguration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b6dfad1ce9e91d741ac3d3eb49a4934e"}],"responses":{"200":{"$ref":"#/components/responses/e8edc9a329d3b8f393f3f5589b8ed235"},"422":{"$ref":"#/components/responses/334f044a06ac03399053182d50e7d356"},"404":{"$ref":"#/components/responses/bca05e365c2eb7bf7bf77ce1ed2807e5"},"401":{"$ref":"#/components/responses/bc5f82125183d08d2548cf3c758ce1c4"},"403":{"$ref":"#/components/responses/8ba73041880ed2b377fbdd1df4978932"},"500":{"$ref":"#/components/responses/921463f8e83f142d77bd60a46d86b2a2"}},"security":[{"basic":[]}]},"patch":{"tags":["Build Configurations"],"summary":"Update a build configuration override","description":"Updates a non-default build environment configuration, including its target server assignments.","operationId":"updateProjectBuildConfiguration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b6dfad1ce9e91d741ac3d3eb49a4934e"}],"responses":{"200":{"$ref":"#/components/responses/d554f319619f9045c45fed9f1bc8f4a7"},"422":{"$ref":"#/components/responses/dbf7e668594c45edae60b27c7ee7952e"},"404":{"$ref":"#/components/responses/4e866654876f73dd8ddb4edf0e1e0310"},"401":{"$ref":"#/components/responses/3c4adafc8b5bb9e7d2a637944b7dd71c"},"403":{"$ref":"#/components/responses/70bfdd45e9dfb5c9eceaa60f4a239cee"},"500":{"$ref":"#/components/responses/423dd5934a1d81a4ea6c3db6763e2ab5"}},"security":[{"basic":[]}]},"delete":{"tags":["Build Configurations"],"summary":"Delete a build configuration override","description":"Removes a non-default build environment configuration. The default configuration cannot be deleted.","operationId":"deleteProjectBuildConfiguration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/b6dfad1ce9e91d741ac3d3eb49a4934e"}],"responses":{"200":{"$ref":"#/components/responses/8542f799b31e127c6fdd14f3ddf94d33"},"422":{"$ref":"#/components/responses/6deeedf97ba51a8c885cd5857c3dc60c"},"404":{"$ref":"#/components/responses/e0074e06be171f3ca38d799d2e9a0c6a"},"401":{"$ref":"#/components/responses/929e87f7bddef04efc9d92504a883ef9"},"403":{"$ref":"#/components/responses/8712b1e641c8a766e70910dbda81491f"},"500":{"$ref":"#/components/responses/d0a710983a898e28ec854690936b84e7"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_configurations/{override_build_configuration_id}/build_languages/{id}":{"put":{"tags":["Language Versions"],"summary":"Update language version","description":"Sets the version of a specific language or runtime package for the project's build environment. Optionally targets a specific build configuration override.","operationId":"replaceProjectBuildConfigurationBuildLanguage","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/8a896a8b20a129d3b73bb95bfc57fb9d"},{"$ref":"#/components/parameters/9aa1361134de003c0ef02160d5412005"}],"requestBody":{"$ref":"#/components/requestBodies/449030214c4dc87cfd15556ce85cfd19"},"responses":{"200":{"$ref":"#/components/responses/3f4a10def6bb70edc8fe97702d7fc008"},"422":{"$ref":"#/components/responses/f60d5757a770330373b1fdc7cb364a67"},"404":{"$ref":"#/components/responses/26e09bdc9e4412ab0fe23dcb055ed022"},"401":{"$ref":"#/components/responses/b3f2baef0eab5e833cce41570d316814"},"403":{"$ref":"#/components/responses/9604f37154b01f2cf31d75ae0a7f58a5"},"500":{"$ref":"#/components/responses/d850a7c4139d627393b3403d7c88dee0"}},"security":[{"basic":[]}]},"patch":{"tags":["Language Versions"],"summary":"Update language version","description":"Sets the version of a specific language or runtime package for the project's build environment. Optionally targets a specific build configuration override.","operationId":"updateProjectBuildConfigurationBuildLanguage","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/8a896a8b20a129d3b73bb95bfc57fb9d"},{"$ref":"#/components/parameters/9aa1361134de003c0ef02160d5412005"}],"requestBody":{"$ref":"#/components/requestBodies/e6e38bbd42cb093e55278b696823fa45"},"responses":{"200":{"$ref":"#/components/responses/820d63b5bafc2a755dc27666b4bea417"},"422":{"$ref":"#/components/responses/1ad94c09f16b2cf2901a5b620e5dae15"},"404":{"$ref":"#/components/responses/9be58472c10166c7b6f1df948dd078af"},"401":{"$ref":"#/components/responses/bb3ee726e46abe1c2ab2f39d0ff6e404"},"403":{"$ref":"#/components/responses/680fd930206d07e93ff770d644b0c1c5"},"500":{"$ref":"#/components/responses/ffe5d0ea0547a86fa6cf6a2f69e25220"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_known_hosts":{"get":{"tags":["Build Known Hosts"],"summary":"List build known hosts","description":"Returns all SSH known host entries configured for the project's build environment.","operationId":"listProjectBuildKnownHosts","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/ebe82b63b4329acd3dddf8fee91cc24f"},"401":{"$ref":"#/components/responses/7b9fd5bdbf96fd804b85b2e6c2ecd47f"},"403":{"$ref":"#/components/responses/3644de56b07cdd11190184706ed228e5"},"500":{"$ref":"#/components/responses/91124dfe5261a3db5446124c45c066d2"}},"security":[{"basic":[]}]},"post":{"tags":["Build Known Hosts"],"summary":"Create a build known host","description":"Adds an SSH known host entry so the build server can connect to the specified hostname without host key verification prompts.","operationId":"createProjectBuildKnownHost","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/5c5597bb575118f899b4115a306de406"},"responses":{"201":{"$ref":"#/components/responses/b38507432d33a09e9a1c71d63ca84cf9"},"422":{"$ref":"#/components/responses/0123928115b10e9584dd95c4614aa51c"},"401":{"$ref":"#/components/responses/805d042081d8e2791b2d86f083a30a65"},"403":{"$ref":"#/components/responses/f3c75caf5fca09f697a0be5278d73477"},"500":{"$ref":"#/components/responses/86562c35b2c1c82cc9892f55977ce5ea"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_known_hosts/{id}":{"delete":{"tags":["Build Known Hosts"],"summary":"Delete a build known host","description":"Removes an SSH known host entry from the project's build environment.","operationId":"deleteProjectBuildKnownHost","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/59042d9f5ca35722edf6abcbe93bd329"}],"responses":{"200":{"$ref":"#/components/responses/8aac6f1c0686440b335afebbace283c8"},"404":{"$ref":"#/components/responses/84d3a7076cc16631c0f33c404272a4d2"},"401":{"$ref":"#/components/responses/f69765a7fcac2f8fdde9a7def72faf24"},"403":{"$ref":"#/components/responses/5b0ee2d9d660cfd7ccf92000753ee379"},"500":{"$ref":"#/components/responses/80f276ba1dea73c861a1ba8ae102e0e8"}},"security":[{"basic":[]}]}},"/projects/{project_id}/build_languages/{id}":{"put":{"tags":["Language Versions"],"summary":"Update language version","description":"Sets the version of a specific language or runtime package for the project's build environment. Optionally targets a specific build configuration override.","operationId":"replaceProjectBuildLanguage","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/8a896a8b20a129d3b73bb95bfc57fb9d"},{"$ref":"#/components/parameters/9aa1361134de003c0ef02160d5412005"}],"requestBody":{"$ref":"#/components/requestBodies/f311e25427f233b4f70b3a14557c0f29"},"responses":{"200":{"$ref":"#/components/responses/96f3fad6f9e24b23d1013fe4dc9f0f85"},"422":{"$ref":"#/components/responses/4af3ad2f321e6e1d2117a79c475a5d2e"},"404":{"$ref":"#/components/responses/e34beb51d3d5b86b0e42b95333ff5b86"},"401":{"$ref":"#/components/responses/2b41b03dc8ce94db238ddc55eb631339"},"403":{"$ref":"#/components/responses/92086045d6667b83a0d23e6384d2fd80"},"500":{"$ref":"#/components/responses/0b685798ea2f52f79e11ba46330bef12"}},"security":[{"basic":[]}]},"patch":{"tags":["Language Versions"],"summary":"Update language version","description":"Sets the version of a specific language or runtime package for the project's build environment. Optionally targets a specific build configuration override.","operationId":"updateProjectBuildLanguage","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/8a896a8b20a129d3b73bb95bfc57fb9d"},{"$ref":"#/components/parameters/9aa1361134de003c0ef02160d5412005"}],"requestBody":{"$ref":"#/components/requestBodies/8bb3f0103e68a86cc8e9c8de5341ed8b"},"responses":{"200":{"$ref":"#/components/responses/47a3de076a177db83406fda6338d0d77"},"422":{"$ref":"#/components/responses/22e4e69436e7e05f0850fea92e3ef44b"},"404":{"$ref":"#/components/responses/d097ef27488d0d8c7fd43330d71cd282"},"401":{"$ref":"#/components/responses/a1a89a6bcb10548f85c7ac5f007db80d"},"403":{"$ref":"#/components/responses/8279f6e80a23c785c7246454936719b7"},"500":{"$ref":"#/components/responses/3ea928f9e3b875b7d3e555919b3ee497"}},"security":[{"basic":[]}]}},"/projects/{project_id}/commands":{"get":{"tags":["Ssh Commands"],"summary":"List SSH commands","description":"Returns all SSH commands configured for the project, ordered ascending. SSH commands run on target servers before or after deployment.","operationId":"listProjectCommands","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/a0e8f1b1afa00c43984e8589972ec010"},"401":{"$ref":"#/components/responses/f2ac18ebc8522852c02944e8a1a6ac36"},"403":{"$ref":"#/components/responses/81e0a3504c3a97330d7f83a4a6d9816c"},"500":{"$ref":"#/components/responses/0187a4844e8ab0039c1e9478dea80210"}},"security":[{"basic":[]}]},"post":{"tags":["Ssh Commands"],"summary":"Create an SSH command","description":"Creates a new SSH command for the project. Commands can be configured to run before or after deployment on specified servers.","operationId":"createProjectCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/9c773e5f685499c0c0504977b72a5d70"},"responses":{"200":{"$ref":"#/components/responses/3264a4230f27d75fe028fed43c41e60a"},"422":{"$ref":"#/components/responses/a0b5df79d6d443c81048a8f37cff0650"},"401":{"$ref":"#/components/responses/837224e8772c3eb9b08f871c13c592ee"},"403":{"$ref":"#/components/responses/fa2ced1939ce7c75ab0f35f95a3ace2c"},"500":{"$ref":"#/components/responses/c7d3d3872e62b6cb433d74852b0e21ec"}},"security":[{"basic":[]}]}},"/projects/{project_id}/commands/{id}":{"get":{"tags":["Ssh Commands"],"summary":"Get an SSH command","description":"Returns details of a specific SSH command, including its timing, target servers, and error handling settings.","operationId":"getProjectCommandById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"responses":{"200":{"$ref":"#/components/responses/d035193d40898efbd372345753bd98f0"},"404":{"$ref":"#/components/responses/c511d7c9958072673643e7fa4a56ef3d"},"401":{"$ref":"#/components/responses/e9a3527058d0e800a28344ee26869ce4"},"403":{"$ref":"#/components/responses/1d1902cc8f3d507b50e5966ec45b9371"},"500":{"$ref":"#/components/responses/0edb9c973bec8684efd1d5d6048756fc"}},"security":[{"basic":[]}]},"put":{"tags":["Ssh Commands"],"summary":"Update an SSH command","description":"Updates an existing SSH command's properties. Server assignments are only modified when server parameters are explicitly provided.","operationId":"replaceProjectCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"requestBody":{"$ref":"#/components/requestBodies/2000da5f7ee9aa5c28258d975de7db2d"},"responses":{"200":{"$ref":"#/components/responses/6aba1a601c7a828183d456ebdb392f6c"},"422":{"$ref":"#/components/responses/b1902c4cdef4672bfb2047f0221a0d29"},"404":{"$ref":"#/components/responses/380eb4f3fad6a03f96c84d944db21eed"},"401":{"$ref":"#/components/responses/ba2aec6233cb9ba0e80fa8c507bcec69"},"403":{"$ref":"#/components/responses/259384ec9a476fd18b73cd47d059b614"},"500":{"$ref":"#/components/responses/8c1a93c26d2d9673bdaa89a77ac3b5f7"}},"security":[{"basic":[]}]},"patch":{"tags":["Ssh Commands"],"summary":"Update an SSH command","description":"Updates an existing SSH command's properties. Server assignments are only modified when server parameters are explicitly provided.","operationId":"updateProjectCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"requestBody":{"$ref":"#/components/requestBodies/1eedc0e0a3c2e8e7adbe013810d11d45"},"responses":{"200":{"$ref":"#/components/responses/50e9806bcca9f904be3e14a5f50c7010"},"422":{"$ref":"#/components/responses/e3227e15bedf15f93b0c5c7f6a3d3a3e"},"404":{"$ref":"#/components/responses/eaea973198a7dbc905cfbe9e36ec09d3"},"401":{"$ref":"#/components/responses/652dbc07e31335dd6ce01af7a1fc31f6"},"403":{"$ref":"#/components/responses/ea93d9212c5b928d94bfbd70ff204c91"},"500":{"$ref":"#/components/responses/3fca3640f6ade0f3a8df48e1a3c461ca"}},"security":[{"basic":[]}]},"delete":{"tags":["Ssh Commands"],"summary":"Delete an SSH command","description":"Removes an SSH command from the project.","operationId":"deleteProjectCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"responses":{"200":{"$ref":"#/components/responses/06567f61e0993db36fcad815f6c81d32"},"500":{"$ref":"#/components/responses/040b9828218b0fd4fbf20d86e51b85a9"},"404":{"$ref":"#/components/responses/f2bc0a9b5890a95ea9777e8b93ac8d1d"},"401":{"$ref":"#/components/responses/7346fd7dc806a78dcc3d8b4bba3e858c"},"403":{"$ref":"#/components/responses/daf29d0bb3d8fe91ec3ccbe92645f829"}},"security":[{"basic":[]}]}},"/projects/{project_id}/config_files":{"get":{"tags":["Config Files"],"summary":"List config files","description":"Returns all configuration files for the project, ordered ascending. Config files are deployed to servers with their content during each deployment.","operationId":"listProjectConfigFiles","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/f17bc126c1e0839c8af91681c3fca93c"},"401":{"$ref":"#/components/responses/7f62554ea850403afbe10ed4c75c2b38"},"403":{"$ref":"#/components/responses/84090e6bfeaf8a514fe46758ecc496e3"},"500":{"$ref":"#/components/responses/802b2702bd78e9c6e82604f5b4e5585d"}},"security":[{"basic":[]}]},"post":{"tags":["Config Files"],"summary":"Create a config file","description":"Creates a new configuration file for the project. The file will be deployed to the specified path on target servers during each deployment.","operationId":"createProjectConfigFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/41229bdeaa56fbce4d758cfcea9104b5"},"responses":{"200":{"$ref":"#/components/responses/76b233bf896ad6ab62ade9ff2d41874b"},"422":{"$ref":"#/components/responses/746e0eefae23eb05b15d9b3e3e506cee"},"401":{"$ref":"#/components/responses/2f7f63154e5da2367babb37ce3e29db6"},"403":{"$ref":"#/components/responses/e9a3a0345a51bb2c6f971e695099281a"},"500":{"$ref":"#/components/responses/01d530e8e03e5bc09a0a80fb94517076"}},"security":[{"basic":[]}]}},"/projects/{project_id}/config_files/{id}":{"get":{"tags":["Config Files"],"summary":"Get a config file","description":"Returns details of a specific configuration file, including its content body and server assignments.","operationId":"getProjectConfigFileById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/e1eaad9f0a6b762bf061b7135cd10962"}],"responses":{"200":{"$ref":"#/components/responses/11d35738e915df2ab4864642289cd74e"},"404":{"$ref":"#/components/responses/e4071cedb098195621a5b8ffe21ec227"},"401":{"$ref":"#/components/responses/9279259b179bdb43dc2254e4af5d06df"},"403":{"$ref":"#/components/responses/1e875c339e979153138aa783bdd12dc6"},"500":{"$ref":"#/components/responses/6b441a28791a33d608bfaaae92393187"}},"security":[{"basic":[]}]},"put":{"tags":["Config Files"],"summary":"Update a config file","description":"Updates an existing configuration file's path, content, server assignments, or other properties.","operationId":"replaceProjectConfigFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/e1eaad9f0a6b762bf061b7135cd10962"}],"requestBody":{"$ref":"#/components/requestBodies/488a8d76ff13a16c769e7e5ce74a2c76"},"responses":{"200":{"$ref":"#/components/responses/8eb1191900b63e2f0eb8ab20945fcbea"},"422":{"$ref":"#/components/responses/b380d378e73643c179a71e6c9319498b"},"404":{"$ref":"#/components/responses/f764504cfbfef87fbeefad8ed13c8dce"},"401":{"$ref":"#/components/responses/cb661ed2e8ee95e6aad25712f0754d87"},"403":{"$ref":"#/components/responses/6aaf185cbf0ac59012b3a89dadb742e7"},"500":{"$ref":"#/components/responses/24328d20d145db5611e11bcf87dba918"}},"security":[{"basic":[]}]},"patch":{"tags":["Config Files"],"summary":"Update a config file","description":"Updates an existing configuration file's path, content, server assignments, or other properties.","operationId":"updateProjectConfigFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/e1eaad9f0a6b762bf061b7135cd10962"}],"requestBody":{"$ref":"#/components/requestBodies/871cddf48f3c9dab3d042126f20c1f97"},"responses":{"200":{"$ref":"#/components/responses/ed09203065f6456f7819f08ca8e27514"},"422":{"$ref":"#/components/responses/427c17de61d34f95907cf51bf2a9e115"},"404":{"$ref":"#/components/responses/215323c4067d6a0498b324a08104b33b"},"401":{"$ref":"#/components/responses/ff6e83d7d1380276d6134ac42cd792c0"},"403":{"$ref":"#/components/responses/9111040fcc540c4f653cd465cc11107e"},"500":{"$ref":"#/components/responses/3f183b7c4ab4f92837c1db36cc45718f"}},"security":[{"basic":[]}]},"delete":{"tags":["Config Files"],"summary":"Delete a config file","description":"Removes a configuration file from the project. The file will no longer be deployed to servers.","operationId":"deleteProjectConfigFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/e1eaad9f0a6b762bf061b7135cd10962"}],"responses":{"200":{"$ref":"#/components/responses/d1a7b85159114b4f41a3195bc3bc9675"},"500":{"$ref":"#/components/responses/b810aa8bfc9c63313f8fed9e6101eec8"},"404":{"$ref":"#/components/responses/44c58cafe768a5e9b3c1f5fd0afd0cab"},"401":{"$ref":"#/components/responses/79c774530ab0587d3a6f255b67b6af58"},"403":{"$ref":"#/components/responses/2b8b1774c039987eca532235f5d481cc"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployment_checks":{"get":{"tags":["Deployment Checks"],"summary":"List deployment checks","description":"Returns every deployment check configured for this project, ordered by position within each stage.","operationId":"listProjectDeploymentChecks","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/240cbbc524f7b3ff33bd81e42a8a79d2"},"401":{"$ref":"#/components/responses/6541c5d13f6d1d582bbf6be3249ee05e"},"403":{"$ref":"#/components/responses/beca5c0625b0c7e06cfc72c2c9cccf24"},"500":{"$ref":"#/components/responses/be94abc61a03fc7d37676c494d2bf09d"}},"security":[{"basic":[]}]},"post":{"tags":["Deployment Checks"],"summary":"Create a deployment check","description":"Creates a new deployment check on the project. A check has a stage (pre_build or post_deploy) and a check_type (ssh, http, or vulnerability_scan). SSH checks run on selected servers; HTTP checks run from the deployment worker against the configured URL; vulnerability_scan checks run a scanner (Snyk, Trivy, or a custom CLI emitting SARIF) on the build server against the checked-out source, and are pre-build only.","operationId":"createProjectDeploymentCheck","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/ad30a6dd2ae89d42db30c281d98282e5"},"responses":{"201":{"$ref":"#/components/responses/2bea0bba0c814b227ce16e83a8e09acf"},"422":{"$ref":"#/components/responses/e8cb9af5bcd986d23bdd3351cd4ade61"},"401":{"$ref":"#/components/responses/a50697e4ebf0911933975ca782a9ef0f"},"403":{"$ref":"#/components/responses/bf2a5efe2d45d2d8fe908ec12c3860b9"},"500":{"$ref":"#/components/responses/0eab0da570c1c8fec56ac5b87d9876a7"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployment_checks/{id}":{"get":{"tags":["Deployment Checks"],"summary":"Show a deployment check","description":"Returns a single deployment check by its identifier.","operationId":"getProjectDeploymentCheckById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2730095186acddcc3bcb1193e53923e9"}],"responses":{"200":{"$ref":"#/components/responses/502b9722d1d774abef884489bdb6e871"},"404":{"$ref":"#/components/responses/810812dd9ebf2e4f276bb404c158d9fe"},"401":{"$ref":"#/components/responses/128bf0ef9de6b4c38cc657992910c442"},"403":{"$ref":"#/components/responses/f04366d2b6b05c1bbb27f131c0b879d7"},"500":{"$ref":"#/components/responses/0d9113df3069beb3f023f96ef2e112fa"}},"security":[{"basic":[]}]},"put":{"tags":["Deployment Checks"],"summary":"Update a deployment check","description":"Updates an existing deployment check. Same payload shape as create — any field can be sent on its own (PATCH semantics: missing keys leave existing values untouched).","operationId":"replaceProjectDeploymentCheck","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2730095186acddcc3bcb1193e53923e9"}],"requestBody":{"$ref":"#/components/requestBodies/ccf468b9fb46583bf247f901e4a07c86"},"responses":{"200":{"$ref":"#/components/responses/fdc6dd8040ba6124e1d2ce381a584fcb"},"422":{"$ref":"#/components/responses/e3f5a17f05b1961ea27445ce0b256b82"},"404":{"$ref":"#/components/responses/52d55a0662724d92afbc61a7d9b4683d"},"401":{"$ref":"#/components/responses/a5c4884361d9f7798d130224b3c8153e"},"403":{"$ref":"#/components/responses/fc65047b0ca748b4886bd41f262f8dd5"},"500":{"$ref":"#/components/responses/eec882317963d5f566260c41612a93f3"}},"security":[{"basic":[]}]},"patch":{"tags":["Deployment Checks"],"summary":"Update a deployment check","description":"Updates an existing deployment check. Same payload shape as create — any field can be sent on its own (PATCH semantics: missing keys leave existing values untouched).","operationId":"updateProjectDeploymentCheck","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2730095186acddcc3bcb1193e53923e9"}],"requestBody":{"$ref":"#/components/requestBodies/75a4eaaae216aab13152349f33ecb37f"},"responses":{"200":{"$ref":"#/components/responses/0a9de2bd07981abedc7ee4cdde2fb9b6"},"422":{"$ref":"#/components/responses/ec7ffeeb7726d6829e9846839ed3a12c"},"404":{"$ref":"#/components/responses/050bff62dba370f007a1cf8fc9c9c0b2"},"401":{"$ref":"#/components/responses/6e2380ca90104bde526034e79e2e6167"},"403":{"$ref":"#/components/responses/aa834ca591c02799efc18d24f0a8b089"},"500":{"$ref":"#/components/responses/a21ef2896af899c4f58d09befcf861ee"}},"security":[{"basic":[]}]},"delete":{"tags":["Deployment Checks"],"summary":"Delete a deployment check","description":"Removes a deployment check from the project.","operationId":"deleteProjectDeploymentCheck","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2730095186acddcc3bcb1193e53923e9"}],"responses":{"200":{"$ref":"#/components/responses/0e1a78c2ed68c85deadc34ac085b3495"},"422":{"$ref":"#/components/responses/4b9b12be132053293778f09837add4a0"},"404":{"$ref":"#/components/responses/b1f53e4bebe99c08a128653af48c708d"},"401":{"$ref":"#/components/responses/b5aca7ffd48f0b9e731d5c0cc2fb3ff6"},"403":{"$ref":"#/components/responses/2d268b5940da410bf076a8cec7adfba8"},"500":{"$ref":"#/components/responses/f0d459f57d86608a7677c69e732e039b"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployments":{"get":{"tags":["Deployments"],"summary":"List deployments for a project","description":"Returns a paginated list of deployments for the project, excluding previews. Can be filtered by target server/group or limited to currently running deployments.","operationId":"listProjectDeployments","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/cb1515e114aa28a733a232e8d1100046"},{"$ref":"#/components/parameters/c0ac2df9fc28efe9bcbb9c668483ad97"},{"$ref":"#/components/parameters/bca97defd28dac01e1b3b02e02310a3b"},{"$ref":"#/components/parameters/0b6ead29a598e0012f980232e26de5ed"}],"responses":{"200":{"$ref":"#/components/responses/e4fa89d4fca61951a834eb71ae5ead05"},"404":{"$ref":"#/components/responses/b4ecc1bf00d53a12415dae5d38b7621e"},"401":{"$ref":"#/components/responses/0a4a4ed475b8d9f9c9ebe366dd2d5ed8"},"403":{"$ref":"#/components/responses/8b924c99eac62f12249368bcaf584861"},"500":{"$ref":"#/components/responses/e70e8250a07edb97d2aa0393dc424987"}},"security":[{"basic":[]}]},"post":{"tags":["Deployments"],"summary":"Queue, preview or schedule a deployment","description":"Creates a new deployment for the project. Set mode to 'queue' to execute immediately or 'preview' to generate a preview of changes without deploying. If a schedule block is provided, the deployment is scheduled for future or recurring execution. Use parent_identifier to target a specific server or server group. The deployment status field can be: pending, running, completed, failed, preview_pending, preview_ready, or preview_failed.","operationId":"createProjectDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/ceea7a8b7e1cad825d7b8e26acbae00f"},"responses":{"201":{"$ref":"#/components/responses/66c338cd706d4c64d764e744c74aeaf0"},"404":{"$ref":"#/components/responses/6ed61e50c53ee4973a1205426305ecc0"},"422":{"$ref":"#/components/responses/749b43a9980c5cb9decf594ffb684583"},"401":{"$ref":"#/components/responses/6a79781379cb353118a711246dc22eef"},"403":{"$ref":"#/components/responses/c635c1642d9ee16a204b24cc17ef3f3c"},"500":{"$ref":"#/components/responses/efd3385d3b8b357ae77a0a17c60fc1cb"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployments/{deployment_id}/steps/{step_id}/logs":{"get":{"tags":["Deployments"],"summary":"List log entries for a deployment step","description":"Returns log entries for a specific deployment step, with optional pagination and download support.","operationId":"listProjectDeploymentStepLogs","parameters":[{"$ref":"#/components/parameters/c307dd4403ec6fabb12df075a19b849c"},{"$ref":"#/components/parameters/1edb42db3fb9d50012c36c500d141f5a"},{"$ref":"#/components/parameters/3747d4604d2cb5485f944c4641d33c84"},{"$ref":"#/components/parameters/4fc417275517e159e1165032a98adb50"},{"$ref":"#/components/parameters/c5ed88df88fbc32a355fc3b0fc86f340"}],"responses":{"200":{"$ref":"#/components/responses/a0a9b94e5cbb6a931ae8eff7f8c3a96b"},"401":{"$ref":"#/components/responses/dd5f60a67708c2b1084ef977621521f0"},"403":{"$ref":"#/components/responses/99d1ef0f248f46e54baf62903128098d"},"500":{"$ref":"#/components/responses/02f2fd64d45c216c2aaf1f47ec49a7e7"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployments/{id}":{"get":{"tags":["Deployments"],"summary":"View a deployment","description":"Returns the full details of a deployment including its steps and log entries. Archived and legacy deployments return simplified responses.","operationId":"getProjectDeploymentById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/c1cf4fde64acdf07b8c3c82cf08cc20e"}],"responses":{"200":{"$ref":"#/components/responses/9931acd2244a28643adb066c29564a3a"},"404":{"$ref":"#/components/responses/3efe45f6b4650f4a1b405c1038ce78f3"},"401":{"$ref":"#/components/responses/08dd1ac0bd18e5d016837c436c44698a"},"403":{"$ref":"#/components/responses/45beaa7574b7c7261226108c0c5d06e7"},"500":{"$ref":"#/components/responses/9c8edc4bae9b0a3473ffebe37958c71c"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployments/{id}/abort":{"post":{"tags":["Deployments"],"summary":"Abort a running deployment","description":"Immediately aborts a deployment that is currently in progress.","operationId":"abortProjectDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/c1cf4fde64acdf07b8c3c82cf08cc20e"}],"responses":{"200":{"$ref":"#/components/responses/807eabbb5ac8ffb92630fa8936ae0571"},"404":{"$ref":"#/components/responses/310293f1146ed4ca650cd2a849b177ec"},"401":{"$ref":"#/components/responses/86b88b4dcc0b8f2614654c9ce9689a1e"},"403":{"$ref":"#/components/responses/2edec342f2f7663957be6c7e5a472c86"},"500":{"$ref":"#/components/responses/c02890e79558d2a30bb669db6b8f1af9"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployments/{id}/retry":{"post":{"tags":["Deployments"],"summary":"Retry a deployment","description":"Retries a failed deployment by resetting it and re-queuing. Only deployments that have completed (successfully or with failure) can be retried.","operationId":"retryProjectDeploymentRetry","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/c79344c276c2fa0e153492f462d1ffd2"}],"responses":{"200":{"$ref":"#/components/responses/35e2140ff3278cede33df3db10f63e66"},"404":{"$ref":"#/components/responses/60dafaa6c493c115ad917b9f1b83c234"},"422":{"$ref":"#/components/responses/726694ae92ad727f826286a90545820b"},"401":{"$ref":"#/components/responses/99bfcdcf33df2924d5b9414382b46740"},"403":{"$ref":"#/components/responses/75764a63a87faeea1dce23a71af71a98"},"500":{"$ref":"#/components/responses/2ea582827f2f7db38bc8917c53334ff7"}},"security":[{"basic":[]}]}},"/projects/{project_id}/deployments/{id}/rollback":{"post":{"tags":["Deployments"],"summary":"Rollback a deployment","description":"Creates a new deployment that reverts the project to the state of the specified deployment. Archived deployments cannot be rolled back.","operationId":"rollbackProjectDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/21bc279099387a81c2d9173928f7494b"},{"$ref":"#/components/parameters/d7f969dda7b74656f28268a86c9bcc17"},{"$ref":"#/components/parameters/4a747e2207b0034846f238e886a65635"},{"$ref":"#/components/parameters/9f3ef084823dc184e5d40ee514501451"}],"responses":{"200":{"$ref":"#/components/responses/dd05608b05805fc5e32e092f85b70c24"},"404":{"$ref":"#/components/responses/368097c8a9e416b79d0285a47c07f114"},"422":{"$ref":"#/components/responses/d9e0a9d2e239ba3618994a7e362a5b8e"},"401":{"$ref":"#/components/responses/d51d7662b4efbf795bc2cc21a4b24ada"},"403":{"$ref":"#/components/responses/a2891a7cc9d0aa4a94a98daa94fdd4a4"},"500":{"$ref":"#/components/responses/fb97273a730eb3d7b781b290c7697d56"}},"security":[{"basic":[]}]}},"/projects/{project_id}/environment_variables":{"get":{"tags":["Environment Variables"],"summary":"List environment variables","description":"Returns all environment variables for the project, ordered by name. Variables can be scoped to the build pipeline or specific servers.","operationId":"listProjectEnvironmentVariables","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/8b302ebaa785858cc1cf71c2440720ef"},"401":{"$ref":"#/components/responses/7582a8f5ea7f4f97093d76807d0cad87"},"403":{"$ref":"#/components/responses/d9fd85cb49fbf91fbeb59eca24a266de"},"500":{"$ref":"#/components/responses/5b8de5ba330d635fe947a221ac501230"}},"security":[{"basic":[]}]},"post":{"tags":["Environment Variables"],"summary":"Create an environment variable","description":"Creates a new environment variable for the project. Variables can be locked to prevent value visibility and scoped to the build pipeline.","operationId":"createProjectEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/9b8d3a49e6398da6511e4c7519af3ec3"},"responses":{"201":{"$ref":"#/components/responses/efaea3e9a6696870004c5b4b01c95889"},"422":{"$ref":"#/components/responses/21e3467ea4b89f365274442be370661c"},"401":{"$ref":"#/components/responses/3795d87eeadd70a474ef98ed27dd7a50"},"403":{"$ref":"#/components/responses/01cb140d81ade074bae672f31dc6ba48"},"500":{"$ref":"#/components/responses/a706250d270aeafdb851e937db93037e"}},"security":[{"basic":[]}]}},"/projects/{project_id}/environment_variables/{id}":{"get":{"tags":["Environment Variables"],"summary":"Get an environment variable","description":"Returns details of a specific environment variable. Locked variables will have their values masked.","operationId":"getProjectEnvironmentVariableById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"responses":{"200":{"$ref":"#/components/responses/5159c9bed1c08fa0c4b8e94724f9e9f7"},"404":{"$ref":"#/components/responses/066a3dc7b98daf76979c7d5aae039e75"},"401":{"$ref":"#/components/responses/37f87bc5dd60e2c916c4a91c20e536d1"},"403":{"$ref":"#/components/responses/42cbd456672296f6bdd5fe17f18eeabc"},"500":{"$ref":"#/components/responses/0db474368aecd2049008e9896cd2c52a"}},"security":[{"basic":[]}]},"put":{"tags":["Environment Variables"],"summary":"Update an environment variable","description":"Updates an existing environment variable's name, value, or settings. Server assignments are only modified when server parameters are explicitly provided.","operationId":"replaceProjectEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"requestBody":{"$ref":"#/components/requestBodies/bb3ed1912dcbf60b8425c61d5ba82d27"},"responses":{"200":{"$ref":"#/components/responses/ed47f87c3808f3398d40631e054663a2"},"422":{"$ref":"#/components/responses/a76488923a0204ceac3a4eba05dd252a"},"404":{"$ref":"#/components/responses/f5dec388a643759c53adb89c9c061cb3"},"401":{"$ref":"#/components/responses/1c9a5db8f7625f8dd02d30c80c1356a0"},"403":{"$ref":"#/components/responses/5d169f44231ad98a30b301d0e2e9dbb6"},"500":{"$ref":"#/components/responses/3260015c1ddad593fcac7862ff613f1f"}},"security":[{"basic":[]}]},"patch":{"tags":["Environment Variables"],"summary":"Update an environment variable","description":"Updates an existing environment variable's name, value, or settings. Server assignments are only modified when server parameters are explicitly provided.","operationId":"updateProjectEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"requestBody":{"$ref":"#/components/requestBodies/df8b2a793f86ebd24f336b4229ef29c8"},"responses":{"200":{"$ref":"#/components/responses/be68bbf0210070867a2028df28996075"},"422":{"$ref":"#/components/responses/70213b26ed9230dbc23e3e0c502252ef"},"404":{"$ref":"#/components/responses/b564c7516e5b4dce30e899617174852f"},"401":{"$ref":"#/components/responses/82d08d0c5647cc1c086ea3102e8c9aa5"},"403":{"$ref":"#/components/responses/123e8332e626c8aa46c770d558c805ad"},"500":{"$ref":"#/components/responses/149f128932e613f5716e706f41677c81"}},"security":[{"basic":[]}]},"delete":{"tags":["Environment Variables"],"summary":"Delete an environment variable","description":"Permanently removes an environment variable from the project.","operationId":"deleteProjectEnvironmentVariable","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/1f04e1e5742863db5a0e350bfae5f36e"}],"responses":{"204":{"$ref":"#/components/responses/d168a956f1b2bba151dbb21df3c592cb"},"404":{"$ref":"#/components/responses/721c961d7b1ab5fefd97ebb62e6d6f36"},"401":{"$ref":"#/components/responses/19922290514f7a0e0a8028d72d0872d7"},"403":{"$ref":"#/components/responses/ddb4780aa82ed3692122bea9021cf58d"},"500":{"$ref":"#/components/responses/e0a4a42625bf2529cc1f86a3cf6632ad"}},"security":[{"basic":[]}]}},"/projects/{project_id}/excluded_files":{"get":{"tags":["Excluded Files"],"summary":"List excluded files","description":"Returns all files excluded from deployment for this project.","operationId":"listProjectExcludedFiles","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/cff47f017fcd12c170265df615d80d2e"},"401":{"$ref":"#/components/responses/154b5b485cfe4701595f3eea22f51403"},"403":{"$ref":"#/components/responses/6f7772eae1504e63298be200f6956c51"},"500":{"$ref":"#/components/responses/7660a68de6e38315d9fc47dbae24bc94"}},"security":[{"basic":[]}]},"post":{"tags":["Excluded Files"],"summary":"Create an excluded file","description":"Adds a file path to the project's exclusion list so it is skipped during deployments.","operationId":"createProjectExcludedFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/bd70ff0293a8e7872dde6e688248aa87"},"responses":{"200":{"$ref":"#/components/responses/a58b1cc793a153a03244130c278e606e"},"422":{"$ref":"#/components/responses/41e451c8d24c4547e8f43263193c62e9"},"401":{"$ref":"#/components/responses/70e2a1a96256f10e79653bb2a8889a85"},"403":{"$ref":"#/components/responses/23d35e15f6de9a126bd9e7d1c777e04b"},"500":{"$ref":"#/components/responses/c82610f9b80f041c35e0840868b121db"}},"security":[{"basic":[]}]}},"/projects/{project_id}/excluded_files/{id}":{"get":{"tags":["Excluded Files"],"summary":"Get an excluded file","description":"Returns the details of a single excluded file by its identifier.","operationId":"getProjectExcludedFileById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fa3c64d02d8df4c1914b1c09c37bcba2"}],"responses":{"200":{"$ref":"#/components/responses/6e27a6b546ea41709b0bd9575dc7a284"},"404":{"$ref":"#/components/responses/c55cabc446750f5c827cc7057b85bcfd"},"401":{"$ref":"#/components/responses/3bc8f3808e5e4397bebd6893344b3068"},"403":{"$ref":"#/components/responses/466a7f5dfb7e0d47a765a942322c6178"},"500":{"$ref":"#/components/responses/8121ce3e5a820991f4aa6ce5eb97b490"}},"security":[{"basic":[]}]},"put":{"tags":["Excluded Files"],"summary":"Update an excluded file","description":"Updates the path of an existing excluded file entry.","operationId":"replaceProjectExcludedFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fa3c64d02d8df4c1914b1c09c37bcba2"}],"requestBody":{"$ref":"#/components/requestBodies/abb0ac48efd13b5c9b298a5381c905e5"},"responses":{"200":{"$ref":"#/components/responses/48a498907444145a88e8f919456cfeb4"},"422":{"$ref":"#/components/responses/e2faeaa314f6ac5586ccc02e08fa03c8"},"404":{"$ref":"#/components/responses/5483714e5bc9a71d43b74f4a46f6d265"},"401":{"$ref":"#/components/responses/96d0cf0a4671de521d574c7bb583cb10"},"403":{"$ref":"#/components/responses/db744fba305edb6f1e1125492f3918a6"},"500":{"$ref":"#/components/responses/335b378c4d678b38e241d3ad0bd5c09a"}},"security":[{"basic":[]}]},"patch":{"tags":["Excluded Files"],"summary":"Update an excluded file","description":"Updates the path of an existing excluded file entry.","operationId":"updateProjectExcludedFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fa3c64d02d8df4c1914b1c09c37bcba2"}],"requestBody":{"$ref":"#/components/requestBodies/865772613ff1fd1bc3ce8df514cbb6f0"},"responses":{"200":{"$ref":"#/components/responses/a7bbbf33a116ff6cdc3ebadfc49bef7e"},"422":{"$ref":"#/components/responses/3d82757c44385f238a74f08d565c14e9"},"404":{"$ref":"#/components/responses/82b141c8fb01c1d567987061245b9089"},"401":{"$ref":"#/components/responses/3b6c25d67f471c3b2732fc84f4894bf7"},"403":{"$ref":"#/components/responses/0d73a7592255f6238b09a1ca81a8b205"},"500":{"$ref":"#/components/responses/107f93ad6e1a45fd9e699e07b5c69ac7"}},"security":[{"basic":[]}]},"delete":{"tags":["Excluded Files"],"summary":"Delete an excluded file","description":"Removes a file path from the project's exclusion list.","operationId":"deleteProjectExcludedFile","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fa3c64d02d8df4c1914b1c09c37bcba2"}],"responses":{"200":{"$ref":"#/components/responses/a40f3fd1516f03906aa5a4dcf7bdfc9d"},"500":{"$ref":"#/components/responses/1e58416b03ac3429a0b6e42f0643966e"},"404":{"$ref":"#/components/responses/7fa67203fc619313f5596a60b3bd2df6"},"401":{"$ref":"#/components/responses/7a6cc46669e441edda08550ff4d398e8"},"403":{"$ref":"#/components/responses/79049e2f5e155bdf4db063ae03b71e11"}},"security":[{"basic":[]}]}},"/projects/{project_id}/integrations":{"get":{"tags":["Integrations"],"summary":"List integrations","description":"Returns all permanent integrations configured for this project.","operationId":"listProjectIntegrations","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/c3857d22ee761d8350114d3be48c5fc3"},"401":{"$ref":"#/components/responses/dcb461fa54d0699e223820455d060998"},"403":{"$ref":"#/components/responses/1735d4e3ae2ed9dfa760a29c641926dd"},"500":{"$ref":"#/components/responses/96253e4824d38cb1e80419164980bfed"}},"security":[{"basic":[]}]},"post":{"tags":["Integrations"],"summary":"Create an integration","description":"Creates a new integration for the project. Only certain hook types are allowed via the API.","operationId":"createProjectIntegration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/752c9d3188a7bf05db19ecffaea20671"},"responses":{"200":{"$ref":"#/components/responses/d7868b87a95ae32e1dba244c41103137"},"422":{"$ref":"#/components/responses/14f9bd400650fb4e23893ad84622bfd4"},"401":{"$ref":"#/components/responses/073c384015a3aebd132b3c5fd65c3f31"},"403":{"$ref":"#/components/responses/a50b159910999cd2b7a7233eca43e80a"},"500":{"$ref":"#/components/responses/32d384b6db4c472fcd8ba09b886e2fae"}},"security":[{"basic":[]}]}},"/projects/{project_id}/integrations/{id}":{"get":{"tags":["Integrations"],"summary":"Get an integration","description":"Returns the details of a single integration by its identifier.","operationId":"getProjectIntegrationById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3bad048e25ea875765021c8867c13e11"}],"responses":{"200":{"$ref":"#/components/responses/0522f4dab223287136a872831715fb2f"},"404":{"$ref":"#/components/responses/2dc75b5b269a6b463e69626342ba159a"},"401":{"$ref":"#/components/responses/99bb409477c56c67cf0225839a4a75c9"},"403":{"$ref":"#/components/responses/bc3137fea46cbeea0b9035f9032be83d"},"500":{"$ref":"#/components/responses/443814f6b07dd8826e241df53739cb33"}},"security":[{"basic":[]}]},"put":{"tags":["Integrations"],"summary":"Update an integration","description":"Updates an existing integration's settings. The hook_type cannot be changed via the API.","operationId":"replaceProjectIntegration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3bad048e25ea875765021c8867c13e11"}],"requestBody":{"$ref":"#/components/requestBodies/d0e3bfd42723809256f7084520f8b24b"},"responses":{"200":{"$ref":"#/components/responses/e6a5165fbeeb7f2a39c0f70ffdfec384"},"422":{"$ref":"#/components/responses/c37b048f1bf03a3289e3f23775f87179"},"404":{"$ref":"#/components/responses/9407873d0092adecb8584df77e58bb9f"},"401":{"$ref":"#/components/responses/4aac72487baea0283b9ae8327b611a4d"},"403":{"$ref":"#/components/responses/404ec24631e2c434b16be67b4d8661ee"},"500":{"$ref":"#/components/responses/6e5818c9983b6072c4baa79ffbacdadd"}},"security":[{"basic":[]}]},"patch":{"tags":["Integrations"],"summary":"Update an integration","description":"Updates an existing integration's settings. The hook_type cannot be changed via the API.","operationId":"updateProjectIntegration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3bad048e25ea875765021c8867c13e11"}],"requestBody":{"$ref":"#/components/requestBodies/ce067dbebf9446e2abb78d597601e6ef"},"responses":{"200":{"$ref":"#/components/responses/da8df55b58c8c631ca4fc581b88d3a63"},"422":{"$ref":"#/components/responses/f45babcba6cfaee2de951f93f4fdf28f"},"404":{"$ref":"#/components/responses/1d5a87c3c73c7cbf87219d8c6e7318c3"},"401":{"$ref":"#/components/responses/9cc48e9b53e2bc4baab55569bff8201d"},"403":{"$ref":"#/components/responses/38908bb4014b0165987f26cb46358008"},"500":{"$ref":"#/components/responses/49520b0927c34ea3177dc99c8b872904"}},"security":[{"basic":[]}]},"delete":{"tags":["Integrations"],"summary":"Delete an integration","description":"Permanently removes an integration from the project.","operationId":"deleteProjectIntegration","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/3bad048e25ea875765021c8867c13e11"}],"responses":{"200":{"$ref":"#/components/responses/2db896ac797f4739899f71b7ac796722"},"422":{"$ref":"#/components/responses/6740856f2e764800fd19c5a10b97f3c7"},"404":{"$ref":"#/components/responses/aa30af6f1e6414646719e4d27dcd1da6"},"401":{"$ref":"#/components/responses/a3a296b9032aa6cee0711df892fb9416"},"403":{"$ref":"#/components/responses/b77ea4ef4271c79938dc73669f155d3c"},"500":{"$ref":"#/components/responses/a74f2059131b8fd99c6511cf5926e23a"}},"security":[{"basic":[]}]}},"/projects/{project_id}/language_versions":{"get":{"tags":["Language Versions"],"summary":"Get project language versions","description":"Returns the language versions configured for this project's default build environment.","operationId":"listProjectLanguageVersions","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/e95105c97d93433ba654525b185d6be9"},"401":{"$ref":"#/components/responses/4a912cf038cc7fbcfe8131af62e8d3d0"},"403":{"$ref":"#/components/responses/b6b7aa4aa10b62219beb7cde1f5aba60"},"500":{"$ref":"#/components/responses/bffd15cac38d82fc6ac7018a94863a3b"}},"security":[{"basic":[]}]}},"/projects/{project_id}/repository":{"get":{"tags":["Repositories"],"summary":"View the repository","description":"Returns the repository configuration for the specified project.","operationId":"getProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/40ed130c568b7d53ad6607a0ae2ee6da"},"404":{"$ref":"#/components/responses/c789a1fb476f586b9f288182f59ad7ea"},"401":{"$ref":"#/components/responses/3a64f0a49fa4e21ea947653da12b9c85"},"403":{"$ref":"#/components/responses/0ff1aabda91b6de265e5e193ed9067a5"},"500":{"$ref":"#/components/responses/44172b2f43e49b3ea987d38004529800"}},"security":[{"basic":[]}]},"post":{"tags":["Repositories"],"summary":"Create or replace the repository","description":"Replaces the project's repository configuration. Any existing repository is destroyed and a new one is created with the provided settings.","operationId":"createProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/7d0a3eaf32e664af8e5fb7efbfb4ba70"},"responses":{"200":{"$ref":"#/components/responses/55c2de1948a9c2b7e95acd11b4e42f95"},"422":{"$ref":"#/components/responses/b5f263e30e80da16cb556cac1b80839a"},"401":{"$ref":"#/components/responses/5683cf8ff2e5086cd198fe72f40e0d30"},"403":{"$ref":"#/components/responses/1b3745aa434d4bb704a2e6f52d3d223c"},"500":{"$ref":"#/components/responses/0d37186b0b61056384dce25d1f044256"}},"security":[{"basic":[]}]},"put":{"tags":["Repositories"],"summary":"Update the repository","description":"Updates the repository's branch, root path, or hosting service type without replacing the entire repository configuration.","operationId":"replaceProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/b86cb33d4dfe1414914b609f54fa7e5a"},"responses":{"200":{"$ref":"#/components/responses/0b7ee6ec3eaef7136409e9d53e8dec96"},"422":{"$ref":"#/components/responses/365acb382211406a8767504bdb1d6a35"},"404":{"$ref":"#/components/responses/3416b7e579a49e8140464052bec1ea7a"},"401":{"$ref":"#/components/responses/eb8616630528cd500c0bc794c84de8d9"},"403":{"$ref":"#/components/responses/ec06097c9ab414447e3c9e2429c4b247"},"500":{"$ref":"#/components/responses/1c5bfb2e492be055a9e8cfd8facb9ed7"}},"security":[{"basic":[]}]},"patch":{"tags":["Repositories"],"summary":"Update the repository","description":"Updates the repository's branch, root path, or hosting service type without replacing the entire repository configuration.","operationId":"updateProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/597169c8196ce72a442026c4066f9f91"},"responses":{"200":{"$ref":"#/components/responses/b78a516f08f313ebf966d08ef14bf569"},"422":{"$ref":"#/components/responses/39baed0239959d1d275d681f031be251"},"404":{"$ref":"#/components/responses/290f301b7c8c813dc260f264c88fccab"},"401":{"$ref":"#/components/responses/822b9aedccb72cc4f614e41f784e4fd8"},"403":{"$ref":"#/components/responses/610db3a0958e1d7c5994c9637718a341"},"500":{"$ref":"#/components/responses/37ddff985a4e62d003046ebe2c567924"}},"security":[{"basic":[]}]}},"/projects/{project_id}/repository/branches":{"get":{"tags":["Repositories"],"summary":"Branches","description":"Returns a map of branch names and their latest commit references for the project's repository.","operationId":"branchesProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/28f5d02aedce1df672a0b2d39611e56c"},"401":{"$ref":"#/components/responses/73f26899a01ec2f902a4336e6fac3943"},"403":{"$ref":"#/components/responses/ec7d8d45a274c3c4b22a3f9c70c37e04"},"500":{"$ref":"#/components/responses/2ee64a17ea728cee3772b4ca5f0c1a16"}},"security":[{"basic":[]}]}},"/projects/{project_id}/repository/commit_info":{"get":{"tags":["Repositories"],"summary":"Commit Info","description":"Returns detailed information about a specific commit including author, message, tags, and avatar URL. Queues a repository update if the commit is not found.","operationId":"commitInfoProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/1563d47f1781e48601105aabbfed33e3"}],"responses":{"200":{"$ref":"#/components/responses/ce7b37b2aba51447dc1e56ce25a3f103"},"404":{"$ref":"#/components/responses/13399002e8c9d0efa43cfcd51f439653"},"422":{"$ref":"#/components/responses/096b62c9e1d083fb068243d2e7f155ff"},"401":{"$ref":"#/components/responses/4599a43004b2f5836b8f4883a9035434"},"403":{"$ref":"#/components/responses/7ac62c40f4c477c9e8436c94bc7fee76"},"500":{"$ref":"#/components/responses/9675afccb35cbdfef5513f8fd9f6b3da"}},"security":[{"basic":[]}]}},"/projects/{project_id}/repository/latest_revision":{"get":{"tags":["Repositories"],"summary":"Latest Revision","description":"Returns the latest remote commit reference for the specified branch.","operationId":"latestRevisionProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/25be58a5af8084855f060c1b13c29728"}],"responses":{"200":{"$ref":"#/components/responses/c2e39330dba5a234c9d32695338126e6"},"401":{"$ref":"#/components/responses/b16531b80170f406f825e9180af22217"},"403":{"$ref":"#/components/responses/cccf5fa9e57517727077c383ae704955"},"500":{"$ref":"#/components/responses/21bb7ba7b03ed35440b09f1564999d9d"}},"security":[{"basic":[]}]}},"/projects/{project_id}/repository/recent_commits":{"get":{"tags":["Repositories"],"summary":"Recent Commits","description":"Returns recent commits and tags for the specified branch. Set update to '1' to fetch the latest data from the remote before returning results.\nRenders the data for the commit selector popup","operationId":"recentCommitsProjectRepository","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/bbfda3f1afbf916883c7c5e5d714acd2"},{"$ref":"#/components/parameters/08820948331d205348d4944917c5a376"}],"responses":{"200":{"$ref":"#/components/responses/9842d98c6c50cfeb7e427db5a13c946f"},"401":{"$ref":"#/components/responses/f8362dfa925794f8093cde435a12064a"},"403":{"$ref":"#/components/responses/b85b36ff27898e29fc5312325cca63a5"},"500":{"$ref":"#/components/responses/9538b824662c27daa2aac84a520bfaf5"}},"security":[{"basic":[]}]}},"/projects/{project_id}/scheduled_deployments":{"get":{"tags":["Scheduled Deployments"],"summary":"List scheduled deployments","description":"Returns all forthcoming scheduled deployments for the project, ordered ascending by scheduled time.","operationId":"listProjectScheduledDeployments","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/504a6bebe604b1b1fcc6a5fe19179dda"},"401":{"$ref":"#/components/responses/a53af47e957dfc517594d0a951d35481"},"403":{"$ref":"#/components/responses/27479d833b96fe80a48c25294029bc50"},"500":{"$ref":"#/components/responses/ce5de726bf90cab21abcd39631591ca4"}},"security":[{"basic":[]}]},"post":{"tags":["Scheduled Deployments"],"summary":"Create a scheduled deployment","description":"Creates a new scheduled deployment for the project.","operationId":"createProjectScheduledDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/42d9393c6abd8d537707081c34bfe855"},"responses":{"201":{"$ref":"#/components/responses/1032b2402c1d8ff0ea1a62e493d3e51c"},"422":{"$ref":"#/components/responses/750d1929299f00cf8c5e367fe29f96fd"},"401":{"$ref":"#/components/responses/e3bd9ac4da482832cdf5b946d76dcd52"},"403":{"$ref":"#/components/responses/c4a4914af306f43902229141ff231fd9"},"500":{"$ref":"#/components/responses/66c60105a7177d6b1ff914b30fe4672a"}},"security":[{"basic":[]}]}},"/projects/{project_id}/scheduled_deployments/{id}":{"get":{"tags":["Scheduled Deployments"],"summary":"Get a scheduled deployment","description":"Returns the details of a single scheduled deployment.","operationId":"getProjectScheduledDeploymentById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/6f7ef02510ee8ccdca1dc05608414123"}],"responses":{"200":{"$ref":"#/components/responses/7fa7fa4566f34d4259affd05480217f2"},"404":{"$ref":"#/components/responses/efa21c76689355236b36e65c3e57d4d1"},"401":{"$ref":"#/components/responses/5957a685ac5bd791255f00ced12af650"},"403":{"$ref":"#/components/responses/784be6eae82c3421b820fb818958a289"},"500":{"$ref":"#/components/responses/350103304c1431153b80c6a1e05dde29"}},"security":[{"basic":[]}]},"put":{"tags":["Scheduled Deployments"],"summary":"Update a scheduled deployment","description":"Updates an existing scheduled deployment.","operationId":"replaceProjectScheduledDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/6f7ef02510ee8ccdca1dc05608414123"}],"requestBody":{"$ref":"#/components/requestBodies/4f3055165f11efbef0bc59b616abe9b7"},"responses":{"200":{"$ref":"#/components/responses/8ac305389f01ce019e5997fb7fb7c8ac"},"422":{"$ref":"#/components/responses/341ab7e5f02957b3b37d5b9cd0e14a4d"},"404":{"$ref":"#/components/responses/779ba58ac87b0dd920d957f2e75b3d0a"},"401":{"$ref":"#/components/responses/2026063774a3aa39bf2e1917df65242d"},"403":{"$ref":"#/components/responses/eabdfed8cbc884e398ad499bf908c5f6"},"500":{"$ref":"#/components/responses/f55d92c2312fcc3f15ae4354a8fed090"}},"security":[{"basic":[]}]},"patch":{"tags":["Scheduled Deployments"],"summary":"Update a scheduled deployment","description":"Updates an existing scheduled deployment.","operationId":"updateProjectScheduledDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/6f7ef02510ee8ccdca1dc05608414123"}],"requestBody":{"$ref":"#/components/requestBodies/a11e19a5e662610aac4117d318d047dd"},"responses":{"200":{"$ref":"#/components/responses/836bb3a2a5533d8e8726af0497df528e"},"422":{"$ref":"#/components/responses/87bdbc6757e806209d359f3738f0987a"},"404":{"$ref":"#/components/responses/0471873bbb4e63c7418e5321b7577472"},"401":{"$ref":"#/components/responses/4cdecf49cc3560e084e7f7123e501bdd"},"403":{"$ref":"#/components/responses/0c1c59b187abb03a2da3fab99ed91e37"},"500":{"$ref":"#/components/responses/f7d61ce1d6a294d463083e3732e21f4c"}},"security":[{"basic":[]}]},"delete":{"tags":["Scheduled Deployments"],"summary":"Cancel a scheduled deployment","description":"Cancels and deletes a scheduled deployment that has not yet run.","operationId":"deleteProjectScheduledDeployment","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/6f7ef02510ee8ccdca1dc05608414123"}],"responses":{"200":{"$ref":"#/components/responses/f40428a1b46102d0c2d0143dcae3d5ec"},"422":{"$ref":"#/components/responses/1faba8e89844a784cd067d1a02fabfae"},"404":{"$ref":"#/components/responses/ce5dda4f7120718a0514e401c4d234f7"},"401":{"$ref":"#/components/responses/67063bd21241ef7f9d30ff7bb0613180"},"403":{"$ref":"#/components/responses/46800fdf72dc4334956146ca931b66c4"},"500":{"$ref":"#/components/responses/1f52fc405df057e2f6bc3383422d7ed0"}},"security":[{"basic":[]}]}},"/projects/{project_id}/server_groups":{"get":{"tags":["Server Groups"],"summary":"List server groups","description":"Returns all server groups for the project, ordered ascending.","operationId":"listProjectServerGroups","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/5ff4c8c5ccf36009800ffa5af773c226"},"401":{"$ref":"#/components/responses/95484088ad7bcd949b69f347845534ed"},"403":{"$ref":"#/components/responses/a944004ae7d204831747fca5f493f080"},"500":{"$ref":"#/components/responses/b36dd25f67c493b21ff430590032f838"}},"security":[{"basic":[]}]},"post":{"tags":["Server Groups"],"summary":"Create a server group","description":"Creates a new server group for the project to organize servers and control deployment order.","operationId":"createProjectServerGroup","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/cedb4e47fa9d1c5a3536b8e382d1673d"},"responses":{"200":{"$ref":"#/components/responses/6d3c2fac14e7e5f50ebd86fafd683175"},"422":{"$ref":"#/components/responses/af1731085713c9ca6ac2a1010f970f2a"},"401":{"$ref":"#/components/responses/31cf677053251122820f18fd71bd7a4f"},"403":{"$ref":"#/components/responses/2193b5a15c83ec3af4ee488f6fbf4bd5"},"500":{"$ref":"#/components/responses/0428433a03603f99411860491dcbe91e"}},"security":[{"basic":[]}]}},"/projects/{project_id}/server_groups/{id}":{"get":{"tags":["Server Groups"],"summary":"Get a server group","description":"Returns the details of a single server group.","operationId":"getProjectServerGroupById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2fde178d2b6116ce573c7925ae4ea619"}],"responses":{"200":{"$ref":"#/components/responses/1acbcd33819bbba55b2d94070bffccc0"},"404":{"$ref":"#/components/responses/892e79fa70811dbfd341ccd75ef53a45"},"401":{"$ref":"#/components/responses/2e0bffa74fe6519dfd5b47fe3eb06143"},"403":{"$ref":"#/components/responses/b49c5a50a7d1a3315042f9896a794b2d"},"500":{"$ref":"#/components/responses/8b581e09b9e13127cb896fdae1da8df5"}},"security":[{"basic":[]}]},"put":{"tags":["Server Groups"],"summary":"Update a server group","description":"Updates an existing server group's settings such as name, branch, or transfer order.","operationId":"replaceProjectServerGroup","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2fde178d2b6116ce573c7925ae4ea619"}],"requestBody":{"$ref":"#/components/requestBodies/ecf1d6749e27ef216bef0e4cb25decd1"},"responses":{"200":{"$ref":"#/components/responses/86cff6ebdc96c12027fd446415cc2ed0"},"422":{"$ref":"#/components/responses/592006f4f4d16b64e2676febfc83b1b0"},"404":{"$ref":"#/components/responses/5c0057ca25510e944a74f1f028d61bbc"},"401":{"$ref":"#/components/responses/b445cbd603b397090e1e115ffb459bae"},"403":{"$ref":"#/components/responses/911c4dbdd40444b025438fcb22f681c4"},"500":{"$ref":"#/components/responses/eed887f259d56cbfee15240517413c98"}},"security":[{"basic":[]}]},"patch":{"tags":["Server Groups"],"summary":"Update a server group","description":"Updates an existing server group's settings such as name, branch, or transfer order.","operationId":"updateProjectServerGroup","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2fde178d2b6116ce573c7925ae4ea619"}],"requestBody":{"$ref":"#/components/requestBodies/ea431c4ea1a8a9fb806ab403db572167"},"responses":{"200":{"$ref":"#/components/responses/2444eaef0edff32d18264255ebe8d72c"},"422":{"$ref":"#/components/responses/acaa3c12ef50f2a6ef665d6aaffd7b4e"},"404":{"$ref":"#/components/responses/655a94ab0a530d84cf0709041b80e62e"},"401":{"$ref":"#/components/responses/a4833ea7f2626c30ddb042b27d4f881d"},"403":{"$ref":"#/components/responses/360f7899635fe86396f475191b3caa63"},"500":{"$ref":"#/components/responses/8421b43d99e7b592fdfdaa8010c6a061"}},"security":[{"basic":[]}]},"delete":{"tags":["Server Groups"],"summary":"Delete a server group","description":"Permanently removes a server group from the project.","operationId":"deleteProjectServerGroup","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/2fde178d2b6116ce573c7925ae4ea619"}],"responses":{"200":{"$ref":"#/components/responses/7d2c502ead02ecb3076930fc3f6fe1ef"},"404":{"$ref":"#/components/responses/5a09582f926cffd73ef301af4de0a050"},"401":{"$ref":"#/components/responses/390231c9e3eadbd859d42939560b3c74"},"403":{"$ref":"#/components/responses/1e58d11f12d2cab98c0235f9db4711c5"},"500":{"$ref":"#/components/responses/e925cc5dee57eac42c0bf4d6d9f53b2a"}},"security":[{"basic":[]}]}},"/projects/{project_id}/servers":{"get":{"tags":["Servers"],"summary":"List all servers","description":"Returns all servers belonging to the specified project, including their agent associations.","operationId":"listProjectServers","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/9878ac8123afacaf53491d870c50e3b1"},"401":{"$ref":"#/components/responses/06ecc44a95bfd2456bf246d4b9c3b1f1"},"403":{"$ref":"#/components/responses/0432deb0f7342624cee676c46f5f8271"},"500":{"$ref":"#/components/responses/e467eba917f4d1d6a36a842a2ea4277d"}},"security":[{"basic":[]}]},"post":{"tags":["Servers"],"summary":"Create a server","description":"Creates a new server in the project. The protocol_type determines which additional connection parameters are required: SSH needs hostname and username, FTP needs hostname, username, and password, S3 needs bucket_name, access_key_id, and secret_access_key. For managed_vps, pass region, size, and os_image at the top level. For static_hosting, pass hosted_website_attributes at the top level.","operationId":"createProjectServer","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/c7dc419041385c74efb8b5972cf3d9d1"},"responses":{"200":{"$ref":"#/components/responses/dbc33a0d805595caadb02445b2c9042a"},"422":{"$ref":"#/components/responses/e8de2e516ac3e7467f08a04ef28ef2e7"},"401":{"$ref":"#/components/responses/24f2c79f23348230f628f39bdc81ece8"},"403":{"$ref":"#/components/responses/80249248f0e050cac478ee1bafa87b21"},"500":{"$ref":"#/components/responses/4739f76e91e55ce5a4c42ca867de5bca"}},"security":[{"basic":[]}]}},"/projects/{project_id}/servers/{id}":{"get":{"tags":["Servers"],"summary":"View a server","description":"Returns the full details of a single server including its connection configuration.\nReturns server details including provisioning state for managed protocols. For static_hosting servers the\nresponse includes a `static_hosting` block (status, url). For managed_vps servers the response includes a\n`managed_vps` block (status, ip_address, region, size). Poll this endpoint to track provisioning.","operationId":"getProjectServerById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fb7f2a6f88abe988d789b08b4345de52"}],"responses":{"200":{"$ref":"#/components/responses/4c3cb25e93bb75a2955e2c3f9d178521"},"404":{"$ref":"#/components/responses/758d076076eff718cd34b8d4072fb4e0"},"401":{"$ref":"#/components/responses/d29531a670483e81279e3d30e9537e41"},"403":{"$ref":"#/components/responses/d6013e069f366818d7582ac1524e4b47"},"500":{"$ref":"#/components/responses/57ea70f927795e28b9a3e0ac9cc64f69"}},"security":[{"basic":[]}]},"put":{"tags":["Servers"],"summary":"Update a server","description":"Updates an existing server's configuration. If the server uses external authentication and is not yet authenticated, a redirect to the OAuth URL is returned.","operationId":"replaceProjectServer","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fb7f2a6f88abe988d789b08b4345de52"}],"requestBody":{"$ref":"#/components/requestBodies/d43e1c79f7b051844eb932de3c509ba7"},"responses":{"200":{"$ref":"#/components/responses/26e54ceee751e33b86dbfab6770a92ea"},"422":{"$ref":"#/components/responses/2266bbf93cd65671a5dbf791044854dd"},"404":{"$ref":"#/components/responses/1e6f4f5f3502ce62d4c1f5afd043332c"},"401":{"$ref":"#/components/responses/de15a8a3b11908c6dc0235a78294ffa5"},"403":{"$ref":"#/components/responses/f19f93b825ed0cf90dbbe1e829ddd977"},"500":{"$ref":"#/components/responses/ccaf8b07478e775544f3dd154fdb4101"}},"security":[{"basic":[]}]},"patch":{"tags":["Servers"],"summary":"Update a server","description":"Updates an existing server's configuration. If the server uses external authentication and is not yet authenticated, a redirect to the OAuth URL is returned.","operationId":"updateProjectServer","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fb7f2a6f88abe988d789b08b4345de52"}],"requestBody":{"$ref":"#/components/requestBodies/44eab7f16b0a60f80cb907a2302747b5"},"responses":{"200":{"$ref":"#/components/responses/43f7b06b8c15dee9310b76b3d7ddb9b5"},"422":{"$ref":"#/components/responses/a94921f35cffbaf79a55b9feb02fa42e"},"404":{"$ref":"#/components/responses/b30e49b0ec2080b276079c692439523b"},"401":{"$ref":"#/components/responses/0a1ac69cac4b41cffab5f685a05bcd58"},"403":{"$ref":"#/components/responses/bd3859e9fc72e877a1c31cf21ced1f7a"},"500":{"$ref":"#/components/responses/faf092eb23192355b9e1f6af8a7910f0"}},"security":[{"basic":[]}]},"delete":{"tags":["Servers"],"summary":"Delete a server","description":"Removes a server from the project. Fails if any deployments are currently running against this server.","operationId":"deleteProjectServer","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fb7f2a6f88abe988d789b08b4345de52"}],"responses":{"200":{"$ref":"#/components/responses/0983c0083858e0ddced0daa4628460d5"},"422":{"$ref":"#/components/responses/8fdc9fdfe049277e02577a75b0368c10"},"404":{"$ref":"#/components/responses/a4b58688072172c535adf96fae646970"},"401":{"$ref":"#/components/responses/d8a3c7fed3c9dec1fd04ad9908d10848"},"403":{"$ref":"#/components/responses/a9e47e9053a40bb7358385bb89d94523"},"500":{"$ref":"#/components/responses/1c8e77f0e47598efcd90275b20981388"}},"security":[{"basic":[]}]}},"/projects/{project_id}/servers/{id}/reset_host_key":{"post":{"tags":["Servers"],"summary":"Reset Host Key","description":"Clears the stored SSH host key for the server, allowing it to accept a new key on the next connection.","operationId":"resetHostKeyProjectServer","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fb7f2a6f88abe988d789b08b4345de52"}],"responses":{"200":{"$ref":"#/components/responses/8e751791121aac02129ee338d47adba2"},"401":{"$ref":"#/components/responses/f4fd2ff8a1a8aa54b11fc511f51f8ef4"},"403":{"$ref":"#/components/responses/9ca2f4f8789b543479e2f4128ade1ebd"},"500":{"$ref":"#/components/responses/a62dbd973de4f912e2a15cb561ed0d15"}},"security":[{"basic":[]}]}},"/projects/{project_id}/servers/{id}/server_metrics":{"get":{"tags":["Servers"],"summary":"Get server metrics","description":"Returns real-time server metrics (CPU, memory, disk, uptime) collected over SSH. Requires beta features and an SSH-capable server.","operationId":"serverMetricsProjectServerServerMetric","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/fb7f2a6f88abe988d789b08b4345de52"}],"responses":{"200":{"$ref":"#/components/responses/3a00ee242794f1ce42a67e0631b9aefd"},"403":{"$ref":"#/components/responses/edfed966d5666948d522ec5056bd7345"},"422":{"$ref":"#/components/responses/75b649dc841bacc7409cca70af93ca6f"},"401":{"$ref":"#/components/responses/5a6ec65510f239b29e8dbfdfdbb7fcff"},"500":{"$ref":"#/components/responses/9730567a843a046988febb274aad2d57"}},"security":[{"basic":[]}]}},"/projects/{project_id}/servers/{server_id}/test_access":{"post":{"tags":["Test Access"],"summary":"Run test access","description":"Triggers a test access run for the project. Tests repository connectivity and all server connections.\nIf a server_id is provided (via nested route), only that server is tested.","operationId":"createProjectServerTestAccess","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/d68c0ce894c748b089f5022ebaae9065"}],"responses":{"201":{"$ref":"#/components/responses/52c838a799e7050ab4fbba8096d46c4d"},"401":{"$ref":"#/components/responses/3fbc108fcb39840aacc231a2126d17de"},"403":{"$ref":"#/components/responses/dfffff19431c4d091d3a119c975df0d0"},"500":{"$ref":"#/components/responses/66b7b4ad1e7eff3755d7576bcc8c1bae"},"422":{"$ref":"#/components/responses/eb693318490641cb808c37dfa19cc0fb"}},"security":[{"basic":[]}]}},"/projects/{project_id}/ssh_commands":{"get":{"tags":["Ssh Commands"],"summary":"List SSH commands","description":"Returns all SSH commands configured for the project, ordered ascending. SSH commands run on target servers before or after deployment.","operationId":"listProjectSshCommands","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"200":{"$ref":"#/components/responses/297e806d231affb51615ef5175ca74df"},"401":{"$ref":"#/components/responses/4067c5bbb4601cb5481af3aea1e058c3"},"403":{"$ref":"#/components/responses/271867a5854832192d3261213b01ab45"},"500":{"$ref":"#/components/responses/8d18689254b840dc2bc88d9ed98579b9"}},"security":[{"basic":[]}]},"post":{"tags":["Ssh Commands"],"summary":"Create an SSH command","description":"Creates a new SSH command for the project. Commands can be configured to run before or after deployment on specified servers.","operationId":"createProjectSshCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"requestBody":{"$ref":"#/components/requestBodies/72006bd395bd93afb9a623fff5c79da0"},"responses":{"200":{"$ref":"#/components/responses/68f23fccdf39eb43f7066c4762f469ea"},"422":{"$ref":"#/components/responses/d261d01cf46bf61ec644a9e49a4b764b"},"401":{"$ref":"#/components/responses/fbc802a1584de6e937091dc1951e7c5b"},"403":{"$ref":"#/components/responses/36599f877b78ee04e2059f6a16b28c30"},"500":{"$ref":"#/components/responses/a1ac20b8c50fc1ab08c97ee8e7f1da4f"}},"security":[{"basic":[]}]}},"/projects/{project_id}/ssh_commands/{id}":{"get":{"tags":["Ssh Commands"],"summary":"Get an SSH command","description":"Returns details of a specific SSH command, including its timing, target servers, and error handling settings.","operationId":"getProjectSshCommandById","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"responses":{"200":{"$ref":"#/components/responses/0ef94e0c78be7eccd9463191e4a70060"},"404":{"$ref":"#/components/responses/16a2533025662f4a2f20adc7170533fc"},"401":{"$ref":"#/components/responses/f9e6b4a526384191b99d24d2f79c4e86"},"403":{"$ref":"#/components/responses/c89d1db274b44f8eb40f3f1bd780bfb3"},"500":{"$ref":"#/components/responses/afe27ad81f16df4f1385c9522545e1ef"}},"security":[{"basic":[]}]},"put":{"tags":["Ssh Commands"],"summary":"Update an SSH command","description":"Updates an existing SSH command's properties. Server assignments are only modified when server parameters are explicitly provided.","operationId":"replaceProjectSshCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"requestBody":{"$ref":"#/components/requestBodies/c0b5a2d39f60bf2fea0c5b1b5e61c15f"},"responses":{"200":{"$ref":"#/components/responses/e56c758afc5cae42a69ee092499edcd2"},"422":{"$ref":"#/components/responses/6612a7b5c3277c5f5f3a3ff8f1da679f"},"404":{"$ref":"#/components/responses/ac791ded1b60b1197d3dd570343fb4d8"},"401":{"$ref":"#/components/responses/b3d37e2420036f16165fc1eedd2a6f15"},"403":{"$ref":"#/components/responses/ded75d310bb3fde675e08ca12106b8e6"},"500":{"$ref":"#/components/responses/29a0a2bc421348d3b47e8a65da0dba31"}},"security":[{"basic":[]}]},"patch":{"tags":["Ssh Commands"],"summary":"Update an SSH command","description":"Updates an existing SSH command's properties. Server assignments are only modified when server parameters are explicitly provided.","operationId":"updateProjectSshCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"requestBody":{"$ref":"#/components/requestBodies/1ae1e9d3da172150ef6f7f2bfd63f5ab"},"responses":{"200":{"$ref":"#/components/responses/b9d7e57ae385105f320d590fb3440e79"},"422":{"$ref":"#/components/responses/94d94c286119d59d14090ba533314cfc"},"404":{"$ref":"#/components/responses/b1e9589245fad77b56ad6c7b6b56b228"},"401":{"$ref":"#/components/responses/21279ed37ace7fbd41f608857670e6e7"},"403":{"$ref":"#/components/responses/d16371942e2feb31dd937e3cceffdc80"},"500":{"$ref":"#/components/responses/f0a32e24a824a27c3c942fc1e3f12788"}},"security":[{"basic":[]}]},"delete":{"tags":["Ssh Commands"],"summary":"Delete an SSH command","description":"Removes an SSH command from the project.","operationId":"deleteProjectSshCommand","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/55c7272632d1eb6c3cc834b5cd16bc4a"}],"responses":{"200":{"$ref":"#/components/responses/ad66716b408e41b5b762b3d7c480d059"},"500":{"$ref":"#/components/responses/b54132ffbc15af27bf6fe60ed9a15cdd"},"404":{"$ref":"#/components/responses/802ba874ea51c9747eea2827059dcd97"},"401":{"$ref":"#/components/responses/1f00eaa686c9daddd216ae188cc9a39c"},"403":{"$ref":"#/components/responses/8c1b351c1a16e2a57ec9a19cd11a950a"}},"security":[{"basic":[]}]}},"/projects/{project_id}/test_access":{"post":{"tags":["Test Access"],"summary":"Run test access","description":"Triggers a test access run for the project. Tests repository connectivity and all server connections.\nIf a server_id is provided (via nested route), only that server is tested.","operationId":"createProjectTestAccess","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"}],"responses":{"201":{"$ref":"#/components/responses/32df4c470370d7e51d1012503efc57d8"},"401":{"$ref":"#/components/responses/be08c5118a9b7faeb8e6998a2b83546f"},"403":{"$ref":"#/components/responses/dff21af36d1c228d49bd312712d02d7a"},"500":{"$ref":"#/components/responses/6f74c367379b1ce47542dde863d39c70"},"422":{"$ref":"#/components/responses/f152f05f1d0fe1a37eec7dfa268258d6"}},"security":[{"basic":[]}]}},"/projects/{project_id}/test_access/{id}":{"get":{"tags":["Test Access"],"summary":"Get test access results","description":"Returns the current status and results of a test access run.","operationId":"getProjectTestAccess","parameters":[{"$ref":"#/components/parameters/c6618cdd1a12a98566027ef478919569"},{"$ref":"#/components/parameters/a46a7ac00a2b2976ea75e8a8e940e6bf"}],"responses":{"200":{"$ref":"#/components/responses/c588bf5d013d7bc7d5a936c9aa85742e"},"404":{"$ref":"#/components/responses/072057c6c79682279ddc39e31f22201b"},"401":{"$ref":"#/components/responses/bb04bc423ebe8b8c10bcd0b2168a92c3"},"403":{"$ref":"#/components/responses/81818c4dc78e544df2ae03b3e0b2c411"},"500":{"$ref":"#/components/responses/6e8914267fad1109497a112375863481"}},"security":[{"basic":[]}]}},"/reset":{"post":{"tags":["Account"],"summary":"Reset password","description":"Initiates a password reset for the given email address. No authentication required.","operationId":"resetPasswordSubmitReset","requestBody":{"$ref":"#/components/requestBodies/5f0f148c45a3280e2dad7435a90c2c7d"},"responses":{"200":{"$ref":"#/components/responses/84c3e3e646523eb3db9611e5e1aa2901"},"422":{"$ref":"#/components/responses/52084ab03125fa5d7f683340979090f9"},"401":{"$ref":"#/components/responses/1a78cb40d89df2a07fc3093d551d618d"},"403":{"$ref":"#/components/responses/97bf43fe70d6d700a4584429ec4b5c6f"},"500":{"$ref":"#/components/responses/a251a53a14692e59df8840ce6b5c3bc3"}},"security":[{"basic":[]}]}},"/security/api_keys":{"post":{"tags":["Security"],"summary":"Create an API key","description":"Creates a new API key for the authenticated user. The full key value is only returned once in this response.","operationId":"createSecurityApiKey","requestBody":{"$ref":"#/components/requestBodies/2f9b562997f257abde3386c59b3c0d63"},"responses":{"200":{"$ref":"#/components/responses/2b3dd9ff2a02a3daa50cea7e254dae4a"},"422":{"$ref":"#/components/responses/23ba0eb1901527b7fafe335ed972bbc9"},"401":{"$ref":"#/components/responses/ea14d4451b440320e691c03724136677"},"403":{"$ref":"#/components/responses/21ae72d79278200b9fd544e8964455f1"},"500":{"$ref":"#/components/responses/c3cbcb1ab869acc6448a8e46a6e2f400"}},"security":[{"basic":[]}]}},"/security/api_keys/{id}":{"delete":{"tags":["Security"],"summary":"Revoke an API key","description":"Revokes an API key by its identifier.","operationId":"deleteSecurityApiKey","parameters":[{"$ref":"#/components/parameters/0bc48a9d3c6be1a8380f9d94b6cabe41"},{"$ref":"#/components/parameters/fa5ac64c5593bf8e54e0e7054dbd6394"}],"responses":{"200":{"$ref":"#/components/responses/b40bdc5d0b36a0f6803b3b315c7751f7"},"422":{"$ref":"#/components/responses/47d09650d51c6a11ad588e53cfd31394"},"404":{"$ref":"#/components/responses/e22bcb72f040680a9e94b55ca1821173"},"401":{"$ref":"#/components/responses/3c50e0dc648360a927becc5c81e249db"},"403":{"$ref":"#/components/responses/eaed1bcc1dfee67b8e8baefe75914471"},"500":{"$ref":"#/components/responses/5c2a3cac75c775cd906a5fa252fec092"}},"security":[{"basic":[]}]}},"/ssh_keys":{"get":{"tags":["Ssh Keys"],"summary":"List all SSH keys","description":"","operationId":"listSshKeys","responses":{"200":{"$ref":"#/components/responses/bb5f1d0970f7dc19056c0a40285cc9a6"},"401":{"$ref":"#/components/responses/f132f39653caf90109804ecd03bea026"},"403":{"$ref":"#/components/responses/ffa83a4b171026234a3d5c8aae9a7bac"},"500":{"$ref":"#/components/responses/ed2a067b832756af00cc46d30df48875"}},"security":[{"basic":[]}]},"post":{"tags":["Ssh Keys"],"summary":"Create an SSH key","description":"","operationId":"createSshKey","requestBody":{"$ref":"#/components/requestBodies/c5be455776887061d50a56584dc4491d"},"responses":{"201":{"$ref":"#/components/responses/c118197247ee4aa5a80745ee21f6e56c"},"400":{"$ref":"#/components/responses/c5f4f0f458a55fe1368b1e90439506d5"},"422":{"$ref":"#/components/responses/c511b0fcff6cd840e72ed7b108f8e421"},"401":{"$ref":"#/components/responses/fc0cbb3b890209c0818b449cea3b7975"},"403":{"$ref":"#/components/responses/a019ed35d22ae1ab4e4727c383682426"},"500":{"$ref":"#/components/responses/74e9e14fbb49b3d55617bd65d88e6a3a"}},"security":[{"basic":[]}]}},"/ssh_keys/{id}":{"delete":{"tags":["Ssh Keys"],"summary":"Delete an SSH key","description":"","operationId":"deleteSshKey","parameters":[{"$ref":"#/components/parameters/d6832191eea6b63663b292cdce3e2c78"}],"responses":{"200":{"$ref":"#/components/responses/2a338109e40351670de6b213d68e1cae"},"500":{"$ref":"#/components/responses/eab0f4026c4c8166122fe26f007f7e3b"},"404":{"$ref":"#/components/responses/352732c4bc931510e121233d982201f9"},"401":{"$ref":"#/components/responses/4a31138a0306b909d09cc4a85128a7d8"},"403":{"$ref":"#/components/responses/e6811f33199b573f7443b55af0a05a7e"}},"security":[{"basic":[]}]}},"/ssh_keys/{id}/download_private_key":{"get":{"tags":["Ssh Keys"],"summary":"Download private key","description":"","operationId":"downloadPrivateKeySshKeyDownloadPrivateKey","parameters":[{"$ref":"#/components/parameters/d6832191eea6b63663b292cdce3e2c78"}],"responses":{"200":{"$ref":"#/components/responses/528ea5694181c77afd31bd1e7981148a"},"403":{"$ref":"#/components/responses/93b7038a69ed5a4cfb09d4e2494e13a3"},"401":{"$ref":"#/components/responses/d1bf2d04035acbba772803ce751a1ac9"},"500":{"$ref":"#/components/responses/15083c0ac245fba1bcdcf618d10914e6"}},"security":[{"basic":[]}]}},"/teams":{"get":{"tags":["Teams"],"summary":"List teams","description":"Returns all teams in the account.","operationId":"listTeams","responses":{"200":{"$ref":"#/components/responses/c853460dc88c8d872e7c971aa2b68cd7"},"401":{"$ref":"#/components/responses/6b1276789246afb02f88922e3cc04e75"},"403":{"$ref":"#/components/responses/42d218c285725ea0045993fd1bbed2b3"},"500":{"$ref":"#/components/responses/f99792b6b3ab2923f437a638b4012a3c"}},"security":[{"basic":[]}]},"post":{"tags":["Teams"],"summary":"Create a team","description":"Creates a new team.","operationId":"createTeam","requestBody":{"$ref":"#/components/requestBodies/464e72bec387cd1bb7640ca74d9b8495"},"responses":{"201":{"$ref":"#/components/responses/75fa73853a18242b80a89fdfe2fb205e"},"422":{"$ref":"#/components/responses/0bc0a6b80036d7e047f84a3945b7b889"},"401":{"$ref":"#/components/responses/d50620a3b93e4b02b2b7501fd5f1432c"},"403":{"$ref":"#/components/responses/25de90792fdd69b63f818ead7edd83f1"},"500":{"$ref":"#/components/responses/7749c09f188e749b150cf17ccbb44176"}},"security":[{"basic":[]}]}},"/teams/{id}":{"get":{"tags":["Teams"],"summary":"View a team","description":"Returns details for a specific team.","operationId":"getTeam","parameters":[{"$ref":"#/components/parameters/bf5920dd9c5c1747d32f8644f5b0c8d3"}],"responses":{"200":{"$ref":"#/components/responses/767a91f7fef57b8edc551214de4d9c77"},"404":{"$ref":"#/components/responses/994a5c4b1d8c50b818b1fdb2cbf629af"},"401":{"$ref":"#/components/responses/19b63f7155963813932d7fa134e1adc9"},"403":{"$ref":"#/components/responses/e4c0a49f1ed84ad7e735b8bc316b66b7"},"500":{"$ref":"#/components/responses/f3e777122be45276d820689ae040ee8c"}},"security":[{"basic":[]}]},"put":{"tags":["Teams"],"summary":"Update a team","description":"Updates an existing team.","operationId":"replaceTeam","parameters":[{"$ref":"#/components/parameters/bf5920dd9c5c1747d32f8644f5b0c8d3"}],"requestBody":{"$ref":"#/components/requestBodies/4bcb6ac069da102c35a85471fccaedde"},"responses":{"200":{"$ref":"#/components/responses/813ccdd617eed3174c04db7ff42f0b58"},"422":{"$ref":"#/components/responses/04e233587296d1dbf811c5a8f9c9acf3"},"404":{"$ref":"#/components/responses/d4352565b8f1ec026933ff6e4becdd0c"},"401":{"$ref":"#/components/responses/e75fe9660fa3d852f5cc525dd49ff4ff"},"403":{"$ref":"#/components/responses/1df91d48b85486589b875c055c7805de"},"500":{"$ref":"#/components/responses/41568ff885b4e57a706c8ed354c2103c"}},"security":[{"basic":[]}]},"patch":{"tags":["Teams"],"summary":"Update a team","description":"Updates an existing team.","operationId":"updateTeam","parameters":[{"$ref":"#/components/parameters/bf5920dd9c5c1747d32f8644f5b0c8d3"}],"requestBody":{"$ref":"#/components/requestBodies/113357a2ce8ed0c9b3d77c686860b089"},"responses":{"200":{"$ref":"#/components/responses/e29af71831dca49095ed35e10ccfb318"},"422":{"$ref":"#/components/responses/83ee3c3787ab7d21ab60d2b6d3fd3dd6"},"404":{"$ref":"#/components/responses/3b9532bdec5c598c633c3edf0004671f"},"401":{"$ref":"#/components/responses/364954450e33b23e97dfa2819ed639d7"},"403":{"$ref":"#/components/responses/49198e1dbeac1a169ec11105a5d4323b"},"500":{"$ref":"#/components/responses/e9fe31f758bff0accd9369e0c0198154"}},"security":[{"basic":[]}]},"delete":{"tags":["Teams"],"summary":"Delete a team","description":"Deletes a team.","operationId":"deleteTeam","parameters":[{"$ref":"#/components/parameters/bf5920dd9c5c1747d32f8644f5b0c8d3"}],"responses":{"200":{"$ref":"#/components/responses/c98e7f6bcc437bea688a0d25478a0d10"},"404":{"$ref":"#/components/responses/3af060cd7ae1482af8be680a8f484266"},"401":{"$ref":"#/components/responses/4a913b4eaff9dcff390c9479c71d4822"},"403":{"$ref":"#/components/responses/b51f5135e513b1e49c3e978c7628fc97"},"500":{"$ref":"#/components/responses/875ef7de07b87cfad8e7a12e27b05fd4"}},"security":[{"basic":[]}]}},"/templates":{"get":{"tags":["Templates"],"summary":"List templates","description":"Returns all private templates belonging to the current account.","operationId":"listTemplates","responses":{"200":{"$ref":"#/components/responses/4ff597ee78e72688b89942288fbf9ba2"},"401":{"$ref":"#/components/responses/7bdff8c849854da73c86b2467c109429"},"403":{"$ref":"#/components/responses/63938718452ed237c8cc5af6e8e8df61"},"500":{"$ref":"#/components/responses/fb86392f0cc6de1fb200e576c0c57c57"}},"security":[{"basic":[]}]},"post":{"tags":["Templates"],"summary":"Create a template","description":"Creates a new template, optionally copying configuration from an existing project.","operationId":"createTemplate","requestBody":{"$ref":"#/components/requestBodies/6523a52c16bad1367638638f1d29e032"},"responses":{"200":{"$ref":"#/components/responses/9c89f935bdf7c2db782762f4cea93c9b"},"422":{"$ref":"#/components/responses/f17414563eb49a07c25c0bd442cad156"},"401":{"$ref":"#/components/responses/ea7b629bfd6dd357eca8f95dc2660b04"},"403":{"$ref":"#/components/responses/b66b867c000ee68e53b7e4a31b9e9a5d"},"500":{"$ref":"#/components/responses/41838a756c20604077dbfdc1d3e3bfb6"}},"security":[{"basic":[]}]}},"/templates/public_templates":{"get":{"tags":["Templates"],"summary":"List public templates","description":"Returns publicly available templates, optionally filtered by framework type.","operationId":"publicTemplatesTemplate","parameters":[{"$ref":"#/components/parameters/4f355c05cd4cbc73947cb6d92e7ea8ce"}],"responses":{"200":{"$ref":"#/components/responses/8c90c157776b53413e08b35460c6a834"},"401":{"$ref":"#/components/responses/4e44f06cf1de4eb4818f840b74df4c63"},"403":{"$ref":"#/components/responses/623b4556e66382420a4e4c0729257f96"},"500":{"$ref":"#/components/responses/056733f9b9b1a7abee41e7a2a4c30854"}},"security":[{"basic":[]}]}},"/templates/{id}":{"put":{"tags":["Templates"],"summary":"Update a template","description":"Updates an existing template's name or description.","operationId":"replaceTemplate","parameters":[{"$ref":"#/components/parameters/4b76a9087fe3c7c6bb7381ac0c71983e"}],"requestBody":{"$ref":"#/components/requestBodies/326e0218b5f14c690ab444385a37570a"},"responses":{"200":{"$ref":"#/components/responses/3f060b15d14dd47ee803a02c33601b7a"},"422":{"$ref":"#/components/responses/445681bd9af6e07dc6d5849e88e0850f"},"404":{"$ref":"#/components/responses/a0ae8f0525af15c21e98ee9d888ae4c6"},"401":{"$ref":"#/components/responses/b7ec520e7a7e8faec8078b1d0e3315e9"},"403":{"$ref":"#/components/responses/d38f3dc72a6c38c8ccee5b909e528c90"},"500":{"$ref":"#/components/responses/4408acefea88bbdeed86a17a72dd489f"}},"security":[{"basic":[]}]},"patch":{"tags":["Templates"],"summary":"Update a template","description":"Updates an existing template's name or description.","operationId":"updateTemplate","parameters":[{"$ref":"#/components/parameters/4b76a9087fe3c7c6bb7381ac0c71983e"}],"requestBody":{"$ref":"#/components/requestBodies/2ac24ffdeec701ae0daa26fa410f7789"},"responses":{"200":{"$ref":"#/components/responses/233191f35c22922c28cafc5e77460a44"},"422":{"$ref":"#/components/responses/6fc029d43dd7b0511932fcf95b548e30"},"404":{"$ref":"#/components/responses/f5a675297f7c82ad38d131b686e2da45"},"401":{"$ref":"#/components/responses/700cde56958e1c9b42c25d7af0b46b72"},"403":{"$ref":"#/components/responses/48cdf82e98c0d5dc4ff47c15e934169c"},"500":{"$ref":"#/components/responses/a6a53ce444ca79f5242021c5db44b229"}},"security":[{"basic":[]}]},"delete":{"tags":["Templates"],"summary":"Delete a template","description":"Permanently removes a template from the account.","operationId":"deleteTemplate","parameters":[{"$ref":"#/components/parameters/4b76a9087fe3c7c6bb7381ac0c71983e"}],"responses":{"200":{"$ref":"#/components/responses/0184434317cac9e9d64c615742e28059"},"404":{"$ref":"#/components/responses/158bcc0d22291fab6cd1362d15ec6c67"},"401":{"$ref":"#/components/responses/ef3879d0dc2f01005650a12e6710dfb7"},"403":{"$ref":"#/components/responses/5237b0b64e5c45e756b948125cb8c147"},"500":{"$ref":"#/components/responses/f4daf9c810bbfb8d726ceaa7733abff8"}},"security":[{"basic":[]}]}},"/templates/{id}/public/{id}":{"get":{"tags":["Templates"],"summary":"Get a public template","description":"Returns the full details of a single public template by its permalink.","operationId":"showPublicTemplateTemplatePublic","parameters":[{"$ref":"#/components/parameters/5a3a4876f81e660436e06f2ffd0fbbf2"}],"responses":{"200":{"$ref":"#/components/responses/6437978b7823c6a333bef8806f91caaf"},"404":{"$ref":"#/components/responses/e41be8156ba55749c1767fb6b60a00c6"},"401":{"$ref":"#/components/responses/ff0e901236f4702b5c48e47f0f04cde5"},"403":{"$ref":"#/components/responses/7a5ca05e9797fe7aa2306b2e56faba53"},"500":{"$ref":"#/components/responses/eb9f89e7c7c276bb21dbe3193099cdf9"}},"security":[{"basic":[]}]}},"/users":{"get":{"tags":["Users"],"summary":"List users","description":"Returns all users in the account.","operationId":"listUsers","responses":{"200":{"$ref":"#/components/responses/107a45711ef92955940b3d26e3502aff"},"401":{"$ref":"#/components/responses/b0d2910112dd88e415836b5ba42cb68c"},"403":{"$ref":"#/components/responses/465b518af585009a52ff2002eeba6130"},"500":{"$ref":"#/components/responses/0bd4a6e9427579b47c63a859d6ac30af"}},"security":[{"basic":[]}]},"post":{"tags":["Users"],"summary":"Invite a user","description":"Invites a new user to the account. Sends an invitation email.","operationId":"createUser","requestBody":{"$ref":"#/components/requestBodies/bc25cd0d4ccb70989df5c03e4e1759d7"},"responses":{"201":{"$ref":"#/components/responses/5aff0ce728f177b31ebdaf389ab630a4"},"422":{"$ref":"#/components/responses/d2365113fe53cc0f45a0f515e319ad4c"},"401":{"$ref":"#/components/responses/468611f7672da1bda624606201e1cd1c"},"403":{"$ref":"#/components/responses/37395e452858e92762baa7c3855534f3"},"500":{"$ref":"#/components/responses/39d2966bad2bab491f1ca985cded5fd6"}},"security":[{"basic":[]}]}},"/users/{id}":{"get":{"tags":["Users"],"summary":"View a user","description":"Returns details for a specific user.","operationId":"getUser","parameters":[{"$ref":"#/components/parameters/0925fefda80c37627bf8ccfc61b1c1ae"}],"responses":{"200":{"$ref":"#/components/responses/932756b8fe0acf2665760a54add581ad"},"404":{"$ref":"#/components/responses/29b3b2c6d252ff4deba83617dc2bb0b3"},"401":{"$ref":"#/components/responses/ba8421eaa37959180044f197090fe2cc"},"403":{"$ref":"#/components/responses/611a572b8111bd2a10f604eac5550e41"},"500":{"$ref":"#/components/responses/765a6568ca261a390cbe949a2f3733c5"}},"security":[{"basic":[]}]},"put":{"tags":["Users"],"summary":"Update a user","description":"Updates an existing user's details and permissions. Only account administrators can change permission flags.","operationId":"replaceUser","parameters":[{"$ref":"#/components/parameters/0925fefda80c37627bf8ccfc61b1c1ae"}],"requestBody":{"$ref":"#/components/requestBodies/1686738a7edb8f9bcf12fb47fb06055b"},"responses":{"200":{"$ref":"#/components/responses/4d9c037ba7c350700f58f92116715d47"},"403":{"$ref":"#/components/responses/452dd4968ea0e9afbd3eb9697c78e836"},"422":{"$ref":"#/components/responses/0f4f7fa37227b6bb65a356d3e865c589"},"404":{"$ref":"#/components/responses/57368d5aa3b620a9e523975bdf5e5b9e"},"401":{"$ref":"#/components/responses/83784925c7359e71520f3bb83674ee9d"},"500":{"$ref":"#/components/responses/1f3f1809de55a6d9d620b126f8b32ace"}},"security":[{"basic":[]}]},"patch":{"tags":["Users"],"summary":"Update a user","description":"Updates an existing user's details and permissions. Only account administrators can change permission flags.","operationId":"updateUser","parameters":[{"$ref":"#/components/parameters/0925fefda80c37627bf8ccfc61b1c1ae"}],"requestBody":{"$ref":"#/components/requestBodies/3b3d5df816efa9b141054f66d4677aee"},"responses":{"200":{"$ref":"#/components/responses/73c69751f5c4d2a5f86f49a1e0051102"},"403":{"$ref":"#/components/responses/5f10cc3d3764a11cadc1f5b836ac2a16"},"422":{"$ref":"#/components/responses/fee2db615800b3aa92ccb6347b4e9c52"},"404":{"$ref":"#/components/responses/d0cf930f1b5b1bfae96fea1834707810"},"401":{"$ref":"#/components/responses/3acf3d3e80dafaafd6dfa33bd666780b"},"500":{"$ref":"#/components/responses/867b1deb2c4645d318f9c1b6edd5a980"}},"security":[{"basic":[]}]},"delete":{"tags":["Users"],"summary":"Remove a user","description":"Removes a user from the account. Cannot remove the account administrator.","operationId":"deleteUser","parameters":[{"$ref":"#/components/parameters/0925fefda80c37627bf8ccfc61b1c1ae"}],"responses":{"200":{"$ref":"#/components/responses/04c39620824d6ca4bfcefa36e0d08508"},"403":{"$ref":"#/components/responses/3f5ebae6fbc3564b7cd778aeb29b7281"},"404":{"$ref":"#/components/responses/9178b7a9d8cf20672694a3e01e38fd54"},"401":{"$ref":"#/components/responses/9ed39d7bfee44f57e8e95ef21d824a99"},"500":{"$ref":"#/components/responses/86f1ec8b49aa04747f01dd49430d524d"}},"security":[{"basic":[]}]}},"/users/{id}/resend_invitation":{"post":{"tags":["Users"],"summary":"Resend invitation","description":"Resends the invitation email for a user who has not yet activated their account.","operationId":"resendInvitationUserResendInvitation","parameters":[{"$ref":"#/components/parameters/0925fefda80c37627bf8ccfc61b1c1ae"}],"responses":{"200":{"$ref":"#/components/responses/fb301bd24bc6c30c287ef995a5b6c7af"},"403":{"$ref":"#/components/responses/09642bb404a8a3636f40f2ea03a0a58b"},"422":{"$ref":"#/components/responses/0bfd013437cd4852d2f412e89b3686b6"},"401":{"$ref":"#/components/responses/ad05a74d05036c7db642a2cafb1dc549"},"500":{"$ref":"#/components/responses/38ab41caa5c6643d9b34ba01619342ce"}},"security":[{"basic":[]}]}},"/zones":{"get":{"tags":["Zones"],"summary":"List available deployment zones","description":"Returns all available deployment zones. Maintenance zones are only visible to admin users.","operationId":"listZones","responses":{"200":{"$ref":"#/components/responses/ef08cf740c3e49d8ac17f3a56b42bc44"},"401":{"$ref":"#/components/responses/cbc0b1d359b29d2dc404356b9a37a4f1"},"403":{"$ref":"#/components/responses/2a1f1682677fad5cd01b9cff4bcec9b1"},"500":{"$ref":"#/components/responses/e8523f0857de406c78bb71bd62aa4f85"}},"security":[{"basic":[]}]}},"/{id}/status_badge.svg":{"get":{"tags":["Projects"],"summary":"Get deployment status badge","description":"Returns an SVG badge showing the last deployment status for the project or a specific server/group. No authentication required.","operationId":"statusBadgeStatusBadge","parameters":[{"$ref":"#/components/parameters/1a67e0e0bcfbd47ccea892c1a083c6f5"},{"$ref":"#/components/parameters/bf75c85e3578342f8e96ddb58e7af384"}],"responses":{"200":{"$ref":"#/components/responses/c7fa4edf358344ef6ce580dacf57a91e"},"404":{"$ref":"#/components/responses/c55d9c6bdb62063ea8d6a6871b668d29"},"401":{"$ref":"#/components/responses/9bdf36baa31f8eaf90dd365d6d9be4a5"},"403":{"$ref":"#/components/responses/69e04b5c36cad8ad2be5c2f3e27bbea2"},"500":{"$ref":"#/components/responses/c9d19ffb3c44249c611b776cbfbfc26e"}},"security":[{"basic":[]}]}},"/{id}/{identifier}/status_badge.svg":{"get":{"tags":["Projects"],"summary":"Get deployment status badge","description":"Returns an SVG badge showing the last deployment status for the project or a specific server/group. No authentication required.","operationId":"statusBadgeStatusBadgeByIdentifier","parameters":[{"$ref":"#/components/parameters/1a67e0e0bcfbd47ccea892c1a083c6f5"},{"$ref":"#/components/parameters/bf75c85e3578342f8e96ddb58e7af384"}],"responses":{"200":{"$ref":"#/components/responses/27ee917247d1016c4f7d94dd80fa43e2"},"404":{"$ref":"#/components/responses/69a1adc5fe613852713a1910241d66b7"},"401":{"$ref":"#/components/responses/78b277bb8b63d67edb6810356d37874e"},"403":{"$ref":"#/components/responses/b88a1b00fa498132243c9e87bd64e2e3"},"500":{"$ref":"#/components/responses/d6fa7919605782334ac171f7a2253ab8"}},"security":[{"basic":[]}]}}},"components":{"requestBodies":{"644fb64792fe6d07abdf4bd904783741":{"description":"Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountObject"}}},"required":true},"1a066f75197c8a873bec56ca56db7d89":{"description":"Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountObject"}}},"required":true},"75ef393fb7b8bb0ab84a6a8ea29d77a8":{"description":"Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountObject"}}},"required":true},"ba75be51071603ebe817ff07c7e7fbde":{"description":"Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountObject"}}},"required":true},"4fb5a59fad4d7bb77a59fa15f4b1b35a":{"description":"Agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentObject"}}},"required":true},"da2cbb8b30ba849e328e7e8a4a75ba93":{"description":"Agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentObject2"}}},"required":true},"77f7326cbdf8e1ebed7815bfcc1d51c7":{"description":"Agent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentObject2"}}},"required":true},"bfbfad7b4b3f2ac265d765fa45ef1278":{"description":"Signup: `email` (required), `password` (required), `terms_accepted` (required, must be true), `account_name` (auto-generated from email if omitted), `full_name` (auto-generated, you can change it later), `package` (billing plan, defaults to pro), `coupon` (coupon code), `newsletter_opt_in` (subscribe to product updates, defaults to false), `signup_source` (referral URL or channel e.g. https://www.deployhq.com/pricing, https://google.com, github-marketplace), `client` (your agent or tool name e.g. claude-code, cursor, codex - please include this for attribution), `utm_params` (tracking parameters).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailPasswordTermsAcceptedObject"},"examples":{"minimal_(required_+_client)":{"$ref":"#/components/examples/0fec2f303f30d3aadc2fb29468ac29d9"},"with_account_name":{"$ref":"#/components/examples/160790222995c4940a528e1cbc21f21a"},"full_options":{"$ref":"#/components/examples/d13ba75a4803147192b243435b272a70"}}}},"required":true},"c59cece82991d620e689791e614398d4":{"description":"Enrollment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProtocolObject"}}},"required":false},"9b1548dae94f60bf68534c451ebd29cd":{"description":"Detection payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetectionObject"},"examples":{"node_app":{"$ref":"#/components/examples/d9aa4f4193ae5e56a9016932bfa1da57"}}}},"required":true},"ebb3ec94049e9b0b61d534e338818fff":{"description":"Config File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileObject"}}},"required":true},"d0c6ef3d21c8923f00ed5f4e6b7dd5e2":{"description":"Config File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileObject"}}},"required":true},"d540ff1656b031da1934a828e0d9f9d4":{"description":"Config File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileObject"}}},"required":true},"f5a6a323bfecca789b938f7e87a1d5c4":{"description":"Environment Variable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableObject"}}},"required":true},"ee43434d10cecfc2fb93542c24e20746":{"description":"Environment Variable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableObject2"}}},"required":true},"3f5c4ba69ba867ecffceff738f82e31e":{"description":"Environment Variable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableObject2"}}},"required":true},"31f971880e92be00ef2b8aad36a25ded":{"description":"Global Server. `protocol_type` accepts lowercase identifiers: ssh, ftp, ftps, rsync, s3, s3_compatible, rackspace, shopify, elastic_beanstalk, heroku, netlify, docker_build, digital_ocean, hetzner_cloud, shell, custom_action, pterodactyl, managed_vps. When `protocol_type` is `managed_vps`, also pass top-level `region`, `size`, and `os_image` alongside the `server` hash to drive provisioning. Additional protocol-specific fields (e.g. hostname, username, port for SSH/FTP) are accepted depending on the chosen protocol_type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerObject"}}},"required":true},"69896c7191677d1ceb1e373f5508c59f":{"description":"Global Server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerObject"}}},"required":true},"52da00d973fd08285674d3949177e04b":{"description":"Global Server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerObject"}}},"required":true},"ce2a5b0af433cd14645b1b1f6fe3ac6e":{"description":"Profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserObject"}}},"required":true},"8ace57011aca38252bed377598ddf9e8":{"description":"Profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserObject"}}},"required":true},"04d3b2f3eeceed0a118e2b7dfd10e106":{"description":"Project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectObject"}}},"required":true},"b1e790c590b053baed8693369cf09395":{"description":"Project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectObject2"}}},"required":true},"c5584c710f56a6e713ddf2de7f8aaa83":{"description":"Project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectObject2"}}},"required":true},"1446240eb00d9cdaf6d01ab69ab7eb37":{"description":"Deployment revision range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartRefEndRefObject"}}},"required":true},"d67b6dbb5c9e0b5e6abaa64f8505a178":{"description":"Regenerate Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectObject3"}}},"required":false},"2141da26ce826115a43c888d6a05cf60":{"description":"Custom Private Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectObject4"}}},"required":true},"a5e72dc87465749069cefd9add1540b8":{"description":"Deployables","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployablesObject"}}},"required":true},"01a4fd6eb08213c5f1877691615b2510":{"description":"Build Cache File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFileObject"}}},"required":true},"2c4554cd3aebdc0c311e53448586bd2e":{"description":"Build Cache File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFileObject"}}},"required":true},"fe2b6aa0b13d118a45b51c54daa99bfe":{"description":"Build Cache File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFileObject"}}},"required":true},"930e2cf03afefd39b7343707ba74fbb8":{"description":"Build Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommandObject"}}},"required":true},"9ac6c663abd19187d6e3bf0c11eaf7f7":{"description":"Build Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommandObject"}}},"required":true},"a2a2b77959da50c72b428d2ba7c691e7":{"description":"Build Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommandObject"}}},"required":true},"e6e38bbd42cb093e55278b696823fa45":{"description":"Build Environment Version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironmentObject"}}},"required":true},"449030214c4dc87cfd15556ce85cfd19":{"description":"Build Environment Version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironmentObject"}}},"required":true},"5c5597bb575118f899b4115a306de406":{"description":"Build Known Host","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildKnownHostObject"}}},"required":true},"8bb3f0103e68a86cc8e9c8de5341ed8b":{"description":"Build Environment Version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironmentObject"}}},"required":true},"f311e25427f233b4f70b3a14557c0f29":{"description":"Build Environment Version","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironmentObject"}}},"required":true},"9c773e5f685499c0c0504977b72a5d70":{"description":"Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandObject"}}},"required":true},"1eedc0e0a3c2e8e7adbe013810d11d45":{"description":"Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandObject2"}}},"required":true},"2000da5f7ee9aa5c28258d975de7db2d":{"description":"Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandObject2"}}},"required":true},"41229bdeaa56fbce4d758cfcea9104b5":{"description":"Config File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileObject"}}},"required":true},"871cddf48f3c9dab3d042126f20c1f97":{"description":"Config File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileObject"}}},"required":true},"488a8d76ff13a16c769e7e5ce74a2c76":{"description":"Config File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileObject"}}},"required":true},"ad30a6dd2ae89d42db30c281d98282e5":{"description":"Deployment Check","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentCheckObject"}}},"required":true},"75a4eaaae216aab13152349f33ecb37f":{"description":"Deployment Check","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentCheckObject2"}}},"required":true},"ccf468b9fb46583bf247f901e4a07c86":{"description":"Deployment Check","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentCheckObject2"}}},"required":true},"ceea7a8b7e1cad825d7b8e26acbae00f":{"description":"Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentScheduleSaveServerBranchObject"}}},"required":true},"9b8d3a49e6398da6511e4c7519af3ec3":{"description":"Environment Variable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableObject3"}}},"required":true},"df8b2a793f86ebd24f336b4229ef29c8":{"description":"Environment Variable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableObject4"}}},"required":true},"bb3ed1912dcbf60b8425c61d5ba82d27":{"description":"Environment Variable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableObject4"}}},"required":true},"bd70ff0293a8e7872dde6e688248aa87":{"description":"Excluded File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFileObject"}}},"required":true},"865772613ff1fd1bc3ce8df514cbb6f0":{"description":"Excluded File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFileObject"}}},"required":true},"abb0ac48efd13b5c9b298a5381c905e5":{"description":"Excluded File","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFileObject"}}},"required":true},"752c9d3188a7bf05db19ecffaea20671":{"description":"Integration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationObject"}}},"required":true},"ce067dbebf9446e2abb78d597601e6ef":{"description":"Integration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationObject2"}}},"required":true},"d0e3bfd42723809256f7084520f8b24b":{"description":"Integration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationObject2"}}},"required":true},"597169c8196ce72a442026c4066f9f91":{"description":"Repository","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepositoryObject"}}},"required":true},"b86cb33d4dfe1414914b609f54fa7e5a":{"description":"Repository","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepositoryObject"}}},"required":true},"7d0a3eaf32e664af8e5fb7efbfb4ba70":{"description":"Repository","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepositoryObject2"}}},"required":true},"42d9393c6abd8d537707081c34bfe855":{"description":"Scheduled Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeploymentObject2"}}},"required":true},"a11e19a5e662610aac4117d318d047dd":{"description":"Scheduled Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeploymentObject3"}}},"required":true},"4f3055165f11efbef0bc59b616abe9b7":{"description":"Scheduled Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeploymentObject3"}}},"required":true},"cedb4e47fa9d1c5a3536b8e382d1673d":{"description":"Server Group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroupObject"}}},"required":true},"ea431c4ea1a8a9fb806ab403db572167":{"description":"Server Group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroupObject"}}},"required":true},"ecf1d6749e27ef216bef0e4cb25decd1":{"description":"Server Group","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroupObject"}}},"required":true},"c7dc419041385c74efb8b5972cf3d9d1":{"description":"Server. `protocol_type` accepts lowercase identifiers: ssh, ftp, ftps, rsync, s3, s3_compatible, rackspace, shopify, elastic_beanstalk, heroku, netlify, docker_build, digital_ocean, hetzner_cloud, shell, custom_action, pterodactyl, managed_vps, static_hosting. For managed_vps pass top-level region, size, os_image. For static_hosting pass top-level hosted_website_attributes (subdomain, spa_mode, subdirectory).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerRegionSizeObject"},"examples":{"standard_server_(ssh)":{"$ref":"#/components/examples/383381e24b95158e6e5d3a7fb8ad35f1"},"static_hosting_site":{"$ref":"#/components/examples/5208f77fc0a367d42f7e0c815f274a4e"},"managed_vps":{"$ref":"#/components/examples/0e744c1697339ae57ae6fff5f96e48c5"}}}},"required":true},"44eab7f16b0a60f80cb907a2302747b5":{"description":"Server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerObject2"}}},"required":true},"d43e1c79f7b051844eb932de3c509ba7":{"description":"Server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerObject2"}}},"required":true},"72006bd395bd93afb9a623fff5c79da0":{"description":"Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandObject"}}},"required":true},"1ae1e9d3da172150ef6f7f2bfd63f5ab":{"description":"Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandObject2"}}},"required":true},"c0b5a2d39f60bf2fea0c5b1b5e61c15f":{"description":"Command","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandObject2"}}},"required":true},"5f0f148c45a3280e2dad7435a90c2c7d":{"description":"Password Reset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailAddressObject"}}},"required":true},"2f9b562997f257abde3386c59b3c0d63":{"description":"API Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyObject"}}},"required":true},"c5be455776887061d50a56584dc4491d":{"description":"SSH Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyPairObject"}}},"required":true},"464e72bec387cd1bb7640ca74d9b8495":{"description":"Team","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamObject"}}},"required":true},"113357a2ce8ed0c9b3d77c686860b089":{"description":"Team","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamObject2"}}},"required":true},"4bcb6ac069da102c35a85471fccaedde":{"description":"Team","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamObject2"}}},"required":true},"6523a52c16bad1367638638f1d29e032":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateObject"}}},"required":true},"2ac24ffdeec701ae0daa26fa410f7789":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateObject2"}}},"required":true},"326e0218b5f14c690ab444385a37570a":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateObject2"}}},"required":true},"bc25cd0d4ccb70989df5c03e4e1759d7":{"description":"User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserObject2"}}},"required":true},"3b3d5df816efa9b141054f66d4677aee":{"description":"User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserObject3"}}},"required":true},"1686738a7edb8f9bcf12fb47fb06055b":{"description":"User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserObject3"}}},"required":true}},"examples":{"0fec2f303f30d3aadc2fb29468ac29d9":{"summary":"Minimal (required + client)","value":{"email":"developer@example.com","password":"securepassword","terms_accepted":true,"client":"claude-code"}},"160790222995c4940a528e1cbc21f21a":{"summary":"With account name","value":{"email":"developer@example.com","password":"securepassword","terms_accepted":true,"account_name":"my-company","full_name":"Jane Smith","client":"cursor"}},"d13ba75a4803147192b243435b272a70":{"summary":"Full options","value":{"email":"developer@example.com","password":"securepassword","terms_accepted":true,"account_name":"my-company","full_name":"Jane Smith","package":"pro","coupon":"LAUNCH2026","newsletter_opt_in":true,"signup_source":"website-referral","client":"codex","utm_params":{"utm_source":"github","utm_medium":"readme"}}},"d9aa4f4193ae5e56a9016932bfa1da57":{"summary":"Node app","value":{"detection":{"filenames":["package.json","vite.config.ts","src/main.tsx"],"files":{"package.json":"{\"dependencies\":{\"react\":\"^18\"},\"devDependencies\":{\"vite\":\"^5\"}}"}}}},"383381e24b95158e6e5d3a7fb8ad35f1":{"summary":"Standard server (SSH)","value":{"server":{"name":"production-web-01","protocol_type":"ssh","hostname":"example.com","username":"deploy","root_path":"/var/www/app"}}},"5208f77fc0a367d42f7e0c815f274a4e":{"summary":"Static Hosting site","value":{"server":{"name":"marketing-site","protocol_type":"static_hosting"},"hosted_website_attributes":{"subdomain":"marketing-site","spa_mode":false}}},"0e744c1697339ae57ae6fff5f96e48c5":{"summary":"Managed VPS","value":{"server":{"name":"web-server","protocol_type":"managed_vps"},"region":"lon1","size":"s-1vcpu-1gb","os_image":"ubuntu-24-04-x64"}}},"responses":{"c665de823c0b81a4245567eead025c6d":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkTimeZoneObject"}}}},"041ad80b812c45ba5d7be6f69ac341bb":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ff441b591acc1d629f1e77978bd7f446":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"56a318396d4c96e5a68a1c1b3ef5d575":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cbb636f111393fbc6a74310a896555c5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8a5fb3916b76affdac3d9df7d2877545":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkTimeZoneObject"}}}},"ef69bf68221698225ceade53dfaf8841":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"5f8a45951f7bacc55821d41a53a1cc62":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9139784279cda44f1b8a673d45f656a2":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"48d58ac21701d8ff15336146f2e79a86":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d17d107685da319554e743ac3d99a252":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"64d78011ead00dcfec91c5f5d18e902c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkTimeZoneObject"}}}},"8f9f8a6fba9bd6172c97f3dff1df0fc5":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"2531fd8b217174578bf0866698787ba0":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"579083de9e26bb67d786d9b63debc8af":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8f4561501516102337afebcc1e4b7758":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"13d59408b279eb1b1c7e5abd17427fb0":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4cecb307e8754b34dda9884728d8e12e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkTimeZoneObject"}}}},"a9f7a2eef628397bbcbe7035eb37a5f4":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"77bf90a0f411e4f1f1845ea1b605cbfd":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c4aeb4c285b3219cd8e7c3666f608fbf":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9bc5d36aa3015b1f64c0ca4666c8560f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4e446e2fed16bfa261780848b4692622":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkTimeZoneObject"}}}},"898565f5e28c5fb8109d7948fcfd3e0f":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"b0ce86f69f822cb5b79fb0295e890a6f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"df93cc716f999632872dbbacf3abf63e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"110131ccfe6a6853f7c09b5d6bcb01f4":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d519c6b94d481da62dddc3e127a19357":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"de2d3701d04bbc262727cd4c2a9277f9":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkTimeZoneObject"}}}},"e0217d05a81378af962648a32fab1c8f":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"aa5604d80a442449a9492874f89abd13":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b64b9dda5839d9d6fe818a046a263c79":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"04cf39ea1fd36a6073ab0dcc0e6c53ac":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b3e3482bf2a2a8ef67b4b7d52c3851c7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"422869ab4c640e00cd1a469772b3074e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackageFrequencyTriallingObject"}}}},"60f43035f1bfa7913cf55de5efcc1090":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fb7452d83fe3381bcd79ef0bbdb435e8":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"458ac5450e3fe2f0efde543d6296f793":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1e68ced21fa040c1dd867d9e6df28064":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentList"}}}},"440208eaef46ab1dcd07db7d6e329fe1":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"da11793fbc235d1a85f699ce0803498d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2e963065819657428890fc4e249993fb":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d61e7aa260c32115ae3221df3a6f01ad":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"4062770b4d4d672e678d7db6064812f9":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimCodeObject"}}}},"34bc186eea212b8f81b75e5881502447":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4f42fac3e022f61e8fd4a0d5e2ae87a9":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"abad08ad7bc61525e808a81c0f13ded3":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a09cbd3286eb779413bef09e65fe4893":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"51fd5d359a1194712d337f829f962b65":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject"}}}},"47316208d5f6fb55b85da2c849b5664b":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e0799b05947d2483adfa555b681f0bdd":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b626a4bb65dbb86f6c8a406afd4dc43d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a8203c0970525bb578494fb00d3ead2a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"44aa2f610b8f7b8421108c3f4c12cf5c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"062ff3062e226097a9c0918da69f1e6d":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject"}}}},"b6d0d7107298022ed208958b012f9998":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e9ba4acf743bb774c494f00b084ddbb0":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2d7d879608f0eab9a63f91b1f89741ec":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"12a395a00f8c6d0e5d8172d524e5e7aa":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"37df2b0d0e8970f776cde5deacded9c8":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"b3fa34659ed201fe387327b842a20c01":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject2"}}}},"3f8ace65c15b14e8019f391705fb146f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"944c9aafa08b8d0393c583154dbbb936":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"800c5eed6ac88f8ba9cfbce750cd10ea":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"98d6a705724146a63894ab044532ba68":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"0bb9b678ee0c69ad759d875e9e934b9a":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject2"}}}},"d45c28b223f939f2a3d591327412a7bb":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"136e3c9f4bf7eda5c72882ee9155aaba":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8cd70662d5b3a511cdc069e568de7519":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountApiKeySshPublicKeyObject"}}}},"96e0f2222ed448e80963c2be471bcaca":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorsObject"}}}},"c7405448fc2cf7b98059648a1d56ac1d":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorsObject"}}}},"6ad05c548e8d9e323708867f354cad59":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorsObject"}}}},"7f550ef71b5972aa31712285241f6aaa":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorsObject"}}}},"9a229ea5f52f2076eff061b748006baf":{"description":"Too Many Requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorsObject"}}}},"034eae2e693a0a10d9d9c2ff3e5c6a11":{"description":"Service Unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorsObject"}}}},"7a71a65f7c9ac5ac439fcb28cc7a1a82":{"description":"Enrolled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrolledBetaFeaturesObject"}}}},"c7096c3140be9a79a00e32ce6a7c7c82":{"description":"Already enrolled — idempotent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrolledBetaFeaturesObject"}}}},"c902f87ed72080a146ee6f9996f52622":{"description":"Admin required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorMessageBetaUrlObject"}}}},"e17f4fa1a958a199085f2dc1fdad83dd":{"description":"Unsupported protocol","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorMessageObject"}}}},"c22d352c9d4fe9ec8b1cf035db82ed85":{"description":"Enrollment failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorMessageObject"}}}},"112315a520da27b3a9dfc07ae419af8e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"139452f9a3f243958dbe303a23530a8e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StackVersionEvidenceObject"}}}},"9a64f6184295d730187ea618080729f8":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"de9b8ab82a149f778567eadb59f5f41c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4397d437356851d3f129a431acccb997":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9f943f615d616f45117912ac838e97ca":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f7c3e110b62b70cb68cf29342e951d54":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileList"}}}},"22078fb50c8b46789c8fc6305b0b38ca":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e48223afa8931a7a3d64d068025f60c7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dbcfbf6009d0d9beee67e5c47b7b8ddd":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"88ad17e7870813a88be4a352ca057fd9":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"1a35561477bf2b192cdb0c9ce1a960b8":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"64c383836fb36c6c1129be83e4423b35":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f7e2ba7e7c1be6e7f41d6b401dea3fe0":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7bea5ec58c5459de41840d9bbbcbda21":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"972df02ba99c6e10e00354e9f6254019":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"0041dea174622a2ce65134553703efa8":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"acbc286a21d4dd7682f81742b7c31176":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a156b4eb95265fc201a7e14ed0aed75f":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5ae3acda2adf4fd33eef0b7d8d4a8819":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bb0103bcab8a3870d6f6690aeed10833":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"cc249b4d017f33853d9e75182a44cfaa":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"04ebbe0b2c0e1d33d81aa43a9e571027":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"aa8494a7852dc58c11f47c1bde8b4c3d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7acad67dc92b783a965dc39407b996ad":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a3d7206d72f13f8095776ec5b36070c3":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"24610b5e5d89e6b632585d03fb79572d":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"e2e530f10f7496aba47df723677a60db":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"01bed04d50e50e47612e877e1b81a068":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a7245ec2d4a466e46f784c0d6e61465d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fa9efe64e2cebb1951dfc144a132c924":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8db4f5c702e45976c56c59b5018bf6bd":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dcf706deff01af633f456d3de442c410":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"f0217d02506c137f2365add167187b2f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b33ef77e9d3e9aaf5410d9a6ef0f2b99":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a49852f39158f3e1e8dc5f1b348f0721":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f29111476c43e7e874784af8ff035c54":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"21cef77e5f5f1a427ac5108a0cc015ba":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableList"}}}},"005cd9a54340a236eda7676fa5ffc1b1":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"254045048ac8aa6e8d334b243ae1eda1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dd05ed70cb4b07c42a9bb55c28ab805b":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c93e91305c0b06a1611d33f07f7327a2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"ac1b06541eb59ebd0a73a832c35d4497":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"9ac2a5269aab16e2f9f8df9125224d61":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"765754ac3624e77bca9b473bea6488c6":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d7442bdf72ed6e06547ae65e84d5827d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"27f03922d6d4db5df55b2b97be10c33b":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"6d493436b89681dbed187839c41bb8ea":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0a4239c40216d5a2f353f4f037c04e35":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a0371751682ae603255e8b72189b41b3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7badd6b1ef8cdf7c2c2a476151288b66":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dd06ffe99ab5adac58c10c6ca61907fd":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"fb0becb80bf1b86e6b38a4abe439b464":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"6270d98592bfa240578aa3e841f5fab5":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f3aa6ad2bcde4af4eb3bfd6d3462464d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7037373a588008f476609c12f3d10dc5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"54062a22c232d9bd2628961a5c22f8eb":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"73ce6437d5a29ce889cd12e133c70517":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"c3c56654a8c7e639ab02116074d76159":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"91140e610b2c97b3f28e1c2a8c3a4e8a":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ba6b80a4f8ec434e6e667c7d6a2d88b9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cdd217cb599c15a555a9bd1c33cf901e":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"051332e78dda4ac37c4db05956548c17":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"68521ef8d89bf2b26f92bdcee641d000":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"31642a6088a356d24e689ec59817bc5f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e692b5389e7dac02ff3ba8f88b6d3f8a":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8624ed5eb7712c7e665a1c24104988a7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"80888d3250596f195b54ab43574f7346":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6863b278a39de2b027591706f7a16c62":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerList"}}}},"148824641d85fe12340e0de7380308a8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4ff386d2f4a0a9a022e93050c6117166":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4989446a8d644bfae1a84c9004fef9d0":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c25d44b2c8e2a8c76643bcf1f3f4e1de":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"aa466175ee0ce28feadec5f3f027776b":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"6a40c698b55f18e601866db89e3b6966":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eeb97b2c1f95075d78e50be037b0e0e5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"62f8a79c78df10cc57eef5982faef987":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2f64de8c5825eb2684bc318c474080b1":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"13bbe0dc031e3e20a9a0eca9031114db":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"07d608a597e647d0b21a4c80c2b873f8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b83df3066cab73f26ac99097280e5bc7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c4386751da79ca4dada23de8ea493493":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"670204086fa9eafdc19bbbacd0c97e95":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"b77c76fbfee15fd09c07cba3d4788c27":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"3c7c2cc397b42e5f138f2cab10df9094":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d06ad690d7808a5962453150d5e1b7f4":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cee78c7e628620bd3086ed191718433f":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fc8c90805e8ea4ba6cc1d1369cca1086":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b46e979e956f606587a7887ed8f0e3d3":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"b95398ac496e36061b2730c18293bfba":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"851f07e2f42527fbec700b887e631cc8":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bd4a6677264d1e83f70685c994543d87":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f834bbb0aba360645094e0f548990631":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"07f7ca158567e429b51bd00eb2e08a22":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"26dd8e17f89b960c38496a4da9b3233f":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"55c18e9cf1124ac3783e8ff4a1a562a5":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"ec2c44ee386502c9a5c3587a5e1599de":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0de51377d2776a353ad64646624fd3fd":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"19666948b8c70341a4398d6270f303c0":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"639db9f3957950158c8219b32f0523d6":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"af8b454269337bb3ca47bcf91531436d":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"6a158ae03e3452ab22cff57f419abc62":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"a60ed1d2212c40f10a39f42c0a72a538":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dc58212c5315fa6541ec024807b9a995":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1cddba3b906454b4e68d2ecbe9260805":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e503a0299e8fc27de98a101b3bc04f9f":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KindIdentifierNameList"}}}},"a5d5622d69aa76c4d6b1150c1fd72ed6":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7501c12131a155d53e72c02271acfd19":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d017964ba2409c8af6dd9567bb44030f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6aacc99e1d8d3f33ef5384a069674c9a":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KindIdentifierNameObject"}}}},"79311822e3af41ecd1fec7b22fd28450":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9131337a6e7d318fecd4318f59e1c68b":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f749441fc0112edf3d4ca68e0072a62d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"aadcda2a77593c815b7892b06a4f905c":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"58da531d399ef3aa368bb409a8e00c19":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"725b497c498ebc6c85626e2dc9dea818":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"7d37e53c9f6f2d92841901e01d795a3a":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"13cb46a7734ff7573ab1f7f9831ef530":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"25e6ba534fa74421fd7321a96f3f95ad":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"766544a5cebaff4d4459a2913614e6ee":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"de41a3b9f570ed3ae5ae21dedf743f7e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7f5097b55fd2c2ef930dc45635ef7428":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"86a8612b8245b719931252dfff717ec5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f982c6014d71d60e3550a9343849d64a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeRubyPhpObject"}}}},"35bd80927d7138e6be9ba301185a44d4":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"221f5952ae476f9b1e2a15f79ad98a9e":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dadfd016a8dd5bff54bd6bd823ab8512":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a9c0a30bf2cd4d11ee77f679f95ccce9":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupedRegionsObject"}}}},"afc0f73097a4e5798b47a498dd5667e0":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"688c8565397e3d95dc13a0cb16208ae5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fc0775d143d68cf6c5b3d74be9f8bf64":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"16d5bb262d48607dd204ee97a4996ea3":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SizesObject"}}}},"409fb6712a27355c86a96aa07250ad08":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a762eb122b1e1c28c274c810ce83f133":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b4cf97e43552fdd62376a0b039dd77e3":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d90c986c9d066985c56797d473730b4d":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirstNameLastNameEmailAddressObject"}}}},"1d4f2b1e56e2e518ba6034883f1f075b":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0b3c2b39a1a7adb4dd80f9bef2b243a2":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a5a65936e8d950bfa346c024f911452c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d0d6285511be7a770ea6c6f2606acd51":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"05328677bb1bfe5a67108d2cc99245c8":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"e4913e25c5008d12039a3da073843d9c":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"3ceb77638b7e12106b9869f9300be71a":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"68901e2ba3d20ba3a9f9a1a5ece2f719":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"048d5428b751216ef65d5fd59721dbb1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7d4e5cdc5cdb7f98fcf6999af80ed948":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"25af31beb241d7a6185c810f5ae0797f":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"ba46cbd88bce14a87514286e0e5f25df":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"cfc8e9c1f39bd1ecb96485f762ae3bb6":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0cb967134b9f4886631c2c024452c013":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9ed34bb14d3601cea2d1751d05a84ada":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e8f0b4dbade0c649739e94441e2e0a69":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1db02819e660b7b205b7ab47e7338629":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkIdentifierList"}}}},"c7f1838eff4cbefbb2f5160dce62cdab":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"05abfb7e6129500348a879b5bb796afe":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c113b643d5a0f0697655fcadeae7a2b2":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"47cf4029d1e29467ec429a0cbb06412d":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"bcc865c374bff6e0955ea7a91bc0d8c6":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"bdfd5a052335ffaf314cf49efbacf6e9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"20d0e4adbce0eb0e2af19c741bdf89ac":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"094cac065ab38456b3dd64ca673a5071":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a4740158fb509a324f989a86304dba93":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NamePermalinkIdentifierObject"}}}},"7236ecbf1ff5c79429d6491c9e817fac":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"053d75dde0108f9dfd11cb2e977d6e47":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"99b98d0afb62b219158e7edb4a9e9199":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ca0dde1acfbdbedded2ee8b923966ad7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"66bf91e3da407b23564691d86da6a79c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"e0125798b2a70d7af4b644ebe9927be6":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"3cfa9631388ec6d1481f5aa82df7117d":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5a2c72419c3037a82406629768cb9625":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"66c2817d9e26cfc76e9e62bc6cec8203":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"51fac4c2b4dce4fbf46bcd432c126dfe":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b734c116750407d6cb88bf1267977681":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"36c40cf1b00e2ef730215df9b52f91cf":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"3db5449b6e372bde4d556021bd4dcee7":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"912f97ae429881855b5598323974bd24":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0fe059a053c89a435e40479bc3de8ed1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7c7878959e09e00c0ed3d668899bd19d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d19bf460a75cdd901bc65b77408695d5":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"d3ad1835a4e70e36a607014853c64609":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseObject"}}}},"8edd208aa5a64ade377507949629ca21":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2f435cdbffefa558b6e8782d3ca76d9f":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"93cf0d644326a809a80e2ed4fd9480b7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4ded389d3b03519777c45da6c096c05b":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"16ae7c94212589f16a7b9049a96b5b17":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessOverviewObject"}}}},"08a470e0ff78a58159aacc9d974080bb":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessErrorObject"}}}},"b63a5082c1261787f9f76419de7c7809":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessErrorObject"}}}},"7184220172c454d53b42d29ad38ffab2":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"acbcb370b979bcf306976ed81c140df2":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4ac93acf52aaafd849c0fd928204ef06":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1a141ddafc39f641d3eef3cd1359ac77":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeriodServersSummaryObject"}}}},"ee875dcf98d4874779e998112c84fa6a":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"03844db9fa60de7d649b85c3fdcf7fd3":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e7fdcea912b735acaa2436c5d94e4179":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4d76907e49d0353fbc4259d98fcde9c8":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicKeyObject"}}}},"93129c3cae6d8eb407f17b3ffd051488":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringList"}}}},"24515b336c75ea0f6604e3eb2e58a4fe":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ba35fab94c72d8ce856ff2ea46a707c1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6d66682e655119b585cea6fd11622531":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"48797af91c6750e1da25ece2ba67cad7":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageStarredObject"}}}},"076272a2485c9139031546c810cf8385":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d08148369f3622a3a4565441e90231be":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"24985112a726bd8e90a4d9f87db31e08":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"60d20ea8ae0601bd7cc4e22854f1a39a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountTruncatedLatestDeployedRevisionObject"}}}},"8c1558305b140b4e0cae2ee7dab39e45":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"3d7f547dee10800a9fd033bf0267cfd9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"97b66a40932b34f4daf2806c5dae1fd3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7bc133121af247ff6303c7e6f3cbc3cd":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e1219e0c3f55888e51cf7af042445514":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicKeyObject"}}}},"2e29a219fa17ac5502d0e1e47b99e012":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringList"}}}},"7ed21c091e7580d57cad351a73d210fc":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2ae51e63d7fcfe975e94017ed5b2392a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"519dd5c6e82656d6ac8595fb07be169a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"07fb1cd8901f8fcce78ba716b4068221":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookUrlDeployablesObject"}}}},"4c22711b1bfd70756131f43297f62088":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c051285e2ca1b1cde8cc31c5aa24669e":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"08adab1740c70370da48488cfe1a6c29":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a737b25546407eb79cf1154290b453c9":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployablesObject2"}}}},"580cf83eafb1ad59d9ace317e28350d7":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"6213afd0e580ab9db66fc7eac0f4c25e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5e74e8fb7af0188df72763e8815b74e0":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2920f7133e0c2c9d24c268cbf12493af":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"be48dab3f0b08cf0ca0f29db8330a36c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFileList"}}}},"4517d5ce7ee0e365be7f876d42ee3d15":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"84d22c4162950a9f0eccb72c9e4e3ad2":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e0fb2ad7c55b558f7d52e54b1699ced3":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1703ef92426e616b18c9819b255e2018":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFile"}}}},"28aa570afd36ab5447ba6ea85d391045":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"9e9b8facd18764edd26fab9150b28798":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"98b32997f65ca5e0ce66792c9adfc370":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"edd8dc8912d71436577d87a6188c8fb7":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"747fd166f9a539ec40f13d734bd47b20":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFile"}}}},"33f4f665151e8c381be808ffca608e21":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"c7dcda6eb1a905d1a00add555d5f92c1":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a3a91a28af993612f96893e11f921743":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f877f505c147ef6516ed4675a664c297":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1e347abe5d9a1918f7b92910c8b8fa0d":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"525620e5357fea06d6c1a75d95b8f087":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCacheFile"}}}},"c9e8493cf0deab2272711bca5542ac75":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"5cdd9a26c8b03f6653d4a3e4f2c528bc":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"883d965b310485256dc92b2d6b3058ee":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"16647919d959bdde99e88dc81008f7be":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5cd7e61a0f0084d5980da72c3fee76ad":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f766836ad09eee91c5f3a479f53fc167":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"351c40bf95da95bb45d3cf7716659b58":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"86bce515350e814a1a74ae81f6f5d9f3":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"56cebcdabc78855859ce4a41b25a9e6d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2899d625ffa3e3068c8c41d4c1a3d6c9":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"151493e9eb8b0f590eea2d729dbac596":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommandList"}}}},"621969286130f35a731ef3b9dd4f26e3":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c7630597f542bdec9bdbebe8aacd5411":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3adb76eced0dd8e7d021956fd45a8c4f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"acd1694fc84fdcdc25e91ef315ee9dfe":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommand"}}}},"75c98c0b226f9fc1d296e2c3bc011f3c":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"33d0369f2de5910f29b4c2561366ae41":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cf09954d38b5158fef28b34e67a9a3da":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"550fb6ca8481d53c48658c5dc1326576":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b793a6af2c31ab8f91285df526bf81dd":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommand"}}}},"d671bfc0c52cb3d5f762805c537e4736":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"9bbc951e18e8f34f2866d1568fef0ec8":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d520f6d4566e66bee70045ac7199e271":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8cdab2cf42f96b7e72ba5a2a4824934a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b0b599bb5aa24aee542e6d8901713ba0":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"243b24f04a64a69b8c8a46206602ce89":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildCommand"}}}},"a504e678177780c9c0c4c7a3ee9f4f85":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"477ccb0731e3ef6a82dd48fc90eedc60":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cdb8920d65dece01618f58274053d506":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d3ccee20989f19183ec9fb02417b0049":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7da97f45a8e1a262646d710f1d9f21b4":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9bb05eec1ef3fe7eb97128611472726a":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"79e03d2a1febd20461c7c22dfbf62f86":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"ea19ebdb12f6e7df5d0eddc7d33425b9":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0ac8544d309fca9f71e0ee32adcef339":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6284074c310a0f24124ec9b9c65b38a8":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"226d6e29c4d627f966692820e778dd91":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"57a9eac2b7c68552409d484852083f58":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"62e3d168c1280b8ba747b696fadac80a":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"de145e86edcfbdba3c36743a7c4a40bc":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5b91b92d8af824095f50f8d1fdf3c8a0":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f57f99207f6979b3bd1148a9bd1b013c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironmentList"}}}},"50a96c1e1a44b16627e45a0df512138a":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8516339c49bb6acfa7f571dc29604345":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"851519f4125f30d5d0d60e07e4e1e0f5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"333cb93ffa5965d38f494817bf713eae":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"5f81f3aa22ad256c5bae712f853c2300":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"8928207c200ce3f865fbf4d330395b85":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9c3810e1ea92a51144f50ce79261b686":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4811897945c94b7f47c9838bef27abb2":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"65566323b603ed75463050b76ea0cb71":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"96f1ddca27d4d2a2e0e84a7df5bc5c8f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"31c7f0c7dfe731a02e7366c0d5a9fbcb":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"668099450f16d2931a43109eedb810df":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b7023fbd9aede6fd3684b8824542120f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d554f319619f9045c45fed9f1bc8f4a7":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"dbf7e668594c45edae60b27c7ee7952e":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"4e866654876f73dd8ddb4edf0e1e0310":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3c4adafc8b5bb9e7d2a637944b7dd71c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"70bfdd45e9dfb5c9eceaa60f4a239cee":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"423dd5934a1d81a4ea6c3db6763e2ab5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e8edc9a329d3b8f393f3f5589b8ed235":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"334f044a06ac03399053182d50e7d356":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"bca05e365c2eb7bf7bf77ce1ed2807e5":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bc5f82125183d08d2548cf3c758ce1c4":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8ba73041880ed2b377fbdd1df4978932":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"921463f8e83f142d77bd60a46d86b2a2":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8542f799b31e127c6fdd14f3ddf94d33":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"6deeedf97ba51a8c885cd5857c3dc60c":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"e0074e06be171f3ca38d799d2e9a0c6a":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"929e87f7bddef04efc9d92504a883ef9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8712b1e641c8a766e70910dbda81491f":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d0a710983a898e28ec854690936b84e7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"820d63b5bafc2a755dc27666b4bea417":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"1ad94c09f16b2cf2901a5b620e5dae15":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"9be58472c10166c7b6f1df948dd078af":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bb3ee726e46abe1c2ab2f39d0ff6e404":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"680fd930206d07e93ff770d644b0c1c5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ffe5d0ea0547a86fa6cf6a2f69e25220":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3f4a10def6bb70edc8fe97702d7fc008":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"f60d5757a770330373b1fdc7cb364a67":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"26e09bdc9e4412ab0fe23dcb055ed022":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b3f2baef0eab5e833cce41570d316814":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9604f37154b01f2cf31d75ae0a7f58a5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d850a7c4139d627393b3403d7c88dee0":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ebe82b63b4329acd3dddf8fee91cc24f":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildKnownHostList"}}}},"7b9fd5bdbf96fd804b85b2e6c2ecd47f":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3644de56b07cdd11190184706ed228e5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"91124dfe5261a3db5446124c45c066d2":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b38507432d33a09e9a1c71d63ca84cf9":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildKnownHost"}}}},"0123928115b10e9584dd95c4614aa51c":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"805d042081d8e2791b2d86f083a30a65":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f3c75caf5fca09f697a0be5278d73477":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"86562c35b2c1c82cc9892f55977ce5ea":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8aac6f1c0686440b335afebbace283c8":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"84d3a7076cc16631c0f33c404272a4d2":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f69765a7fcac2f8fdde9a7def72faf24":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5b0ee2d9d660cfd7ccf92000753ee379":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"80f276ba1dea73c861a1ba8ae102e0e8":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"47a3de076a177db83406fda6338d0d77":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"22e4e69436e7e05f0850fea92e3ef44b":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"d097ef27488d0d8c7fd43330d71cd282":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a1a89a6bcb10548f85c7ac5f007db80d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8279f6e80a23c785c7246454936719b7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3ea928f9e3b875b7d3e555919b3ee497":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"96f3fad6f9e24b23d1013fe4dc9f0f85":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildEnvironment"}}}},"4af3ad2f321e6e1d2117a79c475a5d2e":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"e34beb51d3d5b86b0e42b95333ff5b86":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2b41b03dc8ce94db238ddc55eb631339":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"92086045d6667b83a0d23e6384d2fd80":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0b685798ea2f52f79e11ba46330bef12":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a0e8f1b1afa00c43984e8589972ec010":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandList"}}}},"f2ac18ebc8522852c02944e8a1a6ac36":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"81e0a3504c3a97330d7f83a4a6d9816c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0187a4844e8ab0039c1e9478dea80210":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3264a4230f27d75fe028fed43c41e60a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"a0b5df79d6d443c81048a8f37cff0650":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"837224e8772c3eb9b08f871c13c592ee":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fa2ced1939ce7c75ab0f35f95a3ace2c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c7d3d3872e62b6cb433d74852b0e21ec":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d035193d40898efbd372345753bd98f0":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"c511d7c9958072673643e7fa4a56ef3d":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e9a3527058d0e800a28344ee26869ce4":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1d1902cc8f3d507b50e5966ec45b9371":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0edb9c973bec8684efd1d5d6048756fc":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"50e9806bcca9f904be3e14a5f50c7010":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"e3227e15bedf15f93b0c5c7f6a3d3a3e":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"eaea973198a7dbc905cfbe9e36ec09d3":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"652dbc07e31335dd6ce01af7a1fc31f6":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ea93d9212c5b928d94bfbd70ff204c91":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3fca3640f6ade0f3a8df48e1a3c461ca":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6aba1a601c7a828183d456ebdb392f6c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"b1902c4cdef4672bfb2047f0221a0d29":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"380eb4f3fad6a03f96c84d944db21eed":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ba2aec6233cb9ba0e80fa8c507bcec69":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"259384ec9a476fd18b73cd47d059b614":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8c1a93c26d2d9673bdaa89a77ac3b5f7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"06567f61e0993db36fcad815f6c81d32":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"040b9828218b0fd4fbf20d86e51b85a9":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"f2bc0a9b5890a95ea9777e8b93ac8d1d":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7346fd7dc806a78dcc3d8b4bba3e858c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"daf29d0bb3d8fe91ec3ccbe92645f829":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f17bc126c1e0839c8af91681c3fca93c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFileList"}}}},"7f62554ea850403afbe10ed4c75c2b38":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"84090e6bfeaf8a514fe46758ecc496e3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"802b2702bd78e9c6e82604f5b4e5585d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"76b233bf896ad6ab62ade9ff2d41874b":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"746e0eefae23eb05b15d9b3e3e506cee":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"2f7f63154e5da2367babb37ce3e29db6":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e9a3a0345a51bb2c6f971e695099281a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"01d530e8e03e5bc09a0a80fb94517076":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"11d35738e915df2ab4864642289cd74e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"e4071cedb098195621a5b8ffe21ec227":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9279259b179bdb43dc2254e4af5d06df":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1e875c339e979153138aa783bdd12dc6":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6b441a28791a33d608bfaaae92393187":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ed09203065f6456f7819f08ca8e27514":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"427c17de61d34f95907cf51bf2a9e115":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"215323c4067d6a0498b324a08104b33b":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ff6e83d7d1380276d6134ac42cd792c0":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9111040fcc540c4f653cd465cc11107e":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3f183b7c4ab4f92837c1db36cc45718f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8eb1191900b63e2f0eb8ab20945fcbea":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigFile"}}}},"b380d378e73643c179a71e6c9319498b":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"f764504cfbfef87fbeefad8ed13c8dce":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cb661ed2e8ee95e6aad25712f0754d87":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6aaf185cbf0ac59012b3a89dadb742e7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"24328d20d145db5611e11bcf87dba918":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d1a7b85159114b4f41a3195bc3bc9675":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"b810aa8bfc9c63313f8fed9e6101eec8":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"44c58cafe768a5e9b3c1f5fd0afd0cab":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"79c774530ab0587d3a6f255b67b6af58":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2b8b1774c039987eca532235f5d481cc":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"240cbbc524f7b3ff33bd81e42a8a79d2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameStageList"}}}},"6541c5d13f6d1d582bbf6be3249ee05e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"beca5c0625b0c7e06cfc72c2c9cccf24":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"be94abc61a03fc7d37676c494d2bf09d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2bea0bba0c814b227ce16e83a8e09acf":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameStageObject"}}}},"e8cb9af5bcd986d23bdd3351cd4ade61":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"a50697e4ebf0911933975ca782a9ef0f":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bf2a5efe2d45d2d8fe908ec12c3860b9":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0eab0da570c1c8fec56ac5b87d9876a7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"502b9722d1d774abef884489bdb6e871":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameStageObject"}}}},"810812dd9ebf2e4f276bb404c158d9fe":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject"}}}},"128bf0ef9de6b4c38cc657992910c442":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f04366d2b6b05c1bbb27f131c0b879d7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0d9113df3069beb3f023f96ef2e112fa":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0a9de2bd07981abedc7ee4cdde2fb9b6":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameStageObject"}}}},"ec7ffeeb7726d6829e9846839ed3a12c":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"050bff62dba370f007a1cf8fc9c9c0b2":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6e2380ca90104bde526034e79e2e6167":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"aa834ca591c02799efc18d24f0a8b089":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a21ef2896af899c4f58d09befcf861ee":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fdc6dd8040ba6124e1d2ce381a584fcb":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameStageObject"}}}},"e3f5a17f05b1961ea27445ce0b256b82":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"52d55a0662724d92afbc61a7d9b4683d":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a5c4884361d9f7798d130224b3c8153e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fc65047b0ca748b4886bd41f262f8dd5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eec882317963d5f566260c41612a93f3":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0e1a78c2ed68c85deadc34ac085b3495":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"4b9b12be132053293778f09837add4a0":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"b1f53e4bebe99c08a128653af48c708d":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b5aca7ffd48f0b9e731d5c0cc2fb3ff6":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2d268b5940da410bf076a8cec7adfba8":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f0d459f57d86608a7677c69e732e039b":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"75b4113a8ec52c0c61183af1d6c978ad":{"description":"Preview Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusIdentifierObject"}}}},"a87ed612284c0389e3a571462c513706":{"description":"Scheduled Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeploymentObject"}}}},"66c338cd706d4c64d764e744c74aeaf0":{"description":"Queued Deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"6ed61e50c53ee4973a1205426305ecc0":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"749b43a9980c5cb9decf594ffb684583":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"6a79781379cb353118a711246dc22eef":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c635c1642d9ee16a204b24cc17ef3f3c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"efd3385d3b8b357ae77a0a17c60fc1cb":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e4fa89d4fca61951a834eb71ae5ead05":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginationRecordsObject"}}}},"b4ecc1bf00d53a12415dae5d38b7621e":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"0a4a4ed475b8d9f9c9ebe366dd2d5ed8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8b924c99eac62f12249368bcaf584861":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e70e8250a07edb97d2aa0393dc424987":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a0a9b94e5cbb6a931ae8eff7f8c3a96b":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdStepMessageList"}}}},"dd5f60a67708c2b1084ef977621521f0":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"99d1ef0f248f46e54baf62903128098d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"02f2fd64d45c216c2aaf1f47ec49a7e7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9931acd2244a28643adb066c29564a3a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"3efe45f6b4650f4a1b405c1038ce78f3":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"08dd1ac0bd18e5d016837c436c44698a":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"45beaa7574b7c7261226108c0c5d06e7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9c8edc4bae9b0a3473ffebe37958c71c":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"807eabbb5ac8ffb92630fa8936ae0571":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"310293f1146ed4ca650cd2a849b177ec":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"86b88b4dcc0b8f2614654c9ce9689a1e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2edec342f2f7663957be6c7e5a472c86":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c02890e79558d2a30bb669db6b8f1af9":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"35e2140ff3278cede33df3db10f63e66":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"60dafaa6c493c115ad917b9f1b83c234":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"726694ae92ad727f826286a90545820b":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"99bfcdcf33df2924d5b9414382b46740":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"75764a63a87faeea1dce23a71af71a98":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2ea582827f2f7db38bc8917c53334ff7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dd05608b05805fc5e32e092f85b70c24":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"368097c8a9e416b79d0285a47c07f114":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"d9e0a9d2e239ba3618994a7e362a5b8e":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"d51d7662b4efbf795bc2cc21a4b24ada":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a2891a7cc9d0aa4a94a98daa94fdd4a4":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fb97273a730eb3d7b781b290c7697d56":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8b302ebaa785858cc1cf71c2440720ef":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariableList"}}}},"7582a8f5ea7f4f97093d76807d0cad87":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d9fd85cb49fbf91fbeb59eca24a266de":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5b8de5ba330d635fe947a221ac501230":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"efaea3e9a6696870004c5b4b01c95889":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"21e3467ea4b89f365274442be370661c":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"3795d87eeadd70a474ef98ed27dd7a50":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"01cb140d81ade074bae672f31dc6ba48":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a706250d270aeafdb851e937db93037e":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5159c9bed1c08fa0c4b8e94724f9e9f7":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"066a3dc7b98daf76979c7d5aae039e75":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"37f87bc5dd60e2c916c4a91c20e536d1":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"42cbd456672296f6bdd5fe17f18eeabc":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0db474368aecd2049008e9896cd2c52a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"be68bbf0210070867a2028df28996075":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"70213b26ed9230dbc23e3e0c502252ef":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"b564c7516e5b4dce30e899617174852f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"82d08d0c5647cc1c086ea3102e8c9aa5":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"123e8332e626c8aa46c770d558c805ad":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"149f128932e613f5716e706f41677c81":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ed47f87c3808f3398d40631e054663a2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentVariable"}}}},"a76488923a0204ceac3a4eba05dd252a":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"f5dec388a643759c53adb89c9c061cb3":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1c9a5db8f7625f8dd02d30c80c1356a0":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5d169f44231ad98a30b301d0e2e9dbb6":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3260015c1ddad593fcac7862ff613f1f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d168a956f1b2bba151dbb21df3c592cb":{"description":"No Content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringValue"}}}},"721c961d7b1ab5fefd97ebb62e6d6f36":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"19922290514f7a0e0a8028d72d0872d7":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ddb4780aa82ed3692122bea9021cf58d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e0a4a42625bf2529cc1f86a3cf6632ad":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cff47f017fcd12c170265df615d80d2e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFileList"}}}},"154b5b485cfe4701595f3eea22f51403":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6f7772eae1504e63298be200f6956c51":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7660a68de6e38315d9fc47dbae24bc94":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a58b1cc793a153a03244130c278e606e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFile"}}}},"41e451c8d24c4547e8f43263193c62e9":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"70e2a1a96256f10e79653bb2a8889a85":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"23d35e15f6de9a126bd9e7d1c777e04b":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c82610f9b80f041c35e0840868b121db":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6e27a6b546ea41709b0bd9575dc7a284":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFile"}}}},"c55cabc446750f5c827cc7057b85bcfd":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3bc8f3808e5e4397bebd6893344b3068":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"466a7f5dfb7e0d47a765a942322c6178":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8121ce3e5a820991f4aa6ce5eb97b490":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a7bbbf33a116ff6cdc3ebadfc49bef7e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFile"}}}},"3d82757c44385f238a74f08d565c14e9":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"82b141c8fb01c1d567987061245b9089":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3b6c25d67f471c3b2732fc84f4894bf7":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0d73a7592255f6238b09a1ca81a8b205":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"107f93ad6e1a45fd9e699e07b5c69ac7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"48a498907444145a88e8f919456cfeb4":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExcludedFile"}}}},"e2faeaa314f6ac5586ccc02e08fa03c8":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"5483714e5bc9a71d43b74f4a46f6d265":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"96d0cf0a4671de521d574c7bb583cb10":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"db744fba305edb6f1e1125492f3918a6":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"335b378c4d678b38e241d3ad0bd5c09a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a40f3fd1516f03906aa5a4dcf7bdfc9d":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"1e58416b03ac3429a0b6e42f0643966e":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"7fa67203fc619313f5596a60b3bd2df6":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7a6cc46669e441edda08550ff4d398e8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"79049e2f5e155bdf4db063ae03b71e11":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c3857d22ee761d8350114d3be48c5fc3":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationList"}}}},"dcb461fa54d0699e223820455d060998":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1735d4e3ae2ed9dfa760a29c641926dd":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"96253e4824d38cb1e80419164980bfed":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d7868b87a95ae32e1dba244c41103137":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Integration"}}}},"14f9bd400650fb4e23893ad84622bfd4":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"073c384015a3aebd132b3c5fd65c3f31":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a50b159910999cd2b7a7233eca43e80a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"32d384b6db4c472fcd8ba09b886e2fae":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0522f4dab223287136a872831715fb2f":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Integration"}}}},"2dc75b5b269a6b463e69626342ba159a":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"99bb409477c56c67cf0225839a4a75c9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bc3137fea46cbeea0b9035f9032be83d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"443814f6b07dd8826e241df53739cb33":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"da8df55b58c8c631ca4fc581b88d3a63":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Integration"}}}},"f45babcba6cfaee2de951f93f4fdf28f":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"1d5a87c3c73c7cbf87219d8c6e7318c3":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9cc48e9b53e2bc4baab55569bff8201d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"38908bb4014b0165987f26cb46358008":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"49520b0927c34ea3177dc99c8b872904":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e6a5165fbeeb7f2a39c0f70ffdfec384":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Integration"}}}},"c37b048f1bf03a3289e3f23775f87179":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"9407873d0092adecb8584df77e58bb9f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4aac72487baea0283b9ae8327b611a4d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"404ec24631e2c434b16be67b4d8661ee":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6e5818c9983b6072c4baa79ffbacdadd":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2db896ac797f4739899f71b7ac796722":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"6740856f2e764800fd19c5a10b97f3c7":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"aa30af6f1e6414646719e4d27dcd1da6":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a3a296b9032aa6cee0711df892fb9416":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b77ea4ef4271c79938dc73669f155d3c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a74f2059131b8fd99c6511cf5926e23a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e95105c97d93433ba654525b185d6be9":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeRubyPhpObject2"}}}},"4a912cf038cc7fbcfe8131af62e8d3d0":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b6b7aa4aa10b62219beb7cde1f5aba60":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bffd15cac38d82fc6ac7018a94863a3b":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"40ed130c568b7d53ad6607a0ae2ee6da":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Repository"}}}},"c789a1fb476f586b9f288182f59ad7ea":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3a64f0a49fa4e21ea947653da12b9c85":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0ff1aabda91b6de265e5e193ed9067a5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"44172b2f43e49b3ea987d38004529800":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b78a516f08f313ebf966d08ef14bf569":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Repository"}}}},"39baed0239959d1d275d681f031be251":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"290f301b7c8c813dc260f264c88fccab":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"822b9aedccb72cc4f614e41f784e4fd8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"610db3a0958e1d7c5994c9637718a341":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"37ddff985a4e62d003046ebe2c567924":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0b7ee6ec3eaef7136409e9d53e8dec96":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Repository"}}}},"365acb382211406a8767504bdb1d6a35":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"3416b7e579a49e8140464052bec1ea7a":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eb8616630528cd500c0bc794c84de8d9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ec06097c9ab414447e3c9e2429c4b247":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1c5bfb2e492be055a9e8cfd8facb9ed7":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"55c2de1948a9c2b7e95acd11b4e42f95":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Repository"}}}},"b5f263e30e80da16cb556cac1b80839a":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"5683cf8ff2e5086cd198fe72f40e0d30":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1b3745aa434d4bb704a2e6f52d3d223c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0d37186b0b61056384dce25d1f044256":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"28f5d02aedce1df672a0b2d39611e56c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/35f912b4c5029a0c4c2f7fd81f392ebf"}}}},"73f26899a01ec2f902a4336e6fac3943":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ec7d8d45a274c3c4b22a3f9c70c37e04":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2ee64a17ea728cee3772b4ca5f0c1a16":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ce7b37b2aba51447dc1e56ce25a3f103":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefAuthorEmailObject"}}}},"13399002e8c9d0efa43cfcd51f439653":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageObject"}}}},"096b62c9e1d083fb068243d2e7f155ff":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"4599a43004b2f5836b8f4883a9035434":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7ac62c40f4c477c9e8436c94bc7fee76":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9675afccb35cbdfef5513f8fd9f6b3da":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c2e39330dba5a234c9d32695338126e6":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefObject"}}}},"b16531b80170f406f825e9180af22217":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"cccf5fa9e57517727077c383ae704955":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"21bb7ba7b03ed35440b09f1564999d9d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9842d98c6c50cfeb7e427db5a13c946f":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitsTagsReleasesObject"}}}},"f8362dfa925794f8093cde435a12064a":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b85b36ff27898e29fc5312325cca63a5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9538b824662c27daa2aac84a520bfaf5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"504a6bebe604b1b1fcc6a5fe19179dda":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeploymentList"}}}},"a53af47e957dfc517594d0a951d35481":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"27479d833b96fe80a48c25294029bc50":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ce5de726bf90cab21abcd39631591ca4":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1032b2402c1d8ff0ea1a62e493d3e51c":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeployment"}}}},"750d1929299f00cf8c5e367fe29f96fd":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"e3bd9ac4da482832cdf5b946d76dcd52":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c4a4914af306f43902229141ff231fd9":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"66c60105a7177d6b1ff914b30fe4672a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7fa7fa4566f34d4259affd05480217f2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeployment"}}}},"efa21c76689355236b36e65c3e57d4d1":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5957a685ac5bd791255f00ced12af650":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"784be6eae82c3421b820fb818958a289":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"350103304c1431153b80c6a1e05dde29":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"836bb3a2a5533d8e8726af0497df528e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeployment"}}}},"87bdbc6757e806209d359f3738f0987a":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"0471873bbb4e63c7418e5321b7577472":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4cdecf49cc3560e084e7f7123e501bdd":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0c1c59b187abb03a2da3fab99ed91e37":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f7d61ce1d6a294d463083e3732e21f4c":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8ac305389f01ce019e5997fb7fb7c8ac":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledDeployment"}}}},"341ab7e5f02957b3b37d5b9cd0e14a4d":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"779ba58ac87b0dd920d957f2e75b3d0a":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2026063774a3aa39bf2e1917df65242d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eabdfed8cbc884e398ad499bf908c5f6":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f55d92c2312fcc3f15ae4354a8fed090":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f40428a1b46102d0c2d0143dcae3d5ec":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"1faba8e89844a784cd067d1a02fabfae":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"ce5dda4f7120718a0514e401c4d234f7":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"67063bd21241ef7f9d30ff7bb0613180":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"46800fdf72dc4334956146ca931b66c4":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1f52fc405df057e2f6bc3383422d7ed0":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5ff4c8c5ccf36009800ffa5af773c226":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroupList"}}}},"95484088ad7bcd949b69f347845534ed":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a944004ae7d204831747fca5f493f080":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b36dd25f67c493b21ff430590032f838":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6d3c2fac14e7e5f50ebd86fafd683175":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroup"}}}},"af1731085713c9ca6ac2a1010f970f2a":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"31cf677053251122820f18fd71bd7a4f":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2193b5a15c83ec3af4ee488f6fbf4bd5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0428433a03603f99411860491dcbe91e":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1acbcd33819bbba55b2d94070bffccc0":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroup"}}}},"892e79fa70811dbfd341ccd75ef53a45":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2e0bffa74fe6519dfd5b47fe3eb06143":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b49c5a50a7d1a3315042f9896a794b2d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8b581e09b9e13127cb896fdae1da8df5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2444eaef0edff32d18264255ebe8d72c":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroup"}}}},"acaa3c12ef50f2a6ef665d6aaffd7b4e":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"655a94ab0a530d84cf0709041b80e62e":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a4833ea7f2626c30ddb042b27d4f881d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"360f7899635fe86396f475191b3caa63":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8421b43d99e7b592fdfdaa8010c6a061":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"86cff6ebdc96c12027fd446415cc2ed0":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerGroup"}}}},"592006f4f4d16b64e2676febfc83b1b0":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"5c0057ca25510e944a74f1f028d61bbc":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b445cbd603b397090e1e115ffb459bae":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"911c4dbdd40444b025438fcb22f681c4":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eed887f259d56cbfee15240517413c98":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7d2c502ead02ecb3076930fc3f6fe1ef":{"description":"Deleted or Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"5a09582f926cffd73ef301af4de0a050":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"390231c9e3eadbd859d42939560b3c74":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1e58d11f12d2cab98c0235f9db4711c5":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e925cc5dee57eac42c0bf4d6d9f53b2a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9878ac8123afacaf53491d870c50e3b1":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerList"}}}},"06ecc44a95bfd2456bf246d4b9c3b1f1":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0432deb0f7342624cee676c46f5f8271":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e467eba917f4d1d6a36a842a2ea4277d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dbc33a0d805595caadb02445b2c9042a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"e8de2e516ac3e7467f08a04ef28ef2e7":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"24f2c79f23348230f628f39bdc81ece8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"80249248f0e050cac478ee1bafa87b21":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4739f76e91e55ce5a4c42ca867de5bca":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4c3cb25e93bb75a2955e2c3f9d178521":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdIdentifierNameObject"}}}},"758d076076eff718cd34b8d4072fb4e0":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d29531a670483e81279e3d30e9537e41":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d6013e069f366818d7582ac1524e4b47":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"57ea70f927795e28b9a3e0ac9cc64f69":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"43f7b06b8c15dee9310b76b3d7ddb9b5":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"a94921f35cffbaf79a55b9feb02fa42e":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"b30e49b0ec2080b276079c692439523b":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0a1ac69cac4b41cffab5f685a05bcd58":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bd3859e9fc72e877a1c31cf21ced1f7a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"faf092eb23192355b9e1f6af8a7910f0":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"26e54ceee751e33b86dbfab6770a92ea":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"2266bbf93cd65671a5dbf791044854dd":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"1e6f4f5f3502ce62d4c1f5afd043332c":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"de15a8a3b11908c6dc0235a78294ffa5":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f19f93b825ed0cf90dbbe1e829ddd977":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ccaf8b07478e775544f3dd154fdb4101":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0983c0083858e0ddced0daa4628460d5":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"8fdc9fdfe049277e02577a75b0368c10":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseObject"}}}},"a4b58688072172c535adf96fae646970":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d8a3c7fed3c9dec1fd04ad9908d10848":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a9e47e9053a40bb7358385bb89d94523":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1c8e77f0e47598efcd90275b20981388":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8e751791121aac02129ee338d47adba2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Server"}}}},"f4fd2ff8a1a8aa54b11fc511f51f8ef4":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9ca2f4f8789b543479e2f4128ade1ebd":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a62dbd973de4f912e2a15cb561ed0d15":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3a00ee242794f1ce42a67e0631b9aefd":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CpuMemoryDiskObject"}}}},"edfed966d5666948d522ec5056bd7345":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"75b649dc841bacc7409cca70af93ca6f":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"5a6ec65510f239b29e8dbfdfdbb7fcff":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9730567a843a046988febb274aad2d57":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"52c838a799e7050ab4fbba8096d46c4d":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierStatusCreatedAtObject"}}}},"3fbc108fcb39840aacc231a2126d17de":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dfffff19431c4d091d3a119c975df0d0":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"66b7b4ad1e7eff3755d7576bcc8c1bae":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eb693318490641cb808c37dfa19cc0fb":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"297e806d231affb51615ef5175ca74df":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandList"}}}},"4067c5bbb4601cb5481af3aea1e058c3":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"271867a5854832192d3261213b01ab45":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8d18689254b840dc2bc88d9ed98579b9":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"68f23fccdf39eb43f7066c4762f469ea":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"d261d01cf46bf61ec644a9e49a4b764b":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"fbc802a1584de6e937091dc1951e7c5b":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"36599f877b78ee04e2059f6a16b28c30":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a1ac20b8c50fc1ab08c97ee8e7f1da4f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0ef94e0c78be7eccd9463191e4a70060":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"16a2533025662f4a2f20adc7170533fc":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f9e6b4a526384191b99d24d2f79c4e86":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c89d1db274b44f8eb40f3f1bd780bfb3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"afe27ad81f16df4f1385c9522545e1ef":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b9d7e57ae385105f320d590fb3440e79":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"94d94c286119d59d14090ba533314cfc":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"b1e9589245fad77b56ad6c7b6b56b228":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"21279ed37ace7fbd41f608857670e6e7":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d16371942e2feb31dd937e3cceffdc80":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f0a32e24a824a27c3c942fc1e3f12788":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e56c758afc5cae42a69ee092499edcd2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Command"}}}},"6612a7b5c3277c5f5f3a3ff8f1da679f":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"ac791ded1b60b1197d3dd570343fb4d8":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b3d37e2420036f16165fc1eedd2a6f15":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ded75d310bb3fde675e08ca12106b8e6":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"29a0a2bc421348d3b47e8a65da0dba31":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ad66716b408e41b5b762b3d7c480d059":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"b54132ffbc15af27bf6fe60ed9a15cdd":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"802ba874ea51c9747eea2827059dcd97":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1f00eaa686c9daddd216ae188cc9a39c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8c1b351c1a16e2a57ec9a19cd11a950a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"32df4c470370d7e51d1012503efc57d8":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierStatusCreatedAtObject"}}}},"be08c5118a9b7faeb8e6998a2b83546f":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"dff21af36d1c228d49bd312712d02d7a":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6f74c367379b1ce47542dde863d39c70":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f152f05f1d0fe1a37eec7dfa268258d6":{"description":"The server could not process the request due to semantic errors. Please check your input and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c588bf5d013d7bc7d5a936c9aa85742e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierStatusCreatedAtObject2"}}}},"072057c6c79682279ddc39e31f22201b":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bb04bc423ebe8b8c10bcd0b2168a92c3":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"81818c4dc78e544df2ae03b3e0b2c411":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6e8914267fad1109497a112375863481":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"84c3e3e646523eb3db9611e5e1aa2901":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusMessageObject"}}}},"52084ab03125fa5d7f683340979090f9":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"1a78cb40d89df2a07fc3093d551d618d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"97bf43fe70d6d700a4584429ec4b5c6f":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a251a53a14692e59df8840ce6b5c3bc3":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2b3dd9ff2a02a3daa50cea7e254dae4a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyIdentifierDescriptionObject"}}}},"23ba0eb1901527b7fafe335ed972bbc9":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"ea14d4451b440320e691c03724136677":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"21ae72d79278200b9fd544e8964455f1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c3cbcb1ab869acc6448a8e46a6e2f400":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b40bdc5d0b36a0f6803b3b315c7751f7":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"47d09650d51c6a11ad588e53cfd31394":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"e22bcb72f040680a9e94b55ca1821173":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3c50e0dc648360a927becc5c81e249db":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eaed1bcc1dfee67b8e8baefe75914471":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5c2a3cac75c775cd906a5fa252fec092":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"bb5f1d0970f7dc19056c0a40285cc9a6":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierTitlePublicKeyList"}}}},"f132f39653caf90109804ecd03bea026":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ffa83a4b171026234a3d5c8aae9a7bac":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ed2a067b832756af00cc46d30df48875":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c118197247ee4aa5a80745ee21f6e56c":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierTitlePublicKeyObject"}}}},"c5f4f0f458a55fe1368b1e90439506d5":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"c511b0fcff6cd840e72ed7b108f8e421":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorsObject2"}}}},"fc0cbb3b890209c0818b449cea3b7975":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a019ed35d22ae1ab4e4727c383682426":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"74e9e14fbb49b3d55617bd65d88e6a3a":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2a338109e40351670de6b213d68e1cae":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"eab0f4026c4c8166122fe26f007f7e3b":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject2"}}}},"352732c4bc931510e121233d982201f9":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4a31138a0306b909d09cc4a85128a7d8":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e6811f33199b573f7443b55af0a05a7e":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"528ea5694181c77afd31bd1e7981148a":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringValue"}}}},"93b7038a69ed5a4cfb09d4e2494e13a3":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"d1bf2d04035acbba772803ce751a1ac9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"15083c0ac245fba1bcdcf618d10914e6":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c853460dc88c8d872e7c971aa2b68cd7":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameIsAdminList"}}}},"6b1276789246afb02f88922e3cc04e75":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"42d218c285725ea0045993fd1bbed2b3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f99792b6b3ab2923f437a638b4012a3c":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"75fa73853a18242b80a89fdfe2fb205e":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameIsAdminObject"}}}},"0bc0a6b80036d7e047f84a3945b7b889":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"d50620a3b93e4b02b2b7501fd5f1432c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"25de90792fdd69b63f818ead7edd83f1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7749c09f188e749b150cf17ccbb44176":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"767a91f7fef57b8edc551214de4d9c77":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameIsAdminObject"}}}},"994a5c4b1d8c50b818b1fdb2cbf629af":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"19b63f7155963813932d7fa134e1adc9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e4c0a49f1ed84ad7e735b8bc316b66b7":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f3e777122be45276d820689ae040ee8c":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e29af71831dca49095ed35e10ccfb318":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameIsAdminObject"}}}},"83ee3c3787ab7d21ab60d2b6d3fd3dd6":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"3b9532bdec5c598c633c3edf0004671f":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"364954450e33b23e97dfa2819ed639d7":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"49198e1dbeac1a169ec11105a5d4323b":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e9fe31f758bff0accd9369e0c0198154":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"813ccdd617eed3174c04db7ff42f0b58":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierNameIsAdminObject"}}}},"04e233587296d1dbf811c5a8f9c9acf3":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"d4352565b8f1ec026933ff6e4becdd0c":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e75fe9660fa3d852f5cc525dd49ff4ff":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1df91d48b85486589b875c055c7805de":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"41568ff885b4e57a706c8ed354c2103c":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c98e7f6bcc437bea688a0d25478a0d10":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"3af060cd7ae1482af8be680a8f484266":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4a913b4eaff9dcff390c9479c71d4822":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b51f5135e513b1e49c3e978c7628fc97":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"875ef7de07b87cfad8e7a12e27b05fd4":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4ff597ee78e72688b89942288fbf9ba2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateList"}}}},"7bdff8c849854da73c86b2467c109429":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"63938718452ed237c8cc5af6e8e8df61":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fb86392f0cc6de1fb200e576c0c57c57":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9c89f935bdf7c2db782762f4cea93c9b":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"f17414563eb49a07c25c0bd442cad156":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"ea7b629bfd6dd357eca8f95dc2660b04":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b66b867c000ee68e53b7e4a31b9e9a5d":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"41838a756c20604077dbfdc1d3e3bfb6":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"8c90c157776b53413e08b35460c6a834":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdPermalinkNameList"}}}},"4e44f06cf1de4eb4818f840b74df4c63":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"623b4556e66382420a4e4c0729257f96":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"056733f9b9b1a7abee41e7a2a4c30854":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"233191f35c22922c28cafc5e77460a44":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"6fc029d43dd7b0511932fcf95b548e30":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"f5a675297f7c82ad38d131b686e2da45":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"700cde56958e1c9b42c25d7af0b46b72":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"48cdf82e98c0d5dc4ff47c15e934169c":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"a6a53ce444ca79f5242021c5db44b229":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3f060b15d14dd47ee803a02c33601b7a":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"445681bd9af6e07dc6d5849e88e0850f":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"a0ae8f0525af15c21e98ee9d888ae4c6":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b7ec520e7a7e8faec8078b1d0e3315e9":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d38f3dc72a6c38c8ccee5b909e528c90":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4408acefea88bbdeed86a17a72dd489f":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0184434317cac9e9d64c615742e28059":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"158bcc0d22291fab6cd1362d15ec6c67":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ef3879d0dc2f01005650a12e6710dfb7":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5237b0b64e5c45e756b948125cb8c147":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"f4daf9c810bbfb8d726ceaa7733abff8":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"6437978b7823c6a333bef8806f91caaf":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdPermalinkNameObject"}}}},"e41be8156ba55749c1767fb6b60a00c6":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"ff0e901236f4702b5c48e47f0f04cde5":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"7a5ca05e9797fe7aa2306b2e56faba53":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"eb9f89e7c7c276bb21dbe3193099cdf9":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"107a45711ef92955940b3d26e3502aff":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserList"}}}},"b0d2910112dd88e415836b5ba42cb68c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"465b518af585009a52ff2002eeba6130":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"0bd4a6e9427579b47c63a859d6ac30af":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"5aff0ce728f177b31ebdaf389ab630a4":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdIdentifierFirstNameObject"}}}},"d2365113fe53cc0f45a0f515e319ad4c":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"468611f7672da1bda624606201e1cd1c":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"37395e452858e92762baa7c3855534f3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"39d2966bad2bab491f1ca985cded5fd6":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"932756b8fe0acf2665760a54add581ad":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"29b3b2c6d252ff4deba83617dc2bb0b3":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ba8421eaa37959180044f197090fe2cc":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"611a572b8111bd2a10f604eac5550e41":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"765a6568ca261a390cbe949a2f3733c5":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"73c69751f5c4d2a5f86f49a1e0051102":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdIdentifierFirstNameObject"}}}},"5f10cc3d3764a11cadc1f5b836ac2a16":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorErrorCodeObject"}}}},"fee2db615800b3aa92ccb6347b4e9c52":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"d0cf930f1b5b1bfae96fea1834707810":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"3acf3d3e80dafaafd6dfa33bd666780b":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"867b1deb2c4645d318f9c1b6edd5a980":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"4d9c037ba7c350700f58f92116715d47":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdIdentifierFirstNameObject"}}}},"452dd4968ea0e9afbd3eb9697c78e836":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorErrorCodeObject"}}}},"0f4f7fa37227b6bb65a356d3e865c589":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FieldWithErrorObject"}}}},"57368d5aa3b620a9e523975bdf5e5b9e":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"83784925c7359e71520f3bb83674ee9d":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"1f3f1809de55a6d9d620b126f8b32ace":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"04c39620824d6ca4bfcefa36e0d08508":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"3f5ebae6fbc3564b7cd778aeb29b7281":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorErrorCodeObject"}}}},"9178b7a9d8cf20672694a3e01e38fd54":{"description":"The requested resource could not be found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"9ed39d7bfee44f57e8e95ef21d824a99":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"86f1ec8b49aa04747f01dd49430d524d":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"fb301bd24bc6c30c287ef995a5b6c7af":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusObject"}}}},"09642bb404a8a3636f40f2ea03a0a58b":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorErrorCodeObject"}}}},"0bfd013437cd4852d2f412e89b3686b6":{"description":"Unprocessable Entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorObject2"}}}},"ad05a74d05036c7db642a2cafb1dc549":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"38ab41caa5c6643d9b34ba01619342ce":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"ef08cf740c3e49d8ac17f3a56b42bc44":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifierDescriptionList"}}}},"cbc0b1d359b29d2dc404356b9a37a4f1":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"2a1f1682677fad5cd01b9cff4bcec9b1":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"e8523f0857de406c78bb71bd62aa4f85":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c7fa4edf358344ef6ce580dacf57a91e":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringValue"}}}},"c55d9c6bdb62063ea8d6a6871b668d29":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringValue"}}}},"9bdf36baa31f8eaf90dd365d6d9be4a5":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"69e04b5c36cad8ad2be5c2f3e27bbea2":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"c9d19ffb3c44249c611b776cbfbfc26e":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"27ee917247d1016c4f7d94dd80fa43e2":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringValue"}}}},"69a1adc5fe613852713a1910241d66b7":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StringValue"}}}},"78b277bb8b63d67edb6810356d37874e":{"description":"You are not authorized to access this resource. You need to authenticate yourself first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"b88a1b00fa498132243c9e87bd64e2e3":{"description":"You are not allowed to access this resource. You do not have the necessary permissions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}},"d6fa7919605782334ac171f7a2253ab8":{"description":"An unexpected error occurred on the server. The server was unable to process the request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusErrorObject"}}}}},"schemas":{"NamePermalinkTimeZoneObject":{"type":"object","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"time_zone":{"type":"string"},"package":{"type":"string"},"trialling":{"type":"boolean"},"suspended":{"type":"boolean"},"project_count":{"type":"integer"},"project_limit":{"type":"integer"},"users_allowed":{"type":"boolean"},"ai_features_disabled":{"type":"boolean"}}},"StatusErrorObject":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"}},"required":["status"]},"AccountObject":{"type":"object","properties":{"account":{"type":"object","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"time_zone":{"type":"string"},"ip_restricted":{"type":"boolean"},"two_factor_auth_required":{"type":"boolean"},"strong_password_required":{"type":"boolean"},"ai_features_disabled":{"type":"boolean"}}}},"required":["account"]},"FieldWithErrorObject":{"type":"object","properties":{"field_with_error":{"type":"string"}},"required":["field_with_error"]},"PackageFrequencyTriallingObject":{"type":"object","properties":{"package":{"type":"string"},"frequency":{"type":"integer"},"trialling":{"type":"boolean"},"suspended":{"type":"boolean"},"scheduled_change":{"type":"object","properties":{"target_package":{"type":"string"},"target_frequency":{"type":"integer"},"effective_at":{"type":"string"}}}}},"AgentList":{"type":"array","items":{"type":"object","title":"Agent","properties":{"id":{"type":"integer"},"created_at":{"type":"string"},"identifier":{"type":"string"},"name":{"type":"string"},"online":{"type":"boolean"},"revoked_at":{"type":["null","string"]},"updated_at":{"type":"string"}},"required":[],"additionalProperties":false}},"AgentObject":{"type":"object","properties":{"agent":{"type":"object","properties":{"claim_code":{"type":"string"}},"required":["claim_code"]}},"required":["agent"]},"Agent":{"type":"object","title":"Agent","properties":{"id":{"type":"integer"},"created_at":{"type":"string"},"identifier":{"type":"string"},"name":{"type":"string"},"online":{"type":"boolean"},"revoked_at":{"type":["null","string"]},"updated_at":{"type":"string"}},"required":[],"additionalProperties":false},"ClaimCodeObject":{"type":"object","properties":{"claim_code":{"type":"array","items":{"type":"string"}}},"required":["claim_code"]},"AgentObject2":{"type":"object","properties":{"agent":{"type":"object","properties":{"name":{"type":"string"}}}},"required":["agent"]},"ErrorsObject":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"string"}}},"required":["errors"]},"StatusObject":{"type":"object","properties":{"status":{"type":"string"}}},"FieldWithErrorObject2":{"type":"object","properties":{"field_with_error":{"type":"string"}}},"EmailPasswordTermsAcceptedObject":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"},"terms_accepted":{"type":"boolean"},"account_name":{"type":"string"},"full_name":{"type":"string"},"package":{"type":"string"},"coupon":{"type":"string"},"newsletter_opt_in":{"type":"boolean"},"signup_source":{"type":"string"},"client":{"type":"string"},"utm_params":{"type":"hash"}},"required":["email","password","terms_accepted"]},"AccountApiKeySshPublicKeyObject":{"type":"object","properties":{"account":{"type":"hash"},"api_key":{"type":"string"},"ssh_public_key":{"type":"hash"},"oauth_urls":{"type":"hash"},"mcp_config":{"type":"hash"},"email_verified":{"type":"boolean"},"client":{"type":"string"}}},"StatusErrorsObject":{"type":"object","properties":{"status":{"type":"string"},"errors":{"type":"array","items":{"type":"string"}}}},"ProtocolObject":{"type":"object","properties":{"protocol":{"type":"string"}}},"EnrolledBetaFeaturesObject":{"type":"object","properties":{"enrolled":{"type":"boolean"},"beta_features":{"type":"boolean"}}},"ErrorMessageBetaUrlObject":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"beta_url":{"type":"string"}}},"ErrorMessageObject":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}}},"DetectionObject":{"type":"object","properties":{"detection":{"type":"object","properties":{"filenames":{"type":"array","items":{"type":"string"}},"files":{"type":"hash"}}}},"required":["detection"]},"StackVersionEvidenceObject":{"type":"object","properties":{"stack":{"type":"string"},"version":{"type":"string"},"evidence":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"suggested_protocol":{"type":"string"},"static_hosting":{"type":"object","properties":{"eligibility":{"type":"string"},"root_path":{"type":"string"},"spa_mode":{"type":"boolean"},"confidence":{"type":"string"}}},"build_commands":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string"},"command":{"type":"string"}}}}}},"ErrorObject":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"ConfigFileList":{"type":"array","items":{"type":"object","title":"ConfigFile","properties":{"identifier":{"type":"string"},"description":{"type":"string"},"path":{"type":"string"},"body":{"type":"string"},"build":{"type":"boolean"},"servers":{"type":"array","items":{"type":"object","title":null,"properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":"string"},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":"boolean"},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"hostname":{"type":"string"},"username":{"type":"string"},"port":{"type":"integer"},"use_ssh_keys":{"type":"boolean"},"host_key":{"type":"string"},"unlink_before_upload":{"type":"boolean"}},"required":[],"additionalProperties":false}}},"required":[],"additionalProperties":false}},"ConfigFileObject":{"type":"object","properties":{"config_file":{"type":"object","properties":{"path":{"type":"string"},"body":{"type":"string"},"build":{"type":"boolean"},"language":{"type":"string"},"description":{"type":"string"}}}},"required":["config_file"]},"ConfigFile":{"type":"object","title":"ConfigFile","properties":{"identifier":{"type":"string"},"description":{"type":"string"},"path":{"type":"string"},"body":{"type":"string"},"build":{"type":"boolean"},"servers":{"type":"array","items":{"type":"object","title":null,"properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":"string"},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":"boolean"},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"hostname":{"type":"string"},"username":{"type":"string"},"port":{"type":"integer"},"use_ssh_keys":{"type":"boolean"},"host_key":{"type":"string"},"unlink_before_upload":{"type":"boolean"}},"required":[],"additionalProperties":false}}},"required":[],"additionalProperties":false},"EnvironmentVariableList":{"type":"array","items":{"type":"object","title":"EnvironmentVariable"}},"EnvironmentVariableObject":{"type":"object","properties":{"environment_variable":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"locked":{"type":"boolean"}},"required":["name","value"]}},"required":["environment_variable"]},"EnvironmentVariable":{"type":"object","title":"EnvironmentVariable"},"ErrorsObject2":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"string"}}}},"EnvironmentVariableObject2":{"type":"object","properties":{"environment_variable":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"locked":{"type":"boolean"}}}},"required":["environment_variable"]},"ServerList":{"type":"array","items":{"type":"object","title":"Server","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":["null","string"]},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":["null","boolean"]},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"static_hosting":{"type":"object","title":"static_hosting","properties":{"hosted_website_identifier":{"type":"string"},"subdomain":{"type":"string"},"url":{"type":"string"},"spa_mode":{"type":"boolean"},"status":{"type":"string"},"active_deployment_uuid":{"type":["null","string"]}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false}},"ServerObject":{"type":"object","properties":{"server":{"type":"object","properties":{"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"environment":{"type":"string"},"root_path":{"type":"string"},"agent_id":{"type":"string"},"enabled":{"type":"boolean"},"global_key_pair_id":{"type":"string"}}}},"required":["server"]},"Server":{"type":"object","title":"Server","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":["null","string"]},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":["null","boolean"]},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"static_hosting":{"type":"object","title":"static_hosting","properties":{"hosted_website_identifier":{"type":"string"},"subdomain":{"type":"string"},"url":{"type":"string"},"spa_mode":{"type":"boolean"},"status":{"type":"string"},"active_deployment_uuid":{"type":["null","string"]}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"KindIdentifierNameList":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"identifier":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"monthly_cost":{"type":"float"},"region":{"type":"string"},"size":{"type":"string"},"ip_address":{"type":"string"},"ssh_key":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"public_key":{"type":"string"},"key_type":{"type":"string"},"fingerprint":{"type":"string"},"account":{"type":"string"}}},"subdomain":{"type":"string"},"spa_mode":{"type":"boolean"},"storage_bytes_total":{"type":"integer"},"active_deployment_uuid":{"type":"string"},"deployment_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}},"KindIdentifierNameObject":{"type":"object","properties":{"kind":{"type":"string"},"identifier":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"monthly_cost":{"type":"float"},"region":{"type":"string"},"size":{"type":"string"},"ip_address":{"type":"string"},"ssh_key":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"public_key":{"type":"string"},"key_type":{"type":"string"},"fingerprint":{"type":"string"},"account":{"type":"string"}}},"subdomain":{"type":"string"},"spa_mode":{"type":"boolean"},"storage_bytes_total":{"type":"integer"},"active_deployment_uuid":{"type":"string"},"deployment_count":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"ErrorObject2":{"type":"object","properties":{"error":{"type":"string"}}},"NodeRubyPhpObject":{"type":"object","properties":{"node":{"type":"array","items":{"type":"string"}},"ruby":{"type":"array","items":{"type":"string"}},"php":{"type":"array","items":{"type":"string"}}}},"GroupedRegionsObject":{"type":"object","properties":{"grouped_regions":{"type":"hash"}}},"SizesObject":{"type":"object","properties":{"sizes":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"label":{"type":"string"},"monthly_cost":{"type":"float"},"currency":{"type":"string"}}}}}},"FirstNameLastNameEmailAddressObject":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":"string"},"account":{"type":"object","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"time_zone":{"type":"string"},"package":{"type":"string"},"trialling":{"type":"boolean"},"suspended":{"type":"boolean"},"project_count":{"type":"integer"},"project_limit":{"type":"integer"},"users_allowed":{"type":"boolean"},"beta_features":{"type":"boolean"},"static_hosting_eligible":{"type":"boolean"},"managed_vps_eligible":{"type":"boolean"}}}}},"UserObject":{"type":"object","properties":{"user":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":"string"},"alphabetic_sort":{"type":"boolean"}}}},"required":["user"]},"NamePermalinkIdentifierList":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"identifier":{"type":"string"},"public_key":{"type":"string"},"repository":{"type":"hash"},"repository_url":{"type":"string"},"zone":{"type":"string"},"last_deployed_at":{"type":"string"},"auto_deploy_url":{"type":"string"},"url":{"type":"string"},"app_url":{"type":"string"},"servers_url":{"type":"string"},"deployments_url":{"type":"string"},"config_files_url":{"type":"string"},"environment_variables_url":{"type":"string"},"build_commands_url":{"type":"string"},"starred":{"type":"boolean"}}}},"ProjectObject":{"type":"object","properties":{"project":{"type":"object","properties":{"name":{"type":"string"},"keypair_identifier":{"type":"string"},"zone_id":{"type":"string"},"template_id":{"type":"string"}}}},"required":["project"]},"Project":{"type":"object","title":"Project","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"identifier":{"type":"string"},"public_key":{"type":"string"},"repository":{"type":"object","title":"repository","properties":{"scm_type":{"type":"string"},"url":{"type":"string"},"port":{"type":["null","integer"]},"username":{"type":["null","string"]},"branch":{"type":"string"},"cached":{"type":"boolean"},"hosting_service":{"type":"object","title":"hosting_service","properties":{"name":{"type":"string"},"url":{"type":"string"},"tree_url":{"type":"string"},"commits_url":{"type":"string"}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"repository_url":{"type":"string"},"zone":{"type":"string"},"last_deployed_at":{"type":["null","string"]},"auto_deploy_url":{"type":"string"}},"required":[],"additionalProperties":false},"NamePermalinkIdentifierObject":{"type":"object","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"identifier":{"type":"string"},"public_key":{"type":"string"},"repository":{"type":"hash"},"repository_url":{"type":"string"},"zone":{"type":"string"},"last_deployed_at":{"type":"string"},"auto_deploy_url":{"type":"string"},"url":{"type":"string"},"app_url":{"type":"string"},"servers_url":{"type":"string"},"deployments_url":{"type":"string"},"config_files_url":{"type":"string"},"environment_variables_url":{"type":"string"},"build_commands_url":{"type":"string"},"capabilities":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"url":{"type":"string"}}}},"starred":{"type":"boolean"}}},"ProjectObject2":{"type":"object","properties":{"project":{"type":"object","properties":{"name":{"type":"string"},"email_notify_on":{"type":"string"},"notification_email":{"type":"string"},"notify_committer":{"type":"boolean"},"zone_id":{"type":"string"},"permalink":{"type":"string"},"custom_private_key":{"type":"string"},"check_undeployed_changes":{"type":"string"},"store_artifacts_enabled":{"type":"string"}}}},"required":["project"]},"BaseObject":{"type":"object","properties":{"base":{"type":"array","items":{"type":"string"}}}},"StartRefEndRefObject":{"type":"object","properties":{"start_ref":{"type":"string"},"end_ref":{"type":"string"}},"required":["end_ref"]},"SuccessOverviewObject":{"type":"object","properties":{"success":{"type":"boolean"},"overview":{"type":"string"}},"required":["success","overview"]},"SuccessErrorObject":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}},"required":["success","error"]},"PeriodServersSummaryObject":{"type":"object","properties":{"period":{"type":"integer"},"servers":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"group":{"type":"boolean"},"latest_status":{"type":"string"},"recent_deploys":{"type":"array","items":{"type":"object","properties":{"status":{"type":"string"},"duration":{"type":"integer"}}}},"speed":{"type":"integer"},"reliability":{"type":"float"},"deploys_per_week":{"type":"float"},"total":{"type":"integer"}}}},"summary":{"type":"object","properties":{"total_deploys":{"type":"object","properties":{"value":{"type":"integer"},"delta":{"type":"float"}}},"success_rate":{"type":"object","properties":{"value":{"type":"float"},"delta":{"type":"float"}}},"avg_duration":{"type":"object","properties":{"value":{"type":"integer"},"delta":{"type":"float"}}},"active_servers":{"type":"object","properties":{"value":{"type":"integer"},"delta":{"type":"string"}}}}}}},"ProjectObject3":{"type":"object","properties":{"project":{"type":"object","properties":{"key_type":{"type":"string"}}}}},"PublicKeyObject":{"type":"object","properties":{"public_key":{"type":"string"}},"required":["public_key"]},"StringList":{"type":"array","items":{"type":"string"}},"MessageStarredObject":{"type":"object","properties":{"message":{"type":"string"},"starred":{"type":"boolean"}}},"CountTruncatedLatestDeployedRevisionObject":{"type":"object","properties":{"count":{"type":"integer"},"truncated":{"type":"boolean"},"latest_deployed_revision":{"type":"string"},"current_revision":{"type":"string"},"last_checked_at":{"type":"string"},"check_in_progress":{"type":"boolean"},"check_fail_message":{"type":"string"},"commits":{"type":"array","items":{"type":"object","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"url":{"type":"string"}}}}},"required":["count","truncated","check_in_progress"]},"ProjectObject4":{"type":"object","properties":{"project":{"type":"object","properties":{"custom_private_key":{"type":"string"}},"required":["custom_private_key"]}},"required":["project"]},"WebhookUrlDeployablesObject":{"type":"object","properties":{"webhook_url":{"type":"string"},"deployables":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"auto_deploy":{"type":"boolean"},"preferred_branch":{"type":"string"}}}}}},"DeployablesObject":{"type":"object","properties":{"deployables":{"type":"hash"}},"required":["deployables"]},"DeployablesObject2":{"type":"object","properties":{"deployables":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"auto_deploy":{"type":"boolean"},"preferred_branch":{"type":"string"}}}}}},"BuildCacheFileList":{"type":"array","items":{"type":"object","title":"BuildCacheFile","properties":{"id":{"type":"integer"},"project_id":{"type":["null","integer"]},"identifier":{"type":"string"},"path":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"parent_type":{"type":"string"},"parent_id":{"type":"integer"}},"required":[],"additionalProperties":false}},"BuildCacheFileObject":{"type":"object","properties":{"build_cache_file":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}},"required":["build_cache_file"]},"BuildCacheFile":{"type":"object","title":"BuildCacheFile","properties":{"id":{"type":"integer"},"project_id":{"type":["null","integer"]},"identifier":{"type":"string"},"path":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"parent_type":{"type":"string"},"parent_id":{"type":"integer"}},"required":[],"additionalProperties":false},"BuildCommandList":{"type":"array","items":{"type":"object","title":"BuildCommand","properties":{"id":{"type":"integer"},"project_id":{"type":["null","integer"]},"description":{"type":"string"},"command":{"type":"string"},"halt_on_error":{"type":["null","boolean"]},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"identifier":{"type":"string"},"parent_type":{"type":"string"},"parent_id":{"type":"integer"},"template_name":{"type":["null","string"]},"enabled":{"type":"boolean"},"watch_files":{"type":["null","string"]}},"required":[],"additionalProperties":false}},"BuildCommandObject":{"type":"object","properties":{"build_command":{"type":"object","properties":{"description":{"type":"string"},"command":{"type":"string"},"template_name":{"type":"string"},"halt_on_error":{"type":"boolean"},"enabled":{"type":"boolean"},"watch_files":{"type":"string"}},"required":["command"]}},"required":["build_command"]},"BuildCommand":{"type":"object","title":"BuildCommand","properties":{"id":{"type":"integer"},"project_id":{"type":["null","integer"]},"description":{"type":"string"},"command":{"type":"string"},"halt_on_error":{"type":["null","boolean"]},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"identifier":{"type":"string"},"parent_type":{"type":"string"},"parent_id":{"type":"integer"},"template_name":{"type":["null","string"]},"enabled":{"type":"boolean"},"watch_files":{"type":["null","string"]}},"required":[],"additionalProperties":false},"BuildEnvironment":{"type":"object","title":"BuildEnvironment","properties":{"identifier":{"type":"string"},"packages":{"type":"object","title":"packages","properties":{"ruby":{"type":"string"},"php":{"type":"string"},"composer":{"type":"string"},"java":{"type":"string"},"dotnet":{"type":"string"},"go":{"type":"string"},"python2":{"type":"string"},"python3":{"type":"string"},"node":{"type":"string"},"phantomjs":{"type":"string"}},"required":[],"additionalProperties":false},"default":{"type":"boolean"},"servers":{"type":"array","items":{}}},"required":[],"additionalProperties":false},"BuildEnvironmentList":{"type":"array","items":{"type":"object","title":"BuildEnvironment","properties":{"identifier":{"type":"string"},"packages":{"type":"object","title":"packages","properties":{"ruby":{"type":"string"},"php":{"type":"string"},"composer":{"type":"string"},"java":{"type":"string"},"dotnet":{"type":"string"},"go":{"type":"string"},"python2":{"type":"string"},"python3":{"type":"string"},"node":{"type":"string"},"phantomjs":{"type":"string"}},"required":[],"additionalProperties":false},"default":{"type":"boolean"},"servers":{"type":"array","items":{}}},"required":[],"additionalProperties":false}},"BuildEnvironmentObject":{"type":"object","properties":{"build_environment":{"type":"object","properties":{"version":{"type":"string"}},"required":["version"]}},"required":["build_environment"]},"BuildKnownHostList":{"type":"array","items":{"type":"object","title":"BuildKnownHost","properties":{"identifier":{"type":"string"},"hostname":{"type":"string"},"fingerprint":{"type":"string"}},"required":[],"additionalProperties":false}},"BuildKnownHostObject":{"type":"object","properties":{"build_known_host":{"type":"object","properties":{"hostname":{"type":"string"},"public_key":{"type":"string"}},"required":["hostname","public_key"]}},"required":["build_known_host"]},"BuildKnownHost":{"type":"object","title":"BuildKnownHost","properties":{"identifier":{"type":"string"},"hostname":{"type":"string"},"fingerprint":{"type":"string"}},"required":[],"additionalProperties":false},"CommandList":{"type":"array","items":{"type":"object","title":"Command","properties":{"identifier":{"type":"string"},"cback":{"type":"string"},"position":{"type":"integer"},"description":{"type":"string"},"command":{"type":"string"},"halt_on_error":{"type":"boolean"},"servers":{"type":"array","items":{"type":"object","title":null,"properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":["null","string"]},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":["null","string"]},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"hostname":{"type":"string"},"username":{"type":"string"},"port":{"type":"integer"},"passive":{"type":"boolean"},"force_hidden_files":{"type":"boolean"}},"required":[],"additionalProperties":false}},"timing":{"type":"string"},"timeout":{"type":"integer"},"enabled":{"type":"boolean"}},"required":[],"additionalProperties":false}},"CommandObject":{"type":"object","properties":{"command":{"type":"object","properties":{"cback":{"type":"string"},"position":{"type":"integer"},"description":{"type":"string"},"command":{"type":"string"},"halt_on_error":{"type":"boolean"},"timing":{"type":"string"},"timeout":{"type":"integer"},"enabled":{"type":"boolean"}},"required":["command"]}},"required":["command"]},"Command":{"type":"object","title":"Command","properties":{"identifier":{"type":"string"},"cback":{"type":"string"},"position":{"type":"integer"},"description":{"type":"string"},"command":{"type":"string"},"halt_on_error":{"type":"boolean"},"servers":{"type":"array","items":{"type":"object","title":null,"properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":["null","string"]},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":["null","string"]},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"hostname":{"type":"string"},"username":{"type":"string"},"port":{"type":"integer"},"passive":{"type":"boolean"},"force_hidden_files":{"type":"boolean"}},"required":[],"additionalProperties":false}},"timing":{"type":"string"},"timeout":{"type":"integer"},"enabled":{"type":"boolean"}},"required":[],"additionalProperties":false},"CommandObject2":{"type":"object","properties":{"command":{"type":"object","properties":{"cback":{"type":"string"},"position":{"type":"integer"},"description":{"type":"string"},"command":{"type":"string"},"halt_on_error":{"type":"boolean"},"timing":{"type":"string"},"timeout":{"type":"integer"},"enabled":{"type":"boolean"}}}},"required":["command"]},"IdentifierNameStageList":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"stage":{"type":"string"},"check_type":{"type":"string"},"enabled":{"type":"boolean"},"position":{"type":"integer"},"command":{"type":"string"},"servers":{"type":"array","items":{"type":"string"}},"http_method":{"type":"string"},"http_url":{"type":"string"},"http_expected_status":{"type":"integer"},"http_body_match":{"type":"string"},"timeout_seconds":{"type":"integer"},"scanner":{"type":"string"},"scan_target_kind":{"type":"string"},"scan_target":{"type":"string"},"severity_threshold":{"type":"string"},"fail_on_unfixed_only":{"type":"boolean"},"sarif_output_path":{"type":"string"},"integration_identifier":{"type":"string"},"window_minutes":{"type":"integer"},"match_by_release":{"type":"boolean"}}}},"DeploymentCheckObject":{"type":"object","properties":{"deployment_check":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"stage":{"type":"string"},"check_type":{"type":"string"},"enabled":{"type":"boolean"},"timeout_seconds":{"type":"integer"},"command":{"type":"string"},"servers":{"type":"array","items":{"type":"string"}},"http_method":{"type":"string"},"http_url":{"type":"string"},"http_expected_status":{"type":"integer"},"http_body_match":{"type":"string"},"scanner":{"type":"string"},"scan_target_kind":{"type":"string"},"scan_target":{"type":"string"},"severity_threshold":{"type":"string"},"fail_on_unfixed_only":{"type":"boolean"},"sarif_output_path":{"type":"string"},"integration_identifier":{"type":"string"},"window_minutes":{"type":"integer"},"match_by_release":{"type":"boolean"}},"required":["name","stage","check_type"]}},"required":["deployment_check"]},"IdentifierNameStageObject":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"stage":{"type":"string"},"check_type":{"type":"string"},"enabled":{"type":"boolean"},"position":{"type":"integer"},"command":{"type":"string"},"servers":{"type":"array","items":{"type":"string"}},"http_method":{"type":"string"},"http_url":{"type":"string"},"http_expected_status":{"type":"integer"},"http_body_match":{"type":"string"},"timeout_seconds":{"type":"integer"},"scanner":{"type":"string"},"scan_target_kind":{"type":"string"},"scan_target":{"type":"string"},"severity_threshold":{"type":"string"},"fail_on_unfixed_only":{"type":"boolean"},"sarif_output_path":{"type":"string"},"integration_identifier":{"type":"string"},"window_minutes":{"type":"integer"},"match_by_release":{"type":"boolean"}}},"DeploymentCheckObject2":{"type":"object","properties":{"deployment_check":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"stage":{"type":"string"},"check_type":{"type":"string"},"enabled":{"type":"boolean"},"timeout_seconds":{"type":"integer"},"command":{"type":"string"},"servers":{"type":"array","items":{"type":"string"}},"http_method":{"type":"string"},"http_url":{"type":"string"},"http_expected_status":{"type":"integer"},"http_body_match":{"type":"string"},"scanner":{"type":"string"},"scan_target_kind":{"type":"string"},"scan_target":{"type":"string"},"severity_threshold":{"type":"string"},"fail_on_unfixed_only":{"type":"boolean"},"sarif_output_path":{"type":"string"},"integration_identifier":{"type":"string"},"window_minutes":{"type":"integer"},"match_by_release":{"type":"boolean"}}}},"required":["deployment_check"]},"DeploymentScheduleSaveServerBranchObject":{"type":"object","properties":{"deployment":{"type":"object","properties":{"start_revision":{"type":"string"},"end_revision":{"type":"string"},"copy_config_files":{"type":"boolean"},"notification_addresses":{"type":"string"},"branch":{"type":"string"},"parent_identifier":{"type":"string"},"server_identifier":{"type":"string"},"run_build_commands":{"type":"boolean"},"use_build_cache":{"type":"boolean"},"config_files_deployment":{"type":"boolean"},"mode":{"type":"string"},"use_latest":{"type":"boolean"},"skip_if_not_changes":{"type":"boolean"}}},"schedule":{"type":"object","properties":{"frequency":{"type":"string"},"future":{"type":"object","properties":{"day":{"type":"integer"},"month":{"type":"integer"},"year":{"type":"integer"},"hour":{"type":"integer"},"minute":{"type":"integer"}}},"daily":{"type":"object","properties":{"hour":{"type":"integer"},"minute":{"type":"integer"}}},"weekly":{"type":"object","properties":{"weekday":{"type":"integer"},"hour":{"type":"integer"},"minute":{"type":"integer"}}},"monthly":{"type":"object","properties":{"day":{"type":"integer"},"hour":{"type":"integer"},"minute":{"type":"integer"}}},"custom":{"type":"object","properties":{"schedule":{"type":"string"},"hour":{"type":"integer"},"minute":{"type":"integer"}}}}},"save_server_branch":{"type":"string"},"deployment_overview":{"type":"string"}},"required":["deployment"]},"StatusIdentifierObject":{"type":"object","properties":{"status":{"type":"string"},"identifier":{"type":"string"}}},"ScheduledDeploymentObject":{"type":"object","properties":{"scheduled_deployment":{"type":"hash"}}},"Deployment":{"type":"object","title":"Deployment","properties":{"identifier":{"type":"string"},"servers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":"string"},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":["null","boolean"]},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]}},"required":[],"additionalProperties":false}},"project":{"type":"object","title":"project","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"identifier":{"type":"string"},"public_key":{"type":"string"},"repository":{"type":"object","title":"repository","properties":{"scm_type":{"type":"string"},"url":{"type":"string"},"port":{"type":["null","integer"]},"username":{"type":["null","string"]},"branch":{"type":"string"},"cached":{"type":"boolean"},"hosting_service":{"type":"object","title":"hosting_service","properties":{"name":{"type":"string"},"url":{"type":"string"},"tree_url":{"type":"string"},"commits_url":{"type":"string"}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"repository_url":{"type":"string"},"zone":{"type":"string"},"last_deployed_at":{"type":"string"},"auto_deploy_url":{"type":"string"}},"required":[],"additionalProperties":false},"deployer":{"type":"string"},"deployer_avatar":{"type":"string"},"branch":{"type":"string"},"start_revision":{"type":"object","title":"start_revision","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"tags":{"type":"array","items":{}}},"required":[],"additionalProperties":false},"end_revision":{"type":"object","title":"end_revision","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"tags":{"type":"array","items":{}}},"required":[],"additionalProperties":false},"status":{"type":"string"},"timestamps":{"type":"object","title":"timestamps","properties":{"queued_at":{"type":"string"},"started_at":{"type":"string"},"completed_at":{"type":"string"},"duration":{"type":"number"},"runs_at":{"type":["null","string"]}},"required":[],"additionalProperties":false},"files":{"type":"object","title":"files","properties":{"69c0fd93-21d1-448d-a1cc-5ec9509f1ab1":{"type":"object","title":"69c0fd93-21d1-448d-a1cc-5ec9509f1ab1","properties":{"changed":{"type":"array","items":{}},"removed":{"type":"array","items":{}}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"configuration":{"type":"object","title":"configuration","properties":{"copy_config_files":{"type":"boolean"},"notification_addresses":{"type":["null","string"]},"skip_project_files":{"type":"boolean"}},"required":[],"additionalProperties":false},"legacy":{"type":"boolean"},"deferred":{"type":"boolean"},"config_files_deployment":{"type":"boolean"},"overview":{"type":["null","string"]},"metadata":{"type":"object","title":"metadata","properties":{"deployment_host":{"type":"string"},"github_deployment_id":{"type":"integer"},"github_deployment_url":{"type":"string"}},"required":[],"additionalProperties":false},"archived":{"type":"boolean"},"archived_at":{"type":["null","string"]},"log_summary":{"type":["null","string"]},"steps":{"type":"array","items":{"type":"object","title":null,"properties":{"step":{"type":"string"},"stage":{"type":"string"},"identifier":{"type":"string"},"server":{"type":["null","string"]},"total_items":{"type":["null","string"]},"completed_items":{"type":["null","string"]},"description":{"type":"string"},"status":{"type":"string"},"logs":{"type":"boolean"},"deployment_started_at":{"type":"string"},"updated_at":{"type":"string"}},"required":[],"additionalProperties":false}}},"required":[],"additionalProperties":false},"PaginationRecordsObject":{"type":"object","properties":{"pagination":{"type":"object","properties":{"current_page":{"type":"integer"},"total_pages":{"type":"integer"},"offset":{"type":"integer"},"total_records":{"type":"integer"}}},"records":{"type":"array","items":{"type":"object","title":"Deployment","properties":{"identifier":{"type":"string"},"servers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":"string"},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":["null","string"]},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":["null","boolean"]},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]}},"required":[],"additionalProperties":false}},"project":{"type":"object","title":"project","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"identifier":{"type":"string"},"public_key":{"type":"string"},"repository":{"type":"object","title":"repository","properties":{"scm_type":{"type":"string"},"url":{"type":"string"},"port":{"type":["null","integer"]},"username":{"type":["null","string"]},"branch":{"type":"string"},"cached":{"type":"boolean"},"hosting_service":{"type":"object","title":"hosting_service","properties":{"name":{"type":"string"},"url":{"type":"string"},"tree_url":{"type":"string"},"commits_url":{"type":"string"}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"repository_url":{"type":"string"},"zone":{"type":"string"},"last_deployed_at":{"type":"string"},"auto_deploy_url":{"type":"string"}},"required":[],"additionalProperties":false},"deployer":{"type":"string"},"deployer_avatar":{"type":"string"},"branch":{"type":"string"},"start_revision":{"type":"object","title":"start_revision","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"tags":{"type":"array","items":{}}},"required":[],"additionalProperties":false},"end_revision":{"type":"object","title":"end_revision","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"tags":{"type":"array","items":{}}},"required":[],"additionalProperties":false},"status":{"type":"string"},"timestamps":{"type":"object","title":"timestamps","properties":{"queued_at":{"type":"string"},"started_at":{"type":"string"},"completed_at":{"type":"string"},"duration":{"type":"number"},"runs_at":{"type":["null","string"]}},"required":[],"additionalProperties":false},"files":{"type":"object","title":"files","properties":{"69c0fd93-21d1-448d-a1cc-5ec9509f1ab1":{"type":"object","title":"69c0fd93-21d1-448d-a1cc-5ec9509f1ab1","properties":{"changed":{"type":"array","items":{}},"removed":{"type":"array","items":{}}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"configuration":{"type":"object","title":"configuration","properties":{"copy_config_files":{"type":"boolean"},"notification_addresses":{"type":["null","string"]},"skip_project_files":{"type":"boolean"}},"required":[],"additionalProperties":false},"legacy":{"type":"boolean"},"deferred":{"type":"boolean"},"config_files_deployment":{"type":"boolean"},"overview":{"type":["null","string"]},"metadata":{"type":"object","title":"metadata","properties":{"deployment_host":{"type":"string"},"github_deployment_id":{"type":"integer"},"github_deployment_url":{"type":"string"}},"required":[],"additionalProperties":false},"archived":{"type":"boolean"},"archived_at":{"type":["null","string"]},"log_summary":{"type":["null","string"]}},"required":[],"additionalProperties":false}}}},"IdStepMessageList":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"step":{"type":"string"},"message":{"type":"string"},"detail":{"type":"string"},"type":{"type":"string"}}}},"EnvironmentVariableObject3":{"type":"object","properties":{"environment_variable":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"locked":{"type":"boolean"},"build_pipeline":{"type":"boolean"}},"required":["name","value"]}},"required":["environment_variable"]},"EnvironmentVariableObject4":{"type":"object","properties":{"environment_variable":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"locked":{"type":"boolean"},"build_pipeline":{"type":"boolean"}}}},"required":["environment_variable"]},"StringValue":{"type":"string"},"ExcludedFileList":{"type":"array","items":{"type":"object","title":"ExcludedFile","properties":{"identifier":{"type":"string"},"path":{"type":"string"},"servers":{"type":"array","items":{}}},"required":[],"additionalProperties":false}},"ExcludedFileObject":{"type":"object","properties":{"excluded_file":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}},"required":["excluded_file"]},"ExcludedFile":{"type":"object","title":"ExcludedFile","properties":{"identifier":{"type":"string"},"path":{"type":"string"},"servers":{"type":"array","items":{}}},"required":[],"additionalProperties":false},"IntegrationList":{"type":"array","items":{"type":"object","title":"Integration","properties":{"identifier":{"type":"string"},"hook_type":{"type":"string"},"name":{"type":"string"},"send_on_start":{"type":"boolean"},"send_on_completion":{"type":"boolean"},"send_on_failure":{"type":"boolean"},"send_on_check_failed":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":[],"additionalProperties":false}},"IntegrationObject":{"type":"object","properties":{"integration":{"type":"object","properties":{"hook_type":{"type":"string"},"send_on_completion":{"type":"boolean"},"send_on_failure":{"type":"boolean"},"send_on_check_failed":{"type":"boolean"},"send_on_start":{"type":"boolean"},"name":{"type":"string"},"properties":{"type":"hash"}},"required":["hook_type"]}},"required":["integration"]},"Integration":{"type":"object","title":"Integration","properties":{"identifier":{"type":"string"},"hook_type":{"type":"string"},"name":{"type":"string"},"send_on_start":{"type":"boolean"},"send_on_completion":{"type":"boolean"},"send_on_failure":{"type":"boolean"},"send_on_check_failed":{"type":"boolean"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":[],"additionalProperties":false},"IntegrationObject2":{"type":"object","properties":{"integration":{"type":"object","properties":{"send_on_completion":{"type":"boolean"},"send_on_failure":{"type":"boolean"},"send_on_check_failed":{"type":"boolean"},"send_on_start":{"type":"boolean"},"name":{"type":"string"},"properties":{"type":"hash"}}}},"required":["integration"]},"NodeRubyPhpObject2":{"type":"object","properties":{"node":{"type":"string"},"ruby":{"type":"string"},"php":{"type":"string"}}},"Repository":{"type":"object","title":"Repository","properties":{"scm_type":{"type":"string"},"url":{"type":"string"},"port":{"type":["null","integer"]},"username":{"type":["null","string"]},"branch":{"type":"string"},"cached":{"type":"boolean"},"hosting_service":{"type":"object","title":"hosting_service","properties":{"name":{"type":"string"},"url":{"type":"string"},"tree_url":{"type":"string"},"commits_url":{"type":"string"}},"required":[],"additionalProperties":false}},"required":[],"additionalProperties":false},"RepositoryObject":{"type":"object","properties":{"repository":{"type":"object","properties":{"branch":{"type":"string"},"root_path":{"type":"string"},"hosting_service_type":{"type":"string"}}}},"required":["repository"]},"RepositoryObject2":{"type":"object","properties":{"repository":{"type":"object","properties":{"url":{"type":"string"},"scm_type":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"},"branch":{"type":"string"},"port":{"type":"integer"},"root_path":{"type":"string"},"hosting_service_type":{"type":"string"},"manual_config":{"type":"boolean"}}}},"required":["repository"]},"35f912b4c5029a0c4c2f7fd81f392ebf":{"type":"hash"},"RefAuthorEmailObject":{"type":"object","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"avatar_url":{"type":"string"}},"required":["ref","author","email","timestamp"]},"MessageObject":{"type":"object","properties":{"message":{"type":"string"}}},"RefObject":{"type":"object","properties":{"ref":{"type":"string"}}},"CommitsTagsReleasesObject":{"type":"object","properties":{"commits":{"type":"array","items":{"type":"object","properties":{"ref":{"type":"string"},"author":{"type":"string"},"email":{"type":"string"},"timestamp":{"type":"string"},"message":{"type":"string"},"short_message":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"avatar_url":{"type":"string"}},"required":["ref","author","email","timestamp"]}},"tags":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"ref":{"type":"string"},"message":{"type":"string"}},"required":["name","ref","message"]}},"releases":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"ref":{"type":"string"}}}}}},"ScheduledDeploymentList":{"type":"array","items":{"type":"object","title":"ScheduledDeployment"}},"ScheduledDeploymentObject2":{"type":"object","properties":{"scheduled_deployment":{"type":"object","properties":{"server_identifier":{"type":"string"},"frequency":{"type":"string"},"at":{"type":"string"},"weekday":{"type":"string"},"day":{"type":"integer"},"month":{"type":"integer"},"year":{"type":"integer"},"schedule":{"type":"string"},"copy_config_files":{"type":"boolean"},"run_build_commands":{"type":"boolean"},"use_build_cache":{"type":"boolean"},"skip_if_not_changes":{"type":"boolean"},"start_revision":{"type":"string"},"end_revision":{"type":"string"},"use_latest":{"type":"boolean"},"deploy_entire_repository":{"type":"boolean"}},"required":["server_identifier","frequency"]}},"required":["scheduled_deployment"]},"ScheduledDeployment":{"type":"object","title":"ScheduledDeployment"},"ScheduledDeploymentObject3":{"type":"object","properties":{"scheduled_deployment":{"type":"object","properties":{"frequency":{"type":"string"},"at":{"type":"string"},"weekday":{"type":"string"},"day":{"type":"integer"},"month":{"type":"integer"},"year":{"type":"integer"},"schedule":{"type":"string"},"copy_config_files":{"type":"boolean"},"run_build_commands":{"type":"boolean"},"use_build_cache":{"type":"boolean"},"skip_if_not_changes":{"type":"boolean"}}}},"required":["scheduled_deployment"]},"ServerGroupList":{"type":"array","items":{"type":"object","title":"ServerGroup","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"servers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":"string"},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":"string"},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":"boolean"},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"hostname":{"type":"string"},"username":{"type":"string"},"port":{"type":"integer"},"use_ssh_keys":{"type":"boolean"},"host_key":{"type":"string"},"unlink_before_upload":{"type":"boolean"}},"required":[],"additionalProperties":false}},"preferred_branch":{"type":"string"},"last_revision":{"type":"string"},"environment":{"type":["null","string"]}},"required":[],"additionalProperties":false}},"ServerGroupObject":{"type":"object","properties":{"server_group":{"type":"object","properties":{"name":{"type":"string"},"branch":{"type":"string"},"auto_deploy":{"type":"boolean"},"email_notify_on":{"type":"string"},"notification_email":{"type":"string"},"transfer_order":{"type":"string"},"environment":{"type":"string"}}}},"required":["server_group"]},"ServerGroup":{"type":"object","title":"ServerGroup","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"servers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"last_revision":{"type":"string"},"preferred_branch":{"type":"string"},"branch":{"type":"string"},"notify_email":{"type":"string"},"server_group_identifier":{"type":"string"},"auto_deploy":{"type":"boolean"},"environment":{"type":"string"},"enabled":{"type":"boolean"},"agent":{"type":["null","string"]},"atomic":{"type":"boolean"},"atomic_strategy":{"type":"string"},"atomic_retention":{"type":"integer"},"use_compression":{"type":"boolean"},"use_accelerated_transfer":{"type":"boolean"},"use_parallel_upload":{"type":"boolean"},"root_path":{"type":"string"},"position":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"connection_checked_at":{"type":["null","string"]},"connection_error_message":{"type":["null","string"]},"hostname":{"type":"string"},"username":{"type":"string"},"port":{"type":"integer"},"use_ssh_keys":{"type":"boolean"},"host_key":{"type":"string"},"unlink_before_upload":{"type":"boolean"}},"required":[],"additionalProperties":false}},"preferred_branch":{"type":"string"},"last_revision":{"type":"string"},"environment":{"type":["null","string"]}},"required":[],"additionalProperties":false},"ServerRegionSizeObject":{"type":"object","properties":{"server":{"type":"object","properties":{"name":{"type":"string"},"protocol_type":{"type":"string"},"server_path":{"type":"string"},"email_notify_on":{"type":"string"},"root_path":{"type":"string"},"auto_deploy":{"type":"string"},"notification_email":{"type":"string"},"branch":{"type":"string"},"environment":{"type":"string"},"server_group_identifier":{"type":"string"},"agent_id":{"type":"string"},"enabled":{"type":"boolean"},"global_key_pair_id":{"type":"string"}}},"region":{"type":"string"},"size":{"type":"string"},"os_image":{"type":"string"},"hosted_website_attributes":{"type":"object","properties":{"subdomain":{"type":"string"},"spa_mode":{"type":"boolean"},"subdirectory":{"type":"string"}}}},"required":["server"]},"IdIdentifierNameObject":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"name":{"type":"string"},"protocol_type":{"type":"string"},"enabled":{"type":"boolean"},"static_hosting":{"type":"object","properties":{"hosted_website_identifier":{"type":"string"},"subdomain":{"type":"string"},"url":{"type":"string"},"spa_mode":{"type":"boolean"},"status":{"type":"string"},"active_deployment_uuid":{"type":"string"}}},"managed_vps":{"type":"object","properties":{"hosted_resource_identifier":{"type":"string"},"status":{"type":"string"},"ip_address":{"type":"string"},"region":{"type":"string"},"size":{"type":"string"}}}}},"ServerObject2":{"type":"object","properties":{"server":{"type":"object","properties":{"name":{"type":"string"},"server_path":{"type":"string"},"email_notify_on":{"type":"string"},"root_path":{"type":"string"},"auto_deploy":{"type":"string"},"notification_email":{"type":"string"},"branch":{"type":"string"},"environment":{"type":"string"},"server_group_identifier":{"type":"string"},"agent_id":{"type":"string"},"enabled":{"type":"boolean"},"global_key_pair_id":{"type":"string"}}}},"required":["server"]},"CpuMemoryDiskObject":{"type":"object","properties":{"cpu":{"type":"hash"},"memory":{"type":"hash"},"disk":{"type":"hash"},"uptime":{"type":"string"},"profile":{"type":"hash"}}},"IdentifierStatusCreatedAtObject":{"type":"object","properties":{"identifier":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"}},"required":["identifier","status","created_at"]},"IdentifierStatusCreatedAtObject2":{"type":"object","properties":{"identifier":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"completed_at":{"type":"string"},"results":{"type":"object","properties":{"repository":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"}},"required":["status"]},"servers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"identifier":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}},"required":["name","identifier","status"]}}}}},"required":["identifier","status","created_at"]},"EmailAddressObject":{"type":"object","properties":{"email_address":{"type":"string"}},"required":["email_address"]},"StatusMessageObject":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"}}},"ApiKeyObject":{"type":"object","properties":{"api_key":{"type":"object","properties":{"description":{"type":"string"}},"required":["description"]}},"required":["api_key"]},"ApiKeyIdentifierDescriptionObject":{"type":"object","properties":{"api_key":{"type":"string"},"identifier":{"type":"string"},"description":{"type":"string"},"user_id":{"type":"integer"},"device":{"type":"string"},"html":{"type":"string"}}},"IdentifierTitlePublicKeyList":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"public_key":{"type":"string"},"key_type":{"type":"string"},"fingerprint":{"type":"string"},"account":{"type":"string"}}}},"KeyPairObject":{"type":"object","properties":{"key_pair":{"type":"object","properties":{"title":{"type":"string"},"key_type":{"type":"string"},"use_custom_key":{"type":"boolean"},"custom_private_key":{"type":"string"}},"required":["title"]}},"required":["key_pair"]},"IdentifierTitlePublicKeyObject":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"public_key":{"type":"string"},"key_type":{"type":"string"},"fingerprint":{"type":"string"},"account":{"type":"string"}}},"IdentifierNameIsAdminList":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"is_admin":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"},"members":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"}}}},"project_assignments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"identifier":{"type":"string"},"can_deploy_all":{"type":"boolean"},"can_update_config":{"type":"boolean"},"can_manage_config_files":{"type":"boolean"}}}}}}},"TeamObject":{"type":"object","properties":{"team":{"type":"object","properties":{"name":{"type":"string"},"is_admin":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"},"user_ids":{"type":"array","items":{"type":"integer"}}},"required":["name"]}},"required":["team"]},"IdentifierNameIsAdminObject":{"type":"object","properties":{"identifier":{"type":"string"},"name":{"type":"string"},"is_admin":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"},"members":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"}}}},"project_assignments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"identifier":{"type":"string"},"can_deploy_all":{"type":"boolean"},"can_update_config":{"type":"boolean"},"can_manage_config_files":{"type":"boolean"}}}}}},"TeamObject2":{"type":"object","properties":{"team":{"type":"object","properties":{"name":{"type":"string"},"is_admin":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"},"user_ids":{"type":"array","items":{"type":"integer"}}}}},"required":["team"]},"TemplateList":{"type":"array","items":{"type":"object","title":"Template","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"description":{"type":"string"}},"required":[],"additionalProperties":false}},"TemplateObject":{"type":"object","properties":{"template":{"type":"object","properties":{"name":{"type":"string"},"notification_email":{"type":"string"},"email_notify":{"type":"boolean"},"project_id":{"type":"string"},"description":{"type":"string"}}}},"required":["template"]},"Template":{"type":"object","title":"Template","properties":{"name":{"type":"string"},"permalink":{"type":"string"},"description":{"type":"string"}},"required":[],"additionalProperties":false},"IdPermalinkNameList":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"permalink":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"framework_type":{"type":"string"},"version":{"type":"string"},"public":{"type":"boolean"}}}},"TemplateObject2":{"type":"object","properties":{"template":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}}},"required":["template"]},"IdPermalinkNameObject":{"type":"object","properties":{"id":{"type":"integer"},"permalink":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"framework_type":{"type":"string"},"version":{"type":"string"},"features":{"type":"array","items":{"type":"string"}},"build_commands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}}}},"ssh_commands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}}}},"excluded_files":{"type":"array","items":{"type":"string"}},"config_files":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"body":{"type":"string"}}}}}},"UserList":{"type":"array","items":{"type":"object","title":"User","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":["null","string"]},"id":{"type":"integer"},"identifier":{"type":"string"},"account_administrator":{"type":"boolean"},"activated":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"}},"required":[],"additionalProperties":false}},"UserObject2":{"type":"object","properties":{"user":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":"string"},"account_administrator":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"}},"required":["first_name","email_address"]}},"required":["user"]},"IdIdentifierFirstNameObject":{"type":"object","properties":{"id":{"type":"integer"},"identifier":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":"string"},"account_administrator":{"type":"boolean"},"activated":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"}}},"User":{"type":"object","title":"User","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":["null","string"]},"id":{"type":"integer"},"identifier":{"type":"string"},"account_administrator":{"type":"boolean"},"activated":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"}},"required":[],"additionalProperties":false},"UserObject3":{"type":"object","properties":{"user":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email_address":{"type":"string"},"time_zone":{"type":"string"},"account_administrator":{"type":"boolean"},"can_manage_users":{"type":"boolean"},"can_manage_billing":{"type":"boolean"},"can_manage_agents":{"type":"boolean"},"can_create_projects":{"type":"boolean"},"all_projects_allowed":{"type":"boolean"}}}},"required":["user"]},"ErrorErrorCodeObject":{"type":"object","properties":{"error":{"type":"string"},"error_code":{"type":"string"}}},"IdentifierDescriptionList":{"type":"array","items":{"type":"object","properties":{"identifier":{"type":"string"},"description":{"type":"string"}}}}},"parameters":{"65b7c1ef9a672c9e027100beadc77403":{"name":"id","in":"path","description":"The identifier of the agent to update","required":true,"schema":{"type":"string"},"style":"simple"},"cf3c2621f7f26a86e4f7b3aa67057676":{"name":"id","in":"path","description":"The identifier of the agent to delete","required":true,"schema":{"type":"string"},"style":"simple"},"298320f1616036082cc4bd56d72a6c09":{"name":"id","in":"path","description":"The identifier of the agent to revoke","required":true,"schema":{"type":"string"},"style":"simple"},"45521a405675361f0a85bc01c3ad4550":{"name":"protocol","in":"body","description":"The protocol being enrolled for: static_hosting or managed_vps","required":false,"schema":{"type":"string"}},"2e5bd103a88877e6bf0ce64ab017229d":{"name":"id","in":"path","description":"The identifier (UUID) of the global config file","required":true,"schema":{"type":"string"},"style":"simple"},"1f04e1e5742863db5a0e350bfae5f36e":{"name":"id","in":"path","description":"The identifier of the environment variable","required":true,"schema":{"type":"string"},"style":"simple"},"63af325434e89dbb89f6fc1772268b53":{"name":"id","in":"path","description":"The identifier of the global server","required":true,"schema":{"type":"string"},"style":"simple"},"4f5fca8deca120935a640ae64413387f":{"name":"project_id","in":"query","description":"The identifier of the target project","required":true,"schema":{"type":"string"},"style":"form"},"0bb858b19ca0cc95038712ae24f7ae1f":{"name":"id","in":"path","description":"The identifier of the hosted resource or hosted website","required":true,"schema":{"type":"string"},"style":"simple"},"43beaabd70d1089bd81ac12e4c47beb8":{"name":"id","in":"path","description":"The identifier of the hosted resource to retry","required":true,"schema":{"type":"string"},"style":"simple"},"3fc4d3fa60dd528d714e5558d68c3191":{"name":"id","in":"path","description":"The identifier of the hosted resource to sync","required":true,"schema":{"type":"string"},"style":"simple"},"cb767feb521d5a5082ae6ce105173118":{"name":"id","in":"path","description":"The identifier or permalink of the project","required":true,"schema":{"type":"string"},"style":"simple"},"c6618cdd1a12a98566027ef478919569":{"name":"project_id","in":"path","description":"The identifier or permalink of the project","required":true,"schema":{"type":"string"},"style":"simple"},"5ead782fa8954405b083e973f24d5345":{"name":"period","in":"query","description":"The period to aggregate data for (allowed: 30, 90, 180, 365 days)","required":false,"schema":{"type":"string"},"style":"form"},"30a628c33caf3d3300b538f3ed0dd8c7":{"name":"id","in":"path","description":"Id of existing project.","required":true,"schema":{"type":"string"},"style":"simple"},"b0bcba4423e03a452590ad4c283b76ca":{"name":"id","in":"path","description":"The identifier of the build cache file","required":true,"schema":{"type":"string"},"style":"simple"},"3e5b2bc01916b68ec592a9f9744c92b0":{"name":"id","in":"path","description":"The identifier of the build command","required":true,"schema":{"type":"string"},"style":"simple"},"b6dfad1ce9e91d741ac3d3eb49a4934e":{"name":"id","in":"path","description":"The identifier of the build configuration","required":true,"schema":{"type":"string"},"style":"simple"},"8a896a8b20a129d3b73bb95bfc57fb9d":{"name":"id","in":"path","description":"The identifier of the build language/package","required":true,"schema":{"type":"string"},"style":"simple"},"9aa1361134de003c0ef02160d5412005":{"name":"override_build_configuration_id","in":"query","description":"The identifier of the build environment (optional, uses default build environment if not provided)","required":false,"schema":{"type":"string"},"style":"form"},"59042d9f5ca35722edf6abcbe93bd329":{"name":"id","in":"path","description":"The identifier of the build known host","required":true,"schema":{"type":"string"},"style":"simple"},"55c7272632d1eb6c3cc834b5cd16bc4a":{"name":"id","in":"path","description":"The identifier of the command","required":true,"schema":{"type":"string"},"style":"simple"},"e1eaad9f0a6b762bf061b7135cd10962":{"name":"id","in":"path","description":"The identifier of the config file","required":true,"schema":{"type":"string"},"style":"simple"},"2730095186acddcc3bcb1193e53923e9":{"name":"id","in":"path","description":"The identifier of the deployment check","required":true,"schema":{"type":"string"},"style":"simple"},"cb1515e114aa28a733a232e8d1100046":{"name":"page","in":"query","description":"Page number for pagination (default: 1)","required":false,"schema":{"type":"integer"},"style":"form"},"c0ac2df9fc28efe9bcbb9c668483ad97":{"name":"per_page","in":"query","description":"Number of records per page (default: 10)","required":false,"schema":{"type":"integer"},"style":"form"},"bca97defd28dac01e1b3b02e02310a3b":{"name":"to","in":"query","description":"Filter deployments by parent identifier","required":false,"schema":{"type":"string"},"style":"form"},"0b6ead29a598e0012f980232e26de5ed":{"name":"currently_running","in":"query","description":"Filter to currently running deployments (set to '1')","required":false,"schema":{"type":"string"},"style":"form"},"c307dd4403ec6fabb12df075a19b849c":{"name":"project_id","in":"path","description":"The permalink of the project","required":true,"schema":{"type":"string"},"style":"simple"},"1edb42db3fb9d50012c36c500d141f5a":{"name":"deployment_id","in":"path","description":"The identifier of the deployment","required":true,"schema":{"type":"string"},"style":"simple"},"3747d4604d2cb5485f944c4641d33c84":{"name":"step_id","in":"path","description":"The identifier of the deployment step","required":true,"schema":{"type":"string"},"style":"simple"},"4fc417275517e159e1165032a98adb50":{"name":"before","in":"query","description":"Return log entries with an ID less than this value (for pagination)","required":false,"schema":{"type":"integer"},"style":"form"},"c5ed88df88fbc32a355fc3b0fc86f340":{"name":"download","in":"query","description":"Set to true to download logs as a plain text file","required":false,"schema":{"type":"boolean"},"style":"form"},"c1cf4fde64acdf07b8c3c82cf08cc20e":{"name":"id","in":"path","description":"The identifier of the deployment","required":true,"schema":{"type":"string"},"style":"simple"},"c79344c276c2fa0e153492f462d1ffd2":{"name":"id","in":"path","description":"The identifier of the deployment to retry","required":true,"schema":{"type":"string"},"style":"simple"},"21bc279099387a81c2d9173928f7494b":{"name":"id","in":"path","description":"The identifier of the deployment to rollback","required":true,"schema":{"type":"string"},"style":"simple"},"d7f969dda7b74656f28268a86c9bcc17":{"name":"mode","in":"formData","description":"Set to 'preview' to preview the rollback, or 'queue' to execute immediately (default: 'queue')","required":false,"schema":{"type":"string"}},"4a747e2207b0034846f238e886a65635":{"name":"copy_config_files","in":"formData","description":"Copy config files during rollback (default: true)","required":false,"schema":{"type":"boolean"}},"9f3ef084823dc184e5d40ee514501451":{"name":"run_build_commands","in":"formData","description":"Run build commands during rollback (default: true)","required":false,"schema":{"type":"boolean"}},"fa3c64d02d8df4c1914b1c09c37bcba2":{"name":"id","in":"path","description":"The identifier of the excluded file","required":true,"schema":{"type":"string"},"style":"simple"},"3bad048e25ea875765021c8867c13e11":{"name":"id","in":"path","description":"The identifier of the integration","required":true,"schema":{"type":"string"},"style":"simple"},"1563d47f1781e48601105aabbfed33e3":{"name":"commit","in":"query","description":"The commit reference to get information for","required":true,"schema":{"type":"string"},"style":"form"},"25be58a5af8084855f060c1b13c29728":{"name":"branch","in":"query","description":"The branch name to get the latest revision for","required":true,"schema":{"type":"string"},"style":"form"},"bbfda3f1afbf916883c7c5e5d714acd2":{"name":"branch","in":"query","description":"The branch name to get recent commits for","required":true,"schema":{"type":"string"},"style":"form"},"08820948331d205348d4944917c5a376":{"name":"update","in":"query","description":"Set to '1' to update the repository before getting commits","required":false,"schema":{"type":"string"},"style":"form"},"6f7ef02510ee8ccdca1dc05608414123":{"name":"id","in":"path","description":"The identifier of the scheduled deployment","required":true,"schema":{"type":"string"},"style":"simple"},"2fde178d2b6116ce573c7925ae4ea619":{"name":"id","in":"path","description":"The identifier of the server group","required":true,"schema":{"type":"string"},"style":"simple"},"fb7f2a6f88abe988d789b08b4345de52":{"name":"id","in":"path","description":"The identifier of the server","required":true,"schema":{"type":"string"},"style":"simple"},"d68c0ce894c748b089f5022ebaae9065":{"name":"server_id","in":"path","description":"Id of existing server.","required":true,"schema":{"type":"string"},"style":"simple"},"a46a7ac00a2b2976ea75e8a8e940e6bf":{"name":"id","in":"path","description":"The identifier of the test access run","required":true,"schema":{"type":"string"},"style":"simple"},"0bc48a9d3c6be1a8380f9d94b6cabe41":{"name":"identifier","in":"path","description":"The identifier of the API key to revoke","required":true,"schema":{"type":"string"},"style":"simple"},"fa5ac64c5593bf8e54e0e7054dbd6394":{"name":"id","in":"path","description":"Id of existing api_key.","required":true,"schema":{"type":"string"},"style":"simple"},"d6832191eea6b63663b292cdce3e2c78":{"name":"id","in":"path","description":"The identifier of the SSH key","required":true,"schema":{"type":"string"},"style":"simple"},"bf5920dd9c5c1747d32f8644f5b0c8d3":{"name":"id","in":"path","description":"The team identifier","required":true,"schema":{"type":"string"},"style":"simple"},"4f355c05cd4cbc73947cb6d92e7ea8ce":{"name":"framework_type","in":"query","description":"Filter by framework type (web_frameworks, cms, ecommerce, static_sites, all)","required":false,"schema":{"type":"string"},"style":"form"},"4b76a9087fe3c7c6bb7381ac0c71983e":{"name":"id","in":"path","description":"The identifier or permalink of the template","required":true,"schema":{"type":"string"},"style":"simple"},"5a3a4876f81e660436e06f2ffd0fbbf2":{"name":"id","in":"path","description":"The identifier or permalink of the public template","required":true,"schema":{"type":"string"},"style":"simple"},"0925fefda80c37627bf8ccfc61b1c1ae":{"name":"id","in":"path","description":"The user identifier (UUID)","required":true,"schema":{"type":"string"},"style":"simple"},"1a67e0e0bcfbd47ccea892c1a083c6f5":{"name":"id","in":"path","description":"The project permalink","required":true,"schema":{"type":"string"},"style":"simple"},"bf75c85e3578342f8e96ddb58e7af384":{"name":"identifier","in":"path","description":"Optional server or server group identifier to check status for a specific component","required":false,"schema":{"type":"string"},"style":"simple"}},"securitySchemes":{"basic":{"type":"http","scheme":"basic","description":"Basic auth that takes a base64'd combination of `user:password`."}}},"security":[{"basic":[]}],"tags":[{"name":"Signup","description":"Agent-driven account creation. This is the only unauthenticated API endpoint.\n\n### Flow\n1. `POST /api/v1/signup` with email, password, and optionally `client` (your agent/tool name, e.g. \"claude-code\", \"cursor\", \"codex\")\n2. Receive API key, SSH public key, OAuth URLs, and MCP configuration\n3. Use the returned credentials to authenticate all subsequent API calls\n\n### Agent Attribution\nPlease include the `client` field with your agent or tool name so we can track which integrations are being used.\nFor example: `\"client\": \"claude-code\"` or `\"client\": \"cursor\"`.\n\n### Rate Limiting\nLimited to 3 signups per IP per hour.\n\n### Email Verification\nAccounts are created immediately but deployment is gated until the admin email is verified.\nCheck the `email_verified` field in the response.\n"},{"name":"Account","description":"View and manage account settings, billing status, and security policies."},{"name":"Users","description":"Manage account users: invite, update roles/permissions, and remove members."},{"name":"Profile","description":"View and update the authenticated user's own profile."},{"name":"Security","description":"Manage API keys and login sessions for the authenticated user."},{"name":"Projects","description":"Create, configure, and manage deployment projects."},{"name":"Templates","description":"Reusable project configuration templates that can be copied to new projects."},{"name":"Repositories","description":"Repository connection settings, branch listing, and commit information."},{"name":"Global Servers","description":"Account-wide servers shared across all projects."},{"name":"Servers","description":"### Base Parameters\n\n- `name` - Friendly name for your server\n- `protocol_type` - Connection protocol, either ftp, ftps, rackspace, s3 or ssh\n- `server_path` - Where on the server should your files be placed (for example, public_html/ or /absolute/path/here)\n- `email_notify_on` - When do you want to receive email notifications, either never, failure or always\n- `root_path` - The subdirectory in your repository that you wish to deploy. Leave blank to use the default specified in the project.\n- `auto_deploy` - Should auto deployments be enabled, either true or false\n- `notification_email` - Custom notification e-mail address, leave blank to use the user who started the deployment's address\n- `branch` - Branch to deploy from, leave blank to use the project default\n- `environment` - Production, Testing, Development etc. can be substituted into SSH commands.\n- `server_group_identifier` - The server group that this server belongs to\n- `agent_id` - The ID of the network agent that you wish to connect through (omit if you're connecting directly)\n\nIn addition to the above parameters, the following parameters are available depending on the protocol selected:\n\n### Additional parameters\n\n#### FTP\n- `hostname` (required)\n- `username` (required)\n- `password` (required)\n- `port` - default 21\n- `passive` - true or false\n- `force_hidden_files` - true or false\n\n#### FTPS\nIn addition the the FTP parameters the following parameters are available for FTPS servers.\n\n- `implicit` - true or false\n- `ignore_certificate_errors` - true or false\n\n#### SSH/SFTP\n- `hostname` (required)\n- `username` (required)\n- `password` (required)\n- `port` - default 22\n- `use_ssh_keys` - true or false\n- `atomic` - true or false (for setting up zero-downtime deployments, your server will need to support POSIX commands to take advantage of this feature)\n- `atomic_strategy` - copy_release or copy_cache - this must be set if atomic has been set to true.\n\n#### Amazon S3\n- `bucket_name` - (required)\n- `access_key_id` - (required)\n- `secret_access_key` - (required)\n\n#### Rackspace Cloud\n- `username` - (required)\n- `api_key` - (required)\n- `region` - (required)\n- `container_name` - (required)\n- `droplet_id` - (required)\n- `droplet_name` - (required)\n\n#### DigitalOcean\n- `personal_access_token` - (required)\n- `droplet_id` - (required)\n- `droplet_name` - (required)\n\n#### Shell Server\n- `command` - (required)\n- `timeout` - command timeout, in seconds (can be 5, 10, 30, 60, 90, 180 minutes)\n"},{"name":"Server Groups","description":"Organize servers into logical groups for coordinated deployments."},{"name":"Network Agents","description":"Agents that allow deployments through firewalls via a secure TLS proxy."},{"name":"Ssh Keys","description":"Global SSH key pairs for server authentication."},{"name":"Config Files","description":"Per-server configuration files that are uploaded during deployment."},{"name":"Environment Variables","description":"Environment variables are custom key-value pairs that can be used in Config files, SSH commands, and the Build Pipeline.\n\n### Variable Naming\n- Must start with a letter\n- Can contain only letters (uppercase or lowercase), numbers, and underscores\n- Examples: `DATABASE_URL`, `api_key`, `MyVariable_123`\n\n### Features\n- **Encryption**: All values are encrypted at rest\n- **Locking**: Variables can be locked to prevent changes (irreversible)\n- **Build Pipeline**: Control availability in build commands\n- **Server Targeting**: Assign to all servers, server groups, or specific servers\n\n### Precedence\nWhen the same variable name is defined multiple times:\n1. Server-specific (highest)\n2. Server group\n3. All servers (lowest)\n"},{"name":"Global Environment Variables","description":"Account-wide environment variables shared across all projects."},{"name":"Excluded Files","description":"File patterns to exclude from deployments (e.g. `.git`, `node_modules`)."},{"name":"Ssh Commands","description":"Commands executed on the server via SSH before or after deployment."},{"name":"Build Commands","description":"Commands executed during the build step (e.g. `npm install`, `bundle install`)."},{"name":"Build Configurations","description":"Build environment settings including language versions and caching."},{"name":"Build Cache Files","description":"Directories cached between builds to speed up the build pipeline."},{"name":"Build Known Hosts","description":"SSH known hosts entries required during builds (e.g. for private dependencies)."},{"name":"Language Versions","description":"### Supported languages and versions\n\n#### Ruby (`ruby`)\n\n`3.4.1`, `3.3.6`, `3.2.6`, `3.1.6`, `3.0.7`, `2.7.8`, `2.6.10`, `2.5.9`, `2.4.10`\n\n#### PHP (`php`)\n\n`8.4.2`, `8.3.15`, `8.2.27`, `8.1.27`, `8.0.30`, `7.4.26`, `7.3.33`, `7.2.34`, `7.1.33`, `7.0.33`\n\n#### Composer (`composer`)\n\n`2.6.6`, `1.10.26`\n\n#### Java (`java`)\n\n`8u191`, `10.0.1`, `11.0.26+4`\n\n#### .NET (`dotnet`)\n\n`3.1.32`, `5.0.17`, `6.0.36`, `7.0.18`, `8.0.11`, `9.0.0`\n\n#### Go (`go`)\n\n`1.23.4`, `1.22.3`, `1.21.10`, `1.20.14`, `1.19.13`, `1.18.10`, `1.17.13`, `1.16.13`, `1.15.15`, `1.14.15`, `1.13.15`, `1.12.17`, `1.11.13`, `1.10.8`\n\n#### Python (`python2`)\n\n`2.7.18`\n\n#### Python (`python3`)\n\n`3.13.1`, `3.12.8`, `3.11.9`, `3.10.14`, `3.9.16`, `3.8.16`, `3.7.16`, `3.6.15`, `3.5.10`\n\n#### Node (`node`)\n\n`22.12.0`, `21.7.3`, `20.13.1`, `19.9.0`, `18.20.2`, `17.9.1`, `16.20.0`, `15.14.0`, `14.21.3`, `12.22.9`, `10.24.1`, `9.11.2`, `8.17.0`\n\n#### PhantomJS (`phantomjs`)\n\n`2.1.1`\n"},{"name":"Deployments","description":"Create, monitor, abort, and rollback deployments."},{"name":"Scheduled Deployments","description":"Deployments scheduled to run at a future date and time."},{"name":"Automatic Deployments","description":"Webhook-triggered deployments from repository push events."},{"name":"Integrations","description":"Third-party notification integrations (Slack, email, webhooks, etc.)."}]} \ No newline at end of file diff --git a/internal/commands/watch.go b/internal/commands/watch.go index 4072121..fd0f7b7 100644 --- a/internal/commands/watch.go +++ b/internal/commands/watch.go @@ -85,7 +85,15 @@ func watchDeploymentPlain(ctx context.Context, client *sdk.Client, env *output.E if len(logs) > 15 { start = len(logs) - 15 } + // De-duplicate identical lines: a static deploy can record + // the same advisory/log line more than once, which would + // otherwise print twice here (the web UI shows each once). + seen := make(map[string]bool) for _, l := range logs[start:] { + if seen[l.Message] { + continue + } + seen[l.Message] = true env.Status(" %s", l.Message) } } diff --git a/internal/config/config.go b/internal/config/config.go index 00553be..f5c5a1d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,12 +25,18 @@ type Config struct { OutputFmt string `mapstructure:"format" json:"format,omitempty"` Host string `mapstructure:"host" json:"host,omitempty"` + // Launch-persisted fields written by `dhq launch` to .deployhq.toml. + // Server is the identifier of the provisioned server (e.g. "srv-abc123"). + Server string `mapstructure:"server" json:"server,omitempty"` + // Target is the protocol type used during provisioning (e.g. "static_hosting", "managed_vps"). + Target string `mapstructure:"target" json:"target,omitempty"` + // Resolved metadata (not persisted) Sources map[string]string `json:"sources,omitempty"` // field -> source layer } // Keys is the list of all config keys. -var Keys = []string{"account", "email", "api_key", "project", "format", "host"} +var Keys = []string{"account", "email", "api_key", "project", "format", "host", "server", "target"} // Load reads config from all 4 layers and returns the resolved Config. func Load() (*Config, error) { diff --git a/internal/detect/detect.go b/internal/detect/detect.go new file mode 100644 index 0000000..cb071bd --- /dev/null +++ b/internal/detect/detect.go @@ -0,0 +1,191 @@ +// Package detect provides a minimal local target heuristic for the dhq launch flow. +// +// This is the OFFLINE FALLBACK. The primary path is the backend's POST /detection +// endpoint (see launchDetect), which runs the same StackDetector pipeline as the +// web onboarding wizard — including rule-based detection and, when the account +// permits, AI-assisted framework + static-hosting assessment — over an uploaded +// manifest. That keeps the CLI's recommendation in lockstep with the server and +// is the single source of truth for framework identity and build configuration +// (output directory, build command, SPA routing). +// +// This local heuristic only runs when that endpoint is unavailable (older +// backend, offline, transient error). It deliberately does NOT try to identify +// the framework or infer build configuration — duplicating the backend's +// per-framework logic here only drifts out of sync. It answers one coarse +// question so the launch flow can pre-seed the target prompt: +// +// - a server-runtime manifest/entrypoint (Gemfile [Ruby], requirements.txt / +// Pipfile / pyproject.toml [Python], composer.json / index.php [PHP], +// go.mod [Go], …) → managed_vps +// - a package.json declaring a known static-site framework/bundler → static_hosting +// - no confident signal → "" (let the user choose) +// +// Detection is intentionally heuristic and conservative. False negatives +// (returning "" when a protocol could be inferred) are preferred over false +// positives. CollectManifest (manifest.go) gathers the upload for the primary path. +package detect + +import ( + "encoding/json" + "os" + "path/filepath" +) + +// Protocol constants match the API protocol_type values. +const ( + ProtocolStaticHosting = "static_hosting" + ProtocolManagedVPS = "managed_vps" + // ProtocolNone means no signal was detected — the user should choose manually. + ProtocolNone = "" +) + +// Framework identifies a detected framework. Local detection no longer populates +// it (the backend's /detection response is the authority); the type is retained +// because detectionResultFromAPI maps the backend stack into it and callers read +// it for display. +type Framework string + +// FrameworkUnknown is the zero value — local detection always leaves Framework +// unset, and the backend reports the concrete stack when available. +const FrameworkUnknown Framework = "" + +// Result holds the output of a detection pass. +// +// Local Detect() only sets SuggestedProtocol. The other fields are populated by +// detectionResultFromAPI from the backend /detection response (the authority for +// framework identity and build configuration) and are read by the launch flow. +type Result struct { + // Framework is the detected framework (set from the backend response), or + // FrameworkUnknown when not identified / from local detection. + Framework Framework + + // SuggestedProtocol is "static_hosting", "managed_vps", or "" (no suggestion). + // "" means the caller should ask the user to choose a target manually. + SuggestedProtocol string + + // BuildCommands are the suggested build steps from the backend response — + // each a separate command (e.g. "Install dependencies", "Build"), preserved + // individually rather than collapsed into one shell line, so they match the + // web wizard's build pipeline. Empty from local detection. + BuildCommands []BuildCommandStep + + // OutputDir is the build output directory from the backend response, or "" + // (local detection never sets it). + OutputDir string + + // SPA is true when the backend reports the site needs single-page-application + // routing (all paths rewritten to index.html). Local detection never sets it. + SPA bool + + // ExcludedFiles and BuildCacheFiles are suggested deploy-exclude and + // build-cache patterns from the backend response (the same the web wizard + // applies). Empty from local detection. + ExcludedFiles []string + BuildCacheFiles []string +} + +// BuildCommandStep is a single suggested build step. It mirrors the backend +// catalog entry — description, command, the predefined template_name, and +// halt_on_error — so the launch flow can recreate each step faithfully. +type BuildCommandStep struct { + Description string + Command string + TemplateName string + HaltOnError *bool +} + +// Detect reads the directory at dir and returns a coarse target Result. +// dir should be the root of the project (the directory containing package.json, +// Gemfile, etc.). If dir is empty it defaults to ".". +// +// Detection never fails: when no signal is recognised, it returns a zero-value +// Result with SuggestedProtocol = "". Only SuggestedProtocol is populated. +func Detect(dir string) Result { + if dir == "" { + dir = "." + } + + has := func(names ...string) bool { + for _, name := range names { + if _, err := os.Stat(filepath.Join(dir, name)); err == nil { + return true + } + } + return false + } + + // 1. Server-runtime manifests/entrypoints → Managed VPS. Checked first: + // full-stack frameworks (Rails, Django, Laravel) commonly ship a + // package.json for asset bundling, so a server manifest is the stronger + // signal. PHP is covered both by composer.json (Composer projects: Laravel, + // Symfony, …) and a root index.php entrypoint (Composer-less / legacy PHP, + // WordPress). + if has("Gemfile", "requirements.txt", "Pipfile", "pyproject.toml", "composer.json", "go.mod", "index.php") { + return Result{SuggestedProtocol: ProtocolManagedVPS} + } + + // 2. A package.json declaring a known static-site framework or bundler → + // Static Hosting. + if has("package.json") { + if data, err := os.ReadFile(filepath.Join(dir, "package.json")); err == nil && packageDeclaresStaticBuild(data) { + return Result{SuggestedProtocol: ProtocolStaticHosting} + } + } + + // 3. No confident signal — let the user choose. + return Result{} +} + +// staticBuildDeps are package.json dependencies that indicate a static-site +// build (the output is a directory of static assets suitable for CDN hosting). +// The list is intentionally coarse — it only seeds the offline target suggestion, +// which the user can override. The backend's /detection endpoint is the +// authoritative source for framework identity and build configuration. +var staticBuildDeps = map[string]struct{}{ + "next": {}, + "nuxt": {}, + "nuxt3": {}, + "react": {}, + "react-dom": {}, + "vue": {}, + "@vue/cli-service": {}, + "@angular/core": {}, + "svelte": {}, + "@sveltejs/kit": {}, + "astro": {}, + "gatsby": {}, + "vite": {}, + "preact": {}, + "solid-js": {}, + "gridsome": {}, + "@11ty/eleventy": {}, + "vitepress": {}, + "vuepress": {}, + "@docusaurus/core": {}, +} + +// packageDeclaresStaticBuild reports whether a package.json's dependencies or +// devDependencies include a known static-site framework or bundler. +func packageDeclaresStaticBuild(pkgJSON []byte) bool { + if len(pkgJSON) == 0 { + return false + } + var pkg struct { + Dependencies map[string]json.RawMessage `json:"dependencies"` + DevDependencies map[string]json.RawMessage `json:"devDependencies"` + } + if err := json.Unmarshal(pkgJSON, &pkg); err != nil { + return false + } + for name := range pkg.Dependencies { + if _, ok := staticBuildDeps[name]; ok { + return true + } + } + for name := range pkg.DevDependencies { + if _, ok := staticBuildDeps[name]; ok { + return true + } + } + return false +} diff --git a/internal/detect/detect_test.go b/internal/detect/detect_test.go new file mode 100644 index 0000000..d1b29fe --- /dev/null +++ b/internal/detect/detect_test.go @@ -0,0 +1,210 @@ +package detect + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fixtureDir creates a temporary directory populated with the given files and returns +// the path. t.TempDir handles cleanup. +func fixtureDir(t *testing.T, files map[string]string) string { + t.Helper() + dir := t.TempDir() + for name, content := range files { + path := filepath.Join(dir, name) + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) + } + return dir +} + +func TestDetect_Empty(t *testing.T) { + dir := t.TempDir() + result := Detect(dir) + assert.Equal(t, FrameworkUnknown, result.Framework) + assert.Equal(t, ProtocolNone, result.SuggestedProtocol) + assert.Empty(t, result.BuildCommands) + assert.Empty(t, result.OutputDir) + assert.False(t, result.SPA) +} + +func TestDetect_DefaultsToCurrentDir(t *testing.T) { + // Detect("") should not panic. + result := Detect("") + // We can't assert the exact output since it depends on the test runner's cwd, + // but we assert it returns a valid (non-panicking) result. + _ = result +} + +// --- Server-runtime manifests → Managed VPS --- + +func TestDetect_Gemfile(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "Gemfile": `source "https://rubygems.org"` + "\n" + `gem "rails", "~> 7.1"`, + }) + result := Detect(dir) + assert.Equal(t, ProtocolManagedVPS, result.SuggestedProtocol) + // Local detection no longer identifies the framework or build config. + assert.Equal(t, FrameworkUnknown, result.Framework) + assert.Empty(t, result.BuildCommands) + assert.Empty(t, result.OutputDir) +} + +func TestDetect_RequirementsTxt(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "requirements.txt": "django==4.2\ngunicorn==21.2", + }) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol) +} + +func TestDetect_Pipfile(t *testing.T) { + dir := fixtureDir(t, map[string]string{"Pipfile": "[packages]\nflask = \"*\""}) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol) +} + +func TestDetect_PyprojectToml(t *testing.T) { + dir := fixtureDir(t, map[string]string{"pyproject.toml": "[project]\nname = \"app\""}) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol) +} + +func TestDetect_ComposerJSON(t *testing.T) { + // PHP via Composer (Laravel, Symfony, …). + dir := fixtureDir(t, map[string]string{ + "composer.json": `{"require": {"laravel/framework": "^10.0"}}`, + }) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol) +} + +func TestDetect_IndexPHP(t *testing.T) { + // Composer-less / legacy PHP (plain PHP, WordPress) has no composer.json but a + // root index.php entrypoint — still a server runtime. + dir := fixtureDir(t, map[string]string{ + "index.php": "", + }) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol) +} + +func TestDetect_GoMod(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "go.mod": "module example.com/myapp\n\ngo 1.21", + }) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol) +} + +// --- package.json with a static framework/bundler → Static Hosting --- + +func TestDetect_NextJS(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "package.json": `{ + "name": "my-app", + "dependencies": { "next": "14.0.0", "react": "18.0.0", "react-dom": "18.0.0" }, + "scripts": { "build": "next build" } + }`, + }) + result := Detect(dir) + assert.Equal(t, ProtocolStaticHosting, result.SuggestedProtocol) + // Build config comes from the backend, not local detection. + assert.Empty(t, result.BuildCommands) + assert.Empty(t, result.OutputDir) + assert.False(t, result.SPA) +} + +func TestDetect_Vite_DevDependency(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "package.json": `{ + "name": "react-app", + "dependencies": { "react": "18.0.0", "react-dom": "18.0.0" }, + "devDependencies": { "vite": "5.0.0" }, + "scripts": { "build": "vite build" } + }`, + }) + assert.Equal(t, ProtocolStaticHosting, Detect(dir).SuggestedProtocol) +} + +func TestDetect_Angular(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "package.json": `{ + "name": "ng-app", + "dependencies": { "@angular/core": "17.0.0" }, + "scripts": { "build": "ng build" } + }`, + }) + assert.Equal(t, ProtocolStaticHosting, Detect(dir).SuggestedProtocol) +} + +func TestDetect_Astro(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "package.json": `{"name": "site", "devDependencies": { "astro": "4.0.0" }}`, + }) + assert.Equal(t, ProtocolStaticHosting, Detect(dir).SuggestedProtocol) +} + +// --- Precedence: server manifest wins over a static package.json --- + +func TestDetect_ServerManifestWinsOverStaticPackageJSON(t *testing.T) { + // A Rails app that ships a package.json with Vite for asset bundling must be + // classified as managed_vps — the Gemfile is the stronger signal. This is the + // core reason server manifests are checked first. + dir := fixtureDir(t, map[string]string{ + "Gemfile": `gem "rails"`, + "package.json": `{ + "name": "rails-app", + "devDependencies": { "vite": "5.0.0", "@vitejs/plugin-react": "4.0.0" } + }`, + }) + assert.Equal(t, ProtocolManagedVPS, Detect(dir).SuggestedProtocol, + "a server manifest must outrank a static package.json") +} + +// --- No confident signal → no suggestion --- + +func TestDetect_NodeServerWithoutStaticDep(t *testing.T) { + // An Express API (package.json, no static framework, no server manifest file) + // yields no confident signal — the user is prompted to choose. + dir := fixtureDir(t, map[string]string{ + "package.json": `{"name": "api", "dependencies": { "express": "4.18.0" }}`, + }) + assert.Equal(t, ProtocolNone, Detect(dir).SuggestedProtocol) +} + +func TestDetect_PackageJSONNoDeps(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "package.json": `{"name": "my-tool", "scripts": { "build": "tsc" }, "dependencies": {}}`, + }) + assert.Equal(t, ProtocolNone, Detect(dir).SuggestedProtocol) +} + +func TestDetect_MalformedPackageJSON(t *testing.T) { + dir := fixtureDir(t, map[string]string{ + "package.json": `this is not valid json`, + }) + // Must not panic; returns no suggestion. + assert.Equal(t, ProtocolNone, Detect(dir).SuggestedProtocol) +} + +// --- Helper: packageDeclaresStaticBuild --- + +func TestPackageDeclaresStaticBuild(t *testing.T) { + tests := []struct { + name string + pkg string + expected bool + }{ + {"empty", "", false}, + {"dependency match", `{"dependencies":{"next":"14"}}`, true}, + {"devDependency match", `{"devDependencies":{"vite":"5"}}`, true}, + {"scoped match", `{"dependencies":{"@angular/core":"17"}}`, true}, + {"no static dep", `{"dependencies":{"express":"4"}}`, false}, + {"malformed", `not json`, false}, + {"empty deps", `{"dependencies":{}}`, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, packageDeclaresStaticBuild([]byte(tt.pkg))) + }) + } +} diff --git a/internal/detect/manifest.go b/internal/detect/manifest.go new file mode 100644 index 0000000..3338682 --- /dev/null +++ b/internal/detect/manifest.go @@ -0,0 +1,75 @@ +package detect + +import ( + "os" + "path/filepath" +) + +// manifestFiles are the files whose CONTENTS the backend's StackDetector +// pipeline parses (package.json deps, Gemfile gems, composer.json, framework +// config files, …). Their contents are uploaded so remote detection reaches +// full precision; everything else is sent as a name-only listing. +var manifestFiles = []string{ + // Node / JS + "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", + "vite.config.js", "vite.config.ts", "vite.config.mjs", + "next.config.js", "next.config.mjs", "next.config.ts", + "nuxt.config.js", "nuxt.config.ts", + "astro.config.mjs", "astro.config.ts", + "svelte.config.js", "remix.config.js", + "angular.json", ".eleventy.js", "eleventy.config.js", + // PHP + "composer.json", "composer.lock", + // Ruby + "Gemfile", "Gemfile.lock", + // Python + "requirements.txt", "Pipfile", "pyproject.toml", + // Go + "go.mod", + // Static site generators + "_config.yml", "_config.yaml", "config.toml", "hugo.toml", "hugo.yaml", +} + +// maxManifestBytes caps each uploaded manifest. Vendored monorepo manifests +// (lockfiles especially) can be enormous; detection only needs the head, and +// the server rejects anything larger than its own cap. +const maxManifestBytes = 64 * 1024 + +// maxManifestFiles bounds how many manifest files we upload (the server caps at 32). +const maxManifestFiles = 32 + +// CollectManifest gathers the input for remote framework detection from dir: +// the root-directory filename listing (for existence checks) plus the contents +// of any present manifest files (capped per file and in count). Returns plain +// types so this package stays independent of the SDK wire format. Never fails — +// an unreadable directory yields an empty listing. +func CollectManifest(dir string) (filenames []string, files map[string]string) { + if dir == "" { + dir = "." + } + // Non-nil so the JSON payload is always `"filenames": []`, never `null` + // (which violates the documented array schema). + filenames = []string{} + files = map[string]string{} + + if entries, err := os.ReadDir(dir); err == nil { + for _, e := range entries { + filenames = append(filenames, e.Name()) + } + } + + for _, name := range manifestFiles { + if len(files) >= maxManifestFiles { + break + } + data, err := os.ReadFile(filepath.Join(dir, name)) + if err != nil { + continue + } + if len(data) > maxManifestBytes { + data = data[:maxManifestBytes] + } + files[name] = string(data) + } + return filenames, files +} diff --git a/internal/detect/manifest_test.go b/internal/detect/manifest_test.go new file mode 100644 index 0000000..8c98153 --- /dev/null +++ b/internal/detect/manifest_test.go @@ -0,0 +1,75 @@ +package detect + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCollectManifest_ListsFilesAndReadsManifests(t *testing.T) { + dir := t.TempDir() + write(t, dir, "package.json", `{"dependencies":{"react":"^18"}}`) + write(t, dir, "index.html", "") + write(t, dir, "README.md", "# hi") + + filenames, files := CollectManifest(dir) + + // Every root entry is listed. + assertContains(t, filenames, "package.json") + assertContains(t, filenames, "index.html") + assertContains(t, filenames, "README.md") + + // Only manifest files have their contents uploaded. + if _, ok := files["package.json"]; !ok { + t.Fatalf("package.json contents must be collected, got keys %v", keys(files)) + } + if _, ok := files["README.md"]; ok { + t.Fatalf("README.md is not a manifest and must not be uploaded") + } + if !strings.Contains(files["package.json"], "react") { + t.Fatalf("manifest contents must be the file body, got %q", files["package.json"]) + } +} + +func TestCollectManifest_CapsLargeManifest(t *testing.T) { + dir := t.TempDir() + write(t, dir, "package-lock.json", strings.Repeat("a", maxManifestBytes+5000)) + + _, files := CollectManifest(dir) + if got := len(files["package-lock.json"]); got != maxManifestBytes { + t.Fatalf("manifest must be capped at %d bytes, got %d", maxManifestBytes, got) + } +} + +func TestCollectManifest_MissingDirIsEmpty(t *testing.T) { + filenames, files := CollectManifest(filepath.Join(t.TempDir(), "does-not-exist")) + if len(filenames) != 0 || len(files) != 0 { + t.Fatalf("missing dir must yield empty manifest, got %v / %v", filenames, files) + } +} + +func write(t *testing.T, dir, name, body string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil { + t.Fatal(err) + } +} + +func keys(m map[string]string) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} + +func assertContains(t *testing.T, haystack []string, needle string) { + t.Helper() + for _, h := range haystack { + if h == needle { + return + } + } + t.Fatalf("expected %v to contain %q", haystack, needle) +} diff --git a/pkg/sdk/build_commands.go b/pkg/sdk/build_commands.go index 498a8bd..1cd0c5c 100644 --- a/pkg/sdk/build_commands.go +++ b/pkg/sdk/build_commands.go @@ -24,10 +24,11 @@ func (b BuildCommand) UUID() string { return b.Identifier } // BuildCommandCreateRequest is the payload for creating/updating a build command. type BuildCommandCreateRequest struct { - Description string `json:"description,omitempty"` - Command string `json:"command"` - HaltOnError *bool `json:"halt_on_error,omitempty"` - Enabled *bool `json:"enabled,omitempty"` + Description string `json:"description,omitempty"` + Command string `json:"command"` + TemplateName string `json:"template_name,omitempty"` + HaltOnError *bool `json:"halt_on_error,omitempty"` + Enabled *bool `json:"enabled,omitempty"` } func (c *Client) ListBuildCommands(ctx context.Context, projectID string, opts *ListOptions) ([]BuildCommand, error) { diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index fcaa263..1e00fb8 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -183,6 +183,16 @@ func (c *Client) do(ctx context.Context, method, path string, body, v interface{ func parseAPIError(resp *http.Response) error { apiErr := &APIError{StatusCode: resp.StatusCode} + // Capture the Retry-After backoff hint on 429s (provisioning rate limit). + // Only the integer-seconds form is parsed; an HTTP-date form is left as 0. + if resp.StatusCode == http.StatusTooManyRequests { + if ra := strings.TrimSpace(resp.Header.Get("Retry-After")); ra != "" { + if secs, convErr := strconv.Atoi(ra); convErr == nil && secs >= 0 { + apiErr.RetryAfter = secs + } + } + } + body, err := io.ReadAll(resp.Body) if err != nil || len(body) == 0 { return apiErr diff --git a/pkg/sdk/client_test.go b/pkg/sdk/client_test.go index 4268964..ef2617e 100644 --- a/pkg/sdk/client_test.go +++ b/pkg/sdk/client_test.go @@ -110,6 +110,47 @@ func TestClient_APIError_MultipleErrors(t *testing.T) { assert.True(t, apiErr.IsValidationError()) } +func TestClient_APIError_RateLimited_RetryAfter(t *testing.T) { + // A 429 with a Retry-After header must parse into IsRateLimited() + RetryAfter. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Retry-After", "42") + w.WriteHeader(http.StatusTooManyRequests) + _ = json.NewEncoder(w).Encode(map[string]string{"error": "provisioning rate limit reached"}) + })) + defer server.Close() + + c := newTestClient(t, server) + _, err := c.CreateProject(context.Background(), ProjectCreateRequest{}) + require.Error(t, err) + + apiErr, ok := err.(*APIError) + require.True(t, ok) + assert.Equal(t, 429, apiErr.StatusCode) + assert.True(t, apiErr.IsRateLimited()) + assert.True(t, IsRateLimited(err)) + assert.Equal(t, 42, apiErr.RetryAfter) + // 429 must not be mistaken for the 422 cap. + assert.False(t, apiErr.IsValidationError()) +} + +func TestClient_APIError_RateLimited_NoRetryAfter(t *testing.T) { + // A 429 without a Retry-After header is still rate-limited; RetryAfter is 0. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTooManyRequests) + _ = json.NewEncoder(w).Encode(map[string]string{"error": "slow down"}) + })) + defer server.Close() + + c := newTestClient(t, server) + _, err := c.CreateProject(context.Background(), ProjectCreateRequest{}) + require.Error(t, err) + + apiErr, ok := err.(*APIError) + require.True(t, ok) + assert.True(t, apiErr.IsRateLimited()) + assert.Equal(t, 0, apiErr.RetryAfter) +} + func TestClient_APIError_Unauthorized(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) diff --git a/pkg/sdk/detection.go b/pkg/sdk/detection.go new file mode 100644 index 0000000..6f09d14 --- /dev/null +++ b/pkg/sdk/detection.go @@ -0,0 +1,71 @@ +package sdk + +import "context" + +// DetectionPayload is the uploaded manifest for POST /detection: a filename +// listing (for existence checks) plus the contents of a bounded set of +// manifest files (for content-based detection). Files the server doesn't +// receive simply lower precision; they never error. +type DetectionPayload struct { + Filenames []string `json:"filenames"` + Files map[string]string `json:"files,omitempty"` +} + +// DetectionResponse is the backend's framework detection result — the same +// StackDetector pipeline the web onboarding wizard uses. +type DetectionResponse struct { + Stack string `json:"stack"` + Version string `json:"version"` + Evidence []string `json:"evidence"` + Description string `json:"description"` + SuggestedProtocol string `json:"suggested_protocol"` + StaticHosting DetectionStaticHosting `json:"static_hosting"` + BuildCommands []DetectionBuildCommand `json:"build_commands"` + // ExcludedFiles and BuildCacheFiles are the suggested deploy-exclude and + // build-cache patterns for the detected stack — the same source the web + // onboarding wizard uses. Optional/additive; empty on older backends. + ExcludedFiles []DetectionFile `json:"excluded_files,omitempty"` + BuildCacheFiles []DetectionFile `json:"build_cache_files,omitempty"` + // AIAssisted is true when the backend's AI services contributed to the + // result (only when the account has AI features enabled and the rule-based + // result was ambiguous). Optional/additive; absent on older backends. + AIAssisted bool `json:"ai_assisted,omitempty"` +} + +// DetectionFile is a single suggested file path — an excluded-file pattern or a +// build-cache entry. +type DetectionFile struct { + Path string `json:"path"` +} + +// DetectionStaticHosting holds the static-hosting assessment for the detected stack. +type DetectionStaticHosting struct { + Eligibility string `json:"eligibility"` + RootPath string `json:"root_path"` + SPAMode bool `json:"spa_mode"` + Confidence string `json:"confidence"` +} + +// DetectionBuildCommand is a single suggested build step. halt_on_error and +// template_name mirror the catalog so the CLI can recreate the build pipeline +// faithfully; both are optional/additive (absent on older backends). +type DetectionBuildCommand struct { + Description string `json:"description"` + Command string `json:"command"` + HaltOnError *bool `json:"halt_on_error,omitempty"` + TemplateName string `json:"template_name,omitempty"` +} + +// DetectFramework asks the backend to detect the project's framework from an +// uploaded manifest. The result mirrors the web onboarding wizard's detection, +// keeping the CLI's recommendation in lockstep with the backend. +func (c *Client) DetectFramework(ctx context.Context, payload DetectionPayload) (*DetectionResponse, error) { + body := struct { + Detection DetectionPayload `json:"detection"` + }{Detection: payload} + var resp DetectionResponse + if err := c.post(ctx, "/detection", body, &resp); err != nil { + return nil, err + } + return &resp, nil +} diff --git a/pkg/sdk/errors.go b/pkg/sdk/errors.go index 0012f1f..2dd0dcc 100644 --- a/pkg/sdk/errors.go +++ b/pkg/sdk/errors.go @@ -10,6 +10,10 @@ type APIError struct { StatusCode int `json:"status"` Message string `json:"error,omitempty"` Errors []string `json:"errors,omitempty"` + // RetryAfter is the parsed Retry-After header (in seconds) accompanying a + // 429 response, or 0 when absent/unparseable. Callers hitting a provisioning + // rate limit should back off for this long before retrying. + RetryAfter int `json:"retry_after,omitempty"` } func (e *APIError) Error() string { @@ -44,11 +48,27 @@ func (e *APIError) IsForbidden() bool { return e.StatusCode == http.StatusForbidden } +// IsEmailVerificationRequired is the one 403 the launch flow handles gracefully: +// the account's email is not yet verified, so the backend's deploy gate blocks +// it. It is deliberately distinct from a generic 403 (AccessDenied) — which must +// still surface as a real error. +func (e *APIError) IsEmailVerificationRequired() bool { + return e.StatusCode == http.StatusForbidden && e.Message == "email_verification_required" +} + // IsValidationError returns true if the error is a 422. func (e *APIError) IsValidationError() bool { return e.StatusCode == http.StatusUnprocessableEntity } +// IsRateLimited returns true if the error is a 429. For metered-resource +// provisioning this is the per-account provisioning rate limit — deliberately +// distinct from the 422 cap/kill-switch — and is safe to retry after backing +// off for RetryAfter seconds. +func (e *APIError) IsRateLimited() bool { + return e.StatusCode == http.StatusTooManyRequests +} + // IsServerError returns true if the error is a 5xx. func (e *APIError) IsServerError() bool { return e.StatusCode >= 500 @@ -77,3 +97,11 @@ func IsForbidden(err error) bool { } return false } + +// IsRateLimited checks whether err is an APIError with status 429. +func IsRateLimited(err error) bool { + if apiErr, ok := err.(*APIError); ok { + return apiErr.IsRateLimited() + } + return false +} diff --git a/pkg/sdk/managed_hosting.go b/pkg/sdk/managed_hosting.go new file mode 100644 index 0000000..c61168f --- /dev/null +++ b/pkg/sdk/managed_hosting.go @@ -0,0 +1,152 @@ +package sdk + +import ( + "context" + "fmt" + "strings" +) + +// GetAccountCapabilities returns the beta/eligibility status for the current account. +// The capability flags live on the account sub-object of GET /profile, which — +// unlike GET /account — is readable by ANY authenticated account member (not admin-gated). +// +// Returns AccountCapabilities with beta_features, static_hosting_eligible, and +// managed_vps_eligible populated; the other profile/account fields are ignored. +// +// On an older backend without the capability fields they default to false, and the +// caller should direct the user to /beta_features — handling staging/version mismatches. +func (c *Client) GetAccountCapabilities(ctx context.Context) (*AccountCapabilities, error) { + // Decode only the account capability fields from the profile payload. + var profile struct { + Account AccountCapabilities `json:"account"` + } + if err := c.get(ctx, "/profile", &profile); err != nil { + return nil, err + } + return &profile.Account, nil +} + +// EnrollBeta enrolls the current account in the managed-resources beta. +// This calls POST /beta/enrollments with the given protocol ("static_hosting", +// "managed_vps", or "" to enroll in all managed-resources protocols). +// +// Authorization: +// - Admin users: can flip beta from false to true. +// - Non-admin users who are already enrolled: pass idempotently. +// - Non-admin users who are not yet enrolled: receive a 403 with a structured +// error body. The caller should surface an actionable message with the +// /beta_features deep-link rather than a raw 403. +// +// Returns ErrBetaEnrollAdminRequired (a *APIError with StatusCode 403) when +// the account is not yet enrolled and the current user is not an admin. +func (c *Client) EnrollBeta(ctx context.Context, protocol string) (*BetaEnrollmentResponse, error) { + req := BetaEnrollmentRequest{Protocol: protocol} + var resp BetaEnrollmentResponse + if err := c.post(ctx, "/beta/enrollments", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ListManagedHostingRegions returns the DigitalOcean regions available for +// Managed VPS provisioning. This endpoint requires beta_features to be enabled +// on the account (require_beta_features gate on the backend). +// +// Use the returned Region.Slug as ServerCreateRequest.Region. +func (c *Client) ListManagedHostingRegions(ctx context.Context) ([]ManagedHostingRegion, error) { + var regions []ManagedHostingRegion + if err := c.get(ctx, "/managed_hosting/regions", ®ions); err != nil { + return nil, err + } + return regions, nil +} + +// ListManagedHostingSizes returns the DigitalOcean droplet sizes available for +// Managed VPS provisioning. Prices are denominated in the account's billing +// currency. This endpoint requires beta_features to be enabled on the account. +// +// Use the returned Size.Slug as ServerCreateRequest.Size. +func (c *Client) ListManagedHostingSizes(ctx context.Context) ([]ManagedHostingSize, error) { + var sizes []ManagedHostingSize + if err := c.get(ctx, "/managed_hosting/sizes", &sizes); err != nil { + return nil, err + } + return sizes, nil +} + +// GetServerProvisioningState returns the current server record from the +// project-scoped server show endpoint (GET /projects/:id/servers/:id). +// +// This is the correct polling target for Managed VPS and Static Hosting servers +// during provisioning. The endpoint is gated by project config permission (not +// admin), which is the same access level required to create the server. +// +// When the server is a managed_vps or static_hosting, the returned Server carries +// a protocol-specific block (ManagedVPS or StaticHosting) with the provisioning +// status and, once active, the IP / live URL. Use ProvisioningStatus(server), +// IsProvisioning(server), IsProvisioningActive(server) and LiveURL(server) to read them. +// +// For non-managed servers those blocks are nil. +func (c *Client) GetServerProvisioningState(ctx context.Context, projectID, serverID string) (*Server, error) { + var server Server + if err := c.get(ctx, fmt.Sprintf("/projects/%s/servers/%s", projectID, serverID), &server); err != nil { + return nil, err + } + return &server, nil +} + +// LiveURL extracts the publicly accessible URL from a provisioned server. +// Returns "" when the server is not yet active or is not a managed type. +// +// For static_hosting: returns server.StaticHosting.URL when non-empty. +// For managed_vps: returns "http://" when IPAddress is set. +// For all other protocol types: returns "". +func LiveURL(server *Server) string { + if server == nil { + return "" + } + switch server.ProtocolType { + case "static_hosting": + if server.StaticHosting != nil && server.StaticHosting.URL != "" { + return server.StaticHosting.URL + } + case "managed_vps": + if server.ManagedVPS != nil && server.ManagedVPS.IPAddress != "" { + return "http://" + server.ManagedVPS.IPAddress + } + } + return "" +} + +// ProvisioningStatus returns the managed-resource provisioning lifecycle status +// ("provisioning" / "active" / "error") for a managed_vps or static_hosting server, +// reading whichever protocol-specific block the backend populated. Returns "" for +// non-managed servers or when the block is absent. +func ProvisioningStatus(server *Server) string { + if server == nil { + return "" + } + switch server.ProtocolType { + case "static_hosting": + if server.StaticHosting != nil { + return server.StaticHosting.Status + } + case "managed_vps": + if server.ManagedVPS != nil { + return server.ManagedVPS.Status + } + } + return "" +} + +// IsProvisioning returns true when the server is a managed type whose +// backend resource has not yet finished provisioning. +func IsProvisioning(server *Server) bool { + return strings.EqualFold(ProvisioningStatus(server), "provisioning") +} + +// IsProvisioningActive returns true when the managed server has successfully +// completed provisioning and is ready for deployments. +func IsProvisioningActive(server *Server) bool { + return strings.EqualFold(ProvisioningStatus(server), "active") +} diff --git a/pkg/sdk/managed_hosting_test.go b/pkg/sdk/managed_hosting_test.go new file mode 100644 index 0000000..c305a78 --- /dev/null +++ b/pkg/sdk/managed_hosting_test.go @@ -0,0 +1,354 @@ +package sdk + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Capability flags are served from the account sub-object of GET /profile, +// which (unlike GET /account) is readable by any authenticated account member. + +func TestGetAccountCapabilities(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/profile", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + _ = json.NewEncoder(w).Encode(map[string]any{ + "email_address": "dev@example.com", + "account": map[string]any{ + "name": "Example", + "beta_features": true, + "static_hosting_eligible": true, + "managed_vps_eligible": true, + }, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + caps, err := c.GetAccountCapabilities(context.Background()) + require.NoError(t, err) + assert.True(t, caps.BetaFeatures) + assert.True(t, caps.StaticHostingEligible) + assert.True(t, caps.ManagedVPSEligible) +} + +func TestGetAccountCapabilities_NotBetaEnrolled(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/profile", r.URL.Path) + _ = json.NewEncoder(w).Encode(map[string]any{ + "account": map[string]any{ + "beta_features": false, + "static_hosting_eligible": false, + "managed_vps_eligible": false, + }, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + caps, err := c.GetAccountCapabilities(context.Background()) + require.NoError(t, err) + assert.False(t, caps.BetaFeatures) + assert.False(t, caps.StaticHostingEligible) +} + +func TestGetAccountCapabilities_MissingFieldsDefaultFalse(t *testing.T) { + // An older backend without the capability fields → they decode to false, + // and the caller falls back to directing the user to /beta_features. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "account": map[string]any{"name": "Example"}, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + caps, err := c.GetAccountCapabilities(context.Background()) + require.NoError(t, err) + assert.False(t, caps.BetaFeatures) + assert.False(t, caps.StaticHostingEligible) + assert.False(t, caps.ManagedVPSEligible) +} + +func TestEnrollBeta(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/beta/enrollments", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + var body BetaEnrollmentRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Equal(t, "static_hosting", body.Protocol) + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(BetaEnrollmentResponse{ + Enrolled: true, + BetaFeatures: true, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + resp, err := c.EnrollBeta(context.Background(), "static_hosting") + require.NoError(t, err) + assert.True(t, resp.Enrolled) + assert.True(t, resp.BetaFeatures) +} + +func TestEnrollBeta_AlreadyEnrolled_Idempotent(t *testing.T) { + // Non-admin user who is already enrolled — passes idempotently (D8). + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(BetaEnrollmentResponse{ + Enrolled: true, + BetaFeatures: true, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + resp, err := c.EnrollBeta(context.Background(), "managed_vps") + require.NoError(t, err) + assert.True(t, resp.Enrolled) +} + +func TestEnrollBeta_AdminRequired(t *testing.T) { + // Non-admin user who is NOT enrolled — backend returns 403 with structured error. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + _ = json.NewEncoder(w).Encode(map[string]string{ + "error": "admin_required", + "beta_url": "https://example.deployhq.com/beta_features", + }) + })) + defer server.Close() + + c := newTestClient(t, server) + _, err := c.EnrollBeta(context.Background(), "static_hosting") + require.Error(t, err) + assert.True(t, IsForbidden(err)) + + apiErr, ok := err.(*APIError) + require.True(t, ok) + assert.Equal(t, 403, apiErr.StatusCode) +} + +func TestEnrollBeta_AllProtocols(t *testing.T) { + // Empty protocol → enroll in all managed protocols. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body BetaEnrollmentRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Empty(t, body.Protocol, "empty protocol enrolls in all managed protocols") + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(BetaEnrollmentResponse{Enrolled: true, BetaFeatures: true}) + })) + defer server.Close() + + c := newTestClient(t, server) + resp, err := c.EnrollBeta(context.Background(), "") + require.NoError(t, err) + assert.True(t, resp.Enrolled) +} + +func TestListManagedHostingRegions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/managed_hosting/regions", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + _ = json.NewEncoder(w).Encode([]ManagedHostingRegion{ + {Slug: "lon1", Name: "London, United Kingdom", Available: true}, + {Slug: "nyc3", Name: "New York City, United States", Available: true}, + {Slug: "ams3", Name: "Amsterdam, Netherlands", Available: false}, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + regions, err := c.ListManagedHostingRegions(context.Background()) + require.NoError(t, err) + assert.Len(t, regions, 3) + assert.Equal(t, "lon1", regions[0].Slug) + assert.True(t, regions[0].Available) + assert.False(t, regions[2].Available) +} + +func TestListManagedHostingSizes(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/managed_hosting/sizes", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + _ = json.NewEncoder(w).Encode([]ManagedHostingSize{ + {Slug: "s-1vcpu-1gb", Description: "1 vCPU / 1 GB RAM", PriceMonthly: 6.0, PriceHourly: 0.009, Memory: 1024, VCPUs: 1, Disk: 25}, + {Slug: "s-2vcpu-2gb", Description: "2 vCPU / 2 GB RAM", PriceMonthly: 12.0, PriceHourly: 0.018, Memory: 2048, VCPUs: 2, Disk: 60}, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + sizes, err := c.ListManagedHostingSizes(context.Background()) + require.NoError(t, err) + assert.Len(t, sizes, 2) + assert.Equal(t, "s-1vcpu-1gb", sizes[0].Slug) + assert.Equal(t, 6.0, sizes[0].PriceMonthly) + assert.Equal(t, 1024, sizes[0].Memory) +} + +func TestGetServerProvisioningState_ManagedVPS(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/projects/my-app/servers/srv-vps", r.URL.Path) + _ = json.NewEncoder(w).Encode(Server{ + Identifier: "srv-vps", + Name: "My VPS", + ProtocolType: "managed_vps", + ManagedVPS: &ManagedVPSInfo{ + HostedResourceIdentifier: "hr-1", + Status: "active", + IPAddress: "203.0.113.10", + Region: "lon1", + Size: "s-1vcpu-1gb", + }, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + s, err := c.GetServerProvisioningState(context.Background(), "my-app", "srv-vps") + require.NoError(t, err) + assert.Equal(t, "managed_vps", s.ProtocolType) + assert.Equal(t, "active", ProvisioningStatus(s)) + assert.True(t, IsProvisioningActive(s)) + require.NotNil(t, s.ManagedVPS) + assert.Equal(t, "203.0.113.10", s.ManagedVPS.IPAddress) + assert.Equal(t, "http://203.0.113.10", LiveURL(s)) +} + +func TestGetServerProvisioningState_StaticHosting(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/projects/my-app/servers/srv-static", r.URL.Path) + _ = json.NewEncoder(w).Encode(Server{ + Identifier: "srv-static", + Name: "My Site", + ProtocolType: "static_hosting", + StaticHosting: &StaticHostingInfo{ + URL: "https://my-app.deployhq-sites.com", + Subdomain: "my-app", + Status: "active", + }, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + s, err := c.GetServerProvisioningState(context.Background(), "my-app", "srv-static") + require.NoError(t, err) + assert.Equal(t, "static_hosting", s.ProtocolType) + assert.Equal(t, "active", ProvisioningStatus(s)) + require.NotNil(t, s.StaticHosting) + assert.Equal(t, "https://my-app.deployhq-sites.com", LiveURL(s)) +} + +func TestGetServerProvisioningState_Provisioning(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(Server{ + Identifier: "srv-vps", + ProtocolType: "managed_vps", + ManagedVPS: &ManagedVPSInfo{Status: "provisioning"}, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + s, err := c.GetServerProvisioningState(context.Background(), "my-app", "srv-vps") + require.NoError(t, err) + assert.True(t, IsProvisioning(s)) + assert.False(t, IsProvisioningActive(s)) +} + +// --- LiveURL / status helpers --- + +func TestLiveURL(t *testing.T) { + tests := []struct { + name string + server *Server + expected string + }{ + { + name: "nil server", + server: nil, + expected: "", + }, + { + name: "static_hosting with url", + server: &Server{ProtocolType: "static_hosting", StaticHosting: &StaticHostingInfo{URL: "https://my-app.deployhq-sites.com"}}, + expected: "https://my-app.deployhq-sites.com", + }, + { + name: "static_hosting no StaticHosting block", + server: &Server{ProtocolType: "static_hosting"}, + expected: "", + }, + { + name: "managed_vps with ip", + server: &Server{ProtocolType: "managed_vps", ManagedVPS: &ManagedVPSInfo{IPAddress: "203.0.113.10"}}, + expected: "http://203.0.113.10", + }, + { + name: "managed_vps no ip yet", + server: &Server{ProtocolType: "managed_vps", ManagedVPS: &ManagedVPSInfo{}}, + expected: "", + }, + { + name: "ssh server", + server: &Server{ProtocolType: "ssh", Hostname: "prod.example.com"}, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, LiveURL(tt.server)) + }) + } +} + +func TestIsProvisioning(t *testing.T) { + tests := []struct { + name string + server *Server + expected bool + }{ + {"nil", nil, false}, + {"managed_vps provisioning", &Server{ProtocolType: "managed_vps", ManagedVPS: &ManagedVPSInfo{Status: "provisioning"}}, true}, + {"managed_vps active", &Server{ProtocolType: "managed_vps", ManagedVPS: &ManagedVPSInfo{Status: "active"}}, false}, + {"static_hosting provisioning", &Server{ProtocolType: "static_hosting", StaticHosting: &StaticHostingInfo{Status: "provisioning"}}, true}, + {"static_hosting active", &Server{ProtocolType: "static_hosting", StaticHosting: &StaticHostingInfo{Status: "active"}}, false}, + {"ssh server", &Server{ProtocolType: "ssh"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsProvisioning(tt.server)) + }) + } +} + +func TestIsProvisioningActive(t *testing.T) { + tests := []struct { + name string + server *Server + expected bool + }{ + {"nil", nil, false}, + {"managed_vps active", &Server{ProtocolType: "managed_vps", ManagedVPS: &ManagedVPSInfo{Status: "active"}}, true}, + {"managed_vps provisioning", &Server{ProtocolType: "managed_vps", ManagedVPS: &ManagedVPSInfo{Status: "provisioning"}}, false}, + {"static_hosting active", &Server{ProtocolType: "static_hosting", StaticHosting: &StaticHostingInfo{Status: "active"}}, true}, + {"ssh server", &Server{ProtocolType: "ssh"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsProvisioningActive(tt.server)) + }) + } +} diff --git a/pkg/sdk/server_protocols_test.go b/pkg/sdk/server_protocols_test.go new file mode 100644 index 0000000..555dcb0 --- /dev/null +++ b/pkg/sdk/server_protocols_test.go @@ -0,0 +1,190 @@ +package sdk + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// createServerBody mirrors the wire shape CreateServer sends: the generic server +// object plus the managed-resource provisioning params as TOP-LEVEL siblings of +// `server` (the backend reads params[:region], params[:hosted_website_attributes], +// etc. — not params[:server][:region]). +type createServerBody struct { + Server ServerCreateRequest `json:"server"` + HostedWebsiteAttributes *HostedWebsiteAttributes `json:"hosted_website_attributes"` + Region string `json:"region"` + Size string `json:"size"` + OSImage string `json:"os_image"` +} + +// TestCreateServer_StaticHosting verifies the static_hosting request shape: +// hosted_website_attributes is a top-level sibling of `server` (subdomain, +// spa_mode, subdirectory), and no VPS params are present. +func TestCreateServer_StaticHosting(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/projects/my-app/servers", r.URL.Path) + + var body createServerBody + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + + assert.Equal(t, "static_hosting", body.Server.ProtocolType) + require.NotNil(t, body.HostedWebsiteAttributes, "hosted_website_attributes must be a top-level sibling of server") + assert.Equal(t, "my-site", body.HostedWebsiteAttributes.Subdomain) + assert.Equal(t, "dist", body.HostedWebsiteAttributes.Subdirectory) + assert.True(t, body.HostedWebsiteAttributes.SPAMode) + + // VPS params must be absent + assert.Empty(t, body.Region) + assert.Empty(t, body.Size) + assert.Empty(t, body.OSImage) + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(Server{ + Identifier: "srv-static", + Name: "My Site", + ProtocolType: "static_hosting", + StaticHosting: &StaticHostingInfo{ + URL: "https://my-site.deployhq-sites.com", + Subdomain: "my-site", + Status: "provisioning", + }, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + s, err := c.CreateServer(context.Background(), "my-app", ServerCreateRequest{ + Name: "My Site", + ProtocolType: "static_hosting", + HostedWebsiteAttributes: &HostedWebsiteAttributes{ + Subdomain: "my-site", + Subdirectory: "dist", + SPAMode: true, + }, + }) + require.NoError(t, err) + assert.Equal(t, "srv-static", s.Identifier) + require.NotNil(t, s.StaticHosting) + assert.Equal(t, "https://my-site.deployhq-sites.com", s.StaticHosting.URL) +} + +// TestCreateServer_ManagedVPS verifies the managed_vps request shape: +// region, size, and os_image are top-level siblings of `server` (not nested). +func TestCreateServer_ManagedVPS(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + var body createServerBody + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + + assert.Equal(t, "managed_vps", body.Server.ProtocolType) + assert.Equal(t, "lon1", body.Region) + assert.Equal(t, "s-1vcpu-1gb", body.Size) + assert.Equal(t, "ubuntu-24-04-x64", body.OSImage) + + // Static hosting attributes must be absent + assert.Nil(t, body.HostedWebsiteAttributes) + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(Server{ + Identifier: "srv-vps", + Name: "My VPS", + ProtocolType: "managed_vps", + ManagedVPS: &ManagedVPSInfo{Status: "provisioning"}, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + s, err := c.CreateServer(context.Background(), "my-app", ServerCreateRequest{ + Name: "My VPS", + ProtocolType: "managed_vps", + Region: "lon1", + Size: "s-1vcpu-1gb", + OSImage: "ubuntu-24-04-x64", + }) + require.NoError(t, err) + assert.Equal(t, "srv-vps", s.Identifier) + assert.Equal(t, "provisioning", ProvisioningStatus(s)) +} + +// TestCreateServer_ManagedVPS_DefaultOSImage verifies that os_image is omitted +// from the request entirely when not set — the backend defaults it to +// ubuntu-24-04-x64 (OQ3 hardcoded default), the CLI does not force-fill it. +func TestCreateServer_ManagedVPS_DefaultOSImage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body createServerBody + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + + // OSImage omitted → not hoisted → absent from the top-level body. + assert.Empty(t, body.OSImage) + assert.Equal(t, "lon1", body.Region) + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(Server{Identifier: "srv-vps", ProtocolType: "managed_vps"}) + })) + defer server.Close() + + c := newTestClient(t, server) + s, err := c.CreateServer(context.Background(), "my-app", ServerCreateRequest{ + Name: "My VPS", + ProtocolType: "managed_vps", + Region: "lon1", + Size: "s-1vcpu-1gb", + // OSImage intentionally omitted — backend defaults to ubuntu-24-04-x64 + }) + require.NoError(t, err) + assert.Equal(t, "srv-vps", s.Identifier) +} + +// TestServerProvisioningFields_Unmarshal verifies the Server type deserialises +// the nested managed_vps provisioning block from the project-scoped server show. +func TestServerProvisioningFields_Unmarshal(t *testing.T) { + raw := `{ + "identifier": "srv-vps", + "name": "My VPS", + "protocol_type": "managed_vps", + "enabled": true, + "managed_vps": { + "hosted_resource_identifier": "hr-1", + "status": "active", + "ip_address": "203.0.113.10", + "region": "lon1", + "size": "s-1vcpu-1gb" + } + }` + var s Server + require.NoError(t, json.Unmarshal([]byte(raw), &s)) + assert.Equal(t, "active", ProvisioningStatus(&s)) + require.NotNil(t, s.ManagedVPS) + assert.Equal(t, "203.0.113.10", s.ManagedVPS.IPAddress) + assert.Nil(t, s.StaticHosting) + assert.Equal(t, "http://203.0.113.10", LiveURL(&s)) +} + +func TestStaticHostingInfo_Unmarshal(t *testing.T) { + raw := `{ + "identifier": "srv-sh", + "name": "My Site", + "protocol_type": "static_hosting", + "static_hosting": { + "url": "https://my-app.deployhq-sites.com", + "subdomain": "my-app", + "status": "active" + } + }` + var s Server + require.NoError(t, json.Unmarshal([]byte(raw), &s)) + assert.Equal(t, "active", ProvisioningStatus(&s)) + require.NotNil(t, s.StaticHosting) + assert.Equal(t, "https://my-app.deployhq-sites.com", s.StaticHosting.URL) + assert.Equal(t, "my-app", s.StaticHosting.Subdomain) + assert.Nil(t, s.ManagedVPS) +} diff --git a/pkg/sdk/servers.go b/pkg/sdk/servers.go index fc37ce8..6e137af 100644 --- a/pkg/sdk/servers.go +++ b/pkg/sdk/servers.go @@ -26,9 +26,24 @@ func (c *Client) GetServer(ctx context.Context, projectID, serverID string) (*Se // CreateServer creates a new server in a project. func (c *Client) CreateServer(ctx context.Context, projectID string, req ServerCreateRequest) (*Server, error) { - body := struct { - Server ServerCreateRequest `json:"server"` - }{Server: req} + // The managed-resource provisioning params are top-level siblings of `server` + // in the request body — the backend reads params[:region], params[:os_image], + // params[:hosted_website_attributes], etc. (NOT params[:server][:region]). + // They are tagged json:"-" on ServerCreateRequest so they don't leak into the + // nested server object; hoist them here. + body := map[string]any{"server": req} + if req.HostedWebsiteAttributes != nil { + body["hosted_website_attributes"] = req.HostedWebsiteAttributes + } + if req.Region != "" { + body["region"] = req.Region + } + if req.Size != "" { + body["size"] = req.Size + } + if req.OSImage != "" { + body["os_image"] = req.OSImage + } 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/signup.go b/pkg/sdk/signup.go index 2361791..8a0e7a8 100644 --- a/pkg/sdk/signup.go +++ b/pkg/sdk/signup.go @@ -6,9 +6,12 @@ import ( "fmt" "io" "net/http" + "strings" ) // SignupRequest is the payload for creating a new DeployHQ account. +// TermsAccepted must be true — the API rejects requests where it is false or absent. +// Client should be set to "dhq-cli" so the backend can attribute signups to the CLI funnel. type SignupRequest struct { Email string `json:"email"` Password string `json:"password"` @@ -18,25 +21,43 @@ type SignupRequest struct { Coupon string `json:"coupon,omitempty"` NewsletterOptIn bool `json:"newsletter_opt_in,omitempty"` SignupSource string `json:"signup_source,omitempty"` - Client string `json:"client,omitempty"` + // Client identifies the signup origin. Use "dhq-cli" for CLI-originated signups. + Client string `json:"client,omitempty"` + // TermsAccepted must be true. The API (signups_controller.rb:32) rejects the + // request when this field is absent or false. + TermsAccepted bool `json:"terms_accepted"` } // SignupResponse is the response from creating a new account. +// EmailVerified may be false for new signups — the account and api_key are still +// valid and usable; verification is advisory only. A 422 error mentioning +// "two-factor" signals an existing account with 2FA — use TwoFactorError to +// distinguish this case and redirect the user to browser-based login. type SignupResponse struct { Account struct { Subdomain string `json:"subdomain"` Name string `json:"name"` } `json:"account"` - APIKey string `json:"api_key"` - SSHPublicKey struct { + APIKey string `json:"api_key"` + // EmailVerified is false when the signup email has not yet been confirmed. + // It is non-blocking: the api_key is returned regardless (signup_service.rb:262,346). + EmailVerified bool `json:"email_verified"` + SSHPublicKey struct { PublicKey string `json:"public_key"` Fingerprint string `json:"fingerprint"` } `json:"ssh_public_key"` } // Signup creates a new DeployHQ account. This does not require authentication. +// // userAgent is optional; when empty it defaults to "deployhq-cli". // signupURL is optional; when empty it defaults to "https://api.deployhq.com/api/v1/signup". +// +// The caller must set req.TermsAccepted = true and req.Client = "dhq-cli". +// +// Returns TwoFactorError when the API responds with 422 and the error message +// indicates an existing account with 2FA enabled — the caller should redirect +// the user to browser-based signup/login instead of retrying headlessly. func Signup(req SignupRequest, userAgent, signupURL string) (*SignupResponse, error) { body, err := json.Marshal(req) if err != nil { @@ -66,6 +87,26 @@ func Signup(req SignupRequest, userAgent, signupURL string) (*SignupResponse, er respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusCreated { + // 422 with a two-factor error message means the email belongs to an existing + // account that has 2FA enabled — the user must log in via browser. + if resp.StatusCode == http.StatusUnprocessableEntity { + var errResp struct { + Errors []string `json:"errors"` + Error string `json:"error"` + } + if json.Unmarshal(respBody, &errResp) == nil { + combined := errResp.Error + for _, e := range errResp.Errors { + combined += " " + e + } + if strings.Contains(strings.ToLower(combined), "two-factor") || + strings.Contains(strings.ToLower(combined), "two_factor") || + strings.Contains(strings.ToLower(combined), "2fa") { + return nil, &TwoFactorError{Message: strings.TrimSpace(combined)} + } + } + } + apiErr := &APIError{StatusCode: resp.StatusCode} var errResp struct { Errors []string `json:"errors"` diff --git a/pkg/sdk/signup_test.go b/pkg/sdk/signup_test.go new file mode 100644 index 0000000..f73477e --- /dev/null +++ b/pkg/sdk/signup_test.go @@ -0,0 +1,172 @@ +package sdk + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSignup_Success(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + var body SignupRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Equal(t, "user@example.com", body.Email) + assert.Equal(t, "dhq-cli", body.Client, "client must be 'dhq-cli'") + assert.True(t, body.TermsAccepted, "terms_accepted must be true") + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(SignupResponse{ + Account: struct { + Subdomain string `json:"subdomain"` + Name string `json:"name"` + }{Subdomain: "mycompany", Name: "My Company"}, + APIKey: "test-api-key", + EmailVerified: true, + }) + })) + defer server.Close() + + req := SignupRequest{ + Email: "user@example.com", + Password: "secret", + TermsAccepted: true, + Client: "dhq-cli", + } + result, err := Signup(req, "deployhq-cli", server.URL+"/api/v1/signup") + require.NoError(t, err) + assert.Equal(t, "mycompany", result.Account.Subdomain) + assert.Equal(t, "test-api-key", result.APIKey) + assert.True(t, result.EmailVerified) +} + +func TestSignup_EmailNotVerified(t *testing.T) { + // email_verified: false is non-blocking — api_key is still returned. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(SignupResponse{ + Account: struct { + Subdomain string `json:"subdomain"` + Name string `json:"name"` + }{Subdomain: "myco"}, + APIKey: "key-123", + EmailVerified: false, + }) + })) + defer server.Close() + + req := SignupRequest{Email: "user@example.com", Password: "secret", TermsAccepted: true, Client: "dhq-cli"} + result, err := Signup(req, "", server.URL+"/api/v1/signup") + require.NoError(t, err) + assert.Equal(t, "key-123", result.APIKey, "api_key must be present even when email is unverified") + assert.False(t, result.EmailVerified) +} + +func TestSignup_TwoFactorError(t *testing.T) { + // 422 with two-factor message → TwoFactorError, not generic APIError. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "errors": []string{"Email is already taken. Log in with two-factor authentication via the browser."}, + }) + })) + defer server.Close() + + req := SignupRequest{Email: "existing@example.com", Password: "secret", TermsAccepted: true, Client: "dhq-cli"} + _, err := Signup(req, "", server.URL+"/api/v1/signup") + require.Error(t, err) + + tfErr, ok := err.(*TwoFactorError) + require.True(t, ok, "expected TwoFactorError, got %T: %v", err, err) + assert.Contains(t, tfErr.Error(), "two-factor") +} + +func TestSignup_TwoFactorError_2FA(t *testing.T) { + // Variant with "2fa" in error body (case-insensitive). + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _ = json.NewEncoder(w).Encode(map[string]string{ + "error": "Account requires 2FA verification", + }) + })) + defer server.Close() + + req := SignupRequest{Email: "existing@example.com", Password: "secret", TermsAccepted: true, Client: "dhq-cli"} + _, err := Signup(req, "", server.URL+"/api/v1/signup") + require.Error(t, err) + _, ok := err.(*TwoFactorError) + require.True(t, ok, "expected TwoFactorError for 2FA error, got %T", err) +} + +func TestSignup_ValidationError(t *testing.T) { + // Non-2FA 422 → regular APIError. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _ = json.NewEncoder(w).Encode(map[string][]string{ + "errors": {"Password is too short"}, + }) + })) + defer server.Close() + + req := SignupRequest{Email: "user@example.com", Password: "x", TermsAccepted: true, Client: "dhq-cli"} + _, err := Signup(req, "", server.URL+"/api/v1/signup") + require.Error(t, err) + + // Must NOT be a TwoFactorError + _, isTFA := err.(*TwoFactorError) + assert.False(t, isTFA, "non-2FA 422 must not be a TwoFactorError") + + apiErr, ok := err.(*APIError) + require.True(t, ok) + assert.Equal(t, 422, apiErr.StatusCode) +} + +func TestSignup_TermsAccepted_SentInPayload(t *testing.T) { + // Verify the terms_accepted field is always serialised as true when set. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body map[string]interface{} + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Equal(t, true, body["terms_accepted"], "terms_accepted must be true in payload") + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(SignupResponse{ + Account: struct { + Subdomain string `json:"subdomain"` + Name string `json:"name"` + }{Subdomain: "co"}, + APIKey: "k", + }) + })) + defer server.Close() + + req := SignupRequest{Email: "u@x.com", Password: "p", TermsAccepted: true, Client: "dhq-cli"} + _, err := Signup(req, "", server.URL+"/api/v1/signup") + require.NoError(t, err) +} + +func TestSignup_UserAgentDefault(t *testing.T) { + // When userAgent is empty, the default "deployhq-cli" should be used. + var capturedUA string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedUA = r.Header.Get("User-Agent") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(SignupResponse{ + Account: struct { + Subdomain string `json:"subdomain"` + Name string `json:"name"` + }{Subdomain: "co"}, + APIKey: "k", + }) + })) + defer server.Close() + + req := SignupRequest{Email: "u@x.com", Password: "p", TermsAccepted: true} + _, err := Signup(req, "", server.URL+"/api/v1/signup") + require.NoError(t, err) + assert.Equal(t, "deployhq-cli", capturedUA, "empty userAgent must use default 'deployhq-cli'") +} diff --git a/pkg/sdk/types.go b/pkg/sdk/types.go index e8175cb..e3a0beb 100644 --- a/pkg/sdk/types.go +++ b/pkg/sdk/types.go @@ -101,6 +101,27 @@ type Server struct { UseSSHKeys bool `json:"use_ssh_keys,omitempty"` HostKey string `json:"host_key,omitempty"` UnlinkBeforeUpload bool `json:"unlink_before_upload,omitempty"` + + // Managed-resource provisioning state, populated for managed_vps and + // static_hosting servers by the project-scoped server show endpoint + // (GET /projects/:id/servers/:id). The backend nests provisioning + // detail under a protocol-specific block; use ProvisioningStatus(server) + // to read the lifecycle status uniformly across protocols. + StaticHosting *StaticHostingInfo `json:"static_hosting,omitempty"` + ManagedVPS *ManagedVPSInfo `json:"managed_vps,omitempty"` +} + +// HostedWebsiteAttributes holds Static Hosting-specific provisioning parameters. +// Set this when creating a server with protocol_type "static_hosting". +type HostedWebsiteAttributes struct { + // Subdomain is the globally unique subdomain under deployhq-sites.com. + // Example: "my-app" → https://my-app.deployhq-sites.com + Subdomain string `json:"subdomain"` + // SPAMode enables single-page-application routing (rewrites all paths to index.html). + SPAMode bool `json:"spa_mode,omitempty"` + // Subdirectory is the output folder within the server path to publish. + // Defaults to "" (the server path root) when empty. + Subdirectory string `json:"subdirectory,omitempty"` } // ServerCreateRequest is the payload for creating a server. @@ -151,6 +172,21 @@ type ServerCreateRequest struct { // Shopify StoreURL string `json:"store_url,omitempty"` ThemeName string `json:"theme_name,omitempty"` + + // Managed-resource provisioning params. These are tagged `json:"-"` because + // the DeployHQ servers API expects them as TOP-LEVEL siblings of `server` in + // the request body (params[:region], params[:hosted_website_attributes], …), + // NOT nested inside the server object. CreateServer hoists them accordingly. + + // Static Hosting (protocol_type "static_hosting") — attributes that configure + // the HostedWebsite provisioned alongside the server. + HostedWebsiteAttributes *HostedWebsiteAttributes `json:"-"` + // Region is the DigitalOcean region slug for a Managed VPS (e.g. "lon1", "nyc3"). + Region string `json:"-"` + // Size is the DigitalOcean droplet size slug (e.g. "s-1vcpu-1gb"). + Size string `json:"-"` + // OSImage is the DigitalOcean image slug (defaults to "ubuntu-24-04-x64" when empty). + OSImage string `json:"-"` } // ServerUpdateRequest is the payload for updating a server. @@ -366,3 +402,99 @@ type ListOptions struct { Page int PerPage int } + +// AccountCapabilities holds the managed-resource capability flags carried on the +// account sub-object of GET /profile. +// All authenticated account members can read /profile — it is not admin-gated. +type AccountCapabilities struct { + // BetaFeatures indicates whether the managed-resources beta is enabled for this account. + BetaFeatures bool `json:"beta_features"` + // StaticHostingEligible indicates whether the account can provision Static Hosting sites. + StaticHostingEligible bool `json:"static_hosting_eligible"` + // ManagedVPSEligible indicates whether the account can provision Managed VPS droplets. + ManagedVPSEligible bool `json:"managed_vps_eligible"` +} + +// BetaEnrollmentRequest is the body for POST /beta/enrollments. +// Protocol is optional — omit to enroll in all managed-resources protocols. +type BetaEnrollmentRequest struct { + // Protocol is the managed protocol to enroll in, e.g. "static_hosting" or "managed_vps". + // When empty the backend enrolls the account in all managed-resources protocols. + Protocol string `json:"protocol,omitempty"` +} + +// BetaEnrollmentResponse is the response from POST /beta/enrollments. +type BetaEnrollmentResponse struct { + // Enrolled is true when the account is now enrolled (was already enrolled or just flipped). + Enrolled bool `json:"enrolled"` + // BetaFeatures mirrors the account's beta_features flag after the operation. + BetaFeatures bool `json:"beta_features"` +} + +// ManagedHostingRegion is a DigitalOcean region available for Managed VPS provisioning. +type ManagedHostingRegion struct { + // Slug is the region identifier used in ServerCreateRequest.Region (e.g. "lon1"). + Slug string `json:"slug"` + // Name is the human-readable region name (e.g. "London, United Kingdom"). + Name string `json:"name"` + // Available indicates whether new droplets can currently be created in this region. + Available bool `json:"available"` +} + +// ManagedHostingSize is a DigitalOcean droplet size available for Managed VPS provisioning. +type ManagedHostingSize struct { + // Slug is the size identifier used in ServerCreateRequest.Size (e.g. "s-1vcpu-1gb"). + Slug string `json:"slug"` + // Description is the human-readable size label (e.g. "1 vCPU / 1 GB RAM"). + Description string `json:"description"` + // PriceMonthly is the monthly cost in the account's billing currency. + PriceMonthly float64 `json:"price_monthly"` + // PriceHourly is the hourly cost in the account's billing currency. + PriceHourly float64 `json:"price_hourly"` + // Memory is the RAM in megabytes. + Memory int `json:"memory"` + // VCPUs is the number of virtual CPUs. + VCPUs int `json:"vcpus"` + // Disk is the root disk size in gigabytes. + Disk int `json:"disk"` +} + +// ManagedVPSInfo is the nested `managed_vps` object within a server response for +// managed_vps servers. It carries the provisioning state and droplet +// detail that live on the backing HostedResource rather than on the server row. +type ManagedVPSInfo struct { + // HostedResourceIdentifier is the opaque identifier of the backing HostedResource. + HostedResourceIdentifier string `json:"hosted_resource_identifier"` + // Status is the provisioning lifecycle status, e.g. "provisioning", "active", "error". + Status string `json:"status,omitempty"` + // IPAddress is the public IP of the droplet once active. + IPAddress string `json:"ip_address,omitempty"` + // Region is the DigitalOcean region slug the droplet runs in. + Region string `json:"region,omitempty"` + // Size is the DigitalOcean droplet size slug. + Size string `json:"size,omitempty"` + // MonthlyCost is the droplet's monthly cost (string or number on the wire). + MonthlyCost FlexString `json:"monthly_cost,omitempty"` +} + +// StaticHostingInfo is the nested `static_hosting` object within a server response for static_hosting servers. +type StaticHostingInfo struct { + // URL is the live public URL of the Static Hosting site (e.g. "https://my-app.deployhq-sites.com"). + URL string `json:"url"` + // Subdomain is the subdomain portion of the URL. + Subdomain string `json:"subdomain"` + // Status is the provisioning status of the underlying HostedWebsite. + Status string `json:"status,omitempty"` +} + +// TwoFactorError is a sentinel returned by Signup when the API responds with 422 +// and the error body indicates an existing account with 2FA enabled. +// The caller should redirect the user to browser-based login. +type TwoFactorError struct { + // Message is the human-readable error from the API. + Message string +} + +func (e *TwoFactorError) Error() string { + return "two-factor authentication required: " + e.Message +} diff --git a/script/update-openapi-fixture.sh b/script/update-openapi-fixture.sh new file mode 100755 index 0000000..76037a2 --- /dev/null +++ b/script/update-openapi-fixture.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# +# Refresh the committed OpenAPI fixture used by the spec-validating test client +# (internal/commands/openapi_validation_test.go). +# +# The fixture is a snapshot of the backend's generated OpenAPI document. Tests +# validate every request the SDK sends against it, so it must be refreshed — +# and the diff reviewed/committed — whenever the backend API changes. +# +# Usage: +# script/update-openapi-fixture.sh # local dev backend +# DHQ_SPEC_URL=https://host/docs.json script/update-openapi-fixture.sh +# +# DHQ_SPEC_URL is the same variable the drift-check workflow uses +# (.github/workflows/openapi-drift.yml); DHQ_DOCS_URL is accepted as a legacy +# alias. Requires a running backend (local: `bin/dev` in the deployhq repo). +# Never run in CI — tests read only the committed fixture, keeping them hermetic. + +set -euo pipefail + +DOCS_URL="${DHQ_SPEC_URL:-${DHQ_DOCS_URL:-https://api.deploy.localhost/docs.json}}" +FIXTURE="$(cd "$(dirname "$0")/.." && pwd)/internal/commands/testdata/openapi.json" + +echo "Fetching ${DOCS_URL} ..." +tmp="$(mktemp)" +# -k: local dev uses a self-signed certificate. First hit can be slow (cold boot +# + OAS generation), hence the generous timeout. +curl -skf --max-time 180 "${DOCS_URL}" -o "${tmp}" + +# Sanity check: must be an OpenAPI document, not an error page. +if ! head -c 100 "${tmp}" | grep -q '"openapi"'; then + echo "error: response does not look like an OpenAPI document" >&2 + head -c 300 "${tmp}" >&2 + rm -f "${tmp}" + exit 1 +fi + +mv "${tmp}" "${FIXTURE}" +echo "Wrote $(wc -c <"${FIXTURE}" | tr -d ' ') bytes to ${FIXTURE}" +echo "Review the diff and commit the fixture together with any SDK changes." diff --git a/skills/deployhq/SKILL.md b/skills/deployhq/SKILL.md index 94dbe2a..d50335b 100644 --- a/skills/deployhq/SKILL.md +++ b/skills/deployhq/SKILL.md @@ -57,6 +57,7 @@ The only commands that **cannot** run non-interactively are: `dhq init`, `dhq he | Group | Description | Reference | |-------|-------------|-----------| +| **launch** | One command: provision + deploy to DeployHQ Static Hosting or a Managed VPS | [launch.md](references/launch.md) | | **projects** | Create, list, update, delete projects | [projects.md](references/projects.md) | | **servers** | Manage deployment targets (SSH, FTP, S3, etc.) | [servers.md](references/servers.md) | | **deployments** | Create, monitor, rollback deployments | [deployments.md](references/deployments.md) | @@ -68,6 +69,14 @@ The only commands that **cannot** run non-interactively are: `dhq init`, `dhq he ## Decision Trees +### "Deploy my project in one command (managed hosting)" +The fastest path — provisions DeployHQ Static Hosting or a Managed VPS and deploys in one go: +1. `dhq launch --json` — auto-detects the framework, picks a target, provisions, deploys, returns the live URL +2. Force a target: `dhq launch --static --subdomain my-app --json`, or `dhq launch --vps --accept-cost --region lon1 --json` (a Managed VPS is a managed resource, free for early customers during beta and billed monthly afterwards — `--accept-cost` is required non-interactively) +3. Preview cost/actions with no side effects: `dhq launch --vps --dry-run --json` + +See [launch.md](references/launch.md). After `launch` writes `.deployhq.toml`, use `dhq deploy` for subsequent deploys. + ### "Deploy code" 1. `dhq projects list --json` — find project permalink 2. `dhq servers list -p --json` — find server identifier @@ -133,4 +142,5 @@ dhq api POST /projects//deployments --body '{"deployment":{...}}' - User mentions "rollback", "revert", "undo" → rollback workflow - User mentions "environment variable", "env var", "config", "secret" → configuration - User mentions "branch", "commit", "repository" → repo management +- User mentions "one command", "just deploy this folder", "managed hosting", "managed VPS", "static hosting", "provision and deploy" → `dhq launch` (see [launch.md](references/launch.md)) - User mentions "DeployHQ", "deployhq", "dhq" → general CLI usage diff --git a/skills/deployhq/references/launch.md b/skills/deployhq/references/launch.md new file mode 100644 index 0000000..8518a96 --- /dev/null +++ b/skills/deployhq/references/launch.md @@ -0,0 +1,77 @@ +# Launch Reference (`dhq launch`) + +One command that takes a project folder from nothing to a live URL on DeployHQ's +own infrastructure — **Static Hosting** (Cloudflare-backed) or a **Managed VPS** +(DeployHQ-provisioned DigitalOcean droplet). It runs: auth → framework detection +→ beta enrollment (if needed) → repo check → target selection → project/server +creation → provisioning → deploy → prints the live URL → saves `.deployhq.toml`. + +Use `dhq launch` for first-time setup of a managed target. Use `dhq deploy` for +subsequent deploys once `.deployhq.toml` exists (launch writes it). + +## Usage + +```bash +# Interactive cold start (prompts as needed) +dhq launch + +# Force a target +dhq launch --static +dhq launch --vps + +# Agent / CI (structured JSON result on stdout) +dhq launch --static --subdomain my-app --json +dhq launch --vps --accept-cost --region lon1 --size s-1vcpu-1gb --json + +# Inspect intended actions + cost without doing anything (no side effects) +dhq launch --vps --dry-run --json +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--static` | Force Static Hosting target (skips the target prompt) | +| `--vps` | Force Managed VPS target | +| `--subdomain` | Static Hosting subdomain (default: repo / project name) | +| `--region` | Managed VPS region slug (e.g. `lon1`, `nyc3`). List via `dhq api GET /managed_hosting/regions` | +| `--size` | Managed VPS size slug (e.g. `s-1vcpu-1gb`). List via `dhq api GET /managed_hosting/sizes` | +| `--accept-cost` | Acknowledge Managed VPS provisioning — free for early customers during beta, billed monthly afterwards. **Required** for non-interactive VPS provisioning | +| `--branch` | Branch to deploy (default: repo default) | +| `--project` | Existing project permalink to reuse (skips project creation) | +| `--cleanup-on-failure` | Delete the provisioned server if the deploy fails (prevents orphaned managed resources) | +| `--non-interactive` / `--yes` | Never prompt; fail fast with structured errors. Auto-enabled for agents / piped output | +| `--interactive` | Force prompts even in a piped / agent context | +| `--dry-run` | Print intended actions + monthly cost, do nothing | +| `--json` | Structured result on stdout (global flag) | + +## Agent / non-interactive contract + +- **Config precedence:** flags → env (`DEPLOYHQ_*`) → `.deployhq.toml` → framework detection. A missing required value with no TTY fails fast naming the exact flag — it never hangs. +- **Cost guardrail:** a Managed VPS is a managed resource (free for early customers during beta, billed monthly afterwards); in non-interactive mode it is **never** provisioned without `--accept-cost` (`--yes` alone is not enough). Static Hosting is safe under `--yes`. +- **`--dry-run`** emits `{would, requires, warning}` and makes no changes — use it to preview cost and confirm before provisioning a Managed VPS. +- **Success (`--json`)** emits one object: `{status, target, url, project, server, deployment}`. In plain mode the final stdout line is the live URL. +- **Idempotent:** re-running reads `.deployhq.toml` and resolves the existing project/server instead of double-provisioning. + +### Structured error reasons + +On failure the error carries a stable `reason`, a `retryable` boolean, and a `next_step` an agent can branch on: + +| Reason | Meaning / next step | +|--------|---------------------| +| `auth_required` | No credentials in non-interactive mode — set `DEPLOYHQ_*` env vars (signup is interactive-only) | +| `beta_enroll_required` | Managed-resources beta not enabled and the user isn't an admin — `details.admin_required=true`; an admin enables it (or use your own server via `dhq init`) | +| `accept_cost_required` | Managed VPS requested non-interactively without `--accept-cost` — re-run with `--accept-cost` | +| `repo_unreachable` | No git remote DeployHQ can deploy from — push a remote / connect a provider first | +| `plan_limit_reached` | Free-plan limit hit (e.g. 1 static site) — upgrade or remove an existing resource | +| `subdomain_taken` | Static Hosting subdomain already in use — choose another `--subdomain` | +| `rate_limited` | Per-account provisioning rate limit hit (HTTP 429) — **retryable** (`retryable: true`); back off for `details.retry_after` seconds and re-run the same command. Distinct from `plan_limit_reached` (a hard 422 cap) | +| `provision_failed` | The server failed to provision — check the named resource; retry | +| `deploy_failed` | Provisioning succeeded but the deploy failed — the managed-resource server is named with its teardown command; `--cleanup-on-failure` removes it automatically | + +## Notes + +- Static Hosting deploys are git-based (a connected repo + DeployHQ's build pipeline). `launch` connects the repo for you; it does not yet upload a local build directory. +- **Rollback:** both targets roll back the same way — `dhq rollback ` redeploys the previous revision through the same pipeline (`dhq deployments list` shows history). There's no separate static-vs-VPS rollback command. +- Managed VPS and Static Hosting require the **managed-resources beta**. `launch` enrolls the account automatically when an admin runs it (the enrollment endpoint is idempotent and admin-gated); non-admins get `beta_enroll_required`. +- **Pricing during beta:** Managed VPS and Static Hosting are free for early customers while in beta; the listed monthly rate applies once the beta ends. The CLI's runtime copy is gated by a single switch (`meteredResourcesInBeta` in `internal/commands/metered.go`) — flip it when the resources go GA, and update this beta wording in the same change. diff --git a/skills/deployhq/references/servers.md b/skills/deployhq/references/servers.md index eee1d80..21d20ef 100644 --- a/skills/deployhq/references/servers.md +++ b/skills/deployhq/references/servers.md @@ -25,10 +25,26 @@ Create a server. Flags vary by protocol type. | Flag | Required | Description | |------|----------|-------------| | `--name` | yes | Server name | -| `--protocol-type` | yes | One of: ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean, hetzner_cloud, heroku, netlify, shopify | +| `--protocol-type` | yes | One of: ssh, ftp, ftps, rsync, s3, s3_compatible, digitalocean, hetzner_cloud, heroku, netlify, shopify, static_hosting (beta), managed_vps (beta) | | `--path` | no | Deployment path | | `--environment` | no | Environment label | +**Static Hosting flags (beta, requires managed-resources beta on account):** + +| Flag | Description | +|------|-------------| +| `--subdomain` | Globally unique subdomain under deployhq-sites.com | +| `--spa-mode` | Enable SPA routing (rewrites all paths to index.html) | +| `--subdirectory` | Output subdirectory to publish (e.g. dist) | + +**Managed VPS flags (beta, requires managed-resources beta on account):** + +| Flag | Description | +|------|-------------| +| `--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) | + **SSH/FTP/FTPS/Rsync flags:** | Flag | Description | @@ -78,6 +94,16 @@ dhq servers create -p my-app --name "Static Assets" --protocol-type s3 \ # Netlify dhq servers create -p my-app --name Netlify --protocol-type netlify \ --site-id abc123 --access-token ... --json + +# Static Hosting site (beta) +# Note: requires managed-resources beta enabled; use `dhq launch` for guided setup +dhq servers create -p my-app --name "My Site" --protocol-type static_hosting \ + --subdomain my-app --subdirectory dist --json + +# Managed VPS (beta) +# Note: requires managed-resources beta enabled; use `dhq launch` for guided setup +dhq servers create -p my-app --name "My VPS" --protocol-type managed_vps \ + --region lon1 --size s-1vcpu-1gb --json ``` ### `dhq servers update `