<?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: jasonmills94</title>
    <description>The latest articles on DEV Community by jasonmills94 (@jasonmills94).</description>
    <link>https://dev.to/jasonmills94</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%2F4012747%2F51c617f2-608c-4db8-b581-7c14edff63e9.png</url>
      <title>DEV Community: jasonmills94</title>
      <link>https://dev.to/jasonmills94</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jasonmills94"/>
    <language>en</language>
    <item>
      <title>Isolate Email Tests in Docker CI on AWS</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Fri, 30 Oct 2026 11:23:04 +0000</pubDate>
      <link>https://dev.to/jasonmills94/isolate-email-tests-in-docker-ci-on-aws-3pc5</link>
      <guid>https://dev.to/jasonmills94/isolate-email-tests-in-docker-ci-on-aws-3pc5</guid>
      <description>&lt;p&gt;Email verification tests become unreliable when every CI run shares the same inbox. A retry can consume the token from the first attempt, a parallel branch can read another branch's message, and an old message can make a broken flow look healthy.&lt;/p&gt;

&lt;p&gt;I have had better results treating the test mailbox like a short-lived infrastructure resource. Each run gets an identity, a lease, and a cleanup rule. The application still sends a normal verification email, but the test knows exactly which inbox belongs to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email tests leak across CI runs
&lt;/h2&gt;

&lt;p&gt;The usual shortcut is to configure one test address in a repository secret and poll it until a message arrives. It works on a quiet day. Under parallel CI, it creates a race:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build A -&amp;gt; shared inbox -&amp;gt; token A
build B -&amp;gt; shared inbox -&amp;gt; token B
build A reads whichever message arrives first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is not only the inbox. Test state also lives in the application database, the Docker network, and the CI retry history. A reused fake email address can leave an account, token, or delivery event behind. Then the next run starts with facts it did not create.&lt;/p&gt;

&lt;p&gt;The first rule is simple: a test run must be able to prove ownership of the message it reads. A run ID in the address, a correlation header, or both is enough for most staging systems. The test is not done when the container starts; its done when the expected message is matched and its evidence is saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every run an isolated mailbox lease
&lt;/h2&gt;

&lt;p&gt;Create a mailbox lease before starting the application stack. The lease record should contain at least:&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;"github-1842-7f31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&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-github-1842-7f31@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;"expires_at"&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-10-30T12:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payments-api"&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;"active"&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;The exact provider is less important than the contract. If a test needs a generated throwaway email, the generator should return a unique address and a cleanup deadline together. For short staging checks, a service such as &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmail.so&lt;/a&gt; can be one option for inspecting a disposable inbox, but it should not become a hidden dependency in production code.&lt;/p&gt;

&lt;p&gt;Keep the lease in the CI job's environment, not in a global configuration 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="nb"&gt;export &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;"verify-&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;@example.test"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TEST_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;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; compose.ci.yml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;GITHUB_RUN_ATTEMPT&lt;/code&gt; part matters. A retry of the same workflow should not quietly reuse the state created by the failed attempt. It is a small detail, but it saves a lot of confusing debugging later.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Docker and AWS workflow
&lt;/h2&gt;

&lt;p&gt;In Docker CI, put the application, worker, and email-capture dependency on the same isolated network. Pass only the lease values into the app container. Do not bake a shared mailbox into the image or commit it to a test fixture.&lt;/p&gt;

&lt;p&gt;On AWS, the same pattern works with ephemeral task or namespace metadata. For example, an ECS task can receive &lt;code&gt;TEST_RUN_ID&lt;/code&gt; and &lt;code&gt;TEST_EMAIL&lt;/code&gt; from the job that launched it. A Kubernetes job can use the run ID as part of its namespace and label. The infrastructure name then becomes useful evidence when a test fails.&lt;/p&gt;

&lt;p&gt;Add a short receipt after the message is found. This follows the idea behind &lt;a href="https://dev.to/pong1965/api-smoke-tests-need-receipts-fhl"&gt;API smoke tests with receipts&lt;/a&gt;: record what was observed, not just that an assertion passed.&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;"github-1842-7f31"&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_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;"mail-8841"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"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;"received_at"&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-10-30T12:04:18Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_asserted"&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="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;Store this receipt with the CI artifacts. When the test fails, you can tell whether the application never sent the message, delivery was delayed, or the poller selected the wrong message. That distinction is more useful than a screenshot of a red assertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure handling and cleanup
&lt;/h2&gt;

&lt;p&gt;Polling needs a deadline and a clear terminal state. Use a bounded loop, filter by run ID and subject, and mark the result as &lt;code&gt;received&lt;/code&gt;, &lt;code&gt;timed_out&lt;/code&gt;, or &lt;code&gt;invalid&lt;/code&gt;. Never let an unbounded poll keep an AWS task alive forever.&lt;/p&gt;

&lt;p&gt;Cleanup should run in a &lt;code&gt;finally&lt;/code&gt; step even when the test fails. Delete the mailbox lease, remove the Docker stack, and expire any test account created by the scenario. If the cleanup job fail, the lease expiration is the second safety net. It should be short enough to control cost but long enough for a developer to inspect artifacts.&lt;/p&gt;

&lt;p&gt;One warning: avoid putting the full email body into normal application logs. Tokens and reset links are credentials for the duration of the test. Keep a redacted receipt, and restrict access to the CI artifact. This is also why a typo such as &lt;code&gt;tamp mail com&lt;/code&gt; should stay in test content only, never in a real customer workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Create a new mailbox lease for every run and retry attempt.&lt;/li&gt;
&lt;li&gt;Include the run ID in the address, metadata, or both.&lt;/li&gt;
&lt;li&gt;Run Docker services on an isolated network.&lt;/li&gt;
&lt;li&gt;Filter messages by ownership, subject, and a bounded time window.&lt;/li&gt;
&lt;li&gt;Save a redacted delivery receipt as a CI artifact.&lt;/li&gt;
&lt;li&gt;Clean up in a &lt;code&gt;finally&lt;/code&gt; step with lease expiry as a backup.&lt;/li&gt;
&lt;li&gt;Keep tokens and links out of ordinary logs.&lt;/li&gt;
&lt;li&gt;Connect infrastructure alerts to the same run ID, like &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;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should every test create a new fake email address?
&lt;/h3&gt;

&lt;p&gt;Every isolated run should. Individual assertions inside one run can share a lease when they deliberately test the same account lifecycle. Separate parallel scenarios should not share it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a local email catcher enough?
&lt;/h3&gt;

&lt;p&gt;It is often enough for unit and integration tests. Use a controlled remote inbox when you need to verify provider delivery, DNS, or the same path used by staging. Keep that boundary explicit, so a provider outage does not look like a Docker regression.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the most useful first change?
&lt;/h3&gt;

&lt;p&gt;Add a run ID to the mailbox and save one delivery receipt. That small change usually exposes whether the flakiness comes from shared state, slow delivery, or a missing cleanup path.&lt;/p&gt;

&lt;p&gt;Reliable email tests are mostly an ownership problem. Give each Docker CI run a mailbox lease, carry its identity into AWS resources, and leave behind enough evidence to replay the failure. The test suite becomes a little more structured, but the failures become much less mysterious.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>docker</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>Docker CI Needs a Mailbox Lease on AWS</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Tue, 27 Oct 2026 02:23:02 +0000</pubDate>
      <link>https://dev.to/jasonmills94/docker-ci-needs-a-mailbox-lease-on-aws-2i4b</link>
      <guid>https://dev.to/jasonmills94/docker-ci-needs-a-mailbox-lease-on-aws-2i4b</guid>
      <description>&lt;p&gt;Email-driven tests become unreliable when every CI job shares one inbox. A retry can read an old verification message, a parallel build can consume the wrong code, and a failed cleanup can leave test data around for the next run.&lt;/p&gt;

&lt;p&gt;The fix I use is to treat test email as a leased capability. A Docker job gets one short-lived mailbox, one correlation key, and one clear owner. The job can read only the messages for its run, then releases the mailbox when the test finishes. This makes a free temp email useful for automation without making the inbox a shared test database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a mailbox lease belongs in the CI design
&lt;/h2&gt;

&lt;p&gt;An email assertion has two separate jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prove that the application sent the expected message.&lt;/li&gt;
&lt;li&gt;Prove that this CI run received and consumed its own message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first is a product check. The second is an isolation check. Mixing them creates green builds that are hard to trust.&lt;/p&gt;

&lt;p&gt;The pattern is similar to &lt;a href="https://dev.to/mrdapperx/keep-publish-retries-immutable-2pcd"&gt;keeping retry inputs immutable&lt;/a&gt;. A retry should get the same declared inputs, but it should not inherit an unknown inbox state from an earlier attempt. The mailbox lease gives each run a small, inspectable boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small architecture for Docker and AWS
&lt;/h2&gt;

&lt;p&gt;Keep the email service outside the application container, but keep its client and test runner in the same Docker job. On AWS, the flow can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI runner
  └── Docker test container
        ├── request mailbox lease
        ├── start application flow
        ├── poll message by lease + run ID
        └── write redacted result
                 │
                 └── mailbox service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container receives a short-lived service token through the CI secret mechanism. It should not receive a long-lived account credential or a list of reusable inboxes. The lease response needs only a mailbox identifier, an address, an expiration time, and a random correlation key.&lt;/p&gt;

&lt;p&gt;For an AWS pipeline, store the lease metadata with the job's normal artifacts, but keep message bodies out of the artifact bundle. A small JSON record is enough:&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;"build-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;"mailbox_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;"lease-91"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correlation_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-7f2a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"received_message_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;"msg-733"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"matched"&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 is also useful when &lt;a href="https://dev.to/jasonmills94/cloudwatch-alarm-emails-need-deploy-evidence-4ee1"&gt;connecting alarm emails to deploy evidence&lt;/a&gt;: operators can see which run owned the signal without exposing the whole message in a log search.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lease contract and correlation key
&lt;/h2&gt;

&lt;p&gt;The contract should answer four questions before the test starts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who owns this mailbox?&lt;/li&gt;
&lt;li&gt;When does ownership expire?&lt;/li&gt;
&lt;li&gt;Which messages belong to this run?&lt;/li&gt;
&lt;li&gt;What happens if the lease cannot be released?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I generate the run ID in CI and a separate unpredictable correlation key in the mailbox service. Put the correlation key in a harmless header or a test-only message field when the application supports it. Do not rely only on the recipient address or subject line; those values can repeat.&lt;/p&gt;

&lt;p&gt;A disposable mail address is not automatically isolated. Isolation comes from ownership, expiration, and a server-side lookup that filters by lease. If the service cannot filter by lease, use a unique address per run and reject any message received before the run start time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling without creating false positives
&lt;/h2&gt;

