diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml new file mode 100644 index 0000000000..38e5640509 --- /dev/null +++ b/.github/workflows/preview-cleanup.yml @@ -0,0 +1,62 @@ +name: PR Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + cleanup-preview: + name: Cleanup Preview Environment + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + APP_NAME: relay-pr-${{ github.event.pull_request.number }} + + steps: + - name: Setup Fly CLI + uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Check if app exists + id: check-app + run: | + if flyctl apps list --json | jq -e '.[] | select(.Name == "'"$APP_NAME"'")' > /dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Destroy preview app + if: steps.check-app.outputs.exists == 'true' + run: | + echo "Destroying preview app: $APP_NAME" + flyctl apps destroy $APP_NAME --yes + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: '' + + - name: Update comment to show destroyed + if: steps.find-comment.outputs.comment-id != '' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + edit-mode: replace + body: | + + ## Preview Environment + + | | | + |---|---| + | **Status** | Destroyed | + | **Reason** | PR ${{ github.event.action }} | + + --- + The preview environment has been cleaned up. diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000000..9d82ba65e0 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,186 @@ +name: PR Preview Environment + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy-preview: + name: Deploy Preview + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + APP_NAME: relay-pr-${{ github.event.pull_request.number }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Fly CLI + uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find-comment-initial + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: '' + + - name: Post deploying status + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-comment-initial.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + edit-mode: replace + body: | + + ## Preview Environment + + | | | + |---|---| + | **Status** | Deploying... | + | **Commit** | ${{ github.event.pull_request.head.sha }} | + + --- + Deployment in progress. This comment will be updated when complete. + + - name: Check if app exists + id: check-app + run: | + if flyctl apps list --json | jq -e '.[] | select(.Name == "'"$APP_NAME"'")' > /dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Create Fly.io app + if: steps.check-app.outputs.exists == 'false' + run: | + flyctl apps create $APP_NAME --org personal + + - name: Copy secrets from production + if: steps.check-app.outputs.exists == 'false' + run: | + # Get secrets from production app and set on preview + # Note: This copies secret names but values must be set separately + # For preview, we use staging database URL and other staging secrets + flyctl secrets set \ + SESSION_SECRET="${{ secrets.PREVIEW_SESSION_SECRET }}" \ + DATABASE_URL="${{ secrets.PREVIEW_DATABASE_URL }}" \ + REDIS_URL="${{ secrets.PREVIEW_REDIS_URL }}" \ + GITHUB_CLIENT_ID="${{ secrets.PREVIEW_GITHUB_CLIENT_ID }}" \ + GITHUB_CLIENT_SECRET="${{ secrets.PREVIEW_GITHUB_CLIENT_SECRET }}" \ + VAULT_ENCRYPTION_KEY="${{ secrets.PREVIEW_VAULT_ENCRYPTION_KEY }}" \ + NANGO_SECRET_KEY="${{ secrets.PREVIEW_NANGO_SECRET_KEY }}" \ + --app $APP_NAME + + - name: Generate fly.toml for preview + run: | + cat > fly.preview.toml << EOF + app = "${APP_NAME}" + primary_region = "sjc" + + [build] + dockerfile = "Dockerfile" + + [deploy] + strategy = "immediate" + + [env] + NODE_ENV = "preview" + PORT = "3000" + PREVIEW_MODE = "true" + PR_NUMBER = "${{ github.event.pull_request.number }}" + + [http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] + + [http_service.concurrency] + type = "requests" + hard_limit = 100 + soft_limit = 80 + + [[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 + + [checks] + [checks.health] + grace_period = "30s" + interval = "60s" + method = "get" + path = "/health" + port = 3000 + timeout = "10s" + type = "http" + EOF + + - name: Deploy to Fly.io + id: deploy + run: | + flyctl deploy \ + --config fly.preview.toml \ + --remote-only \ + --wait-timeout 300 \ + --app $APP_NAME + + - name: Wait for health check + run: | + echo "Waiting for app to be healthy..." + PREVIEW_URL="https://${APP_NAME}.fly.dev" + + # Wait up to 2 minutes for health check + for i in {1..24}; do + if curl -sf "${PREVIEW_URL}/health" > /dev/null 2>&1; then + echo "Health check passed!" + exit 0 + fi + echo "Attempt $i/24: Waiting for app to be healthy..." + sleep 5 + done + + echo "Health check failed after 2 minutes" + exit 1 + + - name: Find existing comment + uses: peter-evans/find-comment@v3 + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: '' + + - name: Create or update comment + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + edit-mode: replace + body: | + + ## Preview Environment + + | | | + |---|---| + | **Status** | Deployed | + | **URL** | https://${{ env.APP_NAME }}.fly.dev | + | **Dashboard** | https://${{ env.APP_NAME }}.fly.dev/dashboard | + | **Commit** | ${{ github.event.pull_request.head.sha }} | + + --- + This preview environment will be automatically destroyed when the PR is closed.