name: Auto PR V2 Deployment on: pull_request: types: [opened, synchronize, reopened] permissions: contents: read issues: write pull-requests: write jobs: check-pr: runs-on: ubuntu-latest outputs: should_deploy: ${{ steps.check-conditions.outputs.should_deploy }} pr_number: ${{ github.event.number }} pr_repository: ${{ steps.get-pr-info.outputs.repository }} pr_ref: ${{ steps.get-pr-info.outputs.ref }} steps: - name: Harden Runner uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 with: egress-policy: audit - name: Check deployment conditions id: check-conditions env: PR_TITLE: ${{ github.event.pull_request.title }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_BRANCH: ${{ github.event.pull_request.head.ref }} run: | echo "PR Title: $PR_TITLE" echo "PR Author: $PR_AUTHOR" echo "PR Branch: $PR_BRANCH" # Check if author is authorized authorized_users=( "frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" ) is_authorized=false for user in "${authorized_users[@]}"; do if [[ "$PR_AUTHOR" == "$user" ]]; then is_authorized=true break fi done # Check if title contains V2/version2 keywords (case insensitive) has_v2_keyword=false if [[ "$PR_TITLE" =~ [Vv]2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?[Tt]wo ]]; then has_v2_keyword=true fi # Check if branch name contains V2 or react keywords (case insensitive) has_branch_keyword=false if [[ "$PR_BRANCH" =~ [Vv]2 ]] || [[ "$PR_BRANCH" =~ [Rr]eact ]]; then has_branch_keyword=true fi if [[ "$is_authorized" == "true" ]] && [[ "$has_v2_keyword" == "true" || "$has_branch_keyword" == "true" ]]; then echo "✅ Deployment conditions met" echo "should_deploy=true" >> $GITHUB_OUTPUT else echo "❌ Deployment conditions not met" echo " - Authorized user: $is_authorized" echo " - Has V2 keyword in title: $has_v2_keyword" echo " - Has V2/React keyword in branch: $has_branch_keyword" echo "should_deploy=false" >> $GITHUB_OUTPUT fi - name: Get PR repository and ref id: get-pr-info if: steps.check-conditions.outputs.should_deploy == 'true' run: | # For forks, use the full repository name, for internal PRs use the current repo if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then repository="${{ github.event.pull_request.head.repo.full_name }}" else repository="${{ github.repository }}" fi echo "repository=$repository" >> $GITHUB_OUTPUT echo "ref=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT deploy-v2-pr: needs: check-pr runs-on: ubuntu-latest if: needs.check-pr.outputs.should_deploy == 'true' permissions: contents: read issues: write pull-requests: write steps: - name: Harden Runner uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 with: egress-policy: audit - name: Generate GitHub App Token id: generate-token uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Add deployment started reaction uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.generate-token.outputs.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._` }); - name: Checkout PR uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ needs.check-pr.outputs.pr_repository }} ref: ${{ needs.check-pr.outputs.pr_ref }} token: ${{ secrets.GITHUB_TOKEN }} - name: Set up JDK uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: java-version: "17" distribution: "temurin" - name: Build backend run: | export DISABLE_ADDITIONAL_FEATURES=true ./gradlew clean build env: STIRLING_PDF_DESKTOP_UI: false - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Build frontend run: | cd frontend npm ci npm run build - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Get version number id: versionNumber run: | VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - name: Login to Docker Hub uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Build and push V2 monolith image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . file: ./docker/monolith/Dockerfile push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-pr-${{ needs.check-pr.outputs.pr_number }} build-args: VERSION_TAG=v2-alpha platforms: linux/amd64 - name: Set up SSH run: | mkdir -p ~/.ssh/ echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key sudo chmod 600 ../private.key - name: Deploy V2 to VPS id: deploy run: | # Use same port strategy as regular PRs - just the PR number V2_PORT=${{ needs.check-pr.outputs.pr_number }} # Create docker-compose for V2 monolith cat > docker-compose.yml << EOF version: '3.3' services: stirling-pdf-v2: container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }} image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-pr-${{ needs.check-pr.outputs.pr_number }} ports: - "${V2_PORT}:80" # Frontend port (same as regular PRs) volumes: - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw environment: DISABLE_ADDITIONAL_FEATURES: "true" SECURITY_ENABLELOGIN: "false" SYSTEM_DEFAULTLOCALE: en-GB UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}" UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture" UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}" SYSTEM_MAXFILESIZE: "100" METRICS_ENABLED: "true" SYSTEM_GOOGLEVISIBILITY: "false" restart: on-failure:5 EOF # Deploy to VPS scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose-v2.yml ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH # Create V2 PR-specific directories mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs} # Move docker-compose file to correct location mv /tmp/docker-compose-v2.yml /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/docker-compose.yml # Start or restart the container cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }} docker-compose pull docker-compose up -d ENDSSH # Set port for output echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT - name: Post V2 deployment URL to PR if: success() uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.generate-token.outputs.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; const v2Port = ${{ steps.deploy.outputs.v2_port }}; const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`; const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` + `Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` + `🔗 **V2 Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` + `### 🏗️ Architecture:\n` + `- **Frontend:** React + Vite + Nginx\n` + `- **Backend:** Java Spring Boot\n` + `- **Mode:** Monolith (both services in one container)\n` + `- **API Access:** Frontend automatically proxies /api/* calls to backend\n\n` + `### 🔧 Testing:\n` + `- Access the React UI at the URL above\n` + `- All API calls are handled internally between frontend and backend\n\n` + `_This deployment will be automatically cleaned up when the PR is closed._\n\n` + `🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`; await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: commentBody }); - name: Add success reaction if: success() uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.generate-token.outputs.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; // Get the PR to find the latest comment const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 1, sort: 'created', direction: 'desc' }); if (comments.length > 0) { await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comments[0].id, content: 'rocket' }); } - name: Add failure reaction to comment if: failure() uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.generate-token.outputs.token }} script: | // Get the PR to find the latest comment and add failure reaction const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 1, sort: 'created', direction: 'desc' }); if (comments.length > 0) { console.log(`Adding -1 reaction to comment ID: ${comments[0].id}`); try { const { data: reaction } = await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comments[0].id, content: '-1' }); console.log(`Added -1 reaction with ID: ${reaction.id}`); } catch (error) { console.error(`Failed to add reaction: ${error.message}`); console.error(error); } }