&lt;p&gt;Polling should be bounded and observable. A simple loop can use a short interval, a total deadline, and an explicit match condition:&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;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;SECONDS &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; SECONDS &amp;lt; deadline &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mailbox-cli messages &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--lease&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAILBOX_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--correlation&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CORRELATION_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--after&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_STARTED_AT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--format&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
  sleep &lt;/span&gt;3
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command is intentionally scoped to the lease. Fetching the newest message and hoping it is the right one is a bit more faster to implement, but it creates a false-positive path. When the message does not arrive, record whether the failure was send, delivery, lookup, parsing, or timeout. This check are much more useful than a generic “email test failed”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure evidence and cleanup
&lt;/h2&gt;

&lt;p&gt;Always release the lease in a cleanup step, including when the test container exits with a failing status. In Docker, a shell trap or the CI platform's post-job hook can call the release endpoint. Make release idempotent so a retry of cleanup does not turn a useful failure into another failure.&lt;/p&gt;

&lt;p&gt;Keep only redacted evidence: run ID, lease ID, message ID, timestamps, and a match result. Do not save verification tokens, complete message bodies, or credentials. If a developer searches for a fixture such as &lt;code&gt;temp gamil com&lt;/code&gt;, it should remain plain test data and never become a production routing rule.&lt;/p&gt;

&lt;p&gt;Sometimes the fastest diagnosis is a short-lived manual mailbox. If that is needed, use a controlled service such as &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; only within the team's testing and privacy policy, and never place its address or message contents in public CI logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions teams usually ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can several containers share one lease?
&lt;/h3&gt;

&lt;p&gt;Only when they are one logical test and the service supports consumer ownership. For independent tests, issue separate leases. Shared readers make order-dependent failures almost certain eventually.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if AWS delivery is slow?
&lt;/h3&gt;

&lt;p&gt;Increase the polling deadline based on observed delivery time, not a guess. Keep the deadline visible in metrics and fail with the last stage reached. A longer timeout should not mean an unbounded inbox read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the mailbox be reused between builds?
&lt;/h3&gt;

&lt;p&gt;Usually no. Reuse saves a small setup step but increases stale-message and ownership risk. Reuse is acceptable only when messages are namespaced by run and the service can prove that old messages cannot match a new lease.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical implementation checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Create a unique run ID before requesting email.&lt;/li&gt;
&lt;li&gt;Issue one short-lived lease to each independent Docker test.&lt;/li&gt;
&lt;li&gt;Filter messages by lease, correlation key, and start time.&lt;/li&gt;
&lt;li&gt;Keep tokens and full bodies out of logs and artifacts.&lt;/li&gt;
&lt;li&gt;Report send, delivery, lookup, parsing, and timeout separately.&lt;/li&gt;
&lt;li&gt;Release the lease in success and failure paths.&lt;/li&gt;
&lt;li&gt;Test cleanup itself, including repeated release calls.&lt;/li&gt;
&lt;li&gt;Review the next failed run using its redacted evidence record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This arrangement adds a little setup, but it removes a large class of flaky CI failures. The important part is the contract: the mailbox belongs to one run, the evidence proves that ownership, and the cleanup has a defined end.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>aws</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>Kubernetes CI: Bound Test Mail to the Job</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:23:17 +0000</pubDate>
      <link>https://dev.to/jasonmills94/kubernetes-ci-bound-test-mail-to-the-job-47g5</link>
      <guid>https://dev.to/jasonmills94/kubernetes-ci-bound-test-mail-to-the-job-47g5</guid>
      <description>&lt;p&gt;Email verification tests become unreliable when every Kubernetes job shares the same inbox. One job can read another job's message, a retry can consume an old message, and cleanup becomes somebody else's problem. The failure usually appears as a random timeout, but the underlying issue is missing ownership.&lt;/p&gt;

&lt;p&gt;I get better results when test mail is treated as an ephemeral infrastructure resource. A CI job leases one mailbox, writes the lease identity into its test context, and releases it when the job finishes. The mailbox is not just an address; it is a resource with an owner, a lifetime, and evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a shared test inbox breaks CI isolation
&lt;/h2&gt;

&lt;p&gt;Parallel CI is good at exposing assumptions. If five pods send verification messages to one inbox, each test has to guess which message belongs to it. Subject matching helps, but it does not prove that the message was generated by the current run. A delayed message from a previous attempt can still look valid.&lt;/p&gt;

&lt;p&gt;The result is a race that is hard to reproduce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Job A -&amp;gt; requests code -&amp;gt; reads the message for Job B
Job B -&amp;gt; waits -&amp;gt; times out
Job A retry -&amp;gt; reads an old message and passes by accident
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same boundary matters for links and webhook-driven mail. If a test follows an email link, it should retain the run ID that requested the message. That is similar to &lt;a href="https://dev.to/ryanlee91/react-reducers-for-async-signup-steps-1863"&gt;state handling for async signup steps&lt;/a&gt;: the next state should be accepted only when it belongs to the operation that created it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the mailbox lease
&lt;/h2&gt;

&lt;p&gt;Before adding Kubernetes YAML, define a small contract for the lease:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lease has a unique ID, such as &lt;code&gt;ci-4831-7f2a&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The address is assigned to one job attempt, not to an entire branch.&lt;/li&gt;
&lt;li&gt;The test reader filters by lease ID, recipient, and message type.&lt;/li&gt;
&lt;li&gt;The lease expires automatically if the pod disappears.&lt;/li&gt;
&lt;li&gt;The cleanup action is safe to repeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lease record can be kept in the test service or a small backing store. It should contain the mailbox identifier, creation time, expiry time, and owner. Do not put an access token in a pod label or a public test report. Put credentials in a Kubernetes Secret and expose only the mailbox address and lease ID to the test process.&lt;/p&gt;

&lt;p&gt;It is also worth normalizing noisy input. Search-like strings such as &lt;code&gt;fake e mail com&lt;/code&gt; and &lt;code&gt;temp gamil com&lt;/code&gt; can show up in fixtures or generated test data. Keep them as test cases when they matter, but do not let malformed values become mailbox names or production telemetry dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind the lease to a Kubernetes Job
&lt;/h2&gt;

&lt;p&gt;The Job should carry the ownership data as environment variables. This makes a failed run inspectable without copying the full email body into logs.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generateName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-check-&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-check&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ttlSecondsAfterFinished&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
  &lt;span class="na"&gt;backoffLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;workload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-check&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;tests&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/email-tests:2026.10&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&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;MAILBOX_LEASE_ID&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;fieldRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;fieldPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metadata.uid&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;MAILBOX_API_TOKEN&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&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;test-mail-reader&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pod UID is useful as a correlation value, but the service should still create a lease record before the first email assertion. A restarted pod can have a different UID, and a retry may be a new attempt. If the lease is acquired in an init step, the test container starts only after the ownership check succeeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make cleanup and failure evidence explicit
&lt;/h2&gt;

&lt;p&gt;Kubernetes can remove finished Jobs, but that does not necessarily release an external mailbox. Use a &lt;code&gt;trap&lt;/code&gt; in the test runner or a CI post-step to call a delete-or-expire endpoint. The endpoint should be idempotent: calling it twice returns success when the lease is already closed.&lt;/p&gt;

&lt;p&gt;Record structured events for four points: lease acquired, message requested, message observed, and lease released. Include the job UID, lease ID, message ID, and timestamps. Exclude message bodies, verification codes, and recipient data that is not needed for triage. This makes &lt;a href="https://dev.to/mrdapperx/replayable-webhooks-a-tiny-failure-lab-4idj"&gt;replayable webhook failures&lt;/a&gt; easier to reason about because the email event has a durable correlation key.&lt;/p&gt;

&lt;p&gt;A useful timeout report says which boundary failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lease=ci-4831-7f2a owner=job-9c2 message=requested
wait=30s observed=0 cleanup=attempted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much more actionable than &lt;code&gt;email not received&lt;/code&gt;. The report can point to the inbox API, the application event, or the Kubernetes job log without exposing sensitive content. Small bit of duplicated metadata is cheaper then a week of guessing at flaky tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical CI checklist
&lt;/h2&gt;

&lt;p&gt;Before relying on the workflow, verify these cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two jobs can request mail at the same time without reading each other's messages.&lt;/li&gt;
&lt;li&gt;A retry gets a new lease or a clearly versioned attempt.&lt;/li&gt;
&lt;li&gt;A pod killed during the wait still leaves an expiry path.&lt;/li&gt;
&lt;li&gt;Cleanup succeeds when the application never sent a message.&lt;/li&gt;
&lt;li&gt;A delayed message is rejected after the lease expires.&lt;/li&gt;
&lt;li&gt;Logs identify the failing boundary but contain no token, code, or full email body.&lt;/li&gt;
&lt;li&gt;A run with &lt;code&gt;temp mail com&lt;/code&gt; or &lt;code&gt;create temp mail&lt;/code&gt; as fixture text stays isolated from real recipient routing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key change is ownership. Once the mailbox is bound to the Kubernetes job, email verification stops being a shared global side effect and becomes a normal CI dependency with a lifecycle. It can still fail, but the failure is visible, bounded, and usually much faster to fix.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cicd</category>
      <category>aws</category>
      <category>testing</category>
    </item>
    <item>
      <title>AWS CI: Docker Checks for Test Email Failures</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Fri, 23 Oct 2026 02:23:06 +0000</pubDate>
      <link>https://dev.to/jasonmills94/aws-ci-docker-checks-for-test-email-failures-2mbi</link>
      <guid>https://dev.to/jasonmills94/aws-ci-docker-checks-for-test-email-failures-2mbi</guid>
      <description>&lt;h1&gt;
  
  
  AWS CI: Docker Checks for Test Email Failures
&lt;/h1&gt;

&lt;p&gt;An AWS CI job can report green while the email step was never trustworthy. The Docker container is running, the application endpoint answers, and the test still cannot find its verification message. That is not a small test annoyance. It means the pipeline is missing a dependency signal.&lt;/p&gt;

&lt;p&gt;I have had better results treating test email as an operations dependency. The job should know whether its inbox is reachable before it starts the expensive browser or API checks. A disposable email generator can create isolated test data, but it does not automatically make the delivery path observable. The container, inbox, and CI job need a clear contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why container-green is not pipeline-green
&lt;/h2&gt;

&lt;p&gt;Docker reports a container as healthy when the configured command succeeds. It does not know that an email provider accepted a message, that the correct mailbox received it, or that the message can be read by the test runner.&lt;/p&gt;

