Preview Environments skill for Codex

A live URL per PR, gone when the PR closes. Codex sets up previews that deploy every PR to its own namespace or platform preview with a stable pr-number URL, wildcard TLS and authentication in front. Data comes from seeds or sanitized database branches, secrets point at sandbox accounts, fork PRs never receive credentials, and teardown happens on close with a label-scoped TTL reaper catching anything left behind.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- preview-environments

Installs to ~/.agents/skills/preview-environments/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $preview-environments, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds per-pull-request preview environments - Kubernetes namespace per PR via Argo CD ApplicationSet pull request generator or CI-driven Helm installs, GitLab review apps, platform previews on Vercel, Netlify, Render or Fly, wildcard DNS and TLS, seeded or branched databases, isolated secrets, fork PR safety, PR comments with the URL, and guaranteed teardown on close plus TTL reapers for cost control. Use when a team wants a live URL for every PR, previews leak resources or money, or preview data and secrets need isolating from production.

Use it when

  • Reviewers or product owners want to try changes before merge
  • Preview environments exist but leak resources or cost
  • Preview data or secrets are shared with staging or production
  • Fork contributions need previews without exposing credentials

Use something else when

How to check the result

A test PR gets a working TLS URL with seeded data, a second push updates the same environment, closing the PR removes every resource, and a fork PR deploys nothing.

The mistake it prevents

Relying only on the PR-closed event for teardown, so failed or skipped cleanup jobs leave orphaned environments running for months.

~/.agents/skills/preview-environments/SKILL.md

--- name: preview-environments description: Builds per-pull-request preview environments - Kubernetes namespace per PR via Argo CD ApplicationSet pull request generator or CI-driven Helm… license: MIT ---

A good preview system gives each open PR its own URL within minutes of a push, with realistic but non-production data, secrets that cannot touch production, and teardown that happens automatically on merge or close and again by a TTL reaper for anything missed. Cost is bounded by design, not by someone remembering to clean up.

Inspect first

  1. Read AGENTS.md and the deploy setup: platform (Kubernetes, ECS, a PaaS), how production deploys, and what a full stack needs (DB, cache, queues, third-party APIs).
  2. Check whether the hosting platform already does previews (Vercel, Netlify, Render, Fly, Railway, Heroku review apps). If it does, configure that before building your own.
  3. Measure the cost of one environment (pods, DB instance, load balancer) and the number of concurrently open PRs. Cost per preview times open PRs is the monthly bill.
  4. Find dependencies that cannot be duplicated per PR (payment provider webhooks, SSO callback URLs, email sending) and plan stubs or shared sandboxes for them.

Choose the approach

SituationApproach
Frontend or full stack on a PaaS with built-in previewsPlatform previews; add env vars and a DB strategy
Kubernetes with Argo CDApplicationSet pull request generator, namespace per PR
Kubernetes without GitOpsCI job: helm upgrade --install into pr-<n> namespace, teardown job on close
GitLabReview apps: environment with on_stop and auto_stop_in
Heavy stack (many services)Deploy only changed services per PR; route the rest to a shared staging baseline

Naming and routing

  • Identifier: pr-<number>; it is stable, short and DNS-safe. Branch names are not (length limit 63 for labels, slashes, uppercase).
  • Wildcard DNS *.preview.example.com to the ingress, and a wildcard certificate via DNS-01 (cert-manager or the platform). Per-PR HTTP-01 certificates hit rate limits.
  • URL: https://pr-123.preview.example.com. Add the preview domain to OAuth redirect allowlists with a wildcard if the provider supports it, or use a shared auth proxy.
  • Put previews behind authentication (SSO proxy, basic auth, or platform protection) and X-Robots-Tag: noindex; public previews get indexed and scraped.

