mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 23:55:21 +00:00

This PR restructures testing scripts and Docker configurations to use centralized compose files, introduces new Docker Compose variants with integrated frontend services, and updates related CI workflows. Migrate test scripts to reference testing/compose files and streamline test flows with forced rebuilds and direct curl checks. Add ultra-lite, security, and security-with-login compose files under testing/compose, each defining both backend and frontend services. Rename and adjust frontend imports and update CI workflows to build and validate the frontend separately.
500 lines
20 KiB
YAML
500 lines
20 KiB
YAML
name: Auto PR V2 Deployment
|
||
|
||
on:
|
||
pull_request:
|
||
types: [opened, synchronize, reopened, closed]
|
||
|
||
|
||
permissions:
|
||
contents: read
|
||
issues: write
|
||
pull-requests: write
|
||
|
||
jobs:
|
||
check-pr:
|
||
if: github.event.action != 'closed'
|
||
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"
|
||
echo "PR Base Branch: ${{ github.event.pull_request.base.ref }}"
|
||
|
||
# Define authorized users
|
||
authorized_users=(
|
||
"Frooodle"
|
||
"sf298"
|
||
"Ludy87"
|
||
"LaserKaspar"
|
||
"sbplat"
|
||
"reecebrowne"
|
||
"DarioGii"
|
||
"ConnorYoh"
|
||
)
|
||
|
||
# Check if author is in the authorized list
|
||
is_authorized=false
|
||
for user in "${authorized_users[@]}"; do
|
||
if [[ "$PR_AUTHOR" == "$user" ]]; then
|
||
is_authorized=true
|
||
break
|
||
fi
|
||
done
|
||
|
||
# If PR is targeting V2 and user is authorized, deploy unconditionally
|
||
PR_BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||
if [[ "$PR_BASE_BRANCH" == "V2" && "$is_authorized" == "true" ]]; then
|
||
echo "✅ Deployment forced: PR targets V2 and author is authorized."
|
||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||
exit 0
|
||
fi
|
||
|
||
# Otherwise, continue with original keyword checks
|
||
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
|
||
|
||
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'
|
||
# 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
|
||
pull-requests: write
|
||
|
||
steps:
|
||
- name: Harden Runner
|
||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||
with:
|
||
egress-policy: audit
|
||
|
||
- name: Checkout main repository
|
||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||
with:
|
||
repository: ${{ github.repository }}
|
||
ref: main
|
||
|
||
- name: Setup GitHub App Bot
|
||
if: github.actor != 'dependabot[bot]'
|
||
id: setup-bot
|
||
uses: ./.github/actions/setup-bot
|
||
continue-on-error: true
|
||
with:
|
||
app-id: ${{ secrets.GH_APP_ID }}
|
||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||
|
||
- name: Add deployment started comment
|
||
id: deployment-started
|
||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||
with:
|
||
github-token: ${{ steps.setup-bot.outputs.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 triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||
with:
|
||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||
|
||
|
||
- 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: Get commit hashes for frontend and backend
|
||
id: commit-hashes
|
||
run: |
|
||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||
if [ -z "$FRONTEND_HASH" ]; then
|
||
FRONTEND_HASH="no-frontend-changes"
|
||
fi
|
||
|
||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||
if [ -z "$BACKEND_HASH" ]; then
|
||
BACKEND_HASH="no-backend-changes"
|
||
fi
|
||
|
||
echo "Frontend hash: $FRONTEND_HASH"
|
||
echo "Backend hash: $BACKEND_HASH"
|
||
|
||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||
|
||
# Short hashes for tags
|
||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Check if frontend image exists
|
||
id: check-frontend
|
||
run: |
|
||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||
echo "exists=true" >> $GITHUB_OUTPUT
|
||
echo "Frontend image already exists, skipping build"
|
||
else
|
||
echo "exists=false" >> $GITHUB_OUTPUT
|
||
echo "Frontend image needs to be built"
|
||
fi
|
||
|
||
- name: Check if backend image exists
|
||
id: check-backend
|
||
run: |
|
||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||
echo "exists=true" >> $GITHUB_OUTPUT
|
||
echo "Backend image already exists, skipping build"
|
||
else
|
||
echo "exists=false" >> $GITHUB_OUTPUT
|
||
echo "Backend image needs to be built"
|
||
fi
|
||
|
||
- name: Build and push V2 frontend image
|
||
if: steps.check-frontend.outputs.exists == 'false'
|
||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||
with:
|
||
context: .
|
||
file: ./docker/frontend/Dockerfile
|
||
push: true
|
||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||
build-args: VERSION_TAG=v2-alpha
|
||
platforms: linux/amd64
|
||
|
||
- name: Build and push V2 backend image
|
||
if: steps.check-backend.outputs.exists == 'false'
|
||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||
with:
|
||
context: .
|
||
file: ./docker/backend/Dockerfile
|
||
push: true
|
||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||
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 }}
|
||
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
|
||
|
||
# Create docker-compose for V2 with separate frontend and backend
|
||
cat > docker-compose.yml << EOF
|
||
version: '3.3'
|
||
services:
|
||
stirling-pdf-v2-backend:
|
||
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||
ports:
|
||
- "${BACKEND_PORT}:8080" # Backend API port
|
||
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
|
||
|
||
stirling-pdf-v2-frontend:
|
||
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||
ports:
|
||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||
environment:
|
||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:${BACKEND_PORT}"
|
||
depends_on:
|
||
- stirling-pdf-v2-backend
|
||
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
|
||
|
||
# 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
|
||
|
||
# Clean up old backend/frontend images (older than 2 weeks)
|
||
docker image prune -af --filter "until=336h" --filter "label!=keep=true"
|
||
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.setup-bot.outputs.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://${{ 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` +
|
||
`_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
|
||
});
|
||
|
||
cleanup-v2-deployment:
|
||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||
with:
|
||
egress-policy: audit
|
||
|
||
- name: Checkout repository
|
||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||
|
||
- name: Setup GitHub App Bot
|
||
if: github.actor != 'dependabot[bot]'
|
||
id: setup-bot
|
||
uses: ./.github/actions/setup-bot
|
||
continue-on-error: true
|
||
with:
|
||
app-id: ${{ secrets.GH_APP_ID }}
|
||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||
|
||
- name: Clean up V2 deployment comments
|
||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||
with:
|
||
github-token: ${{ steps.setup-bot.outputs.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 "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||
sudo chmod 600 ../private.key
|
||
|
||
- name: Cleanup V2 deployment
|
||
run: |
|
||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.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 containers by name (in case compose cleanup missed them)
|
||
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
|
||
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
|
||
|
||
echo "V2 cleanup completed"
|
||
else
|
||
echo "V2 PR directory not found, nothing to clean up"
|
||
fi
|
||
|
||
# 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"
|
||
|
||
# Note: We don't remove the commit-based images since they can be reused across PRs
|
||
# Only remove PR-specific containers and directories
|
||
ENDSSH
|
||
|
||
- name: Cleanup temporary files
|
||
if: always()
|
||
run: |
|
||
rm -f ../private.key
|
||
continue-on-error: true
|
||
|