# Post Deploy Smoke

> After a deploy, confirm the live URL actually serves the expected content (not just that the API returned 200) by polling it with a bounded retry loop that greps for an expected string per URL. Use after any deploy to verify propagation, after a rebrand/rename to confirm the new content is live, or before telling the user a deploy is "done".

- Skill: `jcdavis131/post-deploy-smoke` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jcdavis131/post-deploy-smoke`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jcdavis131/post-deploy-smoke/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: jcdavis131 (https://skillmd.com/u/jcdavis131)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/jcdavis131/post-deploy-smoke

---


# Post-Deploy Smoke

A deploy isn't "done" when the build succeeds or the API returns 200. Vercel/edge deploys propagate with a delay; a 200 can come from a cached old version. Done is when the live URL serves the *expected content*. Confirm it with a bounded retry loop that greps for an expected string.

## The shape

```bash
export TERM=dumb
for i in 1 2 3 4 5 6 7 8; do
  ok=1
  for u in "https://hq.example.com|Headquarters" "https://www.storefront.example.com|Storefront"; do
    url="${u%%|*}"; want="${u##*|}"
    hit=$(curl -sL --max-time 20 "$url" | grep -c "$want")
    [ "$hit" -ge 1 ] || ok=0
  done
  [ "$ok" = 1 ] && break
  sleep 5
done
```

Why each piece earns its place:
- **`export TERM=dumb`** — stop terminal control sequences from polluting the scripted output.
- **Bounded retry (`for i in 1..8`)** — a deploy that never propagates shouldn't loop forever. 8 × (curl + sleep) is a reasonable propagation window.
- **`url|want` pairs** — each URL has an expected string ("Headquarters" on hq.example.com, "Storefront" on storefront.example.com). The expected string is content only the *new* deploy would serve, so a match confirms propagation, not a cached 200.
- **`curl -sL --max-time 20`** — silent, follow redirects, bound the request so a hung edge doesn't hang the loop.
- **`grep -c "$want"`** — count matches; `[ "$hit" -ge 1 ]` confirms the expected content is present.
- **All URLs must match (`ok` flag)** — partial propagation (one URL live, one stale) is not done; require all.
- **`break` on success** — stop as soon as all match; don't run all 8 iterations when it propagated on iteration 2.

## Choosing the expected string

Pick a string that:
- Is **only in the new deploy** — so a match rules out the cached old version.
- Is **stable** — not a timestamp, not a random id.
- Is **content, not boilerplate** — a page title, a hero headline, a nav label. "Headquarters" / "Storefront" are good; `<html>` is bad (every deploy has it).

For a rebrand, the expected string is the *new* brand name or label — confirming the rename propagated to the live site.

## When to run

- After every deploy you'll tell the user is "done".
- After a rebrand/rename — confirm the new content is live.
- Before a review handoff — so the URLs you hand over actually serve the new content.

## When NOT to run

- A deploy to a preview URL with no propagation delay — a single curl is enough.
- A backend API deploy with no user-facing content — check the health endpoint instead.

## The backend variant: poll a health endpoint

For a backend deploy (API, worker, service), the "expected content" is the health endpoint appearing. Instead of grepping for a string, poll `/readyz` (or `/health`, `/healthz`) until it returns 200 — signaling the new image is serving:

```
Watching for the new image to go live (/readyz appearing = current build)
```

- The health endpoint returning 200 confirms the new image is up and ready to serve, not just that the deploy API accepted the push.
- A deploy that's "ready" per the platform but whose `/readyz` never appears is a stuck deploy — escalate to `diagnose-before-retry`.
- This is the event-driven form of the smoke loop: arm a monitor on `/readyz` appearing, with a timeout, rather than polling on a fixed cadence (see `event-driven-wait`).

## Anti-patterns

- **"Deploy returned 200, we're done."** A cached 200 from the old deploy isn't propagation.
- **Unbounded retry.** A deploy that never propagates loops forever.
- **Expected string that's in every deploy.** `<html>` matches the old version too.
- **One URL checked, declared done.** Partial propagation is not done.
- **No `--max-time` on curl.** A hung edge hangs the whole loop.

## Pair with

- `use-available-integrations` — the smoke confirms the integration's deploy actually served.
- `event-driven-wait` — a smoke loop is a bounded event-driven wait on "expected content appeared".
- `diagnose-before-retry` — if the smoke fails after 8 iterations, diagnose (check the deploy logs, the build output) before re-deploying.
- `close-the-loop` — the smoke is the proof that "deployed" = "live and serving expected content" before the closure.

