-
Notifications
You must be signed in to change notification settings - Fork 464
π Create a staging environment deployment for pull requests #1070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kasperpeulen
wants to merge
1
commit into
epicweb-dev:main
Choose a base branch
from
kasperpeulen:kasper/staging-app-per-pr
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,25 +3,33 @@ on: | |
| push: | ||
| branches: | ||
| - main | ||
| - dev | ||
| pull_request: {} | ||
| pull_request: | ||
| types: [opened, reopened, synchronize] | ||
| # Clean up the staging environment when a PR is closed | ||
| # Use pull_request_target to also run when the PR has merge conflicts | ||
| pull_request_target: | ||
| types: [closed] | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| actions: write | ||
| contents: read | ||
|
|
||
| env: | ||
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | ||
| # Change this if you want to deploy to a different org | ||
| FLY_ORG: personal | ||
| jobs: | ||
| lint: | ||
| name: ⬣ ESLint | ||
| runs-on: ubuntu-22.04 | ||
| if: ${{ github.event.action != 'closed' }} | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: β Setup node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
|
|
@@ -42,10 +50,10 @@ jobs: | |
| typecheck: | ||
| name: Κ¦ TypeScript | ||
| runs-on: ubuntu-22.04 | ||
| if: ${{ github.event.action != 'closed' }} | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: β Setup node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
|
|
@@ -69,10 +77,10 @@ jobs: | |
| vitest: | ||
| name: β‘ Vitest | ||
| runs-on: ubuntu-22.04 | ||
| if: ${{ github.event.action != 'closed' }} | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: β Setup node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
|
|
@@ -93,11 +101,11 @@ jobs: | |
| playwright: | ||
| name: π Playwright | ||
| runs-on: ubuntu-22.04 | ||
| if: ${{ github.event.action != 'closed' }} | ||
| timeout-minutes: 60 | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: π Copy test env vars | ||
| run: cp .env.example .env | ||
|
|
||
|
|
@@ -146,8 +154,7 @@ jobs: | |
| container: | ||
| name: π¦ Prepare Container | ||
| runs-on: ubuntu-24.04 | ||
| # only prepare container on pushes | ||
| if: ${{ github.event_name == 'push' }} | ||
| if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
@@ -164,37 +171,106 @@ jobs: | |
| - name: π Setup Fly | ||
| uses: superfly/flyctl-actions/[email protected] | ||
|
|
||
| - name: π¦ Build Staging Container | ||
| if: ${{ github.ref == 'refs/heads/dev' }} | ||
| - name: π¦ Build Production Container | ||
| run: | | ||
| flyctl deploy \ | ||
| --build-only \ | ||
| --push \ | ||
| --image-label ${{ github.sha }} \ | ||
| --build-arg COMMIT_SHA=${{ github.sha }} \ | ||
| --app ${{ steps.app_name.outputs.value }}-staging | ||
| env: | ||
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | ||
| --build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ | ||
| --app ${{ steps.app_name.outputs.value }} | ||
|
|
||
| - name: π¦ Build Production Container | ||
| if: ${{ github.ref == 'refs/heads/main' }} | ||
| deploy-staging: | ||
| name: π Deploy staging app for PR | ||
| runs-on: ubuntu-24.04 | ||
| # Only run for PRs from the same repository (skip forks) | ||
| if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} | ||
| outputs: | ||
| url: ${{ steps.deploy.outputs.url }} | ||
| environment: | ||
| name: staging | ||
| url: ${{ steps.deploy.outputs.url }} | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: '50' | ||
| - name: π Read app name | ||
| uses: SebRollen/[email protected] | ||
| id: app_name | ||
| with: | ||
| file: 'fly.toml' | ||
| field: 'app' | ||
|
|
||
| - name: π Setup Fly | ||
| uses: superfly/flyctl-actions/[email protected] | ||
|
|
||
| # Inspired by https://github.com/superfly/fly-pr-review-apps/blob/main/entrypoint.sh | ||
| - name: ποΈ Deploy PR app to Fly.io | ||
| id: deploy | ||
| if: ${{ env.FLY_API_TOKEN }} | ||
| run: | | ||
| FLY_APP_NAME="${{ steps.app_name.outputs.value }}-pr-${{ github.event.number }}" | ||
| FLY_REGION=$(flyctl config show | jq -r '.primary_region') | ||
|
|
||
| # Create app if it doesn't exist | ||
| if ! flyctl status --app "$FLY_APP_NAME"; then | ||
kasperpeulen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # change org name if needed | ||
| flyctl apps create $FLY_APP_NAME --org $FLY_ORG | ||
| flyctl secrets --app $FLY_APP_NAME set SESSION_SECRET=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) | ||
| flyctl consul attach --app $FLY_APP_NAME | ||
| # Don't log the created tigris secrets! | ||
| flyctl storage create --app $FLY_APP_NAME --name epic-stack-$FLY_APP_NAME --yes > /dev/null 2>&1 | ||
| fi | ||
|
|
||
| flyctl secrets --app $FLY_APP_NAME set SENTRY_DSN=${{ secrets.SENTRY_DSN }} RESEND_API_KEY=${{ secrets.RESEND_API_KEY }} | ||
|
|
||
| flyctl deploy \ | ||
| --build-only \ | ||
| --push \ | ||
| --ha=false \ | ||
| --regions $FLY_REGION \ | ||
| --vm-size shared-cpu-1x \ | ||
| --env APP_ENV=staging \ | ||
| --env ALLOW_INDEXING=false \ | ||
| --app $FLY_APP_NAME \ | ||
| --image-label ${{ github.sha }} \ | ||
| --build-arg COMMIT_SHA=${{ github.sha }} \ | ||
| --build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ | ||
| --app ${{ steps.app_name.outputs.value }} | ||
| env: | ||
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | ||
|
|
||
| echo "url=https://$FLY_APP_NAME.fly.dev" >> $GITHUB_OUTPUT | ||
|
|
||
| cleanup-staging: | ||
| name: π§Ή Cleanup staging app | ||
| runs-on: ubuntu-24.04 | ||
| if: ${{ github.event.action == 'closed' }} | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: π Read app name | ||
| uses: SebRollen/[email protected] | ||
| id: app_name | ||
| with: | ||
| file: 'fly.toml' | ||
| field: 'app' | ||
|
|
||
| - name: π Setup Fly | ||
| uses: superfly/flyctl-actions/[email protected] | ||
|
|
||
| - name: π§Ή Cleanup resources | ||
| if: ${{ env.FLY_API_TOKEN }} | ||
| run: | | ||
| FLY_APP_NAME="${{ steps.app_name.outputs.value }}-pr-${{ github.event.number }}" | ||
| flyctl storage destroy epic-stack-$FLY_APP_NAME --yes || true | ||
| flyctl apps destroy "$FLY_APP_NAME" -y || true | ||
| deploy: | ||
| name: π Deploy | ||
| name: π Deploy production | ||
| runs-on: ubuntu-24.04 | ||
| needs: [lint, typecheck, vitest, playwright, container] | ||
| # only deploy on pushes | ||
| if: ${{ github.event_name == 'push' }} | ||
| if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | ||
| environment: | ||
| name: production | ||
| url: https://${{ steps.app_name.outputs.value }}.fly.dev | ||
| steps: | ||
| - name: β¬οΈ Checkout repo | ||
| uses: actions/checkout@v4 | ||
|
|
@@ -211,19 +287,7 @@ jobs: | |
| - name: π Setup Fly | ||
| uses: superfly/flyctl-actions/[email protected] | ||
|
|
||
| - name: π Deploy Staging | ||
| if: ${{ github.ref == 'refs/heads/dev' }} | ||
| run: | | ||
| flyctl deploy \ | ||
| --image "registry.fly.io/${{ steps.app_name.outputs.value }}-staging:${{ github.sha }}" \ | ||
| --app ${{ steps.app_name.outputs.value }}-staging | ||
| env: | ||
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | ||
|
|
||
| - name: π Deploy Production | ||
| if: ${{ github.ref == 'refs/heads/main' }} | ||
| run: | | ||
| flyctl deploy \ | ||
| --image "registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.sha }}" | ||
| env: | ||
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Per-PR Staging Environments | ||
|
|
||
| Date: 2025-12-24 | ||
|
|
||
| Status: accepted | ||
|
|
||
| ## Context | ||
|
|
||
| The Epic Stack previously used a single shared staging environment deployed from the `dev` branch. This approach created several challenges for teams working with multiple pull requests: | ||
|
|
||
| - **Staging bottleneck**: Only one PR could be properly tested in the staging environment at a time, making parallel development difficult. | ||
| - **Unclear test failures**: When QA testing failed, it was hard to determine if the failure was from the specific PR being tested or from other changes that had been deployed to the shared staging environment. | ||
| - **Serial workflow**: Teams couldn't perform parallel quality assurance, forcing them to coordinate who could use staging at any given time. | ||
| - **Extra setup complexity**: During initial deployment, users had to create and configure a separate staging app with its own database, secrets, and resources. | ||
|
|
||
| Fly.io provides native support for PR preview environments through their `fly-pr-review-apps` GitHub Action, which can automatically create, update, and destroy ephemeral applications for each pull request. | ||
|
|
||
| This pattern is common in modern deployment workflows (Vercel, Netlify, Render, etc.) and provides isolated environments for testing changes before they reach production. | ||
|
|
||
| ## Decision | ||
|
|
||
| We've decided to replace the single shared staging environment with per-PR staging environments using Fly.io's PR review apps feature. Each pull request now: | ||
|
|
||
| - Gets its own isolated Fly.io application (e.g., `app-name-pr-123`) | ||
| - Automatically provisions all necessary resources (SQLite volume, Tigris object storage, Consul for LiteFS) | ||
| - Generates and stores secrets (SESSION_SECRET, HONEYPOT_SECRET) | ||
| - Seeds the database with test data for immediate usability | ||
| - Provides a direct URL to the deployed app in the GitHub PR interface | ||
| - Automatically cleans up all resources when the PR is closed | ||
|
|
||
| Staging environment secrets are now managed as GitHub environment secrets and passed to Fly in Github Actions. | ||
|
|
||
| The `dev` branch and its associated staging app have been removed from the deployment workflow. Production deployments continue to run only on pushes to the `main` branch. | ||
|
|
||
| ## Consequences | ||
|
|
||
| **Positive:** | ||
|
|
||
| - **Isolated testing**: Each PR has its own complete environment, making it clear which changes caused any issues | ||
| - **Simplified onboarding**: New users only need to set up one production app, not both production and staging | ||
| - **Better reviews**: Reviewers (including non-technical stakeholders) can click a link to see and interact with changes before merging | ||
| - **Automatic cleanup**: Resources are freed when PRs close, reducing infrastructure costs | ||
| - **Realistic testing**: Each PR tests the actual deployment process, catching deployment-specific issues early | ||
|
|
||
| **Negative:** | ||
|
|
||
| - **Increased resource usage during development**: Each open PR consumes Fly.io resources (though they're automatically cleaned up) | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.