diff --git a/.github/workflows/review-cleanup.yml b/.github/workflows/review-cleanup.yml new file mode 100644 index 000000000..eb9173b0e --- /dev/null +++ b/.github/workflows/review-cleanup.yml @@ -0,0 +1,172 @@ +name: Cleanup review apps + +on: + schedule: + - cron: "*/30 * * * *" # Every 30 minutes + workflow_dispatch: # Allow manual trigger + +jobs: + cleanup-expired: + name: Delete expired review apps (>3h) + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Install clever-tools + run: npm install -g clever-tools + + - name: Find and cleanup expired apps + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + ORGA_ID: ${{ secrets.ORGA_ID }} + run: | + REPO="${{ github.repository }}" + REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) + CLEAN_NAME=$(echo "$REPO_NAME" | tr '_' '-') + NOW=$(date -u +%s) + TTL=10800 # 3 hours in seconds + + echo "=== Checking for expired review apps ===" + echo "Repository: $REPO" + echo "Current time: $NOW" + + # List all open PRs with the deploy-review label + PR_NUMBERS=$(gh pr list --repo "$REPO" --label "deploy-review" --state open --json number --jq '.[].number' 2>/dev/null || echo "") + + if [ -z "$PR_NUMBERS" ]; then + echo "No open PRs with deploy-review label found." + exit 0 + fi + + for PR_NUM in $PR_NUMBERS; do + echo "" + echo "--- Checking PR #$PR_NUM ---" + + # Find the deployment comment with the timestamp marker + COMMENT_DATA=$(gh api "repos/$REPO/issues/$PR_NUM/comments" \ + --jq '[.[] | select(.body | contains(""))] | last | {id: .id, body: .body}' 2>/dev/null || echo "") + + if [ -z "$COMMENT_DATA" ] || [ "$COMMENT_DATA" == "null" ]; then + echo " No deployment comment found, skipping." + continue + fi + + COMMENT_ID=$(echo "$COMMENT_DATA" | jq -r '.id') + COMMENT_BODY=$(echo "$COMMENT_DATA" | jq -r '.body') + + # Extract timestamp from hidden HTML comment + DEPLOY_TS=$(echo "$COMMENT_BODY" | grep -oP '(?<= + ### Review app expired + + The review app was automatically deleted after 3 hours. + _Add the \`deploy-review\` label again to redeploy._" || true + + # Remove the deploy-review label + gh pr edit "$PR_NUM" --repo "$REPO" --remove-label "deploy-review" 2>/dev/null || true + + echo " Cleanup complete for PR #$PR_NUM." + else + REMAINING=$(( (TTL - AGE) / 60 )) + echo " Still active. ~${REMAINING} minutes remaining." + fi + done + + echo "" + echo "=== Expired app cleanup sweep complete ===" + + cleanup-orphans: + name: Delete orphaned review apps + runs-on: ubuntu-latest + steps: + - name: Install clever-tools + run: npm install -g clever-tools + + - name: Find and delete orphaned apps + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + ORGA_ID: ${{ secrets.ORGA_ID }} + run: | + REPO="${{ github.repository }}" + REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) + CLEAN_NAME=$(echo "$REPO_NAME" | tr '_' '-') + # Pattern to match review apps: {clean-repo-name}-PR-{number} + REVIEW_APP_PREFIX="${CLEAN_NAME}-PR-" + + echo "=== Checking for orphaned review apps ===" + echo "Looking for apps matching prefix: ${REVIEW_APP_PREFIX}" + + # List all apps in the org via Clever Cloud API + APPS_JSON=$(clever curl -X GET "/v2/organisations/$ORGA_ID/applications" 2>/dev/null || echo "[]") + + # Extract app names and IDs matching the review app pattern + REVIEW_APPS=$(echo "$APPS_JSON" | jq -r --arg prefix "$REVIEW_APP_PREFIX" \ + '.[] | select(.name | startswith($prefix)) | "\(.name) \(.id)"' 2>/dev/null || echo "") + + if [ -z "$REVIEW_APPS" ]; then + echo "No review apps found on Clever Cloud." + exit 0 + fi + + echo "$REVIEW_APPS" | while IFS=' ' read -r APP_NAME APP_ID; do + [ -z "$APP_NAME" ] && continue + + echo "" + echo "--- Checking app: $APP_NAME (ID: $APP_ID) ---" + + # Extract PR number from app name + PR_NUM=$(echo "$APP_NAME" | grep -oP '(?<=-PR-)\d+$' || echo "") + + if [ -z "$PR_NUM" ]; then + echo " Could not extract PR number, skipping." + continue + fi + + # Check if the PR is still open + PR_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null || echo "NOT_FOUND") + + echo " PR #$PR_NUM state: $PR_STATE" + + if [ "$PR_STATE" != "OPEN" ]; then + echo " ORPHANED (PR is $PR_STATE). Deleting app..." + + # Delete via Clever Cloud API (more reliable without git context) + clever curl -X DELETE "/v2/organisations/$ORGA_ID/applications/$APP_ID" 2>/dev/null || true + + echo " App $APP_NAME deleted." + else + echo " PR is open, keeping app." + fi + done + + echo "" + echo "=== Orphan cleanup sweep complete ===" diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index cafdc9791..1e60c3952 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -1,101 +1,271 @@ +name: Review App + on: - pull_request_target: - types: [opened, closed, synchronize, reopened] - branches: [ main ] - paths: - - 'front/**' - -jobs: - deploy: - name: Deploy/redeploy review app - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - contents: read - steps: - # Vérifier si la branche existe avant le checkout - - name: Check if branch exists - if: ${{ github.event.action != 'closed' }} - id: check-branch - run: | - REPO="${{ github.event.pull_request.head.repo.full_name }}" - BRANCH="${{ github.event.pull_request.head.ref }}" - - # Tester si la branche existe via l'API GitHub - if gh api repos/$REPO/branches/$BRANCH --silent 2>/dev/null; then - echo "branch-exists=true" >> $GITHUB_OUTPUT - echo "✅ Branch $BRANCH exists" - else - echo "branch-exists=false" >> $GITHUB_OUTPUT - echo "❌ Branch $BRANCH not found" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Checkout seulement si la branche existe - - name: Checkout repository - if: ${{ steps.check-branch.outputs.branch-exists == 'true' }} - uses: actions/checkout@v4 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - # Generate clean app name without underscores - - name: Generate clean app name - if: ${{ steps.check-branch.outputs.branch-exists == 'true' }} - id: app-name - run: | - REPO_NAME="${{ github.event.pull_request.base.repo.name }}" - CLEAN_NAME=$(echo "$REPO_NAME" | tr '_' '-') - APP_NAME="${CLEAN_NAME}-PR-${{ github.event.number }}" - echo "app-name=$APP_NAME" >> $GITHUB_OUTPUT - echo "Generated app name: $APP_NAME" - - - name: Create review app - if: ${{ github.event.action != 'closed' && steps.check-branch.outputs.branch-exists == 'true' }} - uses: CleverCloud/clever-cloud-review-app@v2.0.2 - env: - CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} - CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} - ORGA_ID: ${{ secrets.ORGA_ID }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_APP_FOLDER: ${{ secrets.APP_FOLDER }} - GH_CC_CACHE_DEPENDENCIES: ${{secrets.CC_CACHE_DEPENDENCIES}} - GH_CC_CGI_IMPLEMENTATION: ${{secrets.CC_CGI_IMPLEMENTATION}} - GH_CC_NODE_DEV_DEPENDENCIES: ${{secrets.CC_NODE_DEV_DEPENDENCIES}} - GH_CC_WEBROOT: ${{secrets.CC_WEBROOT}} - GH_HOST: ${{secrets.HOST}} - GH_NODE_ENV : ${{secrets.NODE_ENV}} - GH_PORT: ${{secrets.PORT}} - GH_NEXT_PUBLIC_BASE_URL: https://${{ steps.app-name.outputs.app-name }}.cleverapps.io - GH_APP_NAME: ${{ steps.app-name.outputs.app-name }} - GH_POSTGRESQL_ADDON_HOST: ${{secrets.POSTGRESQL_ADDON_HOST}} - GH_POSTGRESQL_ADDON_DB: ${{secrets.POSTGRESQL_ADDON_DB}} - GH_POSTGRESQL_ADDON_USER: ${{secrets.POSTGRESQL_ADDON_USER}} - GH_POSTGRESQL_ADDON_PORT: ${{secrets.POSTGRESQL_ADDON_PORT}} - GH_POSTGRESQL_ADDON_PASSWORD: ${{secrets.POSTGRESQL_ADDON_PASSWORD}} - GH_POSTGRESQL_ADDON_URI: ${{secrets.POSTGRESQL_ADDON_URI}} - # # Optimisations pour éviter SIGKILL sur machine XS avec Yarn - # GH_CC_NODE_BUILD_TOOL: "yarn" - # GH_NODE_OPTIONS: "--max-old-space-size=1024" - # GH_CC_PRE_BUILD_HOOK: "yarn cache clean" - - with: - type: node - set-env: true - flavor: XS - build-flavor: S - - - name: Clean up review app on PR close - if: ${{ github.event.action == 'closed' }} - uses: CleverCloud/clever-cloud-review-app@v2.0.2 - env: - CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} - CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} - ORGA_ID: ${{ secrets.ORGA_ID }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - type: node + pull_request_target: + types: [labeled, unlabeled, synchronize, closed, reopened] + branches: [main] + +jobs: + deploy: + name: Deploy review app + runs-on: ubuntu-latest + if: >- + github.event.action != 'closed' && + github.event.action != 'unlabeled' && + ( + (github.event.action == 'labeled' && github.event.label.name == 'deploy-review') || + (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'deploy-review')) + ) + permissions: + issues: write + pull-requests: write + contents: read + steps: + - name: Check if branch exists + id: check-branch + run: | + REPO="${{ github.event.pull_request.head.repo.full_name }}" + BRANCH="${{ github.event.pull_request.head.ref }}" + if gh api repos/$REPO/branches/$BRANCH --silent 2>/dev/null; then + echo "branch-exists=true" >> $GITHUB_OUTPUT + echo "Branch $BRANCH exists" + else + echo "branch-exists=false" >> $GITHUB_OUTPUT + echo "Branch $BRANCH not found" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout repository + if: steps.check-branch.outputs.branch-exists == 'true' + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Generate clean app name + if: steps.check-branch.outputs.branch-exists == 'true' + id: app-name + run: | + REPO_NAME="${{ github.event.pull_request.base.repo.name }}" + CLEAN_NAME=$(echo "$REPO_NAME" | tr '_' '-') + APP_NAME="${CLEAN_NAME}-PR-${{ github.event.number }}" + echo "app-name=$APP_NAME" >> $GITHUB_OUTPUT + echo "Generated app name: $APP_NAME" + + - name: Install clever-tools + if: steps.check-branch.outputs.branch-exists == 'true' + run: npm install -g clever-tools + + - name: Check if app already exists + if: steps.check-branch.outputs.branch-exists == 'true' + id: app-exists + run: | + APP_NAME="${{ steps.app-name.outputs.app-name }}" + if clever link -o "${{ secrets.ORGA_ID }}" "$APP_NAME" 2>/dev/null; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "App $APP_NAME already exists" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "App $APP_NAME does not exist yet" + fi + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Create app on Clever Cloud + if: >- + steps.check-branch.outputs.branch-exists == 'true' && + steps.app-exists.outputs.exists != 'true' + run: | + APP_NAME="${{ steps.app-name.outputs.app-name }}" + clever create --type node "$APP_NAME" --alias "$APP_NAME" --region par --org "${{ secrets.ORGA_ID }}" + clever scale --alias "$APP_NAME" --flavor XS + clever scale --alias "$APP_NAME" --build-flavor S + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Set environment variables + if: >- + steps.check-branch.outputs.branch-exists == 'true' && + steps.app-exists.outputs.exists != 'true' + run: | + APP_NAME="${{ steps.app-name.outputs.app-name }}" + clever env set --alias "$APP_NAME" APP_FOLDER "${{ secrets.APP_FOLDER }}" + clever env set --alias "$APP_NAME" CC_CACHE_DEPENDENCIES "${{ secrets.CC_CACHE_DEPENDENCIES }}" + clever env set --alias "$APP_NAME" CC_CGI_IMPLEMENTATION "${{ secrets.CC_CGI_IMPLEMENTATION }}" + clever env set --alias "$APP_NAME" CC_NODE_DEV_DEPENDENCIES "${{ secrets.CC_NODE_DEV_DEPENDENCIES }}" + clever env set --alias "$APP_NAME" CC_WEBROOT "${{ secrets.CC_WEBROOT }}" + clever env set --alias "$APP_NAME" HOST "${{ secrets.HOST }}" + clever env set --alias "$APP_NAME" NODE_ENV "${{ secrets.NODE_ENV }}" + clever env set --alias "$APP_NAME" PORT "${{ secrets.PORT }}" + clever env set --alias "$APP_NAME" NEXT_PUBLIC_BASE_URL "https://${{ steps.app-name.outputs.app-name }}.cleverapps.io" + clever env set --alias "$APP_NAME" POSTGRESQL_ADDON_HOST "${{ secrets.POSTGRESQL_ADDON_HOST }}" + clever env set --alias "$APP_NAME" POSTGRESQL_ADDON_DB "${{ secrets.POSTGRESQL_ADDON_DB }}" + clever env set --alias "$APP_NAME" POSTGRESQL_ADDON_USER "${{ secrets.POSTGRESQL_ADDON_USER }}" + clever env set --alias "$APP_NAME" POSTGRESQL_ADDON_PORT "${{ secrets.POSTGRESQL_ADDON_PORT }}" + clever env set --alias "$APP_NAME" POSTGRESQL_ADDON_PASSWORD "${{ secrets.POSTGRESQL_ADDON_PASSWORD }}" + clever env set --alias "$APP_NAME" POSTGRESQL_ADDON_URI "${{ secrets.POSTGRESQL_ADDON_URI }}" + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Link to existing app + if: >- + steps.check-branch.outputs.branch-exists == 'true' && + steps.app-exists.outputs.exists == 'true' + run: clever link -o "${{ secrets.ORGA_ID }}" "${{ steps.app-name.outputs.app-name }}" + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Deploy + if: steps.check-branch.outputs.branch-exists == 'true' + id: deploy + run: | + APP_NAME="${{ steps.app-name.outputs.app-name }}" + if [ "${{ steps.app-exists.outputs.exists }}" == "true" ]; then + clever deploy --alias "$APP_NAME" --force + else + clever deploy --alias "$APP_NAME" + fi + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Generate deployment timestamp + if: steps.deploy.outcome == 'success' + id: timestamp + run: echo "ts=$(date -u +%s)" >> $GITHUB_OUTPUT + + - name: Find existing deployment comment + if: steps.deploy.outcome == 'success' + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.number }} + body-includes: "" + + - name: Post deployment comment + if: steps.deploy.outcome == 'success' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.number }} + body: | + + + ### Review app deployed + + | Info | Details | + | ---- | ------- | + | Commit | `${{ github.event.pull_request.head.sha }}` | + | Preview | https://${{ steps.app-name.outputs.app-name }}.cleverapps.io | + | Auto-expires | ~3 hours after deployment | + + _The app will be automatically deleted after 3 hours. Remove and re-add the `deploy-review` label to deploy again._ + edit-mode: replace + + cleanup-unlabeled: + name: Delete review app (label removed) + runs-on: ubuntu-latest + if: >- + github.event.action == 'unlabeled' && + github.event.label.name == 'deploy-review' + permissions: + issues: write + pull-requests: write + steps: + - name: Generate clean app name + id: app-name + run: | + REPO_NAME="${{ github.event.pull_request.base.repo.name }}" + CLEAN_NAME=$(echo "$REPO_NAME" | tr '_' '-') + APP_NAME="${CLEAN_NAME}-PR-${{ github.event.number }}" + echo "app-name=$APP_NAME" >> $GITHUB_OUTPUT + + - name: Install clever-tools + run: npm install -g clever-tools + + - name: Delete review app + continue-on-error: true + run: | + APP_NAME="${{ steps.app-name.outputs.app-name }}" + clever link -o "${{ secrets.ORGA_ID }}" "$APP_NAME" + clever delete --alias "$APP_NAME" --yes + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Find existing deployment comment + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.number }} + body-includes: "" + + - name: Update comment + if: steps.fc.outputs.comment-id != '' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.number }} + body: | + + ### Review app deleted + + The `deploy-review` label was removed. The review app has been deleted. + _Add the `deploy-review` label again to redeploy._ + edit-mode: replace + + cleanup-closed: + name: Delete review app (PR closed) + runs-on: ubuntu-latest + if: github.event.action == 'closed' + permissions: + issues: write + pull-requests: write + steps: + - name: Generate clean app name + id: app-name + run: | + REPO_NAME="${{ github.event.pull_request.base.repo.name }}" + CLEAN_NAME=$(echo "$REPO_NAME" | tr '_' '-') + APP_NAME="${CLEAN_NAME}-PR-${{ github.event.number }}" + echo "app-name=$APP_NAME" >> $GITHUB_OUTPUT + + - name: Install clever-tools + run: npm install -g clever-tools + + - name: Delete review app + continue-on-error: true + run: | + APP_NAME="${{ steps.app-name.outputs.app-name }}" + clever link -o "${{ secrets.ORGA_ID }}" "$APP_NAME" + clever delete --alias "$APP_NAME" --yes + env: + CLEVER_TOKEN: ${{ secrets.CLEVER_TOKEN }} + CLEVER_SECRET: ${{ secrets.CLEVER_SECRET }} + + - name: Find existing deployment comment + uses: peter-evans/find-comment@v3 + id: fc + with: + issue-number: ${{ github.event.number }} + body-includes: "" + + - name: Update comment + if: steps.fc.outputs.comment-id != '' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.number }} + body: | + + ### Review app deleted + + The PR was closed. The review app has been deleted. + edit-mode: replace