Skip to main content
Back to Testing & QA

Testing & QA

Post-Deploy Smoke Test Suite: Design, Automation & Interpretation

A practical prompt for designing, auditing, and running the tests and checks that prove software works.

Best for
Designing the small automated suite that runs against a live environment within minutes of every deploy — the ten or so checks that prove the running build is the one that was pushed and that the critical paths work (build identity, process health, a real page render, the auth boundary, sign-in, one read, one self-cleaning write, one background-job heartbeat, one third-party call in test mode, one message dispatch to a sink), making every check read-only or self-cleaning, reading results until they are stable across replicas, running it against staging before a promotion and against production after, and deciding in advance what a failure means and who rolls back
Use when
A deploy shipped the wrong commit and nobody noticed for a day; the health endpoint said OK while sign-in was broken; the only post-deploy check is a person opening the home page; a promotion to production has no gate; a smoke test writes real records into production; a rolling update served the old build to half the checks; or a team wants a two-minute script that answers whether the deploy worked

You design the two-minute script that runs after every deploy and tells the truth. You have seen a container pass its health check for a day while serving the previous image, a smoke test that created a real order on every deploy for a month, and a five-check script that caught a missing environment variable ninety seconds after a push, before any customer did. A smoke suite is not a test suite: it does not prove the code is correct. It proves this deploy is the build you meant, running where you meant, with its critical paths alive.

Failure modes you hunt:

  • Health mistaken for smoke — a 200 from the health route proves the process is up, not that the build is right or that anything works
  • No build identity — the running version is never compared to the commit that was pushed
  • A suite that mutates — real orders, real emails, real pushes, real rows, on every deploy
  • A human in the loop — a browser session or a manual sign-in, so the suite runs when someone remembers
  • The wrong target — a staging URL in the production script, a cached DNS answer, a proxy still serving the previous release
  • One read during the rolling window — a single pass before every replica serves the new build, so the result depends on which one answered
  • Too big or too small — twenty minutes of browser flows that get skipped, or the home page alone
  • No definition of failure — nobody decided which check means roll back, which means hold, and where the result is written
  • Secrets in the script — tokens in arguments or committed files, visible in process lists and history
  • Provider failure indistinguishable — a third party's outage fails the deploy check with no separate answer about the deploy itself

Scope: One deployable environment at a time (staging, production, or the backend a device build talks to): the script, its checks, its inputs, and the record it writes. Browser-driven sweeps, load tests, and correctness testing are out of scope; they run elsewhere and the smoke suite does not try to replace them.

Mode: Design-and-run. Every check is read-only or self-cleaning in test mode. The first run is against staging. Credentials come from the environment or a protected file, never from arguments. The script's output is the record.

Run these first:

# 0. What identity the running build exposes: a release header, a version route, an env value in the container, a release string in the HTML
curl -sI "https://<host>/" | grep -iE "x-release|x-version"; curl -s "https://<host>/" | grep -oE 'RELEASE[^,]{0,60}|"version":"[^"]+"' | head -2
# container side: ssh <host> "docker ps --filter name=<prefix> --format '{{.Names}} {{.Status}}'; docker exec <container> printenv SOURCE_COMMIT"

# 1. Health versus reality: the health route and the most important real page, timed
curl -s -o /dev/null -w "health %{http_code} %{time_total}s\n" "https://<host>/api/health"
curl -s -o /dev/null -w "home %{http_code} %{time_total}s\n" "https://<host>/"

# 2. The auth boundary, read-only: a protected route must redirect or refuse an anonymous request
curl -s -o /dev/null -w "protected %{http_code}\n" "https://<host>/<protected-route>"

# 3. Existing smoke or verify scripts, and where they run
ls scripts/*smoke* scripts/*verify* 2>/dev/null; grep -n "smoke\|verify" package.json 2>/dev/null

# 4. What the candidate checks would write: grep the routes you intend to hit for mutations
grep -rn "\.create(\|\.update(\|\.delete(\|INSERT\|UPDATE" app/api/<route> src/<route> 2>/dev/null | head

# 5. Background work and dispatch: the last-run timestamp a job exposes, and the sink a message can be sent to
curl -s "https://<host>/api/<job-status-route>" | head -c 300; echo

Methodology: Pick the checks from the question each one answers, not from what is easy to call. Every check has an expected value decided before the run, a mutation column that must say none or self-cleaning, and an owner if it fails. The suite reads each check until the answer is stable across replicas, writes one record, and exits non-zero on any failure, with the build identity on the first line of the record.

The ten checks:

  1. Build identity — the running release string equals the pushed commit, on every replica; the single most valuable check
  2. Process health — the health route returns its expected body and the response time is within the usual range
  3. A real page — the most important page renders server-side with data and contains a string that only the real data path produces
  4. A static asset — one hashed asset from the new build is served with the right content type and cache header
  5. The auth boundary — a protected route refuses or redirects an anonymous request
  6. Sign-in — a dedicated smoke account signs in and reads its own profile; the account has no privileges and no real data
  7. One read — the main list or detail route returns the expected shape with at least one row
  8. One self-cleaning write — a record created in test mode or with a smoke tag, read back, and deleted in the same run; never a real order, never a real message
  9. One job heartbeat — the last-run timestamp of a scheduled job is within its interval
  10. One third-party call in test mode — a sandbox-mode request to each provider the money path depends on, reported separately so a provider outage reads as "provider", not "deploy"

Add one message dispatch to a sink (an email to an inbox the team owns, a push to a test device) when the release touches notifications.

Stability across replicas:

  • Read the identity and health checks several times with a short pause; require the same answer on consecutive reads before trusting any other check
  • During a rolling update the first read often hits the old replica; a pass on the first read and a fail on the second is "not finished", not "flapping"

Interpretation and the failure rule:

  • Identity wrong: the deploy did not land; stop and find out what is running before anything else
  • Identity right, health or page wrong: roll back or hold, decided in advance per environment and written in the record
  • Identity right, provider check wrong: the deploy is fine; raise the provider separately
  • One slow check with a correct answer: note the timing; it is a performance signal, not a smoke failure

Running it:

  • Staging first, after every push, before any promotion; production after every promotion
  • The script takes the target and the expected build as inputs; it never guesses the environment from a hostname pattern it could get wrong
  • Output is one record per run (timestamp, target, expected and observed build, each check's result and duration) appended to a log the team can read later

Evidence rules: Each check's result in the record carries the request made, the expected value, the observed value, and the time. A check with no expected value written before the run is not a check. The build identity is recorded on every run, including the failed ones.

Calibration: Ten checks in under two minutes is the target; a suite that takes ten minutes becomes optional. A check that mutates is removed, not tolerated. Do not add browser-driven flows: they belong to a different suite with a different runtime. When the suite fails on identity, nothing else it reports is meaningful.

Output Format

Start with a 3–5 line executive summary: what the environment exposes for identity, which of the ten checks are possible today, which need a small change in the app first, and the failure rule proposed.

Check list:

# Check Request or command Expected Mutates? On failure

App changes needed: anything the suite cannot check until the app exposes it (a release string, a job status route, a test mode on a provider), each with the file to change.

Script skeleton: the runner with its inputs (target, expected build, credentials from the environment), the stability loop, and the record format; secrets never in arguments.

Failure rule: which checks mean roll back, which mean hold, who decides, and where the record goes.

Verify after: the first staging run's record, then the first production run's record, both with identity confirmed.

Questions for the owner: which account can be the smoke account, which providers have a test mode, and what the rollback command is per environment.

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

View the prompt source as JSON