<?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: Saffeena Nishad</title>
    <description>The latest articles on DEV Community by Saffeena Nishad (@saffeena_anwar_daeddc560d).</description>
    <link>https://dev.to/saffeena_anwar_daeddc560d</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%2F4241805%2F11be9b0f-7f08-4f7a-94b1-4d98b89cbdc8.png</url>
      <title>DEV Community: Saffeena Nishad</title>
      <link>https://dev.to/saffeena_anwar_daeddc560d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saffeena_anwar_daeddc560d"/>
    <language>en</language>
    <item>
      <title>Stop Reviewing Code You Can't Run: Ephemeral Preview Environments for Every Pull Request</title>
      <dc:creator>Saffeena Nishad</dc:creator>
      <pubDate>Thu, 12 Nov 2026 21:38:45 +0000</pubDate>
      <link>https://dev.to/saffeena_anwar_daeddc560d/stop-reviewing-code-you-cant-run-ephemeral-preview-environments-for-every-pull-request-1fni</link>
      <guid>https://dev.to/saffeena_anwar_daeddc560d/stop-reviewing-code-you-cant-run-ephemeral-preview-environments-for-every-pull-request-1fni</guid>
      <description>&lt;p&gt;Description&lt;/p&gt;

&lt;p&gt;Give every pull request its own short-lived environment with GitHub Actions, Helm and Kubernetes, and delete it automatically when the PR closes.&lt;/p&gt;

&lt;p&gt;Code review has a blind spot: we review diffs, but we ship running systems.&lt;/p&gt;

&lt;p&gt;A reviewer reads 200 changed lines and approves. Nobody ran it. A config tweak, a migration or a changed API contract can all look fine in a diff and still break in a real environment.&lt;/p&gt;

&lt;p&gt;The usual fix is a shared staging environment. That creates new problems:&lt;/p&gt;

&lt;p&gt;Two PRs overwrite each other's changes.&lt;br&gt;
"Who's using staging right now?" becomes a Slack ritual.&lt;br&gt;
Bugs get found after merge, when they are most expensive to fix.&lt;/p&gt;

&lt;p&gt;A better pattern is to give every pull request its own short-lived environment, then delete it when the PR closes.&lt;/p&gt;

&lt;p&gt;The idea&lt;/p&gt;

&lt;p&gt;When a PR opens or is updated:&lt;/p&gt;

&lt;p&gt;Build the image, tagged with the commit SHA.&lt;br&gt;
Deploy it to its own Kubernetes namespace, pr-.&lt;br&gt;
Post the preview URL as a PR comment.&lt;/p&gt;

&lt;p&gt;When the PR closes or merges:&lt;/p&gt;

&lt;p&gt;Delete the namespace. Nothing is left running and nothing costs money.&lt;/p&gt;

&lt;p&gt;Reviewers click a link and test the change, so approval rests on evidence rather than guesswork.&lt;/p&gt;

&lt;p&gt;Implementation (GitHub Actions + Helm)&lt;/p&gt;

&lt;p&gt;This workflow assumes you have a Kubernetes cluster with an ingress controller, a Helm chart in ./chart that accepts image and ingress.host values, and a KUBE_CONFIG secret in your repository.&lt;/p&gt;

&lt;p&gt;yaml&lt;br&gt;
name: preview-env&lt;/p&gt;

&lt;p&gt;on:&lt;br&gt;
  pull_request:&lt;br&gt;
    types: [opened, synchronize, reopened, closed]&lt;/p&gt;

&lt;p&gt;env:&lt;br&gt;
  NAMESPACE: pr-${{ github.event.number }}&lt;br&gt;
  IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}&lt;/p&gt;

&lt;p&gt;jobs:&lt;br&gt;
  deploy:&lt;br&gt;
    if: github.event.action != 'closed'&lt;br&gt;
    runs-on: ubuntu-latest&lt;br&gt;
    permissions:&lt;br&gt;
      contents: read&lt;br&gt;
      packages: write&lt;br&gt;
      pull-requests: write&lt;br&gt;
    steps:&lt;br&gt;
      - uses: actions/checkout@v4&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  - uses: docker/login-action@v3
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

  - name: Build and push image
    run: |
      docker build -t "$IMAGE" .
      docker push "$IMAGE"

  - name: Configure cluster access
    run: |
      mkdir -p ~/.kube
      echo "${{ secrets.KUBE_CONFIG }}" &amp;gt; ~/.kube/config

  - name: Deploy preview
    run: |
      helm upgrade --install app ./chart \
        --namespace "$NAMESPACE" --create-namespace \
        --set image="$IMAGE" \
        --set ingress.host=pr-${{ github.event.number }}.preview.example.com

  - name: Comment preview URL
    uses: marocchino/sticky-pull-request-comment@v2
    with:
      message: "Preview ready: https://pr-${{ github.event.number }}.preview.example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;teardown:&lt;br&gt;
    if: github.event.action == 'closed'&lt;br&gt;
    runs-on: ubuntu-latest&lt;br&gt;
    steps:&lt;br&gt;
      - name: Configure cluster access&lt;br&gt;
        run: |&lt;br&gt;
          mkdir -p ~/.kube&lt;br&gt;
          echo "${{ secrets.KUBE_CONFIG }}" &amp;gt; ~/.kube/config&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  - name: Delete namespace
    run: kubectl delete namespace "$NAMESPACE" --ignore-not-found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You also need a wildcard DNS record, *.preview.example.com, pointing at your ingress controller. After that, each PR gets its own URL with no further setup.&lt;/p&gt;

&lt;p&gt;Making it production-grade&lt;/p&gt;

&lt;p&gt;The basic version works, but a few additions make it safe and cheap:&lt;/p&gt;

&lt;p&gt;Cost control: set a ResourceQuota per namespace, and add a scheduled job that deletes namespaces older than three days in case a teardown fails.&lt;br&gt;
Data: seed each environment with anonymised fixtures. Never point previews at production data.&lt;br&gt;
Secrets: use a separate, low-privilege secret set for previews.&lt;br&gt;
Fork safety: don't expose cluster credentials to PRs from forks. Run previews only for trusted branches.&lt;br&gt;
Speed: cache Docker layers (docker/build-push-action with cache-from and cache-to) to keep deploys fast.&lt;br&gt;
What changes on your team&lt;/p&gt;

&lt;p&gt;After adopting this, the effects are easy to spot:&lt;/p&gt;

&lt;p&gt;Fewer "works on my machine" surprises, because reviewers see a real running system.&lt;br&gt;
Less contention, since there is no shared staging queue.&lt;br&gt;
Defects caught before merge, where they are cheapest to fix.&lt;br&gt;
Designers and product managers can check changes without a local setup.&lt;br&gt;
Takeaway&lt;/p&gt;

&lt;p&gt;Treat the environment as part of the change, not something that exists apart from it. If a PR can't be previewed, you are asking reviewers to approve something nobody has seen working.&lt;/p&gt;

&lt;p&gt;Start small: one service, one namespace per PR, and automatic cleanup. Once your team uses it, going back to a shared staging environment gets hard.&lt;/p&gt;

&lt;p&gt;What's the strangest bug a preview environment has caught on your team? Let me know in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
