build(deps): bump step-security/harden-runner from 2.20.0 to 2.21.0 #15368
Workflow file for this run
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: Auto PR V2 Deployment | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, closed] | |
| workflow_dispatch: | |
| inputs: | |
| pr: | |
| description: "PR number to deploy" | |
| required: true | |
| allow_fork: | |
| description: "Allow deploying fork PR?" | |
| required: false | |
| type: choice | |
| options: | |
| - "true" | |
| - "false" | |
| default: "false" | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| check-pr: | |
| if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| # Only reads the PR via pulls.get with the default GITHUB_TOKEN. | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| should_deploy: ${{ steps.decide.outputs.should_deploy }} | |
| is_fork: ${{ steps.resolve.outputs.is_fork }} | |
| allow_fork: ${{ steps.decide.outputs.allow_fork }} | |
| pr_number: ${{ steps.resolve.outputs.pr_number }} | |
| pr_repository: ${{ steps.resolve.outputs.repository }} | |
| pr_ref: ${{ steps.resolve.outputs.ref }} | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: audit | |
| - name: Resolve PR info | |
| id: resolve | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| let prNumber = context.eventName === 'workflow_dispatch' | |
| ? parseInt(context.payload.inputs.pr, 10) | |
| : context.payload.number; | |
| if (!Number.isInteger(prNumber)) { core.setFailed('Invalid PR number'); return; } | |
| const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); | |
| core.setOutput('pr_number', String(prNumber)); | |
| core.setOutput('repository', pr.head.repo.full_name); | |
| core.setOutput('ref', pr.head.ref); | |
| core.setOutput('is_fork', String(pr.head.repo.fork)); | |
| core.setOutput('author', pr.user.login); | |
| core.setOutput('state', pr.state); | |
| - name: Decide deploy | |
| id: decide | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| STATE: ${{ steps.resolve.outputs.state }} | |
| IS_FORK: ${{ steps.resolve.outputs.is_fork }} | |
| # nur bei workflow_dispatch gesetzt: | |
| ALLOW_FORK_INPUT: ${{ inputs.allow_fork }} | |
| PR_AUTHOR: ${{ steps.resolve.outputs.author }} | |
| run: | | |
| set -e | |
| # Standard: nichts deployen | |
| should=false | |
| allow_fork="$(echo "${ALLOW_FORK_INPUT:-false}" | tr '[:upper:]' '[:lower:]')" | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| if [ "$STATE" != "open" ]; then | |
| echo "PR not open -> skip" | |
| else | |
| if [ "$IS_FORK" = "true" ] && [ "$allow_fork" != "true" ]; then | |
| echo "Fork PR and allow_fork=false -> skip" | |
| else | |
| should=true | |
| fi | |
| fi | |
| else | |
| auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs") | |
| is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done | |
| if [ "$is_auth" = true ]; then | |
| should=true | |
| fi | |
| fi | |
| echo "should_deploy=$should" >> $GITHUB_OUTPUT | |
| echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT | |
| deploy-v2-pr: | |
| environment: pr-preview | |
| needs: check-pr | |
| runs-on: ubuntu-latest | |
| if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true') | |
| # Concurrency control - only one deployment per PR at a time | |
| concurrency: | |
| group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| issues: write | |
| packages: write | |
| pull-requests: write | |
| env: | |
| # Single source of truth for whether this preview embeds the admin portal: | |
| # drives the image build-arg and the deployment comment. | |
| BUILD_PORTAL: "true" | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout main repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: main | |
| - name: Add deployment started comment | |
| id: deployment-started | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = ${{ needs.check-pr.outputs.pr_number }}; | |
| // Delete previous V2 deployment comments to avoid clutter | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| per_page: 100 | |
| }); | |
| const v2Comments = comments.filter(comment => | |
| comment.body.includes('🚀 **Auto-deploying V2 version**') || | |
| comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') || | |
| comment.body.includes('❌ **V2 Auto-deployment failed**') | |
| ); | |
| for (const comment of v2Comments) { | |
| console.log(`Deleting old V2 comment: ${comment.id}`); | |
| await github.rest.issues.deleteComment({ | |
| owner, | |
| repo, | |
| comment_id: comment.id | |
| }); | |
| } | |
| // Create new deployment started comment | |
| const { data: newComment } = 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 for approved V2 contributors._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.` | |
| }); | |
| return newComment.id; | |
| - name: Checkout PR | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| repository: ${{ needs.check-pr.outputs.pr_repository }} | |
| ref: ${{ needs.check-pr.outputs.pr_ref }} | |
| # untrusted tree is built below - never leave credentials in .git/config | |
| persist-credentials: false | |
| fetch-depth: 0 # Fetch full history for commit hash detection | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| - name: Get version number | |
| id: versionNumber | |
| run: | | |
| VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') | |
| echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| - name: Convert repository owner to lowercase | |
| id: repoowner | |
| run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT | |
| - name: Get commit hash for app | |
| id: commit-hash | |
| run: | | |
| # Get last commit that touched the application code | |
| APP_HASH=$(git log -1 --format="%H" -- . 2>/dev/null || echo "") | |
| if [ -z "$APP_HASH" ]; then | |
| APP_HASH="no-changes" | |
| fi | |
| echo "App hash: $APP_HASH" | |
| echo "app_hash=$APP_HASH" >> $GITHUB_OUTPUT | |
| # Short hash for tags | |
| if [ "$APP_HASH" = "no-changes" ]; then | |
| echo "app_short=no-changes" >> $GITHUB_OUTPUT | |
| else | |
| echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Check if image exists | |
| id: check-image | |
| run: | | |
| if docker manifest inspect ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "Image already exists, skipping build" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "Image needs to be built" | |
| fi | |
| env: | |
| IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test | |
| - name: Build and push V2 image | |
| if: steps.check-image.outputs.exists == 'false' | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| file: ./docker/embedded/Dockerfile | |
| push: true | |
| cache-from: type=gha,scope=stirling-pdf-latest | |
| cache-to: type=gha,mode=max,scope=stirling-pdf-latest | |
| tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-${{ steps.commit-hash.outputs.app_short }} | |
| build-args: | | |
| VERSION_TAG=v2-alpha | |
| BUILD_PORTAL=${{ env.BUILD_PORTAL }} | |
| platforms: linux/amd64 | |
| - name: Set up SSH | |
| run: | | |
| mkdir -p ~/.ssh/ | |
| echo "${NEW_VPS_SSH_KEY}" > ../private.key | |
| sudo chmod 600 ../private.key | |
| env: | |
| NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_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 with unified embedded image | |
| 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: ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} | |
| ports: | |
| - "${V2_PORT}:8080" | |
| 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 | |
| - /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/storage:/storage:rw | |
| environment: | |
| DISABLE_ADDITIONAL_FEATURES: "false" | |
| STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true" | |
| SECURITY_ENABLELOGIN: "true" | |
| SECURITY_INITIALLOGIN_USERNAME: "${TEST_LOGIN_USERNAME}" | |
| SECURITY_INITIALLOGIN_PASSWORD: "${TEST_LOGIN_PASSWORD}" | |
| SYSTEM_DEFAULTLOCALE: en-US | |
| UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}" | |
| UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture" | |
| UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}" | |
| SYSTEM_MAXFILESIZE: "100" | |
| METRICS_ENABLED: "true" | |
| SYSTEM_GOOGLEVISIBILITY: "false" | |
| SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud" | |
| baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud" | |
| restart: on-failure:5 | |
| EOF | |
| # Deploy to VPS | |
| scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose-v2.yml | |
| ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH | |
| # Create V2 PR-specific directories | |
| mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs,storage} | |
| # 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 | |
| # Stop any existing container and clean up | |
| cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }} | |
| docker-compose down --remove-orphans 2>/dev/null || true | |
| # Start the new container | |
| docker-compose pull | |
| docker-compose up -d | |
| # Clean up unused Docker resources to save space | |
| docker system prune -af --volumes || true | |
| # Clean up old images (older than 2 weeks) | |
| docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true | |
| ENDSSH | |
| # Set port for output | |
| echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT | |
| env: | |
| IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test | |
| TEST_LOGIN_USERNAME: ${{ secrets.TEST_LOGIN_USERNAME }} | |
| TEST_LOGIN_PASSWORD: ${{ secrets.TEST_LOGIN_PASSWORD }} | |
| NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} | |
| NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} | |
| # ---- Storybook preview (only when this PR touches stories/.storybook) ---- | |
| # Runs inside the same approved-contributor-gated deploy job, so it deploys | |
| # under the exact same access rules as the app preview. | |
| - name: Detect Storybook changes | |
| id: sb-changes | |
| uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| with: | |
| list-files: json | |
| filters: | | |
| storybook: | |
| - 'frontend/**/*.stories.@(ts|tsx|mdx)' | |
| - 'frontend/**/*.mdx' | |
| - 'frontend/.storybook/**' | |
| - name: Set up Node.js for Storybook | |
| if: steps.sb-changes.outputs.storybook == 'true' | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "22" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install Task for Storybook | |
| if: steps.sb-changes.outputs.storybook == 'true' | |
| uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 | |
| - name: Build and deploy Storybook | |
| id: storybook | |
| if: steps.sb-changes.outputs.storybook == 'true' | |
| env: | |
| VPS_HOST: ${{ secrets.NEW_VPS_HOST }} | |
| VPS_USER: ${{ secrets.NEW_VPS_USERNAME }} | |
| run: | | |
| set -euo pipefail | |
| # `prepare` generates the icon set stories import (not committed). | |
| task frontend:prepare | |
| task frontend:storybook:build | |
| PR=${{ needs.check-pr.outputs.pr_number }} | |
| # Served at the ROOT of its own port so Storybook's global MSW worker | |
| # (/mockServiceWorker.js) resolves. Port = PR + 20000 (bijective, offset | |
| # from the app preview's bare-PR-number port). | |
| SB_PORT=$((PR + 20000)) | |
| DIR=/stirling/SB-PR-$PR | |
| tar czf storybook.tgz -C frontend/storybook-static . | |
| scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ | |
| storybook.tgz "$VPS_USER@$VPS_HOST:/tmp/storybook-$PR.tgz" | |
| ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T \ | |
| "$VPS_USER@$VPS_HOST" << ENDSSH | |
| set -e | |
| rm -rf "$DIR" && mkdir -p "$DIR" | |
| tar xzf /tmp/storybook-$PR.tgz -C "$DIR" | |
| rm -f /tmp/storybook-$PR.tgz | |
| docker rm -f storybook-pr-$PR 2>/dev/null || true | |
| docker run -d --name storybook-pr-$PR --restart unless-stopped \ | |
| -p $SB_PORT:80 -v "$DIR":/usr/share/nginx/html:ro nginx:alpine | |
| ENDSSH | |
| echo "url=http://$VPS_HOST:$SB_PORT/" >> "$GITHUB_OUTPUT" | |
| - name: Post V2 deployment URL to PR | |
| if: success() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SB_URL: ${{ steps.storybook.outputs.url }} | |
| SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }} | |
| NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = ${{ needs.check-pr.outputs.pr_number }}; | |
| const v2Port = ${{ steps.deploy.outputs.v2_port }}; | |
| // Delete the "deploying..." comment since we're posting the final result | |
| const deploymentStartedId = ${{ steps.deployment-started.outputs.result }}; | |
| if (deploymentStartedId) { | |
| console.log(`Deleting deployment started comment: ${deploymentStartedId}`); | |
| try { | |
| await github.rest.issues.deleteComment({ | |
| owner, | |
| repo, | |
| comment_id: deploymentStartedId | |
| }); | |
| } catch (error) { | |
| console.log(`Could not delete deployment started comment: ${error.message}`); | |
| } | |
| } | |
| const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${v2Port}`; | |
| // Only mention the portal when this image actually embeds it. | |
| // Use the direct IP URL - the SSL hostname isn't supported yet. | |
| const withPortal = "${{ env.BUILD_PORTAL }}" === "true"; | |
| const portalNote = withPortal | |
| ? `🧩 **Admin portal** included - try it at [${deploymentUrl}/portal](${deploymentUrl}/portal).\n\n` | |
| : ``; | |
| // Storybook preview: only present when this PR changed stories/config. | |
| const sbUrl = process.env.SB_URL; | |
| let storybookNote = ""; | |
| if (sbUrl) { | |
| const files = JSON.parse(process.env.SB_FILES || "[]"); | |
| const stories = files.filter((f) => /\.stories\.(ts|tsx|mdx)$/.test(f)); | |
| const config = files.filter((f) => f.startsWith("frontend/.storybook/")); | |
| const shorten = (f) => | |
| f.replace(/^frontend\/editor\/src\//, "").replace(/^frontend\//, ""); | |
| const storyList = stories.map((f) => `- \`${shorten(f)}\``).join("\n"); | |
| const configList = config.map((f) => `- \`${shorten(f)}\``).join("\n"); | |
| const summary = | |
| `${stories.length} stor${stories.length === 1 ? "y" : "ies"} changed` + | |
| (config.length ? ` (+${config.length} config file${config.length === 1 ? "" : "s"})` : ""); | |
| storybookNote = | |
| `📚 **Storybook:** [${sbUrl}](${sbUrl})\n\n` + | |
| `<details>\n<summary>${summary}</summary>\n\n` + | |
| (storyList ? `**Stories**\n${storyList}\n\n` : "") + | |
| (configList ? `**Config**\n${configList}\n` : "") + | |
| `</details>\n\n`; | |
| } | |
| const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` + | |
| `🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` + | |
| portalNote + | |
| storybookNote + | |
| `_This deployment will be automatically cleaned up when the PR is closed._\n\n` + | |
| `🔄 **Auto-deployed** for approved V2 contributors.`; | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body: commentBody | |
| }); | |
| cleanup-v2-deployment: | |
| # Tearing a preview down is not a deployment - no deployment object. | |
| environment: | |
| name: pr-preview | |
| deployment: false | |
| if: github.event.action == 'closed' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Clean up V2 deployment comments | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const prNumber = ${{ github.event.pull_request.number }}; | |
| // Find and delete V2 deployment comments | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: prNumber | |
| }); | |
| const v2Comments = comments.filter(c => | |
| c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") && | |
| c.user?.type === "Bot" | |
| ); | |
| for (const comment of v2Comments) { | |
| await github.rest.issues.deleteComment({ | |
| owner, | |
| repo, | |
| comment_id: comment.id | |
| }); | |
| console.log(`Deleted V2 deployment comment (ID: ${comment.id})`); | |
| } | |
| - name: Set up SSH | |
| run: | | |
| mkdir -p ~/.ssh/ | |
| echo "${NEW_VPS_SSH_KEY}" > ../private.key | |
| sudo chmod 600 ../private.key | |
| env: | |
| NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} | |
| - name: Cleanup V2 deployment | |
| run: | | |
| ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH' | |
| if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then | |
| echo "Found V2 PR directory, proceeding with cleanup..." | |
| # Stop and remove V2 containers | |
| cd /stirling/V2-PR-${{ github.event.pull_request.number }} | |
| docker-compose down || true | |
| # Go back to root before removal | |
| cd / | |
| # Remove V2 PR-specific directories | |
| rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }} | |
| # Clean up V2 container by name (in case compose cleanup missed it) | |
| docker rm -f stirling-pdf-v2-pr-${{ github.event.pull_request.number }} || true | |
| echo "V2 cleanup completed" | |
| else | |
| echo "V2 PR directory not found, nothing to clean up" | |
| fi | |
| # Remove this PR's Storybook preview (container + files), if any. | |
| docker rm -f storybook-pr-${{ github.event.pull_request.number }} 2>/dev/null || true | |
| rm -rf /stirling/SB-PR-${{ github.event.pull_request.number }} | |
| # Clean up old unused images (older than 2 weeks) but keep recent ones for reuse | |
| docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true | |
| # Note: We don't remove the commit-based images since they can be reused across PRs | |
| # Only remove PR-specific containers and directories | |
| ENDSSH | |
| env: | |
| NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} | |
| NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} | |
| - name: Cleanup temporary files | |
| if: always() | |
| run: | | |
| rm -f ../private.key docker-compose.yml storybook.tgz | |
| continue-on-error: true |