-
Notifications
You must be signed in to change notification settings - Fork 2
190 add a staging environment for previewing and testing ahead of a new deployment #201
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
lmcdonough
wants to merge
55
commits into
main
Choose a base branch
from
190-add-a-staging-environment-for-previewing-and-testing-ahead-of-a-new-deployment
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 39 commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
d456c17
Add GitHub Actions workflow and Docker Compose for PR preview deployment
lmcdonough 3228f90
Refactor GitHub Actions workflow and Docker Compose for PR preview de…
lmcdonough ebf81fd
Optimize Docker build cache strategy for faster PR preview builds
lmcdonough c491f38
Add nightly cache warming workflow for faster PR preview builds
lmcdonough aa23903
Add cleanup workflow for PR preview environments on closure
lmcdonough 8cd2498
Add documentation for PR preview environments setup and usage
lmcdonough 8e84eb2
Enhance PR preview deployment workflow with improved CI checks, cachi…
lmcdonough 24bc45d
Refine CI Dependency Gate to wait for essential checks only, enabling…
lmcdonough 2a5b639
updates deploy-pr-preview ghactions workflow to use neo as the arm64 …
lmcdonough 6ea91e7
Refactor CI workflow to enhance linting and testing stages, ensuring …
lmcdonough af31b8b
Refactor PR preview deployment workflow by removing redundant comment…
lmcdonough 87e0fc7
Refactor PR preview deployment workflow by improving SSH setup, updat…
lmcdonough c3ca3c6
Fix CI workflows: add required toolchain parameter to Rust installati…
lmcdonough ecc8b77
Fix critical deployment bugs in PR preview workflow
lmcdonough b6a5d3c
Fix clippy derivable_impls warning in Status enum
lmcdonough 1d28400
Remove -D warnings from clippy to allow warnings without failing CI
lmcdonough 46ddb50
Make format check non-blocking in CI workflows
lmcdonough 6b7fc7a
Make build provenance attestation non-blocking in PR preview workflow
lmcdonough 7fd6f89
Replace Tailscale GitHub Action with manual verification
lmcdonough d3bc0b4
Fix attestation subject-name to exclude image tag per action requirem…
lmcdonough f9690a6
Fix deployment script to strip newlines from secrets and fix heredoc …
lmcdonough 94105f9
Fix YAML syntax error: correct indentation of heredoc content
lmcdonough b98e675
Fix variable expansion in PR preview deployment script
lmcdonough 17075eb
Fix PR preview deployment: resolve connection string parsing, add RUS…
lmcdonough af7e84c
Refactor environment variable setup in PR preview deployment: streaml…
lmcdonough 995bf35
Refactor environment variable handling in PR preview deployment: swit…
lmcdonough 35e06f9
Sanitize secrets in .env file to prevent authentication failures
lmcdonough 5748dcf
Remove volumes on docker compose down to prevent stale credentials
lmcdonough 16ae553
Enhance PR preview deployment workflow: add detailed comments, improv…
lmcdonough 32b4e85
Make migrator idempotent: ensure schema exists before running migrations
lmcdonough 78b816e
Fix CORS wildcard origin configuration to prevent backend crash
lmcdonough 7bc5799
Fix wildcard CORS handling for PR previews
lmcdonough 11f883d
Make PR preview comment idempotent: delete old and post fresh
lmcdonough 998a2e3
Add PR preview environments section to main README
lmcdonough 7925aad
Update PR preview environments runbook: remove cache section and upda…
lmcdonough 1ea7c40
Remove warm-main-cache workflow and refactor cleanup-pr-preview for c…
lmcdonough 50634b2
Update cleanup workflow to delete PR-specific images from RPi5
lmcdonough ec33560
Update PR preview documentation to reflect current workflow
lmcdonough 9a2a3ad
Update PR preview documentation to clarify deployment times and acces…
lmcdonough 895fa27
fixing indentation error on closing pr ghactions workflow.
lmcdonough 360f8ac
Merge remote-tracking branch 'origin/main' into 190-add-a-staging-env…
lmcdonough 11594fb
Enhance PR cleanup workflow with main-arm64 build and layered caching
lmcdonough 3e49579
Use environment variables for Rust/Cargo configuration
lmcdonough e686bb5
Add PR preview deployment system with reusable workflow
lmcdonough 0232cd4
Simplify PR preview workflows - remove secret duplication
lmcdonough c908140
Add PR preview cleanup workflows
lmcdonough 247956a
Enhance PR preview workflows with detailed secret declarations and cl…
lmcdonough 622b956
Fix PR preview workflows - add secrets inheritance and optimize caching
lmcdonough dff0f0d
Fix PR preview deployment skipping - remove explicit result check
lmcdonough f9a656d
Test PR preview workflow - trigger deployment
lmcdonough 1c7ad13
Fix secret requirements for cross-repo workflow calls
lmcdonough c833b20
Fix deploy-to-rpi5 job skipping - add always() condition
lmcdonough 591d33f
Fix compose file checkout - use backend branch instead of main
lmcdonough c8b2e39
Fix missing environment variables in schema preparation step
lmcdonough dc1d2e8
Fix log level filter - change to uppercase INFO
lmcdonough 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
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
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,304 @@ | ||
| # ============================================================================= | ||
| # PR Preview Cleanup Workflow | ||
| # ============================================================================= | ||
| # Purpose: Cleans up PR preview environments when PRs are closed/merged | ||
| # Features: Selective cleanup, volume retention policy, SSH cleanup on RPi5 | ||
| # Target: Raspberry Pi 5 (ARM64) via Tailscale SSH | ||
| # ============================================================================= | ||
|
|
||
| name: Cleanup PR Preview Environment | ||
|
|
||
| # Trigger when PR is closed (includes both close and merge events) | ||
| on: | ||
| pull_request: | ||
| types: [closed] | ||
| branches: | ||
| - main | ||
|
|
||
| # Manual trigger for cleanup of specific PR numbers | ||
| workflow_dispatch: | ||
| inputs: | ||
| pr_number: | ||
| description: "PR number to clean up" | ||
| required: true | ||
| type: string | ||
|
|
||
| # Only need read access to repo and write to comment on PRs | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| cleanup-preview: | ||
| name: Cleanup PR Preview on RPi5 | ||
| runs-on: [self-hosted, Linux, ARM64, neo] | ||
| environment: pr-preview | ||
|
|
||
| steps: | ||
| # Calculate cleanup context and determine volume retention policy | ||
| - name: Set Cleanup Context | ||
| id: context | ||
| run: | | ||
| # Extract PR metadata | ||
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | ||
| PR_NUM="${{ github.event.pull_request.number }}" | ||
| IS_MERGED="${{ github.event.pull_request.merged }}" | ||
| else | ||
| PR_NUM="${{ inputs.pr_number }}" | ||
| IS_MERGED="false" | ||
| fi | ||
|
|
||
| # Calculate ports for logging/verification (same formula as deployment) | ||
| BACKEND_CONTAINER_PORT=${{ vars.BACKEND_PORT_BASE }} | ||
| BACKEND_EXTERNAL_PORT=$((${{ vars.BACKEND_PORT_BASE }} + PR_NUM)) | ||
| POSTGRES_EXTERNAL_PORT=$((${{ vars.POSTGRES_PORT_BASE }} + PR_NUM)) | ||
| FRONTEND_EXTERNAL_PORT=$((${{ vars.FRONTEND_PORT_BASE }} + PR_NUM)) | ||
|
|
||
| # Store context for subsequent steps | ||
| echo "pr_number=${PR_NUM}" >> $GITHUB_OUTPUT | ||
| echo "is_merged=${IS_MERGED}" >> $GITHUB_OUTPUT | ||
| echo "backend_container_port=${BACKEND_CONTAINER_PORT}" >> $GITHUB_OUTPUT | ||
| echo "backend_port=${BACKEND_EXTERNAL_PORT}" >> $GITHUB_OUTPUT | ||
| echo "postgres_port=${POSTGRES_EXTERNAL_PORT}" >> $GITHUB_OUTPUT | ||
| echo "frontend_port=${FRONTEND_EXTERNAL_PORT}" >> $GITHUB_OUTPUT | ||
| echo "project_name=pr-${PR_NUM}" >> $GITHUB_OUTPUT | ||
|
|
||
| # Cleanup strategy: | ||
| # - PR-specific images removed from RPi5 to prevent accumulation | ||
| # - Shared images (postgres:17) retained on RPi5 for reuse | ||
| # - Images in GHCR kept for auditability and future deployments | ||
| # - Volume cleanup based on whether PR was merged or closed | ||
| if [[ "${IS_MERGED}" == "true" ]]; then | ||
| echo "cleanup_reason=merged" >> $GITHUB_OUTPUT | ||
| echo "volume_action=retain" >> $GITHUB_OUTPUT | ||
| echo "::notice::🔀 PR #${PR_NUM} was merged - retaining volume for 7 days" | ||
| else | ||
| echo "cleanup_reason=closed" >> $GITHUB_OUTPUT | ||
| echo "volume_action=remove" >> $GITHUB_OUTPUT | ||
| echo "::notice::🚫 PR #${PR_NUM} was closed without merge - removing volume immediately" | ||
| fi | ||
|
|
||
| echo "::notice::🗑️ PR-specific images will be removed from RPi5" | ||
| echo "::notice::📦 Images in GHCR retained for auditability and future deployments" | ||
|
|
||
| # Verify we can reach the RPi5 through Tailscale VPN | ||
| - name: Verify Tailscale Connection | ||
| run: | | ||
| # Tailscale is pre-installed and already connected on the self-hosted runner | ||
| # Just verify the connection status | ||
| echo "🔍 Checking Tailscale connection status..." | ||
| tailscale status || echo "⚠️ Tailscale status check failed, but continuing..." | ||
| echo "✅ Tailscale verification complete" | ||
|
|
||
| # Set up SSH key and known hosts to connect securely to RPi5 | ||
| - name: Setup SSH Configuration | ||
| run: | | ||
| mkdir -p ~/.ssh | ||
| chmod 700 ~/.ssh | ||
| echo "${{ secrets.RPI5_SSH_KEY }}" > ~/.ssh/id_ed25519 | ||
| chmod 600 ~/.ssh/id_ed25519 | ||
| echo "${{ secrets.RPI5_HOST_KEY }}" >> ~/.ssh/known_hosts | ||
| chmod 644 ~/.ssh/known_hosts | ||
|
|
||
| # Test SSH connection to RPi5 before attempting cleanup | ||
| - name: Test SSH Connection | ||
| run: | | ||
| echo "🔍 Testing SSH connection to ${{ secrets.RPI5_TAILSCALE_NAME }}..." | ||
| if ! ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes -o ConnectTimeout=10 \ | ||
| -i ~/.ssh/id_ed25519 \ | ||
| ${{ secrets.RPI5_USERNAME }}@${{ secrets.RPI5_TAILSCALE_NAME }} \ | ||
| 'echo "SSH connection successful"'; then | ||
| echo "::error::SSH connection failed to ${{ secrets.RPI5_TAILSCALE_NAME }}" | ||
| exit 1 | ||
| fi | ||
| echo "::notice::✅ SSH connection verified" | ||
|
|
||
| # Execute cleanup commands on RPi5 via SSH | ||
| - name: Cleanup Deployment on RPi5 | ||
| run: | | ||
| PR_NUMBER="${{ steps.context.outputs.pr_number }}" | ||
| PROJECT_NAME="${{ steps.context.outputs.project_name }}" | ||
| VOLUME_ACTION="${{ steps.context.outputs.volume_action }}" | ||
|
|
||
| echo "🧹 Starting cleanup for PR #${PR_NUMBER}..." | ||
|
|
||
| # Execute cleanup script on RPi5 with proper error handling | ||
| cat << 'CLEANUP_SCRIPT' | ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_ed25519 \ | ||
| ${{ secrets.RPI5_USERNAME }}@${{ secrets.RPI5_TAILSCALE_NAME }} \ | ||
| /bin/bash | ||
| set -eo pipefail | ||
|
|
||
| # Variables passed from GitHub Actions | ||
| PR_NUMBER="${{ steps.context.outputs.pr_number }}" | ||
| PROJECT_NAME="${{ steps.context.outputs.project_name }}" | ||
| VOLUME_ACTION="${{ steps.context.outputs.volume_action }}" | ||
| RPI5_USERNAME="${{ secrets.RPI5_USERNAME }}" | ||
|
|
||
| # Guard against accidentally running on the GitHub runner | ||
| if [[ "$(hostname)" == *"runner"* ]] || [[ "$(pwd)" == *"runner"* ]]; then | ||
| echo "❌ Cleanup running on GitHub runner instead of target server!" | ||
| exit 1 | ||
| fi | ||
|
|
||
| cd /home/${RPI5_USERNAME} | ||
|
|
||
| echo "🛑 Stopping and removing containers for ${PROJECT_NAME}..." | ||
| if docker compose -p ${PROJECT_NAME} -f pr-${PR_NUMBER}-compose.yaml down 2>/dev/null; then | ||
| echo "✅ Containers stopped and removed" | ||
| else | ||
| echo "⚠️ No running containers found (already cleaned up?)" | ||
| fi | ||
|
|
||
| echo "📁 Removing compose file..." | ||
| if rm -f pr-${PR_NUMBER}-compose.yaml; then | ||
| echo "✅ Compose file removed" | ||
| else | ||
| echo "⚠️ Compose file not found" | ||
| fi | ||
|
|
||
| echo "📁 Removing environment file..." | ||
| if rm -f pr-${PR_NUMBER}.env; then | ||
| echo "✅ Environment file removed" | ||
| else | ||
| echo "⚠️ Environment file not found" | ||
| fi | ||
|
|
||
| # Volume cleanup based on merge status | ||
| if [[ "${VOLUME_ACTION}" == "remove" ]]; then | ||
| echo "🗑️ Removing database volume (PR closed without merge)..." | ||
| if docker volume rm ${PROJECT_NAME}_postgres_data 2>/dev/null; then | ||
| echo "✅ Volume removed" | ||
| else | ||
| echo "⚠️ Volume not found (may have been cleaned up already)" | ||
| fi | ||
| else | ||
| echo "⏰ Database volume retained for 7 days (PR merged)" | ||
| echo "📅 Volume ${PROJECT_NAME}_postgres_data will auto-expire: $(date -d '+7 days' '+%Y-%m-%d' 2>/dev/null || date -v+7d '+%Y-%m-%d')" | ||
| echo "💡 Manual cleanup command: docker volume rm ${PROJECT_NAME}_postgres_data" | ||
| fi | ||
|
|
||
| # Remove PR-specific Docker images (keep shared postgres:17 image) | ||
| echo "" | ||
| echo "🗑️ Removing PR-specific Docker images..." | ||
| PR_IMAGES=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep "pr-${PR_NUMBER}" || true) | ||
| if [[ -n "$PR_IMAGES" ]]; then | ||
| echo "$PR_IMAGES" | while read -r image; do | ||
| echo " Removing: $image" | ||
| docker rmi -f "$image" 2>/dev/null || echo " ⚠️ Failed to remove $image" | ||
| done | ||
| echo "✅ PR-specific images removed" | ||
| else | ||
| echo "⚠️ No PR-specific images found (may have been cleaned up already)" | ||
| fi | ||
| echo "📦 Shared images retained: postgres:17 (used by all PRs)" | ||
|
|
||
| echo "" | ||
| echo "📊 Remaining PR environments on RPi5:" | ||
| REMAINING=$(docker ps --filter 'name=pr-' --format '{{.Names}}' 2>/dev/null | wc -l) | ||
| if [[ $REMAINING -gt 0 ]]; then | ||
| echo "Active PR environments: $REMAINING" | ||
| docker ps --filter 'name=pr-' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null | head -6 | ||
| else | ||
| echo "No PR environments currently running ✨" | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "✅ Cleanup complete for PR #${PR_NUMBER}!" | ||
| CLEANUP_SCRIPT | ||
|
|
||
| # Post cleanup status to PR as comment for developer visibility | ||
| - name: Update PR Comment with Cleanup Status | ||
| if: github.event_name == 'pull_request' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| // Extract context from previous steps | ||
| const prNumber = ${{ steps.context.outputs.pr_number }}; | ||
| const isMerged = '${{ steps.context.outputs.is_merged }}' === 'true'; | ||
| const cleanupReason = isMerged ? 'merged into main' : 'closed without merging'; | ||
| const volumeStatus = isMerged | ||
| ? '📅 Retained for 7 days (auto-cleanup scheduled)' | ||
| : '🗑️ Removed immediately'; | ||
| const backendPort = ${{ steps.context.outputs.backend_port }}; | ||
| const postgresPort = ${{ steps.context.outputs.postgres_port }}; | ||
|
|
||
| // Create comprehensive cleanup status comment | ||
| const comment = `## 🧹 PR Preview Environment Cleaned Up! | ||
|
|
||
| ### 📊 Cleanup Summary | ||
| | Resource | Status | | ||
| |----------|--------| | ||
| | **Containers** | ✅ Stopped and removed | | ||
| | **PR-Specific Images** | ✅ Removed from RPi5 | | ||
| | **Shared Images** | 📦 Retained (postgres:17) | | ||
| | **Network** | ✅ Removed | | ||
| | **Compose File** | ✅ Deleted | | ||
| | **Environment File** | ✅ Deleted | | ||
| | **Database Volume** | ${volumeStatus} | | ||
|
|
||
| ### 📝 Details | ||
| - **PR Number:** #${prNumber} | ||
| - **Reason:** ${cleanupReason} | ||
| - **Backend Port:** ${backendPort} (now available) | ||
| - **Postgres Port:** ${postgresPort} (now available) | ||
| - **Project Name:** \`pr-${prNumber}\` | ||
|
|
||
| ### 📦 Image Cleanup Policy | ||
| - **PR-specific images removed** from RPi5 to prevent accumulation | ||
| - **Shared images retained** (postgres:17 used by all PRs) | ||
| - Images remain in GHCR for auditability and future deployments | ||
| - Frees disk space on RPi5 while maintaining deployment history | ||
|
|
||
| ### ⏰ Volume Retention Policy | ||
| ${isMerged | ||
| ? '- **Merged PRs:** Database volume retained for 7 days\n- Allows post-merge investigation if needed\n- Volume: `pr-' + prNumber + '_postgres_data`\n- Auto-cleanup: ' + new Date(Date.now() + 7*24*60*60*1000).toISOString().split('T')[0] | ||
| : '- **Closed PRs:** Database volume removed immediately\n- Frees up disk space on RPi5\n- No data retention for abandoned PRs'} | ||
|
|
||
| --- | ||
| *Cleaned up: ${new Date().toISOString()}* | ||
| *Workflow: [\`cleanup-pr-preview.yml\`](https://github.com/${{ github.repository }}/actions/workflows/cleanup-pr-preview.yml)*`; | ||
|
|
||
| // Find and delete existing deployment comment, then post cleanup as new comment | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber, | ||
| }); | ||
|
|
||
| // Look for original deployment comment from bot | ||
| const botComment = comments.find(c => | ||
| c.user.type === 'Bot' && c.body.includes('PR Preview Environment Deployed') | ||
| ); | ||
|
|
||
| if (botComment) { | ||
| // Delete the deployment comment since environment is cleaned up | ||
| await github.rest.issues.deleteComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: botComment.id, | ||
| }); | ||
| console.log('✅ Deleted deployment comment (environment cleaned up)'); | ||
| } | ||
|
|
||
| // Post fresh cleanup comment | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber, | ||
| body: comment, | ||
| }); | ||
| console.log('✅ Posted cleanup status comment'); | ||
|
|
||
| # Log final cleanup summary to workflow output | ||
| - name: Cleanup Summary | ||
| run: | | ||
| echo "::notice::✅ Cleanup complete for PR #${{ steps.context.outputs.pr_number }}" | ||
| echo "::notice::🗑️ Resources removed: containers, PR-specific images, network, compose file, env file" | ||
| echo "::notice::📦 Shared images retained: postgres:17 (used by all PRs)" | ||
| echo "::notice::📦 Images in GHCR retained for auditability and future deployments" | ||
| if [[ "${{ steps.context.outputs.volume_action }}" == "retain" ]]; then | ||
| echo "::notice::📦 Volume retained for 7 days (merged PR retention policy)" | ||
| else | ||
| echo "::notice::🗑️ Volume removed immediately (closed PR cleanup)" | ||
| fi | ||
| echo "::notice::🎉 RPi5 disk space freed for other PR previews" | ||
Oops, something went wrong.
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.