&lt;p&gt;That creates a familiar failure shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ECS task starts -&amp;gt; app health check passes -&amp;gt; test requests verification email
                                      -&amp;gt; inbox wait times out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeout is often blamed on Playwright or on a slow AWS network. Sometimes it is. More often, the test has no evidence for which boundary failed. Was the worker down? Did the message go to the wrong address? Did the inbox API return a rate limit? A generic &lt;code&gt;expected email within 30 seconds&lt;/code&gt; message is not enough to answer that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the email dependency
&lt;/h2&gt;

&lt;p&gt;Before changing retries, write down the minimum contract for one test run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The test gets a unique mailbox or lease.&lt;/li&gt;
&lt;li&gt;The application sends to that exact address.&lt;/li&gt;
&lt;li&gt;The inbox reader can authenticate without exposing a token in logs.&lt;/li&gt;
&lt;li&gt;The reader records message ID, received time, and subject, but not the whole body.&lt;/li&gt;
&lt;li&gt;The lease is cleaned up after the run, even when the test fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a test team evaluating a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temporary email address&lt;/a&gt;, this boundary is more important than choosing a catchy provider name. The address is a fixture, not a place for real user data. Keep it separate from production mail and make the ownership of each mailbox obvious.&lt;/p&gt;

&lt;p&gt;I also keep the expected message identity in the test receipt. The receipt can say &lt;code&gt;signup-verification / run-4812&lt;/code&gt;; it should not copy an OTP or password into CloudWatch. This has made incident review much quicker, becuase we can compare the application event with the inbox event without creating another secret store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Docker health check
&lt;/h2&gt;

&lt;p&gt;The health check should test the narrowest dependency that proves the container is ready for email tests. It should not send a real verification email on every Docker probe. A lightweight inbox capability endpoint is usually enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=20s --timeout=5s --start-period=30s --retries=3 \&lt;/span&gt;
  CMD wget -qO- http://localhost:8080/internal/test-mail/ready || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint can verify configuration and perform a safe, authenticated capability check against the test inbox service. Return a useful status without echoing credentials:&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;"mail_reader"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mailbox_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked_at"&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-10-23T02:00:00Z"&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;Do not make a third-party inbox call on a liveness probe. If the provider is briefly slow, Kubernetes or ECS may restart a perfectly usable container. Use readiness for dependency availability and liveness for whether the process itself is stuck. Mixing those signals caused needless restarts in one of my earlier pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expose useful evidence in AWS
&lt;/h2&gt;

&lt;p&gt;In AWS, send structured events for each boundary rather than one final test failure. A small event set is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mailbox.leased
verification.requested
verification.accepted
inbox.message_seen
verification.asserted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Include the CI run ID, mailbox lease ID, and message ID where available. Avoid the email address if it is not needed for correlation; a short hash is often safer. Set a retention period for these logs, and redact message bodies at the application boundary.&lt;/p&gt;

&lt;p&gt;When the job fails, the operator should be able to see whether the last event was &lt;code&gt;verification.requested&lt;/code&gt; or &lt;code&gt;inbox.message_seen&lt;/code&gt;. That distinction turns a vague email timeout into either an application delivery issue or an inbox reader issue. It also pairs well with &lt;a href="https://dev.to/silviutech/playwright-retries-need-email-receipts-27ab"&gt;email receipts for Playwright retries&lt;/a&gt; and &lt;a href="https://dev.to/sophiax99/oauth-recovery-emails-need-provenance-mc8"&gt;provenance for recovery emails&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry without making the failure worse
&lt;/h2&gt;

&lt;p&gt;Retry only the operation that is safe to retry. Polling an inbox for an existing message is usually safe. Requesting a new verification email may not be: it can create multiple messages, rotate the token, and hide the original failure.&lt;/p&gt;

&lt;p&gt;Use a bounded poll with an explicit deadline:&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;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;SECONDS &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;45&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; SECONDS &amp;lt; deadline &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;inboxctl find &lt;span class="nt"&gt;--run-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CI_RUN_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--event&lt;/span&gt; verification.accepted&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;3
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"verification message not observed before deadline"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command above is only useful if &lt;code&gt;inboxctl&lt;/code&gt; returns non-zero for a real reader error and a different status for “not found yet.” Otherwise a network failure can look like a normal wait. That distinction was easy to miss, but it saves a lot of wasted reruns.&lt;/p&gt;

&lt;p&gt;If a retry must send again, use an idempotency key derived from the CI run and test case. Never let a flaky mailbox check create an unlimited stream of messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical CI checklist
&lt;/h2&gt;

&lt;p&gt;Before relying on Docker-based email tests in AWS, check that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Readiness proves the inbox reader can operate, while liveness only protects the process.&lt;/li&gt;
&lt;li&gt;Each run owns a unique mailbox or lease.&lt;/li&gt;
&lt;li&gt;Logs correlate the run, lease, and message without storing secrets.&lt;/li&gt;
&lt;li&gt;Inbox polling distinguishes “not received” from “reader failed.”&lt;/li&gt;
&lt;li&gt;Retries are bounded and idempotent.&lt;/li&gt;
&lt;li&gt;Cleanup runs after both success and failure.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;tempail mail&lt;/code&gt; search term has not accidentally become a production dependency.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This arrangement is not complicated, but it changes where the debugging starts. The CI job no longer asks only whether a Docker container is alive. It can show which email boundary is ready, which message was expected, and where the handoff stopped. That is the kind of small reliability improvement that makes AWS pipelines much less noisy.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>docker</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>Kubernetes CI Needs a Mailbox Contract</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:22:58 +0000</pubDate>
      <link>https://dev.to/jasonmills94/kubernetes-ci-needs-a-mailbox-contract-3kko</link>
      <guid>https://dev.to/jasonmills94/kubernetes-ci-needs-a-mailbox-contract-3kko</guid>
      <description>&lt;p&gt;Email verification is easy to treat as a tiny detail in a Kubernetes pipeline. A test creates a user, waits for a message, clicks a link, and reports green. The trouble starts when the mailbox is shared, the old message is still there, or the pod that created the test disappears before anyone can explain the failure.&lt;/p&gt;

&lt;p&gt;I use a mailbox contract for these tests. It is a small set of rules that says who owns an inbox, how long it lives, what evidence belongs to the run, and when a missing message should block the deployment. The contract makes a burner email address behave more like an ephemeral infrastructure resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a mailbox needs a contract
&lt;/h2&gt;

&lt;p&gt;Kubernetes gives every workload useful identity primitives: namespaces, labels, service accounts, and pod UIDs. A test mailbox needs the same kind of ownership. Without it, a CI job can find a valid-looking message that was created by a different run.&lt;/p&gt;

&lt;p&gt;The contract should answer four questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which pipeline run created this mailbox?&lt;/li&gt;
&lt;li&gt;Which test or namespace is allowed to read it?&lt;/li&gt;
&lt;li&gt;When does the mailbox expire?&lt;/li&gt;
&lt;li&gt;What exact event proves that the email belongs to this run?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also record a correlation ID in the application request and in the message lookup. A request ID is more reliable than a subject line, which may be translated or changed by a provider. The idea is similar to using &lt;a href="https://dev.to/kevindev27/session-proof-for-email-change-apis-5f7a"&gt;session proof for email change APIs&lt;/a&gt;: the workflow should prove ownership, not just find a convenient message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define ownership and expiry
&lt;/h2&gt;

&lt;p&gt;Give every test run a short identifier, for example &lt;code&gt;ci-8f31a&lt;/code&gt;. Put it in the Kubernetes Job label, the test data, and the email event. The mailbox allocation record can then look 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;"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;"ci-8f31a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pull-requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github-actions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&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-10-22T05:20:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&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-10-22T05:35:00Z"&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;The expiry should be shorter than the normal retention period. If a test runs for ten minutes, a thirty-minute lease may be reasonable; a mailbox that hangs around for days is a source of stale evidence. Cleanup is sometimes forgotten, and that makes the next failure much harder to read.&lt;/p&gt;

&lt;p&gt;For a workflow that needs a use and throw email during a non-production verification, a disposable service such as &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso.com&lt;/a&gt; can be one option. Keep the mailbox isolated from customer data and make the lease explicit. If the test needs to generate throwaway email addresses repeatedly, rate limits and provider errors should be handled as their own state, not silently retried forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire the contract into Kubernetes CI
&lt;/h2&gt;

&lt;p&gt;The test worker can run as a short-lived Job. Pass only the run identifier and a reference to the mailbox lease; do not put inbox credentials in pod arguments or logs.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generateName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-check-&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-check&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-actions&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ttlSecondsAfterFinished&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;verifier&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/email-verifier:stable&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&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;CI_RUN_ID&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;fieldRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;fieldPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metadata.uid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pod UID is useful for tracing, but I still pass a CI run ID from the pipeline because it is easier to find in build logs. A small Docker image is enough; the important part is that the worker emits structured results such as &lt;code&gt;matched&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;wrong_owner&lt;/code&gt;, or &lt;code&gt;provider_error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The pipeline can then collect a bounded result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; pull-requests &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  job/email-check &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; pull-requests logs job/email-check &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not make &lt;code&gt;kubectl logs&lt;/code&gt; the only record. Export the final status, run ID, message ID, and timestamps as CI artifacts before cleanup. This is where &lt;a href="https://dev.to/ryanlee91/use-request-ids-in-react-signup-checks-gef"&gt;request IDs in signup checks&lt;/a&gt; become useful beyond application code: the same identifier ties the browser action, API request, email, and deployment record together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose blocking and warning behavior
&lt;/h2&gt;

&lt;p&gt;Not every email failure should stop an AWS deployment. I normally use three outcomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Block:&lt;/strong&gt; the email is part of the feature being released and the expected message is missing or belongs to another run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warn:&lt;/strong&gt; the application is healthy, but a non-critical notification provider timed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry:&lt;/strong&gt; the provider returned a known transient error, with a fixed retry count and deadline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The check is simple, but it are easy to make the retry path unlimited. Put the deadline in the test output so an operator can tell whether the failure came from Kubernetes, the application, or the external mailbox service. A generic “email failed” message sends people to the wrong dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep evidence after the pod is gone
&lt;/h2&gt;

&lt;p&gt;Short-lived jobs are good for isolation, but they remove local context quickly. Before deleting the namespace or Job, save a small receipt containing the deployment SHA, namespace, run ID, mailbox lease, correlation ID, result state, and message timestamp. Never store the full message when a header or message ID is enough.&lt;/p&gt;

&lt;p&gt;I also check that a failed run cannot read a previous run's inbox. This negative test catches the bug better than another happy-path signup. In practice, teams often write &lt;code&gt;temp mailid&lt;/code&gt; or &lt;code&gt;tepm mail com&lt;/code&gt; in notes while searching for a service; those strings are not an ownership mechanism, so the test still needs a real lease and correlation check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before making the email check a deployment gate, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every mailbox has a run owner and expiry time&lt;/li&gt;
&lt;li&gt;the correlation ID is present in logs and lookup filters&lt;/li&gt;
&lt;li&gt;stale messages are rejected&lt;/li&gt;
&lt;li&gt;the Kubernetes Job has a bounded timeout&lt;/li&gt;
&lt;li&gt;provider errors are separate from application failures&lt;/li&gt;
&lt;li&gt;CI artifacts retain a minimal evidence receipt&lt;/li&gt;
&lt;li&gt;cleanup runs even after a failed test&lt;/li&gt;
&lt;li&gt;the blocking policy is documented for the team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This contract is small, but it removes a surprising amount of guesswork. The mailbox becomes a named dependency with a lifecycle, instead of a hidden shared state that occasionally makes CI green for the wrong reason.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cicd</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>Docker Mailbox Leases for AWS CI</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:23:17 +0000</pubDate>
      <link>https://dev.to/jasonmills94/docker-mailbox-leases-for-aws-ci-5a92</link>
      <guid>https://dev.to/jasonmills94/docker-mailbox-leases-for-aws-ci-5a92</guid>
      <description>&lt;p&gt;Email verification tests become much easier to operate when the test mailbox is treated like any other short-lived cloud resource. I use the same mental model for a container, a temporary database, or an S3 prefix: allocate it for one job, attach an owner, set an expiry, and clean it up when the job ends.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but many CI pipelines still treat an inbox as a shared string in a secret. A parallel job can read the wrong message, a retry can consume an old token, and a failed runner can leave a mailbox around for the next build. The result is a flaky test that looks like an application problem.&lt;/p&gt;

&lt;p&gt;This post describes a lease-based pattern I have used around Docker jobs running in AWS CI. It also fits teams searching for a reliable tempmail disposable workflow without making the email provider the center of the architecture. Email verification is a signal rather than an identity, so the test should prove ownership and timing instead of trusting the newest message blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a mailbox should behave like a leased container resource
&lt;/h2&gt;

&lt;p&gt;A CI mailbox needs boundaries. At minimum, the job should know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a unique mailbox or alias&lt;/li&gt;
&lt;li&gt;the build and test name that own it&lt;/li&gt;
&lt;li&gt;the time the verification action started&lt;/li&gt;
&lt;li&gt;a lease expiration time&lt;/li&gt;
&lt;li&gt;the cleanup status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those fields, debugging turns into archaeology. Someone sees a message from an earlier run and assumes the product sent it correctly. Someone else types &lt;code&gt;tempail&lt;/code&gt; or &lt;code&gt;temp org mail&lt;/code&gt; into a search box while looking for the setup note. That kind of naming confusion is a symptom of missing ownership data, not a useful test strategy.&lt;/p&gt;

&lt;p&gt;The lease does not need to be complicated. A job id plus a random suffix is usually enough for isolation. The important part is that the inbox name is generated at runtime and passed to the application under test, rather than copied from a shared environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lease contract for an AWS CI job
&lt;/h2&gt;

&lt;p&gt;I define the contract before writing the test helper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Allocate:&lt;/strong&gt; create or reserve one mailbox for the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish ownership:&lt;/strong&gt; store the job id, test id, and &lt;code&gt;created_at&lt;/code&gt; with the lease.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use:&lt;/strong&gt; submit the address to the application and poll only for messages newer than the trigger time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expire:&lt;/strong&gt; reject messages received after the lease deadline or from an unexpected mailbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release:&lt;/strong&gt; delete the lease in a &lt;code&gt;finally&lt;/code&gt; path, including after a failed assertion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CI runner should never log the full message body or verification token. Log the mailbox identifier, message id, timestamps, and rejection reason. Those fields are enough for triage in most cases and keep the evidence smaller.&lt;/p&gt;

&lt;p&gt;The same thinking applies to the rest of the pipeline. I keep alert boundaries explicit for cluster maintenance too; the notes on &lt;a href="https://dev.to/jasonmills94/eks-freeze-windows-need-alert-boundaries-hc0"&gt;alert boundaries for EKS freeze windows&lt;/a&gt; are useful when deciding which failures should page and which should only annotate a build.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Docker-based implementation
&lt;/h2&gt;

&lt;p&gt;The test container can receive a lease as environment variables. The provider-specific client stays outside the browser test so the test only sees a small contract.&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;email-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-org/email-tests:ci&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MAILBOX_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILBOX_ID}&lt;/span&gt;
      &lt;span class="na"&gt;MAILBOX_EXPIRES_AT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MAILBOX_EXPIRES_AT}&lt;/span&gt;
      &lt;span class="na"&gt;CI_JOB_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${CI_JOB_ID}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test:email"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper should make the time boundary visible. A simplified TypeScript shape looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MailLease&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailLease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mailbox lease expired&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;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startedAt&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&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;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no owned verification message found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;extractCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real implementation I also record the polling attempts and the provider request id. Keep that evidence attached to the CI artifact, not printed into every test log. For more background on treating verification data as a bounded signal, see &lt;a href="https://dev.to/bitheirstake/email-verification-is-a-signal-not-an-identity-40e4"&gt;email verification as a signal rather than an identity&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure handling and cleanup
&lt;/h2&gt;

&lt;p&gt;There are three failures worth separating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Allocation failure:&lt;/strong&gt; the job never received a lease. Fail fast and do not start browser tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery failure:&lt;/strong&gt; the lease exists, but no matching message arrives before the deadline. Save polling evidence and mark the provider path as suspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership failure:&lt;/strong&gt; a message arrives, but its timestamp, address, or owner metadata does not match. Reject it even if the code would make the test pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cleanup must run independently of the assertion result. In a shell wrapper, use a trap around the test command. In application code, use &lt;code&gt;try/finally&lt;/code&gt;. Also add a scheduled janitor for leases whose runners disappeared; relying only on the runner's exit hook is optimistic, and AWS capacity events can interrupt a job without giving your script much notice.&lt;/p&gt;

&lt;p&gt;Do not make retries reuse the same lease automatically. A retry can reuse the test definition, but it should receive a new owner and a new mailbox lease. Otherwise the second attempt may accidentally validate an email produced by the first attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping this pattern, I check the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the mailbox unique per job and test shard?&lt;/li&gt;
&lt;li&gt;Are &lt;code&gt;started_at&lt;/code&gt; and &lt;code&gt;expires_at&lt;/code&gt; compared in one time standard?&lt;/li&gt;
&lt;li&gt;Does the matcher verify address, owner, subject, and message age?&lt;/li&gt;
&lt;li&gt;Are message bodies and tokens excluded from normal logs?&lt;/li&gt;
&lt;li&gt;Does cleanup run after both pass and fail?&lt;/li&gt;
&lt;li&gt;Is there a janitor for abandoned leases?&lt;/li&gt;
&lt;li&gt;Can a failed run show why each candidate message was rejected?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is yes, the email provider becomes an implementation detail. The Docker job owns a bounded resource, AWS CI gets deterministic failure signals, and a retry no longer hides the original cause.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Should every CI job get a separate mailbox?
&lt;/h3&gt;

&lt;p&gt;Every job that can run in parallel should get a separate lease. Sequential jobs may share a provider account, but they should still use distinct aliases or a fresh lease id so old messages cannot satisfy a new assertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a lease useful if the provider has no delete API?
&lt;/h3&gt;

&lt;p&gt;Yes. Expiration and ownership checks still prevent stale messages from being accepted. A janitor can then remove local lease records even when provider-side cleanup is limited.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should be retried?
&lt;/h3&gt;

&lt;p&gt;Retry the provider poll when the lease is valid. Do not retry indefinitely, and do not accept a message merely because it is the latest one. Once the lease expires, fail with evidence and allocate a new lease for a new attempt.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>aws</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>EKS CI: Keep Test Mailbox Failures Actionable</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:22:58 +0000</pubDate>
      <link>https://dev.to/jasonmills94/eks-ci-keep-test-mailbox-failures-actionable-3kl2</link>
      <guid>https://dev.to/jasonmills94/eks-ci-keep-test-mailbox-failures-actionable-3kl2</guid>
      <description>&lt;h1&gt;
  
  
  EKS CI: Keep Test Mailbox Failures Actionable
&lt;/h1&gt;

&lt;p&gt;Email verification is a small step in many end-to-end tests, but in an EKS pipeline it can become the least observable part of the run. A pod waits for a message, the message arrives late, and the test retries until the whole job is marked flaky. The team then sees a red build without knowing whether the problem was the application, Kubernetes scheduling, the mail provider, or a test fixture that was reused by another worker.&lt;/p&gt;

&lt;p&gt;The fix I prefer is to treat a test mailbox like a leased infrastructure resource. Give it an owner, a deadline, and an evidence trail. That makes a failed email check actionable instead of just annoying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: an email check that hides the real CI failure
&lt;/h2&gt;

&lt;p&gt;A common test sequence looks simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a user with a temporary address.&lt;/li&gt;
&lt;li&gt;Submit the signup form.&lt;/li&gt;
&lt;li&gt;Poll for a verification message.&lt;/li&gt;
&lt;li&gt;Open the link and finish the flow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In a local run, one worker and one mailbox can make this feel reliable. In a Kubernetes CI job, several workers may share a namespace, a NAT gateway, DNS, and the same mailbox pool. A slow consumer can read a message intended for a different test. A restarted pod can lose the polling cursor. A short-lived job can disappear before its final diagnostic is flushed.&lt;/p&gt;

&lt;p&gt;The resulting error is often just &lt;code&gt;verification email not found&lt;/code&gt;. That message has very little operational value. It also make developers add another retry, which increases the time before anyone looks at the real cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  A mailbox lease as a Kubernetes resource
&lt;/h2&gt;

&lt;p&gt;For each test case, create a lease record with these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: the CI workflow and attempt identifier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;worker_id&lt;/code&gt;: the pod or test shard that owns the mailbox&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mailbox_id&lt;/code&gt;: the disposable address or provider-side identifier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;expires_at&lt;/code&gt;: the lifetime of the fixture&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expected_message_id&lt;/code&gt;: a correlation value generated before signup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state&lt;/code&gt;: &lt;code&gt;reserved&lt;/code&gt;, &lt;code&gt;message_seen&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, or &lt;code&gt;expired&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lease does not need to be a custom Kubernetes object. A row in a small test-control service or a namespaced key in Redis can work. The important part is that reservation and cleanup are explicit. Do not let a test infer ownership from an address alone.&lt;/p&gt;

