deps(build)(deps-dev): bump @semantic-release/release-notes-generator from 12.1.0 to 14.1.0 in /backend #65
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
| name: 🔍 PR Preview Environment | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| branches: [main, develop] | |
| pull_request_review: | |
| types: [submitted] | |
| issue_comment: | |
| types: [created, edited] | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| PREVIEW_DOMAIN: preview.yourdomain.com | |
| # Concurrency: one preview per PR | |
| concurrency: | |
| group: pr-preview-${{ github.event.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ============================================================================= | |
| # Check if Preview Should Be Created | |
| # ============================================================================= | |
| check-preview: | |
| name: 🔍 Check Preview Requirements | |
| runs-on: ubuntu-latest | |
| if: | | |
| github.event.pull_request.draft == false && | |
| (github.event_name == 'pull_request' || | |
| github.event_name == 'pull_request_review' || | |
| (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/deploy-preview'))) | |
| timeout-minutes: 5 | |
| outputs: | |
| should_deploy: ${{ steps.decision.outputs.should_deploy }} | |
| pr_number: ${{ steps.decision.outputs.pr_number }} | |
| branch_name: ${{ steps.decision.outputs.branch_name }} | |
| safe_branch_name: ${{ steps.decision.outputs.safe_branch_name }} | |
| preview_url: ${{ steps.decision.outputs.preview_url }} | |
| steps: | |
| - name: 📥 Checkout PR code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: 🔍 Determine deployment decision | |
| id: decision | |
| run: | | |
| # Get PR number from different event types | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| PR_NUMBER="${{ github.event.number }}" | |
| BRANCH_NAME="${{ github.event.pull_request.head.ref }}" | |
| elif [[ "${{ github.event_name }}" == "pull_request_review" ]]; then | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| BRANCH_NAME="${{ github.event.pull_request.head.ref }}" | |
| elif [[ "${{ github.event_name }}" == "issue_comment" ]]; then | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| # Get branch name from PR API | |
| BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq '.headRefName') | |
| fi | |
| # Create safe branch name for subdomain (lowercase, replace special chars) | |
| SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') | |
| # Determine if we should deploy | |
| SHOULD_DEPLOY="false" | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| # Deploy on PR open/sync/reopen | |
| SHOULD_DEPLOY="true" | |
| elif [[ "${{ github.event_name }}" == "pull_request_review" && "${{ github.event.review.state }}" == "approved" ]]; then | |
| # Deploy on approval | |
| SHOULD_DEPLOY="true" | |
| elif [[ "${{ github.event_name }}" == "issue_comment" && "${{ contains(github.event.comment.body, '/deploy-preview') }}" ]]; then | |
| # Deploy on manual command | |
| SHOULD_DEPLOY="true" | |
| fi | |
| # Set outputs | |
| echo "should_deploy=$SHOULD_DEPLOY" >> $GITHUB_OUTPUT | |
| echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
| echo "safe_branch_name=$SAFE_BRANCH_NAME" >> $GITHUB_OUTPUT | |
| echo "preview_url=https://pr-$PR_NUMBER.${{ env.PREVIEW_DOMAIN }}" >> $GITHUB_OUTPUT | |
| echo "🔍 Preview Decision:" | |
| echo " Should Deploy: $SHOULD_DEPLOY" | |
| echo " PR Number: $PR_NUMBER" | |
| echo " Branch: $BRANCH_NAME" | |
| echo " Safe Branch: $SAFE_BRANCH_NAME" | |
| echo " Preview URL: https://pr-$PR_NUMBER.${{ env.PREVIEW_DOMAIN }}" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ============================================================================= | |
| # Build Preview Image | |
| # ============================================================================= | |
| build-preview: | |
| name: 🏗️ Build Preview Image | |
| runs-on: ubuntu-latest | |
| needs: [check-preview] | |
| if: needs.check-preview.outputs.should_deploy == 'true' | |
| timeout-minutes: 20 | |
| outputs: | |
| image_tag: ${{ steps.image.outputs.tag }} | |
| steps: | |
| - name: 📥 Checkout PR code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: 🐳 Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: 🔐 Login to Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: 🏷️ Generate image tag | |
| id: image | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| COMMIT_SHA="${{ github.event.pull_request.head.sha || github.sha }}" | |
| SHORT_SHA=${COMMIT_SHA:0:7} | |
| IMAGE_TAG="pr-${PR_NUMBER}-${SHORT_SHA}" | |
| echo "tag=$IMAGE_TAG" >> $GITHUB_OUTPUT | |
| echo "📦 Preview image tag: $IMAGE_TAG" | |
| - name: 🏗️ Build and push preview image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image.outputs.tag }} | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ needs.check-preview.outputs.pr_number }} | |
| labels: | | |
| org.opencontainers.image.title=ModernAPI Preview | |
| org.opencontainers.image.description=Preview build for PR #${{ needs.check-preview.outputs.pr_number }} | |
| org.opencontainers.image.version=${{ steps.image.outputs.tag }} | |
| org.opencontainers.image.pr.number=${{ needs.check-preview.outputs.pr_number }} | |
| org.opencontainers.image.pr.branch=${{ needs.check-preview.outputs.branch_name }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| BUILD_VERSION=${{ steps.image.outputs.tag }} | |
| BUILD_ENVIRONMENT=preview | |
| BUILD_COMMIT=${{ github.event.pull_request.head.sha || github.sha }} | |
| BUILD_PR=${{ needs.check-preview.outputs.pr_number }} | |
| # ============================================================================= | |
| # Deploy Preview Environment | |
| # ============================================================================= | |
| deploy-preview: | |
| name: 🚀 Deploy Preview | |
| runs-on: ubuntu-latest | |
| needs: [check-preview, build-preview] | |
| if: needs.check-preview.outputs.should_deploy == 'true' | |
| timeout-minutes: 15 | |
| environment: | |
| name: preview-pr-${{ needs.check-preview.outputs.pr_number }} | |
| url: ${{ needs.check-preview.outputs.preview_url }} | |
| steps: | |
| - name: 📥 Checkout PR code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: 🔧 Prepare preview environment | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| IMAGE_TAG="${{ needs.build-preview.outputs.image_tag }}" | |
| echo "🔧 Setting up preview environment for PR #$PR_NUMBER" | |
| # Create preview-specific environment file | |
| cp .env.example .env.preview | |
| # Configure for preview environment | |
| sed -i "s/DOMAIN=yourdomain.com/DOMAIN=${{ env.PREVIEW_DOMAIN }}/" .env.preview | |
| sed -i "s/API_DOMAIN=api.yourdomain.com/API_DOMAIN=pr-$PR_NUMBER.${{ env.PREVIEW_DOMAIN }}/" .env.preview | |
| sed -i "s/ASPNETCORE_ENVIRONMENT=Production/ASPNETCORE_ENVIRONMENT=Preview/" .env.preview | |
| # Set preview-specific secrets (less sensitive than production) | |
| echo "JWT_SECRET=PreviewJWTSecretForPR${PR_NUMBER}Testing123456789!" >> .env.preview | |
| echo "POSTGRES_PASSWORD=preview_password_$PR_NUMBER" >> .env.preview | |
| echo "POSTGRES_DB=modernapi_pr_$PR_NUMBER" >> .env.preview | |
| echo "POSTGRES_USER=modernapi_pr_$PR_NUMBER" >> .env.preview | |
| echo "DATABASE_URL=Host=postgres;Database=modernapi_pr_${PR_NUMBER};Username=modernapi_pr_${PR_NUMBER};Password=preview_password_${PR_NUMBER}" >> .env.preview | |
| # Set monitoring credentials | |
| echo "GRAFANA_ADMIN_PASSWORD=preview_grafana_$PR_NUMBER" >> .env.preview | |
| echo "SEQ_API_KEY=preview_seq_key_$PR_NUMBER" >> .env.preview | |
| echo "ACME_EMAIL=preview@${{ env.PREVIEW_DOMAIN }}" >> .env.preview | |
| # Set image version | |
| echo "MODERNAPI_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" >> .env.preview | |
| echo "PR_NUMBER=$PR_NUMBER" >> .env.preview | |
| echo "PREVIEW_MODE=true" >> .env.preview | |
| - name: 🗄️ Setup preview database | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| echo "🗄️ Setting up isolated database for PR #$PR_NUMBER" | |
| # Create docker-compose override for preview | |
| cat > docker-compose.preview.yml << EOF | |
| version: '3.8' | |
| services: | |
| postgres: | |
| container_name: modernapi-postgres-pr-$PR_NUMBER | |
| environment: | |
| - POSTGRES_DB=modernapi_pr_$PR_NUMBER | |
| - POSTGRES_USER=modernapi_pr_$PR_NUMBER | |
| - POSTGRES_PASSWORD=preview_password_$PR_NUMBER | |
| volumes: | |
| - postgres-pr-$PR_NUMBER-data:/var/lib/postgresql/data | |
| networks: | |
| - modernapi-pr-$PR_NUMBER-network | |
| modernapi: | |
| container_name: modernapi-app-pr-$PR_NUMBER | |
| image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-preview.outputs.image_tag }} | |
| environment: | |
| - ASPNETCORE_ENVIRONMENT=Preview | |
| - DATABASE_URL=Host=postgres;Database=modernapi_pr_${PR_NUMBER};Username=modernapi_pr_${PR_NUMBER};Password=preview_password_${PR_NUMBER} | |
| - JWT_SECRET=PreviewJWTSecretForPR${PR_NUMBER}Testing123456789! | |
| - PREVIEW_MODE=true | |
| - PR_NUMBER=$PR_NUMBER | |
| labels: | |
| - "traefik.http.routers.modernapi-pr-$PR_NUMBER.rule=Host(\`pr-$PR_NUMBER.${{ env.PREVIEW_DOMAIN }}\`)" | |
| - "traefik.http.routers.modernapi-pr-$PR_NUMBER.tls=true" | |
| networks: | |
| - modernapi-pr-$PR_NUMBER-network | |
| volumes: | |
| postgres-pr-$PR_NUMBER-data: | |
| driver: local | |
| networks: | |
| modernapi-pr-$PR_NUMBER-network: | |
| driver: bridge | |
| name: modernapi-pr-$PR_NUMBER-network | |
| EOF | |
| - name: 🚀 Deploy preview environment | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| echo "🚀 Deploying preview environment for PR #$PR_NUMBER" | |
| # Deploy using Docker Compose with preview configuration | |
| docker-compose \ | |
| -f docker-compose.yml \ | |
| -f docker-compose.preview.yml \ | |
| --env-file .env.preview \ | |
| up -d | |
| echo "✅ Preview environment deployed" | |
| - name: ⏳ Wait for preview to be ready | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| PREVIEW_URL="https://pr-$PR_NUMBER.${{ env.PREVIEW_DOMAIN }}" | |
| echo "⏳ Waiting for preview environment to be ready..." | |
| # Wait for health endpoint with timeout | |
| for i in {1..30}; do | |
| if curl -f -k "$PREVIEW_URL/health" > /dev/null 2>&1; then | |
| echo "✅ Preview environment is healthy: $PREVIEW_URL" | |
| break | |
| fi | |
| echo "⏳ Waiting for preview... ($i/30)" | |
| sleep 10 | |
| done | |
| # Test basic functionality | |
| echo "🧪 Testing preview environment..." | |
| curl -f -k "$PREVIEW_URL/health" || exit 1 | |
| - name: 🗄️ Run database migrations | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| echo "🗄️ Running database migrations for preview..." | |
| # Run migrations in preview container | |
| docker exec "modernapi-app-pr-$PR_NUMBER" dotnet ef database update \ | |
| --connection "Host=postgres;Database=modernapi_pr_${PR_NUMBER};Username=modernapi_pr_${PR_NUMBER};Password=preview_password_${PR_NUMBER}" || true | |
| echo "✅ Database migrations completed" | |
| # ============================================================================= | |
| # Test Preview Environment | |
| # ============================================================================= | |
| test-preview: | |
| name: 🧪 Test Preview | |
| runs-on: ubuntu-latest | |
| needs: [check-preview, build-preview, deploy-preview] | |
| if: needs.check-preview.outputs.should_deploy == 'true' | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 📥 Checkout PR code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: 🧪 Run preview tests | |
| run: | | |
| PR_NUMBER="${{ needs.check-preview.outputs.pr_number }}" | |
| PREVIEW_URL="https://pr-$PR_NUMBER.${{ env.PREVIEW_DOMAIN }}" | |
| echo "🧪 Running comprehensive tests against preview environment..." | |
| # Health checks | |
| echo "🔍 Testing health endpoints..." | |
| curl -f -k "$PREVIEW_URL/health" | |
| curl -f -k "$PREVIEW_URL/health/ready" | |
| # API functionality tests | |
| echo "🧪 Testing API functionality..." | |
| # Test user registration | |
| REGISTER_RESPONSE=$(curl -s -w "%{http_code}" -k -o register_response.json -X POST \ | |
| "$PREVIEW_URL/api/auth/register" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "email": "preview-test@example.com", | |
| "password": "Preview123!", | |
| "confirmPassword": "Preview123!", | |
| "displayName": "Preview Test User" | |
| }') | |
| if [[ $REGISTER_RESPONSE -eq 200 ]]; then | |
| echo "✅ User registration test passed" | |
| else | |
| echo "⚠️ User registration test failed: $REGISTER_RESPONSE" | |
| cat register_response.json | |
| fi | |
| # Test admin login | |
| LOGIN_RESPONSE=$(curl -s -w "%{http_code}" -k -o login_response.json -X POST \ | |
| "$PREVIEW_URL/api/auth/login" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "email": "admin@modernapi.dev", | |
| "password": "Admin@123!" | |
| }') | |
| if [[ $LOGIN_RESPONSE -eq 200 ]]; then | |
| echo "✅ Admin login test passed" | |
| TOKEN=$(cat login_response.json | jq -r '.accessToken' 2>/dev/null || echo "") | |
| if [[ -n "$TOKEN" && "$TOKEN" != "null" ]]; then | |
| # Test protected endpoint | |
| PROTECTED_RESPONSE=$(curl -s -w "%{http_code}" -k -o protected_response.json \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| "$PREVIEW_URL/api/auth/me") | |
| if [[ $PROTECTED_RESPONSE -eq 200 ]]; then | |
| echo "✅ Protected endpoint test passed" | |
| else | |
| echo "⚠️ Protected endpoint test failed: $PROTECTED_RESPONSE" | |
| fi | |
| fi | |
| else | |
| echo "⚠️ Admin login test failed: $LOGIN_RESPONSE" | |
| cat login_response.json | |
| fi | |
| # Performance test | |
| echo "⚡ Testing response time..." | |
| RESPONSE_TIME=$(curl -w "%{time_total}" -k -o /dev/null -s "$PREVIEW_URL/health") | |
| echo "📊 Response time: ${RESPONSE_TIME}s" | |
| echo "✅ Preview testing completed" | |
| # ============================================================================= | |
| # Update PR with Preview Info | |
| # ============================================================================= | |
| comment-preview: | |
| name: 💬 Update PR Comment | |
| runs-on: ubuntu-latest | |
| needs: [check-preview, build-preview, deploy-preview, test-preview] | |
| if: | | |
| always() && | |
| needs.check-preview.outputs.should_deploy == 'true' && | |
| github.event_name == 'pull_request' | |
| steps: | |
| - name: 💬 Create or update PR comment | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = ${{ needs.check-preview.outputs.pr_number }}; | |
| const previewUrl = '${{ needs.check-preview.outputs.preview_url }}'; | |
| const imageTag = '${{ needs.build-preview.outputs.image_tag }}'; | |
| const deployResult = '${{ needs.deploy-preview.result }}'; | |
| const testResult = '${{ needs.test-preview.result }}'; | |
| let status = '🚀'; | |
| let statusText = 'Deployed successfully'; | |
| if (deployResult !== 'success') { | |
| status = '❌'; | |
| statusText = 'Deployment failed'; | |
| } else if (testResult !== 'success') { | |
| status = '⚠️'; | |
| statusText = 'Deployed with test failures'; | |
| } | |
| const commentBody = ` | |
| ## ${status} Preview Environment | |
| **Status:** ${statusText} | |
| ### 🔗 Preview Links | |
| - **API:** [${previewUrl}](${previewUrl}) | |
| - **Health:** [${previewUrl}/health](${previewUrl}/health) | |
| - **API Docs:** [${previewUrl}/scalar/v1](${previewUrl}/scalar/v1) | |
| - **Docker Image:** \`ghcr.io/${{ github.repository }}:${imageTag}\` | |
| ### 📊 Test Results | |
| - **Deployment:** ${deployResult === 'success' ? '✅ Passed' : '❌ Failed'} | |
| - **API Tests:** ${testResult === 'success' ? '✅ Passed' : testResult === 'failure' ? '❌ Failed' : '⏭️ Skipped'} | |
| - **Health Checks:** ${deployResult === 'success' ? '✅ Healthy' : '❌ Unhealthy'} | |
| ### 🛠️ Developer Info | |
| - **Branch:** \`${{ needs.check-preview.outputs.branch_name }}\` | |
| - **Commit:** \`${{ github.event.pull_request.head.sha }}\` | |
| - **Image Tag:** \`${imageTag}\` | |
| ### 🧪 Testing Instructions | |
| \`\`\`bash | |
| # Test the preview API | |
| curl -f ${previewUrl}/health | |
| # Test authentication | |
| curl -X POST ${previewUrl}/api/auth/login \\ | |
| -H "Content-Type: application/json" \\ | |
| -d '{"email": "admin@modernapi.dev", "password": "Admin@123!"}' | |
| \`\`\` | |
| ### 🔧 Commands | |
| - Comment \`/deploy-preview\` to redeploy | |
| - Comment \`/destroy-preview\` to destroy this environment | |
| <sub>🤖 This preview environment will be automatically destroyed when the PR is closed or after 7 days of inactivity.</sub> | |
| `; | |
| // Find existing preview comment | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const existingComment = comments.data.find(comment => | |
| comment.user.login === 'github-actions[bot]' && | |
| comment.body.includes('Preview Environment') | |
| ); | |
| if (existingComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existingComment.id, | |
| body: commentBody | |
| }); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: commentBody | |
| }); | |
| } | |
| # ============================================================================= | |
| # Cleanup on PR Close | |
| # ============================================================================= | |
| cleanup-on-close: | |
| name: 🧹 Cleanup Preview on PR Close | |
| runs-on: ubuntu-latest | |
| if: | | |
| github.event_name == 'pull_request' && | |
| github.event.action == 'closed' | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 🧹 Destroy preview environment | |
| run: | | |
| PR_NUMBER="${{ github.event.number }}" | |
| echo "🧹 Cleaning up preview environment for closed PR #$PR_NUMBER" | |
| # Stop and remove containers | |
| docker-compose \ | |
| -f docker-compose.yml \ | |
| -f docker-compose.preview.yml \ | |
| --env-file .env.preview \ | |
| down -v --remove-orphans || true | |
| # Remove preview-specific volumes | |
| docker volume rm "modernapi_postgres-pr-${PR_NUMBER}-data" || true | |
| # Remove preview network | |
| docker network rm "modernapi-pr-${PR_NUMBER}-network" || true | |
| echo "✅ Preview environment cleanup completed" | |
| - name: 🗑️ Delete preview images | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const prNumber = ${{ github.event.number }}; | |
| const packageName = '${{ env.IMAGE_NAME }}'; | |
| try { | |
| // Get all package versions | |
| const packages = await github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg({ | |
| package_type: 'container', | |
| package_name: packageName.split('/')[1], | |
| org: context.repo.owner, | |
| per_page: 100 | |
| }); | |
| // Find and delete PR-specific images | |
| const prImages = packages.data.filter(pkg => | |
| pkg.metadata?.container?.tags?.some(tag => | |
| tag.includes(`pr-${prNumber}`) | |
| ) | |
| ); | |
| for (const image of prImages) { | |
| console.log(`🗑️ Deleting preview image: ${image.name}`); | |
| await github.rest.packages.deletePackageVersionForOrg({ | |
| package_type: 'container', | |
| package_name: packageName.split('/')[1], | |
| org: context.repo.owner, | |
| package_version_id: image.id | |
| }); | |
| } | |
| console.log(`✅ Deleted ${prImages.length} preview images for PR #${prNumber}`); | |
| } catch (error) { | |
| console.log(`ℹ️ Image cleanup failed or not needed: ${error.message}`); | |
| } | |
| - name: 💬 Update PR with cleanup status | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: ${{ github.event.number }}, | |
| body: `## 🧹 Preview Environment Cleaned Up | |
| The preview environment for this PR has been automatically destroyed: | |
| - ✅ Containers stopped and removed | |
| - ✅ Volumes deleted | |
| - ✅ Docker images cleaned up | |
| - ✅ Network resources freed | |
| Thank you for using the preview environment! 🚀` | |
| }); | |
| # ============================================================================= | |
| # Scheduled Cleanup of Old Previews | |
| # ============================================================================= | |
| scheduled-cleanup: | |
| name: 🧹 Scheduled Preview Cleanup | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| timeout-minutes: 15 | |
| steps: | |
| - name: 🧹 Clean up stale preview environments | |
| run: | | |
| echo "🧹 Cleaning up stale preview environments..." | |
| # Find containers older than 7 days with preview labels | |
| OLD_CONTAINERS=$(docker ps -a --filter "label=org.opencontainers.image.pr.number" --format "table {{.Names}}\t{{.CreatedAt}}" | \ | |
| awk 'NR>1 && $2 ~ /[0-9]+ (day|week)s? ago/ && $2 !~ /^[0-6] day/' | cut -f1) | |
| if [[ -n "$OLD_CONTAINERS" ]]; then | |
| echo "🗑️ Removing stale preview containers:" | |
| echo "$OLD_CONTAINERS" | |
| echo "$OLD_CONTAINERS" | xargs -r docker rm -f | |
| # Clean up associated volumes | |
| docker volume prune -f --filter "label=preview=true" | |
| else | |
| echo "ℹ️ No stale preview containers found" | |
| fi | |
| - name: 🗑️ Clean up old preview images | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const packageName = '${{ env.IMAGE_NAME }}'; | |
| const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); | |
| try { | |
| const packages = await github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg({ | |
| package_type: 'container', | |
| package_name: packageName.split('/')[1], | |
| org: context.repo.owner, | |
| per_page: 100 | |
| }); | |
| // Find old preview images | |
| const oldPreviewImages = packages.data.filter(pkg => | |
| pkg.metadata?.container?.tags?.some(tag => tag.includes('pr-')) && | |
| new Date(pkg.created_at) < sevenDaysAgo | |
| ); | |
| for (const image of oldPreviewImages) { | |
| console.log(`🗑️ Deleting old preview image: ${image.name} (${image.created_at})`); | |
| await github.rest.packages.deletePackageVersionForOrg({ | |
| package_type: 'container', | |
| package_name: packageName.split('/')[1], | |
| org: context.repo.owner, | |
| package_version_id: image.id | |
| }); | |
| } | |
| console.log(`✅ Cleaned up ${oldPreviewImages.length} old preview images`); | |
| } catch (error) { | |
| console.log(`ℹ️ Scheduled cleanup failed: ${error.message}`); | |
| } |