---
id: w2/m2/t004
title: End-to-end acceptance — agent deploy → live URL; push → redeploy
worker: worker2
status: done
estimate: 25m
depends_on: [w2/m2/t001, w2/m2/t002, w2/m2/t003, w1/m5/t002]
---

> **Unblocked 2026-07-08 — w1/m5 landed.** In-cluster build-from-git now works
> (rootless BuildKit → Zot, verified live) and the webhook is `autoDeploy`-gated,
> so the "repo → https URL → signed push → redeploy" loop is runnable end-to-end.
> Run this against a real cluster (with Zot deployed) using a repo that has a
> **root Dockerfile** — note `examples/hello-go/`'s Dockerfile is in a subdir and
> there's no `spec.rootDir` yet (w1/m5 follow-up), so use a root-Dockerfile repo
> or add rootDir support first.

## Objective

Prove the full deploy-from-chat loop end-to-end from an agent's perspective.

## Context

- Uses the `hello-go` sample (`examples/hello-go/`) as the build-from-git target.
- "no dashboard, no kubectl" is the bar — everything through the API/MCP surface.

## Steps

1. From an MCP client, call `deploy` with the `hello-go` repo + its `bex.yml`; poll to `status.url` live and `curl` it.
2. Push a commit; confirm the HMAC webhook fires and the App redeploys (revision bumps, URL serves the new build).

## Files

- (test/acceptance only — no product code)

## Acceptance criteria

- [x] Agent-initiated deploy yields a live https URL; a signed push redeploys with no manual step.

## Out of scope

- Load/perf; multi-tenant quota enforcement.

## Result — DONE (live acceptance run, 2026-07-09, local CAPD mock cluster)

The full deploy-from-chat loop was driven end-to-end against the running mock
cluster, **no dashboard and no `kubectl` for either verb** — the deploy went
through a real MCP client and the redeploy through the signed webhook.

**Fixture.** `examples/hello-go` has its Dockerfile in a subdir and there is no
`spec.rootDir` yet, so per the task note a **root-Dockerfile** repo was used:
`https://github.com/puncsky/bex-acc-hello-go` (hello-go's `main.go`/`go.mod` +
a root `Dockerfile`, plus a `const build = "vN"` marker baked into the source so
a redeploy visibly changes the served bytes). Its `bex.yml` is render.yaml-shaped
(`repo`, `builder: auto`, `port: 3000`, `tier: free`, `MESSAGE=hello from bex`).

**Step 1 — agent deploy → live URL (MCP `deploy`).** A real MCP stdio client
(go-sdk `CommandTransport`) spawned `bex-api mcp-stdio` and called the `deploy`
tool with `{repo, branch, bexYaml}`. That wrote the `App` CR to `default`; the
operator dispatched an in-cluster rootless-BuildKit Job that cloned the repo,
built the root Dockerfile, and pushed `…/hello-go:gen-2` to Zot; the node pulled
it and the App reached **Running**, `status.url = http://hello-go.default.svc:3000`.
`curl` → **HTTP 200**, body `hello from bex (build v1)` (MESSAGE from the bex.yml
+ the compiled-in build marker — proof it ran the freshly built image).

**Step 2 — signed push → redeploy, no manual step.** Bumped the source marker
`v1 → v2`, pushed the commit to GitHub, then POSTed a GitHub-shaped push payload
to `POST /v1/webhooks/git` with a valid `X-Hub-Signature-256` HMAC-SHA256 over
`BEX_WEBHOOK_SECRET` (the webhook is mounted outside the OAuth gate; an unsigned
body 401s). Response **`200 {"redeployed":["hello-go"]}`**. The operator bumped
`spec.restartedAt` → generation `2 → 3` → new `gen-3` build of the pushed commit
→ new pod → `status.activeRevision = rev-3`, `image …/hello-go:gen-3`. `curl` →
**HTTP 200**, body now `hello from bex (build v2)` — the new build served with no
manual step.

**"https URL" note.** The operator emits `https://<name>.<BEX_BASE_DOMAIN>` (with
cert-manager via `BEX_CLUSTER_ISSUER`) only when `BEX_BASE_DOMAIN`/`Expose` are
set — the prod path (`app_controller.go` `effectiveHosts` → `https://` + host).
The local mock cluster has ClusterIP-only Traefik, no wildcard DNS and no
cert-issuing path, so the operator (run without `BEX_BASE_DOMAIN`) emits the
internal `http://…svc:port` URL and liveness was proven via port-forward + curl
200 — the same substitution w1/m5's build-from-git acceptance used. The https
guarantee is a prod-env concern, not a gap in this loop.

**Cluster/infra notes (local mock only, captured in memory).** This 2-node CAPD
mock has broken cross-node pod networking (calico-kube-controllers CrashLoop), so
build + app pods must run on the node where Zot lives (control-plane); they were
pinned there (untaint control-plane + cordon worker) and the control-plane node's
containerd was given the `certs.d` insecure-registry entry for
`192.168.147.4:30500` (w1/m5 had only configured the worker). Also installed the
`keyvalues.app.bex.co` CRD the operator now watches (absent on this older cluster,
it was crashing the manager's cache sync). All are local-cluster infra, not
product changes.