&lt;p&gt;I use a short lease for normal tests and renew it only while the test is making progress. If the pod is evicted, the lease expires naturally. A later worker can see that the resource is stale instead of waiting on an inbox that nobody owns anymore.&lt;/p&gt;

&lt;p&gt;For privacy, keep message content out of ordinary application logs. The &lt;a href="https://dev.to/bitheirstake/privacy-checklist-for-disposable-email-workflows-3amg"&gt;privacy checks for disposable email workflows&lt;/a&gt; are a useful companion when deciding what the test service may retain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the evidence path before adding retries
&lt;/h2&gt;

&lt;p&gt;Retries should recover a transient condition, not erase it. Each poll should record structured facts such as:&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-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mailbox_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;"mbx-7f2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lease_remaining_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;41800&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_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout-6c8d9d7f9b-q4k2m"&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;The log should contain identifiers, timings, and state transitions, but not verification tokens or full email bodies. When a poll times out, the test can distinguish “no message was delivered” from “a message was delivered but the worker could not claim it.” Those are different incidents and need different owners.&lt;/p&gt;

&lt;p&gt;It is tempting to use a generic label like &lt;code&gt;temp org mail&lt;/code&gt; in a dashboard and stop there. That label is not enough for debugging. Pair it with the run, shard, and lease state so an engineer can trace one fixture from reservation through cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical EKS implementation
&lt;/h2&gt;

&lt;p&gt;Run the test controller as a small deployment in the same EKS cluster, but keep the mailbox provider behind a narrow service interface. Test pods request a lease through the controller instead of talking to provider APIs directly. This gives the platform team one place to enforce quotas, redact data, and release abandoned fixtures.&lt;/p&gt;

&lt;p&gt;Use a Kubernetes &lt;code&gt;ServiceAccount&lt;/code&gt; with only the permissions the controller needs. It usually does not need permission to list every pod in the cluster. If pod identity is required, pass the worker ID from the test runner and verify it against the request metadata rather than granting broad cluster access.&lt;/p&gt;

&lt;p&gt;For CI jobs, attach a &lt;code&gt;run_id&lt;/code&gt; label to the test pod and include the same value in the lease. Export controller metrics for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lease reservations and expirations&lt;/li&gt;
&lt;li&gt;message delivery latency&lt;/li&gt;
&lt;li&gt;verification completion latency&lt;/li&gt;
&lt;li&gt;claims rejected because of ownership&lt;/li&gt;
&lt;li&gt;cleanup failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then route those signals with the rest of the deployment and rollback signals. The notes on &lt;a href="https://dev.to/jasonmills94/eks-rollbacks-need-signal-routing-3ccg"&gt;signal routing during EKS rollbacks&lt;/a&gt; apply here too: a useful alert says what changed, which run is affected, and what action is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to alert on
&lt;/h2&gt;

&lt;p&gt;Do not alert on one missing message. Alert on a pattern that indicates a boundary failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delivery latency is above the test budget for several runs&lt;/li&gt;
&lt;li&gt;lease expirations increase while application requests remain healthy&lt;/li&gt;
&lt;li&gt;ownership conflicts appear on one shard or namespace&lt;/li&gt;
&lt;li&gt;cleanup failures leave fixtures past their retention window&lt;/li&gt;
&lt;li&gt;the controller cannot reach the provider, but tests continue retrying&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An alert should link to the CI run and the lease evidence. If the controller is unavailable, fail fast with that reason rather than letting hundreds of test pods poll until their deadlines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short checklist for the next pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reserve one mailbox lease per test owner.&lt;/li&gt;
&lt;li&gt;Put the lease deadline below the CI job timeout.&lt;/li&gt;
&lt;li&gt;Correlate the signup request, message, and verification step.&lt;/li&gt;
&lt;li&gt;Keep tokens and message bodies out of logs.&lt;/li&gt;
&lt;li&gt;Make retries bounded and state-aware.&lt;/li&gt;
&lt;li&gt;Emit cleanup metrics even when the test fails.&lt;/li&gt;
&lt;li&gt;Test controller failure separately from application failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions I ask before shipping
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can two workers claim the same message?&lt;/strong&gt; If yes, the fixture boundary is incomplete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens after a pod restart?&lt;/strong&gt; The lease should be recoverable or expire, not remain in an unknown state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an engineer explain a failure from one CI link?&lt;/strong&gt; If not, add correlation fields before adding more retries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does cleanup run when the test is cancelled?&lt;/strong&gt; It should be best-effort, observable, and safe to repeat.&lt;/p&gt;

&lt;p&gt;The useful outcome is not a mailbox that never fails. External delivery has latency and outages. The useful outcome is a pipeline that can say exactly which boundary failed, preserve enough evidence to investigate, and release its temporary resources when the run is over.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Kubernetes CI: Lease Test Mailboxes, Don't Retry Blindly</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Thu, 15 Oct 2026 23:22:44 +0000</pubDate>
      <link>https://dev.to/jasonmills94/kubernetes-ci-lease-test-mailboxes-dont-retry-blindly-2ffn</link>
      <guid>https://dev.to/jasonmills94/kubernetes-ci-lease-test-mailboxes-dont-retry-blindly-2ffn</guid>
      <description>&lt;p&gt;Email verification tests become unreliable when the mailbox is treated like an ordinary HTTP dependency. A pipeline sends a message, waits a few seconds, sees nothing, and retries. The retry may create a duplicate message, race the first delivery, or consume a mailbox that another test owns.&lt;/p&gt;

&lt;p&gt;In Kubernetes CI, the cleaner model is an &lt;strong&gt;inbox lease&lt;/strong&gt;: one test run reserves one isolated mailbox for a bounded period, records that ownership, and releases it whether the test passes or fails. This turns a vague polling problem into a resource-lifecycle problem that can be operated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: a mailbox is not a retryable dependency
&lt;/h2&gt;

&lt;p&gt;Most email assertions have at least four separate steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create or reserve a test mailbox.&lt;/li&gt;
&lt;li&gt;Trigger the application event.&lt;/li&gt;
&lt;li&gt;Wait for the message to arrive and identify the right message.&lt;/li&gt;
&lt;li&gt;Release the mailbox and retain enough evidence to debug the run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If step three is implemented as “retry the whole test,” the pipeline can repeat step two as well. That creates false positives and makes delivery latency look like application flakiness. It also makes parallel Kubernetes Jobs collide when they use a shared address.&lt;/p&gt;

&lt;p&gt;The phrases &lt;strong&gt;temp mail so&lt;/strong&gt; and &lt;strong&gt;temp mail com&lt;/strong&gt; sometimes appear in old test names or search logs. The label does not matter as much as the contract behind the mailbox: ownership, expiration, message identity, and cleanup must be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the inbox as a leased resource
&lt;/h2&gt;

&lt;p&gt;A useful lease record needs a small amount of state:&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;"orders-pr-1842-7f3a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mailbox_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;"mbx-91c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders-e2e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&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-10-16T04:00:12Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&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-10-16T04:10:12Z"&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_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;"mbx-91c2@example.test"&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;The &lt;code&gt;run_id&lt;/code&gt; is the correlation key. The expiration is the safety valve. If a worker disappears, a janitor can reclaim the mailbox after &lt;code&gt;expires_at&lt;/code&gt; without guessing whether a test is still active.&lt;/p&gt;

&lt;p&gt;The lease should be created before the application event is triggered. Pass the mailbox address and run ID into the test as injected configuration, not as a hard-coded environment-wide value. In practice, this one detail prevents a surprising number of cross-run email assertions.&lt;/p&gt;

&lt;p&gt;For a concrete contract-test shape, see &lt;a href="https://dev.to/pong1965/contract-test-api-emails-in-github-actions-45f8"&gt;contract-test API emails in GitHub Actions&lt;/a&gt;. The same separation between triggering an event and proving delivery works well inside a Kubernetes Job.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Kubernetes implementation
&lt;/h2&gt;

&lt;p&gt;Keep the test Job short-lived and give it a deadline. A simplified manifest looks like this:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generateName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-email-&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backoffLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;activeDeadlineSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
  &lt;span class="na"&gt;ttlSecondsAfterFinished&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;e2e&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.test/checkout-e2e:sha-abc123&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&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;MAILBOX_LEASE_ID&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&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;checkout-mailbox-lease&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lease_id&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;TEST_RUN_ID&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders-pr-1842-7f3a"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;backoffLimit: 0&lt;/code&gt; is intentional here. A Kubernetes-level restart can rerun the side effects that send email, while the lease and application test may not be idempotent. If the container needs a retry, make that retry narrow: poll for the expected message, do not submit the order again.&lt;/p&gt;

&lt;p&gt;The Job deadline is not a replacement for the mailbox expiration. They should be related but independent. A stuck pod should not keep a mailbox alive forever, and a mailbox service outage should not cause an unbounded CI wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlate messages before asserting them
&lt;/h2&gt;

&lt;p&gt;Never assert only on recipient and subject when parallel runs are possible. Use a unique run token in a header, URL parameter, or test-specific payload field. Then make the assertion sequence explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lease mailbox
trigger event once
poll for run_id until deadline
assert exactly one matching message
assert token and destination
write run receipt
release lease
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A matching message should be claimed atomically if the mailbox API supports claiming. Otherwise, two test workers can both read the same message and report success. A receipt should include the message ID, delivery timestamp, run ID, and sanitized assertion result. Avoid storing full message bodies when a token or header is sufficient.&lt;/p&gt;

&lt;p&gt;This is the same operational idea as &lt;a href="https://dev.to/pong1965/api-smoke-tests-need-receipts-fhl"&gt;API smoke tests with explicit receipts&lt;/a&gt;: a green check is more useful when it carries proof that can be connected to one run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry policy and cleanup
&lt;/h2&gt;

&lt;p&gt;Separate retries into three budgets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provisioning retry:&lt;/strong&gt; retry lease creation only when the service reports a transient failure, and use an idempotency key based on &lt;code&gt;run_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery polling:&lt;/strong&gt; poll with a capped interval, such as 2, 4, 8, and 10 seconds, until the test deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assertion retry:&lt;/strong&gt; do not retry a failed token or recipient assertion. That is an application failure, not eventual consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always release the lease in a cleanup path. If the test process is killed, the expiration and a periodic janitor are the fallback. Alert on leases that are reclaimed by expiry; they usually indicate a dead worker, a missing cleanup hook, or a timeout that is too short.&lt;/p&gt;

