diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 70a964b020..b5cc0527b0 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -68,7 +68,6 @@ project: &project frontend: &frontend - *ci - frontend/** - - .github/workflows/testdriver.yml - testing/** - docker/** - scripts/translations/*.py diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index e0032955e3..1407939994 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -7,10 +7,6 @@ on: permissions: contents: read -env: - SERVER_IP: ${{ secrets.NEW_VPS_IP }} # Add this to your GitHub secrets - CLEANUP_PERFORMED: "false" # Add flag to track if cleanup occurred - jobs: cleanup: environment: pr-preview diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml deleted file mode 100644 index b9b391af0e..0000000000 --- a/.github/workflows/ai_pr_title_review.yml +++ /dev/null @@ -1,221 +0,0 @@ -name: AI - PR Title Review - -on: - pull_request: - types: [opened, edited] - branches: [main] - -permissions: # required for secure-repo hardening - contents: read - -jobs: - ai-title-review: - # GITHUB_TOKEN obeys this block, so it must cover every API call made below. - permissions: - contents: read # actions/checkout, git fetch/diff - issues: write # issues.listComments / createComment / updateComment on the PR - pull-requests: write # same endpoints when the target is a pull request - models: read # actions/ai-inference - - runs-on: ubuntu-latest - - steps: - - name: Harden Runner - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Configure Git to suppress detached HEAD warning - run: git config --global advice.detachedHead false - - - name: Check if actor is repo developer - id: actor - run: | - if [[ "${{ github.actor }}" == *"[bot]" ]]; then - echo "PR opened by a bot – skipping AI title review." - echo "is_repo_dev=false" >> $GITHUB_OUTPUT - exit 0 - fi - if [ ! -f .github/config/repo_devs.json ]; then - echo "Error: .github/config/repo_devs.json not found" >&2 - exit 1 - fi - # Validate JSON and extract repo_devs - REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; }) - # Convert developer list into Bash array - mapfile -t DEVS_ARRAY <<< "$REPO_DEVS" - if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then - echo "is_repo_dev=true" >> $GITHUB_OUTPUT - else - echo "is_repo_dev=false" >> $GITHUB_OUTPUT - fi - - - name: Get PR diff - if: steps.actor.outputs.is_repo_dev == 'true' - id: get_diff - run: | - git fetch origin ${{ github.base_ref }} - git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff - echo "diff<> $GITHUB_OUTPUT - cat pr.diff >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Check and sanitize PR title - if: steps.actor.outputs.is_repo_dev == 'true' - id: sanitize_pr_title - env: - PR_TITLE_RAW: ${{ github.event.pull_request.title }} - run: | - # Sanitize PR title: max 72 characters, only printable characters - PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g') - if [[ ${#PR_TITLE} -lt 5 ]]; then - echo "PR title is too short. Must be at least 5 characters." >&2 - fi - echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT - - - name: AI PR Title Analysis - if: steps.actor.outputs.is_repo_dev == 'true' - id: ai-title-analysis - uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1 - with: - model: openai/gpt-4o - system-prompt-file: ".github/config/system-prompt.txt" - prompt: | - Based on the following input data: - - { - "diff": "${{ steps.get_diff.outputs.diff }}", - "pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}" - } - - Respond ONLY with valid JSON in the format: - { - "improved_rating": <0-10>, - "improved_ai_title_rating": <0-10>, - "improved_title": "" - } - - - name: Validate and set SCRIPT_OUTPUT - if: steps.actor.outputs.is_repo_dev == 'true' - run: | - cat < ai_response.json - ${{ steps.ai-title-analysis.outputs.response }} - EOF - - # Validate JSON structure - jq -e ' - (keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and - (.improved_rating | type == "number" and . >= 0 and . <= 10) and - (.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and - (.improved_title | type == "string") - ' ai_response.json - if [ $? -ne 0 ]; then - echo "Invalid AI response format" >&2 - cat ai_response.json >&2 - exit 1 - fi - # Parse JSON fields - IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json) - IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json) - # Limit comment length to 1000 characters - COMMENT=$(cat < /tmp/ai-title-comment.md - # Log input and output to the GitHub Step Summary - echo "### šŸ¤– AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY - echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY - echo '```json' >> $GITHUB_STEP_SUMMARY - cat ai_response.json >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: Post comment on PR if needed - if: steps.actor.outputs.is_repo_dev == 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - continue-on-error: true - with: - github-token: ${{ github.token }} - script: | - const fs = require('fs'); - const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8'); - const { GITHUB_REPOSITORY } = process.env; - const [owner, repo] = GITHUB_REPOSITORY.split('/'); - const issue_number = context.issue.number; - - const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/); - const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null; - - const expectedActor = "github-actions[bot]"; - const comments = await github.rest.issues.listComments({ owner, repo, issue_number }); - - const existing = comments.data.find(c => - c.user?.login === expectedActor && - c.body.includes("## šŸ¤– AI PR Title Suggestion") - ); - - if (rating === null) { - console.log("No rating found in AI response – skipping."); - return; - } - - if (rating <= 5) { - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, - comment_id: existing.id, - body - }); - console.log("Updated existing suggestion comment."); - } else { - await github.rest.issues.createComment({ - owner, repo, issue_number, - body - }); - console.log("Created new suggestion comment."); - } - } else { - const praise = `## šŸ¤– AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\nāœ… No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`; - - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, - comment_id: existing.id, - body: praise - }); - console.log("Replaced suggestion with praise."); - } else { - console.log("Rating > 5 and no existing comment – skipping comment."); - } - } - - - name: is not repo dev - if: steps.actor.outputs.is_repo_dev != 'true' - run: | - exit 0 # Skip the AI title review for non-repo developers - - - name: Clean up - if: always() - run: | - rm -f pr.diff ai_response.json /tmp/ai-title-comment.md - echo "Cleaned up temporary files." - continue-on-error: true # Ensure cleanup runs even if previous steps fail diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 9561833f07..54bd4cb907 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -20,6 +20,7 @@ permissions: jobs: build: + environment: ci-unsigned runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml index 08849c683b..0604f7176f 100644 --- a/.github/workflows/build-enterprise.yml +++ b/.github/workflows/build-enterprise.yml @@ -37,6 +37,7 @@ jobs: uses: ./.github/workflows/_runner-pick.yml playwright-e2e-enterprise: + environment: ci-unsigned needs: pick # Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE, # so the suite can't boot premium and would fail. See the header comment. @@ -309,6 +310,7 @@ jobs: # Multi-node regression: builds + seeds the clustered stack (testing/compose/docker-compose-multinode.yml) # and runs behave features/multinode. Licence-gated, so it runs after the Playwright job (not in parallel). multinode-e2e: + environment: ci-unsigned needs: [pick, playwright-e2e-enterprise] # Nightly cron + manual dispatch only (heavy build), fork-gated for the licence secret. if: >- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eee07d599a..2f50249099 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,6 +61,7 @@ jobs: filters: .github/config/.files.yaml gradle-cache-prime: + environment: ci-unsigned name: Prime shared Gradle cache needs: [files-changed] runs-on: ubuntu-latest diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index d45a68e860..2eec970b8f 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -10,6 +10,7 @@ permissions: jobs: check-licence: + environment: ci-unsigned runs-on: ubuntu-latest steps: - name: Harden Runner diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index 751a34a33e..f224ce18cf 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -11,6 +11,7 @@ permissions: jobs: check-generate-openapi-docs: + environment: ci-unsigned runs-on: ubuntu-latest steps: - name: Harden Runner diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index 4181390285..d6a61b45c4 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -13,6 +13,7 @@ permissions: jobs: migration-test: + environment: ci-unsigned runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/.github/workflows/deploy-on-v2-commit.yml b/.github/workflows/deploy-on-v2-commit.yml deleted file mode 100644 index 01114c64b2..0000000000 --- a/.github/workflows/deploy-on-v2-commit.yml +++ /dev/null @@ -1,209 +0,0 @@ -name: Auto V2 Deploy on Push - -on: - push: - branches: - - V2 - - deploy-on-v2-commit - -permissions: - contents: read - -jobs: - deploy-v2-on-push: - environment: pr-preview - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - concurrency: - group: deploy-v2-push-V2 - cancel-in-progress: true - - steps: - - name: Harden Runner - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - 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: Convert repository owner to lowercase - id: repoowner - run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $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: Check if frontend image exists - id: check-frontend - run: | - if docker manifest inspect ${IMAGE_BASE}: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 - - env: - IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - - name: Check if backend image exists - id: check-backend - run: | - if docker manifest inspect ${IMAGE_BASE}: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 - - env: - IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - - - name: Build and push frontend image - if: steps.check-frontend.outputs.exists == 'false' - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: ./docker/frontend/Dockerfile - push: true - cache-from: type=gha,scope=stirling-v2-frontend - cache-to: type=gha,mode=max,scope=stirling-v2-frontend - tags: | - ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} - ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-frontend-latest - build-args: VERSION_TAG=v2-alpha - platforms: linux/amd64 - - - name: Build and push backend image - if: steps.check-backend.outputs.exists == 'false' - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: ./docker/backend/Dockerfile - push: true - cache-from: type=gha,scope=stirling-v2-backend - cache-to: type=gha,mode=max,scope=stirling-v2-backend - tags: | - ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} - ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-backend-latest - build-args: VERSION_TAG=v2-alpha - platforms: linux/amd64 - - - name: Set up SSH - run: | - mkdir -p ~/.ssh/ - echo "${NEW_VPS_SSH_KEY}" > ../private.key - chmod 600 ../private.key - - env: - NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - - name: Deploy to VPS on port 3000 - run: | - export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml - - cat > $UNIQUE_NAME << EOF - version: '3.3' - services: - backend: - container_name: stirling-v2-backend - image: ${IMAGE_BASE}:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} - ports: - - "13000:8080" - volumes: - - /stirling/V2/data:/usr/share/tessdata:rw - - /stirling/V2/config:/configs:rw - - /stirling/V2/logs:/logs:rw - environment: - DISABLE_ADDITIONAL_FEATURES: "true" - SECURITY_ENABLELOGIN: "false" - SYSTEM_DEFAULTLOCALE: en-US - UI_APPNAME: "Stirling-PDF V2" - UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split" - UI_APPNAMENAVBAR: "V2 Deployment" - SYSTEM_MAXFILESIZE: "100" - METRICS_ENABLED: "true" - SYSTEM_GOOGLEVISIBILITY: "false" - SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud" - baseUrl: "https://demo.stirlingpdf.cloud" - restart: on-failure:5 - - frontend: - container_name: stirling-v2-frontend - image: ${IMAGE_BASE}:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} - ports: - - "3000:80" - environment: - VITE_API_BASE_URL: "http://${NEW_VPS_HOST}:13000" - depends_on: - - backend - restart: on-failure:5 - EOF - - # Copy to remote with unique name - scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/$UNIQUE_NAME - - # SSH and rename/move atomically to avoid interference - ssh -i ../private.key -o StrictHostKeyChecking=no ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH - mkdir -p /stirling/V2/{data,config,logs} - mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml - cd /stirling/V2 - docker-compose down || true - docker-compose pull - docker-compose up -d - docker system prune -af --volumes || true - docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true - ENDSSH - - env: - IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} - - name: Cleanup temporary files - if: always() - run: | - rm -f ../private.key diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index ddb35b4e1a..9d5911404f 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -17,6 +17,7 @@ permissions: jobs: docker-compose-tests: + environment: ci-unsigned runs-on: ubuntu-latest permissions: actions: write diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 4aa2a78c89..7bc95df05e 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -11,6 +11,7 @@ permissions: jobs: playwright-e2e-live: + environment: ci-unsigned runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index 1155f01e39..458766660f 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -42,6 +42,8 @@ jobs: filters: .github/config/.files.yaml generate-frontend-license-report: + # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only. + environment: ci-bot if: needs.files-changed.outputs.licenses-frontend == 'true' name: Generate Frontend License Report needs: files-changed @@ -316,6 +318,8 @@ jobs: GH_TOKEN: ${{ steps.setup-bot.outputs.token }} generate-backend-license-report: + # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only. + environment: ci-bot if: needs.files-changed.outputs.licenses-backend == 'true' needs: files-changed name: Generate Backend License Report diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 3bb014a82d..d9477f722f 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -38,6 +38,7 @@ permissions: jobs: determine-matrix: + environment: ci-unsigned if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest outputs: @@ -118,6 +119,7 @@ jobs: env: INPUT_PLATFORM: ${{ github.event.inputs.platform }} build-jars: + environment: ci-unsigned needs: determine-matrix runs-on: ubuntu-latest strategy: @@ -204,7 +206,6 @@ jobs: runs-on: ${{ matrix.platform }} env: SM_API_KEY: ${{ secrets.SM_API_KEY }} - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }} steps: - name: Harden Runner @@ -295,7 +296,7 @@ jobs: # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker id: digicert-setup - if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1 env: SM_API_KEY: ${{ secrets.SM_API_KEY }} @@ -305,7 +306,7 @@ jobs: SM_HOST: ${{ secrets.SM_HOST }} - name: Setup DigiCert KeyLocker Certificate - if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} shell: pwsh run: | Write-Host "Setting up DigiCert KeyLocker environment..." @@ -344,40 +345,8 @@ jobs: SM_API_KEY: ${{ secrets.SM_API_KEY }} SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} - # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - - name: Import Windows Code Signing Certificate - if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} - env: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} - shell: powershell - run: | - if ($env:WINDOWS_CERTIFICATE) { - Write-Host "Importing Windows Code Signing Certificate..." - - # Decode base64 certificate and save to file - $certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE) - $certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx" - [IO.File]::WriteAllBytes($certPath, $certBytes) - - # Import certificate to CurrentUser\My store - $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force) - - # Extract and set thumbprint as environment variable - $thumbprint = $cert.Thumbprint - Write-Host "Certificate imported with thumbprint: $thumbprint" - echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV - - # Clean up certificate file - Remove-Item $certPath - - Write-Host "Windows certificate import completed." - } else { - Write-Host "āš ļø WINDOWS_CERTIFICATE secret not set - building unsigned binary" - } - - name: Import Apple Developer Certificate - if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -398,7 +367,7 @@ jobs: rm certificate.p12 - name: Verify Certificate - if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') run: | echo "Verifying Apple Developer Certificate..." KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db @@ -414,7 +383,7 @@ jobs: # Without this, signCommand failures are opaque (Tauri captures but drops # smctl's stderr) - running these loudly surfaces auth/env/keypair issues. - name: Preflight smctl - if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} shell: pwsh env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -445,7 +414,7 @@ jobs: # smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD # from env (set by prior DigiCert setup step). No --config-file needed. - name: Configure Windows code signing - if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} shell: bash env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -466,7 +435,7 @@ jobs: sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/editor/src-tauri/tauri.windows.conf.json - name: Import release GPG signing key (Linux) - if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') run: | echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import gpg --list-secret-keys --keyid-format=long @@ -498,8 +467,8 @@ jobs: # APPIMAGETOOL_SIGN_PASSPHRASE appimagetool uses this to unlock the GPG key non-interactively # SIGN_KEY appimagetool picks the key matching this fingerprint # Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present. - # Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or V2-master, when secret is present. - SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }} + # Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or the release branch, when secret is present. + SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }} APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} @@ -525,7 +494,7 @@ jobs: env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }} + GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }} SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} run: | @@ -564,7 +533,7 @@ jobs: echo "Stripped bundled libwayland from $(basename "$AI")" - name: Clear release GPG key from runner keyring (Linux) - if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') env: RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }} run: | @@ -579,7 +548,7 @@ jobs: # artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw # cargo output unsigned, so checking it produces false negatives. - name: Verify Windows Code Signature - if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} timeout-minutes: 15 shell: pwsh run: | @@ -911,11 +880,11 @@ jobs: # workflow_dispatch path requires platform=='all' so a single-platform # dispatch can't overwrite an existing release's full latest.json with a # partial one (action-gh-release defaults overwrite_files:true). - # release / V2-master always build the full matrix so no extra guard needed. + # release event / release branch always build the full matrix so no extra guard needed. # fail_on_unmatched_files makes a missing latest.json or installer fail loudly # instead of silently shipping a broken auto-update. - name: Upload binaries to Release - if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master' + if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/release' uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: tag_name: v${{ needs.determine-matrix.outputs.version }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 70e99d6074..c92d17f027 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -127,6 +127,7 @@ jobs: # Runs the @nightly tag (conversion scenarios) plus a 10-shard concurrency run # of every other feature. cucumber-nightly: + environment: ci-unsigned name: Cucumber (nightly scenarios + full concurrency) runs-on: ubuntu-latest # Fork pull requests get no MAVEN_* secrets, so the image build cannot work. diff --git a/.github/workflows/push-docker-base.yml b/.github/workflows/push-docker-base.yml index 658583ea08..97c227f23c 100644 --- a/.github/workflows/push-docker-base.yml +++ b/.github/workflows/push-docker-base.yml @@ -17,6 +17,9 @@ permissions: jobs: push-base: + # Own environment: docker-publish is branch-locked to release/main, + # which excludes the baseDockerImage/accessIssueFix branches this runs on. + environment: docker-base-publish if: ${{ vars.CI_PROFILE != 'lite' && github.actor == 'Frooodle' }} runs-on: ubuntu-24.04-8core permissions: diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index b88d69c3c2..ec9d14822c 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -20,9 +20,8 @@ on: default: false push: branches: - - master + - release - main - - V2-master # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed @@ -91,13 +90,13 @@ jobs: MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} - name: Install cosign - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' + if: github.ref == 'refs/heads/release' uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 with: cosign-release: "v2.4.1" - name: Install cosign - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' + if: github.ref == 'refs/heads/release' uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 with: cosign-release: "v2.4.1" @@ -133,8 +132,8 @@ jobs: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf tags: | - type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} + type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/release' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/release' }} - name: Build and push Unified Dockerfile (latest variant) id: build-push-latest @@ -158,7 +157,7 @@ jobs: sbom: true - name: Sign regular images - if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-latest.outputs.digest != '' + if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-latest.outputs.digest != '' env: DIGEST: ${{ steps.build-push-latest.outputs.digest }} TAGS: ${{ steps.meta.outputs.tags }} @@ -182,8 +181,8 @@ jobs: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf tags: | - type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} - type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} + type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/release' }} + type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/release' }} - name: Build and push Unified Dockerfile (fat variant) id: build-push-fat @@ -204,7 +203,7 @@ jobs: sbom: true - name: Sign fat images - if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-fat.outputs.digest != '' + if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-fat.outputs.digest != '' env: DIGEST: ${{ steps.build-push-fat.outputs.digest }} TAGS: ${{ steps.meta-fat.outputs.tags }} @@ -226,8 +225,8 @@ jobs: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf tags: | - type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} - type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} + type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }} + type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }} - name: Build and push Unified Dockerfile (ultra-lite variant) id: build-push-lite @@ -248,7 +247,7 @@ jobs: sbom: true - name: Sign ultra-lite images - if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-lite.outputs.digest != '' + if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-lite.outputs.digest != '' env: DIGEST: ${{ steps.build-push-lite.outputs.digest }} TAGS: ${{ steps.meta-lite.outputs.tags }} @@ -260,7 +259,7 @@ jobs: done # Standalone unoserver image — versioned independently via - # docker/unoserver/VERSION. master/V2-master: publish +latest + # docker/unoserver/VERSION. release: publish +latest # only when the version is new. main/testMain: republish :alpha only # when the source hash differs from the published image's annotation. - name: Read unoserver image version @@ -319,7 +318,7 @@ jobs: fi case "$EFFECTIVE_REF" in - refs/heads/master|refs/heads/V2-master) + refs/heads/release) if [ "${FORCE_REBUILD}" = "true" ]; then echo "force_unoserver_rebuild=true — building stable regardless" mode="stable" diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 38c1985a83..115de87d4e 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - master + - release # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed @@ -23,6 +23,9 @@ permissions: jobs: push: + # package-publish holds SWAGGERHUB_API_KEY. It requires reviewer approval and + # is limited to main / release / v* tags, so every push to release waits on one. + environment: package-publish if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index ab6d8aca71..ddf1104bac 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -57,6 +57,9 @@ permissions: jobs: determine-matrix: + # Only probes APPLE_CERTIFICATE for presence, so it stays on the unrestricted + # signing environment - release-signing would block every PR run. + environment: ci-signing if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest outputs: @@ -103,6 +106,12 @@ jobs: echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT build: + # Windows/GPG signing only runs on main (see the per-step gates below), so only + # that path needs the reviewer-gated release-signing environment. Everything else + # (PRs, merge queue, nightly) signs macOS only and uses ci-signing, which has no + # approval or branch restriction. + environment: + name: ${{ (inputs.sign && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) && 'release-signing' || 'ci-signing' }} needs: determine-matrix strategy: fail-fast: false @@ -110,7 +119,6 @@ jobs: runs-on: ${{ matrix.platform }} env: SM_API_KEY: ${{ secrets.SM_API_KEY }} - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }} # Per-platform sign gate. macOS signs on any run with the cert available, @@ -264,38 +272,6 @@ jobs: } } - # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - - name: Import Windows Code Signing Certificate - if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }} - env: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} - shell: powershell - run: | - if ($env:WINDOWS_CERTIFICATE) { - Write-Host "Importing Windows Code Signing Certificate..." - - # Decode base64 certificate and save to file - $certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE) - $certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx" - [IO.File]::WriteAllBytes($certPath, $certBytes) - - # Import certificate to CurrentUser\My store - $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force) - - # Extract and set thumbprint as environment variable - $thumbprint = $cert.Thumbprint - Write-Host "Certificate imported with thumbprint: $thumbprint" - echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV - - # Clean up certificate file - Remove-Item $certPath - - Write-Host "Windows certificate import completed." - } else { - Write-Host "āš ļø WINDOWS_CERTIFICATE secret not set - building unsigned binary" - } - - name: Import Apple Developer Certificate if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15' env: diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index 660cd2458b..4a79cb3733 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -37,6 +37,7 @@ jobs: # spring-security=true matrix entry if `task backend:build` and # `task backend:build:ci` produce equivalent JARs (verify before wiring). test-build-docker-images: + environment: ci-unsigned runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml deleted file mode 100644 index 751eaf44f7..0000000000 --- a/.github/workflows/testdriver.yml +++ /dev/null @@ -1,235 +0,0 @@ -name: UI test with TestDriverAI - -on: - push: - branches: ["master", "UITest", "testdriver"] - -# cancel in-progress jobs if a new job is triggered -# This is useful to avoid running multiple builds for the same branch if a new commit is pushed -# or a pull request is updated. -# It helps to save resources and time by ensuring that only the latest commit is built and tested -# This is particularly useful for long-running jobs that may take a while to complete. -# The `group` is set to a combination of the workflow name, event name, and branch name. -# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of -# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened. -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - environment: pr-preview - if: ${{ vars.CI_PROFILE != 'lite' }} - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Harden Runner - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 25 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle User Home - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - restore-keys: | - gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25- - gradle-${{ runner.os }}-${{ runner.arch }}- - - - name: Build with Gradle - run: ./gradlew build - env: - MAVEN_USER: ${{ secrets.MAVEN_USER }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} - DISABLE_ADDITIONAL_FEATURES: true - - - 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: Convert repository owner to lowercase - id: repoowner - run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $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: Build and push test image - 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:test-${{ github.sha }} - build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} - 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 to VPS - run: | - cat > docker-compose.yml << EOF - version: '3.3' - services: - stirling-pdf: - container_name: stirling-pdf-test-${{ github.sha }} - image: ${IMAGE_BASE}:test-${{ github.sha }} - ports: - - "1337:8080" - volumes: - - /stirling/test-${{ github.sha }}/data:/usr/share/tessdata:rw - - /stirling/test-${{ github.sha }}/config:/configs:rw - - /stirling/test-${{ github.sha }}/logs:/logs:rw - environment: - DISABLE_ADDITIONAL_FEATURES: "true" - SECURITY_ENABLELOGIN: "false" - SYSTEM_DEFAULTLOCALE: en-US - UI_APPNAME: "Stirling-PDF Test" - UI_HOMEDESCRIPTION: "Test Deployment" - UI_APPNAMENAVBAR: "Test" - SYSTEM_MAXFILESIZE: "100" - METRICS_ENABLED: "true" - SYSTEM_GOOGLEVISIBILITY: "false" - SYSTEM_ENABLEANALYTICS: "false" - restart: on-failure:5 - EOF - - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml - - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << EOF - mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs} - mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml - cd /stirling/test-${{ github.sha }} - docker-compose pull - docker-compose up -d - EOF - - env: - IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} - NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - files-changed: - if: always() - name: detect what files changed - runs-on: ubuntu-latest - timeout-minutes: 3 - outputs: - frontend: ${{ steps.changes.outputs.frontend }} - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check for file changes - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 - id: changes - with: - filters: ".github/config/.files.yaml" - - test: - environment: pr-preview - if: needs.files-changed.outputs.frontend == 'true' - needs: [deploy, files-changed] - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - cache: "npm" - cache-dependency-path: frontend/package-lock.json - - - name: Run TestDriver.ai - uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3 - with: - key: ${{secrets.TESTDRIVER_API_KEY}} - prerun: | - choco install go-task -y - task frontend:build - cd frontend - npm install dashcam-chrome --save - Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337" - Start-Sleep -Seconds 20 - prompt: | - 1. /run testing/testdriver/test.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FORCE_COLOR: "3" - - cleanup: - environment: pr-preview - needs: [deploy, test] - runs-on: ubuntu-latest - if: always() - - steps: - - name: Harden Runner - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - 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 deployment - if: always() - run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << EOF - cd /stirling/test-${{ github.sha }} - docker-compose down - cd /stirling - rm -rf test-${{ github.sha }} - EOF - env: - NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} - NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - continue-on-error: true # Ensure cleanup runs even if previous steps fail diff --git a/WINDOWS_SIGNING.md b/WINDOWS_SIGNING.md index 58ffd6e657..95cbbd24e2 100644 --- a/WINDOWS_SIGNING.md +++ b/WINDOWS_SIGNING.md @@ -4,6 +4,11 @@ This guide explains how to set up Windows code signing for Stirling-PDF desktop ## Overview +Releases are signed with **DigiCert KeyLocker**, a cloud HSM: the private key never +leaves DigiCert, and the runner signs through a PKCS#11 provider. The older approach +of uploading a base64 `.pfx` to a repository secret has been removed from the +workflows - the sections below describe KeyLocker, which is what actually runs. + Windows code signing is essential for: - Preventing Windows SmartScreen warnings - Building trust with users @@ -49,29 +54,19 @@ openssl pkcs12 -export -out certificate.pfx -inkey private-key.key -in certifica ### Required Secrets -Navigate to your GitHub repository → Settings → Secrets and variables → Actions +Navigate to your GitHub repository → Settings → Environments → `release-signing`. -Add the following secrets: +These live in the `release-signing` environment, not at repository scope. That +environment requires reviewer approval and is limited to `main`, `release`, +`hotfix/*` and `v*` tags. All five come from the DigiCert ONE console. -#### 1. `WINDOWS_CERTIFICATE` -- **Description**: Base64-encoded .pfx certificate file -- **How to create**: - -**On macOS/Linux:** -```bash -base64 -i certificate.pfx | pbcopy # Copies to clipboard -``` - -**On Windows (PowerShell):** -```powershell -[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Set-Clipboard -``` - -Paste the entire base64 string into the GitHub secret. - -#### 2. `WINDOWS_CERTIFICATE_PASSWORD` -- **Description**: Password for the .pfx certificate -- **Value**: The password you set when creating/exporting the .pfx file +| Secret | Description | +| --- | --- | +| `SM_API_KEY` | KeyLocker API key. Also acts as the on/off switch: signing steps are gated on it being non-empty. | +| `SM_CLIENT_CERT_FILE_B64` | Base64-encoded PKCS#12 client authentication certificate. | +| `SM_CLIENT_CERT_PASSWORD` | Password for that client certificate. | +| `SM_KEYPAIR_ALIAS` | Alias of the signing keypair to use. | +| `SM_HOST` | DigiCert ONE host, e.g. `https://clientauth.one.digicert.com`. | ### Optional Secrets for Tauri Updater @@ -110,23 +105,23 @@ The Windows signing configuration is already set up: ### 2. GitHub Workflow (.github/workflows/tauri-build.yml) -The workflow includes three Windows signing steps: +The workflow includes four Windows signing steps, all gated on `SM_API_KEY` being +set and the ref being the release branch: -1. **Import Certificate**: Decodes and imports the .pfx certificate into Windows certificate store -2. **Build Tauri App**: Builds and signs the application using the imported certificate -3. **Verify Signature**: Validates that both .exe and .msi files are properly signed +1. **Setup DigiCert KeyLocker**: Installs the DigiCert signing tools via `digicert/ssm-code-signing` +2. **Setup DigiCert KeyLocker Certificate**: Writes the client cert and exports the PKCS#11 config +3. **Configure Windows code signing / Build Tauri app**: Signs through the PKCS#11 provider +4. **Verify Windows Code Signature**: Validates that the .exe and .msi are properly signed ## Testing the Setup ### 1. Local Testing (Windows Only) -Before pushing to GitHub, test locally: +KeyLocker is CI-only. To check signing locally, install your own certificate into +the Windows store and point Tauri at it; the build no longer reads any certificate +from an environment variable. ```powershell -# Set environment variables -$env:WINDOWS_CERTIFICATE = [Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) -$env:WINDOWS_CERTIFICATE_PASSWORD = "your-certificate-password" - # Build the application cd frontend npm run tauri build @@ -191,9 +186,10 @@ Look for: - Consider EV certificate for immediate reputation ### Certificate Not Found During Build -- Verify `WINDOWS_CERTIFICATE` secret is set -- Check base64 encoding is correct (no extra whitespace) -- Ensure password is correct +- Verify `SM_API_KEY` is present in the `release-signing` environment. If it is empty + the signing steps skip silently and the build succeeds unsigned. +- Check `SM_CLIENT_CERT_FILE_B64` base64 encoding is correct (no extra whitespace) +- Ensure `SM_CLIENT_CERT_PASSWORD` and `SM_KEYPAIR_ALIAS` match the DigiCert keypair ## Security Best Practices @@ -220,11 +216,10 @@ Look for: ## Certificate Lifecycle ### Before Expiration -1. Obtain new certificate from CA (typically annual renewal) -2. Convert to .pfx format if needed -3. Update `WINDOWS_CERTIFICATE` secret with new base64-encoded certificate -4. Update `WINDOWS_CERTIFICATE_PASSWORD` if password changed -5. Test build to verify new certificate works +1. Renew the certificate in the DigiCert ONE console (typically annual) +2. If the keypair alias changed, update `SM_KEYPAIR_ALIAS` in the `release-signing` environment +3. If the client authentication certificate was reissued, update `SM_CLIENT_CERT_FILE_B64` and `SM_CLIENT_CERT_PASSWORD` +4. Test build to verify the new certificate works ### Expired Certificates - Signed binaries remain valid (timestamp proves signing time) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 8bea5eacfc..a76db0ccff 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7398,6 +7398,14 @@ dismissSkipFile = "Skip this file" viewFile = "View file" viewInProcessor = "View in processor" +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + [portal.failures.disabled] closed = "This failure is already closed." noDocument = "This failure was not recorded against a specific document, so there is nothing here to open." diff --git a/frontend/editor/src/core/services/updateService.ts b/frontend/editor/src/core/services/updateService.ts index 043c53bc36..8b23d26deb 100644 --- a/frontend/editor/src/core/services/updateService.ts +++ b/frontend/editor/src/core/services/updateService.ts @@ -185,7 +185,7 @@ export class UpdateService { */ async getCurrentVersionFromGitHub(): Promise { const url = - "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/V2-master/build.gradle"; + "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/release/build.gradle"; try { const response = await fetch(url); diff --git a/frontend/editor/src/portal/components/failures/FileRunEventList.tsx b/frontend/editor/src/portal/components/failures/FileRunEventList.tsx index 8c463d9a2b..98a0948f9a 100644 --- a/frontend/editor/src/portal/components/failures/FileRunEventList.tsx +++ b/frontend/editor/src/portal/components/failures/FileRunEventList.tsx @@ -82,7 +82,7 @@ export function FileRunEventList() { const debugPanel = !import.meta.env.DEV ? null : (
{showJson && (
diff --git a/frontend/editor/src/portal/theme/mantineTheme.ts b/frontend/editor/src/portal/theme/mantineTheme.ts
index 7108a90073..e371b4aeb8 100644
--- a/frontend/editor/src/portal/theme/mantineTheme.ts
+++ b/frontend/editor/src/portal/theme/mantineTheme.ts
@@ -163,6 +163,10 @@ export const mantineTheme = createTheme({
     CloseButton: { defaultProps: { "aria-label": "Close" } },
     Modal: { defaultProps: { closeButtonProps: { "aria-label": "Close" } } },
     Drawer: { defaultProps: { closeButtonProps: { "aria-label": "Close" } } },
+    // The portal's md default radius (8px) is right for cards and buttons but
+    // rounds a 20px checkbox into a circle. Pin it to the smaller radius the
+    // editor's checkboxes use so the box reads as a checkbox.
+    Checkbox: { styles: { input: { borderRadius: "var(--radius-sm)" } } },
   },
   fontFamily: "var(--font-sans)",
   fontFamilyMonospace: "var(--font-mono)",