Every team says they want one clean email smoke test, but a month later three repos have three slightly different GitHub Actions files and nobody remembers which one is the real version. I kept hitting this on product teams shipping the same notification patterns across apps, admin panels, and background workers. The fix that stuck was moving the check into one reusable workflow and treating the calling repos like thin adapters.
That sounds small, but it changes the maintenance math a lot. Instead of editing YAML in five places, you tighten one contract, update one runner path, and ship one clearer triage flow. For email-heavy APIs, that is a very nice win because the failures are often boring, repetitive, and easy to misread when every repo logs them a bit differently.
Why I stopped copying email smoke test YAML between repos
Copied workflow files look fast on day one and messy by week three. One repo waits 15 seconds, another waits 40, one uploads artifacts, another does not, and one still uses an old scenario name from a feature that died ages ago. When a release check fails, the team spends extra time figuring out the workflow itself before they can even debug the email path.
I prefer one shared workflow that every service calls with a few explicit inputs. That keeps the logic centered and makes the per-repo file almost boring:
name: email-smoke
on:
pull_request:
workflow_dispatch:
jobs:
verify-email:
uses: org/platform-ci/.github/workflows/email-api-check.yml@main
with:
scenario: signup-verification
base_url: ${{ vars.APP_BASE_URL }}
max_wait_seconds: 20
The neat part is that boring is good here. If the check breaks, you know the repo-specific inputs first, then the shared workflow second. That order saves more time than people think, honestly.
It also pairs well with patterns around stable approval email handling and avoiding duplicate email sends. Once the app behavior is stable, the CI wrapper should be stable too, not this weird copy-paste museum.
The reusable workflow contract that keeps checks boring
The trick is keeping the contract tiny. I try to standardize just a handful of inputs:
scenariobase_urlmax_wait_secondsartifact_namerun_label
Then the reusable workflow owns the steps for trigger, poll, assert, and upload:
on:
workflow_call:
inputs:
scenario:
required: true
type: string
base_url:
required: true
type: string
max_wait_seconds:
required: false
type: number
default: 20
jobs:
email-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run email API smoke check
run: |
./scripts/email-smoke.sh \
--scenario "${{ inputs.scenario }}" \
--base-url "${{ inputs.base_url }}" \
--max-wait "${{ inputs.max_wait_seconds }}"
I like this because it keeps the repo contract visible and the test logic centralized. If you need to change inbox polling or rename an output field, you do it once. That is much nicer than hunting down five near-identical workflow files with one sneaky difference. Been there, not fun.
Inputs I standardize for every repo
One habit that helped a lot was forcing every caller to pass a run label, even if the script can generate one by default. When the logs and artifacts include the same label, cross-repo debugging gets way less fuzzy. It is also where I can thread in context like branch name, PR number, or environment without rewriting the shared job.
My default shell wrapper looks like this:
#!/usr/bin/env bash
set -euo pipefail
scenario="${1:?scenario required}"
run_label="${GITHUB_REPOSITORY}-${GITHUB_RUN_ID}"
./scripts/trigger-email-scenario.sh \
--scenario "$scenario" \
--run-label "$run_label"
./scripts/assert-email-delivery.sh \
--run-label "$run_label" \
--max-wait "${MAX_WAIT_SECONDS:-20}"
This is where practical keyword coverage can live naturally too. Some teams still search weird phrases like temp gamil com or tepm mail com while trying to find older inbox tooling docs. I would rather encode the real workflow and expected outputs in the shared check so people do less guessing and fewer tribal-memory searches.
For the same reason, I keep the email target generic in docs: use a temporary email address or another isolated inbox pattern that works for your environment, then assert on scenario IDs rather than on fragile message order. If your team says use and throw email in internal docs, fine, but give the workflow a stable artifact and result schema so language drift does not break triage.
How job summaries and artifacts speed up triage
Reusable workflows really pay off when they emit the same summary everywhere. I want the job summary to answer four questions fast:
- what scenario ran
- what inbox or alias was used
- whether the message arrived
- where the artifact bundle lives
GitHub documents reusable workflows through workflow_call, which is what makes this pattern maintainable across repositories (https://docs.github.com/en/actions/using-workflows/reusing-workflows). I also lean on GitHub's job summary support so a failing run shows the key evidence right in the UI instead of burying it in logs (https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary).
Here is the tiny summary step I keep adding:
{
echo "### Email API check"
echo ""
echo "- Scenario: $SCENARIO"
echo "- Run label: $RUN_LABEL"
echo "- Result: $RESULT"
echo "- Artifact: $ARTIFACT_NAME"
} >> "$GITHUB_STEP_SUMMARY"
This is not fancy, but it works realy well. Before, someone had to scan a long log dump to spot the scenario and inbox. After, the job page itself tells you what happened. That kind of before/after improvement is my favorite sort of platform work because it removes friction without asking every product engineer to learn new tools.
Q&A
When should I not use a reusable workflow?
Skip it if the repo has a truly unique delivery path or a compliance rule that forces custom handling. If 90% of the check is shared, though, I would still centralize the common path and let the repo add one thin wrapper.
How many inputs is too many?
Once the caller has to pass ten things, your contract is probly leaking implementation details. I try to keep it to the minimum needed to select a scenario and environment.
What is the biggest practical win?
Less YAML drift, faster rollouts, and better triage when one service starts failing. One shared workflow makes email API checks feel like a platform capability instead of a repo-by-repo chore.
Top comments (0)