Deploy smoke
A custom DSL run that hits critical URLs after a successful deploy. The GitHub App webhook fires it directly — zero GHA minutes, no workflow file; an Action-mode `ci.yml` is included as the GHA-native alternative.
Fires on After each deploy — wire it any of these ways:
- Webhook recommended The GitHub App webhook fires the run directly — no .github/workflows file, no GHA minutes. (fires on deployment_status.success)
- Action A ci.yml step dispatches the run — when it must interleave with other CI jobs or gate the PR.
- Use case
- Hit critical URLs after a successful deploy
- FlareDispatch shape
- 1 typed run + dispatch
- Plain GHA shape
- 1 workflow · 1 job + REST check-run lifecycle
Source
The recommended FlareDispatch shape is shown first. Toggle to Without FlareDispatch to see the full GitHub Actions workflow a team would maintain to do the same job without it.
Webhook mode — triggered directly by the FlareDispatch GitHub App webhook — zero GHA minutes.
// Recipe: post-deploy smoke test — the `deploy-smoke` Run
//
// Re-exports the canonical run from runs/deploy-smoke.ts (the Dispatcher's
// registered implementation). Copy the runs/deploy-smoke.ts file verbatim
// into your own repo when forking this recipe — the indirection here keeps
// the recipe page in sync with the registered run without duplicating code.
//
// Mode: Webhook mode — fires on `deployment_status.success`, no GHA workflow
// file. An Action-mode alternative (./ci.yml) dispatches the same run
// for repos that cannot install the App.
// DSL: see specs/03-dsl.md.
export { deploySmoke } from "@flare-dispatch/runs/deploy-smoke"; # Recipe: post-deploy smoke test — Action-mode alternative
#
# Use case: same `deploy-smoke` run as ./smoke.run.ts, but dispatched from a
# GitHub Actions workflow instead of the FlareDispatch GitHub App webhook.
#
# Webhook mode (the `triggers` block in ./smoke.run.ts) is preferred — it
# burns zero GHA minutes and needs no workflow file. Use this Action-mode
# file when you cannot install the App, or want the smoke test to interleave
# with other jobs on the deployment.
#
# Mode: Action mode, fire-and-forget. See specs/04-gha-integration.md.
# Run: deploy-smoke — defined in ./smoke.run.ts.
name: deploy-smoke
on:
deployment_status:
jobs:
deploy-smoke:
# GHA cannot filter `deployment_status` by state in `on:`, so gate the job
# here. This `if:` mirrors the webhook `gate` in ./smoke.run.ts — success
# transition, production only, environment_url present.
if: >-
github.event.deployment_status.state == 'success' &&
github.event.deployment.environment == 'production' &&
github.event.deployment_status.environment_url != ''
runs-on: ubuntu-latest
steps:
- uses: openhackersclub/flare-dispatch-action@v1
with:
run: deploy-smoke
endpoint: ${{ vars.FLAREDISPATCH_ENDPOINT }}
hmac-secret: ${{ secrets.FLAREDISPATCH_HMAC }}
inputs: |
{
"repo": "${{ github.repository }}",
"sha": "${{ github.event.deployment.sha }}",
"baseURL": "${{ github.event.deployment_status.environment_url }}",
"paths": ["/", "/health", "/api/status"]
}
# A non-zero `failed` count fails the check-run `flare-dispatch/deploy-smoke`
# on the deployed SHA — require it in branch protection, not this GHA job.
A faithful, runnable GHA workflow — what a team actually
maintains to do this job without FlareDispatch.
# Recipe: post-deploy smoke test — BASELINE (without FlareDispatch)
#
# This is what you'd maintain on plain GitHub Actions to fire a smoke test
# against a freshly-deployed URL, gated to production success transitions,
# with a result that fails the check-run on the deployed SHA. Shown here
# ONLY as a comparison — see ./smoke.run.ts for the FlareDispatch version
# (~50 lines of typed Effect-TS, one primitive: `probeHttp`).
#
# Notable costs of the GHA-only path:
# - `on: deployment_status` fires for EVERY transition (pending, in_progress,
# success, failure). You have to gate on `state == 'success'` AND on
# environment AND on environment_url being present — three independent
# `if:` clauses — or smoke tests fire multiple times per deploy.
# - To make this smoke run a REQUIRED check on the deployed SHA, you have
# to manually create a check-run via the GitHub REST API targeting that
# SHA — `actions/github-script` has the calls but the wiring is yours.
# - There's no concept of "deploy ID idempotency" in GHA's trigger model.
# A repeated webhook delivery — or a redeploy of the same SHA — will
# fire the smoke run again unless YOU dedup. FlareDispatch's
# `idempotencyKey` keys on `deployment.id` and de-dups in the kernel.
# - The probe loop has to live in shell/curl. Failure classification (4xx
# vs DNS vs connection-refused) is curl exit codes that you grep by
# hand.
name: deploy-smoke-baseline
on:
deployment_status:
permissions:
contents: read
checks: write
deployments: read
jobs:
smoke:
# GHA cannot pre-filter `deployment_status` by state, so gate the entire
# job. Three independent conditions because there's no idiomatic "all of"
# in `if:` expressions short of `&&`.
if: >-
github.event.deployment_status.state == 'success' &&
github.event.deployment.environment == 'production' &&
github.event.deployment_status.environment_url != ''
runs-on: ubuntu-latest
timeout-minutes: 6
env:
BASE_URL: ${{ github.event.deployment_status.environment_url }}
DEPLOY_SHA: ${{ github.event.deployment.sha }}
steps:
- name: Create in-progress check-run on deployed SHA
# Manual check-run lifecycle — create / update — because the GHA job's
# own status is anchored to the WORKFLOW SHA, not the deployment SHA.
# Branch protection on the deploy SHA needs a check anchored to it.
id: check
uses: actions/github-script@v7
with:
script: |
const { data: cr } = await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: "deploy-smoke",
head_sha: process.env.DEPLOY_SHA,
status: "in_progress",
});
core.setOutput("check_run_id", cr.id);
- name: Probe critical paths
id: probe
env:
PATHS: "/ /health /api/status"
# The probe loop is hand-rolled curl + exit-code classification.
# FlareDispatch's `probeHttp` primitive subsumes this — including
# parallel probes and tagged-error classification of curl exit codes.
run: |
checked=0
failed=0
for p in $PATHS; do
checked=$((checked+1))
url="${BASE_URL%/}$p"
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url" || echo "000")
if [ "$code" -lt 200 ] || [ "$code" -ge 400 ]; then
echo "::error::$url returned $code"
failed=$((failed+1))
else
echo "$url returned $code"
fi
done
echo "checked=$checked" >> "$GITHUB_OUTPUT"
echo "failed=$failed" >> "$GITHUB_OUTPUT"
- name: Finalize check-run
if: always()
uses: actions/github-script@v7
env:
CHECKED: ${{ steps.probe.outputs.checked }}
FAILED: ${{ steps.probe.outputs.failed }}
with:
script: |
const checked = Number(process.env.CHECKED ?? "0");
const failed = Number(process.env.FAILED ?? "0");
const conclusion = failed === 0 ? "success" : "failure";
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: "${{ steps.check.outputs.check_run_id }}",
status: "completed",
conclusion,
output: {
title: `deploy-smoke (${checked - failed}/${checked} healthy)`,
summary: failed === 0
? "All critical paths returned 2xx/3xx."
: `${failed} of ${checked} paths failed.`,
},
});
if (failed > 0) core.setFailed(`${failed} smoke checks failed.`);