Skip to content

Conversation

@kasperpeulen
Copy link
Contributor

Test Plan

Checklist

  • Tests updated
  • Docs updated

Screenshots

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR transitions from a shared staging environment deployed from a dev branch to per-PR staging environments using Fly.io's PR review apps feature. Each pull request now gets its own isolated Fly.io application with automatically provisioned resources, seeded test data, and automatic cleanup when the PR is closed.

Key Changes:

  • Implements per-PR staging environments in GitHub Actions workflow with automatic resource provisioning and cleanup
  • Removes staging environment setup from the Remix initialization script
  • Updates all documentation to reflect the new deployment model where staging uses GitHub environment secrets

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
.github/workflows/deploy.yml Adds new deploy-staging job for per-PR environments with automatic app creation and cleanup; updates production deployment conditions
remix.init/index.mjs Removes staging environment setup prompts and configuration
prisma/seed.staging.sql New SQL seed file with test data for staging environments
other/litefs.yml Adds automatic seeding of staging databases based on APP_ENV variable
fly.toml Updates app name and region (appears to be environment-specific)
docs/deployment.md Simplifies deployment instructions to only require production app setup
docs/secrets.md Documents how to manage secrets for both production (Fly) and staging (GitHub)
docs/monitoring.md Updates Sentry setup instructions for new staging approach
docs/email.md Updates Resend API key setup instructions for new staging approach
docs/database.md Documents staging database seeding approach
docs/decisions/047-pr-staging-environments.md Architectural decision record explaining the change
app/routes/users/index.tsx Adds "Test" suffix to page heading (appears accidental)
prisma/migrations/migration_lock.toml Reformatting only

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +215 to +216
flyctl deploy . \
--ha=false \ # use only one machine for staging
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a syntax error in this line. The comment is placed inline with the backslash continuation character, which will cause the shell command to fail. The comment should either be on its own line before this command or removed entirely.

Suggested change
flyctl deploy . \
--ha=false \ # use only one machine for staging
# use only one machine for staging
flyctl deploy . \
--ha=false \

Copilot uses AI. Check for mistakes.
uses: superfly/flyctl-actions/[email protected]

- name: 🚁️ Deploy PR app to Fly.io
if: ${{ github.event.action != 'closed' && env.FLY_API_TOKEN }}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition only checks if FLY_API_TOKEN exists but doesn't verify that this is a pull request event. This could cause issues if the action type is something other than 'closed' but FLY_API_TOKEN is set. Consider adding a check for github.event_name == 'pull_request' as well.

Copilot uses AI. Check for mistakes.

> **Note**: Make sure this name matches the `app` set in your `fly.toml`
> file. Otherwise, you will not be able to deploy.
1. Change the app name in fly.toml to name of the app you just created.
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instructions say to change the app name in fly.toml to match the app created, but this contradicts the earlier note in line 41 which says "Change the app name in fly.toml to name of the app you just created." The workflow uses the app name from fly.toml to generate PR app names, so the instruction should also mention that the app name will be used as a prefix for PR environments (e.g., app-name-pr-123).

Suggested change
1. Change the app name in fly.toml to name of the app you just created.
1. Change the `app` name in `fly.toml` to match the Fly app you just created. This
name will also be used as a prefix for preview (PR) environments (for example,
`your-app-name-pr-123`).

Copilot uses AI. Check for mistakes.
flyctl secrets --app $FLY_APP_NAME set SESSION_SECRET=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) SENTRY_DSN=${{ secrets.SENTRY_DSN }} RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
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
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The staging deployment is missing the volume creation step. The production setup requires a persistent volume for the SQLite database (created via fly volumes create), but the staging deployment workflow doesn't create this volume. Without it, the staging app will likely fail to start or lose data between deploys. The volume should be created if the app doesn't exist, similar to how consul and storage are set up.

Suggested change
flyctl storage create --app $FLY_APP_NAME --name epic-stack-$FLY_APP_NAME --yes > /dev/null 2>&1
flyctl storage create --app $FLY_APP_NAME --name epic-stack-$FLY_APP_NAME --yes > /dev/null 2>&1
# Create persistent volume for SQLite database
flyctl volumes create data --region "$FLY_REGION" --app "$FLY_APP_NAME" --yes

Copilot uses AI. Check for mistakes.
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 "$app" -y || true
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable reference "$app" is incorrect. It should be "$FLY_APP_NAME" to match the variable defined on line 229.

Suggested change
flyctl apps destroy "$app" -y || true
flyctl apps destroy "$FLY_APP_NAME" -y || true

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +224
- name: 🚁️ Deploy PR app to Fly.io
if: ${{ github.event.action != 'closed' && env.FLY_API_TOKEN }}
run: |
flyctl deploy \
--build-only \
--push \
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
# 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) SENTRY_DSN=${{ secrets.SENTRY_DSN }} RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
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 deploy . \
--ha=false \ # use only one machine for staging
--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 }}
--build-secret SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deploy-staging job doesn't populate the url output that it declares on line 180. This output is referenced in the environment section on line 183, but it's never set using the steps.deploy.outputs syntax. You should set this output, likely after the deployment completes successfully.

Copilot uses AI. Check for mistakes.
await $I`fly secrets set SESSION_SECRET=${getRandomString32()} INTERNAL_COMMAND_TOKEN=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} ALLOW_INDEXING=false --app ${APP_NAME}-staging`
}
await $I`fly secrets set SESSION_SECRET=${getRandomString32()} INTERNAL_COMMAND_TOKEN=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} --app ${APP_NAME}`
await $I`fly secrets set SESSION_SECRET=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} --app ${APP_NAME}`
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The INTERNAL_COMMAND_TOKEN secret has been removed from the fly secrets setup, but it's still required in the codebase. The env.server.ts file expects INTERNAL_COMMAND_TOKEN to be defined (line 8), and it's used in app/routes/admin/cache/sqlite.server.ts for authentication between LiteFS instances. This will cause the application to fail at startup with an "Invalid environment variables" error.

Suggested change
await $I`fly secrets set SESSION_SECRET=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} --app ${APP_NAME}`
await $I`fly secrets set SESSION_SECRET=${getRandomString32()} HONEYPOT_SECRET=${getRandomString32()} INTERNAL_COMMAND_TOKEN=${getRandomString32()} --app ${APP_NAME}`

Copilot uses AI. Check for mistakes.
secrets, this is to prevent duplicate content from being indexed multiple
times by search engines. To do this you can run the following commands:
```sh
gh secret set FLY_API_TOKEN --body "<token>"
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commands shown use the GitHub CLI without specifying the repository. While this may work if run from within the repository directory, it would be clearer and more robust to include the repository context. Consider adding a note that these commands should be run from within the repository directory, or show how to specify the repository explicitly.

Suggested change
gh secret set FLY_API_TOKEN --body "<token>"
gh secret set FLY_API_TOKEN --body "<token>" --repo <GITHUB_OWNER>/<GITHUB_REPO>

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +2
app = "epic-rsc-stack"
primary_region = "ams"
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app name and primary_region have been changed from "epic-stack-template" in "sjc" to "epic-rsc-stack" in "ams". This appears to be specific to a particular deployment rather than a template configuration. These values should typically remain as placeholders or be set during the initialization process, not committed with specific values.

Suggested change
app = "epic-rsc-stack"
primary_region = "ams"
app = "epic-stack-template"
primary_region = "sjc"

Copilot uses AI. Check for mistakes.
# 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
Copy link

Copilot AI Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Secrets are being set during the initial app creation, but if the app already exists and the command fails (line 206), the secrets will never be set. This could happen if an app was manually created or if there was a previous failed deployment. Consider checking if secrets need to be set even for existing apps, or handle the case where the app exists but secrets might be missing.

Suggested change
# Ensure required secrets are set even if the app already existed or a previous deployment failed
REQUIRED_SECRETS=(SESSION_SECRET HONEYPOT_SECRET SENTRY_DSN RESEND_API_KEY)
MISSING_REQUIRED_SECRET=false
for secret in "${REQUIRED_SECRETS[@]}"; do
if ! flyctl secrets list --app "$FLY_APP_NAME" --json | jq -r '.[].Name' | grep -qx "$secret"; then
MISSING_REQUIRED_SECRET=true
break
fi
done
if [ "$MISSING_REQUIRED_SECRET" = true ]; then
flyctl secrets --app $FLY_APP_NAME set SESSION_SECRET=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) SENTRY_DSN=${{ secrets.SENTRY_DSN }} RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
fi

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant