mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8343a6bd94 |
@@ -20,8 +20,8 @@ set -e
|
||||
# - To build the project, use:
|
||||
# ./gradlew build
|
||||
#
|
||||
# - To run the lint/format/secret checks, use:
|
||||
# task pre-commit
|
||||
# - For running pre-commit hooks (if configured), use:
|
||||
# pre-commit run --all-files
|
||||
#
|
||||
# Make sure you are in the project root directory after this script executes.
|
||||
# =============================================================================
|
||||
@@ -70,6 +70,6 @@ echo ""
|
||||
echo " To build the project: "
|
||||
echo -e "\e[34m gradle build\e[0m"
|
||||
echo ""
|
||||
echo " To run the lint/format/secret checks:"
|
||||
echo -e "\e[34m task pre-commit\e[0m"
|
||||
echo " To run pre-commit hooks (if configured):"
|
||||
echo -e "\e[34m pre-commit run --all-files -c .pre-commit-config.yaml\e[0m"
|
||||
echo "=================================================================="
|
||||
|
||||
+63
-98
@@ -1,110 +1,75 @@
|
||||
# Version control
|
||||
.git/
|
||||
# Node modules and build artifacts
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
frontend/build
|
||||
frontend/.vite
|
||||
frontend/.tauri
|
||||
frontend/src-tauri/target
|
||||
|
||||
# Gradle build artifacts
|
||||
.gradle
|
||||
build
|
||||
bin
|
||||
target
|
||||
out
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.git-blame-ignore-revs
|
||||
.gitattributes
|
||||
|
||||
# Build outputs
|
||||
build/
|
||||
*/build/
|
||||
**/build/
|
||||
out/
|
||||
target/
|
||||
**/target/
|
||||
bin/
|
||||
version_builds/
|
||||
|
||||
# Gradle caches (local, not what's in the container)
|
||||
.gradle/
|
||||
**/.gradle/
|
||||
.gradle-home/
|
||||
|
||||
# Task (go-task) cache
|
||||
.task/
|
||||
|
||||
# Node / frontend
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
frontend/node_modules/
|
||||
frontend/editor/dist/
|
||||
frontend/editor/playwright-report/
|
||||
.npm/
|
||||
.yarn/
|
||||
|
||||
# Tauri/desktop builds
|
||||
src-tauri/target/
|
||||
src-tauri/dist/
|
||||
frontend/editor/src-tauri/target/
|
||||
frontend/editor/src-tauri/dist/
|
||||
|
||||
# IDE and editor
|
||||
.idea/
|
||||
.vscode/
|
||||
.settings/
|
||||
.settings.zip
|
||||
.classpath
|
||||
.project
|
||||
.devcontainer/
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Logs and temp files
|
||||
# Logs
|
||||
*.log
|
||||
*.tmp
|
||||
*.pid
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
logs/
|
||||
logs
|
||||
|
||||
# Docker itself
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
|
||||
# CI / CD configs (not needed in build context)
|
||||
.github/
|
||||
.circleci/
|
||||
.gitlab-ci.yml
|
||||
|
||||
# Test reports
|
||||
**/test-results/
|
||||
**/jacoco/
|
||||
test_*.pdf
|
||||
|
||||
# Testing and documentation (not needed in build)
|
||||
testing/
|
||||
docs/
|
||||
devGuide/
|
||||
devTools/
|
||||
*.md
|
||||
README*
|
||||
|
||||
# Separate projects not consumed by the Java/frontend build
|
||||
commonforms-onnx/
|
||||
|
||||
# Runtime mount points used by docker-compose volumes, not build input
|
||||
stirling/
|
||||
customFiles/
|
||||
configs/
|
||||
|
||||
# Claude Code workspace
|
||||
.claude/
|
||||
|
||||
# Python caches
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
|
||||
# Local env
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!engine/.env
|
||||
|
||||
# Misc
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
# OS files
|
||||
.DS_Store
|
||||
.cache/
|
||||
Thumbs.db
|
||||
|
||||
# Java compiled files
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Test reports
|
||||
test-results
|
||||
coverage
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Runtime database and config files (locked by running app)
|
||||
app/core/configs/**
|
||||
stirling/**
|
||||
stirling-pdf-DB*.mv.db
|
||||
stirling-pdf-DB*.trace.db
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
docs
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
|
||||
+1
-2
@@ -14,8 +14,7 @@ indent_size = 4
|
||||
max_line_length = 100
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
max_line_length = 120
|
||||
indent_size = 2
|
||||
|
||||
[*.gradle]
|
||||
indent_size = 4
|
||||
|
||||
@@ -22,9 +22,9 @@ runs:
|
||||
steps:
|
||||
- name: Generate a GitHub App Token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
client-id: ${{ inputs.app-id }}
|
||||
app-id: ${{ inputs.app-id }}
|
||||
private-key: ${{ inputs.private-key }}
|
||||
- name: Configure Git
|
||||
run: |
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Maintainer: Stirling PDF Inc <contact@stirlingpdf.com>
|
||||
pkgname=stirling-pdf-desktop
|
||||
pkgver=2.14.2
|
||||
pkgrel=1
|
||||
pkgdesc="Locally hosted, web-based PDF manipulation tool (Tauri desktop app, official Stirling PDF Inc build)"
|
||||
arch=('x86_64')
|
||||
url="https://www.stirling.com"
|
||||
license=('MIT' 'LicenseRef-Stirling-PDF-Proprietary')
|
||||
depends=('gtk3' 'webkit2gtk-4.1' 'libappindicator-gtk3')
|
||||
provides=('stirling-pdf')
|
||||
conflicts=('stirling-pdf' 'stirling-pdf-git' 'stirling-pdf-bin')
|
||||
options=('!strip')
|
||||
|
||||
source_x86_64=("${pkgname}-${pkgver}.deb::https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${pkgver}/Stirling-PDF-linux-x86_64.deb")
|
||||
sha256sums_x86_64=('PLACEHOLDER_DEB_SHA256')
|
||||
|
||||
package() {
|
||||
# Extract the .deb archive
|
||||
bsdtar -xf data.tar* -C "${pkgdir}"
|
||||
|
||||
# Fix permissions
|
||||
find "${pkgdir}" -type d -exec chmod 755 {} \;
|
||||
|
||||
# Install license
|
||||
install -Dm644 /dev/stdin "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" <<EOF
|
||||
Copyright (c) 2025 Stirling PDF Inc
|
||||
All rights reserved. See https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LICENSE
|
||||
EOF
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
# Maintainer: Stirling PDF Inc <contact@stirlingpdf.com>
|
||||
pkgname=stirling-pdf-server-bin
|
||||
pkgver=2.14.2
|
||||
pkgrel=1
|
||||
pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)"
|
||||
arch=('any')
|
||||
url="https://www.stirling.com"
|
||||
license=('MIT' 'LicenseRef-Stirling-PDF-Proprietary')
|
||||
depends=('java-runtime>=25')
|
||||
provides=('stirling-pdf-server')
|
||||
conflicts=('stirling-pdf-server' 'stirling-pdf-server-git')
|
||||
backup=('etc/stirling-pdf-server/settings.yml')
|
||||
|
||||
source=("Stirling-PDF-with-login-${pkgver}.jar::https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${pkgver}/Stirling-PDF-with-login.jar")
|
||||
sha256sums=('PLACEHOLDER_JAR_SHA256')
|
||||
|
||||
package() {
|
||||
# JAR
|
||||
install -Dm644 "Stirling-PDF-with-login-${pkgver}.jar" \
|
||||
"${pkgdir}/usr/share/stirling-pdf-server/stirling-pdf-server.jar"
|
||||
|
||||
# Wrapper script
|
||||
install -Dm755 /dev/stdin "${pkgdir}/usr/bin/stirling-pdf-server" << 'EOF'
|
||||
#!/bin/sh
|
||||
exec java $JAVA_OPTS -jar /usr/share/stirling-pdf-server/stirling-pdf-server.jar "$@"
|
||||
EOF
|
||||
|
||||
# systemd unit
|
||||
install -Dm644 /dev/stdin "${pkgdir}/usr/lib/systemd/system/stirling-pdf-server.service" << 'EOF'
|
||||
[Unit]
|
||||
Description=Stirling-PDF Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=stirling-pdf
|
||||
Group=stirling-pdf
|
||||
WorkingDirectory=/var/lib/stirling-pdf-server
|
||||
ExecStart=/usr/bin/java -jar /usr/share/stirling-pdf-server/stirling-pdf-server.jar
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=stirling-pdf-server
|
||||
Environment=JAVA_OPTS=-Xmx512m
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# sysusers
|
||||
install -Dm644 /dev/stdin "${pkgdir}/usr/lib/sysusers.d/stirling-pdf-server.conf" << 'EOF'
|
||||
u stirling-pdf - "Stirling-PDF Server" /var/lib/stirling-pdf-server -
|
||||
EOF
|
||||
|
||||
# tmpfiles
|
||||
install -Dm644 /dev/stdin "${pkgdir}/usr/lib/tmpfiles.d/stirling-pdf-server.conf" << 'EOF'
|
||||
d /var/lib/stirling-pdf-server 0750 stirling-pdf stirling-pdf -
|
||||
d /var/log/stirling-pdf-server 0750 stirling-pdf stirling-pdf -
|
||||
EOF
|
||||
|
||||
# Default config stub
|
||||
install -dm755 "${pkgdir}/etc/stirling-pdf-server"
|
||||
install -Dm644 /dev/stdin "${pkgdir}/etc/stirling-pdf-server/settings.yml" << 'EOF'
|
||||
# Stirling-PDF Server configuration
|
||||
# See https://github.com/Stirling-Tools/Stirling-PDF for all options
|
||||
server:
|
||||
port: 8080
|
||||
EOF
|
||||
|
||||
# License
|
||||
install -Dm644 /dev/stdin "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" << 'EOF'
|
||||
MIT License with proprietary carve-outs (open-core).
|
||||
SPDX: MIT AND LicenseRef-Stirling-PDF-Proprietary
|
||||
See https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LICENSE
|
||||
EOF
|
||||
}
|
||||
@@ -1,26 +1,19 @@
|
||||
build: &build
|
||||
- build.gradle
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
- Taskfile.yml
|
||||
- .taskfiles/backend.yml
|
||||
|
||||
openapi: &openapi
|
||||
- *build
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
|
||||
docker-base: &docker-base
|
||||
- docker/base/Dockerfile
|
||||
|
||||
docker: &docker
|
||||
- docker/embedded/Dockerfile
|
||||
- docker/embedded/Dockerfile.fat
|
||||
- docker/embedded/Dockerfile.ultra-lite
|
||||
- Dockerfile
|
||||
- Dockerfile.fat
|
||||
- Dockerfile.ultra-lite
|
||||
- ".github/workflows/build.yml"
|
||||
- ".github/workflows/push-docker.yml"
|
||||
- scripts/init.sh
|
||||
- scripts/init-without-ocr.sh
|
||||
- exampleYmlFiles/**
|
||||
- *docker-base
|
||||
|
||||
project: &project
|
||||
- app/(common|core|proprietary)/src/(main|test)/java/**
|
||||
@@ -31,7 +24,6 @@ project: &project
|
||||
- libs/**
|
||||
- "testing/**/!(requirements*.txt|requirements*.in)*"
|
||||
- *docker
|
||||
- *docker-base
|
||||
- gradle.properties
|
||||
- gradlew
|
||||
- gradlew.bat
|
||||
@@ -40,11 +32,6 @@ project: &project
|
||||
- frontend/**
|
||||
- docker/**
|
||||
- scripts/RestartHelper.java
|
||||
- Taskfile.yml
|
||||
- .taskfiles/backend.yml
|
||||
- .taskfiles/docker.yml
|
||||
- scripts/db-migration/**
|
||||
- .github/workflows/db-migration-test.yml
|
||||
|
||||
frontend: &frontend
|
||||
- frontend/**
|
||||
@@ -52,7 +39,8 @@ frontend: &frontend
|
||||
- testing/**
|
||||
- docker/**
|
||||
- scripts/translations/*.py
|
||||
- .taskfiles/desktop.yml
|
||||
- scripts/build-tauri-jlink.bat
|
||||
- scripts/build-tauri-jlink.sh
|
||||
- scripts/convert_cff_to_ttf.py
|
||||
- scripts/harvest_type3_fonts.py
|
||||
- scripts/ignore_translation.toml
|
||||
@@ -60,73 +48,13 @@ frontend: &frontend
|
||||
- scripts/summarize_type3_signatures.py
|
||||
- scripts/type3_to_cff.py
|
||||
- scripts/update_type3_library.py
|
||||
- Taskfile.yml
|
||||
- .taskfiles/frontend.yml
|
||||
- .taskfiles/e2e.yml
|
||||
|
||||
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
|
||||
# job on changes to any of these.
|
||||
tauri: &tauri
|
||||
- frontend/editor/src-tauri/**
|
||||
- frontend/editor/src/desktop/**
|
||||
- frontend/editor/tsconfig.desktop.vite.json
|
||||
- frontend/package.json
|
||||
- frontend/package-lock.json
|
||||
- frontend/editor/vite.config.ts
|
||||
- .github/workflows/tauri-build.yml
|
||||
- Taskfile.yml
|
||||
- .taskfiles/desktop.yml
|
||||
|
||||
# Files that affect the AI engine (Python tool models, fixers, tests). Gate
|
||||
# the engine validation job on changes to engine sources or to the Java
|
||||
# tool surfaces it generates models from.
|
||||
engine: &engine
|
||||
- engine/**
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
- .github/workflows/ai-engine.yml
|
||||
- Taskfile.yml
|
||||
- .taskfiles/engine.yml
|
||||
|
||||
# Files that can make the committed generated API models (frontend tool API
|
||||
# types + engine tool models) go stale: the Java tool surfaces they derive from,
|
||||
# the generators, the generated files themselves (to catch a hand-edit), and the
|
||||
# tasks that drive generation. Deliberately excludes the broad frontend/docker/
|
||||
# testing globs, so a CSS-only PR does not boot the backend to rebuild the spec.
|
||||
generated-models: &generated-models
|
||||
- *openapi
|
||||
- frontend/editor/scripts/generate-tool-api-types.mts
|
||||
- frontend/editor/src/core/types/toolApiTypes.ts
|
||||
- engine/scripts/generate_tool_models.py
|
||||
- engine/src/stirling/models/tool_models.py
|
||||
- .taskfiles/frontend.yml
|
||||
- .taskfiles/engine.yml
|
||||
- .github/workflows/check-generated-models.yml
|
||||
|
||||
licenses-frontend: &licenses-frontend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/editor/scripts/generate-licenses.js"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
|
||||
licenses-backend: &licenses-backend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
- *build
|
||||
|
||||
# Files that can affect premium / enterprise behaviour. Gate the enterprise
|
||||
# Playwright job on changes to any of these on PRs.
|
||||
proprietary: &proprietary
|
||||
- app/proprietary/**
|
||||
- frontend/editor/src/proprietary/**
|
||||
- frontend/editor/src/core/tests/enterprise/**
|
||||
- testing/compose/docker-compose-keycloak-oauth.yml
|
||||
- testing/compose/docker-compose-keycloak-saml.yml
|
||||
- testing/compose/keycloak-realm-oauth.json
|
||||
- testing/compose/keycloak-realm-saml.json
|
||||
- testing/compose/start-oauth-test.sh
|
||||
- testing/compose/start-saml-test.sh
|
||||
- testing/compose/validate-oauth-test.sh
|
||||
- testing/compose/validate-saml-test.sh
|
||||
- configs/settings.yml.template
|
||||
- build.gradle
|
||||
- app/proprietary/build.gradle
|
||||
- .github/workflows/build-enterprise.yml
|
||||
- *build
|
||||
+27
-121
@@ -13,152 +13,58 @@ updates:
|
||||
- "/app/proprietary"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
- "/" # Location of Dockerfile
|
||||
- "/docker/backend"
|
||||
- "/docker/embedded"
|
||||
- "/docker/frontend"
|
||||
- "/docker/base"
|
||||
- "/docker/engine"
|
||||
- "/docker/unoserver"
|
||||
- "/engine"
|
||||
directory: "/" # Location of Dockerfile
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: npm
|
||||
directories:
|
||||
- /devTools
|
||||
- /frontend
|
||||
directory: /devTools
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /docker/backend
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /docker/embedded
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /docker/frontend
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
rebase-strategy: "auto"
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /frontend
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
groups:
|
||||
embedpdf:
|
||||
patterns:
|
||||
- "@embedpdf/*"
|
||||
mantine:
|
||||
patterns:
|
||||
- "@mantine/*"
|
||||
- "postcss-preset-mantine"
|
||||
mui:
|
||||
patterns:
|
||||
- "@mui/*"
|
||||
tauri-js:
|
||||
patterns:
|
||||
- "@tauri-apps/*"
|
||||
emotion:
|
||||
patterns:
|
||||
- "@emotion/*"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
typescript-eslint:
|
||||
patterns:
|
||||
- "@typescript-eslint/*"
|
||||
- "typescript-eslint"
|
||||
eslint:
|
||||
patterns:
|
||||
- "eslint"
|
||||
- "@eslint/*"
|
||||
vite:
|
||||
patterns:
|
||||
- "vite"
|
||||
- "vite-*"
|
||||
- "@vitejs/*"
|
||||
vitest:
|
||||
patterns:
|
||||
- "vitest"
|
||||
- "@vitest/*"
|
||||
testing-library:
|
||||
patterns:
|
||||
- "@testing-library/*"
|
||||
i18next:
|
||||
patterns:
|
||||
- "i18next"
|
||||
- "i18next-*"
|
||||
- "react-i18next"
|
||||
iconify:
|
||||
patterns:
|
||||
- "@iconify/*"
|
||||
- "@iconify-json/*"
|
||||
stripe:
|
||||
patterns:
|
||||
- "@stripe/*"
|
||||
posthog:
|
||||
patterns:
|
||||
- "@posthog/*"
|
||||
- "posthog-js"
|
||||
supabase:
|
||||
patterns:
|
||||
- "@supabase/*"
|
||||
dnd-kit:
|
||||
patterns:
|
||||
- "@dnd-kit/*"
|
||||
tailwind:
|
||||
patterns:
|
||||
- "tailwindcss"
|
||||
- "@tailwindcss/*"
|
||||
postcss:
|
||||
patterns:
|
||||
- "postcss"
|
||||
- "postcss-*"
|
||||
exclude-patterns:
|
||||
- "postcss-preset-mantine"
|
||||
|
||||
- package-ecosystem: cargo
|
||||
directories:
|
||||
- /frontend/editor/src-tauri
|
||||
- /frontend/editor/src-tauri/thumbnail-handler
|
||||
- /frontend/editor/src-tauri/provisioner
|
||||
directory: /frontend/src-tauri
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
groups:
|
||||
tauri:
|
||||
patterns:
|
||||
- "tauri"
|
||||
- "tauri-build"
|
||||
- "tauri-plugin-*"
|
||||
serde:
|
||||
patterns:
|
||||
- "serde"
|
||||
- "serde_*"
|
||||
tracing:
|
||||
patterns:
|
||||
- "tracing"
|
||||
- "tracing-*"
|
||||
tokio:
|
||||
patterns:
|
||||
- "tokio"
|
||||
- "tokio-*"
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /testing/cucumber
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
rebase-strategy: "auto"
|
||||
|
||||
@@ -51,7 +51,7 @@ labels:
|
||||
|
||||
- label: 'Translation'
|
||||
files:
|
||||
- 'frontend/editor/public/locales/[a-zA-Z]{2}-[a-zA-Z\-]{2,7}/translation.toml'
|
||||
- 'frontend/public/locales/[a-zA-Z]{2}-[a-zA-Z\-]{2,7}/translation.toml'
|
||||
- 'scripts/ignore_translation.toml'
|
||||
- 'scripts/remove_translation_keys.sh'
|
||||
- 'scripts/replace_translation_line.sh'
|
||||
@@ -63,20 +63,17 @@ labels:
|
||||
files:
|
||||
- 'app/core/src/main/resources/static/.*'
|
||||
- 'app/proprietary/src/main/resources/static/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
- 'frontend/**'
|
||||
- 'frontend/.*'
|
||||
- 'frontend/**/.*'
|
||||
|
||||
- label: 'Tauri'
|
||||
files:
|
||||
- 'frontend/editor/src-tauri/**'
|
||||
- 'frontend/editor/src-tauri/.*'
|
||||
|
||||
- label: 'engine'
|
||||
files:
|
||||
- 'engine/**'
|
||||
- 'engine/.*'
|
||||
- 'engine/**/.*'
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src-tauri/.*'
|
||||
|
||||
- label: 'Java'
|
||||
files:
|
||||
|
||||
@@ -17,7 +17,7 @@ Closes #(issue_number)
|
||||
### General
|
||||
|
||||
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable)
|
||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable)
|
||||
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable)
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] My changes generate no new warnings
|
||||
@@ -37,5 +37,4 @@ Closes #(issue_number)
|
||||
|
||||
### Testing (if applicable)
|
||||
|
||||
- [ ] I have run `task check` to verify linters, typechecks, and tests pass
|
||||
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details.
|
||||
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
|
||||
|
||||
+2
-15
@@ -1,11 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
# engine: Python AI engine - not currently live / not yet shipped to users.
|
||||
# Remove this entry once the engine is released.
|
||||
- engine
|
||||
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
@@ -13,22 +6,16 @@ changelog:
|
||||
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- Bugfix
|
||||
- Bug
|
||||
|
||||
- title: Enhancements
|
||||
labels:
|
||||
- enhancement
|
||||
- Java
|
||||
- Front End
|
||||
- Back End
|
||||
- Tauri
|
||||
|
||||
- title: Minor Enhancements
|
||||
labels:
|
||||
- chore
|
||||
- style
|
||||
- refactor
|
||||
- Java
|
||||
- Front End
|
||||
|
||||
- title: Docker Updates
|
||||
labels:
|
||||
|
||||
@@ -13,7 +13,7 @@ Usage:
|
||||
"""
|
||||
|
||||
# Sample for Windows:
|
||||
# python .github/scripts/check_language_toml.py --reference-file frontend/editor/public/locales/en-US/translation.toml --branch "" --files frontend/editor/public/locales/de-DE/translation.toml frontend/editor/public/locales/fr-FR/translation.toml
|
||||
# python .github/scripts/check_language_toml.py --reference-file frontend/public/locales/en-GB/translation.toml --branch "" --files frontend/public/locales/de-DE/translation.toml frontend/public/locales/fr-FR/translation.toml
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
@@ -59,8 +59,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for TOML files (e.g., 1 MB)
|
||||
MAX_FILE_SIZE = 1000 * 1024
|
||||
# Maximum size for TOML files (e.g., 570 KB)
|
||||
MAX_FILE_SIZE = 570 * 1024
|
||||
|
||||
|
||||
def parse_toml_file(file_path):
|
||||
@@ -184,7 +184,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
|
||||
base_dir = Path.cwd() / "frontend" / "editor" / "public" / "locales"
|
||||
base_dir = Path.cwd() / "frontend" / "public" / "locales"
|
||||
|
||||
for file_path in file_arr:
|
||||
file_path = Path(file_path)
|
||||
@@ -211,7 +211,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
)
|
||||
continue
|
||||
|
||||
if basename_current_file == basename_reference_file and locale_dir == "en-US":
|
||||
if basename_current_file == basename_reference_file and locale_dir == "en-GB":
|
||||
continue
|
||||
|
||||
if (
|
||||
@@ -259,27 +259,10 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
report.append(
|
||||
f" - **_Extra keys in `{locale_dir}/{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
report.append("")
|
||||
report.append(" Use the following command to remove them:")
|
||||
report.append(
|
||||
f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`"
|
||||
)
|
||||
report.append("")
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **_Missing keys in `{locale_dir}/{basename_current_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
report.append("")
|
||||
report.append(" Use the following command to add them:")
|
||||
report.append(
|
||||
f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`"
|
||||
)
|
||||
report.append("")
|
||||
|
||||
if missing_keys_list or extra_keys_list:
|
||||
report.append(
|
||||
" See: https://github.com/Stirling-Tools/Stirling-PDF/tree/main/scripts/translations#2-translation_mergerpy"
|
||||
)
|
||||
else:
|
||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
@@ -308,7 +291,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-US/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/editor/public/locales/en-US/translation.toml)"
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/public/locales/en-GB/translation.toml)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
@@ -372,7 +355,6 @@ if __name__ == "__main__":
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"frontend",
|
||||
"editor",
|
||||
"public",
|
||||
"locales",
|
||||
"*",
|
||||
|
||||
@@ -97,294 +97,276 @@ cffi==2.0.0 \
|
||||
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
||||
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||
# via weasyprint
|
||||
cfgv==3.5.0 \
|
||||
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
# via pre-commit
|
||||
cssselect2==0.9.0 \
|
||||
--hash=sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 \
|
||||
--hash=sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb
|
||||
cssselect2==0.8.0 \
|
||||
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
|
||||
--hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a
|
||||
# via weasyprint
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.29.0 \
|
||||
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
|
||||
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
fonttools==4.62.1 \
|
||||
--hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \
|
||||
--hash=sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a \
|
||||
--hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \
|
||||
--hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \
|
||||
--hash=sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82 \
|
||||
--hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \
|
||||
--hash=sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b \
|
||||
--hash=sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e \
|
||||
--hash=sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416 \
|
||||
--hash=sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae \
|
||||
--hash=sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069 \
|
||||
--hash=sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9 \
|
||||
--hash=sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 \
|
||||
--hash=sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed \
|
||||
--hash=sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800 \
|
||||
--hash=sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e \
|
||||
--hash=sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9 \
|
||||
--hash=sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b \
|
||||
--hash=sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 \
|
||||
--hash=sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe \
|
||||
--hash=sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7 \
|
||||
--hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \
|
||||
--hash=sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 \
|
||||
--hash=sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23 \
|
||||
--hash=sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae \
|
||||
--hash=sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260 \
|
||||
--hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \
|
||||
--hash=sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87 \
|
||||
--hash=sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24 \
|
||||
--hash=sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53 \
|
||||
--hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \
|
||||
--hash=sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14 \
|
||||
--hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \
|
||||
--hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \
|
||||
--hash=sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1 \
|
||||
--hash=sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca \
|
||||
--hash=sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c \
|
||||
--hash=sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d \
|
||||
--hash=sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a \
|
||||
--hash=sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782 \
|
||||
--hash=sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c \
|
||||
--hash=sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a \
|
||||
--hash=sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 \
|
||||
--hash=sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3 \
|
||||
--hash=sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7 \
|
||||
--hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d \
|
||||
--hash=sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 \
|
||||
--hash=sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4 \
|
||||
--hash=sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68 \
|
||||
--hash=sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
# via virtualenv
|
||||
fonttools==4.60.1 \
|
||||
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
|
||||
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
|
||||
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
|
||||
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
|
||||
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
|
||||
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
|
||||
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
|
||||
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
|
||||
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
|
||||
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
|
||||
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
|
||||
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
|
||||
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
|
||||
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
|
||||
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
|
||||
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
|
||||
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
|
||||
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
|
||||
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
|
||||
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
|
||||
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
|
||||
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
|
||||
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
|
||||
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
|
||||
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
|
||||
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
|
||||
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
|
||||
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
|
||||
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
|
||||
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
|
||||
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
|
||||
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
|
||||
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
|
||||
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
|
||||
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
|
||||
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
|
||||
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
|
||||
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
|
||||
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
|
||||
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
|
||||
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
|
||||
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
|
||||
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
|
||||
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
|
||||
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
|
||||
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
|
||||
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
|
||||
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
|
||||
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
|
||||
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
|
||||
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
|
||||
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
|
||||
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
|
||||
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
|
||||
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
|
||||
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
|
||||
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
|
||||
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
|
||||
# via weasyprint
|
||||
identify==2.6.19 \
|
||||
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
|
||||
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
nodeenv==1.10.0 \
|
||||
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
numpy==2.4.4 \
|
||||
--hash=sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed \
|
||||
--hash=sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50 \
|
||||
--hash=sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959 \
|
||||
--hash=sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827 \
|
||||
--hash=sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd \
|
||||
--hash=sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233 \
|
||||
--hash=sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc \
|
||||
--hash=sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b \
|
||||
--hash=sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7 \
|
||||
--hash=sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e \
|
||||
--hash=sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a \
|
||||
--hash=sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d \
|
||||
--hash=sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3 \
|
||||
--hash=sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e \
|
||||
--hash=sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb \
|
||||
--hash=sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a \
|
||||
--hash=sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0 \
|
||||
--hash=sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e \
|
||||
--hash=sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113 \
|
||||
--hash=sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103 \
|
||||
--hash=sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93 \
|
||||
--hash=sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af \
|
||||
--hash=sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5 \
|
||||
--hash=sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7 \
|
||||
--hash=sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392 \
|
||||
--hash=sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c \
|
||||
--hash=sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4 \
|
||||
--hash=sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40 \
|
||||
--hash=sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf \
|
||||
--hash=sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44 \
|
||||
--hash=sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b \
|
||||
--hash=sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5 \
|
||||
--hash=sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e \
|
||||
--hash=sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74 \
|
||||
--hash=sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0 \
|
||||
--hash=sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e \
|
||||
--hash=sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec \
|
||||
--hash=sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015 \
|
||||
--hash=sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d \
|
||||
--hash=sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d \
|
||||
--hash=sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842 \
|
||||
--hash=sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150 \
|
||||
--hash=sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8 \
|
||||
--hash=sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a \
|
||||
--hash=sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed \
|
||||
--hash=sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f \
|
||||
--hash=sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008 \
|
||||
--hash=sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e \
|
||||
--hash=sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0 \
|
||||
--hash=sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e \
|
||||
--hash=sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f \
|
||||
--hash=sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a \
|
||||
--hash=sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40 \
|
||||
--hash=sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7 \
|
||||
--hash=sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83 \
|
||||
--hash=sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d \
|
||||
--hash=sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c \
|
||||
--hash=sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871 \
|
||||
--hash=sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502 \
|
||||
--hash=sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252 \
|
||||
--hash=sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8 \
|
||||
--hash=sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115 \
|
||||
--hash=sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f \
|
||||
--hash=sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e \
|
||||
--hash=sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d \
|
||||
--hash=sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0 \
|
||||
--hash=sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119 \
|
||||
--hash=sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e \
|
||||
--hash=sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db \
|
||||
--hash=sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121 \
|
||||
--hash=sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d \
|
||||
--hash=sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e
|
||||
numpy==2.2.6 \
|
||||
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
|
||||
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
|
||||
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
|
||||
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
|
||||
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
|
||||
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
|
||||
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
|
||||
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
|
||||
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
|
||||
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
|
||||
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
|
||||
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
|
||||
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
|
||||
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
|
||||
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
|
||||
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
|
||||
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
|
||||
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
|
||||
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
|
||||
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
|
||||
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
|
||||
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
|
||||
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
|
||||
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
|
||||
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
|
||||
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
|
||||
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
|
||||
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
|
||||
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
|
||||
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
|
||||
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
|
||||
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
|
||||
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
|
||||
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
|
||||
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
|
||||
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
|
||||
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
|
||||
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
|
||||
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
|
||||
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
|
||||
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
|
||||
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
|
||||
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
|
||||
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
|
||||
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
|
||||
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
|
||||
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
|
||||
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
|
||||
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
|
||||
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
|
||||
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
|
||||
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
|
||||
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
|
||||
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
|
||||
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
|
||||
# via opencv-python-headless
|
||||
opencv-python-headless==4.13.0.92 \
|
||||
--hash=sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22 \
|
||||
--hash=sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e \
|
||||
--hash=sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209 \
|
||||
--hash=sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c \
|
||||
--hash=sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb \
|
||||
--hash=sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6 \
|
||||
--hash=sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b \
|
||||
--hash=sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d
|
||||
opencv-python-headless==4.12.0.88 \
|
||||
--hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \
|
||||
--hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \
|
||||
--hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \
|
||||
--hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \
|
||||
--hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \
|
||||
--hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \
|
||||
--hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pdf2image==1.17.0 \
|
||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pillow==12.2.0 \
|
||||
--hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \
|
||||
--hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \
|
||||
--hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \
|
||||
--hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \
|
||||
--hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \
|
||||
--hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \
|
||||
--hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \
|
||||
--hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \
|
||||
--hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \
|
||||
--hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \
|
||||
--hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \
|
||||
--hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \
|
||||
--hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \
|
||||
--hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \
|
||||
--hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \
|
||||
--hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \
|
||||
--hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \
|
||||
--hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \
|
||||
--hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \
|
||||
--hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \
|
||||
--hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \
|
||||
--hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \
|
||||
--hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \
|
||||
--hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \
|
||||
--hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \
|
||||
--hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \
|
||||
--hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \
|
||||
--hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \
|
||||
--hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \
|
||||
--hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \
|
||||
--hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \
|
||||
--hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \
|
||||
--hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \
|
||||
--hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \
|
||||
--hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \
|
||||
--hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \
|
||||
--hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \
|
||||
--hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \
|
||||
--hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \
|
||||
--hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \
|
||||
--hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \
|
||||
--hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \
|
||||
--hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \
|
||||
--hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \
|
||||
--hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \
|
||||
--hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \
|
||||
--hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \
|
||||
--hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \
|
||||
--hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \
|
||||
--hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \
|
||||
--hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \
|
||||
--hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \
|
||||
--hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \
|
||||
--hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \
|
||||
--hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \
|
||||
--hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \
|
||||
--hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \
|
||||
--hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \
|
||||
--hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \
|
||||
--hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \
|
||||
--hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \
|
||||
--hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \
|
||||
--hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \
|
||||
--hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \
|
||||
--hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \
|
||||
--hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \
|
||||
--hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \
|
||||
--hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \
|
||||
--hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \
|
||||
--hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \
|
||||
--hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \
|
||||
--hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \
|
||||
--hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \
|
||||
--hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \
|
||||
--hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \
|
||||
--hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \
|
||||
--hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \
|
||||
--hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \
|
||||
--hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \
|
||||
--hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \
|
||||
--hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \
|
||||
--hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \
|
||||
--hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \
|
||||
--hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \
|
||||
--hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \
|
||||
--hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \
|
||||
--hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \
|
||||
--hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \
|
||||
--hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \
|
||||
--hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \
|
||||
--hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5
|
||||
pillow==12.0.0 \
|
||||
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
|
||||
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
|
||||
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
|
||||
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
|
||||
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
|
||||
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
|
||||
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
|
||||
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
|
||||
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
|
||||
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
|
||||
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
|
||||
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
|
||||
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
|
||||
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
|
||||
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
|
||||
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
|
||||
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
|
||||
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
|
||||
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
|
||||
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
|
||||
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
|
||||
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
|
||||
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
|
||||
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
|
||||
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
|
||||
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
|
||||
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
|
||||
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
|
||||
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
|
||||
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
|
||||
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
|
||||
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
|
||||
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
|
||||
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
|
||||
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
|
||||
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
|
||||
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
|
||||
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
|
||||
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
|
||||
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
|
||||
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
|
||||
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
|
||||
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
|
||||
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
|
||||
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
|
||||
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
|
||||
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
|
||||
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
|
||||
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
|
||||
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
|
||||
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
|
||||
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
|
||||
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
|
||||
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
|
||||
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
|
||||
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
|
||||
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
|
||||
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
|
||||
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
|
||||
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
|
||||
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
|
||||
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
|
||||
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
|
||||
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
|
||||
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
|
||||
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
|
||||
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
|
||||
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
|
||||
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
|
||||
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
|
||||
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
|
||||
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
|
||||
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
|
||||
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
|
||||
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
|
||||
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
|
||||
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
|
||||
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
|
||||
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
|
||||
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
|
||||
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
|
||||
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
|
||||
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
|
||||
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
|
||||
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
|
||||
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
|
||||
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
|
||||
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
|
||||
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
|
||||
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
|
||||
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.9.6 \
|
||||
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
|
||||
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
pre-commit==4.6.0 \
|
||||
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
|
||||
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
# via virtualenv
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
pycparser==2.23 \
|
||||
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
|
||||
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
|
||||
# via cffi
|
||||
pydyf==0.12.1 \
|
||||
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
|
||||
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
|
||||
pydyf==0.11.0 \
|
||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||
# via weasyprint
|
||||
python-discovery==1.2.2 \
|
||||
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
|
||||
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
|
||||
# via virtualenv
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
@@ -460,27 +442,27 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
tinycss2==1.5.1 \
|
||||
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
|
||||
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
|
||||
tinycss2==1.4.0 \
|
||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
tinyhtml5==2.1.0 \
|
||||
--hash=sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67 \
|
||||
--hash=sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a
|
||||
tinyhtml5==2.0.0 \
|
||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||
# via weasyprint
|
||||
unoserver==3.6 \
|
||||
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
|
||||
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
|
||||
unoserver==3.4 \
|
||||
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
|
||||
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==21.2.4 \
|
||||
--hash=sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac \
|
||||
--hash=sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
# via pre-commit
|
||||
weasyprint==68.1 \
|
||||
--hash=sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be \
|
||||
--hash=sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e
|
||||
weasyprint==66.0 \
|
||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
@@ -489,27 +471,86 @@ webencodings==0.5.1 \
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
zopfli==0.4.1 \
|
||||
--hash=sha256:02086247dd12fda929f9bfe8b3962b6bcdbfc8c82e99255aebcf367867cf0760 \
|
||||
--hash=sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e \
|
||||
--hash=sha256:27823dc1161a4031d1c25925fd45d9868ec0cbc7692341830a7dcfa25063662c \
|
||||
--hash=sha256:2f992ac7d83cbddd889e1813ace576cbc91a05d5d7a0a21b366e2e5f492e7707 \
|
||||
--hash=sha256:4238d4d746d1095e29c9125490985e0c12ffd3654f54a24af551e2391e936d54 \
|
||||
--hash=sha256:5a4c22b6161f47f5bd34637dbaee6735abd287cd64e0d1ce28ef1871bf625f4b \
|
||||
--hash=sha256:84a31ba9edc921b1d3a4449929394a993888f32d70de3a3617800c428a947b9b \
|
||||
--hash=sha256:a899eca405662a23ae75054affa3517a060362eae1185d3d791c86a50153c4dd \
|
||||
--hash=sha256:a93c2ecafff372de6c0aa2212eff18a75f6c71a100372fee7b4b129cc0b6f9a7 \
|
||||
--hash=sha256:cb136a74d14a4ecfae29cb0fdecece58a6c115abc9a74c12bc6ac62e80f229d7 \
|
||||
--hash=sha256:d7bcee1b189d64ec33d1e05cfa1b6a1268c29329c382f6ca1bd6245b04925c57 \
|
||||
--hash=sha256:fdfb7ce9f5de37a5b2f75dd2642fd7717956ef2a72e0387302a36d382440db07
|
||||
zopfli==0.2.3.post1 \
|
||||
--hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \
|
||||
--hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \
|
||||
--hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \
|
||||
--hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \
|
||||
--hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \
|
||||
--hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \
|
||||
--hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \
|
||||
--hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \
|
||||
--hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \
|
||||
--hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \
|
||||
--hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \
|
||||
--hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \
|
||||
--hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \
|
||||
--hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \
|
||||
--hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \
|
||||
--hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \
|
||||
--hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \
|
||||
--hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \
|
||||
--hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \
|
||||
--hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \
|
||||
--hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \
|
||||
--hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \
|
||||
--hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \
|
||||
--hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \
|
||||
--hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \
|
||||
--hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \
|
||||
--hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \
|
||||
--hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \
|
||||
--hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \
|
||||
--hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \
|
||||
--hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \
|
||||
--hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \
|
||||
--hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \
|
||||
--hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \
|
||||
--hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \
|
||||
--hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \
|
||||
--hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \
|
||||
--hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \
|
||||
--hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \
|
||||
--hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \
|
||||
--hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \
|
||||
--hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \
|
||||
--hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \
|
||||
--hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \
|
||||
--hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \
|
||||
--hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \
|
||||
--hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \
|
||||
--hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \
|
||||
--hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \
|
||||
--hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \
|
||||
--hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \
|
||||
--hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \
|
||||
--hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \
|
||||
--hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \
|
||||
--hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \
|
||||
--hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \
|
||||
--hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \
|
||||
--hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \
|
||||
--hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \
|
||||
--hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \
|
||||
--hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \
|
||||
--hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \
|
||||
--hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \
|
||||
--hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \
|
||||
--hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \
|
||||
--hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \
|
||||
--hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \
|
||||
--hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \
|
||||
--hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \
|
||||
--hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \
|
||||
--hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==26.0.1 \
|
||||
--hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \
|
||||
--hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8
|
||||
pip==25.3 \
|
||||
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
|
||||
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==82.0.1 \
|
||||
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pre-commit
|
||||
@@ -0,0 +1,113 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
|
||||
#
|
||||
cfgv==3.5.0 \
|
||||
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||
# via pre-commit
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
# via virtualenv
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
# via virtualenv
|
||||
pre-commit==4.5.0 \
|
||||
--hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \
|
||||
--hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b
|
||||
# via -r .github/scripts/requirements_pre_commit.in
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
# via pre-commit
|
||||
@@ -8,7 +8,7 @@ tomli-w==1.2.0 \
|
||||
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
tomlkit==0.14.0 \
|
||||
--hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \
|
||||
--hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064
|
||||
tomlkit==0.13.3 \
|
||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify Tauri updater .sig files against plugins.updater.pubkey in tauri.conf.json.
|
||||
|
||||
Usage: verify-updater-signatures.py <dir-to-scan> [tauri.conf.json]
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
ART_ROOT = Path(sys.argv[1])
|
||||
CONF = Path(
|
||||
sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json"
|
||||
)
|
||||
|
||||
|
||||
def load_pubkey():
|
||||
# tauri pubkey = base64 of a minisign .pub file; last line is base64 of
|
||||
# [2 algo][8 key-id][32 ed25519 public key].
|
||||
raw = json.loads(CONF.read_text())["plugins"]["updater"]["pubkey"]
|
||||
blob = base64.b64decode(base64.b64decode(raw).decode().splitlines()[-1])
|
||||
return blob[2:10], Ed25519PublicKey.from_public_bytes(blob[10:])
|
||||
|
||||
|
||||
def hash_file(path: Path) -> bytes:
|
||||
h = hashlib.blake2b(digest_size=64)
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 16), b""):
|
||||
h.update(chunk)
|
||||
return h.digest()
|
||||
|
||||
|
||||
def verify(artifact: Path, sig_file: Path, keyid_pub, pub) -> str:
|
||||
# tauri .sig = base64 of a minisign signature file (4 lines).
|
||||
try:
|
||||
lines = base64.b64decode(sig_file.read_text()).decode().splitlines()
|
||||
sig_blob = base64.b64decode(lines[1])
|
||||
except (binascii.Error, IndexError, UnicodeDecodeError) as e:
|
||||
return f"FAIL malformed sig ({type(e).__name__})"
|
||||
algo, keyid, sig = sig_blob[:2], sig_blob[2:10], sig_blob[10:74]
|
||||
if keyid != keyid_pub:
|
||||
return f"FAIL key-id mismatch (sig {keyid.hex()} vs pub {keyid_pub.hex()})"
|
||||
# 'ED' = prehashed (BLAKE2b-512), 'Ed' = legacy (raw message).
|
||||
msg = hash_file(artifact) if algo == b"ED" else artifact.read_bytes()
|
||||
try:
|
||||
pub.verify(sig, msg)
|
||||
except InvalidSignature:
|
||||
return f"FAIL signature invalid (algo={algo.decode()})"
|
||||
# Global signature covers sig + trusted_comment.
|
||||
gc = "global-sig FAIL"
|
||||
try:
|
||||
tc = lines[2].split("trusted comment: ", 1)[1]
|
||||
pub.verify(base64.b64decode(lines[3]), sig + tc.encode())
|
||||
gc = "global-sig OK"
|
||||
except (InvalidSignature, IndexError, binascii.Error):
|
||||
pass
|
||||
return f"VALID (algo={algo.decode()}, keyid={keyid.hex()}, {gc})"
|
||||
|
||||
|
||||
keyid_pub, pub = load_pubkey()
|
||||
print(f"updater pubkey keyid={keyid_pub.hex()}\n")
|
||||
sigs = sorted(ART_ROOT.rglob("*.sig"))
|
||||
if not sigs:
|
||||
print(f"WARN: no .sig files under {ART_ROOT} - nothing to verify")
|
||||
sys.exit(0)
|
||||
bad = 0
|
||||
for sig_file in sigs:
|
||||
artifact = sig_file.with_suffix("")
|
||||
if not artifact.exists():
|
||||
print(f" ? {sig_file.name}: artifact missing")
|
||||
bad += 1
|
||||
continue
|
||||
res = verify(artifact, sig_file, keyid_pub, pub)
|
||||
print(f" {artifact.name}: {res}")
|
||||
if not res.startswith("VALID") or "global-sig FAIL" in res:
|
||||
bad += 1
|
||||
print(f"\n{'ALL SIGNATURES VALID' if bad == 0 else f'{bad} SIGNATURE(S) FAILED'}")
|
||||
sys.exit(1 if bad else 0)
|
||||
@@ -23,13 +23,9 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
check-pr:
|
||||
if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch'
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||
@@ -39,13 +35,13 @@ jobs:
|
||||
pr_ref: ${{ steps.resolve.outputs.ref }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Resolve PR info
|
||||
id: resolve
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
@@ -101,8 +97,8 @@ jobs:
|
||||
echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: [pick, check-pr]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
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:
|
||||
@@ -112,19 +108,15 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
@@ -140,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Add deployment started comment
|
||||
id: deployment-started
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -180,20 +172,15 @@ jobs:
|
||||
return newComment.id;
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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 Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -202,7 +189,7 @@ jobs:
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
@@ -237,27 +224,13 @@ jobs:
|
||||
echo "Image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && steps.check-image.outputs.exists == 'false'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push V2 image
|
||||
if: steps.check-image.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.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: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
@@ -287,13 +260,12 @@ jobs:
|
||||
- /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"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
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 }} - Embedded Architecture"
|
||||
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
@@ -310,7 +282,7 @@ jobs:
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||
# Create V2 PR-specific directories
|
||||
mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs,storage}
|
||||
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
|
||||
@@ -335,7 +307,7 @@ jobs:
|
||||
|
||||
- name: Post V2 deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -377,8 +349,7 @@ jobs:
|
||||
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -386,12 +357,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -403,7 +374,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Clean up V2 deployment comments
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -3,67 +3,34 @@ name: PR Deployment via Comment
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to deploy"
|
||||
required: true
|
||||
enable_prototypes:
|
||||
description: "Build with prototypes frontend"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
enable_pro:
|
||||
description: "Enable pro features"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
enable_enterprise:
|
||||
description: "Enable enterprise features"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
disable_security:
|
||||
description: "Disable security/login"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
check-comment:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
if: |
|
||||
vars.CI_PROFILE != 'lite' && (
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
)
|
||||
&&
|
||||
(
|
||||
github.event.comment.user.login == 'frooodle' ||
|
||||
github.event.comment.user.login == 'sf298' ||
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'balazs-szucs' ||
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
)
|
||||
vars.CI_PROFILE != 'lite' &&
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
)
|
||||
&&
|
||||
(
|
||||
github.event.comment.user.login == 'frooodle' ||
|
||||
github.event.comment.user.login == 'sf298' ||
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'balazs-szucs' ||
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
@@ -71,15 +38,14 @@ jobs:
|
||||
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
||||
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||
enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -92,12 +58,10 @@ jobs:
|
||||
|
||||
- name: Get PR data
|
||||
id: get-pr
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.eventName === 'workflow_dispatch'
|
||||
? context.payload.inputs.pr_number
|
||||
: context.payload.issue.number;
|
||||
const prNumber = context.payload.issue.number;
|
||||
console.log(`PR Number: ${prNumber}`);
|
||||
core.setOutput('pr_number', prNumber);
|
||||
|
||||
@@ -105,14 +69,12 @@ jobs:
|
||||
id: check-security-flag
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
DISPATCH_DISABLE_SECURITY: ${{ inputs.disable_security }}
|
||||
run: |
|
||||
if [[ "$IS_DISPATCH" == "true" ]]; then
|
||||
echo "disable_security=$DISPATCH_DISABLE_SECURITY" >> $GITHUB_OUTPUT
|
||||
elif [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
|
||||
if [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
|
||||
echo "Security flags detected in comment"
|
||||
echo "disable_security=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No security flags detected in comment"
|
||||
echo "disable_security=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -120,45 +82,24 @@ jobs:
|
||||
id: check-pro-flag
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
DISPATCH_PRO: ${{ inputs.enable_pro }}
|
||||
DISPATCH_ENTERPRISE: ${{ inputs.enable_enterprise }}
|
||||
run: |
|
||||
if [[ "$IS_DISPATCH" == "true" ]]; then
|
||||
echo "enable_pro=$DISPATCH_PRO" >> $GITHUB_OUTPUT
|
||||
echo "enable_enterprise=$DISPATCH_ENTERPRISE" >> $GITHUB_OUTPUT
|
||||
elif [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
|
||||
if [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
|
||||
echo "pro flags detected in comment"
|
||||
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
||||
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
||||
elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then
|
||||
echo "enterprise flags detected in comment"
|
||||
echo "enable_enterprise=true" >> $GITHUB_OUTPUT
|
||||
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No pro or enterprise flags detected in comment"
|
||||
echo "enable_pro=false" >> $GITHUB_OUTPUT
|
||||
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check for prototypes flag
|
||||
id: check-prototypes-flag
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
DISPATCH_PROTOTYPES: ${{ inputs.enable_prototypes }}
|
||||
run: |
|
||||
if [[ "$IS_DISPATCH" == "true" ]]; then
|
||||
echo "enable_prototypes=$DISPATCH_PROTOTYPES" >> $GITHUB_OUTPUT
|
||||
elif [[ "$COMMENT_BODY" == *"prototypes"* ]]; then
|
||||
echo "Prototypes flag detected in comment"
|
||||
echo "enable_prototypes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No prototypes flag detected in comment"
|
||||
echo "enable_prototypes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Add 'in_progress' reaction to comment
|
||||
if: github.event_name == 'issue_comment'
|
||||
id: add-eyes-reaction
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -179,24 +120,20 @@ jobs:
|
||||
}
|
||||
|
||||
deploy-pr:
|
||||
needs: [pick, check-comment]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -208,24 +145,17 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
||||
@@ -233,77 +163,27 @@ jobs:
|
||||
else
|
||||
export DISABLE_ADDITIONAL_FEATURES=false
|
||||
fi
|
||||
task backend:build
|
||||
./gradlew clean build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push PR-specific image (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: |
|
||||
VERSION_TAG=alpha
|
||||
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push PR-specific image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push PR-specific image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.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: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: |
|
||||
VERSION_TAG=alpha
|
||||
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push engine image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: ./engine
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push engine image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && needs.check-comment.outputs.enable_prototypes == 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: ./engine
|
||||
file: ./engine/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-engine
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-engine
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: VERSION_TAG=alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
@@ -341,64 +221,33 @@ jobs:
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="false"
|
||||
fi
|
||||
|
||||
ENABLE_PROTOTYPES="${{ needs.check-comment.outputs.enable_prototypes }}"
|
||||
PR_NUMBER="${{ needs.check-comment.outputs.pr_number }}"
|
||||
DOCKER_USER="${{ secrets.DOCKER_HUB_USERNAME }}"
|
||||
|
||||
# Build engine env vars for backend (only set when prototypes enabled)
|
||||
if [ "$ENABLE_PROTOTYPES" == "true" ]; then
|
||||
AI_ENGINE_VARS="
|
||||
SYSTEM_AIENGINE_ENABLED: \"true\"
|
||||
SYSTEM_AIENGINE_URL: \"http://stirling-pdf-engine-pr-${PR_NUMBER}:5001\""
|
||||
ENGINE_SERVICE="
|
||||
stirling-pdf-engine:
|
||||
container_name: stirling-pdf-engine-pr-${PR_NUMBER}
|
||||
image: ${DOCKER_USER}/test:engine-pr-${PR_NUMBER}
|
||||
environment:
|
||||
ANTHROPIC_API_KEY: \"${{ secrets.ANTHROPIC_API_KEY }}\"
|
||||
networks:
|
||||
- pr-network
|
||||
restart: on-failure:5"
|
||||
NETWORK_SECTION="
|
||||
networks:
|
||||
pr-network:"
|
||||
BACKEND_NETWORK="
|
||||
networks:
|
||||
- pr-network"
|
||||
else
|
||||
AI_ENGINE_VARS=""
|
||||
ENGINE_SERVICE=""
|
||||
NETWORK_SECTION=""
|
||||
BACKEND_NETWORK=""
|
||||
fi
|
||||
|
||||
# First create the docker-compose content locally
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: stirling-pdf-pr-${PR_NUMBER}
|
||||
image: ${DOCKER_USER}/test:pr-${PR_NUMBER}
|
||||
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
ports:
|
||||
- "${PR_NUMBER}:8080"
|
||||
- "${{ needs.check-comment.outputs.pr_number }}:8080"
|
||||
volumes:
|
||||
- /stirling/PR-${PR_NUMBER}/data:/usr/share/tessdata:rw
|
||||
- /stirling/PR-${PR_NUMBER}/config:/configs:rw
|
||||
- /stirling/PR-${PR_NUMBER}/logs:/logs:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
|
||||
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: "Stirling-PDF PR#${PR_NUMBER}"
|
||||
UI_HOMEDESCRIPTION: "PR#${PR_NUMBER} for Stirling-PDF Latest"
|
||||
UI_APPNAMENAVBAR: "PR#${PR_NUMBER}"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
|
||||
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
PREMIUM_KEY: "${PREMIUM_KEY}"
|
||||
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"${AI_ENGINE_VARS}
|
||||
restart: on-failure:5${BACKEND_NETWORK}${ENGINE_SERVICE}${NETWORK_SECTION}
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Then copy the file and execute commands
|
||||
@@ -406,13 +255,13 @@ jobs:
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||
# Create PR-specific directories
|
||||
mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs}
|
||||
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose.yml /stirling/PR-${PR_NUMBER}/docker-compose.yml
|
||||
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Start or restart the container
|
||||
cd /stirling/PR-${PR_NUMBER}
|
||||
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
ENDSSH
|
||||
@@ -421,8 +270,8 @@ jobs:
|
||||
echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV
|
||||
|
||||
- name: Add success reaction to comment
|
||||
if: success() && github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
if: success()
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -456,8 +305,8 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Add failure reaction to comment
|
||||
if: failure() && github.event_name == 'issue_comment'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
if: failure()
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -477,7 +326,7 @@ jobs:
|
||||
|
||||
- name: Post deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -510,16 +359,15 @@ jobs:
|
||||
|
||||
handle-label-commands:
|
||||
if: ${{ github.event.issue.pull_request != null }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -529,7 +377,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Apply label commands
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Remove 'pr-deployed' label if present
|
||||
id: remove-label-comment
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -121,9 +121,8 @@ jobs:
|
||||
# Remove PR-specific directories
|
||||
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Remove the Docker images
|
||||
# Remove the Docker image
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "PERFORMED_CLEANUP"
|
||||
else
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
name: _runner-pick
|
||||
|
||||
# Tiny reusable workflow that classifies the trigger as either a "fork PR
|
||||
# from an untrusted contributor" or a "trusted commit" so downstream jobs
|
||||
# can pick a runner class without each one duplicating the 200-char gate
|
||||
# expression in their own `runs-on:`.
|
||||
#
|
||||
# Caller pattern:
|
||||
#
|
||||
# jobs:
|
||||
# pick:
|
||||
# uses: ./.github/workflows/_runner-pick.yml
|
||||
#
|
||||
# real-work:
|
||||
# needs: pick
|
||||
# runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
# steps: [...]
|
||||
#
|
||||
# Output:
|
||||
# is_fork: "true" when the trigger is a pull_request from a fork or an
|
||||
# untrusted author_association, "false" otherwise.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
is_fork:
|
||||
description: '"true" if the trigger is an untrusted fork PR.'
|
||||
value: ${{ jobs.pick.outputs.is_fork }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
outputs:
|
||||
is_fork: ${{ steps.decide.outputs.is_fork }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Classify the trigger
|
||||
id: decide
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_REPO_FORK: ${{ github.event.pull_request.head.repo.fork }}
|
||||
AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ -z "${PR_NUMBER:-}" ]; then
|
||||
# Not a pull_request event at all (push, schedule, workflow_dispatch,
|
||||
# workflow_call from a non-PR trigger) -> trusted by default.
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ "${HEAD_REPO_FORK}" = "true" ]; then
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
case "${AUTHOR_ASSOC}" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "is_fork=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "is_fork=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
@@ -1,119 +0,0 @@
|
||||
name: AI Engine CI
|
||||
|
||||
# Runs the engine quality gate (lint, type-check, format-check, tests). Called
|
||||
# from build.yml on PRs and merge_group; also runs directly on push to main as
|
||||
# a post-merge safety net. Freshness of the generated tool_models.py is checked
|
||||
# by the shared check-generated-models workflow.
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
engine:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Quality-check engine
|
||||
id: engine-check
|
||||
run: task engine:check
|
||||
continue-on-error: true
|
||||
|
||||
- name: Comment on engine check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
# issue/PR number, which doesn't exist on merge_group runs.
|
||||
if: steps.engine-check.outcome == 'failure' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- engine-check -->';
|
||||
const body = [
|
||||
marker,
|
||||
'### Engine Check Failed',
|
||||
'',
|
||||
'There are issues with your Python code that will need to be fixed before they can be merged in.',
|
||||
'',
|
||||
'Run `task engine:fix` to auto-fix what can be fixed automatically, then run `task engine:check` to see what still needs fixing manually.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail if engine check failed
|
||||
if: steps.engine-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Engine Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "There are issues with your Python code that"
|
||||
echo "will need to be fixed before they can be merged in."
|
||||
echo ""
|
||||
echo "Run 'task engine:fix' to auto-fix what can be"
|
||||
echo "fixed automatically, then run 'task engine:check'"
|
||||
echo "to see what still needs fixing manually."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Remove engine check comment on success
|
||||
if: steps.engine-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- engine-check -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: AI PR Title Analysis
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: ai-title-analysis
|
||||
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt-file: ".github/config/system-prompt.txt"
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Post comment on PR if needed
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
name: Publish to AUR
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 2.9.2 — no v prefix)"
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Skip the AUR push (safe test)"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
get-release-info:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.info.outputs.version }}
|
||||
deb_sha256: ${{ steps.hashes.outputs.deb_sha256 }}
|
||||
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Extract version from tag or manual input
|
||||
id: info
|
||||
env:
|
||||
DISPATCH_VERSION: ${{ inputs.version }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION="$DISPATCH_VERSION"
|
||||
else
|
||||
VERSION="$RELEASE_TAG"
|
||||
fi
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download release assets and compute SHA256
|
||||
id: hashes
|
||||
env:
|
||||
VERSION: ${{ steps.info.outputs.version }}
|
||||
run: |
|
||||
BASE="https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${VERSION}"
|
||||
|
||||
download_sha256() {
|
||||
local url="$1"
|
||||
local file
|
||||
file=$(basename "$url")
|
||||
curl -fsSL --retry 3 -o "$file" "$url"
|
||||
sha256sum "$file" | awk '{print $1}'
|
||||
}
|
||||
|
||||
DEB_SHA=$(download_sha256 "${BASE}/Stirling-PDF-linux-x86_64.deb")
|
||||
JAR_SHA=$(download_sha256 "${BASE}/Stirling-PDF-with-login.jar")
|
||||
|
||||
echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
publish-aur:
|
||||
needs: get-release-info
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository (for PKGBUILD templates)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Update stirling-pdf-desktop PKGBUILD
|
||||
env:
|
||||
VERSION: ${{ needs.get-release-info.outputs.version }}
|
||||
DEB_SHA: ${{ needs.get-release-info.outputs.deb_sha256 }}
|
||||
run: |
|
||||
PKGBUILD=".github/aur/stirling-pdf-desktop/PKGBUILD"
|
||||
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
|
||||
sed -i "s/'PLACEHOLDER_DEB_SHA256'/'${DEB_SHA}'/" "$PKGBUILD"
|
||||
|
||||
# Disabled until we sort out the server packaging story.
|
||||
# The third-party stirling-pdf-bin on AUR currently covers a similar use case.
|
||||
# - name: Update stirling-pdf-server-bin PKGBUILD
|
||||
# env:
|
||||
# VERSION: ${{ needs.get-release-info.outputs.version }}
|
||||
# JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
|
||||
# run: |
|
||||
# PKGBUILD=".github/aur/stirling-pdf-server-bin/PKGBUILD"
|
||||
# sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
|
||||
# sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
|
||||
# sed -i "s/'PLACEHOLDER_JAR_SHA256'/'${JAR_SHA}'/" "$PKGBUILD"
|
||||
|
||||
- name: Show updated PKGBUILD (for dry-run visibility)
|
||||
run: |
|
||||
echo "--- stirling-pdf-desktop PKGBUILD ---"
|
||||
cat .github/aur/stirling-pdf-desktop/PKGBUILD
|
||||
|
||||
- name: Publish stirling-pdf-desktop to AUR
|
||||
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
|
||||
uses: KSXGitHub/github-actions-deploy-aur@da03e160361ce01bf087e790b6ffd196d7dccff7 # v4.1.3
|
||||
with:
|
||||
pkgname: stirling-pdf-desktop
|
||||
pkgbuild: .github/aur/stirling-pdf-desktop/PKGBUILD
|
||||
commit_username: Stirling PDF Inc
|
||||
commit_email: contact@stirlingpdf.com
|
||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
commit_message: "Update to v${{ needs.get-release-info.outputs.version }}"
|
||||
|
||||
# Disabled until we sort out the server packaging story.
|
||||
# - name: Publish stirling-pdf-server-bin to AUR
|
||||
# if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
|
||||
# uses: KSXGitHub/github-actions-deploy-aur@2ac5a4c1d7035885d46b10e3193393be8460b6f1 # v4.1.1
|
||||
# with:
|
||||
# pkgname: stirling-pdf-server-bin
|
||||
# pkgbuild: .github/aur/stirling-pdf-server-bin/PKGBUILD
|
||||
# commit_username: Stirling PDF Inc
|
||||
# commit_email: contact@stirlingpdf.com
|
||||
# ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
# commit_message: "Update to v${{ needs.get-release-info.outputs.version }}"
|
||||
@@ -16,11 +16,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0
|
||||
- uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
|
||||
with:
|
||||
config_path: .github/labeler-config-srvaroa.yml
|
||||
use_local_config: false
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
name: Backend build, format check, and coverage
|
||||
|
||||
# Reusable workflow called from build.yml. Runs the backend build matrix
|
||||
# (JDK 25 × every flavor), Spotless formatting check, JUnit, and
|
||||
# posts Jacoco coverage to PRs.
|
||||
#
|
||||
# Flavor axis (maps to STIRLING_FLAVOR in settings.gradle):
|
||||
# core - DISABLE_ADDITIONAL_FEATURES=true, no proprietary, no saas
|
||||
# proprietary - default build, no saas
|
||||
# saas - proprietary + the saas subproject (build + JUnit only,
|
||||
# never any runtime/integration testing)
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
build:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [25]
|
||||
flavor: [core, proprietary, saas]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Check Java formatting (Spotless)
|
||||
# Runs once per matrix combination - pick the cheapest leg
|
||||
# (core - no proprietary, no saas) so we don't wait for the
|
||||
# heavier flavors just to fail formatting.
|
||||
if: matrix.jdk-version == 25 && matrix.flavor == 'core'
|
||||
id: spotless-check
|
||||
run: task backend:format:check
|
||||
continue-on-error: true
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Comment on backend format check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
# issue/PR number, which doesn't exist on merge_group runs.
|
||||
if: steps.spotless-check.outcome == 'failure' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- java-formatting-check -->';
|
||||
const body = [
|
||||
marker,
|
||||
'### Backend Format Check Failed',
|
||||
'',
|
||||
'There are formatting issues in your Java code that will need to be fixed before they can be merged in.',
|
||||
'',
|
||||
'Run `task backend:format` to auto-fix, then commit and push the changes.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail if backend format check failed
|
||||
if: steps.spotless-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Backend Format Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "There are formatting issues in your Java code"
|
||||
echo "that will need to be fixed before they can be"
|
||||
echo "merged in."
|
||||
echo ""
|
||||
echo "Run 'task backend:format' to auto-fix, then"
|
||||
echo "commit and push the changes."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Remove backend format check comment on success
|
||||
if: steps.spotless-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- java-formatting-check -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Build with Gradle (flavor=${{ matrix.flavor }})
|
||||
# STIRLING_FLAVOR is read by settings.gradle and expands into the
|
||||
# right combination of DISABLE_ADDITIONAL_FEATURES + ENABLE_SAAS
|
||||
# so we don't have to set them by hand. The saas flavor pulls in
|
||||
# the app/saas subproject (unit tests only - no runtime tests).
|
||||
run: task backend:build:ci
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
STIRLING_FLAVOR: ${{ matrix.flavor }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
if: always()
|
||||
run: |
|
||||
# Common + core + proprietary always build (proprietary is
|
||||
# excluded only at runtime, not from the gradle subproject
|
||||
# graph). Saas builds add a fourth report dir.
|
||||
declare -a dirs=(
|
||||
"app/core/build/reports/tests/"
|
||||
"app/core/build/test-results/"
|
||||
"app/common/build/reports/tests/"
|
||||
"app/common/build/test-results/"
|
||||
"app/proprietary/build/reports/tests/"
|
||||
"app/proprietary/build/test-results/"
|
||||
)
|
||||
if [ "${{ matrix.flavor }}" = "saas" ]; then
|
||||
dirs+=("app/saas/build/reports/tests/" "app/saas/build/test-results/")
|
||||
fi
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Missing $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-flavor-${{ matrix.flavor }}
|
||||
path: |
|
||||
app/**/build/reports/jacoco/test
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py parses JaCoCo XML through defusedxml to
|
||||
# silence security scanners that pattern-match on the stdlib
|
||||
# xml.etree.ElementTree.parse call.
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: JaCoCo coverage step summary
|
||||
# Only the saas leg posts the JUnit summary - it's a strict
|
||||
# superset of the core + proprietary legs (same .exec files plus
|
||||
# the saas subproject). Posting from all three would mean three
|
||||
# near-identical tables crowding out the aggregate report.
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Backend JUnit coverage (JDK ${{ matrix.jdk-version }})" \
|
||||
--jacoco "common=app/common/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
--jacoco "core=app/core/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
--jacoco "proprietary=app/proprietary/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
--jacoco "saas=app/saas/build/reports/jacoco/test/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
|
||||
- name: Upload raw JUnit .exec for aggregate merge
|
||||
# Same dedup rationale as the summary step: upload from the saas
|
||||
# leg only (the most complete set, includes app/saas/.../test.exec)
|
||||
# so the aggregate workflow merges the union rather than three
|
||||
# overlapping subsets.
|
||||
#
|
||||
# Separate artifact from the HTML reports so the aggregate
|
||||
# workflow can grab just the .exec files with a name pattern
|
||||
# (`jacoco-exec-*`) instead of unpacking the whole test-reports
|
||||
# tarball.
|
||||
if: always() && matrix.flavor == 'saas'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-exec-junit-jdk-${{ matrix.jdk-version }}
|
||||
path: app/*/build/jacoco/*.exec
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Add coverage to PR (flavor=${{ matrix.flavor }}, JDK=${{ matrix.jdk-version }})
|
||||
# The action only supports the pull_request event (it posts a PR comment),
|
||||
# so skip it for merge_group runs and workflow_dispatch.
|
||||
if: github.event_name == 'pull_request'
|
||||
id: jacoco
|
||||
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
|
||||
with:
|
||||
paths: |
|
||||
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
min-coverage-overall: 10
|
||||
min-coverage-changed-files: 0
|
||||
comment-type: summary
|
||||
@@ -1,310 +0,0 @@
|
||||
name: Enterprise E2E (Playwright)
|
||||
|
||||
# Enterprise Playwright suite — exercises premium-key gated features (audit,
|
||||
# teams, analytics) plus full OAuth + SAML logins via the Keycloak compose
|
||||
# stacks under testing/compose. Slow and secret-gated, so it runs in four
|
||||
# situations:
|
||||
#
|
||||
# - PRs that touch proprietary / premium / SSO compose / enterprise tests
|
||||
# (driven by build.yml via workflow_call, path-filtered against
|
||||
# .github/config/.files.yaml `proprietary`),
|
||||
# - every push to main (post-merge safety net),
|
||||
# - on a nightly cron schedule (catches Keycloak image drift, license
|
||||
# expiry, upstream proprietary changes),
|
||||
# - manual workflow_dispatch.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
push:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
# No `concurrency:` block here on purpose. When this workflow is called via
|
||||
# workflow_call from build.yml, ${{ github.workflow }}/event_name/pr_number
|
||||
# resolve to the *caller's* values, producing the same group key as build.yml
|
||||
# and causing the workflow_call instantiation to self-cancel — the job
|
||||
# silently fails to spawn while `${{ needs.X.result }}` still reports
|
||||
# `failure`. Standalone runs (push-to-main, nightly cron, dispatch) don't
|
||||
# overlap often enough to need explicit concurrency control.
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
needs: pick
|
||||
# Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE
|
||||
# (nor DEPOT_TOKEN), so the suite can't boot premium and would fail. See the
|
||||
# header comment. GitHub reports the skipped reusable workflow as success.
|
||||
if: needs.pick.outputs.is_fork != 'true'
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
|
||||
PREMIUM_ENABLED: "true"
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (needed for playwright's vite preview webServer)
|
||||
# Enterprise tests target :8080 (Spring Boot's bundled frontend), but
|
||||
# playwright's webServer config still launches `vite preview --port 5173`
|
||||
# before any test run, which needs dist/ to exist. VITE_BUILD_FOR_PREVIEW
|
||||
# forces absolute asset paths so vite preview can serve deep SPA routes.
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
|
||||
- name: Resolve kubernetes.docker.internal to localhost
|
||||
# The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so
|
||||
# Keycloak issues redirect URIs against that host. Docker Desktop
|
||||
# auto-resolves it; GHA runners don't. Map it to 127.0.0.1 so the
|
||||
# browser-driven OAuth flow lands back on Stirling-PDF correctly.
|
||||
run: |
|
||||
echo "127.0.0.1 kubernetes.docker.internal" | sudo tee -a /etc/hosts
|
||||
|
||||
# Helper function used by all phases — boots `:stirling-pdf:bootRun`
|
||||
# with the React frontend baked in (-PbuildWithFrontend=true) so the
|
||||
# SPA serves on :8080 and OAuth/SAML callbacks land on the same host
|
||||
# that the browser is interacting with.
|
||||
- name: Define helpers
|
||||
run: |
|
||||
{
|
||||
echo 'wait_for_backend() {'
|
||||
echo ' start=$SECONDS'
|
||||
echo ' for i in $(seq 1 300); do'
|
||||
echo ' if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then'
|
||||
echo ' echo "Backend up after $((SECONDS - start))s"; return 0'
|
||||
echo ' fi; sleep 2'
|
||||
echo ' done'
|
||||
echo ' tail -200 /tmp/backend.log || true; return 1'
|
||||
echo '}'
|
||||
echo 'stop_backend() {'
|
||||
echo ' if [ -f /tmp/backend.pid ]; then'
|
||||
echo ' kill "$(cat /tmp/backend.pid)" 2>/dev/null || true'
|
||||
echo ' rm -f /tmp/backend.pid'
|
||||
echo ' fi'
|
||||
echo ' pkill -f "gradlew :stirling-pdf:bootRun" 2>/dev/null || true'
|
||||
echo ' for i in $(seq 1 30); do'
|
||||
echo ' curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1 || return 0'
|
||||
echo ' sleep 1'
|
||||
echo ' done'
|
||||
echo '}'
|
||||
} > /tmp/helpers.sh
|
||||
chmod +x /tmp/helpers.sh
|
||||
|
||||
# ───────── OAuth round-trip ─────────
|
||||
- name: Bring up Keycloak (OAuth realm)
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f docker-compose-keycloak-oauth.yml up -d --no-deps keycloak-oauth-db keycloak-oauth
|
||||
- name: Wait for Keycloak (OAuth) ready
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
for i in $(seq 1 60); do
|
||||
bash validate-oauth-test.sh 2>/dev/null && exit 0 || true
|
||||
# validate script also pings stirling on :8080 — accept just the
|
||||
# keycloak realm as our gate here, stirling boots in the next step
|
||||
curl -fsS http://localhost:9080/realms/stirling-oauth >/dev/null 2>&1 && exit 0
|
||||
sleep 5
|
||||
done
|
||||
docker compose -f docker-compose-keycloak-oauth.yml logs --tail=200 keycloak-oauth
|
||||
exit 1
|
||||
- name: Boot Stirling-PDF (frontend baked in, OAuth env)
|
||||
env:
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_LOGINMETHOD: "all"
|
||||
SECURITY_OAUTH2_ENABLED: "true"
|
||||
SECURITY_OAUTH2_AUTOCREATEUSER: "true"
|
||||
# Keycloak issues redirect URIs against KC_HOSTNAME, which the
|
||||
# compose default sets to kubernetes.docker.internal. Match here
|
||||
# (resolves to localhost via /etc/hosts mapping above).
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_ISSUER: "http://kubernetes.docker.internal:9080/realms/stirling-oauth"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTID: "stirling-pdf-client"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTSECRET: "test-client-secret-change-in-production"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_USEASUSERNAME: "email"
|
||||
SECURITY_OAUTH2_CLIENT_KEYCLOAK_SCOPES: "openid,profile,email"
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
wait_for_backend
|
||||
- name: Run enterprise OAuth Playwright tests
|
||||
id: oauth-tests
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results-oauth.json
|
||||
run: task e2e:enterprise -- --grep "OAuth"
|
||||
- name: Stop backend + tear down OAuth Keycloak
|
||||
if: always()
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
(cd testing/compose && docker compose -f docker-compose-keycloak-oauth.yml down -v)
|
||||
|
||||
# ───────── SAML round-trip ─────────
|
||||
- name: Bring up Keycloak (SAML realm)
|
||||
working-directory: testing/compose
|
||||
run: docker compose -f docker-compose-keycloak-saml.yml up -d --no-deps keycloak-saml-db keycloak-saml
|
||||
- name: Wait for Keycloak (SAML) ready
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
for i in $(seq 1 60); do
|
||||
curl -fsS http://localhost:9080/realms/stirling-saml >/dev/null 2>&1 && exit 0
|
||||
sleep 5
|
||||
done
|
||||
docker compose -f docker-compose-keycloak-saml.yml logs --tail=200 keycloak-saml
|
||||
exit 1
|
||||
- name: Generate SAML SP certs + fetch Keycloak IdP cert
|
||||
# The .pem/.crt/.key files are gitignored (test-only certs); the
|
||||
# docker-based start-saml-test.sh generates them at runtime, so do
|
||||
# the same in CI before bootRun reads them.
|
||||
working-directory: testing/compose
|
||||
run: |
|
||||
openssl req -x509 -newkey rsa:2048 \
|
||||
-keyout saml-private-key.key \
|
||||
-out saml-public-cert.crt \
|
||||
-days 3650 -nodes \
|
||||
-subj "/CN=stirling-pdf-saml-sp" >/dev/null 2>&1
|
||||
# Fetch Keycloak's SAML signing cert from the realm descriptor
|
||||
CERT_BODY=$(curl -sf http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor \
|
||||
| awk 'BEGIN{RS="<[^>]*X509Certificate>|</[^>]*X509Certificate>"} NR==2{gsub(/[[:space:]]+/,""); print; exit}')
|
||||
{
|
||||
echo "-----BEGIN CERTIFICATE-----"
|
||||
echo "$CERT_BODY"
|
||||
echo "-----END CERTIFICATE-----"
|
||||
} > keycloak-saml-cert.pem
|
||||
test -s saml-private-key.key
|
||||
test -s saml-public-cert.crt
|
||||
test -s keycloak-saml-cert.pem
|
||||
echo "✓ SAML certs prepared"
|
||||
- name: Boot Stirling-PDF (frontend baked in, SAML env)
|
||||
env:
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_LOGINMETHOD: "all"
|
||||
SECURITY_SAML2_ENABLED: "true"
|
||||
SECURITY_SAML2_AUTOCREATEUSER: "true"
|
||||
SECURITY_SAML2_PROVIDER: "keycloak"
|
||||
SECURITY_SAML2_REGISTRATIONID: "keycloak"
|
||||
SECURITY_SAML2_IDP_ISSUER: "http://localhost:9080/realms/stirling-saml"
|
||||
SECURITY_SAML2_IDP_ENTITYID: "http://localhost:9080/realms/stirling-saml"
|
||||
SECURITY_SAML2_IDP_METADATAURI: "http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor"
|
||||
SECURITY_SAML2_IDPSINGLELOGINURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
|
||||
SECURITY_SAML2_IDPSINGLELOGOUTURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
|
||||
SECURITY_SAML2_IDP_CERT: "${{ github.workspace }}/testing/compose/keycloak-saml-cert.pem"
|
||||
SECURITY_SAML2_PRIVATEKEY: "${{ github.workspace }}/testing/compose/saml-private-key.key"
|
||||
SECURITY_SAML2_SP_CERT: "${{ github.workspace }}/testing/compose/saml-public-cert.crt"
|
||||
# Realm registers the SP entity as the metadata URL — see
|
||||
# keycloak-realm-saml.json `clientId`. Match it here so Keycloak
|
||||
# accepts the AuthnRequest issuer.
|
||||
SECURITY_SAML2_SP_ENTITYID: "http://localhost:8080/saml2/service-provider-metadata/keycloak"
|
||||
SECURITY_SAML2_SP_ACS: "http://localhost:8080/login/saml2/sso/keycloak"
|
||||
SECURITY_SAML2_SP_SLS: "http://localhost:8080/logout/saml2/slo"
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
wait_for_backend
|
||||
- name: Run enterprise SAML Playwright tests
|
||||
id: saml-tests
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results-saml.json
|
||||
run: task e2e:enterprise -- --grep "SAML"
|
||||
- name: Stop backend + tear down SAML Keycloak
|
||||
if: always()
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
(cd testing/compose && docker compose -f docker-compose-keycloak-saml.yml down -v)
|
||||
|
||||
# ───────── License-gated feature tests (no IdP needed) ─────────
|
||||
- name: Wipe DB so InitialSecuritySetup re-runs with admin/adminadmin
|
||||
# Earlier phases (OAuth, SAML) create the default admin/stirling user.
|
||||
# InitialSecuritySetup only honours SECURITY_INITIALLOGIN_* when the
|
||||
# admin user doesn't already exist, so the persisted DB has to be
|
||||
# cleared between phases for the feature env vars to take effect.
|
||||
run: |
|
||||
rm -f app/core/configs/stirling-pdf-DB*.mv.db
|
||||
rm -rf app/core/configs/backup
|
||||
- name: Boot Stirling-PDF (frontend baked in, premium only)
|
||||
env:
|
||||
SECURITY_INITIALLOGIN_USERNAME: admin
|
||||
SECURITY_INITIALLOGIN_PASSWORD: adminadmin
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_LOGINMETHOD: "all"
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
|
||||
echo $! > /tmp/backend.pid
|
||||
wait_for_backend
|
||||
- name: Run enterprise feature Playwright tests
|
||||
id: feature-tests
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results-feature.json
|
||||
run: task e2e:enterprise -- --grep "Enterprise license"
|
||||
- name: Print backend log on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "::group::Enterprise backend log"
|
||||
tail -500 /tmp/backend.log || true
|
||||
echo "::endgroup::"
|
||||
- name: Stop backend (final)
|
||||
if: always()
|
||||
run: |
|
||||
source /tmp/helpers.sh
|
||||
stop_backend
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcomes: a flaky test (passed on retry)
|
||||
# leaves its step green, so this is the only place it surfaces. Merges
|
||||
# all three phase reports (some may be absent if an earlier phase hard-
|
||||
# failed and skipped the rest). Emits ::warning:: annotations + a job
|
||||
# summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: >
|
||||
npx tsx editor/scripts/report-flaky-tests.mts
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-oauth.json"
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-saml.json"
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-feature.json"
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-enterprise-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
+273
-218
@@ -1,18 +1,8 @@
|
||||
name: Build and Test Workflow
|
||||
|
||||
# Top-level PR / merge-queue gate. Detects which paths changed and dispatches
|
||||
# to the dedicated reusable workflows under .github/workflows/. Each child
|
||||
# workflow keeps its own setup/teardown so this file stays a routing layer.
|
||||
#
|
||||
# The final `all-checks-passed` job is the single status check that branch
|
||||
# protection should require — it succeeds only if every required upstream
|
||||
# job either succeeded or was legitimately skipped by its path filter.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
merge_group:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
@@ -40,251 +30,316 @@ jobs:
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
docker-base: ${{ steps.changes.outputs.docker-base }}
|
||||
tauri: ${{ steps.changes.outputs.tauri }}
|
||||
engine: ${{ steps.changes.outputs.engine }}
|
||||
generated-models: ${{ steps.changes.outputs.generated-models }}
|
||||
proprietary: ${{ steps.changes.outputs.proprietary }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
build:
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/backend-build.yml
|
||||
secrets: inherit
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
spring-security: [true, false]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
db-migration-test:
|
||||
# Boots the current bootJar against H2 fixtures captured from past
|
||||
# releases (v2.0.0 / v2.5.0 / v2.10.0) and verifies admin login still
|
||||
# works after Hibernate's ddl-auto=update migrates the schema. Gated on
|
||||
# the `project` filter so doc-only PRs skip this ~5-minute job.
|
||||
if: needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/db-migration-test.yml
|
||||
secrets: inherit
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build -PnoSpotless
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
if: always()
|
||||
run: |
|
||||
declare -a dirs=(
|
||||
"app/core/build/reports/tests/"
|
||||
"app/core/build/test-results/"
|
||||
"app/common/build/reports/tests/"
|
||||
"app/common/build/test-results/"
|
||||
"app/proprietary/build/reports/tests/"
|
||||
"app/proprietary/build/test-results/"
|
||||
)
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Missing $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
app/**/build/reports/jacoco/test
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Add coverage to PR with spring security ${{ matrix.spring-security }} and JDK ${{ matrix.jdk-version }}
|
||||
id: jacoco
|
||||
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
|
||||
with:
|
||||
paths: |
|
||||
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
min-coverage-overall: 10
|
||||
min-coverage-changed-files: 0
|
||||
comment-type: summary
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/check-openapi.yml
|
||||
secrets: inherit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: openapi-docs
|
||||
path: ./SwaggerDoc.json
|
||||
|
||||
frontend-validation:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/frontend-validation.yml
|
||||
secrets: inherit
|
||||
|
||||
playwright-e2e:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/e2e-stubbed.yml
|
||||
secrets: inherit
|
||||
|
||||
playwright-e2e-live:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/e2e-live.yml
|
||||
secrets: inherit
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
if: needs.files-changed.outputs.proprietary == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-enterprise.yml
|
||||
secrets: inherit
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
- name: Type-check frontend
|
||||
run: cd frontend && npm run prebuild && npm run typecheck:all
|
||||
- name: Lint frontend
|
||||
run: cd frontend && npm run lint
|
||||
- name: Build frontend
|
||||
run: cd frontend && npm run build
|
||||
- name: Run frontend tests
|
||||
run: cd frontend && npm run test -- --run
|
||||
- name: Upload frontend build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/dist/
|
||||
retention-days: 3
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/check-licence.yml
|
||||
secrets: inherit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: check the licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
retention-days: 3
|
||||
|
||||
docker-compose-tests:
|
||||
if: needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
checks: write
|
||||
uses: ./.github/workflows/docker-compose-tests.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
|
||||
needs: files-changed
|
||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||
# (github.event_name == 'pull_request' &&
|
||||
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
|
||||
# (
|
||||
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
|
||||
# contains(github.event.pull_request.labels.*.name, 'Java') ||
|
||||
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
|
||||
# contains(github.event.pull_request.labels.*.name, 'Security') ||
|
||||
# contains(github.event.pull_request.labels.*.name, 'API') ||
|
||||
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
|
||||
# contains(github.event.pull_request.labels.*.name, 'Test')
|
||||
# )
|
||||
# )
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
cache-dependency-path: ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Pip requirements
|
||||
run: |
|
||||
pip install --require-hashes -r ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Run Docker Compose Tests
|
||||
run: |
|
||||
chmod +x ./testing/test_webpages.sh
|
||||
chmod +x ./testing/test.sh
|
||||
chmod +x ./testing/test_disabledEndpoints.sh
|
||||
./testing/test.sh
|
||||
|
||||
test-build-docker-images:
|
||||
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed, build, check-generateOpenApiDocs, check-licence]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/test-build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
|
||||
|
||||
tauri-build:
|
||||
if: needs.files-changed.outputs.tauri == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/tauri-build.yml
|
||||
secrets: inherit
|
||||
|
||||
ai-engine:
|
||||
if: needs.files-changed.outputs.engine == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/ai-engine.yml
|
||||
secrets: inherit
|
||||
|
||||
# The generated frontend types and engine tool models are both derived from
|
||||
# the Java OpenAPI spec. This job regenerates and diffs them; it boots the
|
||||
# backend, so it is gated on the narrow generated-models filter (spec source,
|
||||
# generators, generated files, generation tasks) rather than the broad
|
||||
# frontend filter, so a CSS-only PR does not pay for a backend build.
|
||||
generated-models:
|
||||
if: needs.files-changed.outputs.generated-models == 'true'
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/check-generated-models.yml
|
||||
secrets: inherit
|
||||
|
||||
pre-commit:
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/pre_commit.yml
|
||||
secrets: inherit
|
||||
|
||||
dependency-review:
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/dependency-review.yml
|
||||
secrets: inherit
|
||||
|
||||
# Coverage aggregate: merges the JUnit + e2e:live + cucumber .exec
|
||||
# artifacts produced by the jobs above into one report, plus pulls
|
||||
# in vitest + Playwright frontend coverage for the per-area matrix.
|
||||
# `if: always()` so a producer failing partway still gets credit
|
||||
# for whatever did record. Advisory only - intentionally NOT in
|
||||
# all-checks-passed, so a flaky aggregate run never blocks merging.
|
||||
coverage-aggregate:
|
||||
if: always()
|
||||
needs:
|
||||
- build
|
||||
- playwright-e2e-live
|
||||
- docker-compose-tests
|
||||
- frontend-validation
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/coverage-aggregate.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
frontend-validation-result: ${{ needs.frontend-validation.result }}
|
||||
playwright-e2e-live-result: ${{ needs.playwright-e2e-live.result }}
|
||||
|
||||
# Single status check that branch protection should mark as required.
|
||||
# Succeeds when every upstream job is either `success` or `skipped` (path-
|
||||
# gated jobs that didn't apply this run). Any `failure` or `cancelled`
|
||||
# result fails the gate. `if: always()` ensures the gate evaluates even
|
||||
# when an upstream job fails.
|
||||
all-checks-passed:
|
||||
name: All checks passed
|
||||
if: always()
|
||||
needs:
|
||||
- files-changed
|
||||
- build
|
||||
- db-migration-test
|
||||
- check-generateOpenApiDocs
|
||||
- frontend-validation
|
||||
- playwright-e2e
|
||||
- playwright-e2e-live
|
||||
- playwright-e2e-enterprise
|
||||
- check-licence
|
||||
- docker-compose-tests
|
||||
- test-build-docker-images
|
||||
- tauri-build
|
||||
- ai-engine
|
||||
- generated-models
|
||||
- pre-commit
|
||||
- dependency-review
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- docker-rev: docker/embedded/Dockerfile
|
||||
artifact-suffix: Dockerfile
|
||||
- docker-rev: docker/embedded/Dockerfile.ultra-lite
|
||||
artifact-suffix: Dockerfile.ultra-lite
|
||||
- docker-rev: docker/embedded/Dockerfile.fat
|
||||
artifact-suffix: Dockerfile.fat
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Verify every required job passed (or was legitimately skipped)
|
||||
env:
|
||||
RESULTS: |
|
||||
files-changed=${{ needs.files-changed.result }}
|
||||
build=${{ needs.build.result }}
|
||||
db-migration-test=${{ needs.db-migration-test.result }}
|
||||
check-generateOpenApiDocs=${{ needs.check-generateOpenApiDocs.result }}
|
||||
frontend-validation=${{ needs.frontend-validation.result }}
|
||||
playwright-e2e=${{ needs.playwright-e2e.result }}
|
||||
playwright-e2e-live=${{ needs.playwright-e2e-live.result }}
|
||||
playwright-e2e-enterprise=${{ needs.playwright-e2e-enterprise.result }}
|
||||
check-licence=${{ needs.check-licence.result }}
|
||||
docker-compose-tests=${{ needs.docker-compose-tests.result }}
|
||||
test-build-docker-images=${{ needs.test-build-docker-images.result }}
|
||||
tauri-build=${{ needs.tauri-build.result }}
|
||||
ai-engine=${{ needs.ai-engine.result }}
|
||||
generated-models=${{ needs.generated-models.result }}
|
||||
pre-commit=${{ needs.pre-commit.result }}
|
||||
dependency-review=${{ needs.dependency-review.result }}
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Free disk space on runner
|
||||
run: |
|
||||
ok=true
|
||||
while IFS='=' read -r name result; do
|
||||
[ -z "$name" ] && continue
|
||||
case "$result" in
|
||||
success|skipped) printf ' %-30s %s\n' "$name" "$result" ;;
|
||||
*) printf '✗ %-30s %s\n' "$name" "$result"; ok=false ;;
|
||||
esac
|
||||
done <<< "$RESULTS"
|
||||
if [ "$ok" != "true" ]; then
|
||||
echo ""
|
||||
echo "One or more required checks failed or were cancelled."
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
echo "All required checks passed."
|
||||
echo "Disk space before cleanup:" && df -h
|
||||
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
|
||||
docker system prune -af || true
|
||||
echo "Disk space after cleanup:" && df -h
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build application
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Build ${{ matrix.docker-rev }}
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
push: false
|
||||
cache-from: type=gha,scope=${{ matrix.artifact-suffix }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.artifact-suffix }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Upload Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: reports-docker-${{ matrix.artifact-suffix }}
|
||||
path: |
|
||||
build/reports/tests/
|
||||
build/test-results/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
name: Check generated models
|
||||
|
||||
# Verifies the committed generated API models are still in sync with the Java
|
||||
# OpenAPI spec: the frontend tool API types
|
||||
# (frontend/editor/src/core/types/toolApiTypes.ts) and the engine tool
|
||||
# models (engine/src/stirling/models/tool_models.py). Regenerates both with the
|
||||
# single top-level `task tool-models` and fails if either committed file is
|
||||
# out of date. Called from build.yml when the backend Java, frontend, or engine
|
||||
# changes; also runs on push to main as a post-merge safety net.
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generated-models:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
# Rebuilds the OpenAPI spec from the current Java and regenerates both the
|
||||
# frontend types and the engine tool models from it.
|
||||
- name: Regenerate generated models
|
||||
run: task tool-models
|
||||
|
||||
- name: Verify generated models are up to date
|
||||
id: models-check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git diff --exit-code \
|
||||
frontend/editor/src/core/types/toolApiTypes.ts \
|
||||
engine/src/stirling/models/tool_models.py
|
||||
|
||||
- name: Comment on generated models check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
# issue/PR number, which doesn't exist on merge_group runs.
|
||||
if: steps.models-check.outcome == 'failure' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- generated-models-check -->';
|
||||
const body = [
|
||||
marker,
|
||||
'### Generated Models Check Failed',
|
||||
'',
|
||||
'The generated `frontend/editor/src/core/types/toolApiTypes.ts` and/or `engine/src/stirling/models/tool_models.py` are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.',
|
||||
'',
|
||||
'Run `task tool-models` to regenerate both, then commit the updated files.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Fail if generated models check failed
|
||||
if: steps.models-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Generated Models Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "The generated frontend API types and/or engine tool"
|
||||
echo "models are out of date with the Java OpenAPI spec and"
|
||||
echo "will need to be regenerated before they can be merged in."
|
||||
echo ""
|
||||
echo "Run 'task tool-models' to regenerate both, then"
|
||||
echo "commit the updated files."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
- name: Remove generated models check comment on success
|
||||
if: steps.models-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- generated-models-check -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
name: License compatibility check
|
||||
|
||||
# Reusable workflow called from build.yml when build.gradle / module gradle
|
||||
# files change. Verifies all transitive dependencies use a permitted license.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-licence:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Check licenses for compatibility
|
||||
run: task backend:licenses:check
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
retention-days: 3
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Generate OpenAPI documentation
|
||||
|
||||
# Reusable workflow called from build.yml when backend Java sources change.
|
||||
# Generates SwaggerDoc.json so we know the doc still builds against the
|
||||
# current code.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
check-generate-openapi-docs:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Generate OpenAPI documentation
|
||||
run: task backend:swagger
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: openapi-docs
|
||||
path: ./SwaggerDoc.json
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "frontend/editor/public/locales/*/translation.toml"
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
- ".github/scripts/check_language_toml.py"
|
||||
- ".github/workflows/check_toml.yml"
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Get PR data
|
||||
id: get-pr-data
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for TOML translation files
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
|
||||
# Check if any files were found
|
||||
if [ ! -s changed_files.txt ]; then
|
||||
echo "No TOML translation files changed in this PR"
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Determine reference file
|
||||
id: determine-file
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -166,16 +166,16 @@ jobs:
|
||||
|
||||
// Determine reference file
|
||||
let referenceFilePath;
|
||||
if (changedFiles.includes("frontend/editor/public/locales/en-US/translation.toml")) {
|
||||
if (changedFiles.includes("frontend/public/locales/en-GB/translation.toml")) {
|
||||
console.log("Using PR branch reference file.");
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: prRepoOwner,
|
||||
repo: prRepoName,
|
||||
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||
path: "frontend/public/locales/en-GB/translation.toml",
|
||||
ref: branch,
|
||||
});
|
||||
|
||||
referenceFilePath = "pr-branch-translation-en-US.toml";
|
||||
referenceFilePath = "pr-branch-translation-en-GB.toml";
|
||||
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||
fs.writeFileSync(referenceFilePath, content);
|
||||
} else {
|
||||
@@ -183,11 +183,11 @@ jobs:
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||
path: "frontend/public/locales/en-GB/translation.toml",
|
||||
ref: "main",
|
||||
});
|
||||
|
||||
referenceFilePath = "main-branch-translation-en-US.toml";
|
||||
referenceFilePath = "main-branch-translation-en-GB.toml";
|
||||
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||
fs.writeFileSync(referenceFilePath, content);
|
||||
}
|
||||
@@ -196,13 +196,12 @@ jobs:
|
||||
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
@@ -240,7 +239,7 @@ jobs:
|
||||
|
||||
- name: Post comment on PR
|
||||
if: env.SCRIPT_OUTPUT != ''
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -293,6 +292,6 @@ jobs:
|
||||
run: |
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -rf pr-branch
|
||||
rm -f pr-branch-translation-en-US.toml main-branch-translation-en-US.toml changed_files.txt result.txt
|
||||
rm -f pr-branch-translation-en-GB.toml main-branch-translation-en-GB.toml changed_files.txt result.txt
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
name: Aggregate backend coverage
|
||||
|
||||
# Reusable workflow called from build.yml after every backend coverage
|
||||
# producer (JUnit, e2e:live, cucumber) has run. Downloads each job's raw
|
||||
# .exec, merges them into one JaCoCo report, and posts a combined step
|
||||
# summary alongside the per-source ones.
|
||||
#
|
||||
# Kept separate from the per-source jobs so:
|
||||
# - the per-source jobs stay fast and independent (no cross-job waits)
|
||||
# - this job can `if: always()` and still produce something useful when
|
||||
# one of the producers fails partway through
|
||||
# - frontend producers can be added later without touching the
|
||||
# producers themselves
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
frontend-validation-result:
|
||||
description: Result of the frontend-validation producer job
|
||||
required: false
|
||||
type: string
|
||||
default: skipped
|
||||
playwright-e2e-live-result:
|
||||
description: Result of the playwright-e2e-live producer job
|
||||
required: false
|
||||
type: string
|
||||
default: skipped
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
aggregate:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install defusedxml for coverage scripts
|
||||
# Both coverage-summary.py and coverage-matrix.py parse JaCoCo
|
||||
# XML through defusedxml - see the script headers for context.
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
# Pattern matches every artifact this PR's producers might upload:
|
||||
# jacoco-exec-junit-jdk-25 (uploaded only by the saas
|
||||
# leg of backend-build, which
|
||||
# is a strict superset of the
|
||||
# core + proprietary legs)
|
||||
# jacoco-exec-e2e-live
|
||||
# jacoco-exec-cucumber
|
||||
# Each lands as a sibling dir under coverage-execs/, with the .exec
|
||||
# files preserving their original relative paths.
|
||||
- name: Download all .exec artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
pattern: jacoco-exec-*
|
||||
path: coverage-execs/
|
||||
merge-multiple: false
|
||||
continue-on-error: true
|
||||
|
||||
- name: Inventory .exec files
|
||||
id: inventory
|
||||
# Splits the downloaded artifacts into two buckets:
|
||||
# * e2e-only = cucumber + Playwright live (user-flow coverage)
|
||||
# * all = the above plus JUnit (everything we test)
|
||||
#
|
||||
# Bucketing is by artifact-name prefix: download-artifact preserves
|
||||
# the artifact name as the top-level dir, so JUnit's `.exec`s live
|
||||
# under coverage-execs/jacoco-exec-junit-*/... while the others
|
||||
# are under coverage-execs/jacoco-exec-{e2e-live,cucumber}/...
|
||||
#
|
||||
# If nothing was uploaded (e.g. all producers crashed before
|
||||
# writing) we exit gracefully so this advisory job never fails CI.
|
||||
run: |
|
||||
mapfile -t all_execs < <(find coverage-execs -name '*.exec' -type f | sort)
|
||||
mapfile -t e2e_execs < <(find coverage-execs -name '*.exec' -type f -not -path '*/jacoco-exec-junit-*' | sort)
|
||||
if [ "${#all_execs[@]}" -eq 0 ]; then
|
||||
echo "::warning::No .exec artifacts found - skipping aggregate report"
|
||||
echo "found_all=false" >> "$GITHUB_OUTPUT"
|
||||
echo "found_e2e=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
printf 'All %d .exec files:\n' "${#all_execs[@]}"
|
||||
printf ' %s\n' "${all_execs[@]}"
|
||||
IFS=','; all_joined="${all_execs[*]}"
|
||||
echo "files_all=$all_joined" >> "$GITHUB_OUTPUT"
|
||||
echo "found_all=true" >> "$GITHUB_OUTPUT"
|
||||
if [ "${#e2e_execs[@]}" -eq 0 ]; then
|
||||
echo "::notice::No e2e/cucumber .exec files - e2e-only report will be skipped"
|
||||
echo "found_e2e=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
printf 'E2E-only %d .exec files:\n' "${#e2e_execs[@]}"
|
||||
printf ' %s\n' "${e2e_execs[@]}"
|
||||
unset IFS
|
||||
IFS=','; e2e_joined="${e2e_execs[*]}"
|
||||
echo "files_e2e=$e2e_joined" >> "$GITHUB_OUTPUT"
|
||||
echo "found_e2e=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Compile classes for JaCoCo class lookup
|
||||
# jacocoReportFromExec only needs the compiled .class files
|
||||
# under each subproject's build/classes/java/main/. `classes`
|
||||
# (compileJava + processResources) is enough; we skipped the
|
||||
# heavier `assemble` to avoid building bootJar / fat jars that
|
||||
# add 60+ seconds per run for no gain to the report.
|
||||
if: steps.inventory.outputs.found_all == 'true'
|
||||
run: ./gradlew classes -PnoSpotless
|
||||
|
||||
- name: Generate e2e-only JaCoCo report
|
||||
# "Real user-flow" coverage: only counts code reached by an actual
|
||||
# HTTP request from cucumber or live Playwright. Useful for
|
||||
# questions like "how much of our backend does a user actually
|
||||
# hit?". Skipped when neither producer uploaded a .exec.
|
||||
if: steps.inventory.outputs.found_e2e == 'true'
|
||||
run: |
|
||||
./gradlew jacocoReportFromExec \
|
||||
-PexecFile="${{ steps.inventory.outputs.files_e2e }}" \
|
||||
-PreportDir=build/reports/jacoco/aggregate-e2e \
|
||||
-PnoSpotless
|
||||
|
||||
- name: Generate combined JaCoCo report (everything)
|
||||
if: steps.inventory.outputs.found_all == 'true'
|
||||
run: |
|
||||
./gradlew jacocoReportFromExec \
|
||||
-PexecFile="${{ steps.inventory.outputs.files_all }}" \
|
||||
-PreportDir=build/reports/jacoco/aggregate-all \
|
||||
-PnoSpotless
|
||||
|
||||
- name: E2E-only step summary
|
||||
# Rendered first so it gets prime real estate in the Summary
|
||||
# tab - this is the number most readers actually want
|
||||
# ("how much of the backend do real user flows cover?").
|
||||
if: steps.inventory.outputs.found_e2e == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Real user-flow backend coverage (e2e:live + cucumber)" \
|
||||
--jacoco "merged=build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
|
||||
- name: ALL-sources step summary
|
||||
# Separate call (not a multi-input one) because the helper's
|
||||
# rightmost "Aggregate" column would sum the two reports - which
|
||||
# is meaningless when one is a strict superset of the other.
|
||||
if: steps.inventory.outputs.found_all == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Combined backend coverage (JUnit + e2e:live + cucumber)" \
|
||||
--jacoco "merged=build/reports/jacoco/aggregate-all/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
|
||||
- name: Upload combined aggregate report
|
||||
if: steps.inventory.outputs.found_all == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-aggregate-all-${{ github.run_id }}
|
||||
path: build/reports/jacoco/aggregate-all/
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload e2e-only aggregate report
|
||||
if: steps.inventory.outputs.found_e2e == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-aggregate-e2e-${{ github.run_id }}
|
||||
path: build/reports/jacoco/aggregate-e2e/
|
||||
retention-days: 14
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Per-area matrix: rolls backend + frontend coverage into one
|
||||
# table indexed by core/proprietary/saas/desktop. Pulls the
|
||||
# frontend artifacts now (after the JaCoCo step has done its
|
||||
# work) so the per-source backend summaries still render first
|
||||
# even if the matrix step fails.
|
||||
# --------------------------------------------------------------
|
||||
- name: Download vitest coverage artifact
|
||||
# frontend-validation uploads as `frontend-coverage`. Tolerate
|
||||
# absence on backend-only runs by skipping the download entirely
|
||||
# when the producer job was not part of this workflow run.
|
||||
if: inputs.frontend-validation-result == 'success'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: matrix-inputs/vitest/
|
||||
continue-on-error: true
|
||||
|
||||
- name: Download Playwright frontend coverage artifact
|
||||
# e2e-live uploads the artifact with a stable name. Skip the
|
||||
# download entirely when the producer job did not run.
|
||||
if: inputs.playwright-e2e-live-result == 'success'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: playwright-frontend-coverage
|
||||
path: matrix-inputs/playwright/
|
||||
continue-on-error: true
|
||||
|
||||
- name: Coverage matrix step summary
|
||||
if: always()
|
||||
# Matrix references the two aggregate JaCoCo XMLs (already
|
||||
# generated above) plus whichever frontend artifacts landed.
|
||||
# Every input is optional; missing ones render as "-".
|
||||
run: |
|
||||
python scripts/coverage-matrix.py \
|
||||
${{ steps.inventory.outputs.found_all == 'true' && '--jacoco-all build/reports/jacoco/aggregate-all/jacocoTestReport.xml' || '' }} \
|
||||
${{ steps.inventory.outputs.found_e2e == 'true' && '--jacoco-e2e build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml' || '' }} \
|
||||
--vitest matrix-inputs/vitest/coverage-summary.json \
|
||||
--playwright-frontend matrix-inputs/playwright/coverage-pw-summary/coverage-summary.json \
|
||||
--title "Coverage matrix (per-area, e2e vs all)" \
|
||||
--github-step-summary
|
||||
@@ -1,93 +0,0 @@
|
||||
name: DB migration smoke test
|
||||
|
||||
# Boots the current Stirling-PDF JAR against H2 fixtures captured from past
|
||||
# releases (v2.0.0 / v2.5.0 / v2.10.0) and verifies admin login still works.
|
||||
# Catches schema changes that would break existing user databases under
|
||||
# Hibernate's `ddl-auto=update` upgrade path.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
migration-test:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-8' }}
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
# No `-PnoSpotless` here yet because the upstream cache layer matches the
|
||||
# backend build's; reuse keeps cold-cache cost identical.
|
||||
- name: Build Stirling-PDF JAR
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
run: ./gradlew :stirling-pdf:bootJar -PnoSpotless --no-daemon
|
||||
|
||||
- name: Locate built JAR
|
||||
id: jar
|
||||
run: |
|
||||
jar=$(find app/core/build/libs -maxdepth 1 -name 'Stirling-PDF*.jar' -o -name 'stirling-pdf*.jar' 2>/dev/null \
|
||||
| grep -vE '(-plain|-sources)\.jar$' | head -n 1)
|
||||
if [[ -z "$jar" ]]; then
|
||||
echo "::error::No JAR under app/core/build/libs"
|
||||
ls -lah app/core/build/libs || true
|
||||
exit 1
|
||||
fi
|
||||
# Absolute path - the migration script pushd's into a temp workdir
|
||||
# before invoking java, which would dangle a relative path.
|
||||
jar=$(realpath "$jar")
|
||||
echo "path=$jar" >> "$GITHUB_OUTPUT"
|
||||
echo "Built JAR: $jar"
|
||||
|
||||
- name: Run migration smoke test
|
||||
env:
|
||||
STIRLING_JAR: ${{ steps.jar.outputs.path }}
|
||||
run: bash scripts/db-migration/run-migration-test.sh
|
||||
|
||||
- name: Upload app logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: db-migration-app-logs
|
||||
# Path matches the preserved workdir in run-migration-test.sh -
|
||||
# only failing fixtures leave a directory behind.
|
||||
path: /tmp/stirling-migration-failed-*/app.log
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -1,10 +1,13 @@
|
||||
name: Dependency Review
|
||||
|
||||
# Reusable workflow called from build.yml. Scans dependency manifest files
|
||||
# changing in a PR for known-vulnerable packages, using the rules in
|
||||
# .github/config/dependency-review-config.yml.
|
||||
on:
|
||||
workflow_call:
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||
# Once installed, if the workflow run is marked as required,
|
||||
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
name: "Dependency Review"
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,13 +17,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
with:
|
||||
config-file: "./.github/config/dependency-review-config.yml"
|
||||
|
||||
@@ -10,38 +10,23 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
deploy-v2-on-push:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
@@ -100,16 +85,15 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && steps.check-frontend.outputs.exists == 'false'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
@@ -119,44 +103,13 @@ jobs:
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push frontend image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.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: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image (Depot)
|
||||
if: env.USE_DEPOT == 'true' && steps.check-backend.outputs.exists == 'false'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push 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
|
||||
cache-from: type=gha,scope=stirling-v2-backend
|
||||
cache-to: type=gha,mode=max,scope=stirling-v2-backend
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
@@ -188,7 +141,7 @@ jobs:
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2"
|
||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
name: Docker Compose Cucumber tests
|
||||
|
||||
# Reusable workflow called from build.yml when project / docker / testing
|
||||
# sources change. Boots the docker-compose stack and runs the cucumber
|
||||
# scenarios in testing/cucumber.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
docker-base-changed:
|
||||
description: "Whether the docker base image changed (forwarded from files-changed)."
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking. Tuned to 4 because bench showed 16 was within noise of 4."
|
||||
required: false
|
||||
type: string
|
||||
default: "4"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
docker-compose-tests:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '4') }}
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
checks: write
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
# When the PR changes the base image, test.sh builds it locally
|
||||
# (stirling-pdf-base:local) into the daemon image store. A buildx
|
||||
# container builder can't see that store, so skip it here and let
|
||||
# `docker buildx build` fall back to the default docker driver, which
|
||||
# resolves the local base. The gha cache backend is also skipped (its
|
||||
# runtime token isn't exposed) since the docker driver can't use it.
|
||||
- name: Set up Docker Buildx
|
||||
if: inputs.docker-base-changed != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
|
||||
- name: Expose GitHub runtime for Buildx cache
|
||||
if: inputs.docker-base-changed != 'true'
|
||||
uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
cache-dependency-path: ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Pip requirements
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Extract JaCoCo agent for cucumber coverage
|
||||
# Stages build/jacoco/jacocoagent.jar where the coverage override
|
||||
# file bind-mounts it into the cucumber container. The agent jar
|
||||
# never goes into the published image - this is host-only.
|
||||
run: ./gradlew copyJacocoAgent -PnoSpotless
|
||||
|
||||
- name: Run Docker Compose Tests
|
||||
run: |
|
||||
chmod +x ./testing/test_webpages.sh
|
||||
chmod +x ./testing/test.sh
|
||||
chmod +x ./testing/test_disabledEndpoints.sh
|
||||
./testing/test.sh
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DOCKER_BASE_CHANGED: ${{ inputs.docker-base-changed }}
|
||||
# Tells test.sh to layer testing/compose/docker-compose-coverage.override.yml
|
||||
# over the cucumber compose so the container starts with the
|
||||
# JaCoCo agent attached via JAVA_CUSTOM_OPTS.
|
||||
STIRLING_PDF_TEST_COVERAGE: "1"
|
||||
|
||||
- name: Generate cucumber JaCoCo report
|
||||
# `if: always()` so a behave failure still produces partial
|
||||
# coverage from whatever endpoints did run. The exec file only
|
||||
# exists when the container shut down cleanly - guard so the step
|
||||
# is silent on the (rare) crash path.
|
||||
if: always()
|
||||
id: cucumber-coverage
|
||||
run: |
|
||||
if [ -s testing/cucumber-coverage/cucumber.exec ]; then
|
||||
./gradlew jacocoReportFromExec \
|
||||
-PexecFile=testing/cucumber-coverage/cucumber.exec \
|
||||
-PreportDir=build/reports/jacoco/cucumber \
|
||||
-PnoSpotless
|
||||
echo "report=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::No cucumber .exec at testing/cucumber-coverage/cucumber.exec (container may have crashed before flushing)"
|
||||
echo "report=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py parses JaCoCo XML through defusedxml -
|
||||
# see the script header for context.
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: Cucumber coverage step summary
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Cucumber (docker) JaCoCo coverage" \
|
||||
--jacoco "cucumber=build/reports/jacoco/cucumber/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
|
||||
- name: Upload cucumber JaCoCo report
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-cucumber-${{ github.run_id }}
|
||||
path: build/reports/jacoco/cucumber/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload raw cucumber .exec for aggregate merge
|
||||
# Picked up by the coverage-aggregate workflow via the
|
||||
# `jacoco-exec-*` artifact name pattern.
|
||||
if: always() && steps.cucumber-coverage.outputs.report == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-exec-cucumber
|
||||
path: testing/cucumber-coverage/cucumber.exec
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload Cucumber Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cucumber-report
|
||||
path: testing/cucumber/report.html
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docker-compose-test-reports
|
||||
path: testing/reports/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Cucumber Test Report
|
||||
if: always()
|
||||
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
|
||||
with:
|
||||
name: Cucumber Tests
|
||||
path: testing/cucumber/junit/*.xml
|
||||
reporter: java-junit
|
||||
fail-on-error: false
|
||||
@@ -1,227 +0,0 @@
|
||||
name: Playwright E2E (live backend)
|
||||
|
||||
# Reusable workflow called from build.yml. Live-backend Playwright suite —
|
||||
# boots Spring Boot and runs auth + real tool round-trips against the live
|
||||
# server.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-e2e-live:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
- name: Run live E2E tests (chromium) with coverage
|
||||
id: live-tests
|
||||
env:
|
||||
# Attaches the JaCoCo agent to the bootRun JVM (see
|
||||
# .taskfiles/e2e.yml live:backend). The .exec gets flushed on
|
||||
# graceful shutdown when the runner traps EXIT/INT/TERM, so the
|
||||
# report step below sees a populated file.
|
||||
COVERAGE: "1"
|
||||
# Tells the Playwright fixture (test-base.ts) to capture per-test
|
||||
# V8 JS coverage. Raw dumps land under
|
||||
# .test-state/playwright/coverage-pw/ for the post-process step
|
||||
# to aggregate. Chromium-only - other engines silently skip.
|
||||
PW_COVERAGE: "1"
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
run: task e2e:live
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
# leaves the step green, so this is the only place it surfaces. Emits
|
||||
# ::warning:: annotations + a job summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: npx tsx editor/scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
- name: Generate JaCoCo report from e2e:live .exec
|
||||
if: always()
|
||||
id: live-coverage
|
||||
# `if: always()` so even a failed test run still produces a
|
||||
# report from whatever flows did exercise the backend before
|
||||
# the failure. The task itself tolerates a missing .exec
|
||||
# (jacoco emits an empty report rather than crashing) but we
|
||||
# guard with `test -s` to keep the job log clean.
|
||||
run: |
|
||||
if [ -s .test-state/playwright/jacoco.exec ]; then
|
||||
./gradlew jacocoReportFromExec \
|
||||
-PexecFile=.test-state/playwright/jacoco.exec \
|
||||
-PreportDir=build/reports/jacoco/e2e-live \
|
||||
-PnoSpotless
|
||||
echo "report=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::No e2e:live .exec found at .test-state/playwright/jacoco.exec; skipping report"
|
||||
echo "report=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Set up Python for coverage summary
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install defusedxml for coverage summary
|
||||
# coverage-summary.py uses defusedxml instead of stdlib xml.etree
|
||||
# to dodge XXE / billion-laughs scanner findings.
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
run: python -m pip install --quiet defusedxml
|
||||
- name: e2e:live coverage step summary
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Playwright (live backend) JaCoCo coverage" \
|
||||
--jacoco "e2e-live=build/reports/jacoco/e2e-live/jacocoTestReport.xml" \
|
||||
--github-step-summary
|
||||
- name: Upload e2e:live JaCoCo report
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-e2e-live-${{ github.run_id }}
|
||||
path: build/reports/jacoco/e2e-live/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload raw e2e:live .exec for aggregate merge
|
||||
# Picked up by the coverage-aggregate workflow via the
|
||||
# `jacoco-exec-*` artifact name pattern.
|
||||
if: always() && steps.live-coverage.outputs.report == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: jacoco-exec-e2e-live
|
||||
path: .test-state/playwright/jacoco.exec
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Set up Python for frontend coverage summary
|
||||
# Separate from the backend-coverage python step because the
|
||||
# frontend path doesn't depend on a JaCoCo report - it produces
|
||||
# a summary even on backend failure, as long as some Playwright
|
||||
# tests ran far enough to dump V8 coverage.
|
||||
if: always()
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install defusedxml for frontend coverage summary
|
||||
# Idempotent re-install: the backend-coverage step may have
|
||||
# installed it already, but this leg can run on its own when the
|
||||
# backend report step skips (e.g. .exec missing).
|
||||
if: always()
|
||||
run: python -m pip install --quiet defusedxml
|
||||
|
||||
- name: Aggregate Playwright frontend (V8) coverage
|
||||
# Rolls per-test V8 dumps from the test-base fixture into one
|
||||
# vitest-shaped coverage-summary.json. Tolerates a missing dump
|
||||
# dir (firefox/webkit runs, or a failure before any test got
|
||||
# far enough to dump).
|
||||
if: always()
|
||||
id: pw-frontend-coverage
|
||||
run: |
|
||||
if [ -d .test-state/playwright/coverage-pw ] && \
|
||||
find .test-state/playwright/coverage-pw -name '*.json' -type f | grep -q .; then
|
||||
python scripts/playwright-coverage-summary.py \
|
||||
.test-state/playwright/coverage-pw \
|
||||
--out .test-state/playwright/coverage-pw-summary/coverage-summary.json
|
||||
echo "summary=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::notice::No Playwright frontend coverage dumps found (chromium-only feature)"
|
||||
echo "summary=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Playwright frontend coverage step summary
|
||||
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Playwright (live) frontend coverage" \
|
||||
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
|
||||
--github-step-summary
|
||||
|
||||
- name: Upload Playwright frontend coverage
|
||||
# Bundle both the aggregated summary and the raw V8 dumps so
|
||||
# someone debugging "why is this function showing as covered"
|
||||
# can trace it back to the source dump.
|
||||
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-frontend-coverage
|
||||
path: |
|
||||
.test-state/playwright/coverage-pw-summary/
|
||||
.test-state/playwright/coverage-pw/
|
||||
retention-days: 7
|
||||
|
||||
- name: Print backend log on failure
|
||||
if: failure() && steps.live-tests.conclusion == 'failure'
|
||||
run: |
|
||||
echo "::group::Spring Boot backend log (last 500 lines)"
|
||||
tail -500 .test-state/playwright/backend.log || echo "no backend log found"
|
||||
echo "::endgroup::"
|
||||
- name: Upload backend log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: backend-log-live-${{ github.run_id }}
|
||||
path: .test-state/playwright/backend.log
|
||||
retention-days: 7
|
||||
- name: List Playwright output locations (debug)
|
||||
if: always()
|
||||
run: |
|
||||
echo "::group::Playwright output dirs"
|
||||
# Playwright anchors its default outputDir + HTML report to the
|
||||
# nearest package.json, which is frontend/ (frontend/editor has
|
||||
# none), so artifacts land under frontend/, not frontend/editor/.
|
||||
ls -la frontend/playwright-report 2>/dev/null \
|
||||
|| echo "no playwright-report at frontend/"
|
||||
ls -la frontend/test-results 2>/dev/null \
|
||||
|| echo "no test-results at frontend/"
|
||||
find . -name node_modules -prune -o -name 'trace.zip' -print 2>/dev/null || true
|
||||
echo "::endgroup::"
|
||||
- name: Upload Playwright report + traces
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-live-${{ github.run_id }}
|
||||
# test-results/ holds the per-test trace.zip (with browser console
|
||||
# logs) + screenshots/video; playwright-report/ is the HTML report.
|
||||
# Both live under frontend/ (Playwright anchors them to the nearest
|
||||
# package.json, which is frontend/; frontend/editor has none).
|
||||
path: |
|
||||
frontend/playwright-report/
|
||||
frontend/test-results/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Playwright E2E (stubbed)
|
||||
|
||||
# Reusable workflow called from build.yml. Backend-free Playwright suite —
|
||||
# fast, no Spring Boot required. Runs against the `stubbed` project which
|
||||
# mocks API responses in the browser.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking. Tuned to 8 to match the other playwright workflows; bench showed flat scaling above 8."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-e2e:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
- name: Run stubbed E2E tests (chromium)
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
run: task e2e:stubbed -- --workers=3
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
# leaves the step green, so this is the only place it surfaces. Emits
|
||||
# ::warning:: annotations + a job summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: npx tsx editor/scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-stubbed-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||
@@ -19,28 +16,24 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
files-changed:
|
||||
name: detect what files changed
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
licenses-frontend: ${{ steps.changes.outputs.licenses-frontend }}
|
||||
licenses-backend: ${{ steps.changes.outputs.licenses-backend }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
@@ -48,20 +41,20 @@ jobs:
|
||||
generate-frontend-license-report:
|
||||
if: needs.files-changed.outputs.licenses-frontend == 'true'
|
||||
name: Generate Frontend License Report
|
||||
needs: [pick, files-changed]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR head (default)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -76,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Checkout BASE branch (safe script)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
@@ -84,7 +77,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -96,20 +89,12 @@ jobs:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Generate frontend license report (Push only)
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
PR_IS_FORK: "false"
|
||||
run: task frontend:licenses:generate
|
||||
|
||||
- name: Generate frontend license report (internal PR)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
working-directory: frontend
|
||||
env:
|
||||
PR_IS_FORK: "false"
|
||||
run: task frontend:licenses:generate
|
||||
run: npm run generate-licenses
|
||||
|
||||
- name: Generate frontend license report (fork PRs, pinned)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
@@ -117,31 +102,31 @@ jobs:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
working-directory: frontend
|
||||
run: |
|
||||
mkdir -p editor/src/assets
|
||||
npx --yes license-report --only=prod --output=json > editor/src/assets/3rdPartyLicenses.json
|
||||
mkdir -p src/assets
|
||||
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
|
||||
|
||||
- name: Postprocess with project script (BASE version)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
env:
|
||||
PR_IS_FORK: "true"
|
||||
run: |
|
||||
node base/frontend/editor/scripts/generate-licenses.js \
|
||||
--input frontend/editor/src/assets/3rdPartyLicenses.json
|
||||
node base/frontend/scripts/generate-licenses.js \
|
||||
--input frontend/src/assets/3rdPartyLicenses.json
|
||||
|
||||
- name: Copy postprocessed artifacts back (fork PRs)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
run: |
|
||||
mkdir -p frontend/editor/src/assets
|
||||
if [ -f "base/frontend/editor/src/assets/3rdPartyLicenses.json" ]; then
|
||||
cp base/frontend/editor/src/assets/3rdPartyLicenses.json frontend/editor/src/assets/3rdPartyLicenses.json
|
||||
mkdir -p frontend/src/assets
|
||||
if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then
|
||||
cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json
|
||||
fi
|
||||
if [ -f "base/frontend/editor/src/assets/license-warnings.json" ]; then
|
||||
cp base/frontend/editor/src/assets/license-warnings.json frontend/editor/src/assets/license-warnings.json
|
||||
if [ -f "base/frontend/src/assets/license-warnings.json" ]; then
|
||||
cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json
|
||||
fi
|
||||
|
||||
- name: Check for license warnings
|
||||
run: |
|
||||
if [ -f "frontend/editor/src/assets/license-warnings.json" ]; then
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||
@@ -150,7 +135,7 @@ jobs:
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -189,10 +174,10 @@ jobs:
|
||||
echo ""
|
||||
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
|
||||
echo "❌ **Failed** – incompatible or unknown licenses found."
|
||||
if [ -f "frontend/editor/src/assets/license-warnings.json" ]; then
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo ""
|
||||
echo "### Warnings"
|
||||
jq -r '.warnings[] | "- \(.message)"' frontend/editor/src/assets/license-warnings.json || true
|
||||
jq -r '.warnings[] | "- \(.message)"' frontend/src/assets/license-warnings.json || true
|
||||
fi
|
||||
else
|
||||
echo "✅ **Passed** – no license warnings detected."
|
||||
@@ -203,7 +188,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -218,7 +203,7 @@ jobs:
|
||||
const fs = require('fs');
|
||||
let warningDetails = '';
|
||||
try {
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/editor/src/assets/license-warnings.json', 'utf8'));
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
|
||||
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
|
||||
} catch (e) {
|
||||
warningDetails = 'Unable to read warning details';
|
||||
@@ -258,7 +243,7 @@ jobs:
|
||||
- name: Commit changes (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
git add frontend/editor/src/assets/3rdPartyLicenses.json
|
||||
git add frontend/src/assets/3rdPartyLicenses.json
|
||||
# Note: Do NOT commit license-warnings.json - it's only for PR review
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
@@ -276,7 +261,7 @@ jobs:
|
||||
|
||||
The following licenses may require review for corporate compatibility:
|
||||
|
||||
$(cat frontend/editor/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
|
||||
Please review these licenses to ensure they are acceptable for your use case."
|
||||
fi
|
||||
@@ -288,7 +273,7 @@ jobs:
|
||||
- name: Create Pull Request (Push only)
|
||||
id: cpr
|
||||
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update Frontend 3rd Party Licenses"
|
||||
@@ -318,23 +303,21 @@ jobs:
|
||||
|
||||
generate-backend-license-report:
|
||||
if: needs.files-changed.outputs.licenses-backend == 'true'
|
||||
needs: [pick, files-changed]
|
||||
needs: files-changed
|
||||
name: Generate Backend License Report
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -347,27 +330,17 @@ jobs:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
run: task backend:licenses:generate || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||
run: |
|
||||
./gradlew clean checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
@@ -383,7 +356,7 @@ jobs:
|
||||
|
||||
- name: Upload artifact on license issues
|
||||
if: env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: backend-dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
@@ -396,7 +369,7 @@ jobs:
|
||||
|
||||
- name: Delete previous backend license check comments
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -426,7 +399,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - Backend License Check Results
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -509,7 +482,7 @@ jobs:
|
||||
- name: Create Pull Request (push only)
|
||||
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update Backend 3rd Party Licenses"
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
name: Frontend lint, type-check, and build
|
||||
|
||||
# Reusable workflow called from build.yml when frontend / testing sources
|
||||
# change. Runs the consolidated `task frontend:check:all` (lint, types,
|
||||
# unit tests, build) and uploads the dist artifact for downstream jobs.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
frontend-validation:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Quality-check frontend
|
||||
id: frontend-check
|
||||
run: task frontend:check:all
|
||||
continue-on-error: true
|
||||
- name: Comment on frontend check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
# issue/PR number, which doesn't exist on merge_group runs.
|
||||
if: steps.frontend-check.outcome == 'failure' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- frontend-check -->';
|
||||
const body = [
|
||||
marker,
|
||||
'### Frontend Check Failed',
|
||||
'',
|
||||
'There are issues with your frontend code that will need to be fixed before they can be merged in.',
|
||||
'',
|
||||
'Run `task frontend:fix` to auto-fix what can be fixed automatically, then run `task frontend:check:all` to see what still needs fixing manually.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
- name: Fail if frontend check failed
|
||||
if: steps.frontend-check.outcome == 'failure'
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Frontend Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "There are issues with your frontend code that"
|
||||
echo "will need to be fixed before they can be merged in."
|
||||
echo ""
|
||||
echo "Run 'task frontend:fix' to auto-fix what can be"
|
||||
echo "fixed automatically, then run 'task frontend:check:all'"
|
||||
echo "to see what still needs fixing manually."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
- name: Remove frontend check comment on success
|
||||
if: steps.frontend-check.outcome == 'success' && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- frontend-check -->';
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
}
|
||||
- name: Vitest coverage
|
||||
# Separate from `frontend:check:all` so the quality-gate run stays
|
||||
# uninstrumented (faster signal) and coverage stays an informational
|
||||
# follow-up. Continue-on-error keeps the workflow green even when
|
||||
# a handful of test files refuse to import (e.g. missing icon
|
||||
# specifiers) - the summary still gets posted with whatever
|
||||
# vitest managed to instrument.
|
||||
id: frontend-coverage
|
||||
continue-on-error: true
|
||||
run: task frontend:test:coverage
|
||||
- name: Set up Python for coverage summary
|
||||
if: always()
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install defusedxml for coverage summary
|
||||
# See coverage-summary.py header - it parses XML through defusedxml
|
||||
# to dodge the stdlib parser's exposure to XXE / billion-laughs.
|
||||
if: always()
|
||||
run: python -m pip install --quiet defusedxml
|
||||
- name: Vitest coverage step summary
|
||||
if: always()
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Frontend Vitest coverage" \
|
||||
--vitest frontend/editor/coverage/coverage-summary.json \
|
||||
--github-step-summary
|
||||
- name: Upload vitest coverage report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: frontend/editor/coverage/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
- name: Upload frontend build artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/editor/dist/
|
||||
retention-days: 3
|
||||
@@ -15,15 +15,15 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d # v6.0.0
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
yaml-file: .github/labels.yml
|
||||
|
||||
@@ -21,14 +21,8 @@ on:
|
||||
- windows
|
||||
- macos
|
||||
- linux
|
||||
sign:
|
||||
description: "Code sign the binaries (requires signing secrets)"
|
||||
required: false
|
||||
default: "true"
|
||||
type: choice
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
push:
|
||||
branches: [main, V2-master]
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -36,57 +30,38 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
determine-matrix:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
echo "Running gradlew printVersion..."
|
||||
./gradlew printVersion --quiet
|
||||
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
||||
echo "Extracted version: $VERSION"
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
@@ -94,28 +69,26 @@ jobs:
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For push/release events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-jars:
|
||||
needs: [pick, determine-matrix]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
needs: determine-matrix
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant:
|
||||
@@ -133,40 +106,33 @@ jobs:
|
||||
file_suffix: "-server"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
- name: Build JAR
|
||||
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
run: ./gradlew clean build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.variant.disable_security }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
@@ -179,7 +145,7 @@ jobs:
|
||||
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
|
||||
|
||||
- name: Upload JAR artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: jar${{ matrix.variant.file_suffix }}
|
||||
path: ./jar-dist/*.jar
|
||||
@@ -194,19 +160,17 @@ jobs:
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
allowed-endpoints: >
|
||||
one.digicert.com:443
|
||||
clientauth.one.digicert.com:443
|
||||
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
@@ -215,71 +179,105 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
# x86_64 JDK is set up first so the aarch64 step below can leave its
|
||||
# JAVA_HOME as the active one. The macOS universal JRE build needs
|
||||
# jmods from both arches; the x64 path is captured into the env
|
||||
# before the second setup-java overwrites JAVA_HOME.
|
||||
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
||||
if: matrix.platform == 'macos-15'
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Capture x86_64 JAVA_HOME
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
echo "🔧 Building Stirling-PDF JAR..."
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
||||
echo "✅ Built JAR: $STIRLING_JAR"
|
||||
|
||||
# Build the universal JRE before desktop:prepare so the jlink:runtime
|
||||
# task short-circuits on its `test -d runtime/jre` status check.
|
||||
- name: Build universal macOS JRE
|
||||
if: matrix.platform == 'macos-15'
|
||||
# Create Tauri directories
|
||||
mkdir -p ./frontend/src-tauri/libs
|
||||
mkdir -p ./frontend/src-tauri/runtime
|
||||
|
||||
# Copy JAR to Tauri libs
|
||||
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
|
||||
echo "✅ JAR copied to Tauri libs"
|
||||
|
||||
# Analyze JAR dependencies for jlink modules
|
||||
echo "🔍 Analyzing JAR dependencies..."
|
||||
if command -v jdeps &> /dev/null; then
|
||||
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
|
||||
if [ -n "$DETECTED_MODULES" ]; then
|
||||
echo "📋 jdeps detected modules: $DETECTED_MODULES"
|
||||
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
else
|
||||
echo "⚠️ jdeps analysis failed, using predefined modules"
|
||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ jdeps not available, using predefined modules"
|
||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
fi
|
||||
|
||||
# Create custom JRE with jlink
|
||||
echo "🔧 Creating custom JRE with jlink..."
|
||||
echo "📋 Using modules: $MODULES"
|
||||
|
||||
# Remove any existing JRE
|
||||
rm -rf ./frontend/src-tauri/runtime/jre
|
||||
|
||||
# Create the custom JRE
|
||||
jlink \
|
||||
--add-modules "$MODULES" \
|
||||
--strip-debug \
|
||||
--compress=2 \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output ./frontend/src-tauri/runtime/jre
|
||||
|
||||
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
|
||||
echo "❌ Failed to create JLink runtime"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test the bundled runtime
|
||||
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
|
||||
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
|
||||
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
|
||||
else
|
||||
echo "❌ Custom JRE executable not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate runtime size
|
||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
||||
env:
|
||||
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:jlink:universal-mac
|
||||
|
||||
- name: Prepare desktop build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:prepare
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm ci
|
||||
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
||||
uses: digicert/ssm-code-signing@v1.1.0
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
@@ -288,7 +286,7 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ matrix.platform == 'windows-latest' && 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: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
@@ -323,7 +321,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ matrix.platform == 'windows-latest' && 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: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
@@ -354,7 +352,7 @@ jobs:
|
||||
}
|
||||
|
||||
- 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' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -375,7 +373,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' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
@@ -386,82 +384,8 @@ jobs:
|
||||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported successfully."
|
||||
|
||||
# Pre-flight: verify smctl can talk to DigiCert and sync cert before we sign.
|
||||
# Mirrors the setup from working public Tauri+KeyLocker repos (Labric, Meetily).
|
||||
# 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: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
run: |
|
||||
& smctl healthcheck
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
|
||||
& smctl keypair ls
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
|
||||
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
||||
Write-Host "[SUCCESS] smctl preflight passed"
|
||||
|
||||
# Write platform-specific Tauri config that adds signCommand for Windows.
|
||||
# Tauri auto-merges tauri.windows.conf.json with tauri.conf.json (RFC 7396).
|
||||
# Tauri calls this command on every binary BEFORE bundling into the MSI,
|
||||
# substituting %1 with the file path.
|
||||
#
|
||||
# Why OBJECT form (cmd + args) instead of string:
|
||||
# Tauri's string-form parser does a naive split(' ') with no shell/quote handling.
|
||||
# Args with spaces or quote characters get mangled. The object form passes each
|
||||
# arg directly to Rust's Command::arg which handles Windows CreateProcess quoting.
|
||||
#
|
||||
# Why --keypair-alias instead of --fingerprint:
|
||||
# --fingerprint requires smctl windows certsync to have synced the cert to the
|
||||
# Windows cert store first. --keypair-alias goes direct through PKCS11 and works
|
||||
# without certsync. All real-world working Tauri+smctl examples use this flag.
|
||||
#
|
||||
# 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: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
run: |
|
||||
cat > ./frontend/editor/src-tauri/tauri.windows.conf.json <<EOF
|
||||
{
|
||||
"bundle": {
|
||||
"windows": {
|
||||
"signCommand": {
|
||||
"cmd": "smctl",
|
||||
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Generated tauri.windows.conf.json (alias masked):"
|
||||
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')
|
||||
run: |
|
||||
echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --list-secret-keys --keyid-format=long
|
||||
|
||||
- name: Make libjvm discoverable for linuxdeploy (Linux AppImage)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
JAVA_LIBJVM="$JAVA_HOME/lib/server/libjvm.so"
|
||||
if [ -f "$JAVA_LIBJVM" ]; then
|
||||
sudo ln -sf "$JAVA_LIBJVM" /usr/lib/libjvm.so
|
||||
echo "Linked libjvm from $JAVA_LIBJVM -> /usr/lib/libjvm.so"
|
||||
else
|
||||
echo "libjvm not found at $JAVA_LIBJVM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -470,201 +394,169 @@ jobs:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
# AppImage signing — three env vars work together:
|
||||
# SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool
|
||||
# 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' }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
# DigiCert KeyLocker env vars consumed by smctl during signCommand
|
||||
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
|
||||
# Only enable Windows signing in Tauri when on release or V2-master
|
||||
SIGN: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
projectPath: ./frontend
|
||||
tauriScript: npx tauri
|
||||
args: ${{ matrix.args }}
|
||||
updaterJsonKeepUniversal: true
|
||||
|
||||
- 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')
|
||||
env:
|
||||
RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
run: |
|
||||
if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then
|
||||
gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true
|
||||
gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true
|
||||
fi
|
||||
|
||||
# Verify the MSI (outer wrapper users download) AND the inner exe extracted
|
||||
# from it (what actually gets installed and what AV scans). We don't check
|
||||
# target/.../release/stirling-pdf.exe - that's Tauri's intermediate build
|
||||
# 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: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
# Sign with DigiCert KeyLocker (post-build)
|
||||
- name: Sign Windows binaries with DigiCert KeyLocker
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$allSigned = $true
|
||||
Write-Host "=== DigiCert KeyLocker Signing ==="
|
||||
|
||||
# Check MSI installer (outer wrapper - what users download)
|
||||
$msiFiles = Get-ChildItem -Path "./frontend/editor/src-tauri/target" -Filter "*.msi" -Recurse -File
|
||||
if ($msiFiles.Count -eq 0) {
|
||||
Write-Host "[ERROR] No MSI found under target/"
|
||||
# Test smctl connectivity first
|
||||
Write-Host "Testing smctl connection..."
|
||||
$healthCheck = & smctl healthcheck 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
|
||||
} else {
|
||||
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
|
||||
Write-Host $healthCheck
|
||||
exit 1
|
||||
}
|
||||
foreach ($msi in $msiFiles) {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $msi.FullName
|
||||
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] MSI is not signed"
|
||||
$allSigned = $false
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Sync certificates to Windows certificate store
|
||||
Write-Host "Syncing certificates to Windows certificate store..."
|
||||
$syncOutput = & smctl windows certsync 2>&1
|
||||
Write-Host "Cert sync result: $syncOutput"
|
||||
Write-Host ""
|
||||
|
||||
# Find only the files we need to sign
|
||||
$filesToSign = @()
|
||||
|
||||
# Main application executable
|
||||
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
|
||||
if ($mainExe) { $filesToSign += $mainExe }
|
||||
|
||||
# MSI installer
|
||||
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
||||
$filesToSign += $msiFiles
|
||||
|
||||
if ($filesToSign.Count -eq 0) {
|
||||
Write-Host "[ERROR] No files found to sign"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Extract MSI and verify the inner exe (the file that actually gets installed).
|
||||
# This is the critical check - AV flags the installed exe at runtime.
|
||||
$msi = $msiFiles[0].FullName
|
||||
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
|
||||
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
||||
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msi, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
|
||||
if ($proc.ExitCode -eq 0) {
|
||||
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
|
||||
if ($innerExe) {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
|
||||
Write-Host "Inner EXE (from MSI): Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] Inner exe extracted from MSI is NOT signed - AV will flag this at runtime"
|
||||
$allSigned = $false
|
||||
}
|
||||
Write-Host "Found $($filesToSign.Count) files to sign:"
|
||||
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
|
||||
Write-Host ""
|
||||
|
||||
$signedCount = 0
|
||||
foreach ($file in $filesToSign) {
|
||||
Write-Host "Signing: $($file.Name)"
|
||||
|
||||
# Get PKCS11 config file path
|
||||
$pkcs11Config = $env:PKCS11_CONFIG
|
||||
if (-not $pkcs11Config) {
|
||||
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Using PKCS11 config: $pkcs11Config"
|
||||
|
||||
# Try signing with certificate fingerprint first (if available)
|
||||
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
|
||||
if ($fingerprint -and $fingerprint -ne "") {
|
||||
Write-Host "Attempting to sign with certificate fingerprint..."
|
||||
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
} else {
|
||||
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
|
||||
$allSigned = $false
|
||||
Write-Host "No fingerprint provided, using keypair alias..."
|
||||
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
} else {
|
||||
Write-Host "[ERROR] Failed to extract MSI for verification (exit code: $($proc.ExitCode))"
|
||||
$allSigned = $false
|
||||
}
|
||||
|
||||
if (-not $allSigned) {
|
||||
Write-Host "[ERROR] Signature verification failed"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[SUCCESS] MSI and installed exe are properly signed"
|
||||
Write-Host "Exit code: $exitCode"
|
||||
Write-Host "Output: $output"
|
||||
|
||||
# Dump smctl log files on failure. Tauri's signCommand captures smctl output
|
||||
# but drops stderr when the command exits non-zero, making failures opaque.
|
||||
# The real errors live in smctl's log files - surface them here for debugging.
|
||||
- name: Dump smctl logs on failure
|
||||
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||
if (Test-Path $logDir) {
|
||||
Get-ChildItem $logDir | ForEach-Object {
|
||||
Write-Host "=== $($_.FullName) ==="
|
||||
Get-Content $_.FullName -Tail 200
|
||||
Write-Host ""
|
||||
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
|
||||
Write-Host "[ERROR] Signing failed for $($file.Name)"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "smctl log directory not found at $logDir"
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
Write-Host "[ERROR] Failed to sign $($file.Name)"
|
||||
Write-Host "Full error output:"
|
||||
Write-Host $output
|
||||
exit 1
|
||||
}
|
||||
|
||||
$signedCount++
|
||||
Write-Host "[SUCCESS] Signed: $($file.Name)"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Rename + Upload: use always() so artifacts are still collected when verify
|
||||
# fails - we need them to manually inspect what actually came out of the build.
|
||||
Write-Host "=== Summary ==="
|
||||
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
|
||||
|
||||
- name: Rename artifacts
|
||||
if: always() && steps.digicert-setup.conclusion != 'failure'
|
||||
shell: bash
|
||||
run: |
|
||||
# Absolute dist path so the cd below can't break the copy targets.
|
||||
DIST="$GITHUB_WORKSPACE/dist"
|
||||
mkdir -p "$DIST"
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
mkdir -p ./dist
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
echo "=== tauri bundle artifacts ==="
|
||||
find . -path "*/bundle/*" \( -name "*.msi" -o -name "*.deb" \
|
||||
-o -name "*.rpm" -o -name "*.AppImage" -o -name "*.dmg" \
|
||||
-o -name "*.app.tar.gz" -o -name "*.sig" \) 2>/dev/null | sort || true
|
||||
echo "=============================="
|
||||
|
||||
# createUpdaterArtifacts:true signs the native installers in place;
|
||||
# each <bundle> ships with a sibling <bundle>.sig consumed by latest.json.
|
||||
# Find and rename artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
find . -name "*.msi.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi.sig" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
# DMG = manual install; .app.tar.gz (+ .sig) = updater payload.
|
||||
# Raw .app is intentionally not shipped (hundreds of MB of uncompressed input).
|
||||
find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.app.tar.gz" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.app.tar.gz" \;
|
||||
find . -name "*.app.tar.gz.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.app.tar.gz.sig" \;
|
||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
else
|
||||
# The raw .AppImage IS its updater payload (signed -> .AppImage.sig),
|
||||
# not a .tar.gz wrapper - that's only produced under v1Compatible.
|
||||
find . -name "*.deb" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.deb.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.deb.sig" \;
|
||||
find . -name "*.rpm" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.rpm" \;
|
||||
find . -name "*.rpm.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.rpm.sig" \;
|
||||
find . -name "*.AppImage" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
find . -name "*.AppImage.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.AppImage.sig" \;
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: always() && steps.digicert-setup.conclusion != 'failure'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.name }}
|
||||
path: ./dist/*
|
||||
retention-days: 1
|
||||
|
||||
collect-and-release:
|
||||
needs: [pick, determine-matrix, build, build-jars]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
create-release:
|
||||
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master'
|
||||
needs: [determine-matrix, build, build-jars]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
# Sparse-check out the verifier + pubkey before the artifact downloads
|
||||
# so the checkout cannot clobber ./artifacts.
|
||||
- name: Checkout updater verifier
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts/verify-updater-signatures.py
|
||||
frontend/editor/src-tauri/tauri.conf.json
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Download all Tauri artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
pattern: Stirling-PDF-*
|
||||
path: ./artifacts/tauri
|
||||
|
||||
- name: Download JAR artifact (default)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: jar
|
||||
path: ./artifacts/jars
|
||||
|
||||
- name: Download JAR artifact (with login)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: jar-with-login
|
||||
path: ./artifacts/jars
|
||||
|
||||
- name: Download JAR artifact (server only)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: jar-server
|
||||
path: ./artifacts/jars
|
||||
@@ -672,146 +564,16 @@ jobs:
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R ./artifacts
|
||||
|
||||
# tauri-action only emits latest.json when it also publishes the release
|
||||
# (tagName/releaseId set). We publish separately via action-gh-release,
|
||||
# so build latest.json here from the per-platform .sig files.
|
||||
- name: Generate updater latest.json
|
||||
env:
|
||||
VERSION: ${{ needs.determine-matrix.outputs.version }}
|
||||
TAG: v${{ needs.determine-matrix.outputs.version }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
python3 - << 'PYEOF'
|
||||
import json, os, sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
VERSION = os.environ['VERSION']
|
||||
TAG = os.environ['TAG']
|
||||
REPO = os.environ['REPO']
|
||||
|
||||
ART = Path('./artifacts/tauri')
|
||||
|
||||
# Tauri updater looks up {os}-{arch}-{installer} (e.g. linux-x86_64-deb)
|
||||
# before bare {os}-{arch}, so per-format Linux keys let deb/rpm/appimage
|
||||
# each self-update from their matching file. macOS universal serves both
|
||||
# arches from the one .app.tar.gz.
|
||||
PLATFORM_MAP = [
|
||||
{
|
||||
'bundles': ['Stirling-PDF-linux-x86_64.deb'],
|
||||
'targets': ['linux-x86_64-deb'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-linux-x86_64.rpm'],
|
||||
'targets': ['linux-x86_64-rpm'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-linux-x86_64.AppImage'],
|
||||
'targets': ['linux-x86_64-appimage'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-windows-x86_64.msi'],
|
||||
'targets': ['windows-x86_64-msi', 'windows-x86_64'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-macos-universal.app.tar.gz'],
|
||||
'targets': ['darwin-x86_64', 'darwin-aarch64'],
|
||||
},
|
||||
]
|
||||
|
||||
# rglob() because download-artifact varies layout: one artifact -> flat,
|
||||
# many -> nested under <artifact-name>/.
|
||||
def find_signed(name):
|
||||
for bundle_path in sorted(ART.rglob(name)):
|
||||
sig_path = bundle_path.with_name(bundle_path.name + '.sig')
|
||||
if sig_path.exists():
|
||||
return bundle_path, sig_path
|
||||
return None
|
||||
|
||||
platforms = {}
|
||||
skipped = []
|
||||
for entry in PLATFORM_MAP:
|
||||
picked = None
|
||||
for name in entry['bundles']:
|
||||
picked = find_signed(name)
|
||||
if picked:
|
||||
break
|
||||
if not picked:
|
||||
skipped.append(
|
||||
f"{entry['targets']} (no signed bundle among "
|
||||
f"{entry['bundles']} - TAURI_SIGNING_PRIVATE_KEY unset "
|
||||
f"or createUpdaterArtifacts disabled?)"
|
||||
)
|
||||
continue
|
||||
bundle_path, sig_path = picked
|
||||
signature = sig_path.read_text(encoding='utf-8').strip()
|
||||
url = f"https://github.com/{REPO}/releases/download/{TAG}/{bundle_path.name}"
|
||||
for target in entry['targets']:
|
||||
platforms[target] = {'signature': signature, 'url': url}
|
||||
print(f"Added {entry['targets']} from {bundle_path.name}")
|
||||
|
||||
if skipped:
|
||||
print("Skipped platforms:")
|
||||
for s in skipped:
|
||||
print(f" - {s}")
|
||||
|
||||
if not platforms:
|
||||
print(
|
||||
"WARN: no signed updater bundles found - "
|
||||
"skipping latest.json generation"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
manifest = {
|
||||
'version': VERSION,
|
||||
'notes': f"See https://github.com/{REPO}/releases/tag/{TAG}",
|
||||
'pub_date': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||
'platforms': platforms,
|
||||
}
|
||||
|
||||
out = Path('./artifacts/latest.json')
|
||||
out.write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8')
|
||||
print(f"Generated {out} with platforms: {sorted(platforms.keys())}")
|
||||
PYEOF
|
||||
|
||||
- name: Upload merged artifacts for review
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: release-artifacts
|
||||
path: ./artifacts/
|
||||
retention-days: 7
|
||||
|
||||
# Gate publish on valid updater sigs. Runs after the review upload (so
|
||||
# artifacts survive for debugging) and before action-gh-release.
|
||||
- name: Verify updater signatures
|
||||
run: |
|
||||
python3 -m pip install --quiet 'cryptography==44.0.0'
|
||||
python3 .github/scripts/verify-updater-signatures.py \
|
||||
./artifacts/tauri frontend/editor/src-tauri/tauri.conf.json
|
||||
|
||||
# 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.
|
||||
# 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'
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
# Installers + updater payloads + manifest. .sig contents are embedded
|
||||
# in latest.json so the .sig files themselves are not uploaded.
|
||||
files: |
|
||||
./artifacts/**/*.jar
|
||||
./artifacts/**/*.msi
|
||||
./artifacts/**/*.dmg
|
||||
./artifacts/**/*.app.tar.gz
|
||||
./artifacts/**/*.deb
|
||||
./artifacts/**/*.rpm
|
||||
./artifacts/**/*.AppImage
|
||||
./artifacts/latest.json
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
name: Nightly E2E Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *" # 2 AM UTC every night
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
playwright-all-browsers:
|
||||
name: Playwright (chromium + firefox + webkit)
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Install all Playwright browsers
|
||||
run: task e2e:install
|
||||
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
run: task frontend:build
|
||||
|
||||
- name: Run E2E tests (all browsers)
|
||||
run: task e2e:cross-browser
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report-nightly-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 14
|
||||
|
||||
# Builds all desktop platforms on a schedule so the Rust dependency cache is
|
||||
# written on main, where PR and merge-queue tauri builds can restore it.
|
||||
warm-tauri-cache:
|
||||
name: Warm Tauri Rust cache
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/tauri-build.yml
|
||||
with:
|
||||
platform: all
|
||||
sign: false
|
||||
secrets: inherit
|
||||
@@ -1,155 +0,0 @@
|
||||
name: Update Package Manager Manifests
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to test (e.g. 2.9.2 — no v prefix)"
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Skip the git push at the end (safe test)"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
get-release-info:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.info.outputs.version }}
|
||||
dmg_sha256: ${{ steps.hashes.outputs.dmg_sha256 }}
|
||||
msi_sha256: ${{ steps.hashes.outputs.msi_sha256 }}
|
||||
deb_sha256: ${{ steps.hashes.outputs.deb_sha256 }}
|
||||
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Extract version from tag or manual input
|
||||
id: info
|
||||
env:
|
||||
DISPATCH_VERSION: ${{ inputs.version }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION="$DISPATCH_VERSION"
|
||||
else
|
||||
VERSION="$RELEASE_TAG"
|
||||
fi
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download release assets and compute SHA256
|
||||
id: hashes
|
||||
env:
|
||||
VERSION: ${{ steps.info.outputs.version }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
BASE="https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${VERSION}"
|
||||
|
||||
download_sha256() {
|
||||
local url="$1"
|
||||
local file
|
||||
file=$(basename "$url")
|
||||
curl -fsSL --retry 3 -o "$file" "$url"
|
||||
sha256sum "$file" | awk '{print $1}'
|
||||
}
|
||||
|
||||
DMG_SHA=$(download_sha256 "${BASE}/Stirling-PDF-macos-universal.dmg")
|
||||
MSI_SHA=$(download_sha256 "${BASE}/Stirling-PDF-windows-x86_64.msi")
|
||||
DEB_SHA=$(download_sha256 "${BASE}/Stirling-PDF-linux-x86_64.deb")
|
||||
JAR_SHA=$(download_sha256 "${BASE}/Stirling-PDF-with-login.jar")
|
||||
|
||||
echo "dmg_sha256=$DMG_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "msi_sha256=$MSI_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
update-homebrew-and-scoop:
|
||||
needs: get-release-info
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout homebrew-stirling-pdf tap (also hosts Scoop bucket)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: Stirling-Tools/homebrew-stirling-pdf
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
path: tap
|
||||
|
||||
# Stirling-PDF now ships a single universal DMG, so the Cask should
|
||||
# have one sha256 line rather than separate on_arm/on_intel blocks.
|
||||
# We rewrite every `sha256 "..."` in the Cask to the same value so
|
||||
# the workflow keeps working through the Cask migration: pre-migration
|
||||
# both arch blocks get the universal SHA (still correct, since both
|
||||
# would resolve to the same DMG), post-migration the lone sha256 line
|
||||
# is updated.
|
||||
- name: Update Homebrew cask (Casks/stirling-pdf.rb)
|
||||
env:
|
||||
VERSION: ${{ needs.get-release-info.outputs.version }}
|
||||
DMG_SHA: ${{ needs.get-release-info.outputs.dmg_sha256 }}
|
||||
run: |
|
||||
CASK="tap/Casks/stirling-pdf.rb"
|
||||
sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "$CASK"
|
||||
sed -i "s/sha256 \"[^\"]*\"/sha256 \"${DMG_SHA}\"/g" "$CASK"
|
||||
|
||||
- name: Update Homebrew formula (Formula/stirling-pdf-server.rb)
|
||||
env:
|
||||
VERSION: ${{ needs.get-release-info.outputs.version }}
|
||||
JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
|
||||
run: |
|
||||
FORMULA="tap/Formula/stirling-pdf-server.rb"
|
||||
sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "$FORMULA"
|
||||
sed -i "s/sha256 \"[^\"]*\"/sha256 \"${JAR_SHA}\"/" "$FORMULA"
|
||||
|
||||
- name: Update Scoop stirling-pdf.json
|
||||
env:
|
||||
VERSION: ${{ needs.get-release-info.outputs.version }}
|
||||
MSI_SHA: ${{ needs.get-release-info.outputs.msi_sha256 }}
|
||||
run: |
|
||||
MANIFEST="tap/scoop/stirling-pdf.json"
|
||||
jq --arg v "$VERSION" --arg h "$MSI_SHA" \
|
||||
'.version = $v | .architecture["64bit"].url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-windows-x86_64.msi" | .architecture["64bit"].hash = $h' \
|
||||
"$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST"
|
||||
|
||||
- name: Update Scoop stirling-pdf-server.json
|
||||
env:
|
||||
VERSION: ${{ needs.get-release-info.outputs.version }}
|
||||
JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
|
||||
run: |
|
||||
MANIFEST="tap/scoop/stirling-pdf-server.json"
|
||||
jq --arg v "$VERSION" --arg h "$JAR_SHA" \
|
||||
'.version = $v | .url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-with-login.jar" | .hash = $h' \
|
||||
"$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST"
|
||||
|
||||
- name: Show tap diff (for dry-run visibility)
|
||||
working-directory: tap
|
||||
run: |
|
||||
echo "--- diff --stat ---"
|
||||
git diff --stat
|
||||
echo "--- full diff ---"
|
||||
git diff
|
||||
|
||||
- name: Commit and push all tap updates
|
||||
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
|
||||
working-directory: tap
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add Casks/stirling-pdf.rb Formula/stirling-pdf-server.rb scoop/stirling-pdf.json scoop/stirling-pdf-server.json
|
||||
git diff --cached --quiet && echo "No changes" && exit 0
|
||||
git commit -m "chore: bump Stirling-PDF to v${{ needs.get-release-info.outputs.version }}"
|
||||
git push
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Pre-commit
|
||||
|
||||
# Runs the repo-wide lint/format/secret checks via `task pre-commit`.
|
||||
# Called from build.yml on PRs and merge_group; also runnable on demand via workflow_dispatch.
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -12,25 +12,75 @@ permissions:
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Prevents sdist builds → no tar extraction
|
||||
PIP_ONLY_BINARY: ":all:"
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
enable-cache: true
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: "pip" # caching pip dependencies
|
||||
cache-dependency-path: ./.github/scripts/requirements_pre_commit.txt
|
||||
|
||||
- name: Run pre-commit checks
|
||||
run: task pre-commit
|
||||
- name: Run Pre-Commit Hooks
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_pre_commit.txt
|
||||
|
||||
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
||||
continue-on-error: true
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build
|
||||
|
||||
- name: git add
|
||||
run: |
|
||||
git add .
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: ":file_folder: pre-commit"
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: pre-commit
|
||||
title: "🤖 format everything with pre-commit by ${{ steps.setup-bot.outputs.app-slug }}"
|
||||
body: |
|
||||
Auto-generated by [create-pull-request][1] with **${{ steps.setup-bot.outputs.app-slug }}**
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
draft: false
|
||||
delete-branch: true
|
||||
labels: github-actions
|
||||
sign-commits: true
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
name: Push Docker Base Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- baseDockerImage
|
||||
- accessIssueFix
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Base image version (e.g., 1.0.0, 1.0.1)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
push-base:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' && github.actor == 'Frooodle' }}
|
||||
runs-on: ubuntu-24.04-8core
|
||||
permissions:
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Verify authorized user
|
||||
run: |
|
||||
if [ "${{ github.actor }}" != "Frooodle" ]; then
|
||||
echo "Error: Only Frooodle is authorized to run this workflow"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then
|
||||
VERSION="1.0.3"
|
||||
else
|
||||
VERSION="1.0.0"
|
||||
fi
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate tags for base image
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf-base
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-base
|
||||
tags: |
|
||||
type=raw,value=${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Build and push base image
|
||||
id: build-push-base
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: docker/base
|
||||
file: ./docker/base/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-base
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-base
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Sign base images
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-base.outputs.digest }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "$COSIGN_PRIVATE_KEY" ]; then
|
||||
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||
cosign sign --yes \
|
||||
--key env://COSIGN_PRIVATE_KEY \
|
||||
"${tag}@${DIGEST}"
|
||||
done
|
||||
else
|
||||
echo "Warning: COSIGN_PRIVATE_KEY not set, skipping image signing"
|
||||
fi
|
||||
@@ -2,22 +2,6 @@ name: Push Docker Image with VersionNumber
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_main_app:
|
||||
description: "Build & push the main Stirling-PDF image (latest, fat, ultra-lite)."
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
build_unoserver:
|
||||
description: "Build & push the standalone stirling-unoserver image."
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
force_unoserver_rebuild:
|
||||
description: "Rebuild stirling-unoserver even if its source hash is unchanged."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -47,80 +31,53 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
id-token: write
|
||||
# On push events these stay 'true'; on workflow_dispatch they follow the inputs.
|
||||
env:
|
||||
RUN_MAIN_APP: ${{ github.event_name != 'workflow_dispatch' || inputs.build_main_app }}
|
||||
RUN_UNOSERVER: ${{ github.event_name != 'workflow_dispatch' || inputs.build_unoserver }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
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'
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
@@ -128,8 +85,7 @@ jobs:
|
||||
|
||||
- name: Generate tags for latest
|
||||
id: meta
|
||||
if: env.RUN_MAIN_APP == 'true'
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -143,27 +99,23 @@ jobs:
|
||||
|
||||
- name: Build and push Unified Dockerfile (latest variant)
|
||||
id: build-push-latest
|
||||
# Empty-tag guard: build-push-action errors when asked to push with no tags.
|
||||
if: env.RUN_MAIN_APP == 'true' && steps.meta.outputs.tags != ''
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
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
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# No BASE_VERSION pin: inherit the Dockerfile ARG default (single source of truth).
|
||||
build-args: |
|
||||
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
provenance: true
|
||||
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: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-latest.outputs.digest }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
@@ -178,8 +130,8 @@ jobs:
|
||||
|
||||
- name: Generate tags for latest-fat
|
||||
id: meta-fat
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -192,15 +144,15 @@ jobs:
|
||||
|
||||
- name: Build and push Unified Dockerfile (fat variant)
|
||||
id: build-push-fat
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' && steps.meta-fat.outputs.tags != ''
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile.fat
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-fat
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-fat
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta-fat.outputs.tags }}
|
||||
labels: ${{ steps.meta-fat.outputs.labels }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
@@ -209,7 +161,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: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
|
||||
TAGS: ${{ steps.meta-fat.outputs.tags }}
|
||||
@@ -222,8 +174,8 @@ jobs:
|
||||
|
||||
- name: Generate tags for ultra-lite
|
||||
id: meta-lite
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -236,15 +188,15 @@ jobs:
|
||||
|
||||
- name: Build and push Unified Dockerfile (ultra-lite variant)
|
||||
id: build-push-lite
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' && steps.meta-lite.outputs.tags != ''
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile.ultra-lite
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-pdf-ultra-lite
|
||||
cache-to: type=gha,mode=max,scope=stirling-pdf-ultra-lite
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta-lite.outputs.tags }}
|
||||
labels: ${{ steps.meta-lite.outputs.labels }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
@@ -253,7 +205,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: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
|
||||
TAGS: ${{ steps.meta-lite.outputs.tags }}
|
||||
@@ -263,136 +215,3 @@ jobs:
|
||||
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
|
||||
done
|
||||
|
||||
# Standalone unoserver image — versioned independently via
|
||||
# docker/unoserver/VERSION. master/V2-master: publish <version>+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
|
||||
id: unoserverVersion
|
||||
if: env.RUN_UNOSERVER == 'true'
|
||||
run: |
|
||||
version=$(tr -d '[:space:]' < docker/unoserver/VERSION)
|
||||
if [ -z "$version" ]; then
|
||||
echo "docker/unoserver/VERSION is empty"; exit 1
|
||||
fi
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "Unoserver image version (from file): ${version}"
|
||||
|
||||
- name: Compute unoserver image source hash
|
||||
id: unoserverHash
|
||||
if: env.RUN_UNOSERVER == 'true'
|
||||
run: |
|
||||
set -eu
|
||||
hash=$(cat \
|
||||
docker/unoserver/Dockerfile \
|
||||
docker/unoserver/entrypoint.sh \
|
||||
docker/unoserver/healthcheck.sh \
|
||||
docker/unoserver/VERSION \
|
||||
| sha256sum | cut -d' ' -f1)
|
||||
echo "hash=${hash}" >> "$GITHUB_OUTPUT"
|
||||
echo "Unoserver source hash: ${hash}"
|
||||
|
||||
- name: Decide whether to publish unoserver image
|
||||
id: unoserverDecision
|
||||
if: env.RUN_UNOSERVER == 'true'
|
||||
env:
|
||||
UNOSERVER_VERSION: ${{ steps.unoserverVersion.outputs.version }}
|
||||
UNOSERVER_HASH: ${{ steps.unoserverHash.outputs.hash }}
|
||||
UNOSERVER_IMAGE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-unoserver
|
||||
UNOSERVER_HASH_ANNOTATION: org.stirlingpdf.unoserver-source-hash
|
||||
FORCE_REBUILD: ${{ inputs.force_unoserver_rebuild }}
|
||||
GH_REF: ${{ github.ref }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set -eu
|
||||
mode="skip"
|
||||
tags=""
|
||||
|
||||
read_published_hash() {
|
||||
local ref="$1"
|
||||
docker buildx imagetools inspect "$ref" --raw 2>/dev/null \
|
||||
| jq -r --arg key "$UNOSERVER_HASH_ANNOTATION" \
|
||||
'.annotations[$key] // empty' \
|
||||
2>/dev/null || true
|
||||
}
|
||||
|
||||
# Manual dispatch from any branch routes to the :alpha publish path.
|
||||
EFFECTIVE_REF="$GH_REF"
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
EFFECTIVE_REF="refs/heads/testMain"
|
||||
fi
|
||||
|
||||
case "$EFFECTIVE_REF" in
|
||||
refs/heads/master|refs/heads/V2-master)
|
||||
if [ "${FORCE_REBUILD}" = "true" ]; then
|
||||
echo "force_unoserver_rebuild=true — building stable regardless"
|
||||
mode="stable"
|
||||
tags="${UNOSERVER_IMAGE}:${UNOSERVER_VERSION},${UNOSERVER_IMAGE}:latest"
|
||||
elif docker manifest inspect "${UNOSERVER_IMAGE}:${UNOSERVER_VERSION}" >/dev/null 2>&1; then
|
||||
echo "stirling-unoserver:${UNOSERVER_VERSION} already on GHCR — skipping"
|
||||
else
|
||||
echo "stirling-unoserver:${UNOSERVER_VERSION} is new — will publish"
|
||||
mode="stable"
|
||||
tags="${UNOSERVER_IMAGE}:${UNOSERVER_VERSION},${UNOSERVER_IMAGE}:latest"
|
||||
fi
|
||||
;;
|
||||
refs/heads/main|refs/heads/testMain)
|
||||
published_hash=$(read_published_hash "${UNOSERVER_IMAGE}:alpha")
|
||||
if [ "${FORCE_REBUILD}" = "true" ]; then
|
||||
echo "force_unoserver_rebuild=true — rebuilding :alpha regardless"
|
||||
mode="alpha"
|
||||
tags="${UNOSERVER_IMAGE}:alpha"
|
||||
elif [ -n "$published_hash" ] && [ "$published_hash" = "$UNOSERVER_HASH" ]; then
|
||||
echo "Published :alpha source hash matches (${published_hash}) — skipping"
|
||||
else
|
||||
if [ -z "$published_hash" ]; then
|
||||
echo ":alpha has no source-hash annotation (first publish or pre-tracking image) — will publish"
|
||||
else
|
||||
echo "Source hash changed (was ${published_hash}, now ${UNOSERVER_HASH}) — will publish"
|
||||
fi
|
||||
mode="alpha"
|
||||
tags="${UNOSERVER_IMAGE}:alpha"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Branch ${GH_REF} does not publish unoserver image"
|
||||
;;
|
||||
esac
|
||||
echo "mode=${mode}" >> "$GITHUB_OUTPUT"
|
||||
echo "tags=${tags}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push unoserver image
|
||||
id: build-push-unoserver
|
||||
if: env.RUN_UNOSERVER == 'true' && steps.unoserverDecision.outputs.mode != 'skip'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/unoserver/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha,scope=stirling-unoserver
|
||||
cache-to: type=gha,mode=max,scope=stirling-unoserver
|
||||
tags: ${{ steps.unoserverDecision.outputs.tags }}
|
||||
# Manifest annotation read by the decision step above to detect drift.
|
||||
annotations: |
|
||||
index:org.stirlingpdf.unoserver-source-hash=${{ steps.unoserverHash.outputs.hash }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Sign unoserver image
|
||||
if: env.RUN_UNOSERVER == 'true' && steps.unoserverDecision.outputs.mode == 'stable'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-unoserver.outputs.digest }}
|
||||
TAGS: ${{ steps.unoserverDecision.outputs.tags }}
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
run: |
|
||||
if [ -n "$COSIGN_PRIVATE_KEY" ]; then
|
||||
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
|
||||
done
|
||||
else
|
||||
echo "Warning: COSIGN_PRIVATE_KEY not set, skipping unoserver image signing"
|
||||
fi
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
name: Rollback Latest Tags to Version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to rollback to (e.g. 2.8.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rollback:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install crane
|
||||
uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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: Rollback all latest tags to v${{ inputs.version }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
DOCKER_HUB_ORG_USERNAME: ${{ secrets.DOCKER_HUB_ORG_USERNAME }}
|
||||
REPO_OWNER: ${{ steps.repoowner.outputs.lowercase }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
IMAGES=(
|
||||
"${DOCKER_HUB_USERNAME}/s-pdf"
|
||||
"ghcr.io/${REPO_OWNER}/s-pdf"
|
||||
"ghcr.io/${REPO_OWNER}/stirling-pdf"
|
||||
"${DOCKER_HUB_ORG_USERNAME}/stirling-pdf"
|
||||
)
|
||||
|
||||
VARIANTS=(
|
||||
"${VERSION}:latest"
|
||||
"${VERSION}-fat:latest-fat"
|
||||
"${VERSION}-ultra-lite:latest-ultra-lite"
|
||||
)
|
||||
|
||||
FAILED=0
|
||||
|
||||
for image in "${IMAGES[@]}"; do
|
||||
for variant in "${VARIANTS[@]}"; do
|
||||
SOURCE_TAG="${variant%%:*}"
|
||||
TARGET_TAG="${variant##*:}"
|
||||
|
||||
echo "::group::${image} — ${SOURCE_TAG} → ${TARGET_TAG}"
|
||||
|
||||
if crane manifest "${image}:${SOURCE_TAG}" > /dev/null 2>&1; then
|
||||
crane cp "${image}:${SOURCE_TAG}" "${image}:${TARGET_TAG}"
|
||||
echo "✅ ${image}:${TARGET_TAG} now points to ${SOURCE_TAG}"
|
||||
else
|
||||
echo "::warning::⚠️ ${image}:${SOURCE_TAG} not found, skipping"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "::warning::Some source tags were not found. This is expected if not all variants exist for this version."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 Rollback to ${VERSION} complete!"
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 30 days stale issues
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
|
||||
@@ -22,33 +22,24 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
push:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
@@ -56,21 +47,12 @@ jobs:
|
||||
- name: Upload Swagger Documentation to SwaggerHub
|
||||
run: ./gradlew swaggerhubUpload
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
||||
SWAGGERHUB_USER: "Frooodle"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
- name: Set API version as published and default on SwaggerHub
|
||||
run: |
|
||||
|
||||
@@ -7,11 +7,8 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "app/common/build.gradle"
|
||||
- "app/core/build.gradle"
|
||||
- "app/proprietary/build.gradle"
|
||||
- "README.md"
|
||||
- "frontend/editor/public/locales/*/translation.toml"
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
@@ -35,13 +32,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -51,34 +46,21 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||
|
||||
- name: Sort translation TOML files
|
||||
run: |
|
||||
task pre-commit:toml-sort FIX=1
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/public/locales/en-GB/translation.toml" --branch main
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add frontend/editor/public/locales/*/translation.toml
|
||||
git add frontend/public/locales/*/translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files (TOML)" || echo "No changes detected"
|
||||
|
||||
- name: Sync README.md
|
||||
@@ -92,7 +74,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: Update files
|
||||
@@ -108,7 +90,7 @@ jobs:
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`frontend/editor/public/locales/*/translation.toml`) to reflect changes in the reference file `en-US/translation.toml`.
|
||||
- Updated translation files (`frontend/public/locales/*/translation.toml`) to reflect changes in the reference file `en-GB/translation.toml`.
|
||||
- Ensured consistency and synchronization across all supported language files.
|
||||
- Highlighted any missing or incomplete translations.
|
||||
- **Format**: TOML
|
||||
@@ -133,5 +115,5 @@ jobs:
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
frontend/editor/public/locales/*/translation.toml
|
||||
frontend/public/locales/*/translation.toml
|
||||
scripts/ignore_translation.toml
|
||||
|
||||
+340
-410
@@ -1,26 +1,6 @@
|
||||
name: Build Tauri Applications
|
||||
|
||||
# Multi-OS Tauri desktop bundle build matrix (Windows / macOS universal /
|
||||
# Linux). Called from build.yml on PRs that touch desktop sources (gated
|
||||
# via the `tauri` filter in .github/config/.files.yaml). Also runnable
|
||||
# on demand via workflow_dispatch with a per-platform selector.
|
||||
#
|
||||
# Note: editing this file is itself enough to make the `tauri` path filter
|
||||
# match, which is how non-desktop PRs (e.g. backend-only fixes) opt into a
|
||||
# desktop smoke build.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, macos, linux, or all)."
|
||||
required: false
|
||||
type: string
|
||||
default: "all"
|
||||
sign:
|
||||
description: "Sign and notarize the bundles."
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
@@ -33,15 +13,18 @@ on:
|
||||
- windows
|
||||
- macos
|
||||
- linux
|
||||
sign:
|
||||
description: "Sign and notarize the bundles."
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
pull_request:
|
||||
branches: [main, V2-tauri-windows]
|
||||
paths:
|
||||
- "frontend/src-tauri/**"
|
||||
- "frontend/src/desktop/**"
|
||||
- "frontend/tsconfig.desktop.json"
|
||||
- ".github/workflows/tauri-build.yml"
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
@@ -50,41 +33,29 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
||||
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}'
|
||||
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}'
|
||||
|
||||
case "$PLATFORM" in
|
||||
windows) ENTRIES=("$WINDOWS") ;;
|
||||
macos) ENTRIES=("$MACOS") ;;
|
||||
linux) ENTRIES=("$LINUX") ;;
|
||||
*) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;;
|
||||
esac
|
||||
|
||||
# Drop macOS entries when Apple certificate secret is unavailable
|
||||
if [ -z "$APPLE_CERTIFICATE" ]; then
|
||||
echo "⚠️ APPLE_CERTIFICATE secret not available - skipping macOS builds"
|
||||
FILTERED=()
|
||||
for entry in "${ENTRIES[@]}"; do
|
||||
[[ "$entry" != *'"macos'* ]] && FILTERED+=("$entry")
|
||||
done
|
||||
ENTRIES=("${FILTERED[@]}")
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For PR/push events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
JOINED=$(IFS=','; echo "${ENTRIES[*]}")
|
||||
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: determine-matrix
|
||||
strategy:
|
||||
@@ -94,17 +65,14 @@ jobs:
|
||||
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 }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
@@ -113,82 +81,106 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
# Cache the Cargo registry and compiled dependency crates so the build
|
||||
# only recompiles the app crate. Written on main; PRs and the merge queue
|
||||
# restore from it.
|
||||
- name: Cache Rust build
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
workspaces: frontend/editor/src-tauri
|
||||
# Stable key shared across workflows so the nightly warmer.
|
||||
# rust-cache still appends OS + rustc + Cargo.lock.
|
||||
shared-key: tauri-${{ matrix.name }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
# Save the dependency cache even if a later step fails
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
||||
if: matrix.platform == 'macos-15'
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Capture x86_64 JAVA_HOME
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
echo "🔧 Building Stirling-PDF JAR..."
|
||||
# STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
||||
echo "✅ Built JAR: $STIRLING_JAR"
|
||||
|
||||
- name: Build universal macOS JRE
|
||||
if: matrix.platform == 'macos-15'
|
||||
# Create Tauri directories
|
||||
mkdir -p ./frontend/src-tauri/libs
|
||||
mkdir -p ./frontend/src-tauri/runtime
|
||||
|
||||
# Copy JAR to Tauri libs
|
||||
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
|
||||
echo "✅ JAR copied to Tauri libs"
|
||||
|
||||
# Analyze JAR dependencies for jlink modules
|
||||
echo "🔍 Analyzing JAR dependencies..."
|
||||
if command -v jdeps &> /dev/null; then
|
||||
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
|
||||
if [ -n "$DETECTED_MODULES" ]; then
|
||||
echo "📋 jdeps detected modules: $DETECTED_MODULES"
|
||||
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
else
|
||||
echo "⚠️ jdeps analysis failed, using predefined modules"
|
||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ jdeps not available, using predefined modules"
|
||||
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
fi
|
||||
|
||||
# Create custom JRE with jlink (always rebuild)
|
||||
echo "🔧 Creating custom JRE with jlink..."
|
||||
echo "📋 Using modules: $MODULES"
|
||||
|
||||
# Remove any existing JRE
|
||||
rm -rf ./frontend/src-tauri/runtime/jre
|
||||
|
||||
# Create the custom JRE
|
||||
jlink \
|
||||
--add-modules "$MODULES" \
|
||||
--strip-debug \
|
||||
--compress=2 \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output ./frontend/src-tauri/runtime/jre
|
||||
|
||||
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
|
||||
echo "❌ Failed to create JLink runtime"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test the bundled runtime
|
||||
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
|
||||
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
|
||||
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
|
||||
else
|
||||
echo "❌ Custom JRE executable not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate runtime size
|
||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
||||
env:
|
||||
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:jlink:universal-mac
|
||||
|
||||
- name: Prepare desktop build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:prepare
|
||||
|
||||
- name: Run Tauri/Cargo tests
|
||||
run: task desktop:test
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm ci
|
||||
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
uses: digicert/ssm-code-signing@v1.1.0
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||
@@ -197,7 +189,7 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
@@ -232,7 +224,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
@@ -263,7 +255,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -284,7 +276,7 @@ jobs:
|
||||
rm certificate.p12
|
||||
|
||||
- name: Verify Certificate
|
||||
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
@@ -296,7 +288,7 @@ jobs:
|
||||
echo "Certificate imported successfully."
|
||||
|
||||
- name: Check DMG creation dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-15'
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
@@ -306,59 +298,8 @@ jobs:
|
||||
echo "Available tools:"
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Preflight smctl
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
run: |
|
||||
& smctl healthcheck
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
|
||||
& smctl keypair ls
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
|
||||
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
||||
|
||||
- name: Configure Windows code signing
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
run: |
|
||||
cat > ./frontend/editor/src-tauri/tauri.windows.conf.json <<EOF
|
||||
{
|
||||
"bundle": {
|
||||
"windows": {
|
||||
"signCommand": {
|
||||
"cmd": "smctl",
|
||||
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Import release GPG signing key (Linux)
|
||||
if: inputs.sign && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --list-secret-keys --keyid-format=long
|
||||
|
||||
- name: Make libjvm discoverable for linuxdeploy (Linux AppImage)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
JAVA_LIBJVM="$JAVA_HOME/lib/server/libjvm.so"
|
||||
if [ -f "$JAVA_LIBJVM" ]; then
|
||||
sudo ln -sf "$JAVA_LIBJVM" /usr/lib/libjvm.so
|
||||
echo "Linked libjvm from $JAVA_LIBJVM -> /usr/lib/libjvm.so"
|
||||
else
|
||||
echo "libjvm not found at $JAVA_LIBJVM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build Tauri app (signed)
|
||||
if: inputs.sign
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -367,87 +308,183 @@ jobs:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
# AppImage signing — three env vars work together:
|
||||
# SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool
|
||||
# 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 when secret is present AND ref is main (skips PRs from forks/Dependabot).
|
||||
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
|
||||
# Only enable Windows signing in Tauri when on main
|
||||
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
projectPath: ./frontend
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here. AppImage runs in its own
|
||||
# continue-on-error step below so its persistent linuxdeploy
|
||||
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
- name: Build Tauri app (unsigned)
|
||||
if: ${{ !inputs.sign }}
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SIGN: "0"
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here. AppImage runs in its own
|
||||
# continue-on-error step below so its persistent linuxdeploy
|
||||
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
||||
|
||||
# AppImage is decoupled so its linuxdeploy run gets a fresh process
|
||||
# (rpm scratch state torn down) and its failure can't tank deb/rpm.
|
||||
- name: Build Tauri app (Linux AppImage)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
continue-on-error: true
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SIGN: ${{ (inputs.sign && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }} # gitleaks:allow
|
||||
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
args: --bundles appimage
|
||||
|
||||
- name: Clear release GPG key from runner keyring (Linux)
|
||||
if: always() && inputs.sign && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
# Sign with DigiCert KeyLocker (post-build)
|
||||
- name: Sign Windows binaries with DigiCert KeyLocker
|
||||
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then
|
||||
gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true
|
||||
gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true
|
||||
fi
|
||||
Write-Host "=== DigiCert KeyLocker Signing ==="
|
||||
|
||||
# Test smctl connectivity first
|
||||
Write-Host "Testing smctl connection..."
|
||||
$healthCheck = & smctl healthcheck 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
|
||||
} else {
|
||||
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
|
||||
Write-Host $healthCheck
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Sync certificates to Windows certificate store
|
||||
Write-Host "Syncing certificates to Windows certificate store..."
|
||||
$syncOutput = & smctl windows certsync 2>&1
|
||||
Write-Host "Cert sync result: $syncOutput"
|
||||
Write-Host ""
|
||||
|
||||
# List available certificates and check if they have certificates attached
|
||||
Write-Host "Checking for available certificates..."
|
||||
$certList = & smctl keypair ls 2>&1
|
||||
Write-Host "Keypair list output:"
|
||||
Write-Host $certList
|
||||
Write-Host ""
|
||||
|
||||
# Parse the output to check certificate status
|
||||
$lines = $certList -split "`n"
|
||||
$foundKeypair = $false
|
||||
$hasCertificate = $false
|
||||
|
||||
foreach ($line in $lines) {
|
||||
if ($line -match "${{ secrets.SM_KEYPAIR_ALIAS }}") {
|
||||
$foundKeypair = $true
|
||||
Write-Host "[SUCCESS] Found keypair in list"
|
||||
|
||||
# Check if this line has certificate info (not just empty spaces after alias)
|
||||
$parts = $line -split "\s+"
|
||||
if ($parts.Count -gt 2 -and $parts[1] -ne "" -and $parts[1] -ne "CERTIFICATE") {
|
||||
$hasCertificate = $true
|
||||
Write-Host "[SUCCESS] Certificate is associated with keypair"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $foundKeypair) {
|
||||
Write-Host "[ERROR] Keypair not found: ${{ secrets.SM_KEYPAIR_ALIAS }}"
|
||||
Write-Host "Available keypairs are listed above"
|
||||
Write-Host ""
|
||||
Write-Host "Please verify:"
|
||||
Write-Host " 1. Keypair alias is correct in GitHub secret"
|
||||
Write-Host " 2. API key has access to this keypair"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $hasCertificate) {
|
||||
Write-Host "[ERROR] No certificate associated with keypair"
|
||||
Write-Host "This usually means:"
|
||||
Write-Host " 1. Certificate not yet synced to KeyLocker (run sync manually)"
|
||||
Write-Host " 2. Certificate is pending approval"
|
||||
Write-Host " 3. Certificate needs to be attached to the keypair"
|
||||
Write-Host ""
|
||||
Write-Host "Try running in DigiCert ONE portal:"
|
||||
Write-Host " smctl keypair sync"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[SUCCESS] Certificate check passed"
|
||||
Write-Host ""
|
||||
|
||||
# Find only the files we need to sign (not build scripts)
|
||||
$filesToSign = @()
|
||||
|
||||
# Main application executable
|
||||
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
|
||||
if ($mainExe) { $filesToSign += $mainExe }
|
||||
|
||||
# MSI installer
|
||||
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
||||
$filesToSign += $msiFiles
|
||||
|
||||
if ($filesToSign.Count -eq 0) {
|
||||
Write-Host "[ERROR] No files found to sign"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Found $($filesToSign.Count) files to sign:"
|
||||
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
|
||||
Write-Host ""
|
||||
|
||||
$signedCount = 0
|
||||
foreach ($file in $filesToSign) {
|
||||
Write-Host "Signing: $($file.Name)"
|
||||
|
||||
# Get PKCS11 config file path (set by DigiCert action)
|
||||
$pkcs11Config = $env:PKCS11_CONFIG
|
||||
if (-not $pkcs11Config) {
|
||||
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
|
||||
Write-Host "DigiCert KeyLocker action may not have run correctly"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Using PKCS11 config: $pkcs11Config"
|
||||
|
||||
# Try signing with certificate fingerprint first (if available)
|
||||
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
|
||||
if ($fingerprint -and $fingerprint -ne "") {
|
||||
Write-Host "Attempting to sign with certificate fingerprint..."
|
||||
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
} else {
|
||||
Write-Host "No fingerprint provided, using keypair alias..."
|
||||
# Use smctl to sign with keypair alias
|
||||
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Exit code: $exitCode"
|
||||
Write-Host "Output: $output"
|
||||
|
||||
# Check if output contains "FAILED" even with exit code 0
|
||||
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
|
||||
Write-Host ""
|
||||
Write-Host "[ERROR] Signing failed for $($file.Name)"
|
||||
Write-Host "[ERROR] smctl returned success but output indicates failure"
|
||||
Write-Host ""
|
||||
Write-Host "Possible issues:"
|
||||
Write-Host " 1. Certificate not fully synced to KeyLocker (wait a few minutes)"
|
||||
Write-Host " 2. Incorrect keypair alias"
|
||||
Write-Host " 3. API key lacks signing permissions"
|
||||
Write-Host ""
|
||||
Write-Host "Please verify in DigiCert ONE portal:"
|
||||
Write-Host " - Certificate status is 'Issued' (not Pending)"
|
||||
Write-Host " - Keypair status is 'Online'"
|
||||
Write-Host " - 'Can sign' is set to 'Yes'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
Write-Host "[ERROR] Failed to sign $($file.Name)"
|
||||
Write-Host "Full error output:"
|
||||
Write-Host $output
|
||||
exit 1
|
||||
}
|
||||
|
||||
$signedCount++
|
||||
Write-Host "[SUCCESS] Signed: $($file.Name)"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "=== Summary ==="
|
||||
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
|
||||
|
||||
- name: Verify notarization (macOS only)
|
||||
if: inputs.sign && matrix.platform == 'macos-15'
|
||||
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
||||
run: |
|
||||
echo "🔍 Verifying notarization status..."
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
cd ./frontend/src-tauri/target
|
||||
DMG_FILE=$(find . -name "*.dmg" | head -1)
|
||||
if [ -n "$DMG_FILE" ]; then
|
||||
echo "Found DMG: $DMG_FILE"
|
||||
@@ -461,91 +498,82 @@ jobs:
|
||||
- name: Rename artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
# Absolute dist path so the cd below can't break the copy targets.
|
||||
DIST="$GITHUB_WORKSPACE/dist"
|
||||
mkdir -p "$DIST"
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
mkdir -p ./dist
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
# Find and rename artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
# Only ship the MSI installer. The loose exe and WiX toolset exes
|
||||
# are not the user-facing installer - the MSI contains the signed inner exe.
|
||||
find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
else
|
||||
find . -name "*.deb" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.rpm" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.rpm" \;
|
||||
find . -name "*.AppImage" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
fi
|
||||
|
||||
# Verify the MSI AND the inner exe extracted from it are signed.
|
||||
# The inner exe is what gets installed on users' machines and what AV scans.
|
||||
- name: Verify Windows Code Signature
|
||||
if: inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||
if: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$allSigned = $true
|
||||
Write-Host "Verifying Windows code signatures..."
|
||||
|
||||
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}.exe"
|
||||
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
||||
|
||||
# Check MSI (outer wrapper)
|
||||
if (Test-Path $msiPath) {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $msiPath
|
||||
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] MSI is not signed"
|
||||
$allSigned = $false
|
||||
}
|
||||
$allSigned = $true
|
||||
$usingKeyLocker = "${{ env.SM_API_KEY }}" -ne ""
|
||||
$usingPfx = "${{ env.WINDOWS_CERTIFICATE }}" -ne ""
|
||||
|
||||
# Extract MSI and verify inner exe
|
||||
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
|
||||
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
||||
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msiPath, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
|
||||
if ($proc.ExitCode -eq 0) {
|
||||
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
|
||||
if ($innerExe) {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
|
||||
Write-Host "Inner EXE: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] Inner exe is NOT signed - AV will flag this at runtime"
|
||||
$allSigned = $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
|
||||
# Check EXE signature
|
||||
if (Test-Path $exePath) {
|
||||
$exeSig = Get-AuthenticodeSignature -FilePath $exePath
|
||||
Write-Host "EXE Signature Status: $($exeSig.Status)"
|
||||
Write-Host "EXE Signer: $($exeSig.SignerCertificate.Subject)"
|
||||
Write-Host "EXE Timestamp: $($exeSig.TimeStamperCertificate.NotAfter)"
|
||||
|
||||
if ($exeSig.Status -ne "Valid") {
|
||||
Write-Host "[WARNING] EXE is not properly signed (Status: $($exeSig.Status))"
|
||||
if ($usingKeyLocker -or $usingPfx) {
|
||||
Write-Host "[ERROR] Certificate was provided but signing failed"
|
||||
$allSigned = $false
|
||||
} else {
|
||||
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[ERROR] MSI extraction failed (exit code: $($proc.ExitCode))"
|
||||
$allSigned = $false
|
||||
Write-Host "[SUCCESS] EXE is properly signed"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[ERROR] MSI not found at $msiPath"
|
||||
$allSigned = $false
|
||||
}
|
||||
|
||||
if (-not $allSigned) {
|
||||
Write-Host "[ERROR] Signature verification failed"
|
||||
# Check MSI signature
|
||||
if (Test-Path $msiPath) {
|
||||
$msiSig = Get-AuthenticodeSignature -FilePath $msiPath
|
||||
Write-Host "MSI Signature Status: $($msiSig.Status)"
|
||||
Write-Host "MSI Signer: $($msiSig.SignerCertificate.Subject)"
|
||||
Write-Host "MSI Timestamp: $($msiSig.TimeStamperCertificate.NotAfter)"
|
||||
|
||||
if ($msiSig.Status -ne "Valid") {
|
||||
Write-Host "[WARNING] MSI is not properly signed (Status: $($msiSig.Status))"
|
||||
if ($usingKeyLocker -or $usingPfx) {
|
||||
Write-Host "[ERROR] Certificate was provided but signing failed"
|
||||
$allSigned = $false
|
||||
} else {
|
||||
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[SUCCESS] MSI is properly signed"
|
||||
}
|
||||
}
|
||||
|
||||
if (($usingKeyLocker -or $usingPfx) -and -not $allSigned) {
|
||||
Write-Host "[ERROR] Code signing verification failed"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[SUCCESS] MSI and inner exe are properly signed"
|
||||
|
||||
- name: Dump smctl logs on failure
|
||||
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||
if (Test-Path $logDir) {
|
||||
Get-ChildItem $logDir | ForEach-Object {
|
||||
Write-Host "=== $($_.FullName) ==="
|
||||
Get-Content $_.FullName -Tail 200
|
||||
Write-Host ""
|
||||
}
|
||||
} else {
|
||||
Write-Host "smctl log directory not found at $logDir"
|
||||
Write-Host "[SUCCESS] Code signature verification completed"
|
||||
}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.name }}
|
||||
path: ./dist/*
|
||||
@@ -554,7 +582,7 @@ jobs:
|
||||
- name: Verify build artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
@@ -564,7 +592,7 @@ jobs:
|
||||
echo "❌ No Windows executable found"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
||||
echo "Checking for macOS artifacts..."
|
||||
find . -name "*.dmg" | head -5
|
||||
if [ $(find . -name "*.dmg" | wc -l) -eq 0 ]; then
|
||||
@@ -573,8 +601,8 @@ jobs:
|
||||
fi
|
||||
else
|
||||
echo "Checking for Linux artifacts..."
|
||||
find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | head -5
|
||||
if [ $(find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
||||
find . -name "*.deb" -o -name "*.AppImage" | head -5
|
||||
if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No Linux artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
@@ -585,9 +613,9 @@ jobs:
|
||||
- name: Test artifact sizes
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
cd ./frontend/src-tauri/target
|
||||
echo "Artifact sizes for ${{ matrix.name }}:"
|
||||
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
|
||||
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
|
||||
if [ -f "$file" ]; then
|
||||
size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown")
|
||||
echo "$file: $size bytes"
|
||||
@@ -598,109 +626,11 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
pr-comment:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' && needs.build.result == 'success'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden the runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Post/Update PR Comment with Download Links
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = context.issue.number;
|
||||
const runId = context.runId;
|
||||
|
||||
// Fetch artifacts for this workflow run
|
||||
const { data: artifactsList } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner,
|
||||
repo,
|
||||
run_id: runId
|
||||
});
|
||||
|
||||
// Map of expected artifact names to display info
|
||||
const artifactMap = {
|
||||
'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' },
|
||||
'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' },
|
||||
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' }
|
||||
};
|
||||
|
||||
let commentBody = `## 📦 Tauri Desktop Builds Ready!\n\n`;
|
||||
commentBody += `The desktop applications have been built and are ready for testing.\n\n`;
|
||||
commentBody += `### Download Artifacts:\n\n`;
|
||||
|
||||
// Add links for each found artifact
|
||||
let foundArtifacts = 0;
|
||||
for (const artifact of artifactsList.artifacts) {
|
||||
const info = artifactMap[artifact.name];
|
||||
if (info) {
|
||||
foundArtifacts++;
|
||||
// GitHub doesn't provide direct download URLs via API, but we can link to the artifact on the Actions page
|
||||
const artifactUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
|
||||
commentBody += `${info.icon} **${info.platform}**: [Download ${artifact.name}](${artifactUrl}) `;
|
||||
commentBody += `(${info.files}) - ${(artifact.size_in_bytes / 1024 / 1024).toFixed(1)} MB\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundArtifacts === 0) {
|
||||
commentBody += `⚠️ **Warning**: No artifacts found in workflow run.\n`;
|
||||
commentBody += `[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})\n`;
|
||||
}
|
||||
|
||||
commentBody += `\n---\n`;
|
||||
commentBody += `_Built from commit ${context.sha.substring(0, 7)}_\n`;
|
||||
commentBody += `_Artifacts expire in 7 days_`;
|
||||
|
||||
// Find existing comment
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('📦 Tauri Desktop Builds Ready!')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
console.log('Updated existing comment');
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
console.log('Created new comment');
|
||||
}
|
||||
|
||||
report:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Report build results
|
||||
run: |
|
||||
if [ "${{ needs.build.result }}" = "success" ]; then
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
name: Build Docker images (PR test)
|
||||
|
||||
# Reusable workflow called from build.yml on PRs to verify the three
|
||||
# embedded Dockerfiles (default, ultra-lite, fat) still build cleanly,
|
||||
# optionally against a freshly-built base image when the PR touches the
|
||||
# base Dockerfile.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
docker-base-changed:
|
||||
description: "Whether the docker base image changed (forwarded from files-changed)."
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
depot_cores:
|
||||
description: "Depot runner vCPU count (used in runs-on). Override for benchmarking."
|
||||
required: false
|
||||
type: string
|
||||
default: "8"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
# TODO: extract a pre-matrix `prepare` job that runs once and produces
|
||||
# shared artifacts for the three matrix entries below to consume:
|
||||
# 1. `task backend:build` — currently runs 3× in parallel with
|
||||
# identical env (DISABLE_ADDITIONAL_FEATURES=true,
|
||||
# STIRLING_PDF_DESKTOP_UI=false). Build once, upload the JAR as an
|
||||
# artifact, matrix entries download.
|
||||
# 2. The base-image `docker build` (gated on docker-base-changed) —
|
||||
# currently runs 3× in parallel against the same Dockerfile and
|
||||
# context. Build once, `docker save` to an artifact, matrix entries
|
||||
# `docker load` before the embedded build.
|
||||
# Saves ~2 full backend builds + 2 base-image builds per PR that touches
|
||||
# docker. May also be reusable from backend-build.yml's jdk-25 +
|
||||
# spring-security=true matrix entry if `task backend:build` and
|
||||
# `task backend:build:ci` produce equivalent JARs (verify before wiring).
|
||||
test-build-docker-images:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' && inputs.docker-base-changed != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- docker-rev: docker/embedded/Dockerfile
|
||||
artifact-suffix: Dockerfile
|
||||
cache-scope: stirling-pdf-latest
|
||||
- docker-rev: docker/embedded/Dockerfile.ultra-lite
|
||||
artifact-suffix: Dockerfile.ultra-lite
|
||||
cache-scope: stirling-pdf-ultra-lite
|
||||
- docker-rev: docker/embedded/Dockerfile.fat
|
||||
artifact-suffix: Dockerfile.fat
|
||||
cache-scope: stirling-pdf-fat
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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: Free disk space on runner
|
||||
run: |
|
||||
echo "Disk space before cleanup:" && df -h
|
||||
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
|
||||
docker system prune -af || true
|
||||
echo "Disk space after cleanup:" && df -h
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
~/.gradle/caches/modules-2/files-2.1
|
||||
~/.gradle/caches/modules-2/metadata-2.*
|
||||
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
- name: Build application
|
||||
run: task backend:build
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build base image locally (PR base change only)
|
||||
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
|
||||
run: |
|
||||
docker build -t stirling-pdf-base:pr-test -f docker/base/Dockerfile docker/base
|
||||
|
||||
- name: Set base image and platform for this build
|
||||
id: build-params
|
||||
# Pass workflow inputs through env vars rather than expanding `${{ }}`
|
||||
# directly into the shell — defense-in-depth against template injection
|
||||
# if any upstream provider of these values ever becomes less trusted.
|
||||
# GITHUB_EVENT_NAME is already provided by the runner.
|
||||
env:
|
||||
DOCKER_BASE_CHANGED: ${{ inputs.docker-base-changed }}
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ "$DOCKER_BASE_CHANGED" = "true" ]; then
|
||||
echo "base_image=stirling-pdf-base:pr-test" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "base_image=stirlingtools/stirling-pdf-base:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64,linux/arm64/v8" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Base-changed PRs build the embedded image with the local docker driver
|
||||
# so the locally-built stirling-pdf-base:pr-test (in the daemon image
|
||||
# store) resolves. A buildx container builder cannot see it and would try
|
||||
# to pull it from a registry, which fails. Single-platform, no gha cache.
|
||||
- name: Build ${{ matrix.docker-rev }} against local base (PR base change)
|
||||
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
|
||||
run: |
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--build-arg BASE_IMAGE=${{ steps.build-params.outputs.base_image }} \
|
||||
--file ./${{ matrix.docker-rev }} \
|
||||
--tag stirling-pdf-embedded:pr-test \
|
||||
.
|
||||
|
||||
- name: Build ${{ matrix.docker-rev }} (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
push: false
|
||||
platforms: ${{ steps.build-params.outputs.platforms }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ steps.build-params.outputs.base_image }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
# Fork PRs that did NOT change the base use the buildx container builder
|
||||
# (multi-platform + gha cache) against the published base image.
|
||||
- name: Build ${{ matrix.docker-rev }} (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true' && inputs.docker-base-changed != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
push: false
|
||||
cache-from: type=gha,scope=${{ matrix.cache-scope }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
|
||||
platforms: ${{ steps.build-params.outputs.platforms }}
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ steps.build-params.outputs.base_image }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: Upload Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: reports-docker-${{ matrix.artifact-suffix }}
|
||||
path: |
|
||||
build/reports/tests/
|
||||
build/test-results/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
test-build-unoserver-image:
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' && inputs.docker-base-changed != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/unoserver/Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
tags: stirling-unoserver:pr-test
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/unoserver/Dockerfile
|
||||
push: false
|
||||
load: true
|
||||
cache-from: type=gha,scope=stirling-unoserver
|
||||
cache-to: type=gha,mode=max,scope=stirling-unoserver
|
||||
platforms: linux/amd64
|
||||
tags: stirling-unoserver:pr-test
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
- name: Smoke test the built image
|
||||
run: |
|
||||
set -eu
|
||||
docker run -d --name unoserver-smoke \
|
||||
-e UNOSERVER_RECYCLE_INTERVAL_SECONDS=0 \
|
||||
stirling-unoserver:pr-test
|
||||
deadline=$((SECONDS + 60))
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
status=$(docker inspect -f '{{.State.Health.Status}}' unoserver-smoke 2>/dev/null || echo "starting")
|
||||
if [ "$status" = "healthy" ]; then
|
||||
echo "unoserver became healthy"
|
||||
docker logs unoserver-smoke | tail -30
|
||||
docker rm -f unoserver-smoke
|
||||
exit 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
echo "unoserver did not become healthy in time"
|
||||
docker logs unoserver-smoke || true
|
||||
docker rm -f unoserver-smoke || true
|
||||
exit 1
|
||||
@@ -20,54 +20,36 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
uses: ./.github/workflows/_runner-pick.yml
|
||||
|
||||
deploy:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
USE_DEPOT: ${{ needs.pick.outputs.is_fork != 'true' }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "25"
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 9.6.0
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
run: ./gradlew clean 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 Depot CLI
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -76,32 +58,17 @@ jobs:
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push test image (Depot)
|
||||
if: env.USE_DEPOT == 'true'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.16.0
|
||||
with:
|
||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||
context: .
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push test image (Docker fork fallback)
|
||||
if: env.USE_DEPOT != 'true'
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
- name: Build and push test image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.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: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
@@ -129,7 +96,7 @@ jobs:
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF Test"
|
||||
UI_HOMEDESCRIPTION: "Test Deployment"
|
||||
UI_APPNAMENAVBAR: "Test"
|
||||
@@ -153,39 +120,34 @@ jobs:
|
||||
files-changed:
|
||||
if: always()
|
||||
name: detect what files changed
|
||||
needs: pick
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
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@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: ".github/config/.files.yaml"
|
||||
|
||||
test:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [pick, deploy, files-changed]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: [deploy, files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
@@ -195,9 +157,9 @@ jobs:
|
||||
with:
|
||||
key: ${{secrets.TESTDRIVER_API_KEY}}
|
||||
prerun: |
|
||||
choco install go-task -y
|
||||
task frontend:build
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
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
|
||||
@@ -208,13 +170,13 @@ jobs:
|
||||
FORCE_COLOR: "3"
|
||||
|
||||
cleanup:
|
||||
needs: [pick, deploy, test]
|
||||
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||
needs: [deploy, test]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
+3
-49
@@ -23,35 +23,18 @@ customFiles/
|
||||
configs/
|
||||
watchedFolders/
|
||||
clientWebUI/
|
||||
# Scratch dir used by local fixture-regeneration runs (see
|
||||
# app/proprietary/src/test/resources/db-migration-fixtures/README.md).
|
||||
# Holds downloaded JARs and disposable workdirs. Never committed.
|
||||
.alpha-local/
|
||||
!cucumber/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
/stirling/
|
||||
/testing/file_snapshots
|
||||
/testing/cucumber/junit/
|
||||
/testing/cucumber/report.html
|
||||
/testing/.failed_tests
|
||||
/.test-state/
|
||||
SwaggerDoc.json
|
||||
|
||||
# Runtime storage for uploaded files and user data (not Java source code)
|
||||
app/core/storage/
|
||||
|
||||
# Frontend build artifacts copied to backend static resources
|
||||
# These are generated by npm build and should not be committed
|
||||
app/core/src/main/resources/static/assets/
|
||||
app/core/src/main/resources/static/index.html
|
||||
# Prerendered per-route SPA pages (OG/social-preview), e.g. compress.html. api-landing.html is source.
|
||||
app/core/src/main/resources/static/*.html
|
||||
!app/core/src/main/resources/static/api-landing.html
|
||||
!app/core/src/main/resources/static/mobile-upload.html
|
||||
# Prerendered nested-route pages (e.g. settings/people.html)
|
||||
app/core/src/main/resources/static/settings/
|
||||
app/core/src/main/resources/static/locales/
|
||||
app/core/src/main/resources/static/Login/
|
||||
app/core/src/main/resources/static/classic-logo/
|
||||
@@ -59,14 +42,9 @@ app/core/src/main/resources/static/modern-logo/
|
||||
app/core/src/main/resources/static/og_images/
|
||||
app/core/src/main/resources/static/samples/
|
||||
app/core/src/main/resources/static/manifest-classic.json
|
||||
app/core/src/main/resources/static/og-metadata.json
|
||||
app/core/src/main/resources/static/sw-folder-retry.js
|
||||
app/core/src/main/resources/static/robots.txt
|
||||
app/core/src/main/resources/static/pdfium/
|
||||
app/core/src/main/resources/static/pdfjs/
|
||||
app/core/src/main/resources/static/vendor/
|
||||
app/core/src/main/resources/static/**/*.gz
|
||||
app/core/src/main/resources/static/**/*.br
|
||||
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
|
||||
|
||||
# Gradle
|
||||
@@ -171,7 +149,6 @@ app/proprietary/build
|
||||
common/build
|
||||
proprietary/build
|
||||
stirling-pdf/build
|
||||
frontend/editor/src-tauri/provisioner/target
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -179,6 +156,7 @@ __pycache__/
|
||||
*.pyo
|
||||
|
||||
# Virtual environments
|
||||
.env*
|
||||
.venv*
|
||||
env*/
|
||||
venv*/
|
||||
@@ -186,9 +164,6 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Env files (secrets / local overrides). Subproject .gitignore files whitelist any committed defaults.
|
||||
.env*
|
||||
|
||||
# VS Code
|
||||
/.vscode/**/*
|
||||
!/.vscode/settings.json
|
||||
@@ -198,7 +173,6 @@ venv.bak/
|
||||
.idea/
|
||||
*.iml
|
||||
out/
|
||||
.junie/
|
||||
|
||||
# Ignore Mac DS_Store files
|
||||
.DS_Store
|
||||
@@ -221,9 +195,6 @@ out/
|
||||
*.jks
|
||||
*.asc
|
||||
|
||||
# Allow test fixture certificates (synthetic, no real credentials)
|
||||
!frontend/editor/src/core/tests/test-fixtures/certs/**
|
||||
|
||||
# SSH Keys
|
||||
*.pub
|
||||
*.priv
|
||||
@@ -234,21 +205,15 @@ id_ecdsa.pub
|
||||
id_ed25519
|
||||
id_ed25519.pub
|
||||
.ssh/
|
||||
|
||||
# Allow the published GPG release signing public key (safe to share)
|
||||
!docs/security/signing-key.pub
|
||||
*ssh
|
||||
|
||||
# Taskfile checksum cache
|
||||
.task/
|
||||
|
||||
# cache
|
||||
.cache
|
||||
.ruff_cache
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
.build-cache
|
||||
|
||||
|
||||
|
||||
**/jcef-bundle/
|
||||
@@ -264,7 +229,7 @@ node_modules/
|
||||
*compact*.json
|
||||
test_batch.json
|
||||
*.backup.*.json
|
||||
frontend/editor/public/locales/*/translation.backup*.json
|
||||
frontend/public/locales/*/translation.backup*.json
|
||||
|
||||
# Development/build artifacts
|
||||
.gradle-cache/
|
||||
@@ -279,14 +244,3 @@ docs/type3/signatures/
|
||||
# Type3 sample PDFs (development only)
|
||||
**/type3/samples/
|
||||
|
||||
**/application-dev-local.properties
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
# Playwright MCP screenshots / traces
|
||||
.playwright-mcp/
|
||||
*.playwright-mcp.png
|
||||
|
||||
# Local screenshot artifacts from *-screenshots.spec.ts
|
||||
frontend/editor/screenshots/
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# PostHog project-level key - phc_ prefix keys are public/client-side by design
|
||||
# (PostHog client-side tracking embeds them in the browser bundle). Committed
|
||||
# intentionally in #6150 so engine/.env has a working default, with real
|
||||
# credentials overridden via engine/.env.local.
|
||||
engine/.env:generic-api-key:41
|
||||
|
||||
# MCP test fixtures / harness - no real secrets:
|
||||
# - test-only API key constant in an integration test
|
||||
# - JDBC URL + throwaway Keycloak creds in the local test compose
|
||||
# - placeholder / shell-variable Bearer headers in curl-based validation scripts
|
||||
app/proprietary/src/test/java/stirling/software/proprietary/mcp/security/McpApiKeyIntegrationTest.java:generic-api-key:40
|
||||
testing/compose/docker-compose-keycloak-mcp.yml:generic-api-key:25
|
||||
testing/compose/validate-mcp-apikey.sh:curl-auth-header:73
|
||||
testing/compose/validate-mcp-test.sh:curl-auth-header:92
|
||||
testing/compose/validate-mcp-test.sh:curl-auth-header:116
|
||||
|
||||
# Storybook example showing curl with a fake Bearer token placeholder (sk_live_a3f8...).
|
||||
frontend/editor/src/proprietary/ui/CodeBlock.stories.tsx:curl-auth-header:5
|
||||
|
||||
# Truncated placeholder API key in portal docs example (sk_live_8f2c...e10) - not a real secret.
|
||||
frontend/editor/src/portal/components/docs/GettingStartedSection.tsx:generic-api-key:30
|
||||
|
||||
# False positive: generic-api-key matches the Java type name "X509Certificate"
|
||||
# in a method signature (CreateSignatureBase.resolveSignatureAlgorithm) - not a secret.
|
||||
app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java:generic-api-key:224
|
||||
|
||||
# Supabase publishable key (public by design, RLS-protected) used as a CI fallback
|
||||
# default in the tauri-build workflow when the GitHub secret is unset - not a real secret.
|
||||
.github/workflows/tauri-build.yml:generic-api-key:402
|
||||
+44
-12
@@ -1,14 +1,46 @@
|
||||
# The actual checks live in .taskfiles/pre-commit.yml (with helper scripts under
|
||||
# scripts/pre-commit/) and are driven by Task. This hook just delegates to `task
|
||||
# pre-commit` so the git pre-commit hook, CI and a manual `task pre-commit` all
|
||||
# run the exact same thing. Requires `task` and `uv` on PATH. To auto-fix instead
|
||||
# of only checking, run `task pre-commit:fix`.
|
||||
repos:
|
||||
- repo: local
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.8
|
||||
hooks:
|
||||
- id: task-pre-commit
|
||||
name: task pre-commit
|
||||
entry: task pre-commit
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- --line-length=127
|
||||
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
|
||||
exclude: (split_photos.py)
|
||||
- id: ruff-format
|
||||
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
|
||||
exclude: (split_photos.py)
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list=thirdParty,tabEl,tabEls
|
||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||
- --quiet-level=2
|
||||
files: \.(html|css|js|py|md)$
|
||||
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.0
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
|
||||
- id: trailing-whitespace
|
||||
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
|
||||
# - repo: https://github.com/thibaudcolas/pre-commit-stylelint
|
||||
# rev: v16.21.1
|
||||
# hooks:
|
||||
# - id: stylelint
|
||||
# additional_dependencies:
|
||||
# - stylelint@16.21.1
|
||||
# - stylelint-config-standard@38.0.0
|
||||
# - "@stylistic/stylelint-plugin@3.1.3"
|
||||
# files: \.(css)$
|
||||
# args: [--fix]
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
# Gradle invocation strategy:
|
||||
# - Linux/macOS: `./gradlew` runs the POSIX shell wrapper natively.
|
||||
# - Windows: `cmd /c ".\gradlew.bat ..."` invokes the .bat wrapper through
|
||||
# cmd.exe so it inherits the user's Windows-side `JAVA_HOME`
|
||||
# and PATH. Routing through `bash gradlew` on Windows ends up
|
||||
# under WSL or Git-Bash, neither of which inherits
|
||||
# Adoptium/Temurin's default Windows-only Java env - gradlew
|
||||
# then errors with "JAVA_HOME is not set and no 'java' command
|
||||
# could be found".
|
||||
#
|
||||
# The entire `.\gradlew.bat ...` payload is double-quoted so
|
||||
# the leading `.\` survives mvdan/sh's POSIX backslash
|
||||
# stripping; cmd.exe also requires `.\` (not bare `gradlew.bat`)
|
||||
# because modern Windows excludes cwd from cmd's search path.
|
||||
|
||||
tasks:
|
||||
dev:
|
||||
desc: "Start backend dev server"
|
||||
cmds:
|
||||
- task: dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.PORT}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
POLICIES_ENABLED: '{{.POLICIES_ENABLED}}'
|
||||
|
||||
dev:proprietary:
|
||||
desc: "Start backend dev server in proprietary mode"
|
||||
# `dotenv:` reads from the root Taskfile's directory (".") because this
|
||||
# subtaskfile is included with `dir: .`. Local overrides in
|
||||
# .env.proprietary.local win over the committed .env.proprietary defaults.
|
||||
dotenv: ['app/.env.proprietary.local', 'app/.env.proprietary']
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default ""}}'
|
||||
POLICIES_ENABLED: '{{.POLICIES_ENABLED | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
|
||||
dev:bundled:
|
||||
desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean bootRun -PbuildWithFrontend=true"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean bootRun -PbuildWithFrontend=true
|
||||
platforms: [linux, darwin]
|
||||
|
||||
dev:saas:
|
||||
desc: "Start backend in SaaS flavor against Supabase"
|
||||
# `dotenv:` reads from the root Taskfile's directory (".") because this
|
||||
# subtaskfile is included with `dir: .`.
|
||||
dotenv: ['app/.env.saas.local', 'app/.env.saas']
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
# Override to "" to run the pure `saas` profile against your own SAAS_DB_*.
|
||||
PROFILES: '{{.PROFILES | default "dev"}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
STIRLING_FLAVOR: saas
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}}
|
||||
platforms: [linux, darwin]
|
||||
|
||||
build:
|
||||
desc: "Full backend build"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean build"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean build
|
||||
platforms: [linux, darwin]
|
||||
|
||||
build:fast:
|
||||
desc: "Build without tests"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean build -x test"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean build -x test
|
||||
platforms: [linux, darwin]
|
||||
|
||||
build:ci:
|
||||
desc: "Build for CI (formatting checked separately)"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat build -PnoSpotless"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew build -PnoSpotless
|
||||
platforms: [linux, darwin]
|
||||
|
||||
test:
|
||||
desc: "Run backend tests"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat test"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew test
|
||||
platforms: [linux, darwin]
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat spotlessApply"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew spotlessApply
|
||||
platforms: [linux, darwin]
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat spotlessCheck"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew spotlessCheck
|
||||
platforms: [linux, darwin]
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix backend"
|
||||
cmds:
|
||||
- task: format
|
||||
|
||||
swagger:
|
||||
desc: "Generate OpenAPI docs"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
|
||||
platforms: [linux, darwin]
|
||||
sources:
|
||||
- app/core/src/main/java/**/*.java
|
||||
- app/proprietary/src/main/java/**/*.java
|
||||
- app/common/src/main/java/**/*.java
|
||||
generates:
|
||||
- SwaggerDoc.json
|
||||
|
||||
check:
|
||||
desc: "Backend quality gate"
|
||||
cmds:
|
||||
- task: format:check
|
||||
- task: test
|
||||
|
||||
version:
|
||||
desc: "Print project version"
|
||||
silent: true
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat printVersion --quiet" | tail -1
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew printVersion --quiet | tail -1
|
||||
platforms: [linux, darwin]
|
||||
|
||||
licenses:check:
|
||||
desc: "Check dependency licenses"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat checkLicense --no-parallel"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew checkLicense --no-parallel
|
||||
platforms: [linux, darwin]
|
||||
|
||||
licenses:generate:
|
||||
desc: "Check and generate dependency license report"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat checkLicense generateLicenseReport --no-parallel"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
|
||||
platforms: [linux, darwin]
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean
|
||||
platforms: [linux, darwin]
|
||||
@@ -1,222 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
# jdk.dynalink is required by VeraPDF (PDF/A validation); without it the bundled JRE throws
|
||||
# NoClassDefFoundError: jdk/dynalink/Namespace at runtime in get-info-on-pdf and verify-pdf
|
||||
JLINK_MODULES: "java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.dynalink"
|
||||
|
||||
# Minimum Java major the bundled JRE must be. Keep in sync with build.gradle
|
||||
# `modernJavaVersion` - the app JAR is compiled for this, so an older runtime
|
||||
# fails at launch with UnsupportedClassVersionError. Enforced by jlink:verify.
|
||||
REQUIRED_JAVA: "25"
|
||||
|
||||
# Override via JPDFIUM_PLATFORMS env (csv of platform keys, or 'all').
|
||||
JPDFIUM_PLATFORMS:
|
||||
sh: |
|
||||
if [ -n "${JPDFIUM_PLATFORMS:-}" ]; then
|
||||
echo "$JPDFIUM_PLATFORMS"
|
||||
else
|
||||
case "{{OS}}-{{ARCH}}" in
|
||||
darwin-arm64) echo "darwin-arm64";;
|
||||
darwin-amd64) echo "darwin-x64";;
|
||||
linux-amd64) echo "linux-x64";;
|
||||
linux-arm64) echo "linux-arm64";;
|
||||
windows-amd64) echo "windows-x64";;
|
||||
*) echo "all";;
|
||||
esac
|
||||
fi
|
||||
|
||||
tasks:
|
||||
prepare:
|
||||
desc: "Prepare desktop build dependencies"
|
||||
deps:
|
||||
- jlink
|
||||
- task: ":frontend:prepare"
|
||||
vars: { MODE: desktop }
|
||||
- provisioner
|
||||
|
||||
provisioner:
|
||||
desc: "Build installer provisioner"
|
||||
platforms: [windows]
|
||||
dir: editor
|
||||
cmds:
|
||||
- node scripts/build-provisioner.mjs
|
||||
|
||||
dev:
|
||||
desc: "Start Tauri desktop dev mode"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
dir: editor
|
||||
cmds:
|
||||
- npx tauri dev --no-watch
|
||||
|
||||
build:
|
||||
desc: "Build Tauri desktop app (production)"
|
||||
deps: [prepare]
|
||||
dir: editor
|
||||
cmds:
|
||||
- npx tauri build
|
||||
|
||||
build:dev:
|
||||
desc: "Build Tauri desktop app (dev, no bundling)"
|
||||
deps: [prepare]
|
||||
dir: editor
|
||||
cmds:
|
||||
- npx tauri build --no-bundle
|
||||
|
||||
build:dev:mac:
|
||||
desc: "Build Tauri desktop .app bundle (macOS)"
|
||||
deps: [prepare]
|
||||
dir: editor
|
||||
cmds:
|
||||
- npx tauri build --bundles app --config '{"bundle":{"createUpdaterArtifacts":false}}'
|
||||
|
||||
build:dev:windows:
|
||||
desc: "Build Tauri desktop NSIS installer (Windows)"
|
||||
deps: [prepare]
|
||||
dir: editor
|
||||
cmds:
|
||||
- npx tauri build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":false}}'
|
||||
|
||||
build:dev:linux:
|
||||
desc: "Build Tauri desktop AppImage (Linux)"
|
||||
deps: [prepare]
|
||||
dir: editor
|
||||
cmds:
|
||||
- npx tauri build --bundles appimage --config '{"bundle":{"createUpdaterArtifacts":false}}'
|
||||
|
||||
test:
|
||||
desc: "Run Tauri/Cargo tests"
|
||||
deps: [prepare]
|
||||
dir: editor/src-tauri
|
||||
cmds:
|
||||
- cargo test
|
||||
|
||||
clean:
|
||||
desc: "Clean Tauri/Cargo build artifacts"
|
||||
dir: editor
|
||||
cmds:
|
||||
- task: jlink:clean
|
||||
- cd src-tauri && cargo clean
|
||||
- rm -rf dist build
|
||||
|
||||
# ============================================================
|
||||
# JLink — Build bundled Java runtime for Tauri
|
||||
# ============================================================
|
||||
|
||||
jlink:
|
||||
desc: "Build backend JAR and create JLink runtime for Tauri"
|
||||
deps: [jlink:jar, jlink:runtime]
|
||||
# Runs after the runtime is in place. Lives here (not in jlink:runtime's
|
||||
# cmds) so it still fires when jlink:runtime short-circuits on its `status:`
|
||||
# check and reuses an existing runtime/jre - that reuse path is exactly how
|
||||
# a stale, too-old JRE slips through.
|
||||
cmds:
|
||||
- task: jlink:verify
|
||||
|
||||
jlink:verify:
|
||||
desc: "Fail the build if the bundled JRE is older than the app JAR requires"
|
||||
dir: editor
|
||||
env:
|
||||
REQUIRED_JAVA: "{{.REQUIRED_JAVA}}"
|
||||
cmds:
|
||||
- node scripts/verify-bundled-jre.mjs src-tauri/runtime/jre/release
|
||||
|
||||
jlink:jar:
|
||||
desc: "Build backend JAR for Tauri bundling (host-OS natives only by default)"
|
||||
run: once
|
||||
dir: ..
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
cmds:
|
||||
- echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"
|
||||
- cmd: cmd /c gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
platforms: [linux, darwin]
|
||||
- mkdir -p frontend/editor/src-tauri/libs
|
||||
- cp app/core/build/libs/stirling-pdf-*.jar frontend/editor/src-tauri/libs/
|
||||
status:
|
||||
- test -f frontend/editor/src-tauri/libs/stirling-pdf-*.jar
|
||||
|
||||
jlink:runtime:
|
||||
desc: "Create custom JRE with jlink"
|
||||
deps: [jlink:jar]
|
||||
dir: editor/src-tauri
|
||||
cmds:
|
||||
- rm -rf runtime/jre
|
||||
- mkdir -p runtime
|
||||
# Pin jlink to JAVA_HOME so the bundled JRE matches the JDK the build
|
||||
# uses. Bare `jlink` on PATH can resolve to an older system Java (the
|
||||
# ubuntu runner ships Java 11), producing a runtime jlink:verify rejects.
|
||||
#
|
||||
# jdk.crypto.mscapi (the Windows certificate store / SunMSCAPI provider, used by
|
||||
# hardware-backed cert signing) is a Windows-only module - it only exists in a Windows
|
||||
# JDK's jmods, so it is added on Windows only or jlink fails to resolve it elsewhere.
|
||||
- cmd: |
|
||||
JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink"
|
||||
JLINK_COMPRESS="$("$JLINK" --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)"
|
||||
"$JLINK" \
|
||||
--add-modules {{.JLINK_MODULES}},jdk.crypto.mscapi \
|
||||
--strip-debug \
|
||||
--compress="$JLINK_COMPRESS" \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output runtime/jre
|
||||
platforms: [windows]
|
||||
- cmd: |
|
||||
JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink"
|
||||
JLINK_COMPRESS="$("$JLINK" --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)"
|
||||
"$JLINK" \
|
||||
--add-modules {{.JLINK_MODULES}} \
|
||||
--strip-debug \
|
||||
--compress="$JLINK_COMPRESS" \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output runtime/jre
|
||||
platforms: [linux, darwin]
|
||||
# jlink emits its files mode 444 (read-only). Tauri's build-script
|
||||
# resource copier preserves source permissions when staging
|
||||
# `runtime/jre/**/*` into `target/<profile>/runtime/jre/...`, so the
|
||||
# staged copies are read-only too. On any subsequent incremental
|
||||
# build the copier tries to overwrite them and fails with a bare
|
||||
# `Permission denied (os error 13)` (Rust's io::Error Display drops
|
||||
# the path, so the failure is opaque). Make the source writable here
|
||||
# so the staged destinations are writable and can be overwritten.
|
||||
#
|
||||
# Trade-off: this task runs for both `task desktop:dev` and
|
||||
# `task desktop:build`, so production bundles also ship mode-644
|
||||
# JRE files instead of 444. Functionally harmless on POSIX (the
|
||||
# `other` bit is `r--` either way, and on macOS code signing is the
|
||||
# real integrity check) and on Windows the DOS read-only attribute
|
||||
# isn't load-bearing for the bundled JDK. If we ever need strict
|
||||
# 444 in production, split the chmod into a dev-only step and have
|
||||
# `desktop:build` run `jlink:clean` first to force a fresh build.
|
||||
- cmd: chmod -R u+w runtime/jre
|
||||
platforms: [linux, darwin]
|
||||
- cmd: powershell -NoProfile -Command "Get-ChildItem -Recurse runtime/jre | ForEach-Object { $_.IsReadOnly = $false }"
|
||||
platforms: [windows]
|
||||
status:
|
||||
- test -f runtime/jre/release
|
||||
|
||||
jlink:clean:
|
||||
desc: "Remove JLink runtime and bundled JARs"
|
||||
dir: editor/src-tauri
|
||||
cmds:
|
||||
- rm -rf libs runtime
|
||||
|
||||
# macOS-only. Replaces jlink:runtime's single-arch JRE with a universal
|
||||
# (arm64 + x86_64) one for the universal Tauri shell. Runs the x86_64
|
||||
# jlink under Rosetta on Apple Silicon, so it is opt-in and not part of
|
||||
# the default desktop:build flow. Requires AARCH64_JAVA_HOME and
|
||||
# X64_JAVA_HOME to point at matching JDK installations with jmods/.
|
||||
jlink:universal-mac:
|
||||
desc: "Create universal (arm64+x86_64) JRE for the macOS Tauri build"
|
||||
deps: [jlink:jar]
|
||||
platforms: [darwin]
|
||||
dir: editor
|
||||
env:
|
||||
JLINK_MODULES: "{{.JLINK_MODULES}}"
|
||||
OUTPUT_DIR: src-tauri/runtime/jre
|
||||
cmds:
|
||||
- scripts/build-universal-mac-jre.sh
|
||||
@@ -1,68 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
COMPOSE_DIR: docker/compose
|
||||
EMBEDDED_DIR: docker/embedded
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: "Build standard Docker image"
|
||||
cmds:
|
||||
- docker build -t stirling-pdf -f {{.EMBEDDED_DIR}}/Dockerfile .
|
||||
|
||||
build:fat:
|
||||
desc: "Build fat Docker image (all features)"
|
||||
cmds:
|
||||
- docker build -t stirling-pdf-fat -f {{.EMBEDDED_DIR}}/Dockerfile.fat .
|
||||
|
||||
build:ultra-lite:
|
||||
desc: "Build ultra-lite Docker image"
|
||||
cmds:
|
||||
- docker build -t stirling-pdf-ultra-lite -f {{.EMBEDDED_DIR}}/Dockerfile.ultra-lite .
|
||||
|
||||
build:backend:
|
||||
desc: "Build backend-only Docker image (no embedded frontend)"
|
||||
cmds:
|
||||
- docker build -t stirling-pdf-backend -f docker/backend/Dockerfile .
|
||||
|
||||
build:frontend:
|
||||
desc: "Build frontend-only Docker image"
|
||||
cmds:
|
||||
- docker build -t stirling-pdf-frontend -f docker/frontend/Dockerfile .
|
||||
|
||||
build:engine:
|
||||
desc: "Build engine Docker image"
|
||||
dir: engine
|
||||
cmds:
|
||||
- docker build -t stirling-pdf-engine .
|
||||
|
||||
up:
|
||||
desc: "Start standard docker compose stack"
|
||||
cmds:
|
||||
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml up -d
|
||||
|
||||
up:fat:
|
||||
desc: "Start fat docker compose stack"
|
||||
cmds:
|
||||
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.fat.yml up -d
|
||||
|
||||
up:ultra-lite:
|
||||
desc: "Start ultra-lite docker compose stack"
|
||||
cmds:
|
||||
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.ultra-lite.yml up -d
|
||||
|
||||
down:
|
||||
desc: "Stop all running docker compose stacks"
|
||||
cmds:
|
||||
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml down
|
||||
|
||||
logs:
|
||||
desc: "Tail docker compose logs"
|
||||
cmds:
|
||||
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml logs -f
|
||||
|
||||
test:
|
||||
desc: "Run full Docker integration test suite (builds all variants and tests them)"
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash testing/test.sh {{.CLI_ARGS}}
|
||||
@@ -1,266 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install Playwright browsers"
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:install' ]
|
||||
cmds:
|
||||
- npx playwright install {{.CLI_ARGS}} --with-deps
|
||||
|
||||
stubbed:
|
||||
desc: "Run stubbed E2E tests"
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:prepare' ]
|
||||
cmds:
|
||||
- npx playwright test --project=stubbed {{.CLI_ARGS}}
|
||||
|
||||
live:
|
||||
desc: "Run live E2E tests"
|
||||
summary: |
|
||||
Auto-spawns a Spring Boot backend with isolated state under
|
||||
.test-state/playwright/ (purged on every run) so the suite doesn't
|
||||
touch your local DB, settings.yml, backups or customFiles. The
|
||||
backend is stopped automatically when the runner exits.
|
||||
|
||||
Pass extra Playwright flags via -- :
|
||||
task e2e:live -- --headed --grep merge
|
||||
deps:
|
||||
- live:backend
|
||||
- live:runner
|
||||
|
||||
live:backend:
|
||||
internal: true
|
||||
ignore_error: true
|
||||
vars:
|
||||
BASE_DIR: '{{.ROOT_DIR}}/.test-state/playwright'
|
||||
# COVERAGE=1 in the calling environment attaches the JaCoCo agent to
|
||||
# the bootRun JVM and writes to BASE_DIR/jacoco.exec on shutdown.
|
||||
# Off by default to keep local dev runs uninstrumented; CI flips it.
|
||||
env:
|
||||
STIRLING_BASE_PATH: '{{.BASE_DIR}}'
|
||||
# Suppress the analytics opt-in modal that fires on first admin login.
|
||||
# It renders a Mantine overlay that intercepts pointer events and blocks
|
||||
# FirstLoginSlide from rendering, so the bootstrap spec times out waiting
|
||||
# for the password-change prompt.
|
||||
SYSTEM_ENABLEANALYTICS: "false"
|
||||
# NOTE: SECURITY_INITIALLOGIN_USERNAME/PASSWORD are intentionally NOT set.
|
||||
# The live-setup project's bootstrap spec performs the real first-login
|
||||
# flow against the backend's default admin/stirling user, exercising the
|
||||
# forced-password-change UI and leaving the DB at admin/adminadmin for
|
||||
# the rest of the live suite. This is both real coverage of the first-
|
||||
# login flow and a stronger seed than env-var-driven user creation.
|
||||
cmds:
|
||||
# Wrapped in `bash -c` because Task's embedded shell (mvdan/sh) doesn't
|
||||
# support `&` + `$!` + `wait` reliably (gradle was never actually
|
||||
# backgrounded, so `$!` recorded a stale PID and the runner saw an
|
||||
# immediate "backend died"). Real bash backgrounds gradle properly and
|
||||
# gives us a stable PID to clean up.
|
||||
- |
|
||||
bash -c '
|
||||
set -e
|
||||
rm -rf "{{.BASE_DIR}}"
|
||||
mkdir -p "{{.BASE_DIR}}"
|
||||
GRADLE_ARGS=":stirling-pdf:bootRun"
|
||||
if [ -n "${COVERAGE:-}" ]; then
|
||||
# copyJacocoAgent is wired as a dependency of bootRun when
|
||||
# -PjacocoAgent=true, so we do not need to invoke it separately.
|
||||
GRADLE_ARGS="$GRADLE_ARGS -PjacocoAgent=true -PjacocoExec={{.BASE_DIR}}/jacoco.exec"
|
||||
echo "JaCoCo coverage enabled, writing to {{.BASE_DIR}}/jacoco.exec"
|
||||
fi
|
||||
# Background gradle and record its PID so the runner can clean up
|
||||
# the exact process tree (wrapper + forked Spring Boot JVM) without
|
||||
# resorting to fuzzy `pkill -f` patterns. `wait` keeps this script
|
||||
# alive for the lifetime of gradle so Task'"'"'s parallel deps stay
|
||||
# synchronised.
|
||||
bash gradlew $GRADLE_ARGS > "{{.BASE_DIR}}/backend.log" 2>&1 &
|
||||
GRADLE_PID=$!
|
||||
echo $GRADLE_PID > "{{.BASE_DIR}}/backend.pid"
|
||||
wait $GRADLE_PID
|
||||
'
|
||||
|
||||
live:runner:
|
||||
internal: true
|
||||
deps: [ ':frontend:prepare' ]
|
||||
dir: frontend/editor
|
||||
vars:
|
||||
BASE_DIR: '{{.ROOT_DIR}}/.test-state/playwright'
|
||||
cmds:
|
||||
# Wrapped in `bash -c` because Task's embedded shell (mvdan/sh) only
|
||||
# supports `EXIT`/`ERR` in `trap`, not `INT`/`TERM`. CLI_ARGS are
|
||||
# passed positionally so quoted args like --grep "foo bar" survive.
|
||||
- |
|
||||
bash -c '
|
||||
set +e
|
||||
# bootRun forks a separate Spring Boot JVM as a child of the gradle
|
||||
# wrapper; killing only the wrapper leaves that JVM orphaned holding
|
||||
# :8080. Walk the recorded PID'"'"'s process tree via pgrep so we kill
|
||||
# every descendant, then lsof as a last-resort backstop.
|
||||
kill_tree() {
|
||||
local pid=$1
|
||||
for child in $(pgrep -P "$pid" 2>/dev/null); do
|
||||
kill_tree "$child"
|
||||
done
|
||||
kill "$pid" 2>/dev/null || true
|
||||
}
|
||||
cleanup() {
|
||||
if [ -f "{{.BASE_DIR}}/backend.pid" ]; then
|
||||
echo "Stopping backend..."
|
||||
kill_tree "$(cat "{{.BASE_DIR}}/backend.pid")"
|
||||
sleep 1
|
||||
lsof -ti:8080 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
echo "Backend stopped"
|
||||
fi
|
||||
}
|
||||
# Trap so cleanup runs on every exit path: tests pass, tests fail,
|
||||
# backend never starts, user Ctrl-Cs. Without this the gradle JVM
|
||||
# is left orphaned holding :8080 whenever the wait loop times out.
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
echo "Waiting for backend on :8080 (up to 10min; first compile can be slow)..."
|
||||
WAITED=0
|
||||
while [ $WAITED -lt 600 ]; do
|
||||
if curl -sf http://localhost:8080/api/v1/info/status 2>/dev/null | grep -q UP; then
|
||||
echo "Backend ready, starting Playwright"
|
||||
break
|
||||
fi
|
||||
# Bail early if the backend already died: no point waiting 10min.
|
||||
if [ -f "{{.BASE_DIR}}/backend.pid" ]; then
|
||||
BACKEND_PID=$(cat "{{.BASE_DIR}}/backend.pid")
|
||||
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
|
||||
echo "Backend process exited before becoming ready; last 100 log lines:"
|
||||
tail -100 "{{.BASE_DIR}}/backend.log" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
WAITED=$((WAITED + 2))
|
||||
done
|
||||
if [ $WAITED -ge 600 ]; then
|
||||
echo "Backend did not become ready within 10min, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npx playwright test --project=live "$@"
|
||||
' bash {{.CLI_ARGS}}
|
||||
|
||||
enterprise:
|
||||
desc: "Run enterprise E2E tests"
|
||||
summary: |
|
||||
Requires an already-running keycloak compose stack on :8080. Bring one
|
||||
up first with:
|
||||
task e2e:oauth:up
|
||||
task e2e:saml:up
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:prepare' ]
|
||||
cmds:
|
||||
- npx playwright test --project=enterprise {{.CLI_ARGS}}
|
||||
|
||||
cross-browser:
|
||||
desc: "Run stubbed E2E tests on Chromium, Firefox, and WebKit"
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:prepare' ]
|
||||
cmds:
|
||||
- npx playwright test --project=stubbed --project=stubbed-firefox --project=stubbed-webkit {{.CLI_ARGS}}
|
||||
|
||||
check:
|
||||
desc: "Run all E2E tests not requiring Docker"
|
||||
cmds:
|
||||
- task: stubbed
|
||||
- task: live
|
||||
|
||||
check:all:
|
||||
desc: "Run all E2E tests"
|
||||
summary: |
|
||||
Includes the enterprise project, which requires a keycloak compose
|
||||
stack on :8080. Bring one up first with:
|
||||
task e2e:oauth:up
|
||||
task e2e:saml:up
|
||||
cmds:
|
||||
- task: stubbed
|
||||
- task: live
|
||||
- task: enterprise
|
||||
|
||||
oauth:up:
|
||||
desc: "Start the OAuth keycloak test environment"
|
||||
summary: |
|
||||
Set LICENSE_KEY=<KEY> to skip the interactive license prompt:
|
||||
task e2e:oauth:up LICENSE_KEY=abc123
|
||||
Pass extra flags via -- :
|
||||
task e2e:oauth:up -- --auto --nobuild
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash testing/compose/start-oauth-test.sh {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
|
||||
|
||||
oauth:down:
|
||||
desc: "Stop the OAuth keycloak test environment"
|
||||
cmds:
|
||||
- docker compose -f testing/compose/docker-compose-keycloak-oauth.yml down -v
|
||||
|
||||
saml:up:
|
||||
desc: "Start the SAML keycloak test environment"
|
||||
summary: |
|
||||
Set LICENSE_KEY=<KEY> to skip the interactive license prompt:
|
||||
task e2e:saml:up LICENSE_KEY=abc123
|
||||
Pass extra flags via -- :
|
||||
task e2e:saml:up -- --auto --with-storage --nobuild --language sv-SE
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash testing/compose/start-saml-test.sh {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
|
||||
|
||||
saml:down:
|
||||
desc: "Stop the SAML keycloak test environment"
|
||||
cmds:
|
||||
- docker compose -f testing/compose/docker-compose-keycloak-saml.yml down -v
|
||||
|
||||
mcp:up:
|
||||
desc: "Start the MCP keycloak test environment (Stirling as OAuth resource server)"
|
||||
summary: |
|
||||
Brings up Keycloak (OAuth authorization server) + Stirling configured as an
|
||||
MCP resource server, then you can exercise /mcp with real Keycloak tokens.
|
||||
Set LICENSE_KEY=<KEY> to skip the interactive license prompt:
|
||||
task e2e:mcp:up LICENSE_KEY=abc123
|
||||
Pass extra flags via -- :
|
||||
task e2e:mcp:up -- --validate --nobuild
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash testing/compose/start-mcp-test.sh {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
|
||||
|
||||
mcp:manual:
|
||||
desc: "Start the MCP keycloak test env in manual mode (prints URLs + a live token for your client)"
|
||||
summary: |
|
||||
Brings the stack up and prints copy-paste URLs/commands plus a freshly minted
|
||||
access token so you can drive your own MCP client (Inspector, curl, ...).
|
||||
task e2e:mcp:manual LICENSE_KEY=<your-license-key>
|
||||
Add --nobuild if the images are already built:
|
||||
task e2e:mcp:manual LICENSE_KEY=<your-license-key> -- --nobuild
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash testing/compose/start-mcp-test.sh --manual {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
|
||||
|
||||
mcp:apikey:
|
||||
desc: "Start the MCP test env in API-KEY manual mode (no OAuth/IdP): mints a key + prints client settings"
|
||||
summary: |
|
||||
Brings Stirling up in apikey auth mode and prints copy-paste client settings with a freshly
|
||||
minted X-API-KEY - ideal for clients whose OAuth layer can't reach localhost.
|
||||
task e2e:mcp:apikey LICENSE_KEY=<your-license-key>
|
||||
Add --nobuild if images are already built:
|
||||
task e2e:mcp:apikey LICENSE_KEY=<your-license-key> -- --nobuild
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- bash testing/compose/start-mcp-test.sh --apikey {{if .LICENSE_KEY}}--license-key "{{.LICENSE_KEY}}"{{end}} {{.CLI_ARGS}}
|
||||
|
||||
mcp:validate:
|
||||
desc: "Validate the running MCP keycloak test environment end-to-end (oauth mode + real MCP SDK client)"
|
||||
cmds:
|
||||
- bash testing/compose/validate-mcp-test.sh
|
||||
|
||||
mcp:validate-apikey:
|
||||
desc: "Validate the MCP server in API-KEY auth mode (mints a key + real MCP SDK client), then restore oauth"
|
||||
cmds:
|
||||
- bash testing/compose/validate-mcp-apikey.sh
|
||||
|
||||
mcp:down:
|
||||
desc: "Stop the MCP keycloak test environment"
|
||||
cmds:
|
||||
- docker compose -f testing/compose/docker-compose-keycloak-mcp.yml down -v
|
||||
@@ -1,132 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install engine dependencies"
|
||||
run: once
|
||||
cmds:
|
||||
- uv python install 3.13.8
|
||||
- uv sync
|
||||
sources:
|
||||
- uv.lock
|
||||
- pyproject.toml
|
||||
status:
|
||||
- test -d .venv
|
||||
|
||||
prepare:
|
||||
desc: "Set up engine .env from template"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run scripts/setup_env.py
|
||||
sources:
|
||||
- scripts/setup_env.py
|
||||
generates:
|
||||
- .env.local
|
||||
|
||||
run:
|
||||
desc: "Run engine server"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
dir: src
|
||||
vars:
|
||||
PORT: '{{.PORT | default "5001"}}'
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
cmds:
|
||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}"
|
||||
|
||||
dev:
|
||||
desc: "Start engine dev server with hot reload"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
dir: src
|
||||
vars:
|
||||
PORT: '{{.PORT | default "5001"}}'
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
cmds:
|
||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
|
||||
|
||||
lint:
|
||||
desc: "Run linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff check .
|
||||
|
||||
lint:fix:
|
||||
desc: "Auto-fix lint issues"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff check . --fix
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff format .
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run ruff format . --diff
|
||||
|
||||
typecheck:
|
||||
desc: "Run type checking"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run pyright . --warnings
|
||||
|
||||
test:
|
||||
desc: "Run tests"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- uv run pytest tests
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix lint + format"
|
||||
cmds:
|
||||
- task: format # Can auto-fix some things that `lint:fix` can't like line length violations
|
||||
- task: lint:fix
|
||||
- task: format # Ensure that after lint fixing that the code is still formatted correctly
|
||||
|
||||
check:
|
||||
desc: "Full engine quality gate"
|
||||
cmds:
|
||||
- task: typecheck
|
||||
- task: lint
|
||||
- task: format:check
|
||||
- task: test
|
||||
|
||||
tool-models:
|
||||
desc: "Generate tool_models.py from Java OpenAPI spec (SwaggerDoc.json)"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py
|
||||
sources:
|
||||
- ../SwaggerDoc.json
|
||||
- scripts/generate_tool_models.py
|
||||
generates:
|
||||
- src/stirling/models/tool_models.py
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
cmds:
|
||||
- task: '{{if eq .OS "Windows_NT"}}clean-windows{{else}}clean-unix{{end}}'
|
||||
|
||||
clean-unix:
|
||||
internal: true
|
||||
desc: "Clean build artifacts"
|
||||
cmds:
|
||||
- rm -rf .venv data logs output
|
||||
|
||||
# On Windows, use PowerShell as bash failed to delete some dependencies
|
||||
clean-windows:
|
||||
internal: true
|
||||
desc: "Clean build artifacts"
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue .venv
|
||||
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue data
|
||||
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue logs
|
||||
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue output
|
||||
@@ -1,432 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
# Tasks operate from the workspace root (frontend/). Editor commands pass
|
||||
# `editor` as the vite project root (positional after `build` / before the
|
||||
# mode flag) or use `--project editor/...` for tsc — so the editor lives
|
||||
# under frontend/editor/ without each task needing a cd.
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install dependencies"
|
||||
run: once
|
||||
cmds:
|
||||
- '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}'
|
||||
sources:
|
||||
- package-lock.json
|
||||
- package.json
|
||||
status:
|
||||
- test -d node_modules
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
|
||||
prepare:env:
|
||||
internal: true
|
||||
run: when_changed
|
||||
deps: [install]
|
||||
vars:
|
||||
MODE: '{{.MODE | default ""}}'
|
||||
cmds:
|
||||
- npx tsx editor/scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}}
|
||||
sources:
|
||||
- editor/scripts/setup-env.mts
|
||||
generates:
|
||||
- editor/.env.local
|
||||
- editor/.env{{if .MODE}}.{{.MODE}}{{end}}.local
|
||||
|
||||
prepare:icons:
|
||||
internal: true
|
||||
run: once
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/generate-icons.js
|
||||
|
||||
prepare:og:
|
||||
internal: true
|
||||
run: when_changed
|
||||
desc: "Regenerate OG/social-preview metadata from the tool registry"
|
||||
cmds:
|
||||
- node editor/scripts/generate-og-metadata.mjs
|
||||
sources:
|
||||
- editor/src/core/types/toolId.ts
|
||||
- editor/src/core/utils/urlMapping.ts
|
||||
- editor/src/core/data/useTranslatedToolRegistry.tsx
|
||||
- editor/public/og_images/*.png
|
||||
generates:
|
||||
- editor/src/core/data/ogImageMap.json
|
||||
- editor/public/og-metadata.json
|
||||
|
||||
prepare:
|
||||
desc: "Set up dev environment"
|
||||
run: when_changed
|
||||
vars:
|
||||
MODE: '{{.MODE | default ""}}'
|
||||
deps:
|
||||
- task: prepare:env
|
||||
vars: { MODE: '{{.MODE}}' }
|
||||
- prepare:icons
|
||||
- prepare:og
|
||||
|
||||
# ============================================================
|
||||
# Development
|
||||
# ============================================================
|
||||
|
||||
dev:_run:
|
||||
internal: true
|
||||
ignore_error: true
|
||||
vars:
|
||||
MODE: '{{.MODE}}'
|
||||
PORT: '{{.PORT | default "5173"}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
||||
OPEN: '{{.OPEN | default ""}}'
|
||||
env:
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
cmds:
|
||||
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
dev:
|
||||
desc: "Start frontend dev server"
|
||||
cmds:
|
||||
- task: dev:proprietary
|
||||
vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:core:
|
||||
desc: "Start frontend dev server in core mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: dev:_run
|
||||
vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:proprietary:
|
||||
desc: "Start frontend dev server in proprietary mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: dev:_run
|
||||
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:saas:
|
||||
desc: "Start frontend dev server in SaaS mode"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- task: dev:_run
|
||||
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:desktop:
|
||||
desc: "Start frontend dev server in desktop mode"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- task: dev:_run
|
||||
vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:prototypes:
|
||||
desc: "Start frontend dev server in prototypes mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: dev:_run
|
||||
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
# ============================================================
|
||||
# Build
|
||||
# ============================================================
|
||||
|
||||
build:
|
||||
desc: "Production build (default mode)"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build editor
|
||||
|
||||
build:core:
|
||||
desc: "Build for core mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build editor --mode core
|
||||
|
||||
build:proprietary:
|
||||
desc: "Build for proprietary mode"
|
||||
deps: [prepare]
|
||||
vars:
|
||||
PREVIEW: '{{.PREVIEW | default ""}}'
|
||||
cmds:
|
||||
- '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build editor --mode proprietary'
|
||||
|
||||
build:saas:
|
||||
desc: "Build for SaaS mode"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- npx vite build editor --mode saas
|
||||
|
||||
build:desktop:
|
||||
desc: "Build for desktop mode"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- npx vite build editor --mode desktop
|
||||
|
||||
build:prototypes:
|
||||
desc: "Build for prototypes mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build editor --mode prototypes
|
||||
|
||||
|
||||
storybook:
|
||||
desc: "Start Storybook dev server"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx storybook dev -p 6006 {{.CLI_ARGS}}
|
||||
|
||||
storybook:build:
|
||||
desc: "Build static Storybook"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx storybook build {{.CLI_ARGS}}
|
||||
|
||||
# ============================================================
|
||||
# Code quality
|
||||
# ============================================================
|
||||
|
||||
lint:
|
||||
desc: "Run linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- task: lint:eslint
|
||||
- task: lint:dpdm
|
||||
|
||||
lint:eslint:
|
||||
desc: "Run ESLint linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx eslint --max-warnings=0
|
||||
|
||||
lint:dpdm:
|
||||
desc: "Run circular import linting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
# Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is
|
||||
# shell-agnostic. Covers the whole editor tree, including the portal layer.
|
||||
- npx dpdm "editor/src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1
|
||||
|
||||
lint:fix:
|
||||
desc: "Auto-fix lint issues"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx eslint --fix
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx prettier --write .
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- npx prettier --check .
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix lint and format"
|
||||
cmds:
|
||||
- task: format
|
||||
- task: lint:fix
|
||||
|
||||
typecheck:
|
||||
desc: "Typecheck default build of the app"
|
||||
cmds:
|
||||
- task: typecheck:proprietary
|
||||
|
||||
typecheck:_run:
|
||||
internal: true
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
cmds:
|
||||
- '{{ if eq .CI "true" }}npx tsc{{ else }}npx tsgo{{ end }} --noEmit --project {{.PROJECT}}'
|
||||
|
||||
typecheck:core:
|
||||
desc: "Typecheck core build variant"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/core/tsconfig.json }
|
||||
|
||||
typecheck:proprietary:
|
||||
desc: "Typecheck proprietary build variant"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/proprietary/tsconfig.json }
|
||||
|
||||
typecheck:saas:
|
||||
desc: "Typecheck SaaS build variant"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/saas/tsconfig.json }
|
||||
|
||||
typecheck:desktop:
|
||||
desc: "Typecheck desktop build variant"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/desktop/tsconfig.json }
|
||||
|
||||
typecheck:cloud:
|
||||
desc: "Typecheck cloud shared layer (standalone)"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/cloud/tsconfig.json }
|
||||
|
||||
typecheck:scripts:
|
||||
desc: "Typecheck scripts"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/scripts/tsconfig.json }
|
||||
|
||||
typecheck:prototypes:
|
||||
desc: "Typecheck prototypes build variant"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/prototypes/tsconfig.json }
|
||||
|
||||
typecheck:portal:
|
||||
desc: "Typecheck developer portal build variant"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: editor/src/portal/tsconfig.json }
|
||||
|
||||
typecheck:all:
|
||||
desc: "Typecheck all build variants"
|
||||
cmds:
|
||||
- task: typecheck:core
|
||||
- task: typecheck:proprietary
|
||||
- task: typecheck:saas
|
||||
- task: typecheck:desktop
|
||||
- task: typecheck:cloud
|
||||
- task: typecheck:scripts
|
||||
- task: typecheck:prototypes
|
||||
- task: typecheck:portal
|
||||
|
||||
# ============================================================
|
||||
# Quality Gate
|
||||
# ============================================================
|
||||
|
||||
check:
|
||||
desc: "Quick quality gate for local development"
|
||||
cmds:
|
||||
- task: typecheck
|
||||
- task: lint
|
||||
- task: format:check
|
||||
- task: test
|
||||
|
||||
og:check:
|
||||
desc: "Fail if committed OG/social-preview metadata is out of date"
|
||||
cmds:
|
||||
- node editor/scripts/generate-og-metadata.mjs --check
|
||||
|
||||
check:all:
|
||||
desc: "Full CI quality gate"
|
||||
cmds:
|
||||
# Runs first, before prepare regenerates: guards the committed og-metadata.json /
|
||||
# ogImageMap.json that the Cloudflare Pages (plain `vite build`) deploy relies on.
|
||||
- task: og:check
|
||||
- task: typecheck:all
|
||||
- task: lint
|
||||
- task: format:check
|
||||
- task: build
|
||||
- task: test
|
||||
- task: storybook:build
|
||||
|
||||
# ============================================================
|
||||
# Test
|
||||
# ============================================================
|
||||
|
||||
test:
|
||||
desc: "Run tests"
|
||||
cmds:
|
||||
- task: test:editor
|
||||
|
||||
test:editor:
|
||||
desc: "Run editor tests"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vitest run --root editor
|
||||
|
||||
test:watch:
|
||||
desc: "Run tests in watch mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vitest --watch --root editor
|
||||
|
||||
test:coverage:
|
||||
desc: "Run tests with coverage (one-shot; CI-friendly)."
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
# `vitest run` makes this CI-safe (the bare `vitest` form enters watch
|
||||
# mode). Explicit reporter list because v8 + json-summary is what the
|
||||
# coverage-summary.py helper consumes; html/text are kept for humans.
|
||||
#
|
||||
# reportsDirectory is pinned to ./coverage relative to vitest's root
|
||||
# (--root editor), so output lands at frontend/editor/coverage/. The
|
||||
# CI upload step reads from that path. An earlier attempt with
|
||||
# `./editor/coverage` double-nested into frontend/editor/editor/coverage;
|
||||
# pinning future-proofs against vitest changing the default.
|
||||
- >
|
||||
npx vitest run --root editor --coverage
|
||||
--coverage.provider=v8
|
||||
--coverage.reporter=text-summary
|
||||
--coverage.reporter=json-summary
|
||||
--coverage.reporter=html
|
||||
--coverage.reportsDirectory=./coverage
|
||||
|
||||
# ============================================================
|
||||
# Code Generation
|
||||
# ============================================================
|
||||
|
||||
tool-models:
|
||||
desc: "Generate tool API types from the Java OpenAPI spec"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts
|
||||
sources:
|
||||
- editor/scripts/generate-tool-api-types.mts
|
||||
- ../SwaggerDoc.json
|
||||
generates:
|
||||
- editor/src/core/types/toolApiTypes.ts
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if committed tool API types are out of date"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --check
|
||||
|
||||
licenses:generate:
|
||||
desc: "Generate frontend license report"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/generate-licenses.js
|
||||
|
||||
# ============================================================
|
||||
# Clean
|
||||
# ============================================================
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts and caches"
|
||||
cmds:
|
||||
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist
|
||||
platforms: [windows]
|
||||
- cmd: rm -rf node_modules/.vite editor/dist dist
|
||||
platforms: [linux, darwin]
|
||||
@@ -1,133 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
# Repo-wide lint/format/secret checks - the single source of truth that the git
|
||||
# pre-commit hook (.pre-commit-config.yaml) and CI (pre_commit.yml) both call.
|
||||
|
||||
vars:
|
||||
# File selections as git pathspecs: git does the include/exclude matching, so
|
||||
# there is no grep/xargs and it behaves identically on every platform.
|
||||
PY_FILES: >-
|
||||
'scripts/*.py'
|
||||
'.github/scripts/*.py'
|
||||
'app/core/src/main/resources/static/python/*.py'
|
||||
':(exclude)*split_photos.py'
|
||||
SPELL_FILES: >-
|
||||
'*.html'
|
||||
'*.css'
|
||||
'*.js'
|
||||
'*.py'
|
||||
'*.md'
|
||||
':(exclude).vscode/*'
|
||||
':(exclude).devcontainer/*'
|
||||
':(exclude)app/core/src/main/resources/*'
|
||||
':(exclude)app/proprietary/src/main/resources/*'
|
||||
':(exclude)frontend/editor/public/vendor/*'
|
||||
':(exclude)*Dockerfile*'
|
||||
':(exclude)*pdfjs*'
|
||||
':(exclude)*thirdParty*'
|
||||
':(exclude)*bootstrap*'
|
||||
':(exclude)*.min.*'
|
||||
':(exclude)*diff.js'
|
||||
WS_FILES: >-
|
||||
'*.js'
|
||||
'*.java'
|
||||
'*.py'
|
||||
'*.yml'
|
||||
':(exclude)*pdfjs*'
|
||||
':(exclude)*thirdParty*'
|
||||
':(exclude)*bootstrap*'
|
||||
':(exclude)*.min.*'
|
||||
':(exclude)*diff.js'
|
||||
':(exclude).github/workflows/*'
|
||||
LOCALE_TOML: 'frontend/editor/public/locales/*/translation.toml'
|
||||
|
||||
# gitleaks is pinned + checksum-verified by scripts/pre-commit/install_gitleaks.py,
|
||||
# which owns the version and caches the binary here.
|
||||
GITLEAKS_BIN: '.task/bin/gitleaks{{if eq OS "windows"}}.exe{{end}}'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: "Check formatting, spelling, and secrets across the repo"
|
||||
cmds:
|
||||
- task: ruff
|
||||
- task: ruff-format
|
||||
- task: codespell
|
||||
- task: gitleaks
|
||||
- task: whitespace
|
||||
- task: toml-sort
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix formatting, spelling, and secrets issues across the repo"
|
||||
cmds:
|
||||
# Auto-fixers first, then the report-only tools (codespell, gitleaks) so a
|
||||
# finding there does not stop the fixers from running.
|
||||
- task: ruff
|
||||
vars: { FIX: '1' }
|
||||
- task: ruff-format
|
||||
vars: { FIX: '1' }
|
||||
- task: whitespace
|
||||
vars: { FIX: '1' }
|
||||
- task: toml-sort
|
||||
vars: { FIX: '1' }
|
||||
- task: codespell
|
||||
- task: gitleaks
|
||||
|
||||
install:
|
||||
desc: "Install the pinned pre-commit Python tools (ruff, codespell, toml-sort)"
|
||||
run: once
|
||||
cmds:
|
||||
- uv sync --project scripts/pre-commit --locked
|
||||
sources:
|
||||
- scripts/pre-commit/uv.lock
|
||||
- scripts/pre-commit/pyproject.toml
|
||||
status:
|
||||
- test -d scripts/pre-commit/.venv
|
||||
|
||||
clean:
|
||||
desc: "Remove the cached gitleaks binary and the tool virtualenv"
|
||||
cmds:
|
||||
- cmd: rm -rf scripts/pre-commit/.venv .task/bin/gitleaks
|
||||
platforms: [linux, darwin]
|
||||
- cmd: cmd /c "rmdir /s /q scripts\pre-commit\.venv & del /q .task\bin\gitleaks.exe"
|
||||
platforms: [windows]
|
||||
ignore_error: true
|
||||
|
||||
# Individual checks (hidden from `task --list`, but callable, e.g.
|
||||
# `task pre-commit:toml-sort FIX=1`). Pass FIX=1 to auto-fix where supported.
|
||||
ruff:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync ruff check --line-length=127 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}})
|
||||
|
||||
ruff-format:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync ruff format {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}})
|
||||
|
||||
codespell:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
|
||||
toml-sort:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project scripts/pre-commit --no-sync toml-sort --all --ignore-case {{if .FIX}}--in-place{{else}}--check{{end}} {{.LOCALE_TOML}}
|
||||
|
||||
whitespace:
|
||||
cmds:
|
||||
- uv run --no-project python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}}
|
||||
|
||||
gitleaks:
|
||||
deps: [gitleaks-bin]
|
||||
# Scan staged changes only, matching the old hook: the git-mode fingerprints
|
||||
# in .gitleaksignore (file:rule:line) still apply, and with nothing staged
|
||||
# this is a no-op. Secrets are never auto-fixed, so FIX has no effect.
|
||||
cmds:
|
||||
- "{{.GITLEAKS_BIN}} git --pre-commit --redact --staged --verbose"
|
||||
|
||||
gitleaks-bin:
|
||||
internal: true
|
||||
desc: "Ensure the pinned, checksum-verified gitleaks binary is cached in .task/bin"
|
||||
cmds:
|
||||
- uv run --no-project python scripts/pre-commit/install_gitleaks.py
|
||||
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
"vscjava.vscode-spring-boot-dashboard", // Spring Boot dashboard for managing and visualizing Spring Boot applications
|
||||
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles
|
||||
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
|
||||
"GitHub.copilot-chat", // GitHub Copilot AI pair programmer for Visual Studio Code
|
||||
"GitHub.copilot", // GitHub Copilot AI pair programmer for Visual Studio Code
|
||||
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
|
||||
"charliermarsh.ruff", // Ruff code formatter for Python to follow the Ruff Style Guide
|
||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||
|
||||
+20
-12
@@ -1,6 +1,6 @@
|
||||
# Adding New React Tools to Stirling PDF
|
||||
|
||||
This guide covers how to add new PDF tools to the React frontend.
|
||||
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -10,14 +10,14 @@ When adding tools, follow this systematic approach using the established pattern
|
||||
|
||||
Create these files in the correct directories:
|
||||
```
|
||||
frontend/editor/src/hooks/tools/[toolName]/
|
||||
frontend/src/hooks/tools/[toolName]/
|
||||
├── use[ToolName]Parameters.ts # Parameter definitions and validation
|
||||
└── use[ToolName]Operation.ts # Tool operation logic using useToolOperation
|
||||
|
||||
frontend/editor/src/components/tools/[toolName]/
|
||||
frontend/src/components/tools/[toolName]/
|
||||
└── [ToolName]Settings.tsx # Settings UI component (if needed)
|
||||
|
||||
frontend/editor/src/tools/
|
||||
frontend/src/tools/
|
||||
└── [ToolName].tsx # Main tool component
|
||||
```
|
||||
|
||||
@@ -128,7 +128,7 @@ export default [ToolName] as ToolComponent;
|
||||
## 3. Register Tool in System
|
||||
Update these files to register your new tool:
|
||||
|
||||
**Tool Registry** (`frontend/editor/src/data/useTranslatedToolRegistry.tsx`):
|
||||
**Tool Registry** (`frontend/src/data/useTranslatedToolRegistry.tsx`):
|
||||
1. Add imports at the top:
|
||||
```typescript
|
||||
import [ToolName] from "../tools/[ToolName]";
|
||||
@@ -155,7 +155,7 @@ import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Setting
|
||||
## 4. Add Tooltips (Optional but Recommended)
|
||||
Create user-friendly tooltips to help non-technical users understand your tool. **Use simple, clear language - avoid technical jargon:**
|
||||
|
||||
**Tooltip Hook** (`frontend/editor/src/components/tooltips/use[ToolName]Tips.ts`):
|
||||
**Tooltip Hook** (`frontend/src/components/tooltips/use[ToolName]Tips.ts`):
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
@@ -188,7 +188,7 @@ import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const tips = use[ToolName]Tips();
|
||||
|
||||
|
||||
// In your steps array:
|
||||
steps: [
|
||||
{
|
||||
@@ -200,9 +200,9 @@ const [ToolName] = (props: BaseToolProps) => {
|
||||
```
|
||||
|
||||
## 5. Add Translations
|
||||
Update translation files. **Important: Only update `en-US` files** - other languages are handled separately.
|
||||
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
|
||||
|
||||
**File to update:** `frontend/editor/public/locales/en-US/translation.toml`
|
||||
**File to update:** `frontend/public/locales/en-GB/translation.toml`
|
||||
|
||||
**Required Translation Keys**:
|
||||
```toml
|
||||
@@ -251,20 +251,28 @@ Update translation files. **Important: Only update `en-US` files** - other langu
|
||||
```
|
||||
|
||||
**Translation Notes:**
|
||||
- **Only update `en-US/translation.toml`** - other locale files are managed separately
|
||||
- **Only update `en-GB/translation.toml`** - other locale files are managed separately
|
||||
- Use descriptive keys that match your component's `t()` calls
|
||||
- Include tooltip translations if you created tooltip hooks
|
||||
- Add `options.*` keys if your tool has settings with descriptions
|
||||
|
||||
**Tooltip Writing Guidelines:**
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Focus on benefits** - explain what the user gains, not how it works internally
|
||||
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
|
||||
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
|
||||
- **Keep descriptions concise** - 1-2 sentences maximum per section
|
||||
- **Use bullet points** for multiple benefits or features
|
||||
|
||||
## 6. Testing Your Tool
|
||||
## 6. Migration from Thymeleaf
|
||||
When migrating existing Thymeleaf templates:
|
||||
|
||||
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
|
||||
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
|
||||
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
|
||||
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
|
||||
|
||||
## 7. Testing Your Tool
|
||||
- Verify tool appears in UI with correct icon and description
|
||||
- Test with various file sizes and types
|
||||
- Confirm translations work
|
||||
|
||||
@@ -1,508 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to AI Agents when working with code in this repository.
|
||||
|
||||
## Taskfile (Recommended)
|
||||
|
||||
This project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.
|
||||
|
||||
Task `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.
|
||||
|
||||
### Quick Reference
|
||||
- `task install` — install all dependencies
|
||||
- `task dev` — start backend + frontend concurrently
|
||||
- `task dev:all` — start backend + frontend + engine concurrently
|
||||
- `task build` — build all components
|
||||
- `task test` — run all tests (backend + frontend + engine)
|
||||
- `task lint` — run all linters
|
||||
- `task format` — auto-fix formatting across all components
|
||||
- `task check` — full quality gate (lint + typecheck + test)
|
||||
- `task clean` — clean all build artifacts
|
||||
- `task docker:build` — build standard Docker image
|
||||
- `task docker:up` — start Docker compose stack
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
- **Build project**: `task build`
|
||||
- **Run backend locally**: `task backend:dev`
|
||||
- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)
|
||||
- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
|
||||
- **Code formatting**: `task format` (or `task backend:format` for Java only)
|
||||
- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)
|
||||
|
||||
After modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.
|
||||
|
||||
### Docker Development
|
||||
- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)
|
||||
- **Build fat version**: `task docker:build:fat`
|
||||
- **Build ultra-lite**: `task docker:build:ultra-lite`
|
||||
- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)
|
||||
- **Stop compose stack**: `task docker:down`
|
||||
- **View logs**: `task docker:logs`
|
||||
- **Example compose files**: Located in `exampleYmlFiles/` directory
|
||||
|
||||
### Security Mode Development
|
||||
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
|
||||
|
||||
### Python Development (AI Engine)
|
||||
|
||||
The engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.
|
||||
|
||||
#### Python Commands
|
||||
All engine commands run from the repo root using Task:
|
||||
- `task engine:check` — run all checks (typecheck + lint + format-check + test)
|
||||
- `task engine:fix` — auto-fix lint + formatting
|
||||
- `task engine:install` — install Python dependencies via uv
|
||||
- `task engine:dev` — start FastAPI with hot reload (localhost:5001)
|
||||
- `task engine:test` — run pytest
|
||||
- `task engine:lint` — run ruff linting
|
||||
- `task engine:typecheck` — run pyright
|
||||
- `task engine:format` — format code with ruff
|
||||
- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec
|
||||
|
||||
The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.
|
||||
|
||||
#### Python Code Style
|
||||
- Keep `task engine:check` passing.
|
||||
- Use modern Python when it improves clarity.
|
||||
- Prefer explicit names to cleverness.
|
||||
- Avoid nested functions and nested classes unless the language construct requires them.
|
||||
- Prefer composition to inheritance when combining concepts.
|
||||
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
|
||||
- Add comments sparingly and only when they explain non-obvious intent.
|
||||
|
||||
#### Python Typing and Models
|
||||
- Deserialize into Pydantic models as early as possible.
|
||||
- Serialize from Pydantic models as late as possible.
|
||||
- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.
|
||||
- Avoid `Any` wherever possible.
|
||||
- Avoid `cast()` wherever possible (reconsider the structure first).
|
||||
- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.
|
||||
- Do not use string literals for any type annotations, including `cast()`.
|
||||
|
||||
#### Python Configuration
|
||||
- Keep application-owned configuration in `stirling.config`.
|
||||
- Only add `STIRLING_*` environment variables that the engine itself truly owns.
|
||||
- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.
|
||||
- Let `pydantic-ai` own provider authentication configuration when possible.
|
||||
|
||||
#### Python Architecture
|
||||
|
||||
**Package roles:**
|
||||
- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.
|
||||
- `stirling.models`: shared model primitives and generated tool models.
|
||||
- `stirling.agents`: reasoning modules for individual capabilities.
|
||||
- `stirling.api`: HTTP layer, dependency access, and app startup wiring.
|
||||
- `stirling.services`: shared runtime and non-AI infrastructure.
|
||||
- `stirling.config`: application-owned settings.
|
||||
|
||||
**Source of truth:**
|
||||
- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.
|
||||
- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.
|
||||
- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.
|
||||
- If a tool ID must match a parameter model, validate that relationship explicitly in code.
|
||||
|
||||
**Boundaries:**
|
||||
- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.
|
||||
- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.
|
||||
- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.
|
||||
- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.
|
||||
|
||||
#### Python AI Usage
|
||||
- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.
|
||||
- Use AI for reasoning-heavy outputs, not deterministic glue.
|
||||
- Do not ask the model to invent data that Python can derive safely.
|
||||
- Do not fabricate fallback user-facing copy in code to hide incomplete model output.
|
||||
- AI output schemas should be impossible to instantiate incorrectly.
|
||||
- Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.
|
||||
- Prefer Python to derive deterministic follow-up structure from a valid AI result.
|
||||
- Use `NativeOutput(...)` for structured model outputs.
|
||||
- Use `ToolOutput(...)` when the model should select and call delegate functions.
|
||||
|
||||
#### Python Testing
|
||||
- Test contracts directly.
|
||||
- Test agents directly where behaviour matters.
|
||||
- Test API routes as thin integration points.
|
||||
- Prefer dependency overrides or startup-state seams to monkeypatching random globals.
|
||||
|
||||
### Frontend Development
|
||||
- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080
|
||||
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
|
||||
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
|
||||
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
|
||||
- **Package Installation**: `task frontend:install`
|
||||
- **Deployment Options**:
|
||||
- **Desktop App**: `task desktop:build`
|
||||
- **Web Server**: `task frontend:build` then serve dist/ folder
|
||||
- **Development**: `task desktop:dev` for desktop dev mode
|
||||
|
||||
#### Environment Variables
|
||||
- All `VITE_*` variables must be declared in the appropriate committed env file:
|
||||
- `frontend/editor/.env` — core and shared vars (base, loaded in every mode)
|
||||
- `frontend/editor/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)
|
||||
- `frontend/editor/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)
|
||||
- `frontend/editor/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)
|
||||
- These files are committed to Git and must not contain private keys
|
||||
- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top
|
||||
- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files
|
||||
- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file
|
||||
- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks
|
||||
- See `frontend/README.md#environment-variables` for full documentation
|
||||
|
||||
#### Import Paths - CRITICAL
|
||||
**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.
|
||||
|
||||
For a broader explanation of the frontend layering and override architecture, read @frontend/editor/DeveloperGuide.md
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use @app/* for all imports
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
import { useFileContext } from "@app/contexts/FileContext";
|
||||
import { FileContext } from "@app/contexts/FileContext";
|
||||
|
||||
// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code
|
||||
import { AppLayout } from "@core/components/AppLayout";
|
||||
import { useFileContext } from "@proprietary/contexts/FileContext";
|
||||
```
|
||||
|
||||
**Only use explicit aliases when:**
|
||||
- Building layer-specific override that wraps a lower layer's component
|
||||
- Example: `import { AppProviders as CoreAppProviders } from "@core/components/AppProviders"` when creating proprietary/AppProviders.tsx that extends the core version
|
||||
|
||||
The `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/saas/desktop/cloud) and handles the fallback cascade — see "Frontend `cloud/` Layer" below for the full per-flavor order.
|
||||
|
||||
#### Frontend `cloud/` Layer
|
||||
|
||||
`@app/*` resolves through a per-flavor cascade — first existing file wins (shadow/override):
|
||||
|
||||
- **core** → core
|
||||
- **proprietary** → proprietary → core
|
||||
- **saas** → saas → cloud → proprietary → core
|
||||
- **desktop** → desktop → cloud → proprietary → core
|
||||
- **cloud** → cloud → proprietary → core
|
||||
|
||||
What goes where:
|
||||
|
||||
- **core** — OSS base.
|
||||
- **proprietary** — licensed / offline features.
|
||||
- **cloud** — the SHARED hosted/SaaS experience used by BOTH saas + desktop: PAYG, wallet, plan, billing, usage meters, cloud config/team/onboarding.
|
||||
- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.
|
||||
- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.
|
||||
|
||||
`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (enforced by ESLint). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
|
||||
|
||||
Rule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).
|
||||
|
||||
**Cloud feature flags on desktop.** The local `AppConfigContext` reads `/api/v1/config/app-config` from the LOCAL bundled backend, so cloud-only flags (`aiEngineEnabled`, `premiumEnabled`, …) are never seen on desktop. To read the cloud's view, use `useSaasAppConfig()` (`desktop/hooks/useSaasAppConfig.ts`, backed by the general `saasAppConfigService` — SaaS-mode-only, public endpoint, native HTTP, 5-min cache). It returns `null` outside SaaS mode, so cloud features stay off in local/self-hosted and the server keeps the on/off switch (no desktop release needed to flip a flag). Gate a feature behind a per-platform seam — e.g. `useAiEngineEnabled()` (core reads `useAppConfig()`, desktop reads `useSaasAppConfig()`) — rather than hardcoding the flag on.
|
||||
|
||||
#### Component Override Pattern (Stub/Shadow)
|
||||
Use this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.
|
||||
|
||||
**How it works:**
|
||||
1. Core defines stub component (returns null or no-op)
|
||||
2. Desktop/proprietary overrides with same path/name
|
||||
3. Core imports via `@app/*` - higher layer "shadows" core in those builds
|
||||
4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!
|
||||
|
||||
**Example - Desktop-specific footer:**
|
||||
|
||||
```typescript
|
||||
// core/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (stub)
|
||||
interface WorkbenchBarFooterExtensionsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WorkbenchBarFooterExtensions(_props: WorkbenchBarFooterExtensionsProps) {
|
||||
return null; // Stub - does nothing in web builds
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// desktop/components/workbenchBar/WorkbenchBarFooterExtensions.tsx (real implementation)
|
||||
import { Box } from '@mantine/core';
|
||||
import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';
|
||||
|
||||
interface WorkbenchBarFooterExtensionsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WorkbenchBarFooterExtensions({ className }: WorkbenchBarFooterExtensionsProps) {
|
||||
return (
|
||||
<Box className={className}>
|
||||
<BackendHealthIndicator />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// core/components/shared/WorkbenchBar.tsx (usage - works in ALL builds)
|
||||
import { WorkbenchBarFooterExtensions } from '@app/components/workbenchBar/WorkbenchBarFooterExtensions';
|
||||
|
||||
export function WorkbenchBar() {
|
||||
return (
|
||||
<div>
|
||||
{/* In web builds: renders nothing (stub returns null) */}
|
||||
{/* In desktop builds: renders BackendHealthIndicator */}
|
||||
<WorkbenchBarFooterExtensions className="workbench-bar-footer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Build resolution:**
|
||||
- **Core build**: `@app/*` → `core/*` → Gets stub (returns null)
|
||||
- **Desktop build**: `@app/*` → `desktop/*` → Gets real implementation (shadows core)
|
||||
|
||||
**Benefits:**
|
||||
- No runtime checks or feature flags
|
||||
- Type-safe across all builds
|
||||
- Clean, readable code
|
||||
- Build-time optimization (dead code elimination)
|
||||
|
||||
#### Multi-Tool Workflow Architecture
|
||||
Frontend designed for **stateful document processing**:
|
||||
- Users upload PDFs once, then chain tools (split → merge → compress → view)
|
||||
- File state and processing results persist across tool switches
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `frontend/editor/src/core/contexts/FileContext.tsx`
|
||||
- **Active files**: Currently loaded PDFs and their variants
|
||||
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
|
||||
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
|
||||
- **IndexedDB persistence**: File storage with thumbnail caching
|
||||
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
|
||||
|
||||
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
|
||||
|
||||
#### Processing Services
|
||||
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
|
||||
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
|
||||
- **fileStorage**: IndexedDB with LRU cache management
|
||||
|
||||
#### Memory Management Strategy
|
||||
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
|
||||
- PDF.js documents that need explicit .destroy() calls
|
||||
- Blob URLs from tool outputs that need revocation
|
||||
- Web Workers that need termination
|
||||
Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
#### Tool Development
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/editor/src/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- Coordinates all tool operations with consistent interface
|
||||
- Integrates with FileContext for operation tracking
|
||||
- Handles validation, error handling, and UI state management
|
||||
|
||||
- **Supporting Hooks**:
|
||||
- **useToolState**: UI state management (loading, progress, error, files)
|
||||
- **useToolApiCalls**: HTTP requests and file processing
|
||||
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
|
||||
|
||||
- **Utilities**:
|
||||
- **toolErrorHandler**: Standardized error extraction and i18n support
|
||||
- **toolResponseProcessor**: API response handling (single/zip/custom)
|
||||
- **toolOperationTracker**: FileContext integration utilities
|
||||
|
||||
**Three Tool Patterns**:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
buildFormData: (params, file: File) => { /* single file */ },
|
||||
multiFileEndpoint: false,
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'split',
|
||||
endpoint: '/api/v1/general/split-pages',
|
||||
buildFormData: (params, files: File[]) => { /* all files */ },
|
||||
multiFileEndpoint: true,
|
||||
filePrefix: 'split_',
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'convert',
|
||||
customProcessor: async (params, files) => { /* custom logic */ },
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
|
||||
- **Consistent**: All tools follow same pattern and interface
|
||||
- **Maintainable**: Single responsibility hooks, easy to test and modify
|
||||
- **i18n Ready**: Built-in internationalization support
|
||||
- **Type Safe**: Full TypeScript support with generic interfaces
|
||||
- **Memory Safe**: Automatic resource cleanup and blob URL management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application
|
||||
- **Frontend**: React-based SPA in `/frontend` directory
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- **Configuration**: YAML-based configuration with environment variable overrides
|
||||
|
||||
### Controller Architecture
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
- **ConfigInitializer**: Handles runtime configuration and settings files
|
||||
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
|
||||
- **Security Layer**: Authentication, authorization, and user management (when enabled)
|
||||
|
||||
### Frontend Directory Structure
|
||||
The frontend is organized with a clear separation of concerns:
|
||||
|
||||
- **`frontend/editor/src/core/`**: Main application code (shared, production-ready components)
|
||||
- **`core/components/`**: React components organized by feature
|
||||
- `core/components/tools/`: Individual PDF tool implementations
|
||||
- `core/components/viewer/`: PDF viewer components
|
||||
- `core/components/pageEditor/`: Page manipulation UI
|
||||
- `core/components/tooltips/`: Help tooltips for tools
|
||||
- `core/components/shared/`: Reusable UI components
|
||||
- **`core/contexts/`**: React Context providers
|
||||
- `FileContext.tsx`: Central file state management
|
||||
- `file/`: File reducer and selectors
|
||||
- `toolWorkflow/`: Tool workflow state
|
||||
- **`core/hooks/`**: Custom React hooks
|
||||
- `hooks/tools/`: Tool-specific operation hooks (one directory per tool)
|
||||
- `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)
|
||||
- **`core/constants/`**: Application constants and configuration
|
||||
- **`core/data/`**: Static data (tool taxonomy, etc.)
|
||||
- **`core/services/`**: Business logic services (PDF processing, storage, etc.)
|
||||
|
||||
- **`frontend/editor/src/desktop/`**: Desktop-specific (Tauri) code
|
||||
- **`frontend/editor/src/proprietary/`**: Proprietary/licensed features
|
||||
- **`frontend/editor/src-tauri/`**: Tauri (Rust) native desktop application code
|
||||
- **`frontend/editor/public/`**: Static assets served directly
|
||||
- `public/locales/`: Translation JSON files
|
||||
|
||||
### Component Architecture
|
||||
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/editor/public/` (modern)
|
||||
- **Internationalization**:
|
||||
- Backend: `messages_*.properties` files
|
||||
- Frontend: JSON files in `frontend/editor/public/locales/` (converted from .properties)
|
||||
- Conversion Script: `scripts/convert_properties_to_json.py`
|
||||
|
||||
### Configuration Modes
|
||||
- **Ultra-lite**: Basic PDF operations only
|
||||
- **Standard**: Full feature set
|
||||
- **Fat**: Pre-downloaded dependencies for air-gapped environments
|
||||
- **Security Mode**: Adds authentication, user management, and enterprise features
|
||||
|
||||
### Testing Strategy
|
||||
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
|
||||
- **Docker Testing**: `test.sh` validates all Docker variants
|
||||
- **Manual Testing**: No unit tests currently - relies on UI and API testing
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Local Development** (using Taskfile):
|
||||
- Backend + frontend: `task dev`
|
||||
- All services (including AI engine): `task dev:all`
|
||||
- Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)
|
||||
2. **Quality Gate**: Run `task check` before submitting PRs
|
||||
3. **Docker Testing**: Use `./test.sh` for full Docker integration tests
|
||||
4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)
|
||||
5. **Translations**:
|
||||
- Backend: Use helper scripts in `/scripts` for multi-language updates
|
||||
- Frontend: Update JSON files in `frontend/editor/public/locales/` or use conversion script
|
||||
6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
|
||||
|
||||
## Frontend Architecture Status
|
||||
|
||||
- **Core Status**: React SPA architecture complete with multi-tool workflow support
|
||||
- **State Management**: FileContext handles all file operations and tool navigation
|
||||
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
|
||||
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
|
||||
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
|
||||
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
|
||||
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Translation Rules
|
||||
|
||||
- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately
|
||||
- Translation files are located in `frontend/editor/public/locales/`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Requires JDK 25.
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
|
||||
- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer
|
||||
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
|
||||
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
|
||||
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
|
||||
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
|
||||
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
|
||||
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
|
||||
|
||||
## Communication Style
|
||||
- Be direct and to the point
|
||||
- No apologies or conversational filler
|
||||
- Answer questions directly without preamble
|
||||
- Explain reasoning concisely when asked
|
||||
- Avoid unnecessary elaboration
|
||||
|
||||
## Decision Making
|
||||
- Ask clarifying questions before making assumptions
|
||||
- Stop and ask when uncertain about project-specific details
|
||||
- Confirm approach before making structural changes
|
||||
- Request guidance on preferences (cross-platform vs specific tools, etc.)
|
||||
- Verify understanding of requirements before proceeding
|
||||
|
||||
|
||||
## Stack reality check (don't trust LLM training data) <!-- bleeding-edge-stack-note -->
|
||||
|
||||
This codebase is on bleeding-edge versions of its core JVM stack: **Spring Boot 4.0.6**,
|
||||
**Jackson 3 (`tools.jackson`)**, **JDK 21/25 source/target with JDK 25 toolchain**.
|
||||
All three are *post*-2024 releases and your training corpus is overwhelmingly Spring Boot 2/3 and
|
||||
Jackson 2 patterns — those patterns will compile, run differently, or hallucinate APIs that no
|
||||
longer exist.
|
||||
|
||||
Before writing or editing Spring / Jackson / JDK code:
|
||||
|
||||
1. Open an existing module in `app/core/` or `app/common/` and grep for the actual imports being
|
||||
used — `import tools.jackson...` not `import com.fasterxml.jackson...`, and the new
|
||||
`org.springframework.boot` 4.x package layout.
|
||||
2. If you're not sure whether an API exists in this stack version, **check the source on disk
|
||||
first** (the dependency JARs are downloaded under `~/.gradle/caches/modules-2/`).
|
||||
3. Do not silently downgrade a Spring Boot 4 pattern to a Spring Boot 3 equivalent. If something
|
||||
doesn't work, surface it to the human — don't guess.
|
||||
|
||||
Same goes for Jackson 3's API surface (renamed `ObjectMapper` builder methods, new
|
||||
`tools.jackson.databind` namespace) and JDK 25 preview features. Ground your code in this repo's
|
||||
actual imports, not what worked three years ago.
|
||||
@@ -0,0 +1,228 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
- **Build project**: `./gradlew clean build`
|
||||
- **Run locally**: `./gradlew bootRun`
|
||||
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
|
||||
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
|
||||
|
||||
### Docker Development
|
||||
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
|
||||
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
|
||||
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
|
||||
- **Example compose files**: Located in `exampleYmlFiles/` directory
|
||||
|
||||
### Security Mode Development
|
||||
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
|
||||
|
||||
### Frontend Development
|
||||
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
|
||||
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
|
||||
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
|
||||
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
|
||||
- **Package Installation**: DO NOT run npm install commands - package management handled separately
|
||||
- **Deployment Options**:
|
||||
- **Desktop App**: `npm run tauri-build` (native desktop application)
|
||||
- **Web Server**: `npm run build` then serve dist/ folder
|
||||
- **Development**: `npm run tauri-dev` for desktop dev mode
|
||||
|
||||
#### Multi-Tool Workflow Architecture
|
||||
Frontend designed for **stateful document processing**:
|
||||
- Users upload PDFs once, then chain tools (split → merge → compress → view)
|
||||
- File state and processing results persist across tool switches
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `src/contexts/FileContext.tsx`
|
||||
- **Active files**: Currently loaded PDFs and their variants
|
||||
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
|
||||
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
|
||||
- **IndexedDB persistence**: File storage with thumbnail caching
|
||||
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
|
||||
|
||||
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
|
||||
|
||||
#### Processing Services
|
||||
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
|
||||
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
|
||||
- **fileStorage**: IndexedDB with LRU cache management
|
||||
|
||||
#### Memory Management Strategy
|
||||
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
|
||||
- PDF.js documents that need explicit .destroy() calls
|
||||
- Blob URLs from tool outputs that need revocation
|
||||
- Web Workers that need termination
|
||||
Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
#### Tool Development
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- Coordinates all tool operations with consistent interface
|
||||
- Integrates with FileContext for operation tracking
|
||||
- Handles validation, error handling, and UI state management
|
||||
|
||||
- **Supporting Hooks**:
|
||||
- **useToolState**: UI state management (loading, progress, error, files)
|
||||
- **useToolApiCalls**: HTTP requests and file processing
|
||||
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
|
||||
|
||||
- **Utilities**:
|
||||
- **toolErrorHandler**: Standardized error extraction and i18n support
|
||||
- **toolResponseProcessor**: API response handling (single/zip/custom)
|
||||
- **toolOperationTracker**: FileContext integration utilities
|
||||
|
||||
**Three Tool Patterns**:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
buildFormData: (params, file: File) => { /* single file */ },
|
||||
multiFileEndpoint: false,
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'split',
|
||||
endpoint: '/api/v1/general/split-pages',
|
||||
buildFormData: (params, files: File[]) => { /* all files */ },
|
||||
multiFileEndpoint: true,
|
||||
filePrefix: 'split_',
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'convert',
|
||||
customProcessor: async (params, files) => { /* custom logic */ },
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
|
||||
- **Consistent**: All tools follow same pattern and interface
|
||||
- **Maintainable**: Single responsibility hooks, easy to test and modify
|
||||
- **i18n Ready**: Built-in internationalization support
|
||||
- **Type Safe**: Full TypeScript support with generic interfaces
|
||||
- **Memory Safe**: Automatic resource cleanup and blob URL management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- **Configuration**: YAML-based configuration with environment variable overrides
|
||||
|
||||
### Controller Architecture
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
|
||||
- Pattern: `@Controller` + return template names
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
- **ConfigInitializer**: Handles runtime configuration and settings files
|
||||
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
|
||||
- **Security Layer**: Authentication, authorization, and user management (when enabled)
|
||||
|
||||
### Component Architecture
|
||||
- **React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
|
||||
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
|
||||
- **Internationalization**:
|
||||
- Backend: `messages_*.properties` files
|
||||
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
|
||||
- Conversion Script: `scripts/convert_properties_to_json.py`
|
||||
|
||||
### Configuration Modes
|
||||
- **Ultra-lite**: Basic PDF operations only
|
||||
- **Standard**: Full feature set
|
||||
- **Fat**: Pre-downloaded dependencies for air-gapped environments
|
||||
- **Security Mode**: Adds authentication, user management, and enterprise features
|
||||
|
||||
### Testing Strategy
|
||||
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
|
||||
- **Docker Testing**: `test.sh` validates all Docker variants
|
||||
- **Manual Testing**: No unit tests currently - relies on UI and API testing
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Local Development**:
|
||||
- Backend: `./gradlew bootRun` (runs on localhost:8080)
|
||||
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
|
||||
2. **Docker Testing**: Use `./test.sh` before submitting PRs
|
||||
3. **Code Style**: Spotless enforces Google Java Format automatically
|
||||
4. **Translations**:
|
||||
- Backend: Use helper scripts in `/scripts` for multi-language updates
|
||||
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
|
||||
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
|
||||
|
||||
## Frontend Architecture Status
|
||||
|
||||
- **Core Status**: React SPA architecture complete with multi-tool workflow support
|
||||
- **State Management**: FileContext handles all file operations and tool navigation
|
||||
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
|
||||
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
|
||||
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
|
||||
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
|
||||
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Translation Rules
|
||||
|
||||
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
|
||||
- Translation files are located in `frontend/public/locales/`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **Desktop Mode**: Set `STIRLING_PDF_DESKTOP_UI=true` for desktop application mode
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
|
||||
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
|
||||
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
|
||||
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
|
||||
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
|
||||
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
|
||||
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
|
||||
|
||||
## Communication Style
|
||||
- Be direct and to the point
|
||||
- No apologies or conversational filler
|
||||
- Answer questions directly without preamble
|
||||
- Explain reasoning concisely when asked
|
||||
- Avoid unnecessary elaboration
|
||||
|
||||
## Decision Making
|
||||
- Ask clarifying questions before making assumptions
|
||||
- Stop and ask when uncertain about project-specific details
|
||||
- Confirm approach before making structural changes
|
||||
- Request guidance on preferences (cross-platform vs specific tools, etc.)
|
||||
- Verify understanding of requirements before proceeding
|
||||
+2
-16
@@ -15,19 +15,6 @@ Before you start working on an issue, please comment on (or create) the issue an
|
||||
Once you have been assigned an issue, you can start working on it. When you are ready to submit your changes, open a pull request.
|
||||
For a detailed pull request tutorial, see [this guide](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github).
|
||||
|
||||
## Development Quick Start
|
||||
|
||||
This project uses [Task](https://taskfile.dev/) as a unified command runner. After cloning:
|
||||
|
||||
1. Install the `task` CLI: https://taskfile.dev/installation/
|
||||
2. Run `task install` to install all dependencies
|
||||
3. Run `task dev` to start backend + frontend
|
||||
4. Run `task check` before submitting a PR
|
||||
|
||||
Run `task --list` to see all available commands.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
Please make sure your Pull Request adheres to the following guidelines:
|
||||
|
||||
- Use the PR template provided.
|
||||
@@ -52,10 +39,9 @@ If, at any point in time, you have a question, please feel free to ask in the sa
|
||||
|
||||
## Developer Documentation
|
||||
|
||||
For technical guides, setup instructions, and development resources:
|
||||
For technical guides, setup instructions, and development resources, please see our [Developer Documentation](devGuide/) which includes:
|
||||
|
||||
- [Developer Guide](DeveloperGuide.md) - Main setup and architecture guide
|
||||
- [Taskfile.yml](Taskfile.yml) - Unified task runner for all build/dev/test/lint commands
|
||||
- [Developer Guide](devGuide/DeveloperGuide.md) - Main setup and architecture guide
|
||||
- [Exception Handling Guide](devGuide/EXCEPTION_HANDLING_GUIDE.md) - Error handling patterns and i18n
|
||||
- [Translation Guide](devGuide/HowToAddNewLanguage.md) - Adding new languages
|
||||
- And more in the [devGuide folder](devGuide/)
|
||||
|
||||
+201
-101
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite with a modern React SPA (Single Page Application).
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
|
||||
|
||||
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||
|
||||
@@ -11,7 +11,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
**Stirling 2.0** is built using:
|
||||
|
||||
**Backend:**
|
||||
- Spring Boot (requires JDK 25)
|
||||
- Spring Boot (Java 17+, JDK 21 recommended)
|
||||
- PDFBox for core PDF operations
|
||||
- LibreOffice for document conversions
|
||||
- qpdf for PDF optimization
|
||||
@@ -38,31 +38,21 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- PDF file association support
|
||||
- Self-contained JRE bundling with JLink
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
## 3. Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Task](https://taskfile.dev/installation/) — unified command runner (recommended)
|
||||
- Docker
|
||||
- Git
|
||||
- Java JDK 25
|
||||
- Java JDK 17 or later (JDK 21 recommended)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
- [uv](https://docs.astral.sh/uv/) — Python package manager (required for engine development)
|
||||
- Rust and Cargo (required for Tauri desktop app development)
|
||||
- Tauri CLI (install with `cargo install tauri-cli`)
|
||||
|
||||
### Optional System Dependencies
|
||||
|
||||
These are not required to run the app but enable specific features. The app detects them at startup and disables the relevant features if they are missing.
|
||||
|
||||
| Dependency | Feature | Install |
|
||||
|---|---|---|
|
||||
| LibreOffice | File-to-PDF conversions | `brew install libreoffice` / `apt install libreoffice` |
|
||||
| Tesseract | OCR | `brew install tesseract` / `apt install tesseract-ocr` |
|
||||
| WeasyPrint | AI document creation | `brew install weasyprint` / `apt install weasyprint` |
|
||||
| qpdf | PDF optimisation | `brew install qpdf` / `apt install qpdf` |
|
||||
|
||||
### Setup Steps
|
||||
|
||||
1. Clone the repository:
|
||||
@@ -72,7 +62,7 @@ These are not required to run the app but enable specific features. The app dete
|
||||
cd Stirling-PDF
|
||||
```
|
||||
|
||||
2. Install Docker and JDK 25 if not already installed.
|
||||
2. Install Docker and JDK17 if not already installed.
|
||||
|
||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||
1. Only VSCode
|
||||
@@ -92,32 +82,16 @@ Visit the [Lombok website](https://projectlombok.org/setup/) for installation in
|
||||
|
||||
5. Add environment variable
|
||||
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||
6. **Frontend Setup (Required for Stirling 2.0)**
|
||||
5. **Frontend Setup (Required for Stirling 2.0)**
|
||||
Navigate to the frontend directory and install dependencies using npm.
|
||||
|
||||
### Verify Setup
|
||||
|
||||
Run `task install` to install all project dependencies (frontend npm packages, engine Python packages). Gradle manages its own dependencies automatically. Then run `task check` to verify everything builds and passes.
|
||||
|
||||
## 4. Stirling 2.0 Development Workflow
|
||||
|
||||
### Using Taskfile (Recommended)
|
||||
|
||||
The fastest way to start developing:
|
||||
|
||||
1. **Start developing**: `task dev` (runs backend + frontend concurrently — Ctrl+C to stop)
|
||||
2. **Or start services individually** in separate terminals:
|
||||
- `task backend:dev` — Spring Boot on localhost:8080
|
||||
- `task frontend:dev` — Vite on localhost:5173
|
||||
- `task engine:dev` — FastAPI on localhost:5001
|
||||
|
||||
Run `task --list` to see all available commands.
|
||||
|
||||
### Frontend Development (React)
|
||||
The frontend is a React SPA that runs independently during development:
|
||||
|
||||
1. **Start the backend**: `task backend:dev` (serves API endpoints on localhost:8080)
|
||||
2. **Start the frontend dev server**: `task frontend:dev` (serves UI on localhost:5173)
|
||||
1. **Start the backend**: Run the Spring Boot application (serves API endpoints on localhost:8080)
|
||||
2. **Start the frontend dev server**: Navigate to the frontend directory and run the development server (serves UI on localhost:5173)
|
||||
3. **Development flow**: The Vite dev server automatically proxies API calls to the backend
|
||||
|
||||
### File Storage Architecture
|
||||
@@ -126,12 +100,12 @@ Stirling 2.0 uses client-side file storage:
|
||||
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
### Tauri Desktop App Development
|
||||
Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE.
|
||||
|
||||
Using Taskfile: `task desktop:dev` (development) or `task desktop:build` (production build).
|
||||
|
||||
See [the frontend README](frontend/README.md#tauri) for detailed build instructions.
|
||||
See [the frontend README](frontend/README.md#tauri) for build instructions.
|
||||
|
||||
## 5. Project Structure
|
||||
|
||||
@@ -139,26 +113,24 @@ See [the frontend README](frontend/README.md#tauri) for detailed build instructi
|
||||
Stirling-PDF/
|
||||
├── .github/ # GitHub-specific files (workflows, issue templates)
|
||||
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
|
||||
├── frontend/ # Frontend workspace (Stirling 2.0)
|
||||
│ ├── editor/ # PDF editor app (the original React SPA)
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/ # React components
|
||||
│ │ │ ├── tools/ # Tool-specific React components
|
||||
│ │ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── services/ # API and utility services
|
||||
│ │ │ ├── types/ # TypeScript type definitions
|
||||
│ │ │ └── utils/ # Utility functions
|
||||
│ │ ├── src-tauri/ # Tauri desktop app configuration
|
||||
│ │ │ ├── src/ # Rust backend code
|
||||
│ │ │ ├── libs/ # JAR files (generated by build scripts)
|
||||
│ │ │ ├── runtime/ # Bundled JRE (generated by build scripts)
|
||||
│ │ │ ├── Cargo.toml # Rust dependencies
|
||||
│ │ │ └── tauri.conf.json # Tauri configuration
|
||||
│ │ ├── public/
|
||||
│ │ │ └── locales/ # Internationalization files (JSON)
|
||||
│ │ └── vite.config.ts # Vite configuration
|
||||
│ ├── package.json # Shared workspace dependencies
|
||||
│ └── eslint.config.mjs # Shared lint config
|
||||
├── frontend/ # React SPA frontend (Stirling 2.0)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── tools/ # Tool-specific React components
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── services/ # API and utility services
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── src-tauri/ # Tauri desktop app configuration
|
||||
│ │ ├── src/ # Rust backend code
|
||||
│ │ ├── libs/ # JAR files (generated by build scripts)
|
||||
│ │ ├── runtime/ # Bundled JRE (generated by build scripts)
|
||||
│ │ ├── Cargo.toml # Rust dependencies
|
||||
│ │ └── tauri.conf.json # Tauri configuration
|
||||
│ ├── public/
|
||||
│ │ └── locales/ # Internationalization files (JSON)
|
||||
│ ├── package.json # Frontend dependencies
|
||||
│ └── vite.config.ts # Vite configuration
|
||||
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||
├── docs/ # Documentation files
|
||||
├── exampleYmlFiles/ # Example YAML configuration files
|
||||
@@ -182,6 +154,7 @@ Stirling-PDF/
|
||||
│ │ │ ├── css/
|
||||
│ │ │ ├── js/
|
||||
│ │ │ └── pdfjs/
|
||||
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
|
||||
│ └── test/
|
||||
├── testing/ # Cucumber and integration tests
|
||||
│ └── cucumber/ # Cucumber test files
|
||||
@@ -221,7 +194,7 @@ services:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
@@ -256,32 +229,18 @@ docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
|
||||
|
||||
### Building Docker Images
|
||||
|
||||
#### Using Taskfile (Recommended)
|
||||
|
||||
```bash
|
||||
task docker:build # standard image
|
||||
task docker:build:fat # fat image (all features)
|
||||
task docker:build:ultra-lite # ultra-lite image
|
||||
task docker:up # start standard compose stack
|
||||
task docker:up:fat # start fat compose stack
|
||||
task docker:down # stop all stacks
|
||||
task docker:logs # tail logs
|
||||
```
|
||||
|
||||
#### Manual Docker Builds
|
||||
|
||||
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
|
||||
|
||||
1. Set the security environment variable:
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false to enable login and security features for builds
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
2. Build the project with Gradle:
|
||||
|
||||
```bash
|
||||
task backend:build
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
3. Build the Docker images:
|
||||
@@ -305,22 +264,13 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. However, to improve build times these can often be removed depending on your use case
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
## 7. Testing
|
||||
|
||||
### Quick Testing with Taskfile
|
||||
|
||||
Run all unit/integration tests across all components:
|
||||
|
||||
```bash
|
||||
task test # run all tests (backend + frontend + engine)
|
||||
task check # full quality gate: lint + typecheck + test
|
||||
```
|
||||
|
||||
### Comprehensive Testing Script
|
||||
|
||||
Stirling-PDF also provides a `test.sh` script in the root directory for Docker integration tests. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
||||
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
||||
|
||||
To run the test script:
|
||||
|
||||
@@ -346,11 +296,10 @@ Note: The `test.sh` script will run automatically when you raise a PR. However,
|
||||
|
||||
For React frontend development:
|
||||
|
||||
1. Start the backend: `task backend:dev` (serves API endpoints on localhost:8080)
|
||||
2. Start the frontend dev server: `task frontend:dev` (serves UI on localhost:5173)
|
||||
1. Start the backend: Run the Spring Boot application to serve API endpoints on localhost:8080
|
||||
2. Start the frontend dev server: Navigate to the frontend directory and run the development server on localhost:5173
|
||||
3. The Vite dev server automatically proxies API calls to the backend
|
||||
4. Run frontend tests: `task frontend:test` (or `task frontend:test:watch` for watch mode)
|
||||
5. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
|
||||
4. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
|
||||
|
||||
### Local Testing (Java and UI Components)
|
||||
|
||||
@@ -360,13 +309,14 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
|
||||
- RESTful API endpoints
|
||||
- JavaScript functionality
|
||||
- User interface components and styling
|
||||
- Thymeleaf templates
|
||||
|
||||
To run Stirling-PDF locally:
|
||||
|
||||
1. Compile and run the project using built-in IDE methods or by running:
|
||||
|
||||
```bash
|
||||
task backend:dev
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
2. Access the application at `http://localhost:8080` in your web browser.
|
||||
@@ -387,11 +337,10 @@ Important notes:
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
|
||||
4. Test your changes thoroughly in the Docker environment.
|
||||
5. Run the quality gate and integration tests:
|
||||
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
|
||||
|
||||
```bash
|
||||
task check # lint + typecheck + test across all components
|
||||
./test.sh # Docker integration tests (builds all variants + Cucumber)
|
||||
./test.sh
|
||||
```
|
||||
|
||||
6. Push your changes to your fork.
|
||||
@@ -452,13 +401,13 @@ Remember to test your changes thoroughly to ensure they don't break any existing
|
||||
|
||||
### React Component Development (Stirling 2.0)
|
||||
|
||||
For Stirling 2.0, new features are built as React components:
|
||||
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
|
||||
|
||||
#### Creating a New Tool Component
|
||||
|
||||
1. **Create the React Component:**
|
||||
```typescript
|
||||
// frontend/editor/src/tools/NewTool.tsx
|
||||
// frontend/src/tools/NewTool.tsx
|
||||
import { useState } from 'react';
|
||||
import { Button, FileInput, Container } from '@mantine/core';
|
||||
|
||||
@@ -499,6 +448,61 @@ For Stirling 2.0, new features are built as React components:
|
||||
3. **Register in Tool Picker:**
|
||||
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||
|
||||
### Legacy Reference: Overview of Thymeleaf
|
||||
|
||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||
|
||||
### Thymeleaf overview
|
||||
|
||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||
|
||||
Some examples of this are:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
```
|
||||
or
|
||||
```html
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
```
|
||||
|
||||
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||
|
||||
They can be more complex, such as:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||
```
|
||||
|
||||
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||
|
||||
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("exampleData", exampleData);
|
||||
return "new-feature";
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||
|
||||
```html
|
||||
<tbody>
|
||||
<!-- Use th:each to iterate over the list -->
|
||||
<tr th:each="person : ${exampleData}">
|
||||
<td th:text="${person.id}"></td>
|
||||
<td th:text="${person.name}"></td>
|
||||
<td th:text="${person.age}"></td>
|
||||
<td th:text="${person.email}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
```
|
||||
|
||||
This would generate n entries of tr for each person in exampleData
|
||||
|
||||
### Adding a New Feature to the Backend (API)
|
||||
|
||||
1. **Create a New Controller:**
|
||||
@@ -523,7 +527,7 @@ For Stirling 2.0, new features are built as React components:
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return "NewFeatureResponse";
|
||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -578,6 +582,91 @@ For Stirling 2.0, new features are built as React components:
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Feature to the Frontend (UI)
|
||||
|
||||
1. **Create a New Thymeleaf Template:**
|
||||
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||
- Use Thymeleaf attributes to dynamically generate content.
|
||||
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<input type="hidden" id="customMode" name="customMode" value="">
|
||||
<div class="mb-3">
|
||||
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create a New Controller for the UI:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.ui;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/new-feature")
|
||||
public class NewFeatureUIController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||
return "new-feature";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update the Navigation Bar:**
|
||||
- Add a link to the new feature page in the navigation bar.
|
||||
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||
|
||||
```html
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
@@ -587,7 +676,7 @@ When adding a new feature or modifying existing ones in Stirling-PDF, you'll nee
|
||||
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
||||
|
||||
- `messages.properties` (default, usually English)
|
||||
- `messages_en_US.properties`
|
||||
- `messages_en_GB.properties`
|
||||
- `messages_fr_FR.properties`
|
||||
- `messages_de_DE.properties`
|
||||
- etc.
|
||||
@@ -607,4 +696,15 @@ pdfSplitter.input.pages=Enter page numbers to split
|
||||
|
||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||
|
||||
### 3. Use Translations in Thymeleaf Templates
|
||||
|
||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||
|
||||
```html
|
||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||
```
|
||||
|
||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||
|
||||
-444
@@ -1,444 +0,0 @@
|
||||
# File Sharing Feature - Architecture & Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
The File Sharing feature enables users to store files server-side and share them with other registered users or via token-based share links. Files are stored using a pluggable storage provider (local filesystem or database) with optional quota enforcement.
|
||||
|
||||
**Key Capabilities:**
|
||||
- Server-side file storage (upload, update, download, delete)
|
||||
- Optional history bundle and audit log attachments per file
|
||||
- Direct user-to-user sharing with access roles
|
||||
- Token-based share links (requires `system.frontendUrl`)
|
||||
- Optional email notifications for shares (requires `mail.enabled`)
|
||||
- Access audit trail (tracks who accessed a share link and how)
|
||||
- Automatic share link expiration
|
||||
- Storage quotas (per-user and total)
|
||||
- Pluggable storage backend (local filesystem or database BLOB)
|
||||
- Integration with the Shared Signing workflow
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Schema
|
||||
|
||||
**`stored_files`**
|
||||
- One record per uploaded file
|
||||
- Stores file metadata (name, content type, size, storage key)
|
||||
- Optionally links to a history bundle and audit log as separate stored objects
|
||||
- `workflow_session_id` — nullable link to a `WorkflowSession` (signing feature)
|
||||
- `file_purpose` — enum classifying the file's role: `GENERIC`, `SIGNING_ORIGINAL`, `SIGNING_SIGNED`, `SIGNING_HISTORY`
|
||||
|
||||
**`file_shares`**
|
||||
- One record per sharing relationship
|
||||
- Two share types, distinguished by which fields are set:
|
||||
- **User share**: `shared_with_user_id` is set, `share_token` is null
|
||||
- **Link share**: `share_token` is set (UUID), `shared_with_user_id` is null
|
||||
- `access_role` — `EDITOR`, `COMMENTER`, or `VIEWER`
|
||||
- `expires_at` — nullable expiration for link shares
|
||||
- `workflow_participant_id` — when set, marks this as a **workflow share** (hidden from the file manager, accessible only via workflow endpoints)
|
||||
|
||||
**`file_share_accesses`**
|
||||
- One record per access event on a share link
|
||||
- Tracks: user, share link, access type (`VIEW` or `DOWNLOAD`), timestamp
|
||||
|
||||
**`storage_cleanup_entries`**
|
||||
- Queue of storage keys to be deleted asynchronously
|
||||
- Used when a file is deleted but the physical storage object cleanup is deferred
|
||||
|
||||
### Access Roles
|
||||
|
||||
| Role | Can Read | Can Write |
|
||||
|------|----------|-----------|
|
||||
| `EDITOR` | ✅ | ✅ |
|
||||
| `COMMENTER` | ✅ | ❌ |
|
||||
| `VIEWER` | ✅ | ❌ |
|
||||
|
||||
Default role when none is specified: `EDITOR`.
|
||||
|
||||
Owners always have full access regardless of role.
|
||||
|
||||
#### Role Semantics: COMMENTER vs VIEWER
|
||||
|
||||
In the file storage layer, `COMMENTER` and `VIEWER` are equivalent — both grant read-only access and neither can replace file content. The distinction is meaningful in the **signing workflow** context:
|
||||
|
||||
| Context | COMMENTER | VIEWER |
|
||||
|---------|-----------|--------|
|
||||
| File storage | Read only (same as VIEWER) | Read only |
|
||||
| Signing workflow | Can submit a signing action | Read only |
|
||||
|
||||
`WorkflowParticipant.canEdit()` returns `true` for `COMMENTER` (and `EDITOR`) roles, which the signing workflow uses to determine if a participant can still submit a signature. Once a participant has signed or declined, their effective role is automatically downgraded to `VIEWER` regardless of their configured role.
|
||||
|
||||
The rationale: "annotating" a document (submitting a signature) is not the same as "replacing" it. COMMENTER grants annotation rights without file-replacement rights.
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
#### Service Layer
|
||||
|
||||
**FileStorageService** (`1137 lines`)
|
||||
- Core file management service
|
||||
- Upload, update, download, and delete operations
|
||||
- User share management (share, revoke, leave)
|
||||
- Link share management (create, revoke, access)
|
||||
- Access recording and listing
|
||||
- Storage quota enforcement
|
||||
- Configuration feature gate checks
|
||||
|
||||
**StorageCleanupService**
|
||||
- Scheduled daily: deletes orphaned storage keys from `storage_cleanup_entries`
|
||||
- Scheduled daily: purges expired share links from `file_shares`
|
||||
- Processes cleanup in batches of 50 entries
|
||||
|
||||
#### Storage Providers
|
||||
|
||||
**LocalStorageProvider**
|
||||
- Files stored on the filesystem under `storage.local.basePath` (default: `./storage`)
|
||||
- Storage key is a path relative to the base directory
|
||||
|
||||
**DatabaseStorageProvider**
|
||||
- Files stored as BLOBs in `stored_file_blobs` table
|
||||
- No filesystem dependency
|
||||
|
||||
Provider is selected at startup via `storage.provider: local | database`.
|
||||
|
||||
#### Controller Layer
|
||||
|
||||
**FileStorageController** (`/api/v1/storage`)
|
||||
- All endpoints require authentication
|
||||
- File CRUD and sharing operations
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User uploads file → StorageProvider stores bytes → StoredFile record created
|
||||
↓
|
||||
Owner shares file → FileShare record created (user or link)
|
||||
↓
|
||||
Recipient accesses file → Access recorded → File bytes streamed
|
||||
```
|
||||
|
||||
## File Operations
|
||||
|
||||
### Upload File
|
||||
|
||||
```bash
|
||||
POST /api/v1/storage/files
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: document.pdf # Required — main file
|
||||
historyBundle: history.json # Optional — version history
|
||||
auditLog: audit.json # Optional — audit trail
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"fileName": "document.pdf",
|
||||
"contentType": "application/pdf",
|
||||
"sizeBytes": 102400,
|
||||
"owner": "alice",
|
||||
"ownedByCurrentUser": true,
|
||||
"accessRole": "editor",
|
||||
"createdAt": "2025-01-01T12:00:00",
|
||||
"updatedAt": "2025-01-01T12:00:00",
|
||||
"sharedWithUsers": [],
|
||||
"sharedUsers": [],
|
||||
"shareLinks": []
|
||||
}
|
||||
```
|
||||
|
||||
### Update File
|
||||
|
||||
Replaces the file content. Only the owner can update.
|
||||
|
||||
```bash
|
||||
PUT /api/v1/storage/files/{fileId}
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: document_v2.pdf
|
||||
historyBundle: history.json # Optional
|
||||
auditLog: audit.json # Optional
|
||||
```
|
||||
|
||||
### List Files
|
||||
|
||||
Returns all files owned by or shared with the current user. Workflow-shared files (signing participants) are excluded — those are accessible via signing endpoints only.
|
||||
|
||||
```bash
|
||||
GET /api/v1/storage/files
|
||||
```
|
||||
|
||||
Response is sorted by `createdAt` descending.
|
||||
|
||||
### Download File
|
||||
|
||||
```bash
|
||||
GET /api/v1/storage/files/{fileId}/download?inline=false
|
||||
```
|
||||
|
||||
- `inline=false` (default) — `Content-Disposition: attachment`
|
||||
- `inline=true` — `Content-Disposition: inline` (for browser preview)
|
||||
|
||||
### Delete File
|
||||
|
||||
Only the owner can delete. All associated share links and their access records are deleted first, then the database record, then the physical storage object.
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/storage/files/{fileId}
|
||||
```
|
||||
|
||||
## Sharing Operations
|
||||
|
||||
### Share with User
|
||||
|
||||
```bash
|
||||
POST /api/v1/storage/files/{fileId}/shares/users
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "bob", # Username or email address
|
||||
"accessRole": "editor" # "editor", "commenter", or "viewer" (default: "editor")
|
||||
}
|
||||
```
|
||||
|
||||
**Behaviour:**
|
||||
- If the target user exists: creates/updates a `FileShare` with `sharedWithUser` set
|
||||
- If `username` is an email address and the user doesn't exist: creates a share link and sends a notification email (requires `sharing.emailEnabled` and `sharing.linkEnabled`)
|
||||
- If the target user is the owner: returns 400
|
||||
- If sharing is disabled: returns 403
|
||||
|
||||
### Revoke User Share
|
||||
|
||||
Only the owner can revoke.
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/storage/files/{fileId}/shares/users/{username}
|
||||
```
|
||||
|
||||
### Leave Shared File
|
||||
|
||||
The recipient removes themselves from a shared file.
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/storage/files/{fileId}/shares/self
|
||||
```
|
||||
|
||||
### Create Share Link
|
||||
|
||||
Creates a token-based link for anonymous/authenticated access. Requires `sharing.linkEnabled` and `system.frontendUrl` to be configured.
|
||||
|
||||
```bash
|
||||
POST /api/v1/storage/files/{fileId}/shares/links
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"accessRole": "viewer" # Optional (default: "editor")
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"token": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"accessRole": "viewer",
|
||||
"createdAt": "2025-01-01T12:00:00",
|
||||
"expiresAt": "2025-01-04T12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
Expiration is set to `now + sharing.linkExpirationDays` (default: 3 days).
|
||||
|
||||
### Revoke Share Link
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/storage/files/{fileId}/shares/links/{token}
|
||||
```
|
||||
|
||||
Also deletes all access records for that token.
|
||||
|
||||
## Share Link Access
|
||||
|
||||
### Download via Share Link
|
||||
|
||||
Authentication is required (even for share links). Anonymous access is not permitted.
|
||||
|
||||
```bash
|
||||
GET /api/v1/storage/share-links/{token}?inline=false
|
||||
```
|
||||
|
||||
- Returns 401 if unauthenticated
|
||||
- Returns 403 if authenticated but link doesn't permit access
|
||||
- Returns 410 if the link has expired
|
||||
- Records a `FileShareAccess` entry on success
|
||||
|
||||
> **Token-as-credential semantics:** Any authenticated user who holds the token can access the file — the token is the credential. If you need per-user access control (only a specific person can open it), use "Share with User" instead. Share links are appropriate for broader distribution where possession of the token implies authorization.
|
||||
|
||||
### Get Share Link Metadata
|
||||
|
||||
```bash
|
||||
GET /api/v1/storage/share-links/{token}/metadata
|
||||
```
|
||||
|
||||
Returns file name, owner, access role, creation/expiry timestamps, and whether the current user owns the file.
|
||||
|
||||
### List Accessed Share Links
|
||||
|
||||
Returns the most recent access for each non-expired share link the current user has accessed.
|
||||
|
||||
```bash
|
||||
GET /api/v1/storage/share-links/accessed
|
||||
```
|
||||
|
||||
### List Accesses for a Link (Owner Only)
|
||||
|
||||
```bash
|
||||
GET /api/v1/storage/files/{fileId}/shares/links/{token}/accesses
|
||||
```
|
||||
|
||||
Returns per-user access history (username, VIEW/DOWNLOAD, timestamp), sorted descending by time.
|
||||
|
||||
## Workflow Share Integration
|
||||
|
||||
Signing workflow participants access documents via their own `WorkflowParticipant.shareToken`. No `FileShare` record is created for participants; access control is self-contained in the `WorkflowParticipant` entity.
|
||||
|
||||
The `FileShare.workflow_participant_id` column and the `FileShare.isWorkflowShare()` method are **deprecated**. Legacy data (sessions created before this change) may still have `FileShare` records with `workflow_participant_id` set, which continue to work via the existing token lookup path in `UnifiedAccessControlService`. No new records are created.
|
||||
|
||||
`GET /api/v1/storage/files` returns all files owned by or shared with the current user (via `FileShare`). Signing-session PDFs use the `file_purpose` field (`SIGNING_ORIGINAL`, `SIGNING_SIGNED`, etc.) to distinguish them from generic files. The file manager UI can filter on this field if needed.
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| POST | `/api/v1/storage/files` | Upload file | Required |
|
||||
| PUT | `/api/v1/storage/files/{id}` | Update file | Required (owner) |
|
||||
| GET | `/api/v1/storage/files` | List accessible files | Required |
|
||||
| GET | `/api/v1/storage/files/{id}` | Get file metadata | Required |
|
||||
| GET | `/api/v1/storage/files/{id}/download` | Download file | Required |
|
||||
| DELETE | `/api/v1/storage/files/{id}` | Delete file | Required (owner) |
|
||||
| POST | `/api/v1/storage/files/{id}/shares/users` | Share with user | Required (owner) |
|
||||
| DELETE | `/api/v1/storage/files/{id}/shares/users/{username}` | Revoke user share | Required (owner) |
|
||||
| DELETE | `/api/v1/storage/files/{id}/shares/self` | Leave shared file | Required |
|
||||
| POST | `/api/v1/storage/files/{id}/shares/links` | Create share link | Required (owner) |
|
||||
| DELETE | `/api/v1/storage/files/{id}/shares/links/{token}` | Revoke share link | Required (owner) |
|
||||
| GET | `/api/v1/storage/share-links/{token}` | Download via share link | Required |
|
||||
| GET | `/api/v1/storage/share-links/{token}/metadata` | Get share link metadata | Required |
|
||||
| GET | `/api/v1/storage/share-links/accessed` | List accessed share links | Required |
|
||||
| GET | `/api/v1/storage/files/{id}/shares/links/{token}/accesses` | List share accesses | Required (owner) |
|
||||
|
||||
## Configuration
|
||||
|
||||
All storage settings live under the `storage:` key in `settings.yml`:
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
enabled: true # Requires security.enableLogin = true
|
||||
provider: local # 'local' or 'database'
|
||||
local:
|
||||
basePath: './storage' # Filesystem base directory (local provider only)
|
||||
quotas:
|
||||
maxStorageMbPerUser: -1 # Per-user storage cap in MB; -1 = unlimited
|
||||
maxStorageMbTotal: -1 # Total storage cap in MB; -1 = unlimited
|
||||
maxFileMb: -1 # Max size per upload (main + history + audit) in MB; -1 = unlimited
|
||||
sharing:
|
||||
enabled: false # Master switch for all sharing (opt-in)
|
||||
linkEnabled: false # Enable token-based share links (requires system.frontendUrl)
|
||||
emailEnabled: false # Enable email notifications (requires mail.enabled)
|
||||
linkExpirationDays: 3 # Days until share links expire
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
- `storage.enabled` requires `security.enableLogin = true`
|
||||
- `sharing.linkEnabled` requires `system.frontendUrl` to be set (used to build share link URLs)
|
||||
- `sharing.emailEnabled` requires `mail.enabled = true`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Access Control
|
||||
- All endpoints require authentication — there is no anonymous access
|
||||
- Owner-only operations enforced in service layer (not just controller)
|
||||
- `requireReadAccess` / `requireEditorAccess` checked on every download
|
||||
|
||||
### Share Link Security
|
||||
- Tokens are UUIDs (random, not guessable)
|
||||
- Expiration enforced on every access
|
||||
- Expired links return HTTP 410 Gone
|
||||
- Revoked links delete all access records
|
||||
|
||||
### Quota Enforcement
|
||||
- Checked before storing (not after)
|
||||
- Accounts for existing file size when replacing (only the delta counts)
|
||||
- Covers main file + history bundle + audit log in a single check
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
`StorageCleanupService` runs two scheduled jobs daily:
|
||||
|
||||
1. **Orphaned storage cleanup** — processes up to 50 `StorageCleanupEntry` records, deletes the physical storage object, then removes the entry. Failed attempts increment `attemptCount` for retry.
|
||||
|
||||
2. **Expired share link cleanup** — deletes all `FileShare` records where `expiresAt` is in the past and `shareToken` is set.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Storage is disabled":**
|
||||
- Check `storage.enabled: true` in settings
|
||||
- Verify `security.enableLogin: true`
|
||||
|
||||
**"Share links are disabled":**
|
||||
- Check `sharing.linkEnabled: true`
|
||||
- Verify `system.frontendUrl` is set and non-empty
|
||||
|
||||
**"Email sharing is disabled":**
|
||||
- Check `sharing.emailEnabled: true`
|
||||
- Verify `mail.enabled: true` and mail configuration
|
||||
|
||||
**Signing-session PDF appearing in the general file list:**
|
||||
- This is expected — signing PDFs are accessible to owners and shared users
|
||||
- Filter by `file_purpose` (`SIGNING_ORIGINAL`, `SIGNING_SIGNED`) in the UI to distinguish them
|
||||
|
||||
**Share link returns 410:**
|
||||
- Link has expired — check `expires_at` in `file_shares` table
|
||||
- Owner must create a new link
|
||||
|
||||
### Debug Queries
|
||||
|
||||
```sql
|
||||
-- List files and their share counts
|
||||
SELECT sf.stored_file_id, sf.original_filename, u.username as owner,
|
||||
COUNT(DISTINCT fs.file_share_id) FILTER (WHERE fs.shared_with_user_id IS NOT NULL) as user_shares,
|
||||
COUNT(DISTINCT fs.file_share_id) FILTER (WHERE fs.share_token IS NOT NULL) as link_shares
|
||||
FROM stored_files sf
|
||||
LEFT JOIN users u ON sf.owner_id = u.user_id
|
||||
LEFT JOIN file_shares fs ON fs.stored_file_id = sf.stored_file_id
|
||||
GROUP BY sf.stored_file_id, u.username;
|
||||
|
||||
-- Check share link expiration
|
||||
SELECT share_token, access_role, created_at, expires_at,
|
||||
expires_at < NOW() as is_expired
|
||||
FROM file_shares
|
||||
WHERE share_token IS NOT NULL;
|
||||
|
||||
-- Check access history for a share link
|
||||
SELECT u.username, fsa.access_type, fsa.accessed_at
|
||||
FROM file_share_accesses fsa
|
||||
JOIN file_shares fs ON fsa.file_share_id = fs.file_share_id
|
||||
JOIN users u ON fsa.user_id = u.user_id
|
||||
WHERE fs.share_token = '{token}'
|
||||
ORDER BY fsa.accessed_at DESC;
|
||||
|
||||
-- Pending cleanup entries
|
||||
SELECT storage_key, attempt_count, updated_at
|
||||
FROM storage_cleanup_entries
|
||||
ORDER BY updated_at ASC;
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The File Sharing feature provides:
|
||||
- ✅ Server-side file storage with pluggable backend (local/database)
|
||||
- ✅ History bundle and audit log attachments per file
|
||||
- ✅ Direct user-to-user sharing with EDITOR/COMMENTER/VIEWER roles
|
||||
- ✅ Token-based share links with expiration
|
||||
- ✅ Optional email notifications for shares
|
||||
- ✅ Per-access audit trail for share links
|
||||
- ✅ Storage quotas (per-user, total, per-file)
|
||||
- ✅ Automatic cleanup of expired links and orphaned storage
|
||||
- ✅ Workflow integration (signing-session PDFs stored via same infrastructure; participant access via `WorkflowParticipant.shareToken`)
|
||||
@@ -6,22 +6,10 @@ Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "app/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE".
|
||||
* All content that resides under the "app/saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "app/saas/LICENSE".
|
||||
* All content that resides under the "engine/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "engine/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/proprietary/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/desktop/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/desktop/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/saas/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/cloud/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/cloud/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/prototypes/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/prototypes/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/portal/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/portal/LICENSE".
|
||||
* All content that resides under the "frontend/src/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/proprietary/LICENSE".
|
||||
* All content that resides under the "frontend/src/desktop/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE".
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the MIT License as defined below.
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ For full installation options (including desktop and Kubernetes), see our [Docum
|
||||
|
||||
## Support
|
||||
|
||||
- **Community**: [Discord](https://discord.gg/HYmhKj45pU)
|
||||
- **Bug Reports**: [GitHub Issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
- **Community** [Discord](https://discord.gg/HYmhKj45pU)
|
||||
- **Bug Reports**: [Github issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
This project uses [Task](https://taskfile.dev/) as a unified command runner for all build, dev, and test commands. Run `task dev` to get started running the editor, run `task` to see the most common commands, or see the [Developer Guide](DeveloperGuide.md) for full details.
|
||||
For development setup, see the [Developer Guide](DeveloperGuide.md).
|
||||
|
||||
For adding translations, see the [Translation Guide](devGuide/HowToAddNewLanguage.md).
|
||||
|
||||
|
||||
@@ -1,691 +0,0 @@
|
||||
# Shared Signing Feature - Architecture & Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
The Shared Signing feature enables collaborative document signing workflows where a document owner can request signatures from multiple participants. Each participant receives a secure token to access the document, submit their digital signature (with optional wet signature overlay), and track the signing progress.
|
||||
|
||||
**Key Capabilities:**
|
||||
- Multi-participant signing sessions
|
||||
- Digital certificate signatures (P12/PKCS12, JKS, SERVER, USER_CERT, PEM/UPLOAD)
|
||||
- Visual wet signature overlays (drawn, typed, or uploaded) — multiple per participant
|
||||
- Token-based participant access (no authentication required for participants)
|
||||
- Authenticated participant access for registered users via sign-requests API
|
||||
- Progress tracking for session owners
|
||||
- Optional signature summary page appended to finalized PDF
|
||||
- Automatic role downgrade after signing (security)
|
||||
- GDPR-compliant wet signature metadata cleanup
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Schema
|
||||
|
||||
#### Core Tables
|
||||
|
||||
**`workflow_sessions`**
|
||||
- Tracks signing sessions created by document owners
|
||||
- Links to original and processed (signed) PDF files
|
||||
- Stores session metadata (message, due date, status)
|
||||
|
||||
**`workflow_participants`**
|
||||
- One record per participant per session
|
||||
- Tracks participant status: PENDING → VIEWED → SIGNED/DECLINED
|
||||
- `NOTIFIED` status is reserved for a future email notification feature; no current code path sets it
|
||||
- Stores participant-specific metadata (certificates, wet signatures) as JSONB
|
||||
- Each participant holds their own `shareToken` (UUID) for token-based access — no separate `FileShare` record is created
|
||||
- `accessRole` controls what actions the participant can perform. `COMMENTER` (and `EDITOR`) allow submitting a signature; `VIEWER` does not. After signing/declining, effective role is automatically downgraded to `VIEWER`
|
||||
|
||||
**`user_server_certificates`**
|
||||
- Stores auto-generated certificates per user
|
||||
- Enables "Use My Personal Certificate" option
|
||||
|
||||
#### Extended Tables
|
||||
|
||||
**`stored_files`**
|
||||
- Added `workflow_session_id` to link files to signing sessions
|
||||
- Added `file_purpose` enum (SIGNING_ORIGINAL, SIGNING_SIGNED, etc.)
|
||||
|
||||
**`file_shares`**
|
||||
- Regular file shares are created when the session owner shares the document with other users via the file manager
|
||||
- The `workflow_participant_id` column is deprecated; participant access is self-contained in `WorkflowParticipant.shareToken`
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
#### Service Layer
|
||||
|
||||
**WorkflowSessionService** (`816 lines`)
|
||||
- Core workflow management service
|
||||
- Creates sessions with participants
|
||||
- Handles participant status updates
|
||||
- Stores signature metadata (certificates and wet signatures)
|
||||
- Finalizes sessions by coordinating signing process
|
||||
|
||||
Key responsibilities:
|
||||
- Session lifecycle management (create, list, get details, delete)
|
||||
- Participant management (add, remove, notify)
|
||||
- Certificate submission storage
|
||||
- Wet signature metadata storage
|
||||
- Session finalization orchestration
|
||||
|
||||
**UnifiedAccessControlService**
|
||||
- Validates participant tokens
|
||||
- Checks session status and expiration
|
||||
- Maps participant status to effective access role
|
||||
- Automatic role downgrade after signing: SIGNED/DECLINED → VIEWER role
|
||||
|
||||
**UserServerCertificateService**
|
||||
- Auto-generates personal certificates for users
|
||||
- Manages certificate storage and retrieval
|
||||
- Enables "Use My Personal Certificate" signing option
|
||||
|
||||
#### Controller Layer
|
||||
|
||||
**SigningSessionController** (Owner-facing + Authenticated participant endpoints)
|
||||
- `POST /api/v1/security/cert-sign/sessions` - Create signing session
|
||||
- `GET /api/v1/security/cert-sign/sessions` - List user's sessions
|
||||
- `GET /api/v1/security/cert-sign/sessions/{id}` - Get session details
|
||||
- `GET /api/v1/security/cert-sign/sessions/{id}/pdf` - Download original PDF
|
||||
- `POST /api/v1/security/cert-sign/sessions/{id}/finalize` - Finalize and apply signatures
|
||||
- `GET /api/v1/security/cert-sign/sessions/{id}/signed-pdf` - Download signed PDF
|
||||
- `DELETE /api/v1/security/cert-sign/sessions/{id}` - Delete session
|
||||
- `POST /api/v1/security/cert-sign/sessions/{id}/participants` - Add participants
|
||||
- `DELETE /api/v1/security/cert-sign/sessions/{id}/participants/{participantId}` - Remove participant
|
||||
- `GET /api/v1/security/cert-sign/sign-requests` - List sign requests for authenticated user
|
||||
- `GET /api/v1/security/cert-sign/sign-requests/{id}` - Get sign request details
|
||||
- `GET /api/v1/security/cert-sign/sign-requests/{id}/document` - Download document for signing
|
||||
- `POST /api/v1/security/cert-sign/sign-requests/{id}/sign` - Sign document (authenticated)
|
||||
- `POST /api/v1/security/cert-sign/sign-requests/{id}/decline` - Decline sign request (authenticated)
|
||||
|
||||
**WorkflowParticipantController** (Participant-facing, token-based)
|
||||
- `GET /api/v1/workflow/participant/session?token={token}` - View session details
|
||||
- `GET /api/v1/workflow/participant/details?token={token}` - Get participant details
|
||||
- `GET /api/v1/workflow/participant/document?token={token}` - Download PDF
|
||||
- `POST /api/v1/workflow/participant/submit-signature` - Submit signature
|
||||
- `POST /api/v1/workflow/participant/decline?token={token}` - Decline to sign
|
||||
|
||||
#### Data Flow
|
||||
|
||||
```
|
||||
Owner creates session → Participants receive tokens →
|
||||
Participants access via token (or authenticated) → Participants submit signatures →
|
||||
Owner finalizes → System applies signatures → [Optional: append summary page] → Signed PDF generated
|
||||
```
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
#### Quick Access Integration
|
||||
|
||||
**SignPopout Component**
|
||||
- Displays in Quick Access Bar (top navigation)
|
||||
- Shows active and completed signing sessions
|
||||
- Auto-refreshes every 15 seconds to show signature progress
|
||||
- Badge indicator shows count of pending sessions
|
||||
|
||||
**ActiveSessionsPanel**
|
||||
- Lists sessions where user is owner or participant
|
||||
- Shows signature progress: "X/Y signatures" (e.g., "2/5 signatures")
|
||||
- Color-coded badges:
|
||||
- Blue: No signatures yet (0/X)
|
||||
- Yellow: Partial signatures (X/Y)
|
||||
- Green: Ready to finalize (X/X)
|
||||
|
||||
**CompletedSessionsPanel**
|
||||
- Lists finalized sessions and declined sign requests
|
||||
- Allows viewing/downloading signed PDFs
|
||||
|
||||
#### Workbench Views
|
||||
|
||||
**SignRequestWorkbenchView**
|
||||
- Full-screen view for participants to sign documents
|
||||
- Integrated PDF viewer with annotation support
|
||||
- Certificate selection (Personal/Organization/Custom P12)
|
||||
- Wet signature input (draw, type, or upload)
|
||||
- Signature placement on PDF pages
|
||||
|
||||
**SessionDetailWorkbenchView**
|
||||
- Owner's view of session details
|
||||
- Participant list with status indicators
|
||||
- Ability to add/remove participants
|
||||
- Finalize button when all signatures collected
|
||||
- Download original/signed PDF
|
||||
|
||||
#### State Management
|
||||
|
||||
**FileContext Integration**
|
||||
- Signing sessions operate within FileContext workflow
|
||||
- PDFs loaded once, persist across tool switches
|
||||
- Memory management for large files (up to 100GB+)
|
||||
|
||||
**ToolWorkflowContext**
|
||||
- Registers custom workbench views
|
||||
- Manages navigation between viewer and signing tools
|
||||
- Preserves file state during signing operations
|
||||
|
||||
#### Services & Hooks
|
||||
|
||||
**workflowService.ts**
|
||||
- API client for all signing endpoints
|
||||
- Handles session creation, listing, and management
|
||||
- Participant operations (submit, decline)
|
||||
|
||||
**useWorkflowSession.ts**
|
||||
- React hook for owner session management
|
||||
- State management for session list and details
|
||||
|
||||
**useParticipantSession.ts**
|
||||
- React hook for participant signing workflow
|
||||
- Manages signature submission state
|
||||
|
||||
## Signing Workflow Process
|
||||
|
||||
### 1. Session Creation (Owner)
|
||||
|
||||
```
|
||||
Owner → Uploads PDF → Selects participants → Creates session
|
||||
↓
|
||||
System creates:
|
||||
- WorkflowSession record
|
||||
- WorkflowParticipant records (one per participant, each with a unique shareToken)
|
||||
↓
|
||||
Participants receive token (via email or share link)
|
||||
```
|
||||
|
||||
**API Call:**
|
||||
```bash
|
||||
POST /api/v1/security/cert-sign/sessions
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: document.pdf
|
||||
workflowType: SIGNING
|
||||
documentName: "contract.pdf" # Optional display name
|
||||
participantUserIds: [1, 2, 3] # Registered user IDs
|
||||
participantEmails: ["a@b.com"] # External/unregistered users
|
||||
participants: [...] # Detailed participant configs (optional)
|
||||
message: "Please sign this contract"
|
||||
dueDate: "2025-12-31"
|
||||
ownerEmail: "owner@example.com" # Optional, for notifications
|
||||
workflowMetadata: '{"showSignature": false, "showLogo": false, "includeSummaryPage": true}'
|
||||
```
|
||||
|
||||
**Session-level `workflowMetadata` fields:**
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `showSignature` | boolean | Show visible digital signature block on PDF |
|
||||
| `pageNumber` | integer | Page to place digital signature on |
|
||||
| `showLogo` | boolean | Show logo in digital signature block |
|
||||
| `includeSummaryPage` | boolean | Append a signature summary page before digital signing |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "uuid",
|
||||
"documentName": "contract.pdf",
|
||||
"participants": [
|
||||
{
|
||||
"userId": 1,
|
||||
"email": "user1@example.com",
|
||||
"shareToken": "token1",
|
||||
"status": "PENDING"
|
||||
}
|
||||
],
|
||||
"participantCount": 3,
|
||||
"signedCount": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Participant Access
|
||||
|
||||
```
|
||||
Participant → Clicks token link → Views session details
|
||||
↓
|
||||
Status changes: PENDING/NOTIFIED → VIEWED
|
||||
↓
|
||||
Participant downloads PDF to review
|
||||
```
|
||||
|
||||
**Access URL (unauthenticated):**
|
||||
```
|
||||
https://app.example.com/sign?token={participant_token}
|
||||
```
|
||||
|
||||
**Authenticated participants** can also use:
|
||||
```
|
||||
GET /api/v1/security/cert-sign/sign-requests
|
||||
GET /api/v1/security/cert-sign/sign-requests/{sessionId}
|
||||
GET /api/v1/security/cert-sign/sign-requests/{sessionId}/document
|
||||
```
|
||||
|
||||
**Automatic Status Update:**
|
||||
- First access: PENDING/NOTIFIED → VIEWED
|
||||
- Downloads tracked but don't change status
|
||||
|
||||
### 3. Signature Submission
|
||||
|
||||
```
|
||||
Participant → Selects certificate type → Uploads certificate (if needed)
|
||||
→ Draws/uploads wet signatures (optional, multiple supported)
|
||||
→ Submits signature
|
||||
↓
|
||||
System stores:
|
||||
- Certificate data (P12/JKS keystore as base64)
|
||||
- Certificate password
|
||||
- Wet signatures metadata (JSON array: base64 image + coordinates per signature)
|
||||
↓
|
||||
Status changes: VIEWED → SIGNED
|
||||
Access role: EDITOR → VIEWER (automatic downgrade)
|
||||
```
|
||||
|
||||
**API Call (token-based, unauthenticated):**
|
||||
```bash
|
||||
POST /api/v1/workflow/participant/submit-signature
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
participantToken: {token}
|
||||
certType: P12 | JKS | SERVER | USER_CERT
|
||||
p12File: certificate.p12 (if certType=P12)
|
||||
jksFile: keystore.jks (if certType=JKS)
|
||||
password: cert_password
|
||||
showSignature: false
|
||||
pageNumber: 1
|
||||
location: "New York"
|
||||
reason: "I approve this contract"
|
||||
showLogo: false
|
||||
wetSignaturesData: '[{"page":0,"x":100,"y":200,"width":150,"height":50,"type":"IMAGE","data":"base64..."}]'
|
||||
```
|
||||
|
||||
**API Call (authenticated users):**
|
||||
```bash
|
||||
POST /api/v1/security/cert-sign/sign-requests/{sessionId}/sign
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
certType: SERVER | USER_CERT | UPLOAD | PEM | PKCS12 | PFX | JKS
|
||||
p12File: certificate.p12 (if applicable)
|
||||
password: cert_password
|
||||
reason: "I approve this contract"
|
||||
location: "New York"
|
||||
wetSignaturesData: '[...]'
|
||||
```
|
||||
|
||||
**Metadata Storage (JSONB):**
|
||||
```json
|
||||
{
|
||||
"certificateSubmission": {
|
||||
"certType": "P12",
|
||||
"password": "cert_password",
|
||||
"p12Keystore": "base64_encoded_keystore",
|
||||
"showSignature": false,
|
||||
"pageNumber": 1,
|
||||
"location": "New York",
|
||||
"reason": "I approve this contract",
|
||||
"showLogo": false
|
||||
},
|
||||
"wetSignatures": [
|
||||
{
|
||||
"type": "IMAGE",
|
||||
"data": "base64_image",
|
||||
"page": 0,
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"width": 150,
|
||||
"height": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note: Multiple wet signatures are supported per participant (array).
|
||||
|
||||
### 4. Progress Tracking (Owner)
|
||||
|
||||
```
|
||||
Owner → Views session list → Sees "2/5 signatures"
|
||||
→ Clicks session → Views participant status
|
||||
↓
|
||||
Participant list shows:
|
||||
- user1@example.com: SIGNED ✓
|
||||
- user2@example.com: SIGNED ✓
|
||||
- user3@example.com: VIEWED (pending)
|
||||
- user4@example.com: PENDING
|
||||
- user5@example.com: DECLINED ✗
|
||||
↓
|
||||
Auto-refresh every 15 seconds
|
||||
```
|
||||
|
||||
**Badge Colors:**
|
||||
- 🔵 Blue: 0/5 signatures (awaiting)
|
||||
- 🟡 Yellow: 2/5 signatures (partial)
|
||||
- 🟢 Green: 5/5 signatures (ready to finalize)
|
||||
|
||||
### 5. Session Finalization
|
||||
|
||||
```
|
||||
Owner → Clicks "Finalize" → System processes signatures
|
||||
↓
|
||||
Processing steps:
|
||||
1. Apply wet signatures to PDF (visual overlays)
|
||||
1.5. Append signature summary page (if includeSummaryPage=true)
|
||||
2. Apply digital certificates in participant order
|
||||
- Visual signature block suppressed when summary page is enabled
|
||||
3. Store signed PDF
|
||||
4. Clear wet signature metadata (GDPR compliance)
|
||||
↓
|
||||
Owner downloads signed PDF
|
||||
```
|
||||
|
||||
**Finalization Process:**
|
||||
|
||||
1. **Apply Wet Signatures First**
|
||||
```java
|
||||
for (WetSignature sig : wetSignatures) {
|
||||
PDPage page = document.getPage(sig.getPage());
|
||||
byte[] imageBytes = Base64.decode(sig.getData());
|
||||
// Convert Y from top-left (UI) to bottom-left (PDF) coordinate system
|
||||
float pdfY = page.getMediaBox().getHeight() - sig.getY() - sig.getHeight();
|
||||
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "signature");
|
||||
contentStream.drawImage(image, sig.getX(), pdfY, sig.getWidth(), sig.getHeight());
|
||||
}
|
||||
```
|
||||
|
||||
2. **Append Summary Page (optional, before digital signing)**
|
||||
|
||||
If `includeSummaryPage=true`, a new A4 page is appended showing:
|
||||
- Stirling logo and "Signature Summary" title
|
||||
- Document name and session owner
|
||||
- Finalization timestamp
|
||||
- Per-participant: name, email, status, signed timestamp, reason, location, certificate type
|
||||
- Supports overflow to additional pages
|
||||
|
||||
This step occurs **before** digital certificate signing so signatures are not invalidated.
|
||||
When a summary page is added, the visual digital signature block (`showSignature`) is suppressed — wet signatures (hand-drawn overlays) are unaffected.
|
||||
|
||||
3. **Apply Digital Certificates (in participant order)**
|
||||
```java
|
||||
for (Participant p : participants) {
|
||||
if (p.status == SIGNED) {
|
||||
KeyStore keystore = buildKeystore(p.certificate);
|
||||
// Reason: participant override > owner default > "Document Signing"
|
||||
// Location: participant-provided only (no default)
|
||||
CertSignController.sign(pdfBytes, keystore, password, settings);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Store and Cleanup**
|
||||
```java
|
||||
StoredFile signedFile = storeFile(signedPdfBytes, SIGNING_SIGNED);
|
||||
session.setProcessedFile(signedFile);
|
||||
session.setFinalized(true);
|
||||
|
||||
// GDPR: Clear sensitive metadata after finalization
|
||||
for (Participant p : participants) {
|
||||
p.metadata.remove("wetSignatures"); // Clears wet signature image data
|
||||
p.metadata.remove("certificateSubmission"); // Clears keystore bytes + password
|
||||
}
|
||||
```
|
||||
|
||||
**API Call:**
|
||||
```bash
|
||||
POST /api/v1/security/cert-sign/sessions/{sessionId}/finalize
|
||||
Authorization: Bearer {owner_token}
|
||||
```
|
||||
|
||||
**Response:** Binary PDF file with Content-Disposition header
|
||||
|
||||
## Key Technical Features
|
||||
|
||||
### 1. Double JSON Encoding Fix (Recent)
|
||||
|
||||
**Problem:** JSONB columns were storing JSON strings instead of JSON objects, requiring double-parsing.
|
||||
|
||||
**Solution:** Created `JsonMapConverter` JPA AttributeConverter:
|
||||
```java
|
||||
@Convert(converter = JsonMapConverter.class)
|
||||
@Column(name = "participant_metadata", columnDefinition = "jsonb")
|
||||
private Map<String, Object> participantMetadata;
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Single parse on read
|
||||
- Proper JSON storage in PostgreSQL
|
||||
- Type-safe Map access
|
||||
- Backward compatible with legacy data
|
||||
|
||||
### 2. Signature Progress Display (Recent)
|
||||
|
||||
**Implementation:**
|
||||
- `WorkflowSessionResponse` includes `participantCount` and `signedCount`
|
||||
- `WorkflowMapper` calculates counts when converting to DTO
|
||||
- Frontend displays "X/Y signatures" in session list
|
||||
- Auto-refresh every 15 seconds keeps counts updated
|
||||
|
||||
### 3. Token-Based Security
|
||||
|
||||
**No Authentication Required for Participants:**
|
||||
- Participants access via secure token (UUID)
|
||||
- Token linked to specific participant and session
|
||||
- Automatic expiration support
|
||||
- One-time signing (cannot sign twice)
|
||||
|
||||
**Authenticated Participant Access:**
|
||||
- Registered users can also access sign requests via `/api/v1/security/cert-sign/sign-requests`
|
||||
- Standard Spring Security authentication required
|
||||
- Supports additional cert types: UPLOAD, PEM, PKCS12, PFX
|
||||
|
||||
**Automatic Role Downgrade:**
|
||||
- After signing: EDITOR → VIEWER
|
||||
- After declining: EDITOR → VIEWER
|
||||
- Prevents modification after action taken
|
||||
|
||||
### 4. Storage Integration
|
||||
|
||||
**Unified with File Sharing:**
|
||||
- All PDFs stored via `StorageProvider` (Database or Local)
|
||||
- Respects storage quotas
|
||||
- Supports files up to 100GB+ (with Local storage)
|
||||
- Consistent with existing file sharing infrastructure
|
||||
|
||||
### 5. Certificate Types
|
||||
|
||||
**P12/PKCS12/PFX:** User uploads PKCS#12 file + password
|
||||
**JKS:** User uploads Java KeyStore + password
|
||||
**PEM/UPLOAD:** User uploads PEM certificate + private key
|
||||
**SERVER:** Uses organization's server certificate (no upload needed)
|
||||
**USER_CERT:** Uses user's auto-generated personal certificate (one-click)
|
||||
|
||||
Note: UPLOAD, PEM, PKCS12, PFX are available on the authenticated (`sign-requests`) path. The token-based path uses P12, JKS, SERVER, USER_CERT.
|
||||
|
||||
## Frontend Components Overview
|
||||
|
||||
### Owner Workflow Components
|
||||
|
||||
1. **CreateSessionPanel** - Form to create new signing session
|
||||
2. **ActiveSessionsPanel** - List of pending sessions with progress
|
||||
3. **SessionDetailWorkbenchView** - Full session management interface
|
||||
4. **CompletedSessionsPanel** - History of finalized sessions
|
||||
|
||||
### Participant Workflow Components
|
||||
|
||||
1. **SignRequestWorkbenchView** - Main signing interface
|
||||
2. **SignatureSettingsInput** - Certificate selection and configuration
|
||||
3. **WetSignatureInput** - Draw/type/upload signature overlay
|
||||
4. **SignatureSettingsDisplay** - Preview of signature settings
|
||||
|
||||
### Shared Components
|
||||
|
||||
1. **UserSelector** - Multi-select user picker for participants
|
||||
2. **LocalEmbedPDFWithAnnotations** - PDF viewer with signature placement
|
||||
|
||||
## Configuration
|
||||
|
||||
### Backend Configuration
|
||||
|
||||
**application.properties:**
|
||||
```properties
|
||||
# Database (H2 or PostgreSQL)
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
|
||||
# Security
|
||||
DOCKER_ENABLE_SECURITY=true
|
||||
|
||||
# Storage Provider (DATABASE or LOCAL)
|
||||
storage.provider=LOCAL
|
||||
storage.maxFileSize=100GB
|
||||
```
|
||||
|
||||
### Frontend Configuration
|
||||
|
||||
**Quick Access Bar:**
|
||||
- Signing popout accessible from top navigation
|
||||
- Auto-refresh interval: 15 seconds
|
||||
- Badge shows pending session count
|
||||
|
||||
## API Reference Summary
|
||||
|
||||
### Owner Endpoints (Authenticated)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/v1/security/cert-sign/sessions` | Create session |
|
||||
| GET | `/api/v1/security/cert-sign/sessions` | List sessions |
|
||||
| GET | `/api/v1/security/cert-sign/sessions/{id}` | Get details |
|
||||
| POST | `/api/v1/security/cert-sign/sessions/{id}/finalize` | Finalize session |
|
||||
| GET | `/api/v1/security/cert-sign/sessions/{id}/pdf` | Download original |
|
||||
| GET | `/api/v1/security/cert-sign/sessions/{id}/signed-pdf` | Download signed |
|
||||
| DELETE | `/api/v1/security/cert-sign/sessions/{id}` | Delete session |
|
||||
| POST | `/api/v1/security/cert-sign/sessions/{id}/participants` | Add participants |
|
||||
| DELETE | `/api/v1/security/cert-sign/sessions/{id}/participants/{pid}` | Remove participant |
|
||||
|
||||
### Authenticated Participant Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/v1/security/cert-sign/sign-requests` | List sign requests |
|
||||
| GET | `/api/v1/security/cert-sign/sign-requests/{id}` | Get sign request details |
|
||||
| GET | `/api/v1/security/cert-sign/sign-requests/{id}/document` | Download document |
|
||||
| POST | `/api/v1/security/cert-sign/sign-requests/{id}/sign` | Sign document |
|
||||
| POST | `/api/v1/security/cert-sign/sign-requests/{id}/decline` | Decline signing |
|
||||
|
||||
### Token-Based Participant Endpoints (No Auth Required)
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/v1/workflow/participant/session?token={token}` | View session |
|
||||
| GET | `/api/v1/workflow/participant/details?token={token}` | Get participant details |
|
||||
| GET | `/api/v1/workflow/participant/document?token={token}` | Download PDF |
|
||||
| POST | `/api/v1/workflow/participant/submit-signature` | Submit signature |
|
||||
| POST | `/api/v1/workflow/participant/decline?token={token}` | Decline signing |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Data Protection
|
||||
- Wet signature image data cleared after finalization (GDPR compliance)
|
||||
- Certificate submission data (keystore bytes + password) cleared after finalization (GDPR compliance)
|
||||
- Certificate passwords are not encrypted at rest while stored (TODO: encrypt at rest)
|
||||
- Token expiration support
|
||||
|
||||
### Access Control
|
||||
- Owner authentication required for session management
|
||||
- Participant access via secure UUID tokens (no auth) or standard auth (sign-requests)
|
||||
- Automatic role downgrade prevents re-signing
|
||||
- Session status checks prevent unauthorized actions
|
||||
|
||||
### Audit Trail
|
||||
- All participant actions tracked
|
||||
- FileShare access logged
|
||||
- Status transitions recorded
|
||||
- Notification history maintained
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Scalability
|
||||
- Supports PDFs up to 100GB+ (with Local storage provider)
|
||||
- Memory-efficient streaming for large files
|
||||
- IndexedDB caching on frontend
|
||||
- Database indexes on session_id, share_token, workflow_session_id
|
||||
|
||||
### Response Times
|
||||
- Session creation: ~500ms (10MB file)
|
||||
- Session listing: ~100ms
|
||||
- Token validation: ~50ms
|
||||
- Finalization: ~2s per MB of PDF (varies by certificate operations)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- Email notifications for participants
|
||||
- Reminder system for pending signatures
|
||||
- Bulk signing operations
|
||||
- Template-based signing workflows
|
||||
- Signature validation/verification UI
|
||||
- Certificate password encryption at rest
|
||||
- Certificate keystore cleanup after finalization (GDPR)
|
||||
- Webhook support for external integrations
|
||||
- Analytics dashboard for signing metrics
|
||||
|
||||
### Additional Workflow Types
|
||||
- **REVIEW** - Document review with comments
|
||||
- **APPROVAL** - Multi-level approval chains
|
||||
- **COLLABORATION** - Real-time collaborative editing
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**"Token invalid" error:**
|
||||
- Check token exists in workflow_participants table
|
||||
- Verify session is not finalized
|
||||
- Check expiration date (expires_at)
|
||||
|
||||
**Signature not appearing on PDF:**
|
||||
- Verify certificate type is correct
|
||||
- Check certificate password
|
||||
- Review logs for signing errors
|
||||
- Ensure PDFDocumentFactory is available
|
||||
|
||||
**"Awaiting signatures" not updating:**
|
||||
- Backend should return participantCount and signedCount
|
||||
- Frontend auto-refresh every 15 seconds
|
||||
- Check network tab for API errors
|
||||
|
||||
**Wet signatures not visible after finalization:**
|
||||
- Wet signatures are applied first as image overlays (Step 1)
|
||||
- Check `wetSignaturesData` was sent as valid JSON array
|
||||
- Verify page index is within document bounds
|
||||
- Note: wet signatures survive regardless of `includeSummaryPage` setting
|
||||
|
||||
### Debug Queries
|
||||
|
||||
```sql
|
||||
-- Check session status
|
||||
SELECT session_id, status, finalized,
|
||||
(SELECT COUNT(*) FROM workflow_participants WHERE workflow_session_id = ws.id) as participant_count,
|
||||
(SELECT COUNT(*) FROM workflow_participants WHERE workflow_session_id = ws.id AND status = 'SIGNED') as signed_count
|
||||
FROM workflow_sessions ws;
|
||||
|
||||
-- Check participant tokens
|
||||
SELECT email, status, share_token, expires_at
|
||||
FROM workflow_participants
|
||||
WHERE workflow_session_id = (SELECT id FROM workflow_sessions WHERE session_id = '{session_id}');
|
||||
|
||||
-- Check metadata storage
|
||||
SELECT email,
|
||||
participant_metadata->'certificateSubmission'->>'certType' as cert_type,
|
||||
jsonb_array_length(participant_metadata->'wetSignatures') as wet_sig_count
|
||||
FROM workflow_participants;
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The Shared Signing feature provides a complete collaborative signing workflow with:
|
||||
- ✅ Multi-participant support with progress tracking
|
||||
- ✅ Multiple certificate types (P12/PKCS12/PFX, JKS, PEM, SERVER, USER_CERT)
|
||||
- ✅ Visual wet signature overlays (multiple per participant)
|
||||
- ✅ Token-based security for unauthenticated participants
|
||||
- ✅ Authenticated participant access via sign-requests API
|
||||
- ✅ Automatic role management
|
||||
- ✅ Large file support (100GB+)
|
||||
- ✅ GDPR-compliant wet signature metadata cleanup
|
||||
- ✅ Real-time progress updates
|
||||
- ✅ Full frontend integration with Quick Access Bar
|
||||
- ✅ Optional signature summary page with logo and participant details
|
||||
|
||||
The architecture leverages existing file sharing infrastructure while adding workflow-specific features, ensuring consistency and maintainability across the application.
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
output: prefixed
|
||||
|
||||
vars:
|
||||
FIND_FREE_PORT_PS: powershell -NoProfile -File scripts/find-free-port.ps1
|
||||
FIND_FREE_PORT_SH: bash scripts/find-free-port.sh
|
||||
|
||||
includes:
|
||||
backend:
|
||||
taskfile: .taskfiles/backend.yml
|
||||
dir: .
|
||||
frontend:
|
||||
taskfile: .taskfiles/frontend.yml
|
||||
dir: frontend
|
||||
engine:
|
||||
taskfile: .taskfiles/engine.yml
|
||||
dir: engine
|
||||
docker:
|
||||
taskfile: .taskfiles/docker.yml
|
||||
dir: .
|
||||
desktop:
|
||||
taskfile: .taskfiles/desktop.yml
|
||||
dir: frontend
|
||||
e2e:
|
||||
taskfile: .taskfiles/e2e.yml
|
||||
dir: .
|
||||
pre-commit:
|
||||
taskfile: .taskfiles/pre-commit.yml
|
||||
dir: .
|
||||
|
||||
tasks:
|
||||
# ============================================================
|
||||
# Help (shown when you run `task` with no arguments)
|
||||
# ============================================================
|
||||
|
||||
default:
|
||||
desc: "List the most common commands"
|
||||
silent: true
|
||||
cmds:
|
||||
- |
|
||||
echo "Common commands (run 'task --list' to see all):"
|
||||
echo ""
|
||||
echo " task dev Start backend & frontend on free ports"
|
||||
echo " task backend:dev Start backend on default port"
|
||||
echo " task frontend:dev Start frontend on default port"
|
||||
echo " task desktop:dev Start desktop app"
|
||||
echo " task check Quality gate (lint, typecheck, test, etc.)"
|
||||
|
||||
# ============================================================
|
||||
# Setup & Prerequisites
|
||||
# ============================================================
|
||||
|
||||
install:
|
||||
desc: "Install all project dependencies"
|
||||
cmds:
|
||||
- task: frontend:install
|
||||
- task: engine:install
|
||||
|
||||
# ============================================================
|
||||
# Development
|
||||
# ============================================================
|
||||
|
||||
dev:
|
||||
desc: "Start backend + frontend concurrently on free ports"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
deps:
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
- task: frontend:dev
|
||||
vars:
|
||||
PORT: '{{.FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
dev:portal:
|
||||
desc: "Start backend + editor; the portal is an admin route at /portal"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
EDITOR_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
deps:
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
POLICIES_ENABLED: "true"
|
||||
- task: frontend:dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.EDITOR_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
dev:saas:
|
||||
desc: "Start SaaS backend + frontend concurrently on free ports"
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
vars: { FRONTEND: saas, BACKEND: saas }
|
||||
|
||||
dev:all:
|
||||
desc: "Start backend + frontend + engine concurrently on free ports"
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
|
||||
dev:_all:
|
||||
internal: true
|
||||
vars:
|
||||
FRONTEND: '{{.FRONTEND | default "proprietary"}}'
|
||||
BACKEND: '{{.BACKEND | default "proprietary"}}'
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
ENGINE_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||
deps:
|
||||
- task: engine:dev
|
||||
vars:
|
||||
PORT: '{{.ENGINE_PORT}}'
|
||||
- task: 'backend:dev:{{.BACKEND}}'
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}'
|
||||
AIENGINE_ENABLED: "true"
|
||||
- task: 'frontend:dev:{{.FRONTEND}}'
|
||||
vars:
|
||||
PORT: '{{.FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
# ============================================================
|
||||
# Build
|
||||
# ============================================================
|
||||
|
||||
build:
|
||||
desc: "Build all components"
|
||||
cmds:
|
||||
- task: backend:build
|
||||
- task: frontend:build
|
||||
|
||||
# ============================================================
|
||||
# Test
|
||||
# ============================================================
|
||||
|
||||
test:
|
||||
desc: "Run ALL tests (backend + frontend + engine)"
|
||||
cmds:
|
||||
- task: backend:test
|
||||
- task: frontend:test
|
||||
- task: engine:test
|
||||
|
||||
# ============================================================
|
||||
# Lint & Format
|
||||
# ============================================================
|
||||
|
||||
lint:
|
||||
desc: "Run all linters"
|
||||
cmds:
|
||||
- task: frontend:lint
|
||||
- task: engine:lint
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix all components"
|
||||
cmds:
|
||||
- task: backend:fix
|
||||
- task: frontend:fix
|
||||
- task: engine:fix
|
||||
|
||||
format:
|
||||
desc: "Auto-fix formatting across all components"
|
||||
cmds:
|
||||
- task: backend:format
|
||||
- task: frontend:format
|
||||
- task: engine:format
|
||||
|
||||
format:check:
|
||||
desc: "Check formatting across all components"
|
||||
cmds:
|
||||
- task: backend:format:check
|
||||
- task: frontend:format:check
|
||||
- task: engine:format:check
|
||||
|
||||
# ============================================================
|
||||
# Code generation
|
||||
# ============================================================
|
||||
|
||||
tool-models:
|
||||
desc: "Generate all API models from the Java OpenAPI spec"
|
||||
cmds:
|
||||
- task: frontend:tool-models
|
||||
- task: engine:tool-models
|
||||
|
||||
# ============================================================
|
||||
# Quality Gate
|
||||
# ============================================================
|
||||
|
||||
check:
|
||||
desc: "Quick quality gate for local development"
|
||||
cmds:
|
||||
- task: backend:check
|
||||
- task: frontend:check
|
||||
- task: engine:check
|
||||
|
||||
check:all:
|
||||
desc: "Full CI quality gate"
|
||||
cmds:
|
||||
- task: backend:check
|
||||
- task: frontend:check:all
|
||||
- task: engine:check
|
||||
|
||||
# ============================================================
|
||||
# Clean
|
||||
# ============================================================
|
||||
|
||||
clean:
|
||||
desc: "Clean all build artifacts"
|
||||
cmds:
|
||||
- task: backend:clean
|
||||
- task: frontend:clean
|
||||
- task: engine:clean
|
||||
- task: pre-commit:clean
|
||||
+1
-1
@@ -86,7 +86,7 @@ If you're using Tauri's built-in updater feature:
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### 1. Tauri Configuration (frontend/editor/src-tauri/tauri.conf.json)
|
||||
### 1. Tauri Configuration (frontend/src-tauri/tauri.conf.json)
|
||||
|
||||
The Windows signing configuration is already set up:
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Committed defaults for `task backend:dev:proprietary` (self-hosted / proprietary
|
||||
# flavor). Local overrides + secrets live in app/.env.proprietary.local (ignored).
|
||||
|
||||
# Combined-billing account link (Mode A). Feature-flagged: OFF until release.
|
||||
# Flip to true in app/.env.proprietary.local to test linking locally.
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED=false
|
||||
# SaaS base URL the linked instance calls (register + entitlement).
|
||||
STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL=https://stirling.com/app
|
||||
@@ -1,34 +0,0 @@
|
||||
###############################################################################
|
||||
# Stirling-PDF SaaS environment defaults.
|
||||
#
|
||||
# This file is committed and provides non-secret defaults loaded by
|
||||
# `task backend:dev:saas`. Put real values for secrets (passwords, project
|
||||
# refs, edge function secrets) in `.env.saas.local` - any variable set there
|
||||
# takes precedence over what's defined here.
|
||||
#
|
||||
# DO NOT commit `.env.saas.local`. Only `.env.saas` is checked in.
|
||||
###############################################################################
|
||||
|
||||
# ---------- Supabase project ----------
|
||||
# Project reference (the subdomain part of <ref>.supabase.co). Required.
|
||||
# Set in .env.saas.local.
|
||||
SAAS_DB_PROJECT_REF=
|
||||
|
||||
# Edge function secret used by billing/license rollup calls. Set in .env.saas.local.
|
||||
SUPABASE_EDGE_FUNCTION_SECRET=
|
||||
|
||||
# ---------- Database (saas profile) ----------
|
||||
# Direct JDBC URL to the Supabase Postgres. Required when running the plain
|
||||
# `saas` profile (i.e. without `--spring.profiles.include=dev`).
|
||||
# Example: jdbc:postgresql://db.<project-ref>.supabase.co:5432/postgres
|
||||
SAAS_DB_URL=
|
||||
SAAS_DB_USERNAME=postgres
|
||||
SAAS_DB_PASSWORD=
|
||||
|
||||
# ---------- Database (dev profile overrides) ----------
|
||||
# Used when `--spring.profiles.include=dev` is active. The dev profile
|
||||
# defaults the URL/username to the shared dev Supabase project, but the
|
||||
# password must still be provided in .env.saas.local.
|
||||
SAAS_DEV_DB_URL=
|
||||
SAAS_DEV_DB_USERNAME=postgres
|
||||
SAAS_DEV_DB_PASSWORD=
|
||||
@@ -1,4 +0,0 @@
|
||||
# Whitelist committed env defaults. `.env.saas.local` (and any other .env*)
|
||||
# stays ignored via the root .gitignore.
|
||||
!.env.saas
|
||||
!.env.proprietary
|
||||
@@ -44,10 +44,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The MIT License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT-0"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.github.jai-imageio:jai-imageio-core",
|
||||
"moduleLicense": "LICENSE.txt"
|
||||
@@ -80,18 +76,10 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License, Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Apache License, version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The Apache License, Version 2.0"
|
||||
@@ -116,10 +104,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Mozilla Public License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "CDDL+GPL License"
|
||||
@@ -184,14 +168,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License, Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "EPL-2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "LGPL-2.1-only"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Ubuntu Font Licence 1.0"
|
||||
|
||||
+10
-44
@@ -5,12 +5,10 @@ bootRun {
|
||||
spotless {
|
||||
java {
|
||||
target 'src/**/java/**/*.java'
|
||||
targetExclude 'src/main/java/org/apache/**'
|
||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
|
||||
suppressLintsFor { setStep('google-java-format') }
|
||||
|
||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||
toggleOffOn()
|
||||
trimTrailingWhitespace()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
@@ -29,58 +27,26 @@ spotless {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
api "com.google.guava:guava:${guavaVersion}"
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1'
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
api "org.apache.commons:commons-lang3:${commonsLang3}"
|
||||
api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor
|
||||
api 'org.apache.commons:commons-lang3:3.20.0'
|
||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:7.5.10' // RAR archive support for CBR files
|
||||
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14"
|
||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
||||
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||
|
||||
// Tabula table extraction — used by the shared PDF parser and directly by downstream modules.
|
||||
// api-scoped so downstream modules (core, proprietary) retain it on their compile classpath.
|
||||
api ('technology.tabula:tabula:1.0.5') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-simple'
|
||||
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
|
||||
exclude group: 'com.google.code.gson', module: 'gson'
|
||||
}
|
||||
|
||||
api "com.stirling:jpdfium:${jpdfiumVersion}"
|
||||
|
||||
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
def jpdfiumPlatforms = jpdfiumPlatformsProp == 'all'
|
||||
? jpdfiumAllPlatforms
|
||||
: jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it }
|
||||
def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) }
|
||||
if (jpdfiumInvalid) {
|
||||
throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " +
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')} or 'all'.")
|
||||
}
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
|
||||
jpdfiumPlatforms.each { platform ->
|
||||
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
|
||||
}
|
||||
|
||||
// Bucket4j (local in-process token bucket for RateLimitStore default impl)
|
||||
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
|
||||
|
||||
// ArchUnit: enforces module dependency direction (see ArchitectureTest)
|
||||
testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}"
|
||||
}
|
||||
|
||||
+37
-106
@@ -68,36 +68,6 @@ public class EndpointConfiguration {
|
||||
return endpoint.startsWith("/") ? endpoint.substring(1) : endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a full request URI like {@code /api/v1/general/remove-pages} into the endpoint key
|
||||
* used by this configuration ({@code remove-pages}). Convert endpoints are a special case -
|
||||
* {@code /api/v1/convert/pdf/img} is registered as {@code pdf-to-img}. Returns {@code null} if
|
||||
* the URI is not an {@code /api/v1/<group>/<endpoint>} path.
|
||||
*/
|
||||
public static String endpointKeyForUri(String uri) {
|
||||
if (uri == null || !uri.contains("/api/v1")) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = uri.split("/");
|
||||
if (parts.length <= 4) {
|
||||
return null;
|
||||
}
|
||||
if ("convert".equals(parts[3]) && parts.length > 5) {
|
||||
return parts[4] + "-to-" + parts[5];
|
||||
}
|
||||
return parts[4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper around {@link #isEndpointEnabled(String)} that accepts a full request URI
|
||||
* and translates it to the endpoint key. Falls back to treating the URI itself as a key for
|
||||
* non-{@code /api/v1/...} paths so callers can pass arbitrary URIs.
|
||||
*/
|
||||
public boolean isEndpointEnabledForUri(String uri) {
|
||||
String key = endpointKeyForUri(uri);
|
||||
return isEndpointEnabled(key != null ? key : uri);
|
||||
}
|
||||
|
||||
public void enableEndpoint(String endpoint) {
|
||||
String normalized = normalizeEndpoint(endpoint);
|
||||
endpointStatuses.put(normalized, true);
|
||||
@@ -133,9 +103,8 @@ public class EndpointConfiguration {
|
||||
|
||||
// Rule 2: Functional-group override - check if endpoint belongs to any disabled functional
|
||||
// group
|
||||
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
|
||||
String group = entry.getKey();
|
||||
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
|
||||
for (String group : endpointGroups.keySet()) {
|
||||
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
|
||||
// Skip tool groups (qpdf, OCRmyPDF, Ghostscript, LibreOffice, etc.)
|
||||
if (!isToolGroup(group)) {
|
||||
log.debug(
|
||||
@@ -162,11 +131,10 @@ public class EndpointConfiguration {
|
||||
|
||||
// Rule 4: Single-dependency check - if no alternatives defined, check if endpoint belongs
|
||||
// to any disabled tool groups
|
||||
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
|
||||
String group = entry.getKey();
|
||||
for (String group : endpointGroups.keySet()) {
|
||||
if (isToolGroup(group)
|
||||
&& disabledGroups.contains(group)
|
||||
&& entry.getValue().contains(endpoint)) {
|
||||
&& endpointGroups.get(group).contains(endpoint)) {
|
||||
log.debug(
|
||||
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
|
||||
original,
|
||||
@@ -335,23 +303,22 @@ public class EndpointConfiguration {
|
||||
// Adding endpoints to "PageOps" group
|
||||
addEndpointToGroup("PageOps", "remove-pages");
|
||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||
addEndpointToGroup("PageOps", "split-pages");
|
||||
addEndpointToGroup("PageOps", "rearrange-pages");
|
||||
addEndpointToGroup("PageOps", "split-pdfs");
|
||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||
addEndpointToGroup("PageOps", "booklet-imposition");
|
||||
addEndpointToGroup("PageOps", "scale-pages");
|
||||
addEndpointToGroup("PageOps", "crop");
|
||||
addEndpointToGroup("PageOps", "extract-page");
|
||||
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
||||
addEndpointToGroup("PageOps", "auto-split-pdf");
|
||||
addEndpointToGroup("PageOps", "split-by-size-or-count");
|
||||
addEndpointToGroup("PageOps", "overlay-pdf");
|
||||
addEndpointToGroup("PageOps", "split-pdf-by-sections");
|
||||
addEndpointToGroup("PageOps", "split-pdf-by-chapters");
|
||||
addEndpointToGroup("PageOps", "add-page-numbers");
|
||||
addEndpointToGroup("PageOps", "extract-pages");
|
||||
|
||||
// Adding endpoints to "Convert" group (Frontend has 15 convert endpoints)
|
||||
// Adding endpoints to "Convert" group
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||
@@ -367,8 +334,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-epub");
|
||||
// Backend-only endpoints (not in frontend tool registry)
|
||||
addEndpointToGroup("Convert", "pdf-to-vector");
|
||||
addEndpointToGroup("Convert", "vector-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-video");
|
||||
@@ -376,7 +341,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-cbz");
|
||||
addEndpointToGroup("Convert", "pdf-to-json");
|
||||
addEndpointToGroup("Convert", "json-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-rtf");
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
@@ -386,61 +350,52 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Security", "cert-sign");
|
||||
addEndpointToGroup("Security", "remove-cert-sign");
|
||||
addEndpointToGroup("Security", "sanitize-pdf");
|
||||
addEndpointToGroup("Security", "timestamp-pdf");
|
||||
addEndpointToGroup("Security", "auto-redact");
|
||||
addEndpointToGroup("Security", "validate-signature");
|
||||
addEndpointToGroup("Security", "add-stamp");
|
||||
addEndpointToGroup("Security", "unlock-pdf-forms");
|
||||
// Backend-only endpoints (not in frontend tool registry endpoints)
|
||||
addEndpointToGroup("Security", "redact");
|
||||
addEndpointToGroup("Security", "validate-signature");
|
||||
addEndpointToGroup("Security", "verify-pdf");
|
||||
addEndpointToGroup("Security", "stamp");
|
||||
addEndpointToGroup("Security", "sign");
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "extract-images");
|
||||
addEndpointToGroup("Other", "update-metadata");
|
||||
addEndpointToGroup("Other", "change-metadata");
|
||||
addEndpointToGroup("Other", "flatten");
|
||||
addEndpointToGroup("Other", "unlock-pdf-forms");
|
||||
addEndpointToGroup("Other", REMOVE_BLANKS);
|
||||
addEndpointToGroup("Other", "remove-annotations");
|
||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||
addEndpointToGroup("Other", "add-attachments");
|
||||
addEndpointToGroup("Other", "replace-invert-pdf");
|
||||
addEndpointToGroup("Other", "edit-table-of-contents");
|
||||
addEndpointToGroup("Other", "text-editor-pdf");
|
||||
// Backend-only endpoints (not in frontend tool registry endpoints)
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
addEndpointToGroup("Other", "add-page-numbers");
|
||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||
addEndpointToGroup("Other", "remove-image-pdf");
|
||||
addEndpointToGroup("Other", "add-attachments");
|
||||
addEndpointToGroup("Other", "view-pdf");
|
||||
addEndpointToGroup("Other", "replace-and-invert-color-pdf");
|
||||
addEndpointToGroup("Other", "multi-tool");
|
||||
|
||||
// Adding form-related endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "fields");
|
||||
addEndpointToGroup("Other", "modify-fields");
|
||||
addEndpointToGroup("Other", "delete-fields");
|
||||
addEndpointToGroup("Other", "fill");
|
||||
|
||||
// Adding endpoints to "Advance" group
|
||||
addEndpointToGroup("Advance", "adjust-contrast");
|
||||
addEndpointToGroup("Advance", "compress-pdf");
|
||||
addEndpointToGroup("Advance", "extract-image-scans");
|
||||
addEndpointToGroup("Advance", "repair");
|
||||
addEndpointToGroup("Advance", "auto-rename");
|
||||
addEndpointToGroup("Advance", "pipeline");
|
||||
addEndpointToGroup("Advance", "scanner-effect");
|
||||
addEndpointToGroup("Advance", "auto-split-pdf");
|
||||
addEndpointToGroup("Advance", "show-javascript");
|
||||
addEndpointToGroup("Advance", "split-by-size-or-count");
|
||||
addEndpointToGroup("Advance", "overlay-pdf");
|
||||
// Backend-only endpoints
|
||||
addEndpointToGroup("Advance", "adjust-contrast");
|
||||
|
||||
// Adding endpoints to "Automation" group
|
||||
addEndpointToGroup("Automation", "handleData");
|
||||
addEndpointToGroup("Automation", "automate"); // Alias for handleData (user-friendly name)
|
||||
addEndpointToGroup("Automation", "pipeline");
|
||||
|
||||
// Adding endpoints to "DeveloperTools" group
|
||||
addEndpointToGroup("DeveloperTools", "show-javascript");
|
||||
|
||||
// Adding endpoints to "DeveloperDocs" group (fake endpoints for link-only tools)
|
||||
addEndpointToGroup("DeveloperDocs", "dev-api-docs");
|
||||
addEndpointToGroup("DeveloperDocs", "dev-folder-scanning-docs");
|
||||
addEndpointToGroup("DeveloperDocs", "dev-sso-guide-docs");
|
||||
addEndpointToGroup("DeveloperDocs", "dev-airgapped-docs");
|
||||
addEndpointToGroup("Advance", "split-pdf-by-sections");
|
||||
addEndpointToGroup("Advance", "edit-table-of-contents");
|
||||
addEndpointToGroup("Advance", "split-pdf-by-chapters");
|
||||
|
||||
// CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
@@ -481,8 +436,8 @@ public class EndpointConfiguration {
|
||||
// Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
addEndpointToGroup("Java", "remove-pages");
|
||||
addEndpointToGroup("Java", "split-pages");
|
||||
addEndpointToGroup("Java", "rearrange-pages");
|
||||
addEndpointToGroup("Java", "split-pdfs");
|
||||
addEndpointToGroup("Java", "pdf-organizer");
|
||||
addEndpointToGroup("Java", "rotate-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-img");
|
||||
addEndpointToGroup("Java", "img-to-pdf");
|
||||
@@ -490,10 +445,9 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "remove-password");
|
||||
addEndpointToGroup("Java", "change-permissions");
|
||||
addEndpointToGroup("Java", "add-watermark");
|
||||
addEndpointToGroup("Java", "add-stamp");
|
||||
addEndpointToGroup("Java", "add-image");
|
||||
addEndpointToGroup("Java", "extract-images");
|
||||
addEndpointToGroup("Java", "update-metadata");
|
||||
addEndpointToGroup("Java", "change-metadata");
|
||||
addEndpointToGroup("Java", "cert-sign");
|
||||
addEndpointToGroup("Java", "remove-cert-sign");
|
||||
addEndpointToGroup("Java", "multi-page-layout");
|
||||
@@ -503,9 +457,9 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "auto-rename");
|
||||
addEndpointToGroup("Java", "auto-split-pdf");
|
||||
addEndpointToGroup("Java", "sanitize-pdf");
|
||||
addEndpointToGroup("Java", "timestamp-pdf");
|
||||
addEndpointToGroup("Java", "crop");
|
||||
addEndpointToGroup("Java", "get-info-on-pdf");
|
||||
addEndpointToGroup("Java", "extract-page");
|
||||
addEndpointToGroup("Java", "pdf-to-single-page");
|
||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||
addEndpointToGroup("Java", "show-javascript");
|
||||
@@ -515,10 +469,9 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||
addEndpointToGroup("Java", "overlay-pdf");
|
||||
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||
addEndpointToGroup("Java", "split-pdf-by-chapters");
|
||||
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||
addEndpointToGroup("Java", "remove-annotations");
|
||||
addEndpointToGroup("Java", "pdf-to-text");
|
||||
addEndpointToGroup("Java", "remove-image-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||
addEndpointToGroup("Java", "add-attachments");
|
||||
addEndpointToGroup("Java", "compress-pdf");
|
||||
@@ -526,24 +479,15 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "pdf-to-cbz");
|
||||
addEndpointToGroup("Java", "pdf-to-json");
|
||||
addEndpointToGroup("Java", "json-to-pdf");
|
||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||
addEndpointToGroup("Java", "pdf-to-video");
|
||||
addEndpointToGroup("Java", "verify-pdf");
|
||||
addEndpointToGroup("Java", "flatten");
|
||||
addEndpointToGroup("Java", "unlock-pdf-forms");
|
||||
addEndpointToGroup("Java", "validate-signature");
|
||||
addEndpointToGroup("Java", "text-editor-pdf");
|
||||
addEndpointToGroup("Java", "edit-table-of-contents");
|
||||
addEndpointToGroup("Java", "pdf-to-epub");
|
||||
addEndpointToGroup("Java", "eml-to-pdf");
|
||||
addEndpointToGroup("Java", "handleData");
|
||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||
|
||||
// Javascript
|
||||
addEndpointToGroup("Javascript", "rearrange-pages");
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
addEndpointToGroup("Javascript", "sign");
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
addEndpointToGroup("Javascript", "text-editor-pdf");
|
||||
|
||||
/* qpdf */
|
||||
addEndpointToGroup("qpdf", "repair");
|
||||
@@ -554,7 +498,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
addEndpointToGroup("Ghostscript", "crop");
|
||||
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
|
||||
addEndpointToGroup("Ghostscript", "scanner-effect");
|
||||
addEndpointToGroup("Ghostscript", "pdf-to-vector");
|
||||
addEndpointToGroup("Ghostscript", "vector-to-pdf");
|
||||
|
||||
@@ -573,8 +516,6 @@ public class EndpointConfiguration {
|
||||
addEndpointAlternative("compress-pdf", "qpdf");
|
||||
addEndpointAlternative("compress-pdf", "Ghostscript");
|
||||
addEndpointAlternative("compress-pdf", "Java");
|
||||
addEndpointAlternative("crop", "Ghostscript");
|
||||
addEndpointAlternative("crop", "Java");
|
||||
addEndpointAlternative("ocr-pdf", "tesseract");
|
||||
addEndpointAlternative("ocr-pdf", "OCRmyPDF");
|
||||
|
||||
@@ -603,9 +544,6 @@ public class EndpointConfiguration {
|
||||
// Pdftohtml dependent endpoints
|
||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
||||
|
||||
// Calibre dependent endpoints
|
||||
addEndpointToGroup("Calibre", "pdf-to-epub");
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
@@ -638,12 +576,6 @@ public class EndpointConfiguration {
|
||||
return endpointGroups.getOrDefault(group, new HashSet<>());
|
||||
}
|
||||
|
||||
public Set<String> getAllEndpoints() {
|
||||
return endpointGroups.values().stream()
|
||||
.flatMap(Set::stream)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean isToolGroup(String group) {
|
||||
return "qpdf".equals(group)
|
||||
|| "OCRmyPDF".equals(group)
|
||||
@@ -677,9 +609,8 @@ public class EndpointConfiguration {
|
||||
}
|
||||
|
||||
// Check if endpoint belongs to any disabled functional group
|
||||
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
|
||||
String group = entry.getKey();
|
||||
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
|
||||
for (String group : endpointGroups.keySet()) {
|
||||
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
|
||||
if (!isToolGroup(group)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
package stirling.software.SPDF.pdf.parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Detects whether a page is one- or two-column from per-line bounding boxes, and classifies an
|
||||
* X-span into the column it belongs to. Detection is a midpoint vote at {@code pageWidth / 2}.
|
||||
*
|
||||
* <p>Capped at two columns by design — sufficient for the redaction target set (single-column
|
||||
* documents and IEEE-style two-column papers). 3+ column layouts (newspapers, magazines) and
|
||||
* off-centre gutters (asymmetric two-column) would need a histogram or clustering approach to
|
||||
* detect the actual gutter X. (future work)
|
||||
*
|
||||
* <p>Coordinates are PDFTextStripper screen space (top-left origin, Y increases downward).
|
||||
*/
|
||||
public final class PageColumnLayout {
|
||||
|
||||
/**
|
||||
* Slack when checking "crosses a gutter" so single-pixel overshoots don't mark a line as
|
||||
* spanning.
|
||||
*/
|
||||
public static final float SPAN_SLACK_PT = 2f;
|
||||
|
||||
/**
|
||||
* Slack on each side of the page midpoint inside which a line is considered "spanning"
|
||||
* (covering both columns) rather than belonging to one side.
|
||||
*/
|
||||
private static final float MIDPOINT_SLACK_PT = 30f;
|
||||
|
||||
/**
|
||||
* Minimum line width (points) for a line to count toward the two-column tally. Avoids false
|
||||
* positives where right-aligned dates, page numbers, or short "Link" fragments next to a
|
||||
* heading look like a second column when they're really just inline metadata.
|
||||
*/
|
||||
private static final float MIN_COLUMN_LINE_WIDTH_PT = 100f;
|
||||
|
||||
/**
|
||||
* Minimum number of clearly leftish AND clearly rightish lines (each of width ≥ {@link
|
||||
* #MIN_COLUMN_LINE_WIDTH_PT}) required to call the page two-column. Anything below this falls
|
||||
* back to single-column.
|
||||
*/
|
||||
private static final int MIN_SIDE_LINES = 3;
|
||||
|
||||
private final List<float[]> columns;
|
||||
private final List<float[]> gutters;
|
||||
|
||||
private PageColumnLayout(List<float[]> columns, List<float[]> gutters) {
|
||||
this.columns = columns;
|
||||
this.gutters = gutters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines column layout from per-line bounding boxes ({@code [x1, _, x2, _]}). Counts lines
|
||||
* whose X-midpoint sits clearly left of, or clearly right of, the page midpoint (with {@link
|
||||
* #MIDPOINT_SLACK_PT} slack each side). If both sides have at least {@link #MIN_SIDE_LINES}
|
||||
* lines, the page is treated as two-column with the gutter at the page midpoint. Otherwise it's
|
||||
* single-column.
|
||||
*
|
||||
* <p>Cross-column lines must already be split: callers should feed boxes from a line extractor
|
||||
* that splits same-Y glyphs at large X gaps (see {@code AllTextLineExtractor}). Without that
|
||||
* split, IEEE-style aligned-baseline 2-column PDFs produce one wide merged box per row and the
|
||||
* side tallies all end up classified as "spanning", falling to single-column.
|
||||
*/
|
||||
public static PageColumnLayout fromLineBoxes(List<float[]> lineBoxes, float pageWidth) {
|
||||
if (lineBoxes == null || lineBoxes.isEmpty()) {
|
||||
return new PageColumnLayout(List.of(new float[] {0f, pageWidth}), List.of());
|
||||
}
|
||||
float pageMid = pageWidth / 2f;
|
||||
int left = 0, right = 0;
|
||||
for (float[] lb : lineBoxes) {
|
||||
if (lb == null || lb.length < 3) continue;
|
||||
float width = lb[2] - lb[0];
|
||||
// Skip narrow lines — dates, page numbers, "Link" labels next to a heading should
|
||||
// not, on their own, make a single-column doc look two-column.
|
||||
if (width < MIN_COLUMN_LINE_WIDTH_PT) continue;
|
||||
float mid = (lb[0] + lb[2]) * 0.5f;
|
||||
if (mid < pageMid - MIDPOINT_SLACK_PT) left++;
|
||||
else if (mid > pageMid + MIDPOINT_SLACK_PT) right++;
|
||||
}
|
||||
if (left < MIN_SIDE_LINES || right < MIN_SIDE_LINES) {
|
||||
return new PageColumnLayout(List.of(new float[] {0f, pageWidth}), List.of());
|
||||
}
|
||||
float gutterL = pageMid - MIDPOINT_SLACK_PT;
|
||||
float gutterR = pageMid + MIDPOINT_SLACK_PT;
|
||||
return new PageColumnLayout(
|
||||
List.of(new float[] {0f, gutterL}, new float[] {gutterR, pageWidth}),
|
||||
List.of(new float[] {gutterL, gutterR}));
|
||||
}
|
||||
|
||||
/** All columns, left-to-right, as {@code [leftX, rightX]} pairs. Never empty. */
|
||||
public List<float[]> columns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
/** Gutters between columns, left-to-right, as {@code [leftX, rightX]} pairs. */
|
||||
public List<float[]> gutters() {
|
||||
return gutters;
|
||||
}
|
||||
|
||||
public int columnCount() {
|
||||
return columns.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the column index containing the X-midpoint of {@code [x1, x2]}, falling back to the
|
||||
* closest column if the midpoint sits inside a gutter.
|
||||
*/
|
||||
public int columnOf(float x1, float x2) {
|
||||
float mid = (x1 + x2) * 0.5f;
|
||||
int best = 0;
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
float[] c = columns.get(i);
|
||||
if (mid >= c[0] && mid <= c[1]) return i;
|
||||
float dist = mid < c[0] ? c[0] - mid : mid - c[1];
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns every column index whose X-range overlaps {@code [x1, x2]} with at least {@link
|
||||
* #SPAN_SLACK_PT} of intrusion. A normal in-column line returns one index; a line crossing a
|
||||
* gutter returns two or more.
|
||||
*/
|
||||
public int[] columnsCrossing(float x1, float x2) {
|
||||
List<Integer> hits = new ArrayList<>();
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
float[] c = columns.get(i);
|
||||
float overlap = Math.min(x2, c[1]) - Math.max(x1, c[0]);
|
||||
if (overlap > SPAN_SLACK_PT) hits.add(i);
|
||||
}
|
||||
if (hits.isEmpty()) hits.add(columnOf(x1, x2));
|
||||
int[] out = new int[hits.size()];
|
||||
for (int i = 0; i < hits.size(); i++) out[i] = hits.get(i);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package stirling.software.SPDF.pdf.parser;
|
||||
|
||||
import java.awt.geom.Point2D;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
|
||||
/**
|
||||
* PDFGraphicsStreamEngine that intercepts {@code drawImage} calls and records each image's bounding
|
||||
* box in PDF user-space (origin bottom-left, Y up) by transforming the unit square through the
|
||||
* current transformation matrix (CTM).
|
||||
*
|
||||
* <p>Usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* PageImageLocator locator = new PageImageLocator(page, pageIndex);
|
||||
* locator.processPage(page);
|
||||
* List<ImageBox> boxes = locator.getImageBoxes();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Each {@link ImageBox} carries the 0-based page index and the axis-aligned bounding box {@code
|
||||
* (x1, y1, x2, y2)} in PDF user-space coordinates.
|
||||
*/
|
||||
public final class PageImageLocator extends PDFGraphicsStreamEngine {
|
||||
|
||||
/**
|
||||
* Bounding box of a raster or vector image found on a PDF page.
|
||||
*
|
||||
* @param pageIndex 0-based page index
|
||||
* @param x1 left edge in PDF user-space (origin bottom-left)
|
||||
* @param y1 bottom edge in PDF user-space
|
||||
* @param x2 right edge
|
||||
* @param y2 top edge
|
||||
*/
|
||||
public record ImageBox(int pageIndex, float x1, float y1, float x2, float y2) {}
|
||||
|
||||
private final int pageIndex;
|
||||
private final List<ImageBox> imageBoxes = new ArrayList<>();
|
||||
private final Point2D.Float currentPoint = new Point2D.Float();
|
||||
|
||||
/**
|
||||
* @param page the PDPage to process
|
||||
* @param pageIndex 0-based index of this page in the document (stored on each returned {@link
|
||||
* ImageBox})
|
||||
*/
|
||||
public PageImageLocator(PDPage page, int pageIndex) {
|
||||
super(page);
|
||||
this.pageIndex = pageIndex;
|
||||
}
|
||||
|
||||
/** Returns all image bounding boxes collected during {@link #processPage}. */
|
||||
public List<ImageBox> getImageBoxes() {
|
||||
return imageBoxes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(PDImage pdImage) throws IOException {
|
||||
Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
|
||||
// An image occupies the unit square (0,0)→(1,1) in image space.
|
||||
// Transform all four corners through the CTM to get the page-space bounding box.
|
||||
float a = ctm.getScaleX();
|
||||
float b = ctm.getShearY();
|
||||
float c = ctm.getShearX();
|
||||
float d = ctm.getScaleY();
|
||||
float e = ctm.getTranslateX();
|
||||
float f = ctm.getTranslateY();
|
||||
float[] xs = {e, a + e, c + e, a + c + e};
|
||||
float[] ys = {f, b + f, d + f, b + d + f};
|
||||
float x1 = Float.MAX_VALUE, y1 = Float.MAX_VALUE;
|
||||
float x2 = -Float.MAX_VALUE, y2 = -Float.MAX_VALUE;
|
||||
for (float x : xs) {
|
||||
x1 = Math.min(x1, x);
|
||||
x2 = Math.max(x2, x);
|
||||
}
|
||||
for (float y : ys) {
|
||||
y1 = Math.min(y1, y);
|
||||
y2 = Math.max(y2, y);
|
||||
}
|
||||
imageBoxes.add(new ImageBox(pageIndex, x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
// ---------- required abstract methods (no-op for path operations) ----------
|
||||
|
||||
@Override
|
||||
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) {}
|
||||
|
||||
@Override
|
||||
public void clip(int windingRule) {}
|
||||
|
||||
@Override
|
||||
public void moveTo(float x, float y) {
|
||||
currentPoint.setLocation(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lineTo(float x, float y) {
|
||||
currentPoint.setLocation(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) {
|
||||
currentPoint.setLocation(x3, y3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getCurrentPoint() {
|
||||
return currentPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closePath() {}
|
||||
|
||||
@Override
|
||||
public void endPath() {}
|
||||
|
||||
@Override
|
||||
public void strokePath() {}
|
||||
|
||||
@Override
|
||||
public void fillPath(int windingRule) {}
|
||||
|
||||
@Override
|
||||
public void fillAndStrokePath(int windingRule) {}
|
||||
|
||||
@Override
|
||||
public void shadingFill(COSName shadingName) {}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package stirling.software.SPDF.pdf.parser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
|
||||
/**
|
||||
* All PDF parser model types and the table-parser contract in one place.
|
||||
*
|
||||
* <p>Import as {@code import static stirling.software.SPDF.pdf.parser.PdfModels.*;} to use all
|
||||
* nested types without qualification.
|
||||
*/
|
||||
public final class PdfModels {
|
||||
|
||||
private PdfModels() {}
|
||||
|
||||
// ── Geometry ──────────────────────────────────────────────────────────────
|
||||
|
||||
public record Bounds(float x, float y, float width, float height) {
|
||||
|
||||
public float right() {
|
||||
return x + width;
|
||||
}
|
||||
|
||||
public float bottom() {
|
||||
return y + height;
|
||||
}
|
||||
|
||||
public static Bounds merge(Bounds a, Bounds b) {
|
||||
float x = Math.min(a.x, b.x);
|
||||
float y = Math.min(a.y, b.y);
|
||||
return new Bounds(
|
||||
x, y, Math.max(a.right(), b.right()) - x, Math.max(a.bottom(), b.bottom()) - y);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text fragments and lines ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A contiguous run of text from a single PDF content-stream string operation. {@code baseline}
|
||||
* is preserved separately from bounds for line-grouping — characters of different sizes on the
|
||||
* same visual line share a baseline but differ in top Y.
|
||||
*/
|
||||
public record TextFragment(
|
||||
String fragmentId,
|
||||
String text,
|
||||
Bounds bounds,
|
||||
float baseline,
|
||||
float fontSize,
|
||||
String fontName,
|
||||
boolean bold) {}
|
||||
|
||||
public record RawLine(
|
||||
String lineId, List<TextFragment> fragments, Bounds bounds, int pageNumber) {
|
||||
|
||||
public String text() {
|
||||
if (fragments.isEmpty()) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
TextFragment prev = null;
|
||||
for (TextFragment f : fragments) {
|
||||
if (prev != null) {
|
||||
float gap = f.bounds().x() - prev.bounds().right();
|
||||
float avgCharWidth = prev.bounds().width() / Math.max(prev.text().length(), 1);
|
||||
if (gap > avgCharWidth * 0.5f) sb.append(' ');
|
||||
}
|
||||
sb.append(f.text());
|
||||
prev = f;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public float dominantFontSize() {
|
||||
return fragments.stream()
|
||||
.collect(Collectors.groupingBy(TextFragment::fontSize, Collectors.counting()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(Map.Entry::getKey)
|
||||
.orElse(0f);
|
||||
}
|
||||
|
||||
public boolean hasBold() {
|
||||
return fragments.stream().anyMatch(TextFragment::bold);
|
||||
}
|
||||
}
|
||||
|
||||
public record RawPage(int pageNumber, float widthPt, float heightPt, List<RawLine> lines) {}
|
||||
|
||||
// ── Table model ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A single cell within a {@link TableRow}. {@code colSpan} and {@code rowSpan} are always 1 in
|
||||
* v1 — span detection is deferred but the fields keep the schema stable.
|
||||
*/
|
||||
public record TableCell(int colIndex, String text, Bounds bounds, int colSpan, int rowSpan) {
|
||||
|
||||
public static TableCell of(int colIndex, String text, Bounds bounds) {
|
||||
return new TableCell(colIndex, text, bounds, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public record TableRow(int rowIndex, List<TableCell> cells) {}
|
||||
|
||||
/**
|
||||
* A table as it appears on a single page. {@code headers} is empty in v1 — all rows are in
|
||||
* {@code rows}. {@code rawRows} preserves exact Tabula text output for diagnostics. {@code
|
||||
* confidence} is a heuristic score in [0.0, 1.0]. {@code continuedFromPage} is null in v1.
|
||||
*/
|
||||
public record TableFragment(
|
||||
String tableId,
|
||||
int pageNumber,
|
||||
Bounds bounds,
|
||||
List<TableRow> headers,
|
||||
List<TableRow> rows,
|
||||
List<List<String>> rawRows,
|
||||
int columnCount,
|
||||
float confidence,
|
||||
List<String> warnings,
|
||||
Integer continuedFromPage) {}
|
||||
|
||||
// ── Page output ───────────────────────────────────────────────────────────
|
||||
|
||||
public record ParsedPage(
|
||||
int pageNumber,
|
||||
float widthPt,
|
||||
float heightPt,
|
||||
List<TableFragment> tables,
|
||||
List<RawLine> layoutLines) {}
|
||||
|
||||
// ── Parser contract ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extracts tables from a single page of a PDF. The caller owns the document lifecycle;
|
||||
* implementations must not close it.
|
||||
*/
|
||||
public interface TableParser {
|
||||
|
||||
/**
|
||||
* @param document open PDF; must not be closed by the implementation
|
||||
* @param rawPage page metadata and lines for the page to process
|
||||
* @return zero or more table fragments found on the page, never null
|
||||
*/
|
||||
List<TableFragment> parse(PDDocument document, RawPage rawPage) throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package stirling.software.SPDF.pdf.parser;
|
||||
|
||||
import static stirling.software.SPDF.pdf.parser.PdfModels.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import technology.tabula.ObjectExtractor;
|
||||
import technology.tabula.Page;
|
||||
import technology.tabula.RectangularTextContainer;
|
||||
import technology.tabula.Table;
|
||||
import technology.tabula.extractors.BasicExtractionAlgorithm;
|
||||
import technology.tabula.extractors.ExtractionAlgorithm;
|
||||
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||
|
||||
/**
|
||||
* Primary {@link TableParser} implementation using Tabula's lattice-mode extraction.
|
||||
*
|
||||
* <h3>Algorithm</h3>
|
||||
*
|
||||
* Uses {@link SpreadsheetExtractionAlgorithm} (lattice mode), which detects tables from ruled lines
|
||||
* (horizontal and vertical PDF path operators). This is reliable for tables with visible borders.
|
||||
* Borderless or whitespace-delimited tables will not be detected — that requires stream mode, which
|
||||
* is deferred to a later iteration.
|
||||
*
|
||||
* <h3>Coordinate system</h3>
|
||||
*
|
||||
* Tabula normalises page coordinates so that (0,0) is the top-left corner and Y increases downward,
|
||||
* matching the {@link Bounds} convention used throughout this parser. No coordinate transformation
|
||||
* is needed when mapping Tabula cell bounds to {@link Bounds}.
|
||||
*
|
||||
* <h3>Known limitations</h3>
|
||||
*
|
||||
* <ul>
|
||||
* <li>Borderless tables are not detected (use stream mode, deferred).
|
||||
* <li>Colspan and rowspan are not detected; all cells report colSpan=1, rowSpan=1.
|
||||
* <li>Header rows are not identified; all rows appear in {@code rows}, headers is empty.
|
||||
* <li>Cross-page table linking is not performed; each page is independent.
|
||||
* <li>Rotated tables (90°/270° pages) may produce incorrect bounds.
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class TabulaTableParser implements TableParser {
|
||||
|
||||
/** Lattice mode — reliable for tables with visible ruled borders. */
|
||||
@Override
|
||||
public List<TableFragment> parse(PDDocument document, RawPage rawPage) throws IOException {
|
||||
return parseWithAlgorithm(
|
||||
document, rawPage, new SpreadsheetExtractionAlgorithm(), "lattice");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience overload for callers that only have a page number, not a full {@link RawPage}.
|
||||
*/
|
||||
public List<TableFragment> parse(PDDocument document, int pageNumber) throws IOException {
|
||||
return parse(document, new RawPage(pageNumber, 0f, 0f, List.of()));
|
||||
}
|
||||
|
||||
/** Stream mode — whitespace-based column detection for borderless tables. */
|
||||
public List<TableFragment> parseStream(PDDocument document, RawPage rawPage)
|
||||
throws IOException {
|
||||
return parseWithAlgorithm(document, rawPage, new BasicExtractionAlgorithm(), "stream");
|
||||
}
|
||||
|
||||
private List<TableFragment> parseWithAlgorithm(
|
||||
PDDocument document, RawPage rawPage, ExtractionAlgorithm algorithm, String modeName)
|
||||
throws IOException {
|
||||
int pageNumber = rawPage.pageNumber();
|
||||
|
||||
List<Table> tabulaTables;
|
||||
try {
|
||||
// Do NOT use try-with-resources: ObjectExtractor.close() closes the underlying
|
||||
// PDDocument, which we don't own. The extractor holds no resources of its own.
|
||||
ObjectExtractor extractor = new ObjectExtractor(document);
|
||||
Page page = extractor.extract(pageNumber);
|
||||
tabulaTables = new ArrayList<>(algorithm.extract(page));
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Tabula {} extraction failed on page {}: {}",
|
||||
modeName,
|
||||
pageNumber,
|
||||
e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (tabulaTables.isEmpty()) {
|
||||
log.debug("Page {}: no tables detected by Tabula ({})", pageNumber, modeName);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Page {}: Tabula ({}) detected {} table(s)",
|
||||
pageNumber,
|
||||
modeName,
|
||||
tabulaTables.size());
|
||||
|
||||
List<TableFragment> fragments = new ArrayList<>(tabulaTables.size());
|
||||
for (int i = 0; i < tabulaTables.size(); i++) {
|
||||
fragments.add(toFragment(tabulaTables.get(i), pageNumber, i));
|
||||
}
|
||||
return fragments;
|
||||
}
|
||||
|
||||
// ── private helpers ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private TableFragment toFragment(Table table, int pageNumber, int tableIndex) {
|
||||
List<List<RectangularTextContainer>> tabulaRows = table.getRows();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
|
||||
List<List<String>> rawRows = buildRawRows(tabulaRows);
|
||||
int colCount = inferColumnCount(rawRows, warnings);
|
||||
List<TableRow> rows = buildRows(tabulaRows, colCount, warnings);
|
||||
float confidence = computeConfidence(rawRows, colCount, warnings);
|
||||
Bounds bounds = tableBounds(table);
|
||||
|
||||
String tableId = "tbl-p" + pageNumber + "-" + tableIndex;
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
log.warn("Page {}, table {}: {}", pageNumber, tableIndex, warnings);
|
||||
}
|
||||
|
||||
return new TableFragment(
|
||||
tableId,
|
||||
pageNumber,
|
||||
bounds,
|
||||
List.of(), // headers: deferred to v2
|
||||
rows,
|
||||
rawRows,
|
||||
colCount,
|
||||
confidence,
|
||||
Collections.unmodifiableList(warnings),
|
||||
null); // continuedFromPage: deferred to v2
|
||||
}
|
||||
|
||||
private List<List<String>> buildRawRows(List<List<RectangularTextContainer>> tabulaRows) {
|
||||
List<List<String>> rawRows = new ArrayList<>(tabulaRows.size());
|
||||
for (List<RectangularTextContainer> tabulaRow : tabulaRows) {
|
||||
List<String> cells = new ArrayList<>(tabulaRow.size());
|
||||
for (RectangularTextContainer cell : tabulaRow) {
|
||||
cells.add(normaliseText(cell.getText()));
|
||||
}
|
||||
rawRows.add(Collections.unmodifiableList(cells));
|
||||
}
|
||||
return rawRows;
|
||||
}
|
||||
|
||||
private List<TableRow> buildRows(
|
||||
List<List<RectangularTextContainer>> tabulaRows, int colCount, List<String> warnings) {
|
||||
List<TableRow> rows = new ArrayList<>(tabulaRows.size());
|
||||
for (int rowIdx = 0; rowIdx < tabulaRows.size(); rowIdx++) {
|
||||
List<RectangularTextContainer> tabulaRow = tabulaRows.get(rowIdx);
|
||||
List<TableCell> cells = new ArrayList<>(tabulaRow.size());
|
||||
|
||||
for (int colIdx = 0; colIdx < tabulaRow.size(); colIdx++) {
|
||||
RectangularTextContainer c = tabulaRow.get(colIdx);
|
||||
Bounds cellBounds =
|
||||
new Bounds(
|
||||
(float) c.getX(),
|
||||
(float) c.getY(),
|
||||
(float) c.getWidth(),
|
||||
(float) c.getHeight());
|
||||
cells.add(TableCell.of(colIdx, normaliseText(c.getText()), cellBounds));
|
||||
}
|
||||
|
||||
if (tabulaRow.size() != colCount) {
|
||||
warnings.add(
|
||||
"Row "
|
||||
+ rowIdx
|
||||
+ " has "
|
||||
+ tabulaRow.size()
|
||||
+ " cells; expected "
|
||||
+ colCount);
|
||||
}
|
||||
|
||||
rows.add(new TableRow(rowIdx, Collections.unmodifiableList(cells)));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical column count for a table is the size of the widest row. Tabula can produce
|
||||
* uneven rows when a cell's ruling lines are partially missing.
|
||||
*/
|
||||
private int inferColumnCount(List<List<String>> rawRows, List<String> warnings) {
|
||||
if (rawRows.isEmpty()) return 0;
|
||||
int max = rawRows.stream().mapToInt(List::size).max().orElse(0);
|
||||
int mode =
|
||||
rawRows.stream()
|
||||
.collect(
|
||||
java.util.stream.Collectors.groupingBy(
|
||||
List::size, java.util.stream.Collectors.counting()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.max(java.util.Map.Entry.comparingByValue())
|
||||
.map(java.util.Map.Entry::getKey)
|
||||
.orElse(0);
|
||||
if (max != mode) {
|
||||
warnings.add("Inconsistent column count: modal=" + mode + " max=" + max);
|
||||
}
|
||||
return mode > 0 ? mode : max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic confidence score in [0.0, 1.0].
|
||||
*
|
||||
* <ul>
|
||||
* <li>Starts at 1.0.
|
||||
* <li>-0.3 if only one column (single-column tables are usually not real tables).
|
||||
* <li>-0.1 per row with an inconsistent column count, capped at -0.4.
|
||||
* <li>-0.3 if the empty-cell ratio across all cells exceeds 80%.
|
||||
* </ul>
|
||||
*/
|
||||
private float computeConfidence(
|
||||
List<List<String>> rawRows, int colCount, List<String> warnings) {
|
||||
if (rawRows.isEmpty() || colCount == 0) return 0f;
|
||||
|
||||
float score = 1.0f;
|
||||
|
||||
if (colCount == 1) score -= 0.3f;
|
||||
|
||||
long inconsistentRows = rawRows.stream().filter(r -> r.size() != colCount).count();
|
||||
score -= Math.min(inconsistentRows * 0.1f, 0.4f);
|
||||
|
||||
long totalCells = rawRows.stream().mapToLong(List::size).sum();
|
||||
long emptyCells =
|
||||
rawRows.stream().flatMap(Collection::stream).filter(String::isBlank).count();
|
||||
if (totalCells > 0 && (float) emptyCells / totalCells > 0.8f) {
|
||||
score -= 0.3f;
|
||||
}
|
||||
|
||||
return Math.max(0f, Math.min(1f, score));
|
||||
}
|
||||
|
||||
private Bounds tableBounds(Table table) {
|
||||
return new Bounds(
|
||||
(float) table.getX(),
|
||||
(float) table.getY(),
|
||||
(float) table.getWidth(),
|
||||
(float) table.getHeight());
|
||||
}
|
||||
|
||||
private String normaliseText(String raw) {
|
||||
if (raw == null) return "";
|
||||
// Tabula wraps multi-line cell content with \r\n — collapse to a single space.
|
||||
return raw.replace("\r\n", " ").replace("\n", " ").replace("\r", " ").trim();
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -75,12 +75,8 @@ public @interface AutoJobPostMapping {
|
||||
boolean queueable() default false;
|
||||
|
||||
/**
|
||||
* Relative resource weight (1-100). See {@link
|
||||
* stirling.software.common.enumeration.ResourceWeight} for the standard tiers.
|
||||
*
|
||||
* <p>The default is a sentinel ({@link Integer#MIN_VALUE}); {@code
|
||||
* AutoJobPostMappingWeightTest} fails the build if any endpoint leaves it unset. Runtime
|
||||
* readers clamp the value into {@code [1, 100]}.
|
||||
* Relative resource weight (1–100) used by the scheduler to prioritise / throttle jobs. Values
|
||||
* below 1 are clamped to 1, values above 100 to 100.
|
||||
*/
|
||||
int resourceWeight() default Integer.MIN_VALUE;
|
||||
int resourceWeight() default 50;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ package stirling.software.common.aop;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -28,7 +26,7 @@ import stirling.software.common.service.JobExecutorService;
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Order(20) // Lower precedence - executes AFTER audit aspects populate MDC
|
||||
@Order(0) // Highest precedence - executes before audit aspects
|
||||
public class AutoJobAspect {
|
||||
|
||||
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
|
||||
@@ -72,29 +70,26 @@ public class AutoJobAspect {
|
||||
// No retries needed, simple execution
|
||||
return jobExecutorService.runJobGeneric(
|
||||
async,
|
||||
wrapWithMDC(
|
||||
() -> {
|
||||
try {
|
||||
// Note: Progress tracking is handled in
|
||||
// TaskManager/JobExecutorService
|
||||
// The trackProgress flag controls whether detailed progress is
|
||||
// stored
|
||||
// for REST API queries, not WebSocket notifications
|
||||
return joinPoint.proceed(args);
|
||||
} catch (Throwable ex) {
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution: {}",
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
// Rethrow RuntimeException as-is to preserve exception type
|
||||
if (ex instanceof RuntimeException) {
|
||||
throw (RuntimeException) ex;
|
||||
}
|
||||
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||
// BaseAppException
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}),
|
||||
() -> {
|
||||
try {
|
||||
// Note: Progress tracking is handled in TaskManager/JobExecutorService
|
||||
// The trackProgress flag controls whether detailed progress is stored
|
||||
// for REST API queries, not WebSocket notifications
|
||||
return joinPoint.proceed(args);
|
||||
} catch (Throwable ex) {
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution: {}",
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
// Rethrow RuntimeException as-is to preserve exception type
|
||||
if (ex instanceof RuntimeException) {
|
||||
throw (RuntimeException) ex;
|
||||
}
|
||||
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||
// BaseAppException
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
},
|
||||
timeout,
|
||||
queueable,
|
||||
resourceWeight);
|
||||
@@ -128,108 +123,114 @@ public class AutoJobAspect {
|
||||
|
||||
return jobExecutorService.runJobGeneric(
|
||||
async,
|
||||
wrapWithMDC(
|
||||
() -> {
|
||||
// Use iterative approach instead of recursion to avoid stack overflow
|
||||
Throwable lastException = null;
|
||||
() -> {
|
||||
// Use iterative approach instead of recursion to avoid stack overflow
|
||||
Throwable lastException = null;
|
||||
|
||||
// Attempt counter starts at 1 for first try
|
||||
for (int currentAttempt = 1;
|
||||
currentAttempt <= maxRetries;
|
||||
currentAttempt++) {
|
||||
try {
|
||||
if (trackProgress && async) {
|
||||
// Get jobId for progress tracking in TaskManager
|
||||
// This enables REST API progress queries, not WebSocket
|
||||
if (jobIdRef.get() == null) {
|
||||
jobIdRef.set(getJobIdFromContext());
|
||||
}
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
log.debug(
|
||||
"Tracking progress for job {} (attempt {}/{})",
|
||||
jobId,
|
||||
currentAttempt,
|
||||
maxRetries);
|
||||
// Progress is tracked in TaskManager for REST API
|
||||
// access
|
||||
// No WebSocket notifications sent here
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to execute the operation
|
||||
return joinPoint.proceed(args);
|
||||
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution (attempt"
|
||||
+ " {}/{}): {}",
|
||||
// Attempt counter starts at 1 for first try
|
||||
for (int currentAttempt = 1; currentAttempt <= maxRetries; currentAttempt++) {
|
||||
try {
|
||||
if (trackProgress && async) {
|
||||
// Get jobId for progress tracking in TaskManager
|
||||
// This enables REST API progress queries, not WebSocket
|
||||
if (jobIdRef.get() == null) {
|
||||
jobIdRef.set(getJobIdFromContext());
|
||||
}
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
log.debug(
|
||||
"Tracking progress for job {} (attempt {}/{})",
|
||||
jobId,
|
||||
currentAttempt,
|
||||
maxRetries,
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
maxRetries);
|
||||
// Progress is tracked in TaskManager for REST API access
|
||||
// No WebSocket notifications sent here
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should retry
|
||||
if (currentAttempt < maxRetries) {
|
||||
log.info(
|
||||
"Retrying operation, attempt {}/{}",
|
||||
currentAttempt + 1,
|
||||
maxRetries);
|
||||
// Attempt to execute the operation
|
||||
return joinPoint.proceed(args);
|
||||
|
||||
if (trackProgress && async) {
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
log.debug(
|
||||
"Recording retry attempt for job {} in TaskManager",
|
||||
jobId);
|
||||
// Retry info is tracked in TaskManager for REST API
|
||||
// access
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution (attempt"
|
||||
+ " {}/{}): {}",
|
||||
currentAttempt,
|
||||
maxRetries,
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
|
||||
// Use sleep for retry delay
|
||||
// For sync jobs, both sleep and async are blocking at this
|
||||
// point
|
||||
// For async jobs, the delay occurs in the executor thread
|
||||
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
|
||||
// Check if we should retry
|
||||
if (currentAttempt < maxRetries) {
|
||||
log.info(
|
||||
"Retrying operation, attempt {}/{}",
|
||||
currentAttempt + 1,
|
||||
maxRetries);
|
||||
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.debug(
|
||||
"Retry delay interrupted for attempt {}/{}",
|
||||
currentAttempt,
|
||||
maxRetries);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// No more retries, we'll throw the exception after the loop
|
||||
break;
|
||||
if (trackProgress && async) {
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
log.debug(
|
||||
"Recording retry attempt for job {} in TaskManager",
|
||||
jobId);
|
||||
// Retry info is tracked in TaskManager for REST API access
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all retries failed
|
||||
if (lastException != null) {
|
||||
// Rethrow RuntimeException as-is to preserve exception type
|
||||
if (lastException instanceof RuntimeException) {
|
||||
throw (RuntimeException) lastException;
|
||||
// Use non-blocking delay for all retry attempts to avoid blocking
|
||||
// threads
|
||||
// For sync jobs this avoids starving the tomcat thread pool under
|
||||
// load
|
||||
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
|
||||
|
||||
// Execute the retry after a delay through the JobExecutorService
|
||||
// rather than blocking the current thread with sleep
|
||||
CompletableFuture<Object> delayedRetry = new CompletableFuture<>();
|
||||
|
||||
// Use a delayed executor for non-blocking delay
|
||||
CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
|
||||
.execute(
|
||||
() -> {
|
||||
// Continue the retry loop in the next iteration
|
||||
// We can't return from here directly since
|
||||
// we're in a Runnable
|
||||
delayedRetry.complete(null);
|
||||
});
|
||||
|
||||
// Wait for the delay to complete before continuing
|
||||
try {
|
||||
delayedRetry.join();
|
||||
} catch (Exception e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||
// BaseAppException
|
||||
throw new RuntimeException(
|
||||
"Job failed after "
|
||||
+ maxRetries
|
||||
+ " attempts: "
|
||||
+ lastException.getMessage(),
|
||||
lastException);
|
||||
} else {
|
||||
// No more retries, we'll throw the exception after the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should never happen if lastException is properly tracked
|
||||
throw new RuntimeException("Job failed but no exception was recorded");
|
||||
}),
|
||||
// If we get here, all retries failed
|
||||
if (lastException != null) {
|
||||
// Rethrow RuntimeException as-is to preserve exception type
|
||||
if (lastException instanceof RuntimeException) {
|
||||
throw (RuntimeException) lastException;
|
||||
}
|
||||
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||
// BaseAppException
|
||||
throw new RuntimeException(
|
||||
"Job failed after "
|
||||
+ maxRetries
|
||||
+ " attempts: "
|
||||
+ lastException.getMessage(),
|
||||
lastException);
|
||||
}
|
||||
|
||||
// This should never happen if lastException is properly tracked
|
||||
throw new RuntimeException("Job failed but no exception was recorded");
|
||||
},
|
||||
timeout,
|
||||
queueable,
|
||||
resourceWeight);
|
||||
@@ -298,32 +299,4 @@ public class AutoJobAspect {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a supplier to propagate MDC context to background threads. Captures MDC on request
|
||||
* thread and restores it in the background thread. Ensures proper cleanup to prevent context
|
||||
* leakage across jobs in thread pools.
|
||||
*/
|
||||
private <T> Supplier<T> wrapWithMDC(Supplier<T> supplier) {
|
||||
final Map<String, String> captured = MDC.getCopyOfContextMap();
|
||||
return () -> {
|
||||
final Map<String, String> previous = MDC.getCopyOfContextMap();
|
||||
try {
|
||||
// Set the captured context (or clear if none was captured)
|
||||
if (captured != null) {
|
||||
MDC.setContextMap(new HashMap<>(captured));
|
||||
} else {
|
||||
MDC.clear();
|
||||
}
|
||||
return supplier.get();
|
||||
} finally {
|
||||
// Restore previous state (or clear if there was none)
|
||||
if (previous != null) {
|
||||
MDC.setContextMap(previous);
|
||||
} else {
|
||||
MDC.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
/** Health and identity facade for the active cluster backplane. */
|
||||
public interface ClusterBackplane {
|
||||
|
||||
/** Returns {@code true} when the backplane is reachable; used for health endpoints. */
|
||||
boolean isHealthy();
|
||||
|
||||
/** Returns {@code "inprocess"} or {@code "valkey"}. */
|
||||
String backplaneType();
|
||||
|
||||
/** Returns this JVM's stable node id (matches {@code Cluster.resolvedNodeId()}). */
|
||||
String localNodeId();
|
||||
|
||||
/**
|
||||
* Whether this JVM should run the local {@link
|
||||
* stirling.software.common.service.TaskManager#cleanupOldJobs()} loop. Distributed backplanes
|
||||
* own job expiry via their own TTL, so they should override this to return {@code false}.
|
||||
* Defaults to {@code true} so in-process behavior is preserved without an explicit override.
|
||||
*/
|
||||
default boolean shouldRunLocalCleanup() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Cluster;
|
||||
|
||||
/**
|
||||
* Validates that cluster mode is internally consistent.
|
||||
*
|
||||
* <p>Cluster settings are bound on the central {@link ApplicationProperties} under {@code
|
||||
* cluster.*}; this class reads {@link ApplicationProperties#getCluster()} and runs guards in {@link
|
||||
* PostConstruct}. When {@code cluster.enabled=false} (the default) all checks are skipped so a
|
||||
* single-instance install needs no new config.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class ClusterConfig {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@PostConstruct
|
||||
void validate() {
|
||||
Cluster cluster = applicationProperties.getCluster();
|
||||
if (!cluster.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
String backplane = cluster.getBackplane();
|
||||
if ("valkey".equalsIgnoreCase(backplane)) {
|
||||
String url = cluster.getValkey() == null ? null : cluster.getValkey().getUrl();
|
||||
if (url == null || url.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"cluster.enabled=true with backplane=valkey requires"
|
||||
+ " cluster.valkey.url to be set (e.g."
|
||||
+ " redis://valkey:6379).");
|
||||
}
|
||||
} else if ("inprocess".equalsIgnoreCase(backplane)) {
|
||||
// enabled+inprocess only coordinates the local JVM; cross-node lookups will 410.
|
||||
log.warn(
|
||||
"cluster.enabled=true with backplane=inprocess - only the local"
|
||||
+ " JVM is coordinated. Cross-node lookups and the file proxy will fail."
|
||||
+ " Use backplane=valkey for real multi-node deployments.");
|
||||
} else {
|
||||
// Fail fast on typos like "valky" so Spring doesn't later report a cryptic
|
||||
// "no ClusterBackplane bean" - the operator-facing error names the bad value.
|
||||
throw new IllegalStateException(
|
||||
"cluster.enabled=true with unknown backplane '"
|
||||
+ backplane
|
||||
+ "'. Valid values: inprocess | valkey.");
|
||||
}
|
||||
log.info(
|
||||
"Cluster mode enabled (backplane={}, nodeRole={}, nodeId={}).",
|
||||
backplane,
|
||||
cluster.resolvedRole(),
|
||||
cluster.resolvedNodeId());
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Snapshot of a peer node as recorded in the {@link InstanceRegistry}.
|
||||
*
|
||||
* @param internalAddress {@code host:port} the node listens on for {@code /internal/cluster/**}
|
||||
* @param role one of {@code WEB}, {@code WORKER}, {@code BOTH}
|
||||
*/
|
||||
public record ClusterNode(
|
||||
String nodeId, String internalAddress, Instant lastHeartbeat, String role) {}
|
||||
@@ -1,21 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Cluster-wide mutual exclusion primitive; non-reentrant by contract. */
|
||||
public interface DistributedLock {
|
||||
|
||||
Optional<LockHandle> tryAcquire(String lockKey, Duration leaseTime);
|
||||
|
||||
interface LockHandle extends AutoCloseable {
|
||||
void release();
|
||||
|
||||
boolean renew(Duration leaseTime);
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Low-level storage seam for result/job files. */
|
||||
public interface FileStore {
|
||||
|
||||
/** Stored file record. */
|
||||
record Stored(String fileId, long size) {}
|
||||
|
||||
/**
|
||||
* Store the given stream and return a generated file id and total bytes written. {@code owner}
|
||||
* may be null to indicate the file has no associated user (anonymous / desktop / async job with
|
||||
* no propagated security context); a non-null value is persisted alongside the data so {@link
|
||||
* #getOwner(String)} can return it later for authorization checks.
|
||||
*/
|
||||
Stored store(InputStream in, String originalName, String owner) throws IOException;
|
||||
|
||||
/** Store with no owner. Equivalent to {@link #store(InputStream, String, String)} with null. */
|
||||
default Stored store(InputStream in, String originalName) throws IOException {
|
||||
return store(in, originalName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the file at {@code source} and return a generated file id and total bytes written.
|
||||
*
|
||||
* <p>Default implementation opens {@code source} as a stream and delegates to {@link
|
||||
* #store(InputStream, String, String)}. Local-disk implementations should override to use a
|
||||
* direct file-to-file copy ({@code Files.copy(source, dest)} can use {@code sendfile(2)} on
|
||||
* Linux), which avoids the two-memory-copy hit of streaming a disk-backed upload through the
|
||||
* JVM heap.
|
||||
*/
|
||||
default Stored store(Path source, String originalName, String owner) throws IOException {
|
||||
try (InputStream in = Files.newInputStream(source)) {
|
||||
return store(in, originalName, owner);
|
||||
}
|
||||
}
|
||||
|
||||
/** Store with no owner. Equivalent to {@link #store(Path, String, String)} with null. */
|
||||
default Stored store(Path source, String originalName) throws IOException {
|
||||
return store(source, originalName, null);
|
||||
}
|
||||
|
||||
/** Open the stored file for streaming reads. Caller closes. */
|
||||
InputStream retrieve(String fileId) throws IOException;
|
||||
|
||||
/** Load the stored file into a byte array. */
|
||||
byte[] retrieveBytes(String fileId) throws IOException;
|
||||
|
||||
/** Size of the stored file in bytes. */
|
||||
long size(String fileId) throws IOException;
|
||||
|
||||
/** Delete the stored file. Returns true if a file was removed. */
|
||||
boolean delete(String fileId);
|
||||
|
||||
/** Whether the file id exists in the store. */
|
||||
boolean exists(String fileId);
|
||||
|
||||
/**
|
||||
* Returns the owner identifier recorded at store time, or {@code null} if the file does not
|
||||
* exist or was stored without an owner. Implementations must not throw when the file is missing
|
||||
* or when the owner record is absent; they should return null so callers can treat "no owner"
|
||||
* as a non-authoritative case.
|
||||
*/
|
||||
String getOwner(String fileId) throws IOException;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Maps {@code nodeId} to its internal cluster address, with TTL'd heartbeats. */
|
||||
public interface InstanceRegistry {
|
||||
|
||||
/** Register or refresh this node. Idempotent so a wiped backplane self-heals on next tick. */
|
||||
void register(ClusterNode node, Duration heartbeatTtl);
|
||||
|
||||
Optional<ClusterNode> lookup(String nodeId);
|
||||
|
||||
Collection<ClusterNode> activeNodes();
|
||||
|
||||
void deregister(String nodeId);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Cluster-visible storage for job status and result metadata, with TTL'd entries. */
|
||||
public interface JobStore {
|
||||
|
||||
/** Persist or overwrite a job entry. {@code ttl} sets the lifetime of the entry. */
|
||||
void put(JobStoreEntry entry, Duration ttl);
|
||||
|
||||
Optional<JobStoreEntry> get(String jobId);
|
||||
|
||||
void delete(String jobId);
|
||||
|
||||
boolean exists(String jobId);
|
||||
|
||||
/** Reverse lookup: which job owns this result file id? */
|
||||
Optional<String> findJobIdByFileId(String fileId);
|
||||
|
||||
/** Snapshot of every active entry. Used by admin/stats endpoints; may be O(n). */
|
||||
Collection<JobStoreEntry> all();
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Cluster-visible projection of a job's status and result metadata, as persisted in {@link
|
||||
* JobStore}.
|
||||
*
|
||||
* @param owningNodeId the node id that originally executed the job
|
||||
*/
|
||||
public record JobStoreEntry(
|
||||
String jobId,
|
||||
JobState state,
|
||||
String owningNodeId,
|
||||
Instant createdAt,
|
||||
Instant completedAt,
|
||||
String error,
|
||||
List<String> fileIds,
|
||||
Map<String, String> resultMeta) {
|
||||
|
||||
/** Lifecycle states for a job as observed by the cluster. */
|
||||
public enum JobState {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
COMPLETE,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Short-TTL namespaced key/value cache backed by the cluster backplane. */
|
||||
public interface KeyValueCache {
|
||||
|
||||
void put(String namespace, String key, String value, Duration ttl);
|
||||
|
||||
Optional<String> get(String namespace, String key);
|
||||
|
||||
void evict(String namespace, String key);
|
||||
|
||||
void evictNamespace(String namespace);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user