
A full environment for every pull request — deleted automatically
Reviewing a pull request by reading the diff works for the logic and not much else. If the change touches a migration, a Helm template or the behavior of a screen, approving it is an act of faith.
The alternative is for every pull request to spin up its own complete environment, and for it to be deleted automatically when it's closed.
What exactly gets spun up
In the homelab, when a PR carries the preview label, ArgoCD's Pull Request Generator detects it in under a minute and creates the entire environment.
| Resource | What the PR gets |
|---|---|
| Namespace | Its own, with the PR number |
| Database | Its own PostgreSQL, via CNPG |
| Search | Its own Typesense instance |
| Secrets | Its own, from Infisical |
| Domain | Its own subdomain |
The list isn't what matters — what matters is that it shares nothing with staging. A destructive migration in a preview breaks that preview and nothing else.
The two halves meet at the SHA
The setup is two pieces that never talk to each other: GitHub Actions builds the images and ArgoCD deploys the manifests. Nobody calls anybody. What keeps them aligned is that both use the pull request's commit as the identifier.
On the GitHub side, the workflow listens for PR events —opened, updated, reopened, labeled and closed— and only acts if it carries the preview label. It builds the four services in parallel and tags each image with the PR number and the SHA of its head:
tags: ${{ env.REGISTRY }}/${{ matrix.image }}:pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}The matrix runs without canceling its siblings when one fails. That's not an oversight: the public registry's storage returns intermittent certificate errors, and with cancellation enabled a transient failure in one service would take down the build of the other three.
On the ArgoCD side there is no webhook. A pull request generator asks GitHub every sixty seconds for PRs carrying that label:
generators:
- pullRequest:
github:
owner: Zetesis-Labs
repo: ZetesisPortal
labels:
- preview
requeueAfterSeconds: 60For each PR it finds, it materializes three applications in two waves. And each one points at the exact commit of the pull request, not its branch:
| Wave | Application | What it creates |
|---|---|---|
| 0 | pr-secrets | ExternalSecrets from Infisical |
| 0 | pr-infra | PostgreSQL with CNPG and Typesense |
| 1 | pr-app | The Helm chart with its domains and its tags |
The application's ApplicationSet is where the circle closes: it overrides the chart parameters with the same scheme the workflow used when tagging.
targetRevision: "{{ .head_sha }}"
- name: web.image.tag
value: "pr-{{ .number }}-{{ .head_sha }}"
- name: web.domain
value: "pr-{{ .number }}.staging.zetesis.xyz"Manifest and image come from the same commit without either side knowing the other. Pushing another commit to the PR changes the SHA, and with it both the tag the workflow builds and the revision ArgoCD deploys change at once.
When the pull request closes, teardown happens on both sides: ArgoCD deletes the applications —and with them the entire namespace, with its database and its secrets— because the generator stops seeing it, and a final workflow job deletes that PR's images from Harbor. No orphaned environments and no registry growing with images nobody will ever download again.
And with real data
In Konect the preview starts by copying the staging data. That difference changes what you can test: migrations run against the real schema, with its volume and its edge cases, not against an empty database where everything passes.
That's where the migrations that take twenty minutes show up, or the ones that fail because there's a row with a null nobody expected.
It deletes itself
An ephemeral environment that doesn't clean itself up stops being ephemeral and becomes a cluster full of garbage. Cleanup is automatic when the PR closes, and the portal also has a twenty-four-hour cap.
The label matters too: a preview only spins up if the PR carries it. Without that, every documentation change would drag a database along with it.
When it's not worth it
The cost isn't CPU: it's that you need the platform underneath. Without GitOps, without external secrets and without a database that knows how to provision itself, building per-PR previews means building those three things first.
If you already have them, it's one of the cheapest things you can add. If you don't, the correct order is the reverse.
Keep reading

Building a homelab from scratch: Kubernetes and GitOps - Homelab (03/06)

Konect: audio transcription and analysis

DevOps and infrastructure consulting

One deployment per customer or one space per customer

Search and agents over your own documentation
