PR Preview Environments
Code review on a change catches typos and bad names, but looking at the code won’t catch “this button is in the wrong place,” “the loading state flashes weirdly,” or “the new copy is too long on mobile.” You find those looking at the application running in the same way it would in production.
The solution has been around forever: spin up a real version of the app for every pull request and put a link to it on the PR. The annoying part has always been the part where you actually do that — wiring up CI to build the branch, getting it somewhere reachable, giving it its own URL, and cleaning up after.
Today we’re shipping PR Preview Environments, powered by a new lower-level primitive called an ephemeral deploy. Together they turn that whole workflow into a few lines of config in your CI.
What an ephemeral deploy is
A PR Preview Environment is, mechanically, an ephemeral deploy: a labelled, time-boxed version of your app that lives alongside the active version. You make one with the command you already use:
miren deploy --ephemeral pr-42 --ttl 48h
The label becomes a subdomain on your app’s route. If your app lives at myapp.example.com, the ephemeral version answers at pr-42.myapp.example.com. TLS provisions on the first hit, so the preview gets a real certificate. After the TTL expires, the version is garbage-collected.
The ephemeral version gets its own subdomain, sandbox, and lifecycle, and it shares config, addons, and services with the primary app. More on that below.
The PR workflow
Here’s the actual GitHub Actions workflow we use on this website. Every PR gets a preview, and the URL gets posted back onto the PR description:
name: Deploy Preview
on:
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- id: deploy
uses: mirendev/actions/deploy@main
with:
cluster: ${{ secrets.MIREN_CLUSTER_PREVIEW }}
app: mirendev
ephemeral: pr-${{ github.event.number }}
ttl: 48h
- name: Update PR with preview link
uses: actions/github-script@v7
with:
script: |
const url = '${{ steps.deploy.outputs.url }}';
// ...post `url` onto the PR body
The deploy action takes two new inputs: ephemeral (the label) and ttl (how long it should live). It exports the preview URL as steps.deploy.outputs.url, so the “post a link to the PR” step is one read of an output variable, not log parsing.
Everything else is the secretless OIDC flow we shipped earlier this year. The runner has no stored credentials. GitHub mints a short-lived token for the job, the cluster validates it, and the binding scopes the token to a single app.
Seeing what’s live
Ephemeral versions show up in their own listing:
$ miren app versions --ephemeral
LABEL VERSION STATUS EXPIRES
pr-42 v17 running in 41h
pr-44 v18 running in 47h
feat-nav v19 running in 11h
miren app versions without the flag is the regular list of versions. We keep the two lists separate so the production timeline doesn’t get cluttered with pr-42, and the ephemeral list answers “what’s alive right now and when does it go away.”
The TTL is a backstop, not a workflow step. As the PR changes, the same label is deployed again and again, resetting the TTL. And when the PR isn’t being used anymore, it ages out a couple of days. Nothing to remember to clean up.
Run previews against staging, not production
Because ephemeral deploys share config and addons with their parent app, running them against your production app means the previews are talking to your production database. A buggy branch can write garbage into real user rows. Not great.
The pattern we recommend is a separate myapp-staging app that you targeting your preview workflow at. The PR Preview Environments docs walk through the setup, including the further-isolated variant where staging runs on its own cluster (which is what we do).
Just the web service, for now
Today, ephemeral deploys only spin up your app’s web service. Background workers, scheduled jobs, and other non-web services that the parent app runs are not started for the preview. That rules out some workflows — if your PR’s whole point is a change to a worker, the preview won’t run it.
In practice, this captures what most teams actually want a PR preview for: looking at UI changes, clicking through a flow, checking what the new copy looks like on mobile. Those all live in the web service. The rest is on the list.
Eating our own cooking
We use this on this website. Every PR you see in mirendev/mirendev gets a preview on the preview cluster, with the URL posted back onto the PR. The post you’re reading right now went through the loop. I had it on a branch, the preview built on every push, and the link in the PR body was where I read the draft.
Using the preview deployments to review website changes removes any development environment differences and make sure that everyone is talking about the same change.
Try it
If you’re already running Miren v0.8, PR Preview Environments are available now. Add --ephemeral to a deploy from your laptop to see the underlying ephemeral deploy work, then drop the workflow above into your repo to get a preview on every PR.
The full reference is in the PR Preview Environments docs, with related details in the miren deploy docs and the CI/CD deployment docs. If you haven’t tried Miren yet, this is a good excuse to get started.