&lt;p&gt;Be careful with logs. A typo like &lt;code&gt;tempail&lt;/code&gt; or &lt;code&gt;tamp mail com&lt;/code&gt; can be useful when tracing legacy configuration, but it should not become a mailbox identity or a routing rule. Store the canonical lease ID and run ID alongside any human-readable labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;p&gt;Before relying on this pattern, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every parallel Job receives a unique mailbox lease.&lt;/li&gt;
&lt;li&gt;The application event is triggered once per run.&lt;/li&gt;
&lt;li&gt;Polling filters on a run-specific identifier.&lt;/li&gt;
&lt;li&gt;Kubernetes retries cannot repeat an email-producing side effect.&lt;/li&gt;
&lt;li&gt;Lease expiration is shorter than the overall cleanup window.&lt;/li&gt;
&lt;li&gt;Receipts contain message ID, run ID, and sanitized outcomes.&lt;/li&gt;
&lt;li&gt;A janitor reclaims abandoned leases and emits a useful metric.&lt;/li&gt;
&lt;li&gt;CI fails on assertion mismatch instead of hiding it in another retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main payoff is boring, predictable email testing. Kubernetes can schedule the work, but it should not be responsible for understanding mailbox ownership. Put that contract in the test workflow, give every lease a deadline, and make each successful delivery carry its own receipt.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Dockerized Mail Capture for AWS CI Pipelines</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Tue, 13 Oct 2026 02:23:04 +0000</pubDate>
      <link>https://dev.to/jasonmills94/dockerized-mail-capture-for-aws-ci-pipelines-1i8d</link>
      <guid>https://dev.to/jasonmills94/dockerized-mail-capture-for-aws-ci-pipelines-1i8d</guid>
      <description>&lt;p&gt;Email verification tests become unreliable when every CI job reads from the same inbox. A message from a previous run can look valid, a retry can consume the wrong token, and a failed pipeline can leave sensitive URLs in artifacts.&lt;/p&gt;

&lt;p&gt;A better boundary is to give each run its own disposable address and capture service. Package the capture layer in Docker, make the fixture lifecycle explicit, and pass only a small receipt back to the AWS pipeline. The result is easier to debug without turning email content into permanent test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email capture belongs inside the test boundary
&lt;/h2&gt;

&lt;p&gt;An email test has more moving parts than a normal API assertion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the application creates a verification token;&lt;/li&gt;
&lt;li&gt;a mail provider or local capture service accepts the message;&lt;/li&gt;
&lt;li&gt;the test finds the expected message;&lt;/li&gt;
&lt;li&gt;the browser or API consumes the token;&lt;/li&gt;
&lt;li&gt;cleanup removes the fixture after the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those parts share a mailbox, the test has no strong answer to a basic question: did this message belong to this attempt? Subject matching is not enough when two AWS jobs run in parallel. A unique recipient, run identifier, and expiration time are much stronger correlation fields.&lt;/p&gt;

&lt;p&gt;The same ownership idea is useful for &lt;a href="https://dev.to/ryanlee91/typescript-email-fixtures-need-one-owner-ooc"&gt;one-owner email fixtures&lt;/a&gt;. One test attempt should create, read, and release its own fixture. Other jobs should not be able to discover it by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every CI run its own fixture
&lt;/h2&gt;

&lt;p&gt;The fixture contract can stay small. At creation time, record something 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;"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;"build-1842-attempt-1"&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;"build-1842-attempt-1@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;"fixture_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;"mail-7f31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&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-10-13T03:00:00Z"&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;The address does not need to be memorable. It needs to be unique, searchable, and scoped to one attempt. Store the fixture identifier in the test environment and use it when querying messages. Do not use the newest message in an inbox as an implicit selector.&lt;/p&gt;

&lt;p&gt;A useful rule is that a retry creates a new fixture and a new token. Reusing the original one seems efficient, but the failure history become ambiguous. It also make parallel debugging harder because the same message can be claimed twice. Fresh identifiers cost very little compared with investigating a false-positive test.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small Docker capture service
&lt;/h2&gt;

&lt;p&gt;The capture container can expose a private HTTP API to the test runner while accepting SMTP from the application container. It should support only the operations the tests need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /fixtures       create a run-scoped fixture
POST /messages/query find messages for a fixture
POST /messages/claim claim one matching message
DELETE /fixtures/id  release a fixture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the service on an isolated Docker network. The application can reach the SMTP endpoint, while the public edge of the test environment cannot. A minimal compose fragment might look like this:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mail-capture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal/mail-capture:latest&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;test-net&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FIXTURE_TTL_SECONDS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;900"&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mail-capture&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2525"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;test-net&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image should be pinned by digest in the real pipeline, not left on a floating &lt;code&gt;latest&lt;/code&gt; tag. The capture service also needs a bounded body size and a clear policy for attachments. Most verification tests need a link and a few headers, not a permanent archive of every MIME part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep AWS pipeline logs useful but safe
&lt;/h2&gt;

&lt;p&gt;AWS CI logs are valuable when they show correlation, timing, and the final assertion. They are risky when they show a complete verification URL. A receipt should contain identifiers and outcomes, for example:&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;"fixture_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;"mail-7f31"&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_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;"msg-91ab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"received_at"&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-10-13T02:35:12Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_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;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cleanup_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;"released"&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;Keep the raw token in memory for the shortest practical time. Redact query strings before the logger sees them, rather than trying to hide them later in a dashboard. If a debugging artifact must include the message, encrypt it, shorten its retention, and make access explicit.&lt;/p&gt;

&lt;p&gt;This receipt pattern also fits &lt;a href="https://dev.to/jasonmills94/ses-deploy-gates-need-sqs-receipts-39k"&gt;delivery receipts in deployment gates&lt;/a&gt;. A pipeline should be able to prove that delivery and consumption happened for the intended run, without copying the whole email into the release record.&lt;/p&gt;

&lt;p&gt;Legacy suites sometimes contain search values such as &lt;code&gt;dummy e mail&lt;/code&gt; or &lt;code&gt;fake e mail com&lt;/code&gt;. Keep those as plain test data when compatibility requires it. They should never become backlink anchors, credentials, or production selectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup, retries, and failure receipts
&lt;/h2&gt;

&lt;p&gt;Cleanup must work on both success and failure. Ask the test runner to release the fixture in a &lt;code&gt;finally&lt;/code&gt; block, then let a janitor remove abandoned fixtures after their TTL. The janitor is the last line of defense for cancelled jobs and broken agents.&lt;/p&gt;

&lt;p&gt;For an AWS pipeline, publish the small receipt as a short-lived artifact and emit metrics for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time from fixture creation to message arrival;&lt;/li&gt;
&lt;li&gt;messages rejected because their run id did not match;&lt;/li&gt;
&lt;li&gt;expired fixtures found by the janitor;&lt;/li&gt;
&lt;li&gt;token claims that were already consumed;&lt;/li&gt;
&lt;li&gt;cleanup failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not make the cleanup step depend on the browser test passing. The pipeline become much easier to operate when resource release has its own result and alert path. A failed cleanup should be visible, but it should not cause the test to retry and send another message without a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;p&gt;Before adopting this pattern, check that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every attempt gets a unique disposable address and fixture id.&lt;/li&gt;
&lt;li&gt;The capture image is pinned and the Docker network is private.&lt;/li&gt;
&lt;li&gt;Queries require fixture correlation, not only a subject line.&lt;/li&gt;
&lt;li&gt;Verification URLs and message bodies are excluded from normal logs.&lt;/li&gt;
&lt;li&gt;Retries create fresh fixtures and tokens.&lt;/li&gt;
&lt;li&gt;TTL cleanup handles cancelled and retryed jobs.&lt;/li&gt;
&lt;li&gt;Receipts contain enough timing data to diagnose delivery delays.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The contract pay off when a failure happens at 2 AM: the pipeline can show what was created, what arrived, what was claimed, and what was released without exposing the secret that made the test pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions teams usually ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should the capture service run in every job?
&lt;/h3&gt;

&lt;p&gt;For parallel or security-sensitive suites, yes. A per-job container gives the cleanest ownership boundary. A shared service can work for lower-risk tests if fixture ids, permissions, and cleanup are enforced, but it adds another coordination layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do we need a real mail provider?
&lt;/h3&gt;

&lt;p&gt;Not for every verification test. Local Docker capture is usually enough to prove that the application generated the right message and that the consumer handled the token. Keep a smaller provider-backed suite for delivery behavior that cannot be simulated locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  What belongs in a failure artifact?
&lt;/h3&gt;

&lt;p&gt;Keep the fixture id, message id, timestamps, status codes, and redacted headers. A full message body is prefered only when a narrowly scoped incident investigation requires it, with short retention and controlled access.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS Pipelines Need Isolated Mail Test Containers</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:22:59 +0000</pubDate>
      <link>https://dev.to/jasonmills94/aws-pipelines-need-isolated-mail-test-containers-1e47</link>
      <guid>https://dev.to/jasonmills94/aws-pipelines-need-isolated-mail-test-containers-1e47</guid>
      <description>&lt;p&gt;Email tests become unreliable when their mailbox is treated as a shared utility. A local test can create an address, wait for a message, and pass. The same test in an AWS pipeline can read a message from another run, retry against a stale container, or leave data behind for the next build. The fix is not simply a faster poller. The mailbox needs the same isolation boundary as the application under test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why containerized mail tests fail in AWS pipelines
&lt;/h2&gt;

&lt;p&gt;I have seen teams run an email sink beside the application container and assume the problem is solved. It works until two jobs use the same inbox name, a runner is reused, or a failed build skips cleanup. Then the queue are isolated only by convention, which is not isolation at all.&lt;/p&gt;

&lt;p&gt;An AWS-based pipeline usually has several moving parts: a hosted runner or self-hosted worker, a Docker network, application containers, a mail fixture, and a cleanup step. Each part can outlive a single test. If the test asks for “the newest message,” it is already depending on shared state and timing.&lt;/p&gt;

&lt;p&gt;Search terms like “temp mail com” and “disposable email” often describe the user-facing need, but CI needs a stricter contract. A test mailbox should be created for one run, accept only the messages that belong to that run, expose a small delivery receipt, and disappear when the run ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the quarantine boundary
&lt;/h2&gt;

&lt;p&gt;Start with a run ID generated by the pipeline, not by the mailbox service. Pass it into the application as a test-only correlation value and derive a mailbox namespace from it. The namespace should be unique enough for parallel jobs, while the raw address does not need to appear in every log line.&lt;/p&gt;

&lt;p&gt;The quarantine boundary has four rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A run can read only messages tagged for that run.&lt;/li&gt;
&lt;li&gt;A message can be consumed once by the intended assertion.&lt;/li&gt;
&lt;li&gt;A failed test cannot make another run pass by leaving a usable message behind.&lt;/li&gt;
&lt;li&gt;Cleanup is safe to repeat when a retry or cancellation runs it again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is also where product policy matters. A disposable inbox is a useful test tool; it is not proof that a real person owns an address. Keep the fixture contract separate from production account trust decisions. The same distinction is useful when reviewing &lt;a href="https://dev.to/sophiax99/secure-secondary-email-changes-1pg4"&gt;secondary email security boundaries&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Docker fixture that leaves a receipt
&lt;/h2&gt;

&lt;p&gt;The Docker fixture should have a small API rather than making every test know how to search raw messages. A useful interface might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /runs/{run_id}/mailbox
GET  /runs/{run_id}/messages?after={cursor}
POST /runs/{run_id}/messages/{message_id}/consume
DELETE /runs/{run_id}/mailbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test creates a mailbox, records the initial cursor, triggers the application action, and asks for messages after that cursor. It filters by the run ID and expected recipient. If a message arrive with the wrong correlation value, it is evidence of a fixture or application bug, not a reason to broaden the search.&lt;/p&gt;

&lt;p&gt;The important output is a redacted receipt, for example:&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;"ci-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;"message_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;"msg-7f2a"&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_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_at"&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-10-10T14:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"consumed"&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="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;Do not print verification links or tokens in the receipt. The fixture doesnt need to know the application's secret format; it only needs to prove which message was delivered and consumed. If a UI test needs to click a link, return it through a protected test channel and keep the receipt free of credentials. A &lt;a href="https://dev.to/ryanlee91/react-email-verification-design-a-safe-retry-button-dme"&gt;retry-safe email verification UI&lt;/a&gt; should still have a bounded retry policy on top of this fixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS pipeline controls
&lt;/h2&gt;

&lt;p&gt;In AWS CI, give the fixture an explicit lifecycle. Start the Docker network and mailbox before the test, attach the run ID through environment configuration, and register cleanup before the first application request. Cleanup should run for success, failure, cancellation, and timeout where the runner supports those hooks.&lt;/p&gt;

&lt;p&gt;Keep logs useful but narrow. Log the run ID, container name, cursor, message ID, and state transition. Redact mailbox contents and tokens. The worker should fail fastly when the fixture health check is unavailable; a long poll against a dead container only makes the eventual failure harder to diagnose.&lt;/p&gt;

&lt;p&gt;Use a bounded poll with a stop reason such as &lt;code&gt;message_not_observed&lt;/code&gt;, &lt;code&gt;wrong_run&lt;/code&gt;, or &lt;code&gt;fixture_unhealthy&lt;/code&gt;. When a retry is allowed, retry the observation or delivery operation, not the whole account-creation flow without checking idempotency. Retries needs an owner and a budget, even in a test.&lt;/p&gt;

&lt;p&gt;The cleanup job run should be safe to execute twice. Removing a mailbox that is already gone is a successful end state. This matters when AWS interrupts a runner and the pipeline later replays its finalization steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and tradeoffs
&lt;/h2&gt;

&lt;p&gt;The most common shortcut is one shared mailbox per branch. It saves setup time, but it turns ordering into a hidden dependency. A run-scoped mailbox costs a little more startup work and makes parallel behavior much easier to reason about.&lt;/p&gt;

&lt;p&gt;Another shortcut is deleting the container immediately after the first assertion. That makes debugging poor: the receipt says that something failed, but there is no bounded evidence to inspect. Retain redacted metadata for the pipeline's normal log window, while deleting message bodies and secrets quickly.&lt;/p&gt;

&lt;p&gt;A third issue is naming. Queries for phrases like “tempail mail” or “tamp mail com” can appear in test data or user-input fixtures. They should be treated as ordinary strings, not as a signal to change retention, routing, or authentication behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every mailbox and Docker network has a run-scoped identifier.&lt;/li&gt;
&lt;li&gt;[ ] Tests read after a cursor and filter by recipient and correlation value.&lt;/li&gt;
&lt;li&gt;[ ] Message consumption is single-use and idempotent.&lt;/li&gt;
&lt;li&gt;[ ] The receipt contains IDs and outcomes, never tokens or message bodies.&lt;/li&gt;
&lt;li&gt;[ ] Polling has a deadline and a named failure reason.&lt;/li&gt;
&lt;li&gt;[ ] Cleanup runs on success, failure, cancellation, and timeout.&lt;/li&gt;
&lt;li&gt;[ ] Repeated cleanup is safe.&lt;/li&gt;
&lt;li&gt;[ ] Production trust decisions never depend on the test mailbox implementation.&lt;/li&gt;
&lt;li&gt;[ ] Parallel AWS jobs cannot read each other's messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions teams ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should every test get a new Docker container?
&lt;/h3&gt;

&lt;p&gt;Not always. A container can serve several tests if the mailbox namespace, cursor, and cleanup contract are run-scoped. Use a fresh container when the fixture has state that cannot be reliably reset. The boundary is more important than the container count.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a shared mailbox ever acceptable?
&lt;/h3&gt;

&lt;p&gt;For a single-threaded local smoke test, possibly. For parallel CI, it is usually a false economy. Shared state makes flaky tests look random and makes a passing result harder to trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should a failure report include?
&lt;/h3&gt;

&lt;p&gt;Include the run ID, fixture health, cursor, expected recipient hash, observed message ID if any, poll deadline, and cleanup status. That is enough to locate the failed boundary without exposing email content or verification secrets.&lt;/p&gt;

&lt;p&gt;The practical lesson is simple: treat test mail as quarantined infrastructure, not as a convenient public inbox. A Docker fixture with a run-scoped contract, a redacted receipt, and an AWS-aware cleanup path gives the pipeline evidence it can trust. It also makes the next failure boring, which is exactly what operations work should aim for.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>docker</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>AWS Test Mailboxes Need a Cost and TTL Budget</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:22:59 +0000</pubDate>
      <link>https://dev.to/jasonmills94/aws-test-mailboxes-need-a-cost-and-ttl-budget-31hp</link>
      <guid>https://dev.to/jasonmills94/aws-test-mailboxes-need-a-cost-and-ttl-budget-31hp</guid>
      <description>&lt;p&gt;An AWS test mailbox is a cloud resource, not just a test detail. Give each run a lease, a TTL, and a cleanup owner before CI starts creating disposable email accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mailbox cost belongs in the test design
&lt;/h2&gt;

&lt;p&gt;Email verification tests often begin with a simple assumption: create a temp mail address, wait for a message, and delete it later. That works on a laptop. In a busy CI system, it creates a resource-lifecycle problem.&lt;/p&gt;

&lt;p&gt;Parallel jobs can leave mailboxes behind after a cancelled runner. Retries can create another mailbox while the first one is still active. A failed cleanup commmand may not be noticed until the monthly bill or a provider quota makes the problem visible. The test passed, but the cloud system is accumulating state.&lt;/p&gt;

&lt;p&gt;I treat every test mailbox as a short-lived lease with four fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: the pipeline run and attempt that owns it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;address&lt;/code&gt;: the disposable email address assigned to the test&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expires_at&lt;/code&gt;: the hard end of the lease&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanup_state&lt;/code&gt;: pending, deleted, or cleanup_failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This also makes the boundary clearer for privacy. An email verification flow should retain only the message data needed by the test, then remove the rest. The discussion in &lt;a href="https://dev.to/bitheirstake/email-verification-needs-a-privacy-budget-5hl8"&gt;email verification needs a privacy budget&lt;/a&gt; is a useful reminder that test data still deserves an explicit retention policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run-scoped lease
&lt;/h2&gt;

&lt;p&gt;The lease should be created once per CI run or per isolated test group, depending on the provider's limits. I prefer one mailbox per run when parallel tests need separate recipients, and one mailbox per test group when the suite is small and the message cursor is reliable.&lt;/p&gt;

&lt;p&gt;The name should include an opaque run identifier, not a user email or a secret. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ci-20261009-1842-attempt-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL is a budget, not a suggestion. If the normal test completes in two minutes, a five-minute lease may be reasonable. Add room for queue time and one retry, but avoid a twenty-four-hour default because it feels safe. Long leases hide cleanup failures.&lt;/p&gt;

&lt;p&gt;The lease record can live beside the test run metadata:&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;"1842-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-1842-1@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;"expires_at"&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-10-09T08:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cleanup_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&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;The mailbox client should reject reads for a different &lt;code&gt;run_id&lt;/code&gt;. That check prevents a late message from a previous attempt becoming a false positive in the current test.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small AWS implementation
&lt;/h2&gt;

&lt;p&gt;For an AWS-based pipeline, I keep the lease metadata in a small DynamoDB table or in the system that already records CI fixtures. The important part is the contract, not the storage product:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The job requests a lease with a bounded TTL.&lt;/li&gt;
&lt;li&gt;The service returns the address and a correlation token.&lt;/li&gt;
&lt;li&gt;The test filters messages by recipient, run ID, subject, and token.&lt;/li&gt;
&lt;li&gt;The job marks the lease consumed or failed.&lt;/li&gt;
&lt;li&gt;A scheduled cleanup worker deletes expired leases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CI role should have only the permissions needed for its path. The test role may create and consume a lease, while the cleanup role can delete expired records. Separating those permissions makes a bug easier to contain.&lt;/p&gt;

&lt;p&gt;I also pass the lease identifier through the pipeline as ordinary metadata, not as a secret. The mailbox address is not a credential. Provider tokens and AWS credentials must stay in the CI secret store and should never be copied into logs.&lt;/p&gt;

&lt;p&gt;At the start of a run, write a structured event such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mailbox_lease_created run_id=1842-1 ttl_seconds=300 owner=signup-tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end, write the matching state transition. This is more useful than printing the full message body, which adds noise and can expose test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promotion and cleanup gates
&lt;/h2&gt;

&lt;p&gt;A green test result is not enough for a production promotion. I use two gates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test gate:&lt;/strong&gt; all expected messages were consumed with the correct run ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle gate:&lt;/strong&gt; the lease was deleted, or an explicit cleanup task was queued with an owner and deadline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lifecycle gate catches a common failure mode: the application test passes, the runner disappears, and the cleanup step never runs. A &lt;code&gt;finally&lt;/code&gt; block is still useful, but it is not a recovery plan for a terminated worker.&lt;/p&gt;

&lt;p&gt;For deployment evidence, I keep the lease state and test result linked to the pipeline execution. This follows the same operational idea as &lt;a href="https://dev.to/jasonmills94/docker-ci-releases-need-a-verifiable-aws-receipt-622"&gt;a verifiable AWS receipt for CI releases&lt;/a&gt;: a later operator should be able to see what ran, what it proved, and what resources it left behind.&lt;/p&gt;

