mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Add CI steps to enable PR deploy servers to link to prod saas. This will allow pr testing of payment flows, usage of real credits etc
611 lines
26 KiB
YAML
611 lines
26 KiB
YAML
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@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.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
|
|
|
|
# The Stirling account previews connect to. Derived from the ref rather than stored as a URL
|
|
# so it cannot drift from the key: a mismatched pair is accepted by the browser and rejected
|
|
# by Supabase, surfacing much later as "session expired" on Usage rather than at sign-in.
|
|
# Secret only to match Saas-Dev-Deploy.yml, which owns the same value; a project ref is not
|
|
# itself sensitive, which is why SAAS_API_BASE_URL next to it is a plain variable.
|
|
- name: Resolve Stirling account config
|
|
id: saas
|
|
env:
|
|
PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }}
|
|
API_BASE_OVERRIDE: ${{ vars.SAAS_API_BASE_URL }}
|
|
run: |
|
|
# Set, this is the one value both halves use: the browser's portal reads and the backend's
|
|
# register/entitlement calls have to land on the same SaaS, and nothing checks that they
|
|
# do. Unset, only the backend gets a base, from its own compiled-in default.
|
|
API_BASE="${API_BASE_OVERRIDE:-https://stirling.com/app}"
|
|
echo "backend_base=${API_BASE}" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ -z "${PROJECT_REF}" ]; then
|
|
echo "Not configured for this environment: the preview will build without a Stirling"
|
|
echo "account, and the connect dialog will say so. To wire one up, set on the"
|
|
echo "pr-preview environment the secrets SAAS_DB_PROJECT_REF and"
|
|
echo "SAAS_SUPABASE_PUBLISHABLE_KEY, both from the same Supabase project."
|
|
echo "supabase_url=" >> "$GITHUB_OUTPUT"
|
|
echo "frontend_base=" >> "$GITHUB_OUTPUT"
|
|
else
|
|
# Only whether, not which: the ref is a secret here, so Actions masks it out of any
|
|
# line it appears in, derived URL included.
|
|
echo "Stirling account configured, at ${API_BASE}."
|
|
echo "supabase_url=https://${PROJECT_REF}.supabase.co" >> "$GITHUB_OUTPUT"
|
|
# Deliberately the override and not API_BASE: the backend's default is a subpath URL
|
|
# nobody has confirmed answers /api/v1, and prod CORS does not list preview hostnames,
|
|
# so portal reads stay off until someone sets a base they have checked. Empty leaves the
|
|
# committed .env default alone, which is the clean "not configured" state.
|
|
echo "frontend_base=${API_BASE_OVERRIDE}" >> "$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 }}
|
|
VITE_SUPABASE_URL=${{ steps.saas.outputs.supabase_url }}
|
|
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${{ secrets.SAAS_SUPABASE_PUBLISHABLE_KEY }}
|
|
VITE_SAAS_API_URL=${{ steps.saas.outputs.frontend_base }}
|
|
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"
|
|
STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL: "${{ steps.saas.outputs.backend_base }}"
|
|
# Off so preview traffic never accrues against a real wallet or trips its cap. The
|
|
# 402 gate is separate and stays on, so gating is still testable here.
|
|
STIRLING_BILLING_ACCOUNT_LINK_METERING_ENABLED: "false"
|
|
# Stated rather than inferred from the request: the callback has to come back to the
|
|
# preview hostname, not to the container's own :8080 behind this proxy.
|
|
SYSTEM_FRONTENDURL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
|
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@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.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
|