PR Preview Deployments

Each pull request gets an isolated Fly.io preview app with its own ephemeral database.

How It Works

  1. PR opened/updated (non-draft only)
  2. pr-preview.yml detect-changes job decides which jobs run (app code vs docs-only vs schema changes)
  3. Builds PR-specific Docker image (tag: pr-<number>)
  4. Creates Fly.io app renewa-app-pr-{number} (APP_ENV=preview)
  5. Provisions the ephemeral database via the manage-pr-database composite action
  6. Fetches app secrets from Infisical via OIDC and stages them with flyctl secrets set --stage
  7. Deploys, runs e2e tests (after a blocking warmup), posts preview URL as PR comment

Workflow Files

WorkflowTriggerPurpose
.github/workflows/pr-preview.ymlPR open/syncOrchestrator: build + deploy + reusable CI workflows + ci-gate
.github/workflows/pr-cleanup.ymlPR closeDestroy preview app + database + Tigris objects + Entra redirect URI
.github/workflows/cleanup-orphaned-previews.ymlDaily 02:00 UTCRemove stale preview apps

The legacy config-sync step in cleanup was decommissioned and Infisical sourcing fixed in PR#1968.

Secrets

App secrets come from Infisical EU (pr-preview scope), fetched in CI via GitHub OIDC (Infisical/secrets-action, no long-lived tokens) and applied per preview app with flyctl secrets set --stage. Only CI-infra credentials (e.g. FLY_API_TOKEN) remain GitHub Actions secrets. Unlike development/staging/production, preview apps are NOT covered by Infisical’s native Fly sync.

Database Behavior

  • Created on first deploy for the PR (renewa-app-pr-{number})
  • Recreated only when schema/migration files change (detect-changes filters on backend/src/db/schema.ts, backend/src/db/schema/**, and migration files)
  • Data persists across non-schema commits (faster deploys)
  • manage-pr-database action supports create, verify, reset, create-if-missing
  • Uses the minimal mock dataset (3 users, 3 buildings, 5 projects, essential reference data) for startup in <30s; departments/employees come from the Entra sync

E2E Warmup Gate

Fly preview machines auto-stop when idle (auto_stop_machines = true, min_machines_running = 0), so e2e re-runs hit a cold app. .github/scripts/e2e-warmup.sh (I#1947, PR#1949) blocks until health is up AND a bot login round-trips, or fails the job with an explicit infrastructure error so no Playwright specs run cold. The script itself is tested by the ci-e2e-warmup-script job in quality-checks.yml.

Fly.io Configuration

Preview apps use a dedicated config: renewa-one/fly.pr-preview.toml

Key differences from production:

  • Smaller VM size, single region (Frankfurt)
  • auto_stop_machines with zero minimum running
  • Ephemeral storage only; Fly HTTP compression disabled (nginx handles it)

Required Status Check

The ci-gate job in pr-preview.yml is the single required check for merging. It validates all upstream jobs (quality-checks, security-scanning, migration-validation, build, deploy-preview, e2e-tests, container-security-scan) passed or were correctly skipped. See CI-CD Workflows.

HubSpot OAuth

The hubspot-app-update job (and scripts/manage-hubspot-pr-preview.sh) keeps the preview app’s redirect URI registered with HubSpot, mirroring the fixed local nginx port slots (see Docker Setup).

Promotion Path

Preview deployments are independent from the promotion pipeline:

PR Preview (pr-<number>)     -- ephemeral, per-PR
Development (main-<sha>)     -- auto-deploy on merge
Staging                      -- manual promotion
Production                   -- manual promotion

Key Files

  • .github/workflows/pr-preview.yml
  • .github/workflows/pr-cleanup.yml
  • .github/actions/manage-pr-database/
  • .github/scripts/e2e-warmup.sh
  • renewa-one/fly.pr-preview.toml

See Also