<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonathan</title>
    <description>The latest articles on DEV Community by Jonathan (@pong1965).</description>
    <link>https://dev.to/pong1965</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015281%2F493b7797-432e-4e1c-b960-d582475ea1bb.png</url>
      <title>DEV Community: Jonathan</title>
      <link>https://dev.to/pong1965</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pong1965"/>
    <language>en</language>
    <item>
      <title>Replay Email Evidence in GitHub Actions</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:23:06 +0000</pubDate>
      <link>https://dev.to/pong1965/replay-email-evidence-in-github-actions-20f0</link>
      <guid>https://dev.to/pong1965/replay-email-evidence-in-github-actions-20f0</guid>
      <description>&lt;p&gt;Email-dependent CI checks are often treated like a single assertion: send a message, poll an inbox, click a link, pass or fail. That model is fast when everything works and nearly useless when it does not.&lt;/p&gt;

&lt;p&gt;The useful question is not “did the email test fail?” It is “what did this run observe, and can I replay that observation?” A small evidence bundle in GitHub Actions turns a noisy API failure into a short debugging session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure pattern
&lt;/h2&gt;

&lt;p&gt;Suppose a signup job creates a user, calls an email API, and waits for verification. A retry might find an old message. A shared inbox might contain another branch’s mail. Or the provider may accept the request while delivery is still pending. The final assertion hides all three cases.&lt;/p&gt;

&lt;p&gt;This gets worse when teams use a create temporary mail flow or a throwaway email generator for test identities but do not record which address belonged to which run. The inbox is isolated in theory, yet the CI logs can’t prove it.&lt;/p&gt;

&lt;p&gt;Start with a run identifier and carry it through every layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUN_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_RUN_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_RUN_ATTEMPT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TEST_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci+&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUN_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@example.test"&lt;/span&gt;

curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMAIL_API&lt;/span&gt;&lt;span class="s2"&gt;/messages"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;to&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_EMAIL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;run_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_ID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; email-submit.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never put a verification token in a normal log line. Save the response body as a restricted artifact, or redact the token before printing. This is a small habit, but it saves time when a failed run needs to be shared with someone outside the feature team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store evidence in the job
&lt;/h2&gt;

&lt;p&gt;Capture four small files, even when the test passes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;request.json&lt;/code&gt; — the safe request inputs and run ID.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;submit.json&lt;/code&gt; — the email API receipt, including provider status.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;poll.jsonl&lt;/code&gt; — timestamped polling results and message IDs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assertion.txt&lt;/code&gt; — the final reason for pass or failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A polling record should say what was checked, not just dump a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-09-07T14:20:10Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-09-07T14:20:16Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"m_4821"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"subject_ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the retention period short and the artifact name predictable. Developers should be able to find evidence in seconds, and old test mail should not become a permanent data store. One common mistake is calling it a temp org mail address in a note while the actual fixture uses a different naming rule; consistent labels matter more than clever labels.&lt;/p&gt;

&lt;p&gt;For inbox matching, record the predicate too: recipient, subject prefix, run ID, and minimum message timestamp. These fields make a false positive visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay the check locally
&lt;/h2&gt;

&lt;p&gt;The fastest fix is usually a replay against saved evidence, not a full rerun of the entire pipeline. Make the parser accept a fixture directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/check_email.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--submit&lt;/span&gt; .artifacts/submit.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--poll&lt;/span&gt; .artifacts/poll.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expected-run&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command should return a distinct exit code for “no matching message,” “wrong message,” and “malformed provider response.” That distinction is more actionable than a generic timeout. It also lets you add regression fixtures for provider quirks without sending another message.&lt;/p&gt;

&lt;p&gt;If your test uses Playwright, pair the browser trace with the inbox record. The guide on &lt;a href="https://dev.to/silviutech/playwright-inbox-filters-for-flaky-signup-tests-24io"&gt;inbox filters for flaky signup tests&lt;/a&gt; is a useful companion for making the message selection explicit. Before an agent or parallel job starts, &lt;a href="https://dev.to/mrdapperx/freeze-email-test-plans-before-agent-runs-319j"&gt;freeze email test plans&lt;/a&gt; so the expected contract stays stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact workflow
&lt;/h2&gt;

&lt;p&gt;In GitHub Actions, the shape can stay simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run email API check&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/run-email-check.sh&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload email evidence&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-evidence-${{ github.run_id }}-${{ github.run_attempt }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.artifacts/email/&lt;/span&gt;
    &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if: always()&lt;/code&gt; line is the important shortcut. Evidence that only exists on success is not evidence; it is decoration. In practice, this workflow makes failures less guessy and avoids rerunning unrelated build steps just to inspect one missing message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Give every test run a unique email correlation ID.&lt;/li&gt;
&lt;li&gt;Isolate the inbox or use a strict recipient and timestamp filter.&lt;/li&gt;
&lt;li&gt;Save the API receipt, poll history, and assertion reason.&lt;/li&gt;
&lt;li&gt;Redact tokens and keep artifacts for a short period.&lt;/li&gt;
&lt;li&gt;Make the checker replayable from local fixtures.&lt;/li&gt;
&lt;li&gt;Upload evidence when the job fails as well as when it passes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff is modest but real: a flaky email check becomes a bounded investigation. GitHub Actions still tells you that the build failed, but the artifact tells you why.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>api</category>
      <category>devtools</category>
    </item>
    <item>
      <title>GitHub Actions Matrix Jobs Need a Failure Map</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:24:29 +0000</pubDate>
      <link>https://dev.to/pong1965/github-actions-matrix-jobs-need-a-failure-map-40ci</link>
      <guid>https://dev.to/pong1965/github-actions-matrix-jobs-need-a-failure-map-40ci</guid>
      <description>&lt;p&gt;Matrix jobs are great right up untill four variants fail at once and the whole workflow turns into log roulette. You know the kind: &lt;code&gt;node18-linux&lt;/code&gt; failed in one way, &lt;code&gt;node20-macos&lt;/code&gt; failed in another, and the only shared output is a red X.&lt;/p&gt;

&lt;p&gt;What helped me most was adding a failure map step after the matrix run. Instead of asking every engineer to open each job and compare logs by hand, the workflow collects one small JSON result per variant and prints a single summary table in the parent job. It is not fancy, but it is fast, and fast wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why matrix failures waste time
&lt;/h2&gt;

&lt;p&gt;Most matrix workflows are very good at parallelism and pretty bad at storytelling.&lt;/p&gt;

&lt;p&gt;Each variant knows what broke in its own enviroment, but the workflow rarely answers the bigger questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did all failures come from the same API contract drift?&lt;/li&gt;
&lt;li&gt;is one operating system the outlier?&lt;/li&gt;
&lt;li&gt;did only the slowest variants fail after a timeout?&lt;/li&gt;
&lt;li&gt;was the break introduced before the test even reached the interesting step?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Actions already gives us the building blocks for this pattern: matrix strategy, artifacts, job outputs, and step summaries (&lt;a href="https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs&lt;/a&gt;, &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions&lt;/a&gt;). The missing piece is deciding that every matrix leg should leave behind one compact, machine-readable verdict.&lt;/p&gt;

&lt;p&gt;That is very similar to the discipline behind &lt;a href="https://dev.to/mrdapperx/release-ready-email-checks-3nam"&gt;release-ready email checks&lt;/a&gt;. The point is not more logs. The point is better evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure map pattern
&lt;/h2&gt;

&lt;p&gt;The shape is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each matrix job writes &lt;code&gt;result.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A follow-up job downloads all artifacts.&lt;/li&gt;
&lt;li&gt;A small script merges them into &lt;code&gt;failure-map.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The workflow writes a human summary to &lt;code&gt;$GITHUB_STEP_SUMMARY&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I keep each result file tiny on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node20-ubuntu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contract-check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"http_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1684&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schema mismatch on billing response"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"artifact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"result-node20-ubuntu"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one record is enough to group failures by phase, sort them by duration, and spot whether the problem is broad or isolated. If every failing job points to &lt;code&gt;contract-check&lt;/code&gt;, you probably have one regression. If failures split between &lt;code&gt;setup&lt;/code&gt;, &lt;code&gt;seed&lt;/code&gt;, and &lt;code&gt;contract-check&lt;/code&gt;, you likely have workflow debt.&lt;/p&gt;

&lt;p&gt;I also like this because it makes review calmer. A teammate can scan one summary, open one artifact, and move on. No one has to remember which run had the weird temp mailid branch name in a copied command or which retry log was the one you actualy meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  A GitHub Actions workflow shape
&lt;/h2&gt;

&lt;p&gt;Here is the basic pattern I reuse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api-matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;macos-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;18&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run API checks&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p out&lt;/span&gt;
          &lt;span class="s"&gt;node scripts/run-checks.mjs \&lt;/span&gt;
            &lt;span class="s"&gt;--os "${{ matrix.os }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--node "${{ matrix.node }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--out out/result.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload result&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;result-${{ matrix.node }}-${{ matrix.os }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;out/result.json&lt;/span&gt;

  &lt;span class="na"&gt;summarize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-matrix&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/download-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;artifacts&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build failure map&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;node scripts/build-failure-map.mjs artifacts &amp;gt; failure-map.json&lt;/span&gt;
          &lt;span class="s"&gt;node scripts/write-summary.mjs failure-map.json &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details matter a lot here.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;fail-fast: false&lt;/code&gt; is worth it when you are trying to compare variants, not just stop early. Second, artifact upload should run with &lt;code&gt;if: always()&lt;/code&gt; or you lose the very evidence you need on broken legs. Third, the summarizer should be dirt simple. If your merge script needs a framework, it is probably overbuilt.&lt;/p&gt;

&lt;p&gt;I learned the same lesson while working on flows with &lt;a href="https://dev.to/kevindev27/version-email-events-in-node-apis-pg5"&gt;versioned email events&lt;/a&gt;: when the system spans several steps, the best tool is often a tiny contract file that every stage agrees on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to store as artifacts
&lt;/h2&gt;

&lt;p&gt;My default artifact bundle has three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;result.json&lt;/code&gt; for the verdict&lt;/li&gt;
&lt;li&gt;raw logs for the failing command&lt;/li&gt;
&lt;li&gt;optional API request or response samples when the check touches external services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part is where teams sometimes overdo it. You do not need to archive the whole world. You need enough context to explain the branch between pass and fail.&lt;/p&gt;

&lt;p&gt;For API and Automation work, I usually store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;phase name&lt;/li&gt;
&lt;li&gt;final status&lt;/li&gt;
&lt;li&gt;duration in milliseconds&lt;/li&gt;
&lt;li&gt;one short reason string&lt;/li&gt;
&lt;li&gt;path to the raw log or payload file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a workflow also verifies inbox side effects or other disposable-address cases, I still keep them inside the same result contract. The keyword tempmailso may show up in search planning or team notes, but the CI artifact itself should stay product-neutral and boring. Boring is good here.&lt;/p&gt;

&lt;p&gt;One more thing that helps a lot: sort the summary by failure phase before you print it. Engineers think faster when similar breakages sit next to each other. That sounds smal, but it trims minutes from triage in nearly every busy release week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should the summary job fail the workflow too?
&lt;/h3&gt;

&lt;p&gt;Usually no. Let the matrix legs own pass or fail. The summary job should explain the blast radius, not create a second source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this better than using job outputs only?
&lt;/h3&gt;

&lt;p&gt;For tiny workflows, maybe not. But once you have more than a couple variants, artifacts scale better because you can keep structured data per leg instead of squeezing everything into one output string.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the biggest win?
&lt;/h3&gt;

&lt;p&gt;Cleaner handoffs. A workflow run stops being "something failed somewhere" and becomes "three variants failed in contract-check after the same response drift." That is the kind of sentence an on-call engineer can use imediately.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>api</category>
      <category>devtools</category>
    </item>
    <item>
      <title>GitHub Actions Need Inbox Replay Data</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Mon, 31 Aug 2026 05:23:59 +0000</pubDate>
      <link>https://dev.to/pong1965/github-actions-need-inbox-replay-data-35hj</link>
      <guid>https://dev.to/pong1965/github-actions-need-inbox-replay-data-35hj</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Actions Need Inbox Replay Data
&lt;/h1&gt;

&lt;p&gt;When a workflow says "email not received," I do not add another retry first. I add replay data. A small inbox timeline makes flaky API checks faster to debug and much easier to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why replay data matters more than another retry
&lt;/h2&gt;

&lt;p&gt;One pattern keeps showing up in GitHub Actions pipelines: the send step looks healthy, the app returns success, and the inbox assertion still fails with almost no context. Teams usually patch that with longer sleeps. I used to do that too, and it sort of worked, until it didnt.&lt;/p&gt;

&lt;p&gt;The real problem is that most workflows preserve the final verdict but not the path that led there. If the mailbox poller checked five times, matched two near-miss subjects, and saw one stale recipient, that sequence matters more than a generic timeout error. Once I started storing that sequence, triage got shorter and less argue-y.&lt;/p&gt;

&lt;p&gt;This is close in spirit to &lt;a href="https://dev.to/mrdapperx/freeze-email-test-plans-before-agent-runs-319j"&gt;freezing email test plans before runs&lt;/a&gt; and building &lt;a href="https://dev.to/sophiax99/facebook-signup-email-checks-without-lockouts-12i7"&gt;safer signup email checks&lt;/a&gt;. Both ideas push toward the same outcome: make the workflow explain itself while it is still fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tiny event model I keep in every workflow
&lt;/h2&gt;

&lt;p&gt;I do not want a giant observability project inside CI. I just want enough state to replay what happened. For email-driven APIs, that usually means one JSON lines file with four event types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requested&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;polled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matched&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verdict&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each event gets a timestamp, scenario id, recipient, and one or two useful fields. Thats it. The file stays small, diffable, and easy to inspect in an artifact download.&lt;/p&gt;

&lt;p&gt;Here is the kind of shape I mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"requested"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"invite-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"qa+invite-204@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"You're invited"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"polled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"invite-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"messages_seen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"invite-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"msg_481"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"You're invited"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"invite-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"passed"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds basic, but the payoff is realy good. A developer can open one file and understand whether the issue was send timing, recipient mismatch, or an assertion that drifted from the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  A GitHub Actions job layout that stays debuggable
&lt;/h2&gt;

&lt;p&gt;The setup I like is boring on purpose. Boring wins in CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create inbox run envelope&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;mkdir -p .tmp/inbox&lt;/span&gt;
    &lt;span class="s"&gt;cat &amp;gt; .tmp/inbox/context.json &amp;lt;&amp;lt;'JSON'&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"scenario": "${{ matrix.scenario }}",&lt;/span&gt;
      &lt;span class="s"&gt;"recipient": "${{ env.TEST_RECIPIENT }}",&lt;/span&gt;
      &lt;span class="s"&gt;"startedAt": "${{ github.run_id }}"&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;JSON&lt;/span&gt;
    &lt;span class="s"&gt;: &amp;gt; .tmp/inbox/replay.jsonl&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Append requested event&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/email-log.js requested .tmp/inbox/context.json .tmp/inbox/replay.jsonl&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Poll inbox&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/poll-inbox.js .tmp/inbox/context.json .tmp/inbox/replay.jsonl&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload replay artifact&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inbox-replay-${{ github.run_id }}-${{ matrix.scenario }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.tmp/inbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three habits make this hold up better than ad-hoc logs.&lt;/p&gt;

&lt;p&gt;First, every script appends to the same replay file instead of inventing its own format. Second, the artifact name maps cleanly to one scenario. Third, the summary points people to the artifact instead of dumping everything into step output, which gets noisy fast and is annoying to scan when youre half awake.&lt;/p&gt;

&lt;p&gt;If you support many sandbox signups or review apps, this is also where a &lt;code&gt;throwaway email&lt;/code&gt; flow can help. I treat it as test plumbing, not the story itself. For example, a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmail disposable&lt;/a&gt; inbox is handy when I need a short-lived recipient for isolated workflow runs, but the bigger productivity win still comes from the replay trail around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where throwaway email fits without taking over the post
&lt;/h2&gt;

&lt;p&gt;I have seen teams over-focus on the inbox provider and under-focus on workflow discipline. A provider switch can help, sure, but it does not fix missing evidence. If your logs cannot answer "what did this exact run observe and when?", the next provider will inherit the same fog.&lt;/p&gt;

&lt;p&gt;My checklist is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One scenario should map to one recipient.&lt;/li&gt;
&lt;li&gt;Poll attempts should be written as events, not hidden in console noise.&lt;/li&gt;
&lt;li&gt;Near matches should be captured before the final fail.&lt;/li&gt;
&lt;li&gt;The artifact name should be unique enough to fetch in seconds.&lt;/li&gt;
&lt;li&gt;Strange copied inputs like &lt;code&gt;temp gamil com&lt;/code&gt; should be visible as test data, not mistaken for infrastructure bugs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When that discipline is in place, even a plain &lt;code&gt;tempmailso&lt;/code&gt; link becomes contextual instead of spammy, because it supports a real workflow choice rather than trying to carry the whole article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Q&amp;amp;A
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Should I keep replay artifacts for successful runs?
&lt;/h2&gt;

&lt;p&gt;Yes. Passing runs are your baseline. When a flaky case appears, good baseline artifacts save a lot of guesswork, and thats usualy the cheapest debug acceleration you can buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is JSONL better than one big JSON file?
&lt;/h2&gt;

&lt;p&gt;For CI, yes. Appending is simpler, partial writes are easier to reason about, and local inspection with standard tools is nicer. You can &lt;code&gt;rg&lt;/code&gt; or &lt;code&gt;jq&lt;/code&gt; through it without much fuss.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the smallest useful version?
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;requested&lt;/code&gt;, &lt;code&gt;polled&lt;/code&gt;, and &lt;code&gt;verdict&lt;/code&gt;. Add &lt;code&gt;matched&lt;/code&gt; when you need to explain why a message was close but not accepted. Keep it lean, keep it readable, and resist the urge to turn the workflow into a mini data lake.&lt;/p&gt;

&lt;p&gt;The main shift here is not technical genius. It is deciding that inbox evidence deserves first-class treatment in your developer tools. Once you do that, GitHub Actions stops feeling random, and API-related email tests become a lot less spooky.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>api</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Make API Failures Obvious in GitHub Actions</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Fri, 28 Aug 2026 08:24:37 +0000</pubDate>
      <link>https://dev.to/pong1965/make-api-failures-obvious-in-github-actions-29p</link>
      <guid>https://dev.to/pong1965/make-api-failures-obvious-in-github-actions-29p</guid>
      <description>&lt;p&gt;Most API failures are clear in code and murky in CI.&lt;/p&gt;

&lt;p&gt;I keep seeing GitHub Actions jobs fail with enough raw logs to prove something went wrong, but not enough structure to tell the next engineer what to do first. That gap is where teams lose time. The request path is somewhere in the output, the status code is buried, and one retried step makes the whole thing look noisier than it realy is.&lt;/p&gt;

&lt;p&gt;For developer tooling, I have started treating workflow output like a product surface. If a job can fail, it should leave a readable receipt with the endpoint, scenario, and likely owner. This is not fancy observability. It is just a disciplined way to make APIs and GitHub Actions less annoying on the worst day of the week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why failed API jobs still waste time
&lt;/h2&gt;

&lt;p&gt;The usual anti-pattern is simple: one shell step runs a probe, prints a wall of curl output, exits non-zero, and the team calls that "good enough." It is technically enough, but operationally weak.&lt;/p&gt;

&lt;p&gt;GitHub's own docs show how much teams rely on Actions for delivery and automation workflows, with millions of developers building around the platform's CI primitives (&lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;GitHub Actions documentation&lt;/a&gt;). In practice, that means your failure output is part of the developer experience, not just a side effect.&lt;/p&gt;

&lt;p&gt;The failure triage gets slower when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the step name is generic&lt;/li&gt;
&lt;li&gt;the failing input is not echoed back safely&lt;/li&gt;
&lt;li&gt;retries overwrite the original clue&lt;/li&gt;
&lt;li&gt;logs mix transport failures with product assertions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I learned a similar lesson from writing &lt;a href="https://dev.to/silviutech/stable-playwright-email-tests-start-with-contracts-1h1d"&gt;stable test contracts&lt;/a&gt;: once the expected shape is explicit, the failure gets easier to discuss across QA, backend, and platform folks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the failure summary before you debug
&lt;/h2&gt;

&lt;p&gt;My favorite shortcut is to generate the workflow summary on purpose, not as an afterthought. If the probe fails, the engineer opening the run should see a short diagnosis in the &lt;code&gt;GITHUB_STEP_SUMMARY&lt;/code&gt; panel before they read the raw log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Probe account API&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;node scripts/probe-account-api.mjs &amp;gt;&amp;gt; result.json&lt;/span&gt;
    &lt;span class="s"&gt;cat result.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Summarize result&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/write-summary.mjs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in &lt;code&gt;write-summary.mjs&lt;/code&gt;, write the fields that actually matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;result.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;## API probe summary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`- Endpoint: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`- Status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`- Scenario: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`- Retryable: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`- Hint: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure is boring in a good way. It turns a failed run into something teammates can scan on mobile, in Slack, or five minutes before a release call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return a tiny contract from every probe
&lt;/h2&gt;

&lt;p&gt;The probe script should not dump random text and hope the reader is patient. Return a tiny JSON contract instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/v1/accounts/verify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-smoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upstream dependency timeout"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps APIs testable and summaries consistent. It also stops the drift where one script says "bad response," another says "unexpected body," and a third says nothing useful at all. If you ever have to validate signup or notification flows that touch a temp inbox, be equally explicit about the test input. Search terms around &lt;code&gt;tem email&lt;/code&gt; and even nonsense like &lt;code&gt;temp gamil com&lt;/code&gt; show up more often than people expect in support and growth investigations, so keeping the original scenario label helps a lot.&lt;/p&gt;

&lt;p&gt;If your workflow uses a disposable inbox during non-production checks, keep that tooling contextual and minimal. One example is a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;fake emails generator&lt;/a&gt; for isolated verification flows, but it should support the scenario rather than dominate the article or the pipeline.&lt;/p&gt;

&lt;p&gt;I also like pairing this with the discipline behind &lt;a href="https://dev.to/mrdapperx/keep-publish-retries-immutable-2pcd"&gt;immutable publish retries&lt;/a&gt;: each attempt should preserve enough context that the second reader is not reconstructing the first failure from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use annotations for the one detail that matters
&lt;/h2&gt;

&lt;p&gt;Workflow summaries are great for the broad story. Annotations are better for the one fact a reviewer must not miss.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::error title=API probe failed::/v1/accounts/verify returned 503 during signup-smoke"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you an obvious marker in the run timeline. I use one annotation max per failing probe, because five annotations feel like panic and one usually feels like signal. If the failure is rate-limit related, include the budget or reset window. If it is schema related, include the exact field name. Tiny decisions like that make throwaway email and account-creation checks much easier to hand off between teams.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://cloud.google.com/devops/state-of-devops" rel="noopener noreferrer"&gt;State of DevOps reports collected by Google Cloud&lt;/a&gt;, fast feedback loops correlate with better software delivery outcomes. That does not mean every pipeline needs a platform team makeover. It does mean a readable summary is often the cheapest useful upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short shipping checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;give each probe a scenario name that means something outside your team&lt;/li&gt;
&lt;li&gt;output JSON from the probe and Markdown from the summary step&lt;/li&gt;
&lt;li&gt;fail once, in one place, after writing the receipt&lt;/li&gt;
&lt;li&gt;annotate only the highest-signal detail&lt;/li&gt;
&lt;li&gt;keep retries visible instead of silently replacing the first result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a GitHub Actions run fails, the goal is not more logs. The goal is a faster first decision. If the next engineer can tell whether they should retry, inspect an upstream API, or fix a broken assumption in under a minute, the workflow is doing its job prety well.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>api</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Use Artifacts to Debug Email APIs Faster</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Fri, 28 Aug 2026 05:41:46 +0000</pubDate>
      <link>https://dev.to/pong1965/use-artifacts-to-debug-email-apis-faster-cb9</link>
      <guid>https://dev.to/pong1965/use-artifacts-to-debug-email-apis-faster-cb9</guid>
      <description>&lt;p&gt;Email API smoke tests often fail in a boring, expensive way. The request went through, the inbox maybe received something, and the CI log still leaves the team guessing. I see this a lot in GitHub Actions pipelines: the step output says "timeout" and everybody has to reconstruct what the run actually tried to do.&lt;/p&gt;

&lt;p&gt;What helped most on teams I support was not another retry loop. It was treating every email-related check like a tiny evidence pipeline. Each run should publish a compact artifact bundle with the request payload, timing, inbox metadata, and the exact message match result. Once we did that, triage got way less annoying and a bit more honest.&lt;/p&gt;

&lt;p&gt;If you have already worked through &lt;a href="https://dev.to/sophiax99/bind-email-change-links-to-the-active-session-50oa"&gt;session-bound email change checks&lt;/a&gt; or used &lt;a href="https://dev.to/pong1965/git-diffs-make-email-checks-reproducible-1o32"&gt;reproducible email check diffs&lt;/a&gt;, this pattern fits right after them. The focus here is speed: when a check fails at 2 AM, the next person should not need to rerun the workflow just to understand the shape of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email API failures take too long to triage
&lt;/h2&gt;

&lt;p&gt;Most flaky email checks are not truly mysterious. They are just under-instrumented.&lt;/p&gt;

&lt;p&gt;The workflow sends an API request, waits for a message, and logs a single generic failure. That leaves out the details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which recipient address was used&lt;/li&gt;
&lt;li&gt;when the app said it queued the email&lt;/li&gt;
&lt;li&gt;what subject or headers the poller expected&lt;/li&gt;
&lt;li&gt;whether the inbox was empty, stale, or almost right&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those details, the post-failure discussion gets weirdly speculative. Someone blames rate limits, someone blames the provider, and someone else starts searching for tempail mail or tepm mail com because the symptom looks familiar. Sometimes that guess is right, but more often the run simply did not save enough evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact bundle that changes the game
&lt;/h2&gt;

&lt;p&gt;The fix is small: create one artifact directory per workflow run and put the same four files in it every time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;request.json&lt;/code&gt; with the sanitized API request and correlation id.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;poll-result.json&lt;/code&gt; with the inbox id, wait duration, and matched message summary.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timeline.txt&lt;/code&gt; with human-readable timestamps for trigger, first poll, last poll, and finish.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;debug.md&lt;/code&gt; with one short explanation of what the workflow expected and what it actually saw.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That bundle is enough to answer most first-pass questions. It also keeps the CI log shorter, which I like a lot. Logs are useful for scanning, but artifacts are better for evidence you may need ten minutes later.&lt;/p&gt;

&lt;p&gt;One more benefit: artifact bundles make it obvious when the contract is too fuzzy. If &lt;code&gt;poll-result.json&lt;/code&gt; has three possible matches and your script still says "success", thats a product bug in the test helper, not a platform mystery.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small GitHub Actions pattern
&lt;/h2&gt;

&lt;p&gt;This is the shape I keep coming back to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Trigger email flow&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/send-check.js &amp;gt; .tmp/request.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for message&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/wait-for-email.js &amp;gt; .tmp/poll-result.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build timeline&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/write-timeline.js &amp;gt; .tmp/timeline.txt&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Summarize debug state&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/write-debug-summary.js &amp;gt; .tmp/debug.md&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload email evidence&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-check-${{ github.run_id }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.tmp/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing fancy there, which is the point. The workflow keeps moving fast, but every run leaves behind a receipt. When the check fails, I open the artifact first and the raw log second. That order matters because it avoids chasing noise from unrelated setup steps.&lt;/p&gt;

&lt;p&gt;For teams using a burner email in non-production API checks, this also creates a cleaner boundary between "provider issue" and "our matcher logic is too loose." You can inspect the saved poll result and see whether the run never received mail, received the wrong message, or matched something stale. Thats a much better conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to save for every failing run
&lt;/h2&gt;

&lt;p&gt;I would keep this checklist brutally consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the correlation id sent to the app&lt;/li&gt;
&lt;li&gt;the exact recipient or inbox alias&lt;/li&gt;
&lt;li&gt;the expected subject fragment&lt;/li&gt;
&lt;li&gt;the receive-after timestamp used by the poller&lt;/li&gt;
&lt;li&gt;the first 10 lines of the matched body, if policy allows it&lt;/li&gt;
&lt;li&gt;the normalized reason code for failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last field is the sneaky important one. "Timeout" is too broad. "timeout_no_messages" versus "timeout_only_stale_messages" versus "timeout_subject_mismatch" gives you a useful next action right away. It sounds small, but it saves a suprising amount of time.&lt;/p&gt;

&lt;p&gt;I also prefer saving a short Markdown summary because humans read it faster than JSON when they are half-distracted. Two or three sentences are enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Expected a verification email within 60s after request &lt;span class="sb"&gt;`req_8421`&lt;/span&gt;.
Inbox received 2 messages, both older than the run start time.
Result: timeout_only_stale_messages.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is usually the moment the bug stops feeling random.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should artifacts be uploaded on successful runs too?
&lt;/h3&gt;

&lt;p&gt;Usually yes, but keep them small and set a short retention period. Success artifacts help when a failure starts showing up only every few days, and you want one known-good example nearby.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need full email bodies in the artifact?
&lt;/h3&gt;

&lt;p&gt;Not always. Metadata, snippet text, and matcher fields are often enough. Save less by default, then expand only if the team truly needs more detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this only for GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;Nope. The same idea works in any CI system. GitHub Actions just makes the artifact habit easy, so it is a nice place to start even if your stack is otherwise pretty plain.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>api</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Use Step Outputs to Shorten CI Triage</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Fri, 28 Aug 2026 05:24:09 +0000</pubDate>
      <link>https://dev.to/pong1965/use-step-outputs-to-shorten-ci-triage-3g4h</link>
      <guid>https://dev.to/pong1965/use-step-outputs-to-shorten-ci-triage-3g4h</guid>
      <description>&lt;p&gt;The fastest CI fix I know is not a smarter dashboard. It is a boring habit: make each important step produce one tiny output that the next step can read without parsing a wall of logs.&lt;/p&gt;

&lt;p&gt;I started leaning on this harder when workflow runs mixed shell scripts, API checks, and inbox-style assertions. The run would fail, somebody would open logs, and ten minutes later we still had no clear answer. Once I moved the important facts into step outputs, triage got way less noisy and a bit more honest too.&lt;/p&gt;

&lt;p&gt;This pattern sits nicely next to &lt;a href="https://dev.to/mrdapperx/replay-packs-make-cron-jobs-easier-to-fix-4mg8"&gt;replay packs for broken cron runs&lt;/a&gt; and &lt;a href="https://dev.to/pong1965/github-actions-summaries-for-email-checks-3coj"&gt;concise summaries for noisy email checks&lt;/a&gt;. All three ideas are really about the same thing: preserve the signal first, then preserve the details second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why step outputs are the fastest handoff in CI
&lt;/h2&gt;

&lt;p&gt;Logs are good for deep dives. They are awful as the main handoff between steps.&lt;/p&gt;

&lt;p&gt;GitHub Actions supports step outputs through the &lt;code&gt;GITHUB_OUTPUT&lt;/code&gt; file, which gives you a clean way to pass structured values forward without regex games (&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter&lt;/a&gt;). That matters because the next step can branch on a small verdict like &lt;code&gt;passed&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, or &lt;code&gt;subject_mismatch&lt;/code&gt; instead of scanning fifty lines of shell noise.&lt;/p&gt;

&lt;p&gt;When I am debugging a &lt;code&gt;get temporary email&lt;/code&gt; flow in CI, I mostly want these facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what scenario ran&lt;/li&gt;
&lt;li&gt;whether a message arrived&lt;/li&gt;
&lt;li&gt;how long we waited&lt;/li&gt;
&lt;li&gt;what evidence bundle was saved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything beyond that belongs in an artifact, not in the control path. This seperation sounds tiny, but it makes the whole workflow feel more deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny pattern for outputs, summaries, and artifacts
&lt;/h2&gt;

&lt;p&gt;My default contract is just three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;step outputs for decisions&lt;/li&gt;
&lt;li&gt;job summary for humans&lt;/li&gt;
&lt;li&gt;artifacts for raw evidence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The shell step writes a few outputs and stores a minimal JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; artifacts

&lt;span class="nv"&gt;result_json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"artifacts/result.json"&lt;/span&gt;
node ./scripts/check-inbox.mjs &lt;span class="nt"&gt;--scenario&lt;/span&gt; signup &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$result_json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;verdict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.verdict'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$result_json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;wait_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.wait_ms'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$result_json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;artifact_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"artifacts"&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"verdict=&lt;/span&gt;&lt;span class="nv"&gt;$verdict&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"wait_ms=&lt;/span&gt;&lt;span class="nv"&gt;$wait_ms&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"artifact_dir=&lt;/span&gt;&lt;span class="nv"&gt;$artifact_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a later step can build the human summary from those exact values instead of re-reading the whole file. That keeps the workflow pretty tidy, even when the original check touches odd search strings like &lt;code&gt;temp gamil com&lt;/code&gt; during test setup or fixture validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitHub Actions workflow I keep reusing
&lt;/h2&gt;

&lt;p&gt;The YAML is intentionally plain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;verify-scenario&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run scenario&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/run-scenario.sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write summary&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;echo "## Scenario result"&lt;/span&gt;
            &lt;span class="s"&gt;echo ""&lt;/span&gt;
            &lt;span class="s"&gt;echo "- Verdict: ${{ steps.check.outputs.verdict }}"&lt;/span&gt;
            &lt;span class="s"&gt;echo "- Wait ms: ${{ steps.check.outputs.wait_ms }}"&lt;/span&gt;
            &lt;span class="s"&gt;echo "- Artifact dir: ${{ steps.check.outputs.artifact_dir }}"&lt;/span&gt;
          &lt;span class="s"&gt;} &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload artifacts&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scenario-${{ github.run_id }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;artifacts/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub recommends artifacts for preserving workflow data after completion, and that is exactly the role I want here: the summary stays short, while the raw receipts are still available when someone needs to inspect them later (&lt;a href="https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The useful trick is that &lt;code&gt;Automation&lt;/code&gt; work becomes composable. One step decides. One step explains. One step archives. If you mix all three, the workflow usualy gets messy again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I include when debugging email-style checks
&lt;/h2&gt;

&lt;p&gt;I do not mean only email tests here. This also fits API polling, queue assertions, and webhook smoke tests. Still, email-like checks are a good example because they fail in annoying ways.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;result.json&lt;/code&gt; usually contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scenario&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verdict&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait_ms&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matched_subject&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attempt_count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;failure_reason&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, the summary only needs a few lines. If the verdict is &lt;code&gt;timeout&lt;/code&gt;, the next human already knows where to look. If the verdict is &lt;code&gt;subject_mismatch&lt;/code&gt;, the artifact bundle has the raw payload. That is enough context for a quick fix, and it saves the team from reading logs that were never designed to be a report.&lt;/p&gt;

&lt;p&gt;One more benefit: outputs force naming. When you have to expose &lt;code&gt;failure_reason&lt;/code&gt; or &lt;code&gt;attempt_count&lt;/code&gt;, you naturally clean up vague script behavior. That is maybe the most under-rated productivity win in &lt;code&gt;GitHub Actions&lt;/code&gt;. The workflow starts speaking in nouns instead of panic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Are step outputs enough on their own?
&lt;/h3&gt;

&lt;p&gt;No. They are for routing and compact status, not for evidence. If a failure needs deeper debugging, pair outputs with artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I pass large JSON through outputs?
&lt;/h3&gt;

&lt;p&gt;I would not. Keep outputs tiny and stable. Put the big JSON in a file, then upload it. Small contracts age better, and they break less often.&lt;/p&gt;

&lt;h3&gt;
  
  
  When does this pattern help the most?
&lt;/h3&gt;

&lt;p&gt;It helps most when one step produces a result and another step decides what happens next. If your current triage starts with "open the logs and scroll", you can probly improve it with this pattern in an afternoon.&lt;/p&gt;

&lt;p&gt;That is why I keep reusing it. Not because it is clever, but because it removes friction in the exact spot where teams lose time every week.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>API Smoke Tests Need Receipts</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Thu, 27 Aug 2026 17:23:53 +0000</pubDate>
      <link>https://dev.to/pong1965/api-smoke-tests-need-receipts-fhl</link>
      <guid>https://dev.to/pong1965/api-smoke-tests-need-receipts-fhl</guid>
      <description>&lt;p&gt;I like small smoke tests, but I do not like mystery failures. A lot of CI pipelines run one API call, get one red mark, and leave the next engineer to guess what changed. That is not really a smoke test anymore. It is a slot machine with logs.&lt;/p&gt;

&lt;p&gt;The fix that helped me most was boring: every smoke test writes a receipt. Not a huge artifact dump, just a compact record of what was called, what came back, how long it took, and why the job decided pass or fail. In GitHub Actions, this turns noisy checks into something you can triage in a minute, not half an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why smoke tests fail twice
&lt;/h2&gt;

&lt;p&gt;The first failure is the actual product issue. The second failure is the pipeline not explaining itself.&lt;/p&gt;

&lt;p&gt;This happens a lot with APIs because the request path, auth state, and response shape can all drift independently. A job might tell you &lt;code&gt;curl&lt;/code&gt; exited non-zero, but that still leaves a bunch of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did DNS fail or did auth fail?&lt;/li&gt;
&lt;li&gt;was the status code wrong or was the body wrong?&lt;/li&gt;
&lt;li&gt;was latency climbing for a few runs before it broke?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Actions gives us step summaries, annotations, and artifacts that are made for this kind of thing (&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions&lt;/a&gt;). If you use them together, the pipeline starts reading more like an incident receipt and less like a random terminal capture.&lt;/p&gt;

&lt;p&gt;I have seen the same benefit in adjacent workflows like &lt;a href="https://dev.to/ryanlee91/type-safe-invite-email-checks-for-react-apps-2dh7"&gt;type-safe invite email checks&lt;/a&gt;, where the main win is not more tooling, but better evidence around the tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipt pattern I add first
&lt;/h2&gt;

&lt;p&gt;My default shape is three files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;request.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;response.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;receipt.json&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are raw enough for debugging. The third one is the human layer. It should be tiny, stable, and easy to diff between runs.&lt;/p&gt;

&lt;p&gt;Here is the sort of receipt I mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing-health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"http_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema_ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"response passed but latency crossed warning budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smoke-4821"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one object lets the workflow print a clean summary, raise a warning when needed, and archive the detailed files without making the main log unreadable. It sounds tiny because it is tiny, but it saves a suprising amount of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A GitHub Actions shape that stays readable
&lt;/h2&gt;

&lt;p&gt;I prefer a single shell step that gathers evidence and then a second step that formats the verdict. Keeping those seperate makes reruns easier when a team wants to inspect the raw output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run billing smoke test&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;mkdir -p artifacts&lt;/span&gt;
    &lt;span class="s"&gt;echo "::group::Call health endpoint"&lt;/span&gt;
    &lt;span class="s"&gt;curl -sS \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Authorization: Bearer $API_TOKEN" \&lt;/span&gt;
      &lt;span class="s"&gt;-o artifacts/response.json \&lt;/span&gt;
      &lt;span class="s"&gt;-w "%{http_code}" \&lt;/span&gt;
      &lt;span class="s"&gt;https://api.example.com/health &amp;gt; artifacts/status.txt&lt;/span&gt;
    &lt;span class="s"&gt;echo "::endgroup::"&lt;/span&gt;

    &lt;span class="s"&gt;node scripts/write-receipt.mjs \&lt;/span&gt;
      &lt;span class="s"&gt;--status-file artifacts/status.txt \&lt;/span&gt;
      &lt;span class="s"&gt;--response-file artifacts/response.json \&lt;/span&gt;
      &lt;span class="s"&gt;--out artifacts/receipt.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Summarize result&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;status="$(jq -r '.status' artifacts/receipt.json)"&lt;/span&gt;
    &lt;span class="s"&gt;reason="$(jq -r '.reason' artifacts/receipt.json)"&lt;/span&gt;
    &lt;span class="s"&gt;latency="$(jq -r '.latency_ms' artifacts/receipt.json)"&lt;/span&gt;

    &lt;span class="s"&gt;if [ "$status" != "ok" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "::warning::${reason}"&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;

    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;echo "## API smoke test"&lt;/span&gt;
      &lt;span class="s"&gt;echo ""&lt;/span&gt;
      &lt;span class="s"&gt;echo "- Status: \`$status\`"&lt;/span&gt;
      &lt;span class="s"&gt;echo "- Latency: ${latency}ms"&lt;/span&gt;
      &lt;span class="s"&gt;echo "- Reason: $reason"&lt;/span&gt;
      &lt;span class="s"&gt;echo "- Files: response.json, receipt.json"&lt;/span&gt;
    &lt;span class="s"&gt;} &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is close to the same thinking behind keeping &lt;a href="https://dev.to/jasonmills94/keep-eks-rollout-alerts-tied-to-one-deploy-508i"&gt;rollout alerts tied to one deploy&lt;/a&gt;: each run should tell one coherent story. If the evidence spans several retries or environments, the workflow needs to stitch that into one obvious verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where temporary inbox checks fit
&lt;/h2&gt;

&lt;p&gt;A lot of product smoke tests are not just API calls. They are API calls plus side effects: invite mail, password reset mail, receipt mail, and trial onboarding mail. That is where teams often lose the clean story, because they validate the endpoint but not the outcome.&lt;/p&gt;

&lt;p&gt;When I need to add mailbox validation, I keep it as one bounded sub-check and give it the same receipt treatment. For example, the workflow might record inbox polling attempts, final message latency, and the subject line that matched. If I need a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;free temp email&lt;/a&gt; flow for a disposable verification path, I treat that as supporting evidence, not the entire test strategy.&lt;/p&gt;

&lt;p&gt;The useful bit is consistency. API step, inbox step, and final verdict should all share the same run id. If they do not, engineers end up grepping for weird strings like &lt;code&gt;tempail mail&lt;/code&gt; and hoping they found the correct attempt. That is the sort of mess a receipt pattern avoids.&lt;/p&gt;

&lt;p&gt;One more practical tip: keep warning budgets distinct from failure budgets. A 2-second response might still pass the smoke test today but deserve a warning, while a schema mismatch should fail imediately. Mixing those together makes dashboards look stable right untill they are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should every smoke test write JSON artifacts?
&lt;/h3&gt;

&lt;p&gt;Not every single one, but any check that can fail for multiple reasons probably should. The more branches in the diagnosis tree, the more value you get from a compact receipt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you put the full response body in the step summary?
&lt;/h3&gt;

&lt;p&gt;No. The summary should stay human-sized. Put the verdict there, and keep the raw body in artifacts. Otherwise the summary becomes another wall of text.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the biggest payoff?
&lt;/h3&gt;

&lt;p&gt;Faster reruns, faster triage, and fewer Slack threads asking what a red job even means. For developer tools and APIs work, that is a pretty high return for a very smal pattern.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>api</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Workflow Commands for Faster CI Triage</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Thu, 27 Aug 2026 14:24:34 +0000</pubDate>
      <link>https://dev.to/pong1965/workflow-commands-for-faster-ci-triage-245f</link>
      <guid>https://dev.to/pong1965/workflow-commands-for-faster-ci-triage-245f</guid>
      <description>&lt;p&gt;If a CI job fails and the first useful clue is hidden 700 lines up, the pipeline is not doing its job. I keep seeing this with API and email checks: the test itself is fine, but the logs are so flat and noisy that triage takes longer than the fix. One of the smaler changes that helped my teams most was using GitHub Actions workflow commands on purpose instead of treating them like trivia.&lt;/p&gt;

&lt;p&gt;This is not a fancy platform rewrite. It is a boring set of habits: group the right output, emit short warnings with context, and write a clean summary at the end. For checks that depend on a temporary email address or a free throwaway email in test flows, that structure matters a lot because timing bugs look random untill you line the evidence up the same way every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why raw CI logs slow teams down
&lt;/h2&gt;

&lt;p&gt;Most jobs already have enough signal. The problem is presentation. A broken HTTP assertion, a retry loop, and one delayed email event all land in the same unstructured stream. When a teammate opens the run, they have to manually reconstruct the story.&lt;/p&gt;

&lt;p&gt;That gets worse when the workflow does several things at once: boot services, run migrations, hit an API, poll for a message, then archive artifacts. Humans are bad at scanning that wall of text under pressure. GitHub's workflow commands exist partly so logs can be grouped, annotated, and summarized in a way the UI can surface cleanly (&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I like pairing that with patterns similar to &lt;a href="https://dev.to/pong1965/github-actions-summaries-for-email-checks-3coj"&gt;GitHub Actions summaries for email checks&lt;/a&gt;, because once the important evidence is collected in one place, people stop guessing and start comparing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow commands I add first
&lt;/h2&gt;

&lt;p&gt;The three commands I reach for first are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;::group::&lt;/code&gt; and &lt;code&gt;::endgroup::&lt;/code&gt; for collapsible blocks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;::warning::&lt;/code&gt; for soft failures or degraded paths&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$GITHUB_STEP_SUMMARY&lt;/code&gt; for the final human-readable recap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the shell shape I reuse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::group::Trigger verification flow"&lt;/span&gt;
./scripts/request-verification.sh &lt;span class="nt"&gt;--env&lt;/span&gt; ci &lt;span class="nt"&gt;--out&lt;/span&gt; artifacts/request.json
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::endgroup::"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::group::Poll inbox"&lt;/span&gt;
./scripts/wait-for-message.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scenario&lt;/span&gt; verification &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt; 20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out&lt;/span&gt; artifacts/inbox.json
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::endgroup::"&lt;/span&gt;

&lt;span class="nv"&gt;latency_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.latency_ms'&lt;/span&gt; artifacts/inbox.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;latency_ms&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 12000 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::warning::Email latency crossed 12s (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;latency_ms&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms)"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is basic stuff, but it changes the feel of a failing run imediately. Instead of reading every line, you expand the one block you need. Instead of scrolling for "maybe bad?" clues, you get warnings pinned where the issue happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small pattern for email and API checks
&lt;/h2&gt;

&lt;p&gt;I usually split the job into three evidence layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;request data&lt;/li&gt;
&lt;li&gt;delivery or API response data&lt;/li&gt;
&lt;li&gt;a short verdict object&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last part is the one teams skip too often. They keep the raw JSON, but they never write the one object that says what the run means. I prefer a tiny &lt;code&gt;result.json&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delayed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"http_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message_found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13840&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delivery exceeded warning budget"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the shell step can turn that into annotations and a summary without reparsing half the world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.status'&lt;/span&gt; artifacts/result.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.reason'&lt;/span&gt; artifacts/result.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"ok"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"::warning::&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"## Verification check"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- Status: &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- Reason: &lt;/span&gt;&lt;span class="nv"&gt;$reason&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- Artifacts: request.json, inbox.json, result.json"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also works well with &lt;a href="https://dev.to/jasonmills94/a-better-ci-check-for-aws-approval-emails-22na"&gt;better CI checks for approval emails&lt;/a&gt;, where the main win is not more automation, but cleaner evidence around the same automation.&lt;/p&gt;

&lt;p&gt;If your team is still searching odd strings like &lt;code&gt;tamp mail com&lt;/code&gt; during incident triage, that is often a smell that the workflow output is not giving them one obvious place to look first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I put in the job summary
&lt;/h2&gt;

&lt;p&gt;My rule is simple: the summary should answer whether the run is safe to ignore, safe to rerun, or worth opening an incident for. Anything beyond that belongs in artifacts.&lt;/p&gt;

&lt;p&gt;I usually include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scenario name&lt;/li&gt;
&lt;li&gt;environment&lt;/li&gt;
&lt;li&gt;final status&lt;/li&gt;
&lt;li&gt;one timing number&lt;/li&gt;
&lt;li&gt;one direct pointer to the saved artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub documents job summaries as a way to render Markdown attached to the run, which makes them ideal for this human-first recap (&lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary&lt;/a&gt;). For busy teams, this is realy the fastest before/after change: less log archaeology, more direct answers.&lt;/p&gt;

&lt;p&gt;One caution, though: do not dump secrets, full tokens, or entire payload bodies into the summary. Keep it tight. A summary is for triage, not for forensics. When you need deeper review, link the artifact names and let the next step stay seperate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do workflow commands replace proper observability?
&lt;/h3&gt;

&lt;p&gt;No. They just make CI less annoying and more honest. Observability tells you what the system did over time; workflow commands help the person staring at one failed run right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  When do you use a warning instead of failing the step?
&lt;/h3&gt;

&lt;p&gt;When the signal is useful but not release-blocking yet, like elevated email latency or an API fallback path that still passed. Warnings are a good way to show drift before it becomes a pager.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the biggest payoff?
&lt;/h3&gt;

&lt;p&gt;People spend less time interpreting logs and more time fixing the actual regression. For developer tools work, that is a surprizingly high-leverage improvement for such a small change.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>api</category>
      <category>devtools</category>
    </item>
    <item>
      <title>GitHub Actions Logs for Email API Retries</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 08:23:53 +0000</pubDate>
      <link>https://dev.to/pong1965/github-actions-logs-for-email-api-retries-ba8</link>
      <guid>https://dev.to/pong1965/github-actions-logs-for-email-api-retries-ba8</guid>
      <description>&lt;p&gt;Email API tests often look healthy right until a workflow turns flaky. The send step passes, the poller retries a few times, and then the job finally fails with a vague timeout. What is missing is not more retry logic. It is a better record of what each retry actually saw.&lt;/p&gt;

&lt;p&gt;I have found that GitHub Actions gets much easier to trust when retry attempts are treated like artifacts instead of throwaway console noise. This is specially useful when a workflow creates a disposable temporary email for a signup or verification path, because the inbox state changes across a short window and the timing matters a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email API retries become invisible in CI
&lt;/h2&gt;

&lt;p&gt;Many teams already log the final failure, but they do not log the sequence that led there. That creates a weird debugging gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;attempt 1 may have queried the wrong inbox&lt;/li&gt;
&lt;li&gt;attempt 2 may have matched the right inbox but the wrong subject&lt;/li&gt;
&lt;li&gt;attempt 3 may have found the message after the expiry window&lt;/li&gt;
&lt;li&gt;the final job summary only says "message not found"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pattern burns time because engineers end up re-running the workflow just to get fresh clues. The same idea shows up in &lt;a href="https://dev.to/silviutech/playwright-inbox-checks-that-do-not-flake-21j6"&gt;stable inbox checks&lt;/a&gt;: flakiness usually gets smaller once the workflow records exactly what it was trying to prove.&lt;/p&gt;

&lt;p&gt;One more thing I keep seeing is placeholder drift. A local helper might still mention &lt;code&gt;temp mailid&lt;/code&gt; or &lt;code&gt;tempail mail&lt;/code&gt; in a fixture name, while the CI step now uses a real inbox provider or a different address contract. The mismatch sounds tiny, but it makes logs harder to scan when you are already tired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log attempts as a first-class workflow artifact
&lt;/h2&gt;

&lt;p&gt;The most useful upgrade is boring: write one JSON line per poll attempt, then upload the file even on failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"elapsed_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"inbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"signup-1842"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"elapsed_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"empty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"inbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"signup-1842"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"elapsed_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"matched"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Verify your email"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file gives you a timeline, not just a verdict. It also means you can keep console output short while preserving the details that matter.&lt;/p&gt;

&lt;p&gt;I like pairing that with a markdown summary written to &lt;code&gt;$GITHUB_STEP_SUMMARY&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"## Email retry summary"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- inbox: &lt;/span&gt;&lt;span class="nv"&gt;$INBOX_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- attempts: &lt;/span&gt;&lt;span class="nv"&gt;$ATTEMPTS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- final state: &lt;/span&gt;&lt;span class="nv"&gt;$FINAL_STATE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- artifact: retry-log.jsonl"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the person opening the run gets the short story first, and the artifact second. That split is realy nice for on-call work because you do not have to read 500 lines before deciding whether the failure was expected noise or a real regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small GitHub Actions pattern that helps fast
&lt;/h2&gt;

&lt;p&gt;You do not need a big framework to do this. A few explicit steps are enough:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create the inbox contract&lt;/li&gt;
&lt;li&gt;trigger the API that should send the message&lt;/li&gt;
&lt;li&gt;poll on a fixed schedule&lt;/li&gt;
&lt;li&gt;append each attempt to a retry log&lt;/li&gt;
&lt;li&gt;publish the summary and upload the artifact&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The shell flow can stay small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUN_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"artifacts/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_RUN_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

./scripts/create-inbox.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt;
./scripts/send-verification.sh &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt;
./scripts/poll-email.sh &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt; &lt;span class="nt"&gt;--log&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/retry-log.jsonl"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- retry artifact: retry-log.jsonl"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's artifact docs are worth using here because retention and sharing are already built in: &lt;a href="https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts" rel="noopener noreferrer"&gt;https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts&lt;/a&gt;. If you have ever had to explain a failed workflow in chat, artifact links are a much cleaner handoff than pasted logs.&lt;/p&gt;

&lt;p&gt;This is also why I like &lt;a href="https://dev.to/mrdapperx/replay-logs-make-automation-clis-debuggable-1537"&gt;replay logs for automation&lt;/a&gt;. The principle is the same: if a tool makes decisions over time, capture the trail in a format another engineer can replay mentally in under a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to record for a disposable temporary email check
&lt;/h2&gt;

&lt;p&gt;For most APIs, I do not think you need the full email body in your retry log. A lean record is better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inbox or scenario ID&lt;/li&gt;
&lt;li&gt;attempt number&lt;/li&gt;
&lt;li&gt;elapsed time&lt;/li&gt;
&lt;li&gt;matched subject, if any&lt;/li&gt;
&lt;li&gt;message timestamp, if any&lt;/li&gt;
&lt;li&gt;terminal reason for stop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives enough context to see whether the retry budget is wrong, the send path is slow, or the lookup filter is too loose. If the workflow is testing link parsing or token extraction, then yes, add a richer artifact for that case only.&lt;/p&gt;

&lt;p&gt;One practical tip: keep retry intervals fixed inside the log output. Exponential backoff is fine for production clients, but in CI it can make failures harder to compare across runs. Consistent intervals are less clever, but way easier to debug and that tradeoff is usualy worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should every failed poll attempt be printed to the main log?
&lt;/h3&gt;

&lt;p&gt;No. Put the details in an artifact and keep the main log readable. The workflow summary should point to the artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this only for signup tests?
&lt;/h3&gt;

&lt;p&gt;No. Password reset, email change, team invite, and approval flows all benefit from the same pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the fastest win if a team is short on time?
&lt;/h3&gt;

&lt;p&gt;Start with one &lt;code&gt;retry-log.jsonl&lt;/code&gt; artifact and one short summary block. That alone removes a lot of guesswork from GitHub Actions runs.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>api</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Git Diffs Make Email Checks Reproducible</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:25:17 +0000</pubDate>
      <link>https://dev.to/pong1965/git-diffs-make-email-checks-reproducible-1o32</link>
      <guid>https://dev.to/pong1965/git-diffs-make-email-checks-reproducible-1o32</guid>
      <description>&lt;p&gt;When an email check fails in CI, the broken part is often not the mail step itself. It is the missing context around it. Somebody changed a template, a feature flag, or a redirect rule, but the workflow summary only says the inbox did not get what it expected. That leaves the next engineer spelunking through logs and guessing which change actualy mattered.&lt;/p&gt;

&lt;p&gt;What has worked better for me is saving the Git diff beside the inbox evidence for the same run. If a GitHub Actions job creates a disposable inbox, triggers a signup flow, and records the exact files changed in that commit or PR, the failure becomes much easier to replay. It turns the check from "email was weird" into "this diff changed the email path." That is a better debugging story, and it is faster too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why workflow logs are not enough
&lt;/h2&gt;

&lt;p&gt;Plain logs are fine for happy-path runs. They are lousy when you need to compare one flaky result against another. You can search them, sure, but they do not give you a stable object to diff across reruns.&lt;/p&gt;

&lt;p&gt;I keep coming back to the same lesson behind &lt;a href="https://dev.to/bitheirstake/signup-privacy-logs-need-expiration-rules-27m7"&gt;expiration rules for signup logs&lt;/a&gt; and &lt;a href="https://dev.to/silviutech/stop-guessing-in-playwright-email-waits-21og"&gt;more predictable email waits&lt;/a&gt;: test evidence should be compact, explicit, and reviewable later by somebody who was not in the room.&lt;/p&gt;

&lt;p&gt;For email automation, the minimum useful context is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the commit SHA or PR head SHA&lt;/li&gt;
&lt;li&gt;the changed files that touched the email path&lt;/li&gt;
&lt;li&gt;the inbox address or label used for the run&lt;/li&gt;
&lt;li&gt;the expected subject or event name&lt;/li&gt;
&lt;li&gt;the final verdict with wait time and mismatch reason&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one setup step used a &lt;code&gt;tepm mail com&lt;/code&gt; placeholder in local config while another script expected a normal inbox pattern, this structure exposes the mismatch pretty fast. You do not need more clever retries first. You need cleaner run evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save the diff that triggered the email path
&lt;/h2&gt;

&lt;p&gt;This is the part I wish more teams did sooner. Before creating the inbox, write a diff summary into the run directory. Not the full patch if it is huge, just the files and maybe a focused stat block.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUN_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"artifacts/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_RUN_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_BASE_REF&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/changed-files.txt"&lt;/span&gt;
git diff &lt;span class="nt"&gt;--stat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_BASE_REF&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/changed-files.stat"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your email assertion step can say, "This run failed after changes to &lt;code&gt;auth/signup.ts&lt;/code&gt;, &lt;code&gt;mail/templates/welcome.mjml&lt;/code&gt;, and &lt;code&gt;config/flags.ts&lt;/code&gt;." That tiny move gives reviewers a much smaller search space. It also makes Automation posts and runbooks way easier to explain, because the artifact folder starts reading like a real case file instead of random console noise.&lt;/p&gt;

&lt;p&gt;When I use a disposable inbox service such as &lt;code&gt;tempmailso&lt;/code&gt; for non-production checks, I want the inbox metadata and the Git evidence sitting side by side. Same run folder, same naming, no mystery. It sounds boring, but boring is what reproducible CI should feel like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build one run folder per check
&lt;/h2&gt;

&lt;p&gt;The pattern I like is one folder that owns everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;changed-files.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;changed-files.stat&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inbox.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trigger.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verdict.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then every script accepts the same folder path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/make-inbox.sh &lt;span class="nt"&gt;--out&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt;
./scripts/trigger-flow.sh &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/trigger.json"&lt;/span&gt;
./scripts/assert-email.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trigger&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/trigger.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--diff&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/changed-files.txt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/verdict.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last parameter matters more than it looks. Once the assertion script can read the diff summary, it can produce a better failure line. Instead of "message body mismatch," you get something closer to "message body mismatch after template and redirect changes." That is not fancy AI. It is just decent tooling, and it saves a lot of back-and-forth.&lt;/p&gt;

&lt;p&gt;I also like dropping tiny human clues into the verdict when helpful. Maybe the setup used a &lt;code&gt;temp mailid&lt;/code&gt; for a smoke test label. Fine. Put it in the artifact plainly so the next person is not left inferring it from bash history.&lt;/p&gt;

&lt;h2&gt;
  
  
  A GitHub Actions pattern that is easy to replay
&lt;/h2&gt;

&lt;p&gt;The workflow can stay small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;capture diff context&lt;/li&gt;
&lt;li&gt;create inbox&lt;/li&gt;
&lt;li&gt;trigger the app path&lt;/li&gt;
&lt;li&gt;poll for the email with fixed boundaries&lt;/li&gt;
&lt;li&gt;write a verdict artifact&lt;/li&gt;
&lt;li&gt;upload the folder&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The upload step is important because GitHub Actions artifacts can be retained for a configurable period, which makes reruns and PR review much less hand-wavy. GitHub documents the artifact flow here: &lt;a href="https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts" rel="noopener noreferrer"&gt;https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is the shell shape I keep around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUN_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"artifacts/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_RUN_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_BASE_REF&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/changed-files.txt"&lt;/span&gt;
git diff &lt;span class="nt"&gt;--stat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_BASE_REF&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/changed-files.stat"&lt;/span&gt;
./scripts/create-inbox.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt;
./scripts/trigger-signup.sh &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/trigger.json"&lt;/span&gt;
./scripts/assert-message.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trigger&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/trigger.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--diff&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/changed-files.txt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/verdict.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup is not trying to do everything. It just keeps Git, GitHub Actions, and the inbox evidence in one place so a rerun is understandable. That is the real productivity win for developer tools work, at least for me. Less detective work, fewer "works on my branch" debates, and faster fixes when the email path shifts a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I store the full patch?
&lt;/h3&gt;

&lt;p&gt;Usually no. File names and a stat summary are enough for most email checks. If you archive giant diffs for every run, the workflow gets noisy real fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this only useful for signup emails?
&lt;/h3&gt;

&lt;p&gt;Nope. Password resets, invite flows, billing notices, or release alerts all benefit when the evidence points back to the exact code changes that probably caused the behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  What improves first after adopting this?
&lt;/h3&gt;

&lt;p&gt;Review speed. Engineers stop rereading raw logs and start looking at one small folder with the diff, inbox data, and verdict. It is not glamorous, but it is realy effective.&lt;/p&gt;

</description>
      <category>git</category>
      <category>githubactions</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>API Inbox Contracts in GitHub Actions</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:24:44 +0000</pubDate>
      <link>https://dev.to/pong1965/api-inbox-contracts-in-github-actions-31ee</link>
      <guid>https://dev.to/pong1965/api-inbox-contracts-in-github-actions-31ee</guid>
      <description>&lt;p&gt;Email API tests rarely fail because the send endpoint is mysterious. They fail because the workflow around the inbox is fuzzy. One step creates an address, another step polls for the message, and a third step writes a summary that leaves out the run context you actualy need.&lt;/p&gt;

&lt;p&gt;I have had better results by freezing an inbox contract before GitHub Actions starts doing real work. The goal is simple: each run should declare which inbox it owns, what message shape it expects, and what artifact it will produce at the end. That sounds small, but it cuts a lot of noisy debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email API checks fail in CI
&lt;/h2&gt;

&lt;p&gt;The annoying part of email verification tests is not the happy path. It is the second rerun, when someone else on the team tries to understand whether the failure came from the API, the queue, or the inbox lookup. If the workflow only says "message not found", you are stuck guessing.&lt;/p&gt;

&lt;p&gt;That is why I like patterns such as &lt;a href="https://dev.to/silviutech/playwright-inbox-filters-for-flaky-signup-tests-24io"&gt;tighter inbox filters&lt;/a&gt; and &lt;a href="https://dev.to/sophiax99/magic-link-emails-need-redirect-guardrails-3lfk"&gt;redirect guardrails for auth emails&lt;/a&gt;. Both point at the same lesson: define the evidence you want before the test runs, not after it breaks.&lt;/p&gt;

&lt;p&gt;In practice, most flaky checks have one of these problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the inbox identifier is generated but never saved&lt;/li&gt;
&lt;li&gt;the workflow summary shows pass or fail, but not the message metadata&lt;/li&gt;
&lt;li&gt;retries happen invisibly, so the final state is hard to trust&lt;/li&gt;
&lt;li&gt;one job uses a temp gamil com style placeholder during setup and another job assumes a different address format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is advanced engineering. It is just contract drift, and it makes ordinary API work feel way more chaotic than it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze the inbox contract before the workflow starts
&lt;/h2&gt;

&lt;p&gt;I like to write one JSON file near the start of the run and keep every later step honest against it. That file is tiny on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gha-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inbox_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-verification-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confirm your account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_wait_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every script has the same source of truth. The inbox creator reads it. The API trigger step reads it. The polling step reads it. If one piece wants to improvise, the file exposes that pretty fast.&lt;/p&gt;

&lt;p&gt;For teams that need a &lt;code&gt;generate throwaway email&lt;/code&gt; step, I would still keep that creation command dumb and explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/create-inbox.sh &lt;span class="nt"&gt;--contract&lt;/span&gt; artifacts/inbox-contract.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; artifacts/inbox.json
./scripts/trigger-signup.sh &lt;span class="nt"&gt;--contract&lt;/span&gt; artifacts/inbox-contract.json &lt;span class="nt"&gt;--inbox&lt;/span&gt; artifacts/inbox.json
./scripts/assert-message.sh &lt;span class="nt"&gt;--contract&lt;/span&gt; artifacts/inbox-contract.json &lt;span class="nt"&gt;--inbox&lt;/span&gt; artifacts/inbox.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flow is not glamorous, but it is handoff-friendly. If a coworker opens the artifact folder, they can infer what happened in about thirty seconds, which is a big win realy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish one small verdict artifact
&lt;/h2&gt;

&lt;p&gt;The other habit that pays off is writing one verdict file at the end instead of spraying clues across log lines. I usually want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run ID&lt;/li&gt;
&lt;li&gt;inbox address&lt;/li&gt;
&lt;li&gt;trigger response code&lt;/li&gt;
&lt;li&gt;whether the message arrived&lt;/li&gt;
&lt;li&gt;matched subject&lt;/li&gt;
&lt;li&gt;observed wait time&lt;/li&gt;
&lt;li&gt;final failure reason&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last field matters. "Timed out" is weaker than "timed out after 45 seconds waiting for Confirm your account". Small wording upgrades like that save a lot of back-and-forth.&lt;/p&gt;

&lt;p&gt;I also prefer uploading the verdict as a workflow artifact so reruns and handoffs stay comparable. GitHub documents artifact retention and sharing clearly in its workflow docs (&lt;a href="https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts" rel="noopener noreferrer"&gt;https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts&lt;/a&gt;). You do not need a huge archive. One compact JSON file and maybe one log excerpt are usualy enough.&lt;/p&gt;

&lt;p&gt;This is also where weird placeholder mistakes surface early. If somebody typed &lt;code&gt;tempail mail&lt;/code&gt; into a local config while mocking an address pattern, the verdict file makes the mismatch obvious instead of burying it in 400 lines of console output.&lt;/p&gt;

&lt;h2&gt;
  
  
  A GitHub Actions pattern that stays debuggable
&lt;/h2&gt;

&lt;p&gt;The workflow shape I keep coming back to is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create contract&lt;/li&gt;
&lt;li&gt;create inbox&lt;/li&gt;
&lt;li&gt;trigger API call&lt;/li&gt;
&lt;li&gt;poll with fixed wait boundaries&lt;/li&gt;
&lt;li&gt;write verdict&lt;/li&gt;
&lt;li&gt;upload artifact&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the core shell flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUN_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"artifacts/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_RUN_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

./scripts/make-contract.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox-contract.json"&lt;/span&gt;
./scripts/create-inbox.sh &lt;span class="nt"&gt;--contract&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox-contract.json"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt;
./scripts/trigger-signup.sh &lt;span class="nt"&gt;--contract&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox-contract.json"&lt;/span&gt; &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt;
./scripts/assert-message.sh &lt;span class="nt"&gt;--contract&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox-contract.json"&lt;/span&gt; &lt;span class="nt"&gt;--inbox&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/inbox.json"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_DIR&lt;/span&gt;&lt;span class="s2"&gt;/verdict.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this because each command does one thing, and the run folder becomes the portable truth of the test. If a workflow fails overnight, the next engineer does not need your mental notes. They need the contract and the verdict.&lt;/p&gt;

&lt;p&gt;That is the productivity angle for me. Better APIs and better GitHub Actions setups are nice, but the real quality jump comes from making CI evidence boring, consistent, and easy to diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should the workflow store the full email body?
&lt;/h3&gt;

&lt;p&gt;Only if the body itself is under test. For most API checks, subject, recipient, timing, and one verification link host are enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many retries should the poller hide?
&lt;/h3&gt;

&lt;p&gt;Very few. I would rather expose three explicit attempts than bury ten retries behind a "smart" helper. Hidden resilience often becomes hidden confusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes first when a team adopts this?
&lt;/h3&gt;

&lt;p&gt;Debugging gets less social. People stop asking who last touched the script and start reading the same artifacts, which is a much healthier place to be.&lt;/p&gt;

</description>
      <category>api</category>
      <category>githubactions</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>GitHub Actions Need Email Run Artifacts</title>
      <dc:creator>Jonathan</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:24:48 +0000</pubDate>
      <link>https://dev.to/pong1965/github-actions-need-email-run-artifacts-170m</link>
      <guid>https://dev.to/pong1965/github-actions-need-email-run-artifacts-170m</guid>
      <description>&lt;h1&gt;
  
  
  GitHub Actions Need Email Run Artifacts
&lt;/h1&gt;

&lt;p&gt;When an email API check fails in CI, the workflow log is rarely enough on its own. You can see a request, maybe a 202 response, maybe a timeout later, and still have no clue what actually happened to that one scenario. That gap is why I started treating email evidence as a first-class artifact instead of a side effect buried in logs.&lt;/p&gt;

&lt;p&gt;This got more obvious after seeing issue notes with strings like &lt;code&gt;tamp mail com&lt;/code&gt; and &lt;code&gt;temp mailid&lt;/code&gt; copied from hurried triage. The test was not broken in one dramatic place. The run just did not preserve enough evidence to explain itself. Once that happens, people re-run the job, tweak a timeout, and hope the failure stays gone. Thats fast, but it is not real debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email checks fail after the API already worked
&lt;/h2&gt;

&lt;p&gt;Most flaky email checks in GitHub Actions are not about sending. They are about matching.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workflow knows a message was requested, but not which recipient belonged to which scenario.&lt;/li&gt;
&lt;li&gt;The inbox poller saves only the final error, not the near matches.&lt;/li&gt;
&lt;li&gt;Parallel jobs reuse naming patterns that look unique until traffic gets busy.&lt;/li&gt;
&lt;li&gt;The summary says "verification email missing" even though the app sent something slightly different.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub's own documentation on &lt;a href="https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts" rel="noopener noreferrer"&gt;workflow artifacts&lt;/a&gt; is not about email specifically, but the principle fits perfectly: if a run produces information you will need later, persist it while the run is still alive.&lt;/p&gt;

&lt;p&gt;I also like comparing my setup against articles on &lt;a href="https://dev.to/pong1965/concurrency-keys-for-email-api-checks-5fd2"&gt;concurrency-safe inbox checks&lt;/a&gt; and &lt;a href="https://dev.to/mrdapperx/preview-env-email-checks-with-one-run-id-3gad"&gt;preview environment inbox runs&lt;/a&gt;. Both push toward the same idea: make every run explainable before you make it faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact set I keep for every CI run
&lt;/h2&gt;

&lt;p&gt;For email-related APIs, I want four small files from every workflow run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;request.json&lt;/code&gt; with scenario id, recipient, endpoint, and send timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;poll-log.json&lt;/code&gt; with each inbox check attempt and when it happened.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;matches.json&lt;/code&gt; with the messages that almost matched but did not.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verdict.json&lt;/code&gt; with the final pass or fail reason.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds a bit boring, but it changes triage alot. Instead of asking "did email break again?", you can ask "which condition failed for this scenario?" Those are very different conversations.&lt;/p&gt;

&lt;p&gt;Here is the pattern I keep coming back to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write email evidence&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;mkdir -p artifacts/email&lt;/span&gt;
    &lt;span class="s"&gt;cp .tmp/request.json artifacts/email/request.json&lt;/span&gt;
    &lt;span class="s"&gt;cp .tmp/poll-log.json artifacts/email/poll-log.json&lt;/span&gt;
    &lt;span class="s"&gt;cp .tmp/matches.json artifacts/email/matches.json&lt;/span&gt;
    &lt;span class="s"&gt;cp .tmp/verdict.json artifacts/email/verdict.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload email artifacts&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-run-${{ github.run_id }}-${{ matrix.scenario }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;artifacts/email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is not the upload step by itself. It is the naming discipline around it. I want the artifact name to map cleanly to one scenario, one recipient, and one run. If that mapping is fuzzy, the artifact exists but the debug value is still weak.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small GitHub Actions pattern that scales better
&lt;/h2&gt;

&lt;p&gt;The lowest-friction improvement is to create a run-scoped envelope before the first API call. Mine is usually just a tiny JSON object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenarioId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-1742"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qa+signup-1742@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedSubject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Verify your account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-08T17:22:21Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every script in the workflow reads from that envelope and appends evidence back to disk. The sender script writes request metadata. The poller writes attempts. The assertion step writes the verdict. Seperately, those files are ordinary. Together, they give you a clean timeline.&lt;/p&gt;

&lt;p&gt;I also keep the workflow summary short and point it at the artifact, not the whole story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"### Email check"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- Scenario: &lt;/span&gt;&lt;span class="nv"&gt;$SCENARIO_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- Recipient: &lt;/span&gt;&lt;span class="nv"&gt;$RECIPIENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"- Verdict: see uploaded artifact email-run-&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_RUN_ID&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$SCENARIO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_STEP_SUMMARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split matters. A summary should help a developer decide where to look next. It should not try to become the evidence store. When teams cram every detail into log output, review gets noisy realy fast.&lt;/p&gt;

&lt;p&gt;If you use a &lt;code&gt;free throwaway email&lt;/code&gt; flow for automated verification, this artifact-first layout also makes local replay easier. You can download one run, inspect the evidence, and compare it against the API payload without reopening half the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to review before blaming the mail provider
&lt;/h2&gt;

&lt;p&gt;Before I blame the provider, I check these in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did the workflow bind one recipient to one scenario only?&lt;/li&gt;
&lt;li&gt;Did the polling logic record near matches and headers?&lt;/li&gt;
&lt;li&gt;Did the expected subject or template drift from the product change?&lt;/li&gt;
&lt;li&gt;Did parallel jobs generate overlapping identifiers?&lt;/li&gt;
&lt;li&gt;Did the final verdict file explain the fail in one sentence?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If two or three of those are weak, the mail provider is often just the easiest thing to blame. The run itself is under-instrumented.&lt;/p&gt;

&lt;p&gt;This is also where developer tooling pays off. Better APIs help, but better evidence helps more often. A workflow that can explain why it failed is easier to trust, easier to hand off, and usualy cheaper to maintain over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Q&amp;amp;A
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Should I upload artifacts for passing runs too?
&lt;/h2&gt;

&lt;p&gt;Yes, at least for a retention window that matches your debugging cycle. Passing examples are useful baselines when a failure suddenly looks wierd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this only for end-to-end tests?
&lt;/h2&gt;

&lt;p&gt;No. It works for signup verification, password reset, invite flows, billing emails, and internal notification checks. Any workflow that waits on an inbox can benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the smallest version worth shipping?
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;request.json&lt;/code&gt;, &lt;code&gt;poll-log.json&lt;/code&gt;, and &lt;code&gt;verdict.json&lt;/code&gt;. That is enough to make most failures explainable without turning the pipeline into a science project.&lt;/p&gt;

&lt;p&gt;Email API automation gets calmer once each run leaves a trail you can read in five minutes. That is the productivity win I chase now, because fewer mystery failures means less rerun theater and more time fixing the actual system.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>api</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