&lt;p&gt;Set an alert for expired leases older than the allowed grace period. The alert should point to the run ID and cleanup owner, not just say “email test failed.” That difference saves time during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to measure in CloudWatch
&lt;/h2&gt;

&lt;p&gt;The minimum useful metrics are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;leases created and deleted&lt;/li&gt;
&lt;li&gt;cleanup failures&lt;/li&gt;
&lt;li&gt;active leases by environment&lt;/li&gt;
&lt;li&gt;message wait latency&lt;/li&gt;
&lt;li&gt;expired leases beyond the grace period&lt;/li&gt;
&lt;li&gt;test retries caused by missing messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use only the number of successful tests. A rising success count can hide a growing active-lease count. A small dashboard with these signals gives a better view of reliability and cost.&lt;/p&gt;

&lt;p&gt;If a team searches for “temp org mail” while troubleshooting, the runbook should direct them to the lease dashboard and the provider request ID. Search phrases are not a substitute for an ownership record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist for the next pipeline run
&lt;/h2&gt;

&lt;p&gt;Before enabling this pattern, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every lease has a bounded TTL.&lt;/li&gt;
&lt;li&gt;The run ID is included in message correlation.&lt;/li&gt;
&lt;li&gt;Cleanup runs on success, failure, and expiry.&lt;/li&gt;
&lt;li&gt;The cleanup worker is idempotent.&lt;/li&gt;
&lt;li&gt;CI permissions are split from cleanup permissions.&lt;/li&gt;
&lt;li&gt;Message bodies are not copied into normal logs.&lt;/li&gt;
&lt;li&gt;CloudWatch alarms cover cleanup failures and stale leases.&lt;/li&gt;
&lt;li&gt;Promotion evidence contains the test and lifecycle states.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One small warning: do not make the cleanup job depend on the same runner that owns the test. Put recovery on a seperate schedule or worker. Otherwise the failure that needs cleanup can also remove the only process capable of doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions I ask before shipping
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What if the provider delays a message?
&lt;/h3&gt;

&lt;p&gt;Use a bounded polling policy with a clear timeout and record the latency. Do not retry forever, and do not read the newest message without checking its correlation token.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the cleanup API is unavailable?
&lt;/h3&gt;

&lt;p&gt;Keep the lease record, mark &lt;code&gt;cleanup_failed&lt;/code&gt;, and let the recovery worker retry with backoff. The record should survive long enough to explain the failure, but the mailbox itself should still have a provider-side expiry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a disposable email account safe for every test?
&lt;/h3&gt;

&lt;p&gt;No. Use synthetic or controlled addresses for sensitive workflows and follow the provider's terms. A disposable mailbox is a test fixture, not an identity system.&lt;/p&gt;

&lt;p&gt;The practical outcome is simple: a temp mail fixture should have the same operational discipline as any other AWS resource. A short TTL, explicit ownership, and visible cleanup state keep CI predictable without letting forgotten test data become tomorrow's cloud bill.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS CI/CD Email Tests Need a Promotion Receipt</title>
      <dc:creator>jasonmills94</dc:creator>
      <pubDate>Tue, 06 Oct 2026 23:23:18 +0000</pubDate>
      <link>https://dev.to/jasonmills94/aws-cicd-email-tests-need-a-promotion-receipt-25hc</link>
      <guid>https://dev.to/jasonmills94/aws-cicd-email-tests-need-a-promotion-receipt-25hc</guid>
      <description>&lt;p&gt;Email verification tests often end with a boolean: passed or failed. That is enough for a local test, but it is thin evidence for an AWS CI/CD pipeline that is deciding whether to promote a build.&lt;/p&gt;

&lt;p&gt;The useful change I made was to produce one promotion receipt for every run. It ties the build, test attempt, message ID, cleanup result, and decision together. When a test flakes, the receipt shows whether the application failed, the inbox lookup read the wrong message, or cleanup left state behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a green test is not enough
&lt;/h2&gt;

&lt;p&gt;A green email test can still be weak evidence. The test may have found a message from a previous retry, matched the newest message in a shared inbox, or passed before its cleanup step ran. Those failures are not always visible in the final CI status.&lt;/p&gt;

&lt;p&gt;This gets awkward when a pipeline uses an ephemeral mailbox or a service that can &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;get temporary email&lt;/a&gt; for each run. The test still needs to prove which message it consumed. A mailbox identity alone does not establish ownership.&lt;/p&gt;

&lt;p&gt;I now treat the email check as a small transaction with an evidence record:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a run and attempt ID.&lt;/li&gt;
&lt;li&gt;Create the test address and a unique correlation token.&lt;/li&gt;
&lt;li&gt;Trigger the application action.&lt;/li&gt;
&lt;li&gt;Find the message by recipient, token, and message type.&lt;/li&gt;
&lt;li&gt;Save the message ID and assertion result.&lt;/li&gt;
&lt;li&gt;Clean up the fixture and record what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pipeline should be able to answer these steps after the worker container is gone. It is easy to loose that context when logs are the only record, and the failure is usualy harder to explain the next morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a run-scoped receipt
&lt;/h2&gt;

&lt;p&gt;A JSON artifact in the AWS CodeBuild output is enough to start:&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;"build_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;"codebuild-myapp-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;"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;"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;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fixture"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-1842-a2@example.invalid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correlation_token"&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-1842-a2"&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;span class="nl"&gt;"message"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"msg_7f31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matched_by"&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="s2"&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;"correlation_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subject"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test"&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;"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;"passed"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cleanup"&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;"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;"pending"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"promotion"&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;"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;"blocked"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep credentials and full message bodies out of this artifact. Store the address only when it is safe for the CI workspace, and redact tokens if they could be reused outside the test environment. The receipt should be small enought to inspect quickly; it is for correlation, not a second secret store.&lt;/p&gt;

&lt;p&gt;The build ID should come from the CI provider, while the run and attempt IDs should be generated by the test harness. During a retry, the build stays the same but the attempt must change. Otherwise a late message can look valid for a new attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the email check deterministic
&lt;/h2&gt;

&lt;p&gt;The inbox query should be narrow and explainable. Do not ask for the newest message and accept whatever comes back. Match the recipient, a run-specific token in the body or subject, and the expected message type. The same principle appears in these &lt;a href="https://dev.to/silviutech/playwright-email-tests-catch-the-wrong-message-4d26"&gt;Playwright checks for the wrong email&lt;/a&gt; and in &lt;a href="https://dev.to/kevindev27/testing-rest-api-verification-emails-without-polluting-shared-inboxes-5693"&gt;isolated REST API verification tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is the shape of the assertion I want, regardless of the mail API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find_message(
  recipient=fixture.address,
  token=fixture.correlation_token,
  kind="signup_verification"
)
assert message.created_at &amp;gt;= attempt.started_at
assert message.id not in attempt.consumed_message_ids
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp check is a useful guard, but it should not replace the token. Clock skew and provider delays can make timestamps fuzzy. The token is the ownership boundary; time is supporting evidence.&lt;/p&gt;

&lt;p&gt;If the provider returns several matches, fail with the candidate IDs and the matching fields. This is much more useful than a generic timeout. A search for a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;fake email address&lt;/a&gt; should never become an excuse to accept an ambigous result. It should produce a visible diagnostic and let the promotion decision stay blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make retries and cleanup visible
&lt;/h2&gt;

&lt;p&gt;Retries need their own state. I use &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;matched&lt;/code&gt;, &lt;code&gt;asserted&lt;/code&gt;, &lt;code&gt;cleaned&lt;/code&gt;, and &lt;code&gt;failed&lt;/code&gt; as the main fixture states. A retry creates a new fixture and token; it does not reset the old record to &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cleanup runs in a &lt;code&gt;finally&lt;/code&gt; path and also in a scheduled sweeper. The first path handles normal test completion. The sweeper handles a killed container, a cancelled build, or a provider timeout. Both paths write to the same receipt, or append a follow-up cleanup record when the build artifact is already closed.&lt;/p&gt;

&lt;p&gt;The cleanup operation must be idempotent. “Already absent” is a successful cleanup outcome, while “provider unavailable” is not. That distinction prevents a broken cleanup API from being hidden behind a green test. It also makes incident review less guessy. This keeps retries from becoming to expensive when an external service is slow.&lt;/p&gt;

&lt;p&gt;Older fixtures sometimes contain a label such as &lt;code&gt;dummy e mail&lt;/code&gt;; keep that typo as plain migration text, never as a selector or backlink. The collector should identify resources with structured fixture IDs instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate promotion on evidence
&lt;/h2&gt;

&lt;p&gt;The deploy job should consume the receipt. A minimal shell gate can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.test.status == "passed"'&lt;/span&gt; promotion-receipt.json
jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.message.id and (.message.matched_by | length &amp;gt;= 3)'&lt;/span&gt; promotion-receipt.json
jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.cleanup.status == "cleaned" or .cleanup.status == "already_absent"'&lt;/span&gt; promotion-receipt.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also check that the receipt &lt;code&gt;build_id&lt;/code&gt; matches the current build and that the attempt has not been superseded by a later retry. If cleanup is pending, promotion stops. It is better to re-run a cheap test than deploy with unknown external state.&lt;/p&gt;

&lt;p&gt;This gate is deliberately boring. The deploy role should not need permission to search mailboxes or delete fixtures. A test role creates and cleans the fixture; the promotion job reads an immutable artifact. Narrow permissions make the failure boundary clearer and reduce the blast radius of a compromised build worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operating checklist
&lt;/h2&gt;

&lt;p&gt;Before relying on an email test in an AWS promotion pipeline, I check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each attempt has a unique run ID, attempt number, address, and correlation token.&lt;/li&gt;
&lt;li&gt;The message query requires ownership fields instead of recency alone.&lt;/li&gt;
&lt;li&gt;The receipt stores the message ID and matching fields, not a full sensitive body.&lt;/li&gt;
&lt;li&gt;Retries create new fixtures rather than reusing a stale address.&lt;/li&gt;
&lt;li&gt;Cleanup runs on pass, failure, timeout, and cancellation.&lt;/li&gt;
&lt;li&gt;A sweeper can find orphaned fixtures after the build worker disappears.&lt;/li&gt;
&lt;li&gt;Promotion reads the receipt and blocks on ambiguous or pending state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The receipt is a small artifact, but it changes how the pipeline is operated. An email test no longer says only “the check passed.” It says which run owned the message, what was asserted, whether the external fixture was removed, and why deployment was allowed. That is the level of evidence an AWS CI/CD promotion decision can safely use.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
