Testing emails in GitHub Actions

Updated October 16, 2026 · Zyntra

Keep the API key in a repository secret, pass it to the test step as an environment variable, and build each address from GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT plus something per test. The one surprise: workflows triggered by pull requests from forks get no secrets, so email tests there must be skipped.

The workflow

.github/workflows/e2e.yml
name: e2e
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
        env:
          ZYNTRA_API_KEY: ${{ secrets.ZYNTRA_API_KEY }}
          ZYNTRA_TEAM_ID: ${{ vars.ZYNTRA_TEAM_ID }}
          BASE_URL: https://staging.your-app.com

The API key is a secret (Settings → Secrets and variables → Actions → Secrets). The team ID is not sensitive, so a repository variable is enough and shows up readably in logs.

Addresses that are unique per run, attempt and test

GITHUB_RUN_ID is unique for each workflow run in a repository, but it stays the same when you re-run the workflow; GITHUB_RUN_ATTEMPT goes up by one on each re-run. Use both, plus a random part for the test itself:

tests/address.ts
import { randomUUID } from "node:crypto";

const run = process.env.GITHUB_RUN_ID
  ? `gh${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`
  : "local";

export const newAddress = (tag: string) =>
  `${process.env.ZYNTRA_TEAM_ID}.${tag}-${run}-${randomUUID().slice(0, 6)}@zyntramail.com`;

The run ID in the address also helps when something fails: you can find every email a given run received.

Pull requests from forks

GitHub does not pass secrets (other than GITHUB_TOKEN) to workflows triggered from a forked repository. The key is empty, and every email test fails with an authentication error. Skip them instead, with a message that says why:

tests/email.spec.ts
import { test } from "@playwright/test";

test.skip(!process.env.ZYNTRA_API_KEY, "No ZYNTRA_API_KEY: email tests skipped");

Keep the number of requests in view

A workflow on every push and pull request runs far more often than a nightly job. Count the email reads per run and multiply; the polling post has the arithmetic. Running email tests only on the main branch and on a schedule is a common compromise.

Questions

Does the runner need anything installed for email?
No. Reading email is an HTTPS request from the test; there is no mail server or client on the runner.
Can I use the same setup in GitLab CI?
Yes. Use a masked CI/CD variable for the key, and CI_PIPELINE_ID and CI_JOB_ID where this post uses the GitHub run variables.