Skip to content

Add deploy API and unify with the webhook into deploy-server#14

Merged
miharp merged 1 commit into
mainfrom
feat/deploy-api
Jul 24, 2026
Merged

Add deploy API and unify with the webhook into deploy-server#14
miharp merged 1 commit into
mainfrom
feat/deploy-api

Conversation

@miharp

@miharp miharp commented Jul 24, 2026

Copy link
Copy Markdown
Owner

The last Code Manager reflex is the programmatic deploy: POST a deploy, get an id, poll status. This adds it — and unifies it with the webhook into one control plane, because both need one shared history ("what deployed, however triggered") and one worker (so they don't run r10k over each other).

internal/deployserver

Owns the deploy queue, a single worker, and an in-memory bounded history, routing both front doors:

Route Auth
POST /v1/deploys Bearer token trigger; returns a record with an id, or blocks with {"wait":true}
GET /v1/deploys/{id} Bearer token one deploy's status
GET /v1/deploys Bearer token history, newest first
POST /v1/webhook shared secret push → deploy, now recorded too

A record carries status (queued/running/complete/failed), source (api or webhook), per-environment code_id and commit, and lifecycle timestamps. complete means the deploy landed on the primary (r10k ran, sealed, publisher signaled); compilers converge by polling, so the record doesn't wait on them.

$ curl -H 'Authorization: Bearer <token>' -d '{"environments":["production"],"wait":true}' \
    https://primary:8170/v1/deploys
{ "id":"e210f6…", "status":"complete", "source":"api",
  "results":[{"environment":"production","code_id":"778ae2…","commit":"cafe123"}] }

Unification

internal/webhook shrinks to a pure request parser (Authenticate + Environment); its queue and worker move into deployserver, so there's exactly one of each. The command becomes deploy-server (API when --api-token is set, webhook when --secret is); codavox webhook stays as an alias running it webhook-only, so nothing that shipped breaks.

Auth

Bearer token from a file (constant-time), matching Code Manager's token model and working for CI with no Puppet cert. TLS client-auth is per-connection not per-route, so mixing mTLS with the webhook's no-client-cert TLS on one listener isn't possible — application-level auth is the right fit. Server TLS uses the node cert; --no-tls for behind-a-proxy.

Safety

Serialized deploys: the single worker plus the staging flock from #13 mean an API deploy, a webhook deploy, and a CLI codavox deploy never overlap r10k. An end-to-end smoke confirmed the API deploys, GET /v1/deploys shows a unified api+webhook history, and a missing token → 401.

Tests

deployserver against a fake Deployer (no r10k): async and wait, all vs environments, token missing/wrong, validation, get/list/404, failed-deploy recording, disabled-route 404, and the webhook route recording into the shared history. A race on the async response — handler reading the record pointer the worker was mutating — is fixed by returning a locked snapshot from submit, verified under -race.

Docs: webhooks.mddeploy-server.md (API + webhook + records); commands.md/README updated.

Verified: go test -race ./..., golangci-lint 0 issues, gofmt/vet clean, markdownlint 0 issues.

🤖 Generated with Claude Code

The last Code Manager reflex customers reach for is the programmatic deploy:
POST a deploy, get an id, poll status. This adds it — and unifies it with the
webhook into one control plane, because both must share one deploy history to
answer 'what deployed, however it was triggered', and one worker so they do not
run r10k over each other.

New internal/deployserver owns the deploy queue, a single worker, and an
in-memory bounded history, and routes both front doors:

  POST /v1/deploys            trigger a deploy (bearer token); returns a record
                              with an id, or blocks with {"wait":true}
  GET  /v1/deploys/{id}       one deploy's status
  GET  /v1/deploys            history, newest first
  POST /v1/webhook            push -> deploy (shared secret), now recorded too

A record carries status (queued/running/complete/failed), source (api or
webhook), per-environment code_id and commit, and lifecycle timestamps. complete
means the deploy landed on the primary — r10k ran, sealed, publisher signaled;
compilers converge by polling, so the record does not wait on them.

internal/webhook shrinks to a pure request parser (Authenticate + Environment);
its queue and worker move into deployserver, so there is exactly one of each.
The command becomes deploy-server, enabling the API when --api-token is set and
the webhook when --secret is; codavox webhook stays as an alias that runs it
with only the webhook, so nothing that shipped breaks.

Auth is a bearer token from a file (constant-time), matching Code Manager's
token model and working for CI with no Puppet cert; TLS client-auth is
per-connection not per-route, so mixing it with the webhook's no-client-cert TLS
on one listener is not possible, and application-level auth is the right fit.

Tested against a fake Deployer with no r10k: async and wait, all vs
environments, token missing/wrong, validation, get/list/404, failed-deploy
recording, disabled-route 404, and the webhook route recording into the shared
history. A race on the async response — the handler read the record pointer the
worker was mutating — is fixed by returning a locked snapshot from submit.

Signed-off-by: Michael Harp <mike@mikeharp.com>
@miharp
miharp merged commit dd9ea0d into main Jul 24, 2026
7 checks passed
@miharp
miharp deleted the feat/deploy-api branch July 24, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant