Was sich auf einem Runner ändert
Der Test selbst ändert sich nicht. Was sich ändert, ist alles drum herum, und jeder dieser Punkte hat eine konkrete Antwort statt eines Achselzuckens:
- Es gibt kein Secret zu mounten
- Ein öffentliches Postfach zu lesen braucht weder Schlüssel noch Konto noch Header, die Postfach-Seite fügt
secretsalso nichts hinzu. Die einzigen Zugangsdaten im Job sind die, die Ihre Anwendung ohnehin zum Versenden von Mail braucht — SendGrid, Postmark, SES, was auch immer —, und die gehören zu Ihrer App, nicht zum Test. - Der Runner muss das Internet erreichen
- Ausgehendes HTTPS zu
grabmail.io, und ausgehend zu was auch immer Ihr Mailer verwendet. Von GitHub gehostete Runner erlauben beides standardmäßig; ein selbst gehosteter Runner hinter einem Filter für ausgehenden Datenverkehr braucht eine zusätzliche Regel. - Läufe überlappen sich
- Zwei Pull Requests, vier Shards, die Wiederholung eines instabilen Jobs — mehrere Kopien desselben Tests lesen gleichzeitig Mail. Eine geteilte Adresse würde es ihnen erlauben, gegenseitig ihre Codes zu lesen; eine frische Adresse pro Test macht die ganze Klasse von Problemen unmöglich.
- Zeit wird abgerechnet
- Ein Test, der sechzig Sekunden auf Mail wartet, ist unproblematisch. Ein Job, der das in jedem von vierzig Tests tut, sind vierzig Minuten abgerechneter Runner-Zeit. Die Wartezeiten müssen begrenzt sein, und die Suite muss geshardet werden, sobald sie wächst.
Der Testcode, den diese Workflows ausführen, ist derselbe wie in der Playwright-Anleitung oder der Python-Anleitung: eine frische Adresse, ein Warten mit einer Deadline, ein Extraktor, verankert in Ihrer Vorlage. Nichts davon ist CI-spezifisch, und genau das ist der Punkt — die runner-spezifischen Teile stecken vollständig in der Workflow-Datei.
Der Workflow, für Playwright
Ein Job. Er startet Ihre Anwendung mit einem echten ausgehenden Mailer, wartet, bis sie antwortet, führt die Suite aus und behält den Bericht nur, wenn etwas fehlgeschlagen ist.
name: e2e
on:
push:
branches: [main]
pull_request:
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 20 # the whole job, comfortably above every wait inside it
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
# Your application, started the way it runs in staging: a REAL outbound
# mailer. Its credentials are YOUR secret; the mailbox side needs none.
- name: Start the application
run: npm run start:test &
env:
MAILER_API_KEY: ${{ secrets.MAILER_API_KEY }}
APP_URL: http://localhost:3000
- name: Wait for the application
run: npx wait-on --timeout 60000 http://localhost:3000/health
- name: Run the suite
run: npx playwright test
env:
BASE_URL: http://localhost:3000
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7Drei Zeilen tragen das Gewicht. timeout-minutes: 20 ist die äußere Grenze, in die sich alles andere einbettet. Die Anwendung wird mit ihren echten Mailer-Zugangsdaten gestartet, weil ein Test, der echte Mail liest, echte versendete Mail braucht. Und der Bericht wird nur bei einem Fehlschlag hochgeladen, mit kurzer Aufbewahrung — ein erfolgreicher Lauf hat nichts, was sich zu behalten lohnt.
Der Workflow, für pytest
Dieselbe Form mit der Python-Toolchain: die Anwendung starten, auf ihren Health-Endpoint warten, die Suite mit einem Timeout pro Test über der Mail-Deadline ausführen, den JUnit-Bericht bei einem Fehlschlag behalten.
name: e2e
on:
push:
branches: [main]
pull_request:
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install -r requirements.txt -r requirements-test.txt
- name: Start the application
run: python -m app.server &
env:
MAILER_API_KEY: ${{ secrets.MAILER_API_KEY }}
APP_URL: http://localhost:8000
- name: Wait for the application
run: |
for i in $(seq 1 60); do
curl -sf http://localhost:8000/health && exit 0
sleep 1
done
echo "application did not come up" >&2; exit 1
- name: Run the suite
run: pytest tests/e2e -q --timeout=120 --junitxml=report.xml
env:
BASE_URL: http://localhost:8000
- uses: actions/upload-artifact@v4
if: failure()
with:
name: pytest-report
path: report.xml--timeout=120 stammt vom Plugin pytest-timeout und ist die Obergrenze pro Test; die Mail-Deadline im Helfer beträgt sechzig Sekunden, ein Test, der auf eine Nachricht wartet und danach noch etwas Browser-Arbeit macht, passt also noch hinein. Die Health-Check-Schleife ist ausgeschrieben statt als Action eingebunden, weil sie acht Zeilen umfasst und es darin nichts gibt, was man falsch machen könnte.
Die eine Netzwerkregel
Ein Postfach zu lesen ist eine ausgehende HTTPS-Anfrage vom Runner zu grabmail.io. Das ist der gesamte Netzwerk-Fußabdruck:
- Nichts Eingehendes. Nichts verbindet sich zum Runner. Es gibt keinen Webhook zu empfangen, keinen SMTP-Server zu betreiben, keinen Port freizugeben.
- Kein SMTP vom Runner aus. Die Mail wird von Ihrer Anwendung über deren Provider verschickt, über die API oder den SMTP-Endpoint dieses Providers — genauso wie in der Produktion. Der Runner spricht selbst nie SMTP.
- Nur
grabmail.io:443hinzuzufügen auf einem Runner mit einer Freigabeliste für ausgehenden Datenverkehr — dazu Ihr Mail-Provider und Ihre Paket-Registry, die der Job ohnehin schon brauchte.
- uses: step-security/harden-runner@v2
with:
egress-policy: block
allowed-endpoints: >
grabmail.io:443
api.your-mail-provider.example:443
registry.npmjs.org:443Wenn die Suite lokal durchläuft und in der CI mit einem Verbindungsfehler vom Helfer scheitert, ist diese Regel das Erste, was zu prüfen ist, und fast immer schon die ganze Antwort. Ein selbst gehosteter Runner in einem Unternehmensnetzwerk hat ausgehendes HTTPS typischerweise nach Hostname gefiltert; der freizugebende Hostname ist der der API, und die Anfrage ist einfaches HTTPS auf Port 443.
Shards, Matrix-Jobs und Wiederholungen
Sobald die Suite langsam genug ist, um sie zu sharden, sharden Sie sie. Playwright teilt einen Lauf mit --shard auf mehrere Jobs auf, und weil jeder Test sein eigenes Postfach öffnet, brauchen die Shards nichts voneinander:
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
# ... the same steps as above, then:
- run: npx playwright test --shard=${{ matrix.shard }}/4Dieselbe Eigenschaft deckt die anderen beiden Arten ab, wie ein Test am Ende gleichzeitig zweimal läuft:
- Zwei Pull Requests gleichzeitig
- Zwei Jobs, zwei Sätze zufälliger Adressen, keine Überschneidung. Die Obergrenze pro Client bei der API liegt bei 1200 Anfragen pro Minute, das sind zwanzig Postfächer, einmal pro Sekunde gepollt — ein Job, der jeweils ein Postfach pollt, ist davon weit entfernt.
- Ein wiederholter Job
- Eine Wiederholung führt den Testkörper erneut aus, der wieder eine neue Adresse erfindet. Das alte Postfach hält die alte Nachricht noch 5 Tage lang, und nichts liest sie — die Wiederholung sieht sie nie.
- Playwrights eigenes
retries - Dasselbe, eine Ebene tiefer: Jeder Versuch führt die Fixture erneut aus. Verschieben Sie die Adresse nicht in ein
beforeAll, um Zeit zu sparen; genau das ist die Art von Teilen, die einen Versuch den Code des vorherigen Versuchs lesen lässt.
Timeouts, die in den Job passen
Es gibt vier Uhren, und sie müssen ineinander verschachtelt sein, die innerste am kürzesten. Ist das nicht so, wird der Fehlschlag von der falschen gemeldet und zeigt auf die falsche Ursache.
| Uhr | Wo gesetzt | Ein vernünftiger Wert |
|---|---|---|
| Mail-Deadline | Im Helfer (timeoutMs, timeout=) | 60 s. Transaktionale Mail landet in Sekunden; eine Minute deckt eine langsame Provider-Warteschlange ab. |
| Test-Timeout | playwright.config.ts / --timeout | 120 s. Über der Deadline plus der Browser-Arbeit drumherum. |
| Step-Timeout | timeout-minutes am Step, falls gesetzt | Meist nicht gesetzt; die Job-Grenze reicht. |
| Job-Timeout | timeout-minutes am Job | 20 min. Genug für Installation, Start, die Suite und den Upload; niedrig genug, dass eine hängende Anwendung nicht eine Stunde lang abgerechnet wird. |
Das Symptom einer kaputten Verschachtelung ist eindeutig: Ein Test, der sechzig Sekunden innerhalb von Playwrights standardmäßigem Dreißig-Sekunden-Test-Timeout wartet, stirbt jedes Mal bei dreißig Sekunden mit einer Meldung über den Test und sagt nichts über die Mail aus. Setzen Sie zuerst das Test-Timeout, dann alles, was weiter außen liegt.
Einen Fehlschlag lesen
Ein fehlgeschlagener Lauf sollte Ihnen sagen, welches der drei Dinge passiert ist — die Mail kam nie, die falsche Mail kam, oder der Code darin war falsch —, ohne dass irgendetwas erneut laufen muss. Vier Gewohnheiten sorgen dafür:
- Loggen Sie die Adresse. Die Fehlermeldung des Helfers nennt das Postfach, auf das gewartet wurde. Geben Sie sie noch einmal am Anfang des Tests aus, damit sie im Job-Log steht, auch wenn die Prüfung an anderer Stelle scheitert.
- Öffnen Sie das Postfach von Hand. Nachrichten bleiben 5 Tage lang erhalten, sodass
/inbox/<address>auf dieser Website für den Rest der Woche genau zeigt, was der Runner gesehen hat — oder eben nicht. Das ist der mit Abstand nützlichste Vorteil eines echten Postfachs gegenüber einem gemockten. - Behalten Sie den Bericht bei einem Fehlschlag. Playwrights Trace zeigt den Klick, der die Mail hätte auslösen sollen; die JUnit-Datei zeigt, welcher Test und wie lange er gewartet hat.
- Prüfen Sie den Dienststatus, bevor Sie dem Test die Schuld geben. Die Statusseite wird von außen alle zwei Minuten geprüft; war eingehende Mail zum Zeitpunkt des Laufs gestört, war der Fehlschlag echt und nicht Ihrer.
Aufräumen, optional
Alles läuft nach 5 Tagen ab, ob es nun jemand löscht oder nicht, ein Lauf, der das Aufräumen auslässt, kostet also nichts. Zu löschen, was der Lauf gelesen hat, ist trotzdem einen Step wert, weil der nächste Fehlschlag dann gegen ein wirklich leeres Postfach gelesen wird. Es ist idempotent — zweimal löschen beantwortet immer noch mit 200 —, kann also von sich aus nie einen Build zum Scheitern bringen:
- name: Delete what the run read
if: always()
run: |
for addr in $(cat .e2e-addresses 2>/dev/null); do
curl -sG https://grabmail.io/api/v1/mailbox --data-urlencode "address=$addr" \
| jq -r '.messages[].id' \
| xargs -r -I{} curl -sX DELETE -G "https://grabmail.io/api/v1/message/{}" --data-urlencode "mailbox=$addr" -o /dev/null
doneMachen Sie ihn zu if: always() und niemals verpflichtend: ein Aufräumen, das fehlschlägt, sollte eine Warnung im Log sein, kein roter Build.
Bevor Sie es für fertig erklären
- Keine Postfach-Zugangsdaten in
secrets; nur der eigene Mailer-Schlüssel Ihrer Anwendung. - Ausgehendes HTTPS zu
grabmail.ioerlaubt, und nichts Eingehendes. - Eine frische Adresse pro Test, erfunden im Testkörper — sicher unter Shards und Wiederholungen.
- Die vier Timeouts verschachtelt: Deadline < Test < Step < Job.
- Der Bericht bei einem Fehlschlag hochgeladen, mit der Adresse im Log.
- Aufräumen als Step, der immer läuft und nie verpflichtend ist.
Das ist alles, was der Runner hinzufügt. Die Testdisziplin darunter — Deadline, frische Adresse, verankertes Muster — steht in einen E-Mail-Bestätigungsablauf Ende-zu-Ende testen, und die Extraktionsregeln für sich genommen stehen in OTP-Codes in automatisierten Tests.
Fragen
Muss ich ein GrabMail-Secret zum Repository hinzufügen?
Nein. Die öffentlichen Domains verlangen weder Schlüssel noch Konto noch Header, es gibt also nichts, das zu secrets hinzugefügt werden müsste. Die einzigen Zugangsdaten im Workflow sind die, die Ihre Anwendung zum Versenden von Mail verwendet und die sie ohnehin bräuchte, um überhaupt zu laufen.
Funktioniert das in einem privaten Repository oder auf einem selbst gehosteten Runner?
Ja. Der Runner stellt ausgehende HTTPS-Anfragen an grabmail.io und sonst nichts; wo Sie den Runner hosten, ist irrelevant. Auf einem selbst gehosteten Runner hinter einem Filter für ausgehenden Datenverkehr geben Sie diesen Hostnamen auf Port 443 frei.
Stoßen gleichzeitige Jobs an das Ratenlimit?
In der Praxis nicht. Das Limit pro Adresse liegt bei einem Lesevorgang pro Sekunde, was der Helfer respektiert, und die Obergrenze pro Client liegt bei 1200 Anfragen pro Minute — zwanzig Postfächer, einmal pro Sekunde gepollt, von einem Runner aus. Mehrere Runner sind mehrere Clients. Ein 429 wird mit Retry-After beantwortet, und der Helfer wartet das ab, statt zu scheitern.
Kann ich das nach einem Zeitplan laufen lassen, als synthetischen Check der Produktions-Anmeldung?
Ja, und das ist eine gute Verwendung dafür: Ein on: schedule-Workflow, der sich stündlich mit einer frischen Adresse anmeldet und den Code liest, beweist den gesamten Mail-Pfad der Produktion, Provider inklusive. Halten Sie das Adresspräfix erkennbar, damit sich die Anmeldungen auf Ihrer Seite leicht bereinigen lassen.
Was, wenn meine Anwendung Wegwerf-Domains ablehnt?
Richten Sie eine Domain, die Ihnen gehört, auf den Dienst aus — ein MX-Eintrag, kein Konto — und verwenden Sie diese Domain in der Fixture. Die Einrichtung dauert ein paar Minuten, und unbegrenzte Testkonten auf einer Domain zeigt das Muster in einer Suite.
Ist irgendetwas im Postfach privat?
Nein. Wer eine Adresse kennt, kann sie lesen, auf einer öffentlichen Domain genauso wie auf Ihrer eigenen. Für eine zufällige Adresse mit einem einzigen Wegwerf-Code ist das irrelevant; für eine Staging-Umgebung, die echte Kundendaten verschickt, ist es ein Ausschlusskriterium — richten Sie so eine nicht hierhin aus.


