diff --git a/.github/workflows/Saas-Dev-Deploy.yml b/.github/workflows/Saas-Dev-Deploy.yml new file mode 100644 index 0000000000..733b92bca2 --- /dev/null +++ b/.github/workflows/Saas-Dev-Deploy.yml @@ -0,0 +1,246 @@ +name: Auto SaaS Dev Deployment + +on: + push: + branches: + - saas-prod + workflow_dispatch: + +permissions: + contents: read + +env: + FRONTEND_PORT: "901" + BACKEND_PORT: "902" + DEPLOY_DIR: /stirling/SAAS-DEV + +jobs: + deploy-saas-dev: + runs-on: ubuntu-latest + environment: saas-dev + concurrency: + group: saas-dev-deploy + cancel-in-progress: true + permissions: + contents: read + packages: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Check SaaS configuration + id: config + env: + PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }} + run: | + echo "supabase_url=https://${PROJECT_REF}.supabase.co" >> "$GITHUB_OUTPUT" + echo "meter_endpoint=https://${PROJECT_REF}.supabase.co/functions/v1/meter-payg-units" >> "$GITHUB_OUTPUT" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - 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 + id: commit-hash + run: echo "app_short=$(git rev-parse --short=8 HEAD)" >> $GITHUB_OUTPUT + + - name: Build and push backend image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: ./docker/backend/Dockerfile + push: true + cache-from: type=gha,scope=stirling-saas-backend + cache-to: type=gha,mode=max,scope=stirling-saas-backend + tags: | + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-backend-${{ steps.commit-hash.outputs.app_short }} + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-backend-latest + build-args: | + VERSION_TAG=v2-alpha + STIRLING_FLAVOR=saas + platforms: linux/amd64 + + - name: Build and push frontend image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: ./docker/frontend/Dockerfile + push: true + cache-from: type=gha,scope=stirling-saas-frontend + cache-to: type=gha,mode=max,scope=stirling-saas-frontend + tags: | + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-frontend-${{ steps.commit-hash.outputs.app_short }} + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-frontend-latest + build-args: | + VERSION_TAG=v2-alpha + STIRLING_FLAVOR=saas + VITE_BUILD_MODE=development + VITE_SUPABASE_URL=${{ steps.config.outputs.supabase_url }} + VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${{ secrets.SAAS_SUPABASE_PUBLISHABLE_KEY }} + platforms: linux/amd64 + + - name: Build and push AI engine image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: ./engine/Dockerfile + push: true + cache-from: type=gha,scope=stirling-saas-engine + cache-to: type=gha,mode=max,scope=stirling-saas-engine + tags: | + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-engine-${{ steps.commit-hash.outputs.app_short }} + ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:saas-engine-latest + platforms: linux/amd64 + + - name: Set up SSH + env: + SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} + run: | + mkdir -p ~/.ssh/ + echo "$SSH_KEY" > ../private.key + sudo chmod 600 ../private.key + + - name: Deploy to VPS + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + IMAGE_TAG: ${{ steps.commit-hash.outputs.app_short }} + GHCR_USER: ${{ github.actor }} + GHCR_TOKEN: ${{ github.token }} + VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + VPS_HOST: ${{ secrets.NEW_VPS_HOST }} + SAAS_DB_URL: ${{ secrets.SAAS_DB_URL }} + SAAS_DB_USERNAME: ${{ secrets.SAAS_DB_USERNAME || 'postgres' }} + SAAS_DB_PASSWORD: ${{ secrets.SAAS_DB_PASSWORD }} + SAAS_DB_PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }} + SUPABASE_EDGE_FUNCTION_SECRET: ${{ secrets.SUPABASE_EDGE_FUNCTION_SECRET }} + PAYG_METER_ENDPOINT: ${{ steps.config.outputs.meter_endpoint }} + STIRLING_KEYGEN_ENABLED: ${{ secrets.KEYGEN_ACCOUNT_ID != '' && secrets.KEYGEN_API_TOKEN != '' && secrets.KEYGEN_POLICY_ID != '' }} + KEYGEN_ACCOUNT_ID: ${{ secrets.KEYGEN_ACCOUNT_ID }} + KEYGEN_API_TOKEN: ${{ secrets.KEYGEN_API_TOKEN }} + KEYGEN_POLICY_ID: ${{ secrets.KEYGEN_POLICY_ID }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + run: | + set -euo pipefail + + BASE_URL="http://${VPS_HOST}:${FRONTEND_PORT}" + + yaml() { + printf "'%s'" "$(printf '%s' "$1" | sed -e "s/'/''/g" -e 's/\$/$$/g')" + } + + ENGINE_SECRET="$(openssl rand -hex 32)" + AI_BACKEND_VARS=" + SYSTEM_AIENGINE_ENABLED: \"true\" + SYSTEM_AIENGINE_URL: \"http://saas-engine:5001\" + APP_AI_SERVICEBASEURL: \"http://saas-engine:5001\" + STIRLING_ENGINE_SHARED_SECRET: $(yaml "$ENGINE_SECRET")" + AI_SERVICE=" + + saas-engine: + container_name: stirling-saas-dev-engine + image: ${IMAGE_BASE}:saas-engine-${IMAGE_TAG} + environment: + ANTHROPIC_API_KEY: $(yaml "$ANTHROPIC_API_KEY") + VOYAGE_API_KEY: $(yaml "$VOYAGE_API_KEY") + STIRLING_ENGINE_SHARED_SECRET: $(yaml "$ENGINE_SECRET") + restart: on-failure:5" + + cat > docker-compose.yml << EOF + version: '3.3' + services: + saas-backend: + container_name: stirling-saas-dev-backend + image: ${IMAGE_BASE}:saas-backend-${IMAGE_TAG} + ports: + - "${BACKEND_PORT}:8080" + volumes: + - ${DEPLOY_DIR}/config:/configs:rw + - ${DEPLOY_DIR}/logs:/logs:rw + - ${DEPLOY_DIR}/storage:/storage:rw + environment: + SPRING_PROFILES_ACTIVE: "saas" + DISABLE_ADDITIONAL_FEATURES: "false" + SAAS_DB_URL: $(yaml "$SAAS_DB_URL") + SAAS_DB_USERNAME: $(yaml "$SAAS_DB_USERNAME") + SAAS_DB_PASSWORD: $(yaml "$SAAS_DB_PASSWORD") + SAAS_DB_PROJECT_REF: $(yaml "$SAAS_DB_PROJECT_REF") + SUPABASE_EDGE_FUNCTION_SECRET: $(yaml "$SUPABASE_EDGE_FUNCTION_SECRET") + PAYG_METER_ENDPOINT: $(yaml "$PAYG_METER_ENDPOINT") + STIRLING_KEYGEN_ENABLED: $(yaml "$STIRLING_KEYGEN_ENABLED") + KEYGEN_ACCOUNT_ID: $(yaml "$KEYGEN_ACCOUNT_ID") + KEYGEN_API_TOKEN: $(yaml "$KEYGEN_API_TOKEN") + KEYGEN_POLICY_ID: $(yaml "$KEYGEN_POLICY_ID") + SYSTEM_DEFAULTLOCALE: en-US + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "false" + SWAGGER_SERVER_URL: "${BASE_URL}" + baseUrl: "${BASE_URL}"${AI_BACKEND_VARS} + restart: on-failure:5 + + saas-frontend: + container_name: stirling-saas-dev-frontend + image: ${IMAGE_BASE}:saas-frontend-${IMAGE_TAG} + ports: + - "${FRONTEND_PORT}:80" + environment: + VITE_API_BASE_URL: "http://saas-backend:8080" + depends_on: + - saas-backend + restart: on-failure:5${AI_SERVICE} + EOF + + SSH_OPTS=(-i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null) + + scp "${SSH_OPTS[@]}" docker-compose.yml "${VPS_USERNAME}@${VPS_HOST}:/tmp/saas-dev-docker-compose.yml" + + ssh "${SSH_OPTS[@]}" -T "${VPS_USERNAME}@${VPS_HOST}" << ENDSSH + set -e + mkdir -p ${DEPLOY_DIR}/{config,logs,storage} + mv /tmp/saas-dev-docker-compose.yml ${DEPLOY_DIR}/docker-compose.yml + chmod 600 ${DEPLOY_DIR}/docker-compose.yml + cd ${DEPLOY_DIR} + printf '%s' "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin + docker-compose down --remove-orphans 2>/dev/null || true + docker-compose pull + docker-compose up -d + docker logout ghcr.io >/dev/null 2>&1 || true + docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true + ENDSSH + + - name: Wait for the backend to answer + env: + VPS_HOST: ${{ secrets.NEW_VPS_HOST }} + run: | + URL="http://${VPS_HOST}:${BACKEND_PORT}/api/v1/info/status" + for i in $(seq 1 60); do + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL" || true) + if [ "$code" = "200" ]; then echo "Healthy after $((i * 10))s"; exit 0; fi + sleep 10 + done + echo "::error::SaaS dev backend did not become healthy within 10 minutes" + exit 1 + + - name: Cleanup temporary files + if: always() + run: rm -f ../private.key docker-compose.yml + continue-on-error: true