Kubernetes with Argo CD

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: api-previews, namespace: argocd }
spec:
  goTemplate: true
  generators:
    - pullRequest:
        github:
          owner: acme
          repo: api
          tokenRef: { secretName: github-token, key: token }
          labels: [preview]            # opt-in per PR
        requeueAfterSeconds: 300
  template:
    metadata:
      name: 'api-pr-{{ .number }}'
    spec:
      project: previews
      source:
        repoURL: https://github.com/acme/api.git
        targetRevision: '{{ .head_sha }}'
        path: deploy/chart
        helm:
          parameters:
            - { name: image.tag, value: 'sha-{{ .head_short_sha }}' }
            - { name: ingress.host, value: 'pr-{{ .number }}.preview.example.com' }
      destination:
        server: https://kubernetes.default.svc
        namespace: 'pr-{{ .number }}'
      syncPolicy:
        automated: { prune: true, selfHeal: true }
        syncOptions: [CreateNamespace=true]

When the PR closes or loses the label, the Application is deleted. Make sure the Application has the resources finalizer so its namespace contents are pruned, and give the previews AppProject a destination restricted to pr-* namespaces. Apply a ResourceQuota and LimitRange to each preview namespace.

CI-driven (GitHub Actions)

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]
concurrency:
  group: preview-${{ github.event.pull_request.number }}
  cancel-in-progress: true
jobs:
  deploy:
    if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    environment:
      name: preview
      url: https://pr-${{ github.event.pull_request.number }}.preview.example.com
    steps:
      - run: >
          helm upgrade --install api-pr-${{ github.event.pull_request.number }} deploy/chart
          --namespace pr-${{ github.event.pull_request.number }} --create-namespace
          --set image.tag=sha-${{ github.event.pull_request.head.sha }} --wait --timeout 10m
  teardown:
    if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    steps:
      - run: kubectl delete namespace pr-${{ github.event.pull_request.number }} --ignore-not-found

The head.repo.full_name check skips fork PRs: they must never receive deploy credentials. For fork contributions, let a maintainer trigger a preview after review (label or workflow_dispatch), never via pull_request_target checking out fork code.

Data

OptionUse when
Seed script with fixturesDefault; fast, deterministic, no personal data
Database branching (Neon, Supabase, PlanetScale-style branches)Need realistic volume; branch from a sanitized parent, never raw production
Shared preview DB with schema per PRMany previews, small data; drop schema on teardown
Snapshot of anonymized stagingLarge datasets where seeding is too slow

Run migrations on deploy; a PR with a migration must not touch any shared database. Never copy production personal data into previews.

Secrets and side effects

  • Separate credentials for previews, scoped to sandbox accounts: payment test mode, email to a catch-all (Mailpit or a provider sandbox), feature-flag environment preview.
  • Preview secrets live in their own store path or environment; the preview deploy role cannot read production paths.
  • Background jobs and cron in previews: disable or point at preview-only queues.

Cleanup and cost

  • Teardown on PR close or merge (event-driven)
  • Scheduled reaper daily: delete preview namespaces or apps whose PR is closed or whose last deploy is older than N days (7 is common)
  • Scale-to-zero or sleep after inactivity where the platform supports it
  • Quotas per namespace and a cap on concurrent previews
  • Label every resource with preview=true and pr=<n> so reapers and cost reports find them

Reapers delete infrastructure. Show the user the selection logic and a dry-run list before enabling deletion, and scope it by label so it can never match non-preview resources.

PR feedback

Post or update a single comment with the URL, commit SHA and status rather than a new comment per push (use the deployment environment URL or an upserted comment keyed by a hidden marker). Report failures with a link to logs.

Verification

  • Open a test PR: URL is live with TLS, behind auth, data seeded, within the target time (under 10 minutes is a reasonable goal).
  • Push again: the same environment updates; no second namespace appears.
  • Close the PR: all resources gone within minutes; the reaper dry run finds nothing left.
  • A fork PR does not deploy and does not receive secrets.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Preview Environments"
  short_description: "A live URL per PR, gone when the PR closes"
  default_prompt: "$preview-environments Give every pull request its own preview URL on our Kubernetes cluster"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc