diff --git a/.github/workflows/review-app-comment-trigger.yml b/.github/workflows/review-app-comment-trigger.yml new file mode 100644 index 000000000..0d724a636 --- /dev/null +++ b/.github/workflows/review-app-comment-trigger.yml @@ -0,0 +1,147 @@ +name: Trigger Review App Deployment from Comments + +on: + issue_comment: + types: [created] + +jobs: + process_comment: + runs-on: ubuntu-latest + if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy') }} + + steps: + - name: Checkout code to access workflow file + uses: actions/checkout@v4 + + - name: Extract available sites + id: available_sites + uses: mikefarah/yq@de2f77b49cbd40fd67031ee602245d0acc4ac482 + with: + cmd: yq '.on.workflow_dispatch.inputs.site.options[]' '.github/workflows/review-app.yml' + + - name: Format available sites + id: format_sites + run: | + # Format the sites as a comma-separated list, excluding ecospheres + AVAILABLE_SITES=$(echo "${{ steps.available_sites.outputs.result }}" | grep -v "ecospheres" | tr '\n' ',' | sed 's/,$//') + echo "AVAILABLE_SITES=$AVAILABLE_SITES" >> $GITHUB_ENV + echo "available_sites=$AVAILABLE_SITES" >> $GITHUB_OUTPUT + echo "Available sites: $AVAILABLE_SITES" + + - name: Get PR details + id: pr_details + uses: actions/github-script@v6 + with: + script: | + const prNumber = context.issue.number; + core.setOutput('pr_number', prNumber); + + // Get PR information to verify it's from the same repository + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const isSameRepo = pullRequest.head.repo.full_name === context.repo.full_name; + core.setOutput('is_same_repo', isSameRepo.toString()); + + return { prNumber, isSameRepo }; + + - name: Parse deployment command + id: parse_command + if: ${{ steps.pr_details.outputs.is_same_repo == 'true' }} + run: | + COMMENT="${{ github.event.comment.body }}" + AVAILABLE_SITES="${{ env.AVAILABLE_SITES }}" + + # Extract sites from command + if [[ $COMMENT =~ /deploy\ +([a-zA-Z0-9,-]+) ]]; then + SITES="${BASH_REMATCH[1]}" + + # Remove spaces if any + SITES=$(echo $SITES | tr -d '[:space:]') + + # If "all" is specified, use all available sites (excluding ecospheres) + if [[ "$SITES" == "all" ]]; then + SITES="$AVAILABLE_SITES" + fi + + echo "SITES=$SITES" >> $GITHUB_ENV + echo "sites=$SITES" >> $GITHUB_OUTPUT + echo "has_valid_sites=true" >> $GITHUB_OUTPUT + else + # No site specified, fail + echo "No site specified in the deployment command" >> $GITHUB_STEP_SUMMARY + echo "has_valid_sites=false" >> $GITHUB_OUTPUT + fi + + - name: Add reaction to comment + if: ${{ steps.pr_details.outputs.is_same_repo == 'true' && steps.parse_command.outputs.has_valid_sites == 'true' }} + uses: actions/github-script@v6 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + - name: Trigger deployments + if: ${{ steps.pr_details.outputs.is_same_repo == 'true' && steps.parse_command.outputs.has_valid_sites == 'true' }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const sites = '${{ steps.parse_command.outputs.sites }}'.split(','); + const prNumber = '${{ steps.pr_details.outputs.pr_number }}'; + const availableSites = '${{ steps.format_sites.outputs.available_sites }}'.split(',').filter(Boolean); + + // Create a comment to inform about the deployments + let deploymentMessage = `šŸ“¦ Triggering deployment for site(s): **${sites.join(', ')}**\n\n`; + let validSitesFound = false; + + for (const site of sites) { + // Skip ecospheres as it's deployed directly by PR events + if (site === 'ecospheres') { + deploymentMessage += `ā„¹ļø Note: Site 'ecospheres' is deployed automatically by PR events and will be skipped.\n`; + continue; + } + + // Validate against available sites + if (!availableSites.includes(site)) { + deploymentMessage += `āš ļø Warning: Site '${site}' is not recognized and will be skipped.\n`; + continue; + } + + validSitesFound = true; + deploymentMessage += `šŸš€ Starting deployment for **${site}**...\n`; + + try { + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'review-app.yml', + ref: 'main', + inputs: { + site: site, + pr_number: prNumber + } + }); + } catch (error) { + deploymentMessage += `āŒ Failed to trigger deployment for ${site}: ${error.message}\n`; + continue; + } + } + + if (!validSitesFound) { + deploymentMessage += `\nāŒ No valid sites were found to deploy. Please check your command and try again.`; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: deploymentMessage + }); diff --git a/.github/workflows/review-app.yml b/.github/workflows/review-app.yml index 572d70eb1..3331bd6cf 100644 --- a/.github/workflows/review-app.yml +++ b/.github/workflows/review-app.yml @@ -5,56 +5,80 @@ name: Deploy review app on: pull_request: types: [opened, synchronize, reopened, closed] + workflow_dispatch: + inputs: + site: + description: 'Site to deploy (ecospheres, meteo-france, or logistique)' + required: true + default: 'ecospheres' + type: choice + options: + - ecospheres + - meteo-france + - logistique + - hackathon + - defis + pr_number: + description: 'PR number (required for manual deployments)' + required: true + type: string jobs: handle_review_app: runs-on: ubuntu-latest concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.site }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.site || 'ecospheres' }} cancel-in-progress: false - strategy: - matrix: - site: - - ecospheres - - meteo-france - - logistique permissions: deployments: write - # only run if the PR is from the same repo - if: github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request' + if: | + (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || + github.event_name == 'workflow_dispatch' steps: + - name: Set environment variables + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "SITE_ID=${{ github.event.pull_request.head.ref == 'main' && 'ecospheres' || 'ecospheres' }}" >> $GITHUB_ENV + echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV + else + echo "SITE_ID=${{ inputs.site }}" >> $GITHUB_ENV + echo "PR_NUMBER=${{ inputs.pr_number }}" >> $GITHUB_ENV + fi + - name: Debug event run: | echo "Event name: ${{ github.event_name }}" echo "Event action: ${{ github.event.action }}" - echo "Pull request state: ${{ github.event.pull_request.state }}" + echo "Pull request state: ${{ github.event.pull_request.state || 'N/A' }}" + echo "Site ID: ${{ env.SITE_ID }}" + echo "PR number: ${{ env.PR_NUMBER }}" - name: Cloning repo - if: github.event.action != 'closed' + if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed' uses: actions/checkout@v4 with: fetch-depth: 0 - name: Start deployment - if: github.event.action != 'closed' + if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed' uses: chrnorm/deployment-action@v2 id: deployment with: token: ${{ github.token }} - environment: ${{ matrix.site }}-preview + environment: ${{ env.SITE_ID }}-preview initial-status: in_progress transient-environment: true - name: Create the review app - if: github.event.action == 'opened' || github.event.action == 'reopened' + if: github.event_name == 'workflow_dispatch' || github.event.action == 'opened' || github.event.action == 'reopened' uses: dokku/github-action@master + # ignore errors as the app might already exist on workflow_dispatch + continue-on-error: true with: command: review-apps:create git_remote_url: ${{ secrets.REVIEW_APP_SSH_URL }} - review_app_name: deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }} + review_app_name: deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }} ssh_private_key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }} - # omitting this will prevent the app from being built (which is what we want) - # branch: 'main' - name: Set site id as build arg if: always() @@ -65,20 +89,20 @@ jobs: key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }} port: 22 script: | - docker-options:add deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }} build "--build-arg VITE_SITE_ID=${{ matrix.site }}" + docker-options:add deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }} build "--build-arg VITE_SITE_ID=${{ env.SITE_ID }}" - name: Push to dokku - if: github.event.action != 'closed' + if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed' uses: dokku/github-action@master with: git_remote_url: ${{ secrets.REVIEW_APP_SSH_URL }} - review_app_name: deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }} + review_app_name: deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }} ssh_private_key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }} git_push_flags: '--force' branch: 'main' - name: Enable SSL with Let's Encrypt - if: github.event.action == 'opened' || github.event.action == 'reopened' + if: github.event_name == 'workflow_dispatch' || github.event.action == 'opened' || github.event.action == 'reopened' uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.REVIEW_APP_SSH_HOST }} @@ -86,22 +110,30 @@ jobs: key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }} port: 22 script: | - letsencrypt:enable deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }} + letsencrypt:enable deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }} - - name: Destroy the review app - if: github.event.action == 'closed' - uses: dokku/github-action@master + - name: Extract available sites + uses: mikefarah/yq@de2f77b49cbd40fd67031ee602245d0acc4ac482 + id: get-sites with: - command: review-apps:destroy - git_remote_url: ${{ secrets.REVIEW_APP_SSH_URL }} - review_app_name: deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }} - ssh_private_key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }} + cmd: yq '.on.workflow_dispatch.inputs.site.options[]' '.github/workflows/review-app.yml' + + - name: Destroy all review apps + if: github.event.action == 'closed' + run: | + echo "${{ steps.get-sites.outputs.result }}" | while read site; do + echo "Destroying review app for site: $site" + ssh -i <(echo "${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}") \ + -o StrictHostKeyChecking=no \ + dokku@${{ secrets.REVIEW_APP_SSH_HOST }} \ + apps:destroy deploy-preview-${{ env.PR_NUMBER }}--$site || true + done - name: Update deployment status - if: github.event.action != 'closed' + if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed' uses: chrnorm/deployment-status@v2 with: token: ${{ github.token }} - environment-url: https://deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }}.sandbox.data.developpement-durable.gouv.fr + environment-url: https://deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }}.sandbox.data.developpement-durable.gouv.fr state: ${{ job.status == 'success' && 'success' || 'failure' }} deployment-id: ${{ steps.deployment.outputs.deployment_id }}