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:
- Build identity — the running release string equals the pushed commit, on every replica; the single most valuable check
- Process health — the health route returns its expected body and the response time is within the usual range
- A real page — the most important page renders server-side with data and contains a string that only the real data path produces
- A static asset — one hashed asset from the new build is served with the right content type and cache header
- The auth boundary — a protected route refuses or redirects an anonymous request
- Sign-in — a dedicated smoke account signs in and reads its own profile; the account has no privileges and no real data
- One read — the main list or detail route returns the expected shape with at least one row
- 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
- One job heartbeat — the last-run timestamp of a scheduled job is within its interval
- 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.