mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Merge branch 'Stirling-Tools:main' into main
AI Engine CI / engine (push) Has been cancelled
License Report Workflow / pick (push) Has been cancelled
Push Docker Image with VersionNumber / push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Sync Files (TOML) / sync-files (push) Has been cancelled
License Report Workflow / detect what files changed (push) Has been cancelled
License Report Workflow / Generate Frontend License Report (push) Has been cancelled
License Report Workflow / Generate Backend License Report (push) Has been cancelled
AI Engine CI / engine (push) Has been cancelled
License Report Workflow / pick (push) Has been cancelled
Push Docker Image with VersionNumber / push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Sync Files (TOML) / sync-files (push) Has been cancelled
License Report Workflow / detect what files changed (push) Has been cancelled
License Report Workflow / Generate Frontend License Report (push) Has been cancelled
License Report Workflow / Generate Backend License Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
||||||
|
{
|
||||||
|
"name": "Stirling-PDF Dev Container",
|
||||||
|
"build": {
|
||||||
|
// Sets the run context to one level up instead of the .devcontainer folder.
|
||||||
|
"context": "..",
|
||||||
|
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
|
||||||
|
"dockerfile": "../Dockerfile.dev"
|
||||||
|
},
|
||||||
|
"runArgs": [
|
||||||
|
"-e",
|
||||||
|
"GIT_EDITOR=code --wait",
|
||||||
|
"--security-opt",
|
||||||
|
"label=disable"
|
||||||
|
],
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [8080, 2002, 2003],
|
||||||
|
"portsAttributes": {
|
||||||
|
"8080": {
|
||||||
|
"label": "Stirling-PDF Dev Port"
|
||||||
|
},
|
||||||
|
"2002": {
|
||||||
|
"label": "unoserver Port"
|
||||||
|
},
|
||||||
|
"2003": {
|
||||||
|
"label": "UnoConvert Port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
||||||
|
"mounts": [
|
||||||
|
"source=logs-volume,target=/workspace/logs,type=volume",
|
||||||
|
"source=build-volume,target=/workspace/build,type=volume"
|
||||||
|
],
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
|
"editor.wordSegmenterLocales": "",
|
||||||
|
"editor.guides.bracketPairs": "active",
|
||||||
|
"editor.guides.bracketPairsHorizontal": "active",
|
||||||
|
"cSpell.enabled": false,
|
||||||
|
"[java]": {
|
||||||
|
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
||||||
|
},
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
|
"java.format.enabled": true,
|
||||||
|
"java.format.settings.profile": "GoogleStyle",
|
||||||
|
"java.format.settings.google.version": "1.28.0",
|
||||||
|
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||||
|
"java.saveActions.cleanup": true,
|
||||||
|
"java.cleanup.actions": [
|
||||||
|
"invertEquals",
|
||||||
|
"instanceofPatternMatch"
|
||||||
|
],
|
||||||
|
"java.completion.engine": "dom",
|
||||||
|
"java.completion.enabled": true,
|
||||||
|
"java.completion.importOrder": [
|
||||||
|
"java",
|
||||||
|
"javax",
|
||||||
|
"org",
|
||||||
|
"com",
|
||||||
|
"net",
|
||||||
|
"io",
|
||||||
|
"jakarta",
|
||||||
|
"lombok",
|
||||||
|
"me",
|
||||||
|
"stirling"
|
||||||
|
],
|
||||||
|
"java.project.resourceFilters": [
|
||||||
|
".devcontainer/",
|
||||||
|
".git/",
|
||||||
|
".github/",
|
||||||
|
".gradle/",
|
||||||
|
".venv/",
|
||||||
|
".venv*/",
|
||||||
|
".vscode/",
|
||||||
|
"bin/",
|
||||||
|
"app/core/bin/",
|
||||||
|
"app/common/bin/",
|
||||||
|
"app/proprietary/bin/",
|
||||||
|
"build/",
|
||||||
|
"app/core/build/",
|
||||||
|
"app/common/build/",
|
||||||
|
"app/proprietary/build/",
|
||||||
|
"configs/",
|
||||||
|
"app/core/configs/",
|
||||||
|
"customFiles/",
|
||||||
|
"app/core/customFiles/",
|
||||||
|
"docs/",
|
||||||
|
"exampleYmlFiles",
|
||||||
|
"gradle/",
|
||||||
|
"images/",
|
||||||
|
"logs/",
|
||||||
|
"pipeline/",
|
||||||
|
"scripts/",
|
||||||
|
"testings/",
|
||||||
|
".git-blame-ignore-revs",
|
||||||
|
".gitattributes",
|
||||||
|
".gitignore",
|
||||||
|
"app/core/.gitignore",
|
||||||
|
"app/common/.gitignore",
|
||||||
|
"app/proprietary/.gitignore",
|
||||||
|
".pre-commit-config.yaml"
|
||||||
|
],
|
||||||
|
"java.signatureHelp.enabled": true,
|
||||||
|
"java.signatureHelp.description.enabled": true,
|
||||||
|
"java.maven.downloadSources": true,
|
||||||
|
"java.import.gradle.enabled": true,
|
||||||
|
"java.eclipse.downloadSources": true,
|
||||||
|
"java.import.gradle.wrapper.enabled": true,
|
||||||
|
"spring.initializr.defaultLanguage": "Java",
|
||||||
|
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
|
||||||
|
"spring.initializr.defaultArtifactId": "SPDF"
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
|
||||||
|
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
|
||||||
|
"ms-python.black-formatter", // Python code formatter using Black
|
||||||
|
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
|
||||||
|
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
|
||||||
|
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||||
|
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||||
|
"streetsidesoftware.code-spell-checker", // Spell checker for code to avoid typos
|
||||||
|
"vmware.vscode-boot-dev-pack", // Developer tools for Spring Boot by VMware
|
||||||
|
"vscjava.vscode-java-pack", // Java Extension Pack with essential Java tools for VS Code
|
||||||
|
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles
|
||||||
|
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
|
||||||
|
"charliermarsh.ruff", // Ruff extension for Ruff language support
|
||||||
|
"github.vscode-github-actions", // GitHub Actions extension for Visual Studio Code
|
||||||
|
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||||
|
"redhat.vscode-yaml" // YAML extension for Visual Studio Code
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
"remoteUser": "devuser",
|
||||||
|
"shutdownAction": "stopContainer",
|
||||||
|
"initializeCommand": "bash ./.devcontainer/git-init.sh",
|
||||||
|
"postStartCommand": "./.devcontainer/init-setup.sh"
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
GIT_USER=$(git config --get user.name)
|
||||||
|
GIT_EMAIL=$(git config --get user.email)
|
||||||
|
|
||||||
|
# Exit if GIT_USER or GIT_EMAIL is empty
|
||||||
|
if [ -z "$GIT_USER" ] || [ -z "$GIT_EMAIL" ]; then
|
||||||
|
echo "GIT_USER or GIT_EMAIL is not set. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --local user.name "$GIT_USER"
|
||||||
|
git config --local user.email "$GIT_EMAIL"
|
||||||
|
|
||||||
|
# This directory should contain custom Git hooks for the repository
|
||||||
|
# Set the path for Git hooks to /workspace/hooks
|
||||||
|
git config --local core.hooksPath '%(prefix)/workspace/hooks'
|
||||||
|
# Set the safe directory to the workspace path
|
||||||
|
git config --local --add safe.directory /workspace
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dev Container Initialization Script (init-setup.sh)
|
||||||
|
#
|
||||||
|
# This script runs when the Dev Container starts and provides guidance on
|
||||||
|
# how to interact with the project. It prints an ASCII logo, displays the
|
||||||
|
# current user, changes to the project root, and then shows helpful command
|
||||||
|
# instructions.
|
||||||
|
#
|
||||||
|
# Instructions for future developers:
|
||||||
|
#
|
||||||
|
# - To start the application, use:
|
||||||
|
# ./gradlew bootRun --no-daemon -Dspring-boot.run.fork=true -Dserver.address=0.0.0.0
|
||||||
|
#
|
||||||
|
# - To run tests, use:
|
||||||
|
# ./gradlew test
|
||||||
|
#
|
||||||
|
# - To build the project, use:
|
||||||
|
# ./gradlew build
|
||||||
|
#
|
||||||
|
# - 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.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
echo "Devcontainer started successfully!"
|
||||||
|
|
||||||
|
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||||
|
GRADLE_VERSION=$(gradle -version | grep "^Gradle " | awk '{print $2}')
|
||||||
|
GRADLE_PATH=$(which gradle)
|
||||||
|
JAVA_VERSION=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
|
||||||
|
JAVA_PATH=$(which java)
|
||||||
|
|
||||||
|
echo """
|
||||||
|
____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____
|
||||||
|
/ ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___|
|
||||||
|
\___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_
|
||||||
|
___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _|
|
||||||
|
|____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_|
|
||||||
|
"""
|
||||||
|
echo -e "Stirling-PDF Version: \e[32m$VERSION\e[0m"
|
||||||
|
echo -e "Gradle Version: \e[32m$GRADLE_VERSION\e[0m"
|
||||||
|
echo -e "Gradle Path: \e[32m$GRADLE_PATH\e[0m"
|
||||||
|
echo -e "Java Version: \e[32m$JAVA_VERSION\e[0m"
|
||||||
|
echo -e "Java Path: \e[32m$JAVA_PATH\e[0m"
|
||||||
|
|
||||||
|
# Display current active user (for permission/debugging purposes)
|
||||||
|
echo -e "Current user: \e[32m$(whoami)\e[0m"
|
||||||
|
|
||||||
|
# Change directory to the project root (parent directory of the script)
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
echo -e "Changed to project root: \e[32m$(pwd)\e[0m"
|
||||||
|
|
||||||
|
# Display available commands for developers
|
||||||
|
echo "=================================================================="
|
||||||
|
echo "Available commands:"
|
||||||
|
echo ""
|
||||||
|
echo " To start unoserver: "
|
||||||
|
echo -e "\e[34m nohup /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0 > /tmp/unoserver.log 2>&1 &\e[0m"
|
||||||
|
echo
|
||||||
|
echo " To start the application: "
|
||||||
|
echo -e "\e[34m gradle bootRun\e[0m"
|
||||||
|
echo ""
|
||||||
|
echo " To run tests: "
|
||||||
|
echo -e "\e[34m gradle test\e[0m"
|
||||||
|
echo ""
|
||||||
|
echo " To build the project: "
|
||||||
|
echo -e "\e[34m gradle build\e[0m"
|
||||||
|
echo ""
|
||||||
|
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 "=================================================================="
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
# Version control
|
||||||
|
.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/dist-portal/
|
||||||
|
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/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# Logs and temp files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.pid
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
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
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!engine/.env
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
.cache/
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 127
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.java]
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[*.gradle]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.html]
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.css]
|
||||||
|
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.jsonc]
|
||||||
|
# JSONC (JSON with comments) files also follow the standard JSON formatting with an indent size of 2 spaces.
|
||||||
|
indent_size = 2
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# Formatting
|
# Formatting
|
||||||
5f771b785130154ed47952635b7acef371ffe0ec
|
5f771b785130154ed47952635b7acef371ffe0ec
|
||||||
|
7fa5e130d99227c2202ebddfdd91348176ec0c7b
|
||||||
|
14d4fbb2a36195eedb034785e5a5ff6a47f268c6
|
||||||
|
ee8030c1c4148062cde15c49c67d04ef03930c55
|
||||||
|
fcd41924f5f261febfa9d9a92994671f3ebc97d6
|
||||||
|
|
||||||
# Normalize files
|
# Normalize files
|
||||||
55d4fda01b2f39f5b7d7b4fda5214bd7ff0fd5dd
|
55d4fda01b2f39f5b7d7b4fda5214bd7ff0fd5dd
|
||||||
|
|||||||
+7
-7
@@ -1,10 +1,10 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
# Ignore all JavaScript files in a directory
|
# Ignore all JavaScript files in a directory
|
||||||
src/main/resources/static/pdfjs/* linguist-vendored
|
app/core/src/main/resources/static/pdfjs/* linguist-vendored
|
||||||
src/main/resources/static/pdfjs/** linguist-vendored
|
app/core/src/main/resources/static/pdfjs/** linguist-vendored
|
||||||
src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
app/core/src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
||||||
src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
app/core/src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
||||||
src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
app/core/src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
||||||
src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
app/core/src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
||||||
src/main/resources/static/css/fonts/* linguist-vendored
|
app/core/src/main/resources/static/css/fonts/* linguist-vendored
|
||||||
|
|||||||
+18
-2
@@ -1,2 +1,18 @@
|
|||||||
# All PRs to V1 must be approved by Frooodle
|
# All PRs must be approved by Frooodle or Ludy87
|
||||||
* @Frooodle
|
* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh @balazs-szucs
|
||||||
|
|
||||||
|
#V2 frontend
|
||||||
|
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @balazs-szucs
|
||||||
|
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 @balazs-szucs
|
||||||
|
|
||||||
|
#V2 docker
|
||||||
|
/docker/backend/** @Frooodle @Ludy87 @DarioGii
|
||||||
|
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
|
||||||
|
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
|
||||||
|
|
||||||
|
|
||||||
|
#GHA (All users)
|
||||||
|
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 @balazs-szucs
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: Frooodle # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
custom: ['https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
@@ -11,6 +11,18 @@ body:
|
|||||||
|
|
||||||
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: installation-method
|
||||||
|
attributes:
|
||||||
|
label: Installation Method
|
||||||
|
description: |
|
||||||
|
Indicate whether you are using Docker or a local installation.
|
||||||
|
options:
|
||||||
|
- Docker
|
||||||
|
- Docker ultra lite
|
||||||
|
- Docker fat
|
||||||
|
- Local Installation
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 💬 Discord Server
|
- name: 💬 Discord Server
|
||||||
url: https://discord.gg/Cn8pWhQRxZ
|
url: https://discord.gg/HYmhKj45pU
|
||||||
about: You can join our Discord server for real time discussion and support
|
about: You can join our Discord server for real time discussion and support
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: 'Setup GitHub App Bot'
|
||||||
|
description: 'Generates a GitHub App Token and configures Git for a bot'
|
||||||
|
inputs:
|
||||||
|
app-id:
|
||||||
|
description: 'GitHub App ID'
|
||||||
|
required: True
|
||||||
|
private-key:
|
||||||
|
description: 'GitHub App Private Key'
|
||||||
|
required: True
|
||||||
|
outputs:
|
||||||
|
token:
|
||||||
|
description: 'Generated GitHub App Token'
|
||||||
|
value: ${{ steps.generate-token.outputs.token }}
|
||||||
|
committer:
|
||||||
|
description: 'Committer string for Git'
|
||||||
|
value: "${{ steps.generate-token.outputs.app-slug }}[bot] <${{ steps.generate-token.outputs.app-slug }}[bot]@users.noreply.github.com>"
|
||||||
|
app-slug:
|
||||||
|
description: 'GitHub App slug'
|
||||||
|
value: ${{ steps.generate-token.outputs.app-slug }}
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Generate a GitHub App Token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
|
with:
|
||||||
|
client-id: ${{ inputs.app-id }}
|
||||||
|
private-key: ${{ inputs.private-key }}
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "${{ steps.generate-token.outputs.app-slug }}[bot]"
|
||||||
|
git config --global user.email "${{ steps.generate-token.outputs.app-slug }}[bot]@users.noreply.github.com"
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Maintainer: Stirling PDF Inc <[email protected]>
|
||||||
|
pkgname=stirling-pdf-desktop
|
||||||
|
pkgver=2.12.0
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Maintainer: Stirling PDF Inc <[email protected]>
|
||||||
|
pkgname=stirling-pdf-server-bin
|
||||||
|
pkgver=2.12.0
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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
|
||||||
|
- ".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/**
|
||||||
|
- *build
|
||||||
|
- "app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||||
|
- exampleYmlFiles/**
|
||||||
|
- gradle/**
|
||||||
|
- libs/**
|
||||||
|
- "testing/**/!(requirements*.txt|requirements*.in)*"
|
||||||
|
- *docker
|
||||||
|
- *docker-base
|
||||||
|
- gradle.properties
|
||||||
|
- gradlew
|
||||||
|
- gradlew.bat
|
||||||
|
- launch4jConfig.xml
|
||||||
|
- settings.gradle
|
||||||
|
- 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/**
|
||||||
|
- .github/workflows/testdriver.yml
|
||||||
|
- testing/**
|
||||||
|
- docker/**
|
||||||
|
- scripts/translations/*.py
|
||||||
|
- .taskfiles/desktop.yml
|
||||||
|
- scripts/convert_cff_to_ttf.py
|
||||||
|
- scripts/harvest_type3_fonts.py
|
||||||
|
- scripts/ignore_translation.toml
|
||||||
|
- scripts/index_type3_catalogue.py
|
||||||
|
- 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
|
||||||
|
|
||||||
|
licenses-frontend: &licenses-frontend
|
||||||
|
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||||
|
- "frontend/package.json"
|
||||||
|
- "frontend/package-lock.json"
|
||||||
|
- "frontend/editor/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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
allow-ghsas: GHSA-wrw7-89jp-8q8g
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"label_changer": [
|
||||||
|
"Frooodle",
|
||||||
|
"Ludy87",
|
||||||
|
"balazs-szucs"
|
||||||
|
],
|
||||||
|
"repo_devs": [
|
||||||
|
"Frooodle",
|
||||||
|
"sf298",
|
||||||
|
"Ludy87",
|
||||||
|
"LaserKaspar",
|
||||||
|
"sbplat",
|
||||||
|
"reecebrowne",
|
||||||
|
"DarioGii",
|
||||||
|
"ConnorYoh",
|
||||||
|
"EthanHealy01",
|
||||||
|
"jbrunton96",
|
||||||
|
"balazs-szucs"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
You are a professional software engineer specializing in reviewing pull request titles.
|
||||||
|
|
||||||
|
Your job is to analyze a git diff and an existing PR title, then evaluate and improve the PR title.
|
||||||
|
|
||||||
|
You must:
|
||||||
|
- Always return valid JSON
|
||||||
|
- Only return the JSON response (no Markdown, no formatting)
|
||||||
|
- Use one of these conventional commit types at the beginning of the title: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
|
||||||
|
- Use lowercase only, no emojis, no trailing period
|
||||||
|
- Ensure the title is between 5 and 72 printable ASCII characters
|
||||||
|
- Never let spelling or grammar errors affect the rating
|
||||||
|
- If the PR title is rated 6 or higher and only contains spelling or grammar mistakes, correct it - do not rephrase it
|
||||||
|
- If the PR title is rated below 6, generate a new, better title based on the diff
|
||||||
+151
-2
@@ -6,10 +6,159 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "gradle" # See documentation for possible values
|
- package-ecosystem: "gradle" # See documentation for possible values
|
||||||
directory: "/" # Location of package manifests
|
directories:
|
||||||
|
- "/" # Location of package manifests
|
||||||
|
- "/app/common"
|
||||||
|
- "/app/core"
|
||||||
|
- "/app/proprietary"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
|
rebase-strategy: "auto"
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/" # Location of Dockerfile
|
directories:
|
||||||
|
- "/" # Location of Dockerfile
|
||||||
|
- "/docker/backend"
|
||||||
|
- "/docker/embedded"
|
||||||
|
- "/docker/frontend"
|
||||||
|
- "/docker/base"
|
||||||
|
- "/docker/engine"
|
||||||
|
- "/docker/unoserver"
|
||||||
|
- "/engine"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
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
|
||||||
|
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
|
||||||
|
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"
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
version: 1
|
||||||
|
labels:
|
||||||
|
|
||||||
|
- label: "Bugfix"
|
||||||
|
title: '^fix(\([^)]*\))?:|^fix:.*'
|
||||||
|
|
||||||
|
- label: "enhancement"
|
||||||
|
title: '^feat(\([^)]*\))?:|^feat:.*'
|
||||||
|
|
||||||
|
- label: "build"
|
||||||
|
title: '^build(\([^)]*\))?:|^build:.*'
|
||||||
|
|
||||||
|
- label: "chore"
|
||||||
|
title: '^chore(\([^)]*\))?:|^chore:.*'
|
||||||
|
|
||||||
|
- label: "ci"
|
||||||
|
title: '^ci(\([^)]*\))?:|^ci:.*'
|
||||||
|
|
||||||
|
- label: "ci"
|
||||||
|
title: '^.*\(ci\):.*'
|
||||||
|
|
||||||
|
- label: "perf"
|
||||||
|
title: '^perf(\([^)]*\))?:|^perf:.*'
|
||||||
|
|
||||||
|
- label: "refactor"
|
||||||
|
title: '^refactor(\([^)]*\))?:|^refactor:.*'
|
||||||
|
|
||||||
|
- label: "revert"
|
||||||
|
title: '^revert(\([^)]*\))?:|^revert:.*'
|
||||||
|
|
||||||
|
- label: "style"
|
||||||
|
title: '^style(\([^)]*\))?:|^style:.*'
|
||||||
|
|
||||||
|
- label: "Documentation"
|
||||||
|
title: '^docs(\([^)]*\))?:|^docs:.*'
|
||||||
|
|
||||||
|
- label: "Documentation"
|
||||||
|
title: '^.*\(docs\):.*'
|
||||||
|
|
||||||
|
- label: "dependencies"
|
||||||
|
title: '^deps(\([^)]*\))?:|^deps:.*'
|
||||||
|
|
||||||
|
- label: "dependencies"
|
||||||
|
title: '^.*\(deps\):.*'
|
||||||
|
|
||||||
|
- label: 'API'
|
||||||
|
title: '.*openapi.*|.*swagger.*|.*api.*'
|
||||||
|
|
||||||
|
- label: 'v3'
|
||||||
|
base-branch: 'V3'
|
||||||
|
|
||||||
|
- label: 'Translation'
|
||||||
|
files:
|
||||||
|
- 'frontend/editor/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'
|
||||||
|
- 'scripts/translations/.*'
|
||||||
|
- '.github/scripts/check_language_toml.py'
|
||||||
|
- 'scripts/counter_translation_v3.py'
|
||||||
|
|
||||||
|
- label: 'Front End'
|
||||||
|
files:
|
||||||
|
- 'app/core/src/main/resources/static/.*'
|
||||||
|
- 'app/proprietary/src/main/resources/static/.*'
|
||||||
|
- 'frontend/**'
|
||||||
|
- 'frontend/.*'
|
||||||
|
- 'frontend/**/.*'
|
||||||
|
|
||||||
|
- label: 'Tauri'
|
||||||
|
files:
|
||||||
|
- 'frontend/editor/src-tauri/**'
|
||||||
|
- 'frontend/editor/src-tauri/.*'
|
||||||
|
|
||||||
|
- label: 'engine'
|
||||||
|
files:
|
||||||
|
- 'engine/**'
|
||||||
|
- 'engine/.*'
|
||||||
|
- 'engine/**/.*'
|
||||||
|
|
||||||
|
- label: 'Java'
|
||||||
|
files:
|
||||||
|
- 'app/common/src/main/java/.*.java'
|
||||||
|
- 'app/proprietary/src/main/java/.*.java'
|
||||||
|
- 'app/core/src/main/java/.*.java'
|
||||||
|
|
||||||
|
- label: 'Back End'
|
||||||
|
files:
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/config/.*'
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/controller/.*'
|
||||||
|
- 'app/core/src/main/resources/settings.yml.template'
|
||||||
|
- 'app/core/src/main/resources/application.properties'
|
||||||
|
- 'app/core/src/main/resources/banner.txt'
|
||||||
|
- 'app/core/src/main/resources/static/python/png_to_webp.py'
|
||||||
|
- 'app/core/src/main/resources/static/python/split_photos.py'
|
||||||
|
- 'app/core/src/main/resources/static/pipeline/defaultWebUIConfigs/**'
|
||||||
|
- 'application.properties'
|
||||||
|
|
||||||
|
- label: 'Security'
|
||||||
|
files:
|
||||||
|
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/.*'
|
||||||
|
- 'scripts/download-security-jar.sh'
|
||||||
|
- '.github/workflows/dependency-review.yml'
|
||||||
|
- '.github/workflows/scorecards.yml'
|
||||||
|
|
||||||
|
- label: 'API'
|
||||||
|
files:
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/controller/api/.*'
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/model/api/.*'
|
||||||
|
- 'app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java'
|
||||||
|
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*'
|
||||||
|
- 'app/core/src/main/resources/static/python/png_to_webp.py'
|
||||||
|
- 'app/core/src/main/resources/static/python/split_photos.py'
|
||||||
|
- '.github/workflows/swagger.yml'
|
||||||
|
|
||||||
|
- label: 'Documentation'
|
||||||
|
files:
|
||||||
|
- '.*.md'
|
||||||
|
- 'scripts/counter_translation.py'
|
||||||
|
- 'scripts/ignore_translation.toml'
|
||||||
|
|
||||||
|
- label: 'Docker'
|
||||||
|
files:
|
||||||
|
- '.github/workflows/build.yml'
|
||||||
|
- '.github/workflows/push-docker.yml'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'Dockerfile.fat'
|
||||||
|
- 'Dockerfile.ultra-lite'
|
||||||
|
- 'exampleYmlFiles/.*.yml'
|
||||||
|
- 'scripts/download-security-jar.sh'
|
||||||
|
- 'scripts/init.sh'
|
||||||
|
- 'scripts/init-without-ocr.sh'
|
||||||
|
- 'scripts/installFonts.sh'
|
||||||
|
- 'test.sh'
|
||||||
|
- 'test2.sh'
|
||||||
|
- 'docker/**'
|
||||||
|
|
||||||
|
- label: 'Devtools'
|
||||||
|
files:
|
||||||
|
- '.devcontainer/.*'
|
||||||
|
- 'Dockerfile.dev'
|
||||||
|
- '.vscode/.*'
|
||||||
|
- '.editorconfig'
|
||||||
|
- '.pre-commit-config'
|
||||||
|
- '.github/workflows/pre_commit.yml'
|
||||||
|
- 'devGuide/.*'
|
||||||
|
- 'devTools/.*'
|
||||||
|
|
||||||
|
- label: 'Test'
|
||||||
|
files:
|
||||||
|
- 'app/common/src/test/.*'
|
||||||
|
- 'app/proprietary/src/test/.*'
|
||||||
|
- 'app/core/src/test/.*'
|
||||||
|
- 'testing/.*'
|
||||||
|
- '.github/workflows/scorecards.yml'
|
||||||
|
- 'exampleYmlFiles/test_cicd.yml'
|
||||||
|
|
||||||
|
- label: 'Github'
|
||||||
|
files:
|
||||||
|
- '.github/.*'
|
||||||
|
|
||||||
|
- label: 'Gradle'
|
||||||
|
files:
|
||||||
|
- 'gradle/.*'
|
||||||
|
- 'gradlew'
|
||||||
|
- 'gradlew.bat'
|
||||||
|
- 'settings.gradle'
|
||||||
|
- 'build.gradle'
|
||||||
|
- 'app/common/build.gradle'
|
||||||
|
- 'app/proprietary/build.gradle'
|
||||||
|
- 'app/core/build.gradle'
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
Translation:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
|
||||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
|
||||||
|
|
||||||
Front End:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/templates/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/static/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
|
||||||
|
|
||||||
Java:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
|
||||||
|
|
||||||
Back End:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
|
||||||
|
|
||||||
Security:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
|
|
||||||
|
|
||||||
API:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
|
||||||
|
|
||||||
Documentation:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: '**/*.md'
|
|
||||||
- any-glob-to-any-file: 'scripts/counter_translation.py'
|
|
||||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
|
||||||
|
|
||||||
Docker:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'Dockerfile'
|
|
||||||
- any-glob-to-any-file: 'Dockerfile-*'
|
|
||||||
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
|
||||||
|
|
||||||
Test:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'cucumber/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/test**/*'
|
|
||||||
+112
-2
@@ -3,9 +3,12 @@
|
|||||||
#
|
#
|
||||||
# The repository labels will be automatically configured using this file and
|
# The repository labels will be automatically configured using this file and
|
||||||
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
||||||
|
- name: "Licenses"
|
||||||
|
color: "EDEDED"
|
||||||
|
from_name: "licenses"
|
||||||
- name: "Back End"
|
- name: "Back End"
|
||||||
color: "20CE6C"
|
color: "20CE6C"
|
||||||
description: "Issues related to back-end development"
|
description: "Issues or pull requests related to back-end development"
|
||||||
from_name: "Back end"
|
from_name: "Back end"
|
||||||
- name: "Bug"
|
- name: "Bug"
|
||||||
description: "Something isn't working"
|
description: "Something isn't working"
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
from_name: "documentation"
|
from_name: "documentation"
|
||||||
- name: "Done for next release"
|
- name: "Done for next release"
|
||||||
color: "0CDBD1"
|
color: "0CDBD1"
|
||||||
|
description: "Items that are completed and will be included in the next release"
|
||||||
- name: "Done"
|
- name: "Done"
|
||||||
color: "60F13B"
|
color: "60F13B"
|
||||||
- name: "duplicate"
|
- name: "duplicate"
|
||||||
@@ -37,7 +41,8 @@
|
|||||||
description: "Fix needs to be confirmed"
|
description: "Fix needs to be confirmed"
|
||||||
- name: "Front End"
|
- name: "Front End"
|
||||||
color: "BBD2F1"
|
color: "BBD2F1"
|
||||||
description: "Issues related to front-end development"
|
description: "Issues or pull requests related to front-end development"
|
||||||
|
from_name: "frontend"
|
||||||
- name: "github-actions"
|
- name: "github-actions"
|
||||||
description: "Pull requests that update GitHub Actions code"
|
description: "Pull requests that update GitHub Actions code"
|
||||||
color: "999999"
|
color: "999999"
|
||||||
@@ -73,10 +78,15 @@
|
|||||||
- name: "Translation"
|
- name: "Translation"
|
||||||
color: "9FABF9"
|
color: "9FABF9"
|
||||||
from_name: "translation"
|
from_name: "translation"
|
||||||
|
description: "Issues or pull requests related to translation"
|
||||||
- name: "upstream"
|
- name: "upstream"
|
||||||
color: "DEDEDE"
|
color: "DEDEDE"
|
||||||
- name: "v2"
|
- name: "v2"
|
||||||
color: "FFFF00"
|
color: "FFFF00"
|
||||||
|
description: "Issues or pull requests related to the v2 branch"
|
||||||
|
- name: "v3"
|
||||||
|
color: "FFA500"
|
||||||
|
description: "Issues or pull requests related to the v3 branch"
|
||||||
- name: "wontfix"
|
- name: "wontfix"
|
||||||
description: "This will not be worked on"
|
description: "This will not be worked on"
|
||||||
color: "FFFFFF"
|
color: "FFFFFF"
|
||||||
@@ -91,3 +101,103 @@
|
|||||||
description: "Testing-related issues or pull requests"
|
description: "Testing-related issues or pull requests"
|
||||||
- name: "Stale"
|
- name: "Stale"
|
||||||
color: "000000"
|
color: "000000"
|
||||||
|
description: "Issues or pull requests that have become inactive"
|
||||||
|
- name: "Priority: Critical"
|
||||||
|
color: "000000"
|
||||||
|
description: "Issues or pull requests with the highest priority"
|
||||||
|
- name: "Priority: High"
|
||||||
|
color: "FF0000"
|
||||||
|
description: "Issues or pull requests with high priority"
|
||||||
|
- name: "Priority: Medium"
|
||||||
|
color: "FFFF00"
|
||||||
|
description: "Issues or pull requests with medium priority"
|
||||||
|
- name: "Priority: Low"
|
||||||
|
color: "00FF00"
|
||||||
|
description: "Issues or pull requests with low priority"
|
||||||
|
- name: "Devtools"
|
||||||
|
color: "FF9E1F"
|
||||||
|
description: "Development tools"
|
||||||
|
- name: "Bugfix"
|
||||||
|
color: "FF9E1F"
|
||||||
|
description: "Pull requests that fix bugs"
|
||||||
|
- name: "Gradle"
|
||||||
|
color: "FF9E1F"
|
||||||
|
description: "Pull requests that update Gradle code"
|
||||||
|
- name: "build"
|
||||||
|
color: "1E90FF"
|
||||||
|
description: "Changes that affect the build system or external dependencies"
|
||||||
|
- name: "chore"
|
||||||
|
color: "FFD700"
|
||||||
|
description: "Routine tasks or maintenance that don't modify src or test files"
|
||||||
|
- name: "ci"
|
||||||
|
color: "4682B4"
|
||||||
|
description: "Changes to CI configuration files and scripts"
|
||||||
|
- name: "perf"
|
||||||
|
color: "FF69B4"
|
||||||
|
description: "Changes that improve performance"
|
||||||
|
- name: "refactor"
|
||||||
|
color: "9932CC"
|
||||||
|
description: "Code changes that neither fix a bug nor add a feature"
|
||||||
|
- name: "revert"
|
||||||
|
color: "DC143C"
|
||||||
|
description: "Reverts a previous commit"
|
||||||
|
- name: "style"
|
||||||
|
color: "FFA500"
|
||||||
|
description: "Changes that do not affect the meaning of the code (formatting, etc.)"
|
||||||
|
- name: "admin"
|
||||||
|
color: "195055"
|
||||||
|
- name: "codex"
|
||||||
|
color: "ededed"
|
||||||
|
description: null
|
||||||
|
- name: "Github"
|
||||||
|
color: "0052CC"
|
||||||
|
- name: "github_actions"
|
||||||
|
color: "000000"
|
||||||
|
description: "Pull requests that update GitHub Actions code"
|
||||||
|
- name: "needs-changes"
|
||||||
|
color: "A65A86"
|
||||||
|
- name: "on-hold"
|
||||||
|
color: "2526F9"
|
||||||
|
- name: "python"
|
||||||
|
color: "2b67c6"
|
||||||
|
description: "Pull requests that update Python code"
|
||||||
|
- name: "size:L"
|
||||||
|
color: "eb9500"
|
||||||
|
description: "This PR changes 100-499 lines ignoring generated files."
|
||||||
|
- name: "size:M"
|
||||||
|
color: "ebb800"
|
||||||
|
description: "This PR changes 30-99 lines ignoring generated files."
|
||||||
|
- name: "size:S"
|
||||||
|
color: "77b800"
|
||||||
|
description: "This PR changes 10-29 lines ignoring generated files."
|
||||||
|
- name: "size:XL"
|
||||||
|
color: "ff823f"
|
||||||
|
description: "This PR changes 500-999 lines ignoring generated files."
|
||||||
|
- name: "size:XS"
|
||||||
|
color: "00ff00"
|
||||||
|
description: "This PR changes 0-9 lines ignoring generated files."
|
||||||
|
- name: "size:XXL"
|
||||||
|
color: "ffb8b8"
|
||||||
|
description: "This PR changes 1000+ lines ignoring generated files."
|
||||||
|
- name: "to research"
|
||||||
|
color: "FBCA04"
|
||||||
|
- name: "pr-deployed"
|
||||||
|
color: "00FF00"
|
||||||
|
description: "Pull request has been deployed to a test environment"
|
||||||
|
- name: "codex"
|
||||||
|
color: "ededed"
|
||||||
|
description: "chatgpt AI generated code"
|
||||||
|
- name: "break-change"
|
||||||
|
color: "FF0000"
|
||||||
|
description: "This PR introduces a breaking API change."
|
||||||
|
- name: "Rust"
|
||||||
|
color: "DEA584"
|
||||||
|
description: "Pull requests that update Rust code"
|
||||||
|
from_name: "rust"
|
||||||
|
- name: "Tauri"
|
||||||
|
color: "24C8FF"
|
||||||
|
description: "Pull requests that update Tauri code"
|
||||||
|
from_name: "tauri"
|
||||||
|
- name: "license-review-required"
|
||||||
|
color: "EDEDED"
|
||||||
|
description: "This PR requires a license review"
|
||||||
|
|||||||
@@ -1,18 +1,41 @@
|
|||||||
# Description
|
# Description of Changes
|
||||||
|
|
||||||
Please provide a summary of the changes, including relevant motivation and context.
|
<!--
|
||||||
|
Please provide a summary of the changes, including:
|
||||||
|
|
||||||
|
- What was changed
|
||||||
|
- Why the change was made
|
||||||
|
- Any challenges encountered
|
||||||
|
|
||||||
Closes #(issue_number)
|
Closes #(issue_number)
|
||||||
|
-->
|
||||||
|
|
||||||
## Checklist:
|
---
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
- [ ] 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 [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
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
|
|
||||||
## Contributor License Agreement
|
### Documentation
|
||||||
|
|
||||||
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
|
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
||||||
|
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||||
|
|
||||||
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)
|
### Translations (if applicable)
|
||||||
|
|
||||||
|
- [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
|
||||||
|
|
||||||
|
### UI Changes (if applicable)
|
||||||
|
|
||||||
|
- [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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:
|
||||||
|
- break-change
|
||||||
|
|
||||||
|
- title: Bug Fixes
|
||||||
|
labels:
|
||||||
|
- Bugfix
|
||||||
|
- Bug
|
||||||
|
|
||||||
|
- title: Enhancements
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
- Java
|
||||||
|
- Front End
|
||||||
|
- Back End
|
||||||
|
- Tauri
|
||||||
|
|
||||||
|
- title: Minor Enhancements
|
||||||
|
labels:
|
||||||
|
- chore
|
||||||
|
- style
|
||||||
|
- refactor
|
||||||
|
|
||||||
|
- title: Docker Updates
|
||||||
|
labels:
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
- title: Translation Changes
|
||||||
|
labels:
|
||||||
|
- Translation
|
||||||
|
|
||||||
|
- title: Development Tools
|
||||||
|
labels:
|
||||||
|
- Devtools
|
||||||
|
|
||||||
|
- title: Other Changes
|
||||||
|
labels:
|
||||||
|
- "*"
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def find_duplicate_keys(file_path):
|
|
||||||
"""
|
|
||||||
Finds duplicate keys in a properties file and returns their occurrences.
|
|
||||||
|
|
||||||
This function reads a properties file, identifies any keys that occur more than
|
|
||||||
once, and returns a dictionary with these keys and the line numbers of their occurrences.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
file_path (str): The path to the properties file to be checked.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: A dictionary where each key is a duplicated key in the file, and the value is a list
|
|
||||||
of line numbers where the key occurs.
|
|
||||||
"""
|
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
|
||||||
lines = file.readlines()
|
|
||||||
|
|
||||||
keys = {}
|
|
||||||
duplicates = {}
|
|
||||||
|
|
||||||
for line_number, line in enumerate(lines, start=1):
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith("#") and "=" in line:
|
|
||||||
key = line.split("=", 1)[0].strip()
|
|
||||||
if key in keys:
|
|
||||||
# If the key already exists, add the current line number
|
|
||||||
duplicates.setdefault(key, []).append(line_number)
|
|
||||||
# Also add the first instance of the key if not already done
|
|
||||||
if keys[key] not in duplicates[key]:
|
|
||||||
duplicates[key].insert(0, keys[key])
|
|
||||||
else:
|
|
||||||
# Store the line number of the first instance of the key
|
|
||||||
keys[key] = line_number
|
|
||||||
|
|
||||||
return duplicates
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
failed = False
|
|
||||||
for ar in sys.argv[1:]:
|
|
||||||
duplicates = find_duplicate_keys(ar)
|
|
||||||
if duplicates:
|
|
||||||
for key, lines in duplicates.items():
|
|
||||||
lines_str = ", ".join(map(str, lines))
|
|
||||||
print(f"{key} duplicated in {ar} on lines {lines_str}")
|
|
||||||
failed = True
|
|
||||||
if failed:
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
"""
|
||||||
|
Author: Ludy87
|
||||||
|
Description: This script processes TOML translation files for localization checks. It compares translation files in a branch with
|
||||||
|
a reference file to ensure consistency. The script performs two main checks:
|
||||||
|
1. Verifies that the number of translation keys in the translation files matches the reference file.
|
||||||
|
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
|
||||||
|
|
||||||
|
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
|
||||||
|
adjusting the format.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python check_language_toml.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import tomllib # Python 3.11+ (stdlib)
|
||||||
|
import tomli_w # For writing TOML files
|
||||||
|
|
||||||
|
|
||||||
|
def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||||
|
"""
|
||||||
|
Identifies duplicate keys in a TOML file (including nested keys).
|
||||||
|
:param file_path: Path to the TOML file.
|
||||||
|
:param keys: Dictionary to track keys (used for recursion).
|
||||||
|
:param prefix: Prefix for nested keys.
|
||||||
|
:return: List of tuples (key, first_occurrence_path, duplicate_path).
|
||||||
|
"""
|
||||||
|
if keys is None:
|
||||||
|
keys = {}
|
||||||
|
|
||||||
|
duplicates = []
|
||||||
|
|
||||||
|
# Load TOML file
|
||||||
|
file_path = Path(file_path)
|
||||||
|
with file_path.open("rb") as file:
|
||||||
|
data = tomllib.load(file)
|
||||||
|
|
||||||
|
def process_dict(obj, current_prefix=""):
|
||||||
|
for key, value in obj.items():
|
||||||
|
full_key = f"{current_prefix}.{key}" if current_prefix else key
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
process_dict(value, full_key)
|
||||||
|
else:
|
||||||
|
if full_key in keys:
|
||||||
|
duplicates.append((full_key, keys[full_key], full_key))
|
||||||
|
else:
|
||||||
|
keys[full_key] = full_key
|
||||||
|
|
||||||
|
process_dict(data, prefix)
|
||||||
|
return duplicates
|
||||||
|
|
||||||
|
|
||||||
|
# Maximum size for TOML files (e.g., 1 MB)
|
||||||
|
MAX_FILE_SIZE = 1000 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def parse_toml_file(file_path):
|
||||||
|
"""
|
||||||
|
Parses a TOML translation file and returns a flat dictionary of all keys.
|
||||||
|
:param file_path: Path to the TOML file.
|
||||||
|
:return: Dictionary with flattened keys.
|
||||||
|
"""
|
||||||
|
file_path = Path(file_path)
|
||||||
|
with file_path.open("rb") as file:
|
||||||
|
data = tomllib.load(file)
|
||||||
|
|
||||||
|
def flatten_dict(d, parent_key="", sep="."):
|
||||||
|
items = {}
|
||||||
|
for k, v in d.items():
|
||||||
|
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||||
|
if isinstance(v, dict):
|
||||||
|
items.update(flatten_dict(v, new_key, sep=sep))
|
||||||
|
else:
|
||||||
|
items[new_key] = v
|
||||||
|
return items
|
||||||
|
|
||||||
|
return flatten_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def unflatten_dict(d, sep="."):
|
||||||
|
"""
|
||||||
|
Converts a flat dictionary with dot notation keys back to nested dict.
|
||||||
|
:param d: Flattened dictionary.
|
||||||
|
:param sep: Separator used in keys.
|
||||||
|
:return: Nested dictionary.
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for key, value in d.items():
|
||||||
|
parts = key.split(sep)
|
||||||
|
current = result
|
||||||
|
for part in parts[:-1]:
|
||||||
|
if part not in current:
|
||||||
|
current[part] = {}
|
||||||
|
current = current[part]
|
||||||
|
current[parts[-1]] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def write_toml_file(file_path, updated_properties):
|
||||||
|
"""
|
||||||
|
Writes updated properties back to the TOML file.
|
||||||
|
:param file_path: Path to the TOML file.
|
||||||
|
:param updated_properties: Dictionary of updated properties to write.
|
||||||
|
"""
|
||||||
|
nested_data = unflatten_dict(updated_properties)
|
||||||
|
|
||||||
|
file_path = Path(file_path)
|
||||||
|
with file_path.open("wb") as file:
|
||||||
|
tomli_w.dump(nested_data, file)
|
||||||
|
|
||||||
|
|
||||||
|
def update_missing_keys(reference_file, file_list, branch=""):
|
||||||
|
"""
|
||||||
|
Updates missing keys in the translation files based on the reference file.
|
||||||
|
:param reference_file: Path to the reference TOML file.
|
||||||
|
:param file_list: List of translation files to update.
|
||||||
|
:param branch: Branch where the files are located.
|
||||||
|
"""
|
||||||
|
reference_file = Path(reference_file)
|
||||||
|
reference_properties = parse_toml_file(reference_file)
|
||||||
|
branch_path = Path(branch) if branch else Path()
|
||||||
|
|
||||||
|
for file_path in file_list:
|
||||||
|
file_path = Path(file_path)
|
||||||
|
language_dir = file_path.parent.name
|
||||||
|
reference_lang_dir = reference_file.parent.name
|
||||||
|
if (
|
||||||
|
language_dir == reference_lang_dir
|
||||||
|
or file_path.suffix != ".toml"
|
||||||
|
or file_path.parents[1].name != "locales"
|
||||||
|
):
|
||||||
|
print(f"Skipping file: {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_properties = parse_toml_file(branch_path / file_path)
|
||||||
|
updated_properties = {}
|
||||||
|
|
||||||
|
for ref_key, ref_value in reference_properties.items():
|
||||||
|
if ref_key in current_properties:
|
||||||
|
# Keep the current translation
|
||||||
|
updated_properties[ref_key] = current_properties[ref_key]
|
||||||
|
else:
|
||||||
|
# Add missing key with reference value
|
||||||
|
updated_properties[ref_key] = ref_value
|
||||||
|
|
||||||
|
write_toml_file(branch_path / file_path, updated_properties)
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_missing_keys(reference_file, file_list, branch):
|
||||||
|
update_missing_keys(reference_file, file_list, branch)
|
||||||
|
|
||||||
|
|
||||||
|
def read_toml_keys(file_path):
|
||||||
|
file_path = Path(file_path)
|
||||||
|
if file_path.is_file():
|
||||||
|
return parse_toml_file(file_path)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_differences(reference_file, file_list, branch, actor):
|
||||||
|
reference_branch = branch
|
||||||
|
reference_file = Path(reference_file)
|
||||||
|
basename_reference_file = reference_file.name
|
||||||
|
branch_path = Path(branch) if branch else Path()
|
||||||
|
|
||||||
|
report = []
|
||||||
|
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||||
|
reference_keys = read_toml_keys(reference_file)
|
||||||
|
has_differences = False
|
||||||
|
|
||||||
|
only_reference_file = True
|
||||||
|
|
||||||
|
file_arr = file_list
|
||||||
|
|
||||||
|
if len(file_list) == 1:
|
||||||
|
file_arr = file_list[0].split()
|
||||||
|
|
||||||
|
base_dir = Path.cwd() / "frontend" / "editor" / "public" / "locales"
|
||||||
|
|
||||||
|
for file_path in file_arr:
|
||||||
|
file_path = Path(file_path)
|
||||||
|
file_normpath = file_path
|
||||||
|
absolute_path = file_normpath.resolve()
|
||||||
|
|
||||||
|
basename_current_file = (branch_path / file_normpath).name
|
||||||
|
locale_dir = file_normpath.parent.name
|
||||||
|
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
|
||||||
|
|
||||||
|
# Verify that file is within the expected directory
|
||||||
|
if not absolute_path.is_relative_to(base_dir):
|
||||||
|
has_differences = True
|
||||||
|
report.append(
|
||||||
|
f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Verify file size before processing
|
||||||
|
if (branch_path / file_normpath).stat().st_size > MAX_FILE_SIZE:
|
||||||
|
has_differences = True
|
||||||
|
report.append(
|
||||||
|
f"\n⚠️ The file `{locale_dir}/{basename_current_file}` is too large and could pose a security risk.\n\n---\n"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if basename_current_file == basename_reference_file and locale_dir == "en-US":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
file_normpath.suffix != ".toml"
|
||||||
|
or basename_current_file != "translation.toml"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
only_reference_file = False
|
||||||
|
current_keys = read_toml_keys(branch_path / file_path)
|
||||||
|
reference_key_count = len(reference_keys)
|
||||||
|
current_key_count = len(current_keys)
|
||||||
|
|
||||||
|
if reference_key_count != current_key_count:
|
||||||
|
report.append("")
|
||||||
|
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||||
|
report.append(" - **Issue:**")
|
||||||
|
has_differences = True
|
||||||
|
if reference_key_count > current_key_count:
|
||||||
|
report.append(
|
||||||
|
f" - **_Mismatched key count_**: {reference_key_count} (reference) vs {current_key_count} (current). Translation keys are missing."
|
||||||
|
)
|
||||||
|
elif reference_key_count < current_key_count:
|
||||||
|
report.append(
|
||||||
|
f" - **_Too many keys_**: {reference_key_count} (reference) vs {current_key_count} (current). Please verify if there are additional keys that need to be removed."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||||
|
|
||||||
|
# Check for missing or extra keys
|
||||||
|
current_keys_set = set(current_keys.keys())
|
||||||
|
reference_keys_set = set(reference_keys.keys())
|
||||||
|
missing_keys = current_keys_set.difference(reference_keys_set)
|
||||||
|
extra_keys = reference_keys_set.difference(current_keys_set)
|
||||||
|
missing_keys_list = list(missing_keys)
|
||||||
|
extra_keys_list = list(extra_keys)
|
||||||
|
|
||||||
|
if missing_keys_list or extra_keys_list:
|
||||||
|
has_differences = True
|
||||||
|
missing_keys_str = "`, `".join(missing_keys_list)
|
||||||
|
extra_keys_str = "`, `".join(extra_keys_list)
|
||||||
|
report.append("2. **Test Status:** ❌ **_Failed_**")
|
||||||
|
report.append(" - **Issue:**")
|
||||||
|
if missing_keys_list:
|
||||||
|
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_**")
|
||||||
|
|
||||||
|
if find_duplicate_keys(branch_path / file_normpath):
|
||||||
|
has_differences = True
|
||||||
|
output = "\n".join(
|
||||||
|
[
|
||||||
|
f" - `{key}`: first at {first}, duplicate at `{duplicate}`"
|
||||||
|
for key, first, duplicate in find_duplicate_keys(
|
||||||
|
branch_path / file_normpath
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
report.append("3. **Test Status:** ❌ **_Failed_**")
|
||||||
|
report.append(" - **Issue:**")
|
||||||
|
report.append(" - duplicate entries were found:")
|
||||||
|
report.append(output)
|
||||||
|
else:
|
||||||
|
report.append("3. **Test Status:** ✅ **_Passed_**")
|
||||||
|
|
||||||
|
report.append("")
|
||||||
|
report.append("---")
|
||||||
|
report.append("")
|
||||||
|
|
||||||
|
if has_differences:
|
||||||
|
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)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||||
|
report.append("")
|
||||||
|
report.append(
|
||||||
|
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not only_reference_file:
|
||||||
|
print("\n".join(report))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Find missing keys in TOML translation files"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--actor",
|
||||||
|
required=False,
|
||||||
|
help="Actor from PR.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--reference-file",
|
||||||
|
required=True,
|
||||||
|
help="Path to the reference file.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--branch",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="Branch name.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--check-file",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="List of changed files, separated by spaces.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--files",
|
||||||
|
nargs="+",
|
||||||
|
required=False,
|
||||||
|
help="List of changed files, separated by spaces.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Sanitize --actor input to avoid injection attacks
|
||||||
|
if args.actor:
|
||||||
|
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
|
||||||
|
|
||||||
|
# Sanitize --branch input to avoid injection attacks
|
||||||
|
if args.branch:
|
||||||
|
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
|
||||||
|
|
||||||
|
file_list = args.files
|
||||||
|
if file_list is None:
|
||||||
|
if args.check_file:
|
||||||
|
file_list = [args.check_file]
|
||||||
|
else:
|
||||||
|
file_list = glob.glob(
|
||||||
|
os.path.join(
|
||||||
|
os.getcwd(),
|
||||||
|
"frontend",
|
||||||
|
"editor",
|
||||||
|
"public",
|
||||||
|
"locales",
|
||||||
|
"*",
|
||||||
|
"translation.toml",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
update_missing_keys(args.reference_file, file_list)
|
||||||
|
else:
|
||||||
|
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
"""check_tabulator.py"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def check_tabs(file_path):
|
|
||||||
"""
|
|
||||||
Checks for tabs in the specified file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path (str): The path to the file to be checked.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if tabs are found, False otherwise.
|
|
||||||
"""
|
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
|
||||||
content = file.read()
|
|
||||||
|
|
||||||
if "\t" in content:
|
|
||||||
print(f"Tab found in {file_path}")
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def replace_tabs_with_spaces(file_path, replace_with=" "):
|
|
||||||
"""
|
|
||||||
Replaces tabs with a specified number of spaces in the file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path (str): The path to the file where tabs will be replaced.
|
|
||||||
replace_with (str): The character(s) to replace tabs with. Defaults to two spaces.
|
|
||||||
"""
|
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
|
||||||
content = file.read()
|
|
||||||
|
|
||||||
updated_content = content.replace("\t", replace_with)
|
|
||||||
|
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
|
||||||
file.write(updated_content)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""
|
|
||||||
Main function to replace tabs with spaces in the provided files.
|
|
||||||
The replacement character and files to check are taken from command line arguments.
|
|
||||||
"""
|
|
||||||
# Create ArgumentParser instance
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Replace tabs in files with specified characters."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define optional argument `--replace_with`
|
|
||||||
parser.add_argument(
|
|
||||||
"--replace_with",
|
|
||||||
default=" ",
|
|
||||||
help="Character(s) to replace tabs with. Default is two spaces.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define argument for file paths
|
|
||||||
parser.add_argument("files", metavar="FILE", nargs="+", help="Files to process.")
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Extract replacement characters and files from the parsed arguments
|
|
||||||
replace_with = args.replace_with
|
|
||||||
files_checked = args.files
|
|
||||||
|
|
||||||
error = False
|
|
||||||
|
|
||||||
for file_path in files_checked:
|
|
||||||
if check_tabs(file_path):
|
|
||||||
replace_tabs_with_spaces(file_path, replace_with)
|
|
||||||
error = True
|
|
||||||
|
|
||||||
if error:
|
|
||||||
print("Error: Originally found tabs in HTML files, now replaced.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import re
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
# Paths to the files
|
|
||||||
chart_yaml_path = "chart/stirling-pdf/Chart.yaml"
|
|
||||||
gradle_path = "build.gradle"
|
|
||||||
|
|
||||||
|
|
||||||
def get_chart_version(path):
|
|
||||||
"""
|
|
||||||
Reads the appVersion from Chart.yaml.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): The file path to the Chart.yaml.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The appVersion if found, otherwise an empty string.
|
|
||||||
"""
|
|
||||||
with open(path, encoding="utf-8") as file:
|
|
||||||
chart_yaml = yaml.safe_load(file)
|
|
||||||
return chart_yaml.get("appVersion", "")
|
|
||||||
|
|
||||||
|
|
||||||
def get_gradle_version(path):
|
|
||||||
"""
|
|
||||||
Extracts the version from build.gradle.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): The file path to the build.gradle.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The version if found, otherwise an empty string.
|
|
||||||
"""
|
|
||||||
with open(path, encoding="utf-8") as file:
|
|
||||||
for line in file:
|
|
||||||
if "version =" in line:
|
|
||||||
# Extracts the value after 'version ='
|
|
||||||
return re.search(r'version\s*=\s*[\'"](.+?)[\'"]', line).group(1)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def update_chart_version(path, new_version):
|
|
||||||
"""
|
|
||||||
Updates the appVersion in Chart.yaml with a new version.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): The file path to the Chart.yaml.
|
|
||||||
new_version (str): The new version to update to.
|
|
||||||
"""
|
|
||||||
with open(path, encoding="utf-8") as file:
|
|
||||||
chart_yaml = yaml.safe_load(file)
|
|
||||||
chart_yaml["appVersion"] = new_version
|
|
||||||
with open(path, "w", encoding="utf-8") as file:
|
|
||||||
yaml.safe_dump(chart_yaml, file)
|
|
||||||
|
|
||||||
|
|
||||||
# Main logic
|
|
||||||
chart_version = get_chart_version(chart_yaml_path)
|
|
||||||
gradle_version = get_gradle_version(gradle_path)
|
|
||||||
|
|
||||||
if chart_version != gradle_version:
|
|
||||||
print(
|
|
||||||
f"Versions do not match. Updating Chart.yaml from {chart_version} to {gradle_version}."
|
|
||||||
)
|
|
||||||
update_chart_version(chart_yaml_path, gradle_version)
|
|
||||||
else:
|
|
||||||
print("Versions match. No update required.")
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
pip
|
||||||
|
setuptools
|
||||||
|
WeasyPrint
|
||||||
|
pdf2image
|
||||||
|
pillow
|
||||||
|
unoserver
|
||||||
|
opencv-python-headless
|
||||||
|
pre-commit
|
||||||
|
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||||
|
#
|
||||||
|
# WARNING: pip install will require the following package to be hashed.
|
||||||
|
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
|
||||||
|
# CVE-2025-6176 mitigation: pin brotli to a specific commit
|
||||||
|
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||||
|
# via
|
||||||
|
# -r .github/scripts/requirements_dev.in
|
||||||
|
# fonttools
|
||||||
|
cffi==2.0.0 \
|
||||||
|
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
|
||||||
|
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
|
||||||
|
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
|
||||||
|
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
|
||||||
|
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
|
||||||
|
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
|
||||||
|
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
|
||||||
|
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
|
||||||
|
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
|
||||||
|
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
|
||||||
|
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
|
||||||
|
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
|
||||||
|
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
|
||||||
|
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
|
||||||
|
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
|
||||||
|
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
|
||||||
|
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
|
||||||
|
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
|
||||||
|
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
|
||||||
|
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
|
||||||
|
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
|
||||||
|
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
|
||||||
|
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
|
||||||
|
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
|
||||||
|
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
|
||||||
|
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
|
||||||
|
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
|
||||||
|
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
|
||||||
|
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
|
||||||
|
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
|
||||||
|
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
|
||||||
|
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
|
||||||
|
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
|
||||||
|
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
|
||||||
|
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
|
||||||
|
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
|
||||||
|
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
|
||||||
|
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
|
||||||
|
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
|
||||||
|
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
|
||||||
|
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
|
||||||
|
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
|
||||||
|
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
|
||||||
|
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
|
||||||
|
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
|
||||||
|
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
|
||||||
|
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
|
||||||
|
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
|
||||||
|
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
|
||||||
|
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
|
||||||
|
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
|
||||||
|
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
|
||||||
|
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
|
||||||
|
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
|
||||||
|
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
|
||||||
|
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
|
||||||
|
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
|
||||||
|
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
|
||||||
|
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
|
||||||
|
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
|
||||||
|
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
|
||||||
|
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
|
||||||
|
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
|
||||||
|
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
|
||||||
|
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
|
||||||
|
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
|
||||||
|
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
|
||||||
|
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
|
||||||
|
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
|
||||||
|
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
|
||||||
|
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
|
||||||
|
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
|
||||||
|
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
|
||||||
|
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
|
||||||
|
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
|
||||||
|
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
|
||||||
|
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
|
||||||
|
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
|
||||||
|
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
|
||||||
|
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
|
||||||
|
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
|
||||||
|
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
|
||||||
|
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
||||||
|
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||||
|
# via weasyprint
|
||||||
|
cfgv==3.5.0 \
|
||||||
|
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
|
||||||
|
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
|
||||||
|
# via pre-commit
|
||||||
|
cssselect2==0.9.0 \
|
||||||
|
--hash=sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 \
|
||||||
|
--hash=sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb
|
||||||
|
# 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
|
||||||
|
# via weasyprint
|
||||||
|
identify==2.6.19 \
|
||||||
|
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
|
||||||
|
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
|
||||||
|
# via pre-commit
|
||||||
|
nodeenv==1.10.0 \
|
||||||
|
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||||
|
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
pycparser==3.0 \
|
||||||
|
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||||
|
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||||
|
# via cffi
|
||||||
|
pydyf==0.12.1 \
|
||||||
|
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
|
||||||
|
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
|
||||||
|
# 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 \
|
||||||
|
--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
|
||||||
|
tinycss2==1.5.1 \
|
||||||
|
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
|
||||||
|
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
|
||||||
|
# via
|
||||||
|
# cssselect2
|
||||||
|
# weasyprint
|
||||||
|
tinyhtml5==2.1.0 \
|
||||||
|
--hash=sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67 \
|
||||||
|
--hash=sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a
|
||||||
|
# via weasyprint
|
||||||
|
unoserver==3.6 \
|
||||||
|
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
|
||||||
|
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
virtualenv==21.2.4 \
|
||||||
|
--hash=sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac \
|
||||||
|
--hash=sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada
|
||||||
|
# via pre-commit
|
||||||
|
weasyprint==68.1 \
|
||||||
|
--hash=sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be \
|
||||||
|
--hash=sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
webencodings==0.5.1 \
|
||||||
|
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||||
|
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||||
|
# via
|
||||||
|
# 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
|
||||||
|
# via fonttools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
pip==26.0.1 \
|
||||||
|
--hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \
|
||||||
|
--hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
setuptools==82.0.1 \
|
||||||
|
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||||
|
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pre-commit
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#
|
||||||
|
# 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.29.0 \
|
||||||
|
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
|
||||||
|
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
|
||||||
|
# via
|
||||||
|
# python-discovery
|
||||||
|
# virtualenv
|
||||||
|
identify==2.6.19 \
|
||||||
|
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
|
||||||
|
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
|
||||||
|
# via pre-commit
|
||||||
|
nodeenv==1.10.0 \
|
||||||
|
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
|
||||||
|
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
|
||||||
|
# via pre-commit
|
||||||
|
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
|
||||||
|
# via -r .github/scripts/requirements_pre_commit.in
|
||||||
|
python-discovery==1.2.2 \
|
||||||
|
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
|
||||||
|
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
|
||||||
|
# via virtualenv
|
||||||
|
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==21.2.4 \
|
||||||
|
--hash=sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac \
|
||||||
|
--hash=sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada
|
||||||
|
# via pre-commit
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
tomlkit
|
||||||
|
tomli-w
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||||
|
#
|
||||||
|
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
|
||||||
|
# via -r .github/scripts/requirements_sync_readme.in
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#!/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)
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
name: Auto PR V2 Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pr:
|
||||||
|
description: "PR number to deploy"
|
||||||
|
required: true
|
||||||
|
allow_fork:
|
||||||
|
description: "Allow deploying fork PR?"
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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' }}
|
||||||
|
outputs:
|
||||||
|
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||||
|
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||||
|
allow_fork: ${{ steps.decide.outputs.allow_fork }}
|
||||||
|
pr_number: ${{ steps.resolve.outputs.pr_number }}
|
||||||
|
pr_repository: ${{ steps.resolve.outputs.repository }}
|
||||||
|
pr_ref: ${{ steps.resolve.outputs.ref }}
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Resolve PR info
|
||||||
|
id: resolve
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
let prNumber = context.eventName === 'workflow_dispatch'
|
||||||
|
? parseInt(context.payload.inputs.pr, 10)
|
||||||
|
: context.payload.number;
|
||||||
|
|
||||||
|
if (!Number.isInteger(prNumber)) { core.setFailed('Invalid PR number'); return; }
|
||||||
|
|
||||||
|
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||||
|
core.setOutput('pr_number', String(prNumber));
|
||||||
|
core.setOutput('repository', pr.head.repo.full_name);
|
||||||
|
core.setOutput('ref', pr.head.ref);
|
||||||
|
core.setOutput('is_fork', String(pr.head.repo.fork));
|
||||||
|
core.setOutput('author', pr.user.login);
|
||||||
|
core.setOutput('state', pr.state);
|
||||||
|
|
||||||
|
- name: Decide deploy
|
||||||
|
id: decide
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
STATE: ${{ steps.resolve.outputs.state }}
|
||||||
|
IS_FORK: ${{ steps.resolve.outputs.is_fork }}
|
||||||
|
# nur bei workflow_dispatch gesetzt:
|
||||||
|
ALLOW_FORK_INPUT: ${{ inputs.allow_fork }}
|
||||||
|
PR_AUTHOR: ${{ steps.resolve.outputs.author }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
# Standard: nichts deployen
|
||||||
|
should=false
|
||||||
|
allow_fork="$(echo "${ALLOW_FORK_INPUT:-false}" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||||
|
if [ "$STATE" != "open" ]; then
|
||||||
|
echo "PR not open -> skip"
|
||||||
|
else
|
||||||
|
if [ "$IS_FORK" = "true" ] && [ "$allow_fork" != "true" ]; then
|
||||||
|
echo "Fork PR and allow_fork=false -> skip"
|
||||||
|
else
|
||||||
|
should=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs")
|
||||||
|
is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done
|
||||||
|
if [ "$is_auth" = true ]; then
|
||||||
|
should=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "should_deploy=$should" >> $GITHUB_OUTPUT
|
||||||
|
echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
deploy-v2-pr:
|
||||||
|
needs: [pick, check-pr]
|
||||||
|
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||||
|
if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true')
|
||||||
|
# Concurrency control - only one deployment per PR at a time
|
||||||
|
concurrency:
|
||||||
|
group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout main repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Add deployment started comment
|
||||||
|
id: deployment-started
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||||
|
|
||||||
|
// Delete previous V2 deployment comments to avoid clutter
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
per_page: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
const v2Comments = comments.filter(comment =>
|
||||||
|
comment.body.includes('🚀 **Auto-deploying V2 version**') ||
|
||||||
|
comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') ||
|
||||||
|
comment.body.includes('❌ **V2 Auto-deployment failed**')
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const comment of v2Comments) {
|
||||||
|
console.log(`Deleting old V2 comment: ${comment.id}`);
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: comment.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new deployment started comment
|
||||||
|
const { data: newComment } = await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment for approved V2 contributors._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
|
||||||
|
});
|
||||||
|
return newComment.id;
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||||
|
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||||
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
|
- name: Get commit hash for app
|
||||||
|
id: commit-hash
|
||||||
|
run: |
|
||||||
|
# Get last commit that touched the application code
|
||||||
|
APP_HASH=$(git log -1 --format="%H" -- . 2>/dev/null || echo "")
|
||||||
|
if [ -z "$APP_HASH" ]; then
|
||||||
|
APP_HASH="no-changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "App hash: $APP_HASH"
|
||||||
|
echo "app_hash=$APP_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Short hash for tags
|
||||||
|
if [ "$APP_HASH" = "no-changes" ]; then
|
||||||
|
echo "app_short=no-changes" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if image exists
|
||||||
|
id: check-image
|
||||||
|
run: |
|
||||||
|
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Image already exists, skipping build"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Image needs to be built"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Deploy V2 to VPS
|
||||||
|
id: deploy
|
||||||
|
run: |
|
||||||
|
# Use same port strategy as regular PRs - just the PR number
|
||||||
|
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||||
|
|
||||||
|
# Create docker-compose for V2 with unified embedded image
|
||||||
|
cat > docker-compose.yml << EOF
|
||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf-v2:
|
||||||
|
container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||||
|
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
|
||||||
|
ports:
|
||||||
|
- "${V2_PORT}:8080"
|
||||||
|
volumes:
|
||||||
|
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||||
|
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||||
|
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||||
|
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/storage:/storage:rw
|
||||||
|
environment:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||||
|
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||||
|
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
|
||||||
|
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||||
|
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||||
|
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||||
|
restart: on-failure:5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Deploy to VPS
|
||||||
|
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose-v2.yml
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Move docker-compose file to correct location
|
||||||
|
mv /tmp/docker-compose-v2.yml /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/docker-compose.yml
|
||||||
|
|
||||||
|
# Stop any existing container and clean up
|
||||||
|
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
|
||||||
|
docker-compose down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start the new container
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Clean up unused Docker resources to save space
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
|
||||||
|
# Clean up old images (older than 2 weeks)
|
||||||
|
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
# Set port for output
|
||||||
|
echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Post V2 deployment URL to PR
|
||||||
|
if: success()
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||||
|
const v2Port = ${{ steps.deploy.outputs.v2_port }};
|
||||||
|
|
||||||
|
// Delete the "deploying..." comment since we're posting the final result
|
||||||
|
const deploymentStartedId = ${{ steps.deployment-started.outputs.result }};
|
||||||
|
if (deploymentStartedId) {
|
||||||
|
console.log(`Deleting deployment started comment: ${deploymentStartedId}`);
|
||||||
|
try {
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: deploymentStartedId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Could not delete deployment started comment: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${v2Port}`;
|
||||||
|
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||||
|
|
||||||
|
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||||
|
`Your V2 PR with embedded architecture has been deployed!\n\n` +
|
||||||
|
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||||
|
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||||
|
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||||
|
`🔄 **Auto-deployed** for approved V2 contributors.`;
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanup-v2-deployment:
|
||||||
|
if: github.event.action == 'closed'
|
||||||
|
needs: pick
|
||||||
|
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
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: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Clean up V2 deployment comments
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = ${{ github.event.pull_request.number }};
|
||||||
|
|
||||||
|
// Find and delete V2 deployment comments
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const v2Comments = comments.filter(c =>
|
||||||
|
c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") &&
|
||||||
|
c.user?.type === "Bot"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const comment of v2Comments) {
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: comment.id
|
||||||
|
});
|
||||||
|
console.log(`Deleted V2 deployment comment (ID: ${comment.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Cleanup V2 deployment
|
||||||
|
run: |
|
||||||
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH'
|
||||||
|
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
|
||||||
|
echo "Found V2 PR directory, proceeding with cleanup..."
|
||||||
|
|
||||||
|
# Stop and remove V2 containers
|
||||||
|
cd /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||||
|
docker-compose down || true
|
||||||
|
|
||||||
|
# Go back to root before removal
|
||||||
|
cd /
|
||||||
|
|
||||||
|
# Remove V2 PR-specific directories
|
||||||
|
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
|
# Clean up V2 container by name (in case compose cleanup missed it)
|
||||||
|
docker rm -f stirling-pdf-v2-pr-${{ github.event.pull_request.number }} || true
|
||||||
|
|
||||||
|
echo "V2 cleanup completed"
|
||||||
|
else
|
||||||
|
echo "V2 PR directory not found, nothing to clean up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up old unused images (older than 2 weeks) but keep recent ones for reuse
|
||||||
|
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||||
|
|
||||||
|
# Note: We don't remove the commit-based images since they can be reused across PRs
|
||||||
|
# Only remove PR-specific containers and directories
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f ../private.key
|
||||||
|
continue-on-error: true
|
||||||
@@ -0,0 +1,656 @@
|
|||||||
|
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' }}
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
outputs:
|
||||||
|
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||||
|
comment_id: ${{ github.event.comment.id }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Get PR data
|
||||||
|
id: get-pr
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prNumber = context.eventName === 'workflow_dispatch'
|
||||||
|
? context.payload.inputs.pr_number
|
||||||
|
: context.payload.issue.number;
|
||||||
|
console.log(`PR Number: ${prNumber}`);
|
||||||
|
core.setOutput('pr_number', prNumber);
|
||||||
|
|
||||||
|
- name: Check for security/login flag
|
||||||
|
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
|
||||||
|
echo "disable_security=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "disable_security=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check for pro flag
|
||||||
|
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
|
||||||
|
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then
|
||||||
|
echo "enable_enterprise=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`);
|
||||||
|
try {
|
||||||
|
const { data: reaction } = await github.rest.reactions.createForIssueComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: context.payload.comment.id,
|
||||||
|
content: 'eyes'
|
||||||
|
});
|
||||||
|
console.log(`Added reaction with ID: ${reaction.id}`);
|
||||||
|
return { success: true, id: reaction.id };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add reaction: ${error.message}`);
|
||||||
|
console.error(error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy-pr:
|
||||||
|
needs: [pick, check-comment]
|
||||||
|
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
java-version: "25"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
|
- 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
|
||||||
|
export DISABLE_ADDITIONAL_FEATURES=true
|
||||||
|
else
|
||||||
|
export DISABLE_ADDITIONAL_FEATURES=false
|
||||||
|
fi
|
||||||
|
task backend: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
|
||||||
|
|
||||||
|
- 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: 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
|
||||||
|
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 }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Deploy to VPS
|
||||||
|
id: deploy
|
||||||
|
run: |
|
||||||
|
# Set security settings based on flags
|
||||||
|
if [ "${{ needs.check-comment.outputs.disable_security }}" == "false" ]; then
|
||||||
|
DISABLE_ADDITIONAL_FEATURES="false"
|
||||||
|
LOGIN_SECURITY="true"
|
||||||
|
SECURITY_STATUS="🔒 Security Enabled"
|
||||||
|
else
|
||||||
|
DISABLE_ADDITIONAL_FEATURES="true"
|
||||||
|
LOGIN_SECURITY="false"
|
||||||
|
SECURITY_STATUS="Security Disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set pro/enterprise settings (enterprise implies pro)
|
||||||
|
if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then
|
||||||
|
PREMIUM_ENABLED="true"
|
||||||
|
PREMIUM_KEY="${{ secrets.ENTERPRISE_KEY }}"
|
||||||
|
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
||||||
|
elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then
|
||||||
|
PREMIUM_ENABLED="true"
|
||||||
|
PREMIUM_KEY="${{ secrets.PREMIUM_KEY }}"
|
||||||
|
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
||||||
|
else
|
||||||
|
PREMIUM_ENABLED="false"
|
||||||
|
PREMIUM_KEY=""
|
||||||
|
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}
|
||||||
|
ports:
|
||||||
|
- "${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
|
||||||
|
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_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}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Then copy the file and execute commands
|
||||||
|
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Move docker-compose file to correct location
|
||||||
|
mv /tmp/docker-compose.yml /stirling/PR-${PR_NUMBER}/docker-compose.yml
|
||||||
|
|
||||||
|
# Start or restart the container
|
||||||
|
cd /stirling/PR-${PR_NUMBER}
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
# Set output for use in PR comment
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
||||||
|
try {
|
||||||
|
const { data: reaction } = await github.rest.reactions.createForIssueComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: ${{ needs.check-comment.outputs.comment_id }},
|
||||||
|
content: 'rocket'
|
||||||
|
});
|
||||||
|
console.log(`Added rocket reaction with ID: ${reaction.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add reaction: ${error.message}`);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add label to PR
|
||||||
|
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||||
|
try {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
labels: ['pr-deployed']
|
||||||
|
});
|
||||||
|
console.log(`Added 'pr-deployed' label to PR #${prNumber}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add label to PR: ${error.message}`);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Add failure reaction to comment
|
||||||
|
if: failure() && github.event_name == 'issue_comment'
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
||||||
|
try {
|
||||||
|
const { data: reaction } = await github.rest.reactions.createForIssueComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: ${{ needs.check-comment.outputs.comment_id }},
|
||||||
|
content: '-1'
|
||||||
|
});
|
||||||
|
console.log(`Added -1 reaction with ID: ${reaction.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add reaction: ${error.message}`);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Post deployment URL to PR
|
||||||
|
if: success()
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { GITHUB_REPOSITORY } = process.env;
|
||||||
|
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||||
|
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||||
|
const securityStatus = process.env.security_status || "Security Disabled";
|
||||||
|
|
||||||
|
const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${prNumber}`;
|
||||||
|
const commentBody = `## 🚀 PR Test Deployment\n\n` +
|
||||||
|
`Your PR has been deployed for testing!\n\n` +
|
||||||
|
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` +
|
||||||
|
`${securityStatus}\n\n` +
|
||||||
|
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "Cleaning up temporary files..."
|
||||||
|
rm -f ../private.key docker-compose.yml
|
||||||
|
echo "Cleanup complete."
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
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' }}
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Check out the repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Apply label commands
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { comment, issue } = context.payload;
|
||||||
|
const commentBody = comment?.body ?? '';
|
||||||
|
if (!commentBody.includes('::label::')) {
|
||||||
|
core.info('No label commands detected in comment.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'config', 'repo_devs.json');
|
||||||
|
const repoDevsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
const label_changer = (repoDevsConfig.label_changer || []).map((login) => login.toLowerCase());
|
||||||
|
|
||||||
|
const commenter = (comment?.user?.login || '').toLowerCase();
|
||||||
|
if (!label_changer.includes(commenter)) {
|
||||||
|
core.info(`User ${commenter} is not authorized to manage labels.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsConfigPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'labels.yml');
|
||||||
|
const labelsFile = fs.readFileSync(labelsConfigPath, 'utf8');
|
||||||
|
|
||||||
|
const labelNameMap = new Map();
|
||||||
|
for (const match of labelsFile.matchAll(/-\s+name:\s*(?:"([^"]+)"|'([^']+)'|([^\n]+))/g)) {
|
||||||
|
const labelName = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
||||||
|
|
||||||
|
if (!labelName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = labelName.toLowerCase();
|
||||||
|
if (!labelNameMap.has(normalized)) {
|
||||||
|
labelNameMap.set(normalized, labelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelNameMap.size) {
|
||||||
|
core.warning('No labels could be read from .github/labels.yml; aborting label commands.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowedLabelNames = new Set(labelNameMap.values());
|
||||||
|
|
||||||
|
const labelsToAdd = new Set();
|
||||||
|
const labelsToRemove = new Set();
|
||||||
|
const commandRegex = /^(\w+)::(label)::"([^"]+)"/gim;
|
||||||
|
let match;
|
||||||
|
while ((match = commandRegex.exec(commentBody)) !== null) {
|
||||||
|
core.info(`Found label command: ${match[0]} (action: ${match[1]}, label: ${match[2]}, labelName: ${match[3]})`);
|
||||||
|
const action = match[1].toLowerCase();
|
||||||
|
const labelName = match[3].trim();
|
||||||
|
|
||||||
|
if (!labelName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = labelName.toLowerCase();
|
||||||
|
const resolvedLabelName = labelNameMap.get(normalized);
|
||||||
|
if (action === 'add') {
|
||||||
|
if (!resolvedLabelName) {
|
||||||
|
core.warning(`Label "${labelName}" is not defined in .github/labels.yml and cannot be added.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!allowedLabelNames.has(resolvedLabelName)) {
|
||||||
|
core.warning(`Label "${resolvedLabelName}" is not allowed for add commands and will be skipped.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
labelsToAdd.add(resolvedLabelName);
|
||||||
|
} else if (action === 'rm') {
|
||||||
|
const labelToRemove = resolvedLabelName ?? labelName;
|
||||||
|
if (!resolvedLabelName) {
|
||||||
|
core.warning(`Label "${labelName}" is not defined in .github/labels.yml; attempting to remove as provided.`);
|
||||||
|
}
|
||||||
|
labelsToRemove.add(labelToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLabels = Array.from(labelsToAdd);
|
||||||
|
const removeLabels = Array.from(labelsToRemove);
|
||||||
|
|
||||||
|
if (!addLabels.length && !removeLabels.length) {
|
||||||
|
core.info('No valid label commands found after parsing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueParams = {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (addLabels.length) {
|
||||||
|
core.info(`Adding labels: ${addLabels.join(', ')}`);
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
...issueParams,
|
||||||
|
labels: addLabels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const labelName of removeLabels) {
|
||||||
|
core.info(`Removing label: ${labelName}`);
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
...issueParams,
|
||||||
|
name: labelName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
core.warning(`Label "${labelName}" was not present on the pull request.`);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: comment.id,
|
||||||
|
});
|
||||||
|
core.info('Processed label commands and deleted the comment.');
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
name: PR Deployment cleanup
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
SERVER_IP: ${{ secrets.NEW_VPS_IP }} # Add this to your GitHub secrets
|
||||||
|
CLEANUP_PERFORMED: "false" # Add flag to track if cleanup occurred
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
if: github.event.action == 'closed'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Remove 'pr-deployed' label if present
|
||||||
|
id: remove-label-comment
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const prNumber = ${{ github.event.pull_request.number }};
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
|
||||||
|
// Get all labels on the PR
|
||||||
|
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasLabel = labels.some(label => label.name === 'pr-deployed');
|
||||||
|
|
||||||
|
if (hasLabel) {
|
||||||
|
console.log("Label 'pr-deployed' found. Removing...");
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
name: 'pr-deployed'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Label 'pr-deployed' not found. Nothing to do.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing bot comments about the deployment
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber
|
||||||
|
});
|
||||||
|
const deploymentComments = comments.filter(c =>
|
||||||
|
c.body?.includes("## 🚀 PR Test Deployment") &&
|
||||||
|
c.user?.type === "Bot"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deploymentComments.length > 0) {
|
||||||
|
for (const comment of deploymentComments) {
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: comment.id
|
||||||
|
});
|
||||||
|
console.log(`Deleted deployment comment (ID: ${comment.id})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No matching deployment comments found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set flag if either label or comment was present
|
||||||
|
const hasDeploymentComment = deploymentComments.length > 0;
|
||||||
|
core.setOutput('present', (hasLabel || hasDeploymentComment) ? 'true' : 'false');
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
if: steps.remove-label-comment.outputs.present == 'true'
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Cleanup PR deployment
|
||||||
|
if: steps.remove-label-comment.outputs.present == 'true'
|
||||||
|
id: cleanup
|
||||||
|
run: |
|
||||||
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH'
|
||||||
|
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
||||||
|
echo "Found PR directory, proceeding with cleanup..."
|
||||||
|
|
||||||
|
# Stop and remove containers
|
||||||
|
cd /stirling/PR-${{ github.event.pull_request.number }}
|
||||||
|
docker-compose down || true
|
||||||
|
|
||||||
|
# Go back to root before removal
|
||||||
|
cd /
|
||||||
|
|
||||||
|
# Remove PR-specific directories
|
||||||
|
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
|
# Remove the Docker images
|
||||||
|
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
|
||||||
|
echo "PR directory not found, nothing to clean up"
|
||||||
|
echo "NO_CLEANUP_NEEDED"
|
||||||
|
fi
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "Cleaning up temporary files..."
|
||||||
|
rm -f ../private.key
|
||||||
|
echo "Cleanup complete."
|
||||||
|
continue-on-error: true
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
name: AI Engine CI
|
||||||
|
|
||||||
|
# Validates the Python AI engine: regenerates tool models and 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.
|
||||||
|
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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.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.5.1
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||||
|
|
||||||
|
- name: Regenerate tool models
|
||||||
|
run: task engine:tool-models
|
||||||
|
|
||||||
|
- name: Verify tool models are up to date
|
||||||
|
id: tool-models-check
|
||||||
|
continue-on-error: true
|
||||||
|
run: git diff --exit-code engine/src/stirling/models/tool_models.py
|
||||||
|
|
||||||
|
- name: Comment on tool 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.tool-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 = '<!-- tool-models-check -->';
|
||||||
|
const body = [
|
||||||
|
marker,
|
||||||
|
'### Tool Models Check Failed',
|
||||||
|
'',
|
||||||
|
'The generated `engine/src/stirling/models/tool_models.py` is out of date with the Java OpenAPI spec and will need to be regenerated before it can be merged in.',
|
||||||
|
'',
|
||||||
|
'Run `task engine:tool-models` to regenerate, then commit the updated file.',
|
||||||
|
].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 tool models check failed
|
||||||
|
if: steps.tool-models-check.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
echo "============================================"
|
||||||
|
echo " Tool Models Check Failed"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
echo "The generated engine/src/stirling/models/tool_models.py"
|
||||||
|
echo "is out of date with the Java OpenAPI spec and will"
|
||||||
|
echo "need to be regenerated before it can be merged in."
|
||||||
|
echo ""
|
||||||
|
echo "Run 'task engine:tool-models' to regenerate, then"
|
||||||
|
echo "commit the updated file."
|
||||||
|
echo "============================================"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Remove tool models check comment on success
|
||||||
|
if: steps.tool-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 = '<!-- tool-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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
name: AI - PR Title Review
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions: # required for secure-repo hardening
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ai-title-review:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
models: read
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git to suppress detached HEAD warning
|
||||||
|
run: git config --global advice.detachedHead false
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Check if actor is repo developer
|
||||||
|
id: actor
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.actor }}" == *"[bot]" ]]; then
|
||||||
|
echo "PR opened by a bot – skipping AI title review."
|
||||||
|
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ ! -f .github/config/repo_devs.json ]; then
|
||||||
|
echo "Error: .github/config/repo_devs.json not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Validate JSON and extract repo_devs
|
||||||
|
REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; })
|
||||||
|
# Convert developer list into Bash array
|
||||||
|
mapfile -t DEVS_ARRAY <<< "$REPO_DEVS"
|
||||||
|
if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then
|
||||||
|
echo "is_repo_dev=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get PR diff
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
id: get_diff
|
||||||
|
run: |
|
||||||
|
git fetch origin ${{ github.base_ref }}
|
||||||
|
git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff
|
||||||
|
echo "diff<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat pr.diff >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check and sanitize PR title
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
id: sanitize_pr_title
|
||||||
|
env:
|
||||||
|
PR_TITLE_RAW: ${{ github.event.pull_request.title }}
|
||||||
|
run: |
|
||||||
|
# Sanitize PR title: max 72 characters, only printable characters
|
||||||
|
PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g')
|
||||||
|
if [[ ${#PR_TITLE} -lt 5 ]]; then
|
||||||
|
echo "PR title is too short. Must be at least 5 characters." >&2
|
||||||
|
fi
|
||||||
|
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: AI PR Title Analysis
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
id: ai-title-analysis
|
||||||
|
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
|
||||||
|
with:
|
||||||
|
model: openai/gpt-4o
|
||||||
|
system-prompt-file: ".github/config/system-prompt.txt"
|
||||||
|
prompt: |
|
||||||
|
Based on the following input data:
|
||||||
|
|
||||||
|
{
|
||||||
|
"diff": "${{ steps.get_diff.outputs.diff }}",
|
||||||
|
"pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Respond ONLY with valid JSON in the format:
|
||||||
|
{
|
||||||
|
"improved_rating": <0-10>,
|
||||||
|
"improved_ai_title_rating": <0-10>,
|
||||||
|
"improved_title": "<ai generated title>"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Validate and set SCRIPT_OUTPUT
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
run: |
|
||||||
|
cat <<EOF > ai_response.json
|
||||||
|
${{ steps.ai-title-analysis.outputs.response }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Validate JSON structure
|
||||||
|
jq -e '
|
||||||
|
(keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and
|
||||||
|
(.improved_rating | type == "number" and . >= 0 and . <= 10) and
|
||||||
|
(.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and
|
||||||
|
(.improved_title | type == "string")
|
||||||
|
' ai_response.json
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Invalid AI response format" >&2
|
||||||
|
cat ai_response.json >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Parse JSON fields
|
||||||
|
IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json)
|
||||||
|
IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json)
|
||||||
|
# Limit comment length to 1000 characters
|
||||||
|
COMMENT=$(cat <<EOF
|
||||||
|
## 🤖 AI PR Title Suggestion
|
||||||
|
|
||||||
|
**PR-Title Rating**: $IMPROVED_RATING/10
|
||||||
|
|
||||||
|
### ⬇️ Suggested Title (copy & paste):
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
$IMPROVED_TITLE
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by GitHub Models AI*
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
echo "$COMMENT" > /tmp/ai-title-comment.md
|
||||||
|
# Log input and output to the GitHub Step Summary
|
||||||
|
echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat ai_response.json >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Post comment on PR if needed
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8');
|
||||||
|
const { GITHUB_REPOSITORY } = process.env;
|
||||||
|
const [owner, repo] = GITHUB_REPOSITORY.split('/');
|
||||||
|
const issue_number = context.issue.number;
|
||||||
|
|
||||||
|
const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/);
|
||||||
|
const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null;
|
||||||
|
|
||||||
|
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||||
|
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
|
||||||
|
|
||||||
|
const existing = comments.data.find(c =>
|
||||||
|
c.user?.login === expectedActor &&
|
||||||
|
c.body.includes("## 🤖 AI PR Title Suggestion")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rating === null) {
|
||||||
|
console.log("No rating found in AI response – skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rating <= 5) {
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner, repo,
|
||||||
|
comment_id: existing.id,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
console.log("Updated existing suggestion comment.");
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner, repo, issue_number,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
console.log("Created new suggestion comment.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner, repo,
|
||||||
|
comment_id: existing.id,
|
||||||
|
body: praise
|
||||||
|
});
|
||||||
|
console.log("Replaced suggestion with praise.");
|
||||||
|
} else {
|
||||||
|
console.log("Rating > 5 and no existing comment – skipping comment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: is not repo dev
|
||||||
|
if: steps.actor.outputs.is_repo_dev != 'true'
|
||||||
|
run: |
|
||||||
|
exit 0 # Skip the AI title review for non-repo developers
|
||||||
|
|
||||||
|
- name: Clean up
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f pr.diff ai_response.json /tmp/ai-title-comment.md
|
||||||
|
echo "Cleaned up temporary files."
|
||||||
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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: [email protected]
|
||||||
|
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: [email protected]
|
||||||
|
# ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
# commit_message: "Update to v${{ needs.get-release-info.outputs.version }}"
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
name: "Pull Request Labeler"
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
labeler:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/labeler@v5
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
configuration-path: .github/labeler-config.yml
|
|
||||||
sync-labels: true
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: "Auto Pull Request Labeler V2"
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- V3
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
labeler:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0
|
||||||
|
with:
|
||||||
|
config_path: .github/labeler-config-srvaroa.yml
|
||||||
|
use_local_config: false
|
||||||
|
fail_on_error: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: "${{ steps.setup-bot.outputs.token }}"
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
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.5.1
|
||||||
|
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
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
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 three
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Auto-skipped when secrets.PREMIUM_KEY_ENTERPRISE is missing (forks, dependabot).
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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: Upload Playwright report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: playwright-report-enterprise-${{ github.run_id }}
|
||||||
|
path: frontend/editor/playwright-report/
|
||||||
|
retention-days: 7
|
||||||
+253
-19
@@ -1,36 +1,270 @@
|
|||||||
name: "Build repo"
|
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:
|
on:
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
files-changed:
|
||||||
|
name: detect what files changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 3
|
||||||
|
outputs:
|
||||||
|
build: ${{ steps.changes.outputs.build }}
|
||||||
|
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 }}
|
||||||
|
proprietary: ${{ steps.changes.outputs.proprietary }}
|
||||||
|
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: Check for file changes
|
||||||
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: .github/config/.files.yaml
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: [files-changed]
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
|
pull-requests: write
|
||||||
|
uses: ./.github/workflows/backend-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
strategy:
|
db-migration-test:
|
||||||
fail-fast: false
|
# 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
|
||||||
|
|
||||||
|
check-generateOpenApiDocs:
|
||||||
|
if: needs.files-changed.outputs.openapi == 'true'
|
||||||
|
needs: [files-changed]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/check-openapi.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
check-licence:
|
||||||
|
if: needs.files-changed.outputs.build == 'true'
|
||||||
|
needs: [files-changed, build]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/check-licence.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
- pre-commit
|
||||||
|
- dependency-review
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: actions/checkout@v4
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
egress-policy: audit
|
||||||
distribution: "temurin"
|
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- name: Verify every required job passed (or was legitimately skipped)
|
||||||
with:
|
env:
|
||||||
gradle-version: 8.7
|
RESULTS: |
|
||||||
|
files-changed=${{ needs.files-changed.result }}
|
||||||
- name: Build with Gradle
|
build=${{ needs.build.result }}
|
||||||
run: ./gradlew build --no-build-cache
|
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 }}
|
||||||
|
pre-commit=${{ needs.pre-commit.result }}
|
||||||
|
dependency-review=${{ needs.dependency-review.result }}
|
||||||
|
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."
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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.5.1
|
||||||
|
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
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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.5.1
|
||||||
|
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
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
name: Check TOML Translation Files on PR
|
||||||
|
|
||||||
|
# This workflow validates TOML translation files
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
paths:
|
||||||
|
- "frontend/editor/public/locales/*/translation.toml"
|
||||||
|
- ".github/scripts/check_language_toml.py"
|
||||||
|
- ".github/workflows/check_toml.yml"
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # Allow read access to repository content
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-files:
|
||||||
|
if: github.event_name == 'pull_request_target'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write # Allow posting comments on issues/PRs
|
||||||
|
pull-requests: write # Allow writing to pull requests
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout main branch first
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Get PR data
|
||||||
|
id: get-pr-data
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const repoOwner = context.payload.repository.owner.login;
|
||||||
|
const repoName = context.payload.repository.name;
|
||||||
|
const branch = context.payload.pull_request.head.ref;
|
||||||
|
|
||||||
|
console.log(`PR Number: ${prNumber}`);
|
||||||
|
console.log(`Repo Owner: ${repoOwner}`);
|
||||||
|
console.log(`Repo Name: ${repoName}`);
|
||||||
|
console.log(`Branch: ${branch}`);
|
||||||
|
|
||||||
|
core.setOutput("pr_number", prNumber);
|
||||||
|
core.setOutput("repo_owner", repoOwner);
|
||||||
|
core.setOutput("repo_name", repoName);
|
||||||
|
core.setOutput("branch", branch);
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Fetch PR changed files
|
||||||
|
id: fetch-pr-changes
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
run: |
|
||||||
|
echo "Fetching PR changed files..."
|
||||||
|
echo "Getting list of changed files from PR..."
|
||||||
|
# Check if PR number exists
|
||||||
|
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
|
||||||
|
echo "Error: PR number is empty"
|
||||||
|
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"
|
||||||
|
# Check if any files were found
|
||||||
|
if [ ! -s changed_files.txt ]; then
|
||||||
|
echo "No TOML translation files changed in this PR"
|
||||||
|
echo "Workflow will exit early as no relevant files to check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Found $(wc -l < changed_files.txt) matching TOML files"
|
||||||
|
|
||||||
|
- name: Determine reference file
|
||||||
|
id: determine-file
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const prNumber = ${{ steps.get-pr-data.outputs.pr_number }};
|
||||||
|
const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}";
|
||||||
|
const repoName = "${{ steps.get-pr-data.outputs.repo_name }}";
|
||||||
|
|
||||||
|
const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}";
|
||||||
|
const prRepoName = "${{ github.event.pull_request.head.repo.name }}";
|
||||||
|
const branch = "${{ steps.get-pr-data.outputs.branch }}";
|
||||||
|
|
||||||
|
console.log(`Determining reference file for PR #${prNumber}`);
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
const validateInput = (input, regex, name) => {
|
||||||
|
if (!regex.test(input)) {
|
||||||
|
throw new Error(`Invalid ${name}: ${input}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner");
|
||||||
|
validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name");
|
||||||
|
validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name");
|
||||||
|
|
||||||
|
// Get the list of changed files in the PR
|
||||||
|
const { data: files } = await github.rest.pulls.listFiles({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
pull_number: prNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter for relevant TOML files based on the PR changes
|
||||||
|
const changedFiles = files
|
||||||
|
.filter(file =>
|
||||||
|
file.status !== "removed" &&
|
||||||
|
/^frontend\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename)
|
||||||
|
)
|
||||||
|
.map(file => file.filename);
|
||||||
|
|
||||||
|
console.log("Changed files:", changedFiles);
|
||||||
|
|
||||||
|
// Create a temporary directory for PR files
|
||||||
|
const tempDir = "pr-branch";
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and save each changed file
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: prRepoOwner,
|
||||||
|
repo: prRepoName,
|
||||||
|
path: file,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
const filePath = path.join(tempDir, file);
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
console.log(`Saved file: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the list of changed files for further processing
|
||||||
|
const fileList = changedFiles.join(" ");
|
||||||
|
core.exportVariable("FILES_LIST", fileList);
|
||||||
|
console.log("Files saved and listed in FILES_LIST.");
|
||||||
|
|
||||||
|
// Determine reference file
|
||||||
|
let referenceFilePath;
|
||||||
|
if (changedFiles.includes("frontend/editor/public/locales/en-US/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",
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
referenceFilePath = "pr-branch-translation-en-US.toml";
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
|
} else {
|
||||||
|
console.log("Using main branch reference file.");
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||||
|
ref: "main",
|
||||||
|
});
|
||||||
|
|
||||||
|
referenceFilePath = "main-branch-translation-en-US.toml";
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Reference file path: ${referenceFilePath}`);
|
||||||
|
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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
|
||||||
|
|
||||||
|
- name: Run Python script to check files
|
||||||
|
id: run-check
|
||||||
|
run: |
|
||||||
|
echo "Running Python script to check TOML files..."
|
||||||
|
python .github/scripts/check_language_toml.py \
|
||||||
|
--actor ${{ github.event.pull_request.user.login }} \
|
||||||
|
--reference-file "${REFERENCE_FILE}" \
|
||||||
|
--branch "pr-branch" \
|
||||||
|
--files "${FILES_LIST[@]}" > result.txt
|
||||||
|
continue-on-error: true # Continue the job even if this step fails
|
||||||
|
|
||||||
|
- name: Capture output
|
||||||
|
id: capture-output
|
||||||
|
run: |
|
||||||
|
if [ -f result.txt ] && [ -s result.txt ]; then
|
||||||
|
echo "Capturing output..."
|
||||||
|
SCRIPT_OUTPUT=$(cat result.txt)
|
||||||
|
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
echo "${SCRIPT_OUTPUT}"
|
||||||
|
|
||||||
|
# Determine job failure based on script output
|
||||||
|
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
|
||||||
|
echo "FAIL_JOB=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No output found."
|
||||||
|
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
|
||||||
|
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post comment on PR
|
||||||
|
if: env.SCRIPT_OUTPUT != ''
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||||
|
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||||
|
const issueNumber = context.issue.number;
|
||||||
|
|
||||||
|
// Find existing comment
|
||||||
|
const comments = await github.rest.issues.listComments({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
issue_number: issueNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const comment = comments.data.find(c => c.body.includes("## 🌐 TOML Translation Verification Summary"));
|
||||||
|
|
||||||
|
// Only update or create comments by the action user
|
||||||
|
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||||
|
|
||||||
|
if (comment && comment.user.login === expectedActor) {
|
||||||
|
// Update existing comment
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
comment_id: comment.id,
|
||||||
|
body: `## 🌐 TOML Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||||
|
});
|
||||||
|
console.log("Updated existing comment.");
|
||||||
|
} else if (!comment) {
|
||||||
|
// Create new comment if no existing comment is found
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body: `## 🌐 TOML Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||||
|
});
|
||||||
|
console.log("Created new comment.");
|
||||||
|
} else {
|
||||||
|
console.log("Comment update attempt denied. Actor does not match.");
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Fail job if errors found
|
||||||
|
if: env.FAIL_JOB == 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failing the job because errors were detected."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
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
|
||||||
|
echo "Cleanup complete."
|
||||||
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
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:
|
||||||
|
|
||||||
|
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.3.1
|
||||||
|
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 so a backend-only PR still produces the matrix with
|
||||||
|
# just backend rows populated.
|
||||||
|
if: always()
|
||||||
|
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 as `playwright-frontend-coverage-<run_id>`.
|
||||||
|
# Same tolerance as vitest - matrix script handles missing inputs.
|
||||||
|
if: always()
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||||
|
with:
|
||||||
|
name: playwright-frontend-coverage-${{ github.run_id }}
|
||||||
|
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
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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.5.1
|
||||||
|
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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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: "Dependency Review"
|
||||||
|
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||||
|
with:
|
||||||
|
config-file: "./.github/config/dependency-review-config.yml"
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
name: Auto V2 Deploy on Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- V2
|
||||||
|
- deploy-on-v2-commit
|
||||||
|
|
||||||
|
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' }}
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: env.USE_DEPOT != 'true'
|
||||||
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
|
- name: Get commit hashes for frontend and backend
|
||||||
|
id: commit-hashes
|
||||||
|
run: |
|
||||||
|
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||||
|
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||||
|
if [ -z "$FRONTEND_HASH" ]; then
|
||||||
|
FRONTEND_HASH="no-frontend-changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||||
|
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||||
|
if [ -z "$BACKEND_HASH" ]; then
|
||||||
|
BACKEND_HASH="no-backend-changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Frontend hash: $FRONTEND_HASH"
|
||||||
|
echo "Backend hash: $BACKEND_HASH"
|
||||||
|
|
||||||
|
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||||
|
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Short hashes for tags
|
||||||
|
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||||
|
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||||
|
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if frontend image exists
|
||||||
|
id: check-frontend
|
||||||
|
run: |
|
||||||
|
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Frontend image already exists, skipping build"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Frontend image needs to be built"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if backend image exists
|
||||||
|
id: check-backend
|
||||||
|
run: |
|
||||||
|
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Backend image already exists, skipping build"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Backend image needs to be built"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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
|
||||||
|
with:
|
||||||
|
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||||
|
context: .
|
||||||
|
file: ./docker/frontend/Dockerfile
|
||||||
|
push: true
|
||||||
|
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 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
|
||||||
|
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
|
||||||
|
build-args: VERSION_TAG=v2-alpha
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Deploy to VPS on port 3000
|
||||||
|
run: |
|
||||||
|
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||||
|
|
||||||
|
cat > $UNIQUE_NAME << EOF
|
||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
container_name: stirling-v2-backend
|
||||||
|
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||||
|
ports:
|
||||||
|
- "13000:8080"
|
||||||
|
volumes:
|
||||||
|
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||||
|
- /stirling/V2/config:/configs:rw
|
||||||
|
- /stirling/V2/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: "Stirling-PDF V2"
|
||||||
|
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||||
|
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||||
|
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
|
||||||
|
baseUrl: "https://demo.stirlingpdf.cloud"
|
||||||
|
restart: on-failure:5
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: stirling-v2-frontend
|
||||||
|
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
environment:
|
||||||
|
VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:13000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: on-failure:5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Copy to remote with unique name
|
||||||
|
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||||
|
|
||||||
|
# SSH and rename/move atomically to avoid interference
|
||||||
|
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
|
||||||
|
mkdir -p /stirling/V2/{data,config,logs}
|
||||||
|
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||||
|
cd /stirling/V2
|
||||||
|
docker-compose down || true
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f ../private.key
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
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.5.1
|
||||||
|
cache-disabled: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
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
|
||||||
|
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
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
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"
|
||||||
|
run: task e2e:live
|
||||||
|
- 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-${{ github.run_id }}
|
||||||
|
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
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
|
run: task e2e:stubbed -- --workers=3
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: playwright-report-stubbed-${{ github.run_id }}
|
||||||
|
path: frontend/editor/playwright-report/
|
||||||
|
retention-days: 7
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
name: License Report Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
merge_group:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
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' }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Check for file changes
|
||||||
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: .github/config/.files.yaml
|
||||||
|
|
||||||
|
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' }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout PR head (default)
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)) && github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout BASE branch (safe script)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
path: base
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- 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 frontend dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
env:
|
||||||
|
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 (internal PR)
|
||||||
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||||
|
env:
|
||||||
|
PR_IS_FORK: "false"
|
||||||
|
run: task frontend:licenses:generate
|
||||||
|
|
||||||
|
- name: Generate frontend license report (fork PRs, pinned)
|
||||||
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||||
|
env:
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check for license warnings
|
||||||
|
run: |
|
||||||
|
if [ -f "frontend/editor/src/assets/license-warnings.json" ]; then
|
||||||
|
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = context.issue.number;
|
||||||
|
|
||||||
|
// Get all comments on the PR
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
per_page: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter for license check comments
|
||||||
|
const licenseComments = comments.filter(comment =>
|
||||||
|
comment.body.includes('## ✅ Frontend License Check Passed') ||
|
||||||
|
comment.body.includes('## ❌ Frontend License Check Failed')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete old license check comments
|
||||||
|
for (const comment of licenseComments) {
|
||||||
|
console.log(`Deleting old license check comment: ${comment.id}`);
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: comment.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Summarize results (fork PRs)
|
||||||
|
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) || github.actor == 'dependabot[bot]'
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "## Frontend License Check"
|
||||||
|
echo ""
|
||||||
|
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
|
||||||
|
echo "❌ **Failed** – incompatible or unknown licenses found."
|
||||||
|
if [ -f "frontend/editor/src/assets/license-warnings.json" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "### Warnings"
|
||||||
|
jq -r '.warnings[] | "- \(.message)"' frontend/editor/src/assets/license-warnings.json || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "✅ **Passed** – no license warnings detected."
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "_Note: This is a fork PR. PR comments are disabled; use this summary._"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = context.issue.number;
|
||||||
|
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
|
||||||
|
|
||||||
|
let commentBody;
|
||||||
|
|
||||||
|
if (hasWarnings) {
|
||||||
|
// Read warnings file to get specific issues
|
||||||
|
const fs = require('fs');
|
||||||
|
let warningDetails = '';
|
||||||
|
try {
|
||||||
|
const warnings = JSON.parse(fs.readFileSync('frontend/editor/src/assets/license-warnings.json', 'utf8'));
|
||||||
|
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
|
||||||
|
} catch (e) {
|
||||||
|
warningDetails = 'Unable to read warning details';
|
||||||
|
}
|
||||||
|
|
||||||
|
commentBody = `## ❌ Frontend License Check Failed
|
||||||
|
|
||||||
|
The frontend license check has detected compatibility warnings that require review:
|
||||||
|
|
||||||
|
${warningDetails}
|
||||||
|
|
||||||
|
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
||||||
|
|
||||||
|
_This check will fail the PR until license issues are resolved._`;
|
||||||
|
} else {
|
||||||
|
commentBody = `## ✅ Frontend License Check Passed
|
||||||
|
|
||||||
|
All frontend licenses have been validated and no compatibility warnings were detected.
|
||||||
|
|
||||||
|
The frontend license report has been updated successfully.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Fail workflow if license warnings exist (PR only)
|
||||||
|
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||||
|
run: |
|
||||||
|
echo "❌ License warnings detected. Failing the workflow."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# Push Event: Commit license files and create PR
|
||||||
|
- name: Commit changes (Push only)
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
git add frontend/editor/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
|
||||||
|
|
||||||
|
- name: Prepare PR body (Push only)
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||||
|
|
||||||
|
This PR updates the frontend license report based on changes to package.json dependencies."
|
||||||
|
|
||||||
|
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
|
||||||
|
PR_BODY="$PR_BODY
|
||||||
|
|
||||||
|
## ⚠️ License Compatibility Warnings
|
||||||
|
|
||||||
|
The following licenses may require review for corporate compatibility:
|
||||||
|
|
||||||
|
$(cat frontend/editor/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||||
|
|
||||||
|
Please review these licenses to ensure they are acceptable for your use case."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$PR_BODY" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- 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
|
||||||
|
with:
|
||||||
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
commit-message: "Update Frontend 3rd Party Licenses"
|
||||||
|
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||||
|
author: ${{ steps.setup-bot.outputs.committer }}
|
||||||
|
signoff: true
|
||||||
|
branch: update-frontend-3rd-party-licenses
|
||||||
|
base: main
|
||||||
|
title: "Update Frontend 3rd Party Licenses"
|
||||||
|
body: ${{ env.PR_BODY }}
|
||||||
|
labels: Licenses,github-actions,frontend
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
|
sign-commits: true
|
||||||
|
|
||||||
|
- name: Enable Pull Request Automerge (Push only)
|
||||||
|
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||||
|
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
|
||||||
|
- name: Add review required label (Push only)
|
||||||
|
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||||
|
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
|
||||||
|
generate-backend-license-report:
|
||||||
|
if: needs.files-changed.outputs.licenses-backend == 'true'
|
||||||
|
needs: [pick, files-changed]
|
||||||
|
name: Generate Backend License Report
|
||||||
|
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)) && github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
java-version: "25"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Check for license compatibility issues
|
||||||
|
run: |
|
||||||
|
if [ -f build/reports/dependency-license/dependencies-without-allowed-license.json ] && \
|
||||||
|
jq '.dependenciesWithoutAllowedLicenses | length > 0' build/reports/dependency-license/dependencies-without-allowed-license.json | grep -q true; then
|
||||||
|
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
- name: Upload artifact on license issues
|
||||||
|
if: env.LICENSE_WARNINGS_EXIST == 'true'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: backend-dependencies-without-allowed-license.json
|
||||||
|
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||||
|
|
||||||
|
- name: Move license file
|
||||||
|
if: env.LICENSE_CHECK_FAILED != 'true' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||||
|
run: |
|
||||||
|
mkdir -p app/core/src/main/resources/static
|
||||||
|
cp build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json
|
||||||
|
|
||||||
|
- 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
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const prNumber = context.issue.number;
|
||||||
|
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
per_page: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
const backendLicenseComments = comments.filter(comment =>
|
||||||
|
comment.body.includes('## ✅ Backend License Check Passed') ||
|
||||||
|
comment.body.includes('## ❌ Backend License Check Failed')
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const comment of backendLicenseComments) {
|
||||||
|
console.log(`Deleting old backend license comment: ${comment.id}`);
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: comment.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
|
||||||
|
const fs = require('fs');
|
||||||
|
let warningDetails = '';
|
||||||
|
|
||||||
|
if (hasWarnings) {
|
||||||
|
try {
|
||||||
|
const warningsFile = 'build/reports/dependency-license/dependencies-without-allowed-license.json';
|
||||||
|
if (fs.existsSync(warningsFile)) {
|
||||||
|
const data = JSON.parse(fs.readFileSync(warningsFile, 'utf8'));
|
||||||
|
if (data.length > 0) {
|
||||||
|
warningDetails = data.map(dep => `- **${dep.moduleName}@${dep.moduleVersion}** – ${dep.moduleLicenses.map(l => l.licenseName).join(', ')}`).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
warningDetails = 'Unable to parse warning details.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let commentBody;
|
||||||
|
if (hasWarnings) {
|
||||||
|
commentBody = `## ❌ Backend License Check Failed
|
||||||
|
|
||||||
|
The backend license check has detected dependencies with incompatible or unallowed licenses:
|
||||||
|
|
||||||
|
${warningDetails || 'See uploaded artifact for details.'}
|
||||||
|
|
||||||
|
**Action Required:** Please review these licenses and resolve before merging.
|
||||||
|
|
||||||
|
_This check will fail the PR until license issues are resolved._`;
|
||||||
|
} else {
|
||||||
|
commentBody = `## ✅ Backend License Check Passed
|
||||||
|
|
||||||
|
All backend dependencies have valid and allowed licenses.
|
||||||
|
|
||||||
|
The backend license report has been updated successfully.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Fail workflow if license warnings exist (PR only)
|
||||||
|
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||||
|
run: |
|
||||||
|
echo "❌ Backend license warnings detected. Failing the workflow."
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Commit changes (push only)
|
||||||
|
if: github.event_name == 'push' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||||
|
run: |
|
||||||
|
git config user.name "${{ steps.setup-bot.outputs.committer }}"
|
||||||
|
git config user.email "${{ steps.setup-bot.outputs.committer-email || '[email protected]' }}"
|
||||||
|
git add app/core/src/main/resources/static/3rdPartyLicenses.json
|
||||||
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Prepare PR body (push only)
|
||||||
|
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||||
|
run: |
|
||||||
|
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||||
|
|
||||||
|
This PR updates the backend license report based on dependency changes."
|
||||||
|
|
||||||
|
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
|
||||||
|
PR_BODY="$PR_BODY
|
||||||
|
|
||||||
|
## ⚠️ License Compatibility Warnings
|
||||||
|
|
||||||
|
Incompatible licenses detected – manual review required before merge."
|
||||||
|
fi
|
||||||
|
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$PR_BODY" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- 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
|
||||||
|
with:
|
||||||
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
commit-message: "Update Backend 3rd Party Licenses"
|
||||||
|
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||||
|
author: ${{ steps.setup-bot.outputs.committer }}
|
||||||
|
signoff: true
|
||||||
|
branch: update-backend-3rd-party-licenses
|
||||||
|
base: main
|
||||||
|
title: "Update Backend 3rd Party Licenses"
|
||||||
|
body: ${{ env.PR_BODY }}
|
||||||
|
labels: Licenses,github-actions,backend
|
||||||
|
delete-branch: true
|
||||||
|
sign-commits: true
|
||||||
|
|
||||||
|
- name: Enable Pull Request Automerge (push only, no warnings)
|
||||||
|
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||||
|
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
|
||||||
|
- name: Add review required label (push only, with warnings)
|
||||||
|
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||||
|
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
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
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
name: License Report Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "build.gradle"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
generate-license-report:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "adopt"
|
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
|
||||||
|
|
||||||
- name: Run Gradle Command
|
|
||||||
run: ./gradlew clean generateLicenseReport
|
|
||||||
|
|
||||||
- name: Move and Rename License File
|
|
||||||
run: |
|
|
||||||
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
|
|
||||||
|
|
||||||
- name: Set up git config
|
|
||||||
run: |
|
|
||||||
git config --global user.email "GitHub Action <[email protected]>"
|
|
||||||
git config --global user.name "GitHub Action <[email protected]>"
|
|
||||||
|
|
||||||
- name: Run git add
|
|
||||||
run: |
|
|
||||||
git add src/main/resources/static/3rdPartyLicenses.json
|
|
||||||
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@v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
commit-message: "Update 3rd Party Licenses"
|
|
||||||
committer: GitHub Action <[email protected]>
|
|
||||||
author: GitHub Action <[email protected]>
|
|
||||||
signoff: true
|
|
||||||
branch: update-3rd-party-licenses
|
|
||||||
title: "Update 3rd Party Licenses"
|
|
||||||
body: |
|
|
||||||
Auto-generated by [create-pull-request][1]
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
|
||||||
draft: false
|
|
||||||
delete-branch: true
|
|
||||||
@@ -6,18 +6,24 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
name: Labeler
|
name: Labeler
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Run Labeler
|
- name: Run Labeler
|
||||||
uses: crazy-max/ghaction-github-labeler@v5
|
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
yaml-file: .github/labels.yml
|
yaml-file: .github/labels.yml
|
||||||
|
|||||||
@@ -0,0 +1,817 @@
|
|||||||
|
name: Multi-OS Tauri Releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
test_mode:
|
||||||
|
description: "Run in test mode (skip release step)"
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
platform:
|
||||||
|
description: "Platform to build (windows, macos, linux, or all)"
|
||||||
|
required: true
|
||||||
|
default: "all"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- windows
|
||||||
|
- macos
|
||||||
|
- linux
|
||||||
|
sign:
|
||||||
|
description: "Code sign the binaries (requires signing secrets)"
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
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' }}
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- 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 dependencies
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
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.5.1
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
"macos")
|
||||||
|
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $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":"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
|
||||||
|
;;
|
||||||
|
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
|
||||||
|
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 }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
variant:
|
||||||
|
- name: "default"
|
||||||
|
disable_security: true
|
||||||
|
build_frontend: true
|
||||||
|
file_suffix: ""
|
||||||
|
- name: "with-login"
|
||||||
|
disable_security: false
|
||||||
|
build_frontend: true
|
||||||
|
file_suffix: "-with-login"
|
||||||
|
- name: "server-only"
|
||||||
|
disable_security: true
|
||||||
|
build_frontend: false
|
||||||
|
file_suffix: "-server"
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- 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: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: matrix.variant.build_frontend == true
|
||||||
|
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: Build JAR
|
||||||
|
run: ./gradlew 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
|
||||||
|
|
||||||
|
- name: Rename JAR
|
||||||
|
run: |
|
||||||
|
echo "Version from determine-matrix: ${{ needs.determine-matrix.outputs.version }}"
|
||||||
|
echo "Looking for: app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar"
|
||||||
|
ls -la app/core/build/libs/
|
||||||
|
mkdir -p ./jar-dist
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
name: jar${{ matrix.variant.file_suffix }}
|
||||||
|
path: ./jar-dist/*.jar
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: determine-matrix
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||||
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
|
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||||
|
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
targets: ${{ matrix.platform == 'macos-15' && '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
|
||||||
|
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"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
env:
|
||||||
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||||
|
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||||
|
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
|
||||||
|
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||||
|
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') }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||||
|
|
||||||
|
# Decode client certificate
|
||||||
|
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
|
||||||
|
$certPath = "D:\Certificate_pkcs12.p12"
|
||||||
|
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
# Get PKCS11 config path from DigiCert action
|
||||||
|
$pkcs11Config = $env:PKCS11_CONFIG
|
||||||
|
if ($pkcs11Config) {
|
||||||
|
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
|
||||||
|
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
|
||||||
|
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
|
||||||
|
if (Test-Path $defaultPath) {
|
||||||
|
Write-Host "Found config at default path: $defaultPath"
|
||||||
|
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
Write-Host "Warning: Could not find PKCS11 config file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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') }}
|
||||||
|
env:
|
||||||
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
|
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
if ($env:WINDOWS_CERTIFICATE) {
|
||||||
|
Write-Host "Importing Windows Code Signing Certificate..."
|
||||||
|
|
||||||
|
# Decode base64 certificate and save to file
|
||||||
|
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
|
||||||
|
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
|
||||||
|
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||||
|
|
||||||
|
# Import certificate to CurrentUser\My store
|
||||||
|
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
|
||||||
|
|
||||||
|
# Extract and set thumbprint as environment variable
|
||||||
|
$thumbprint = $cert.Thumbprint
|
||||||
|
Write-Host "Certificate imported with thumbprint: $thumbprint"
|
||||||
|
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
# Clean up certificate file
|
||||||
|
Remove-Item $certPath
|
||||||
|
|
||||||
|
Write-Host "Windows certificate import completed."
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Import Apple Developer Certificate
|
||||||
|
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "Importing Apple Developer Certificate..."
|
||||||
|
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||||
|
# Create temporary keychain
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
# Import certificate
|
||||||
|
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
# Clean up
|
||||||
|
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')
|
||||||
|
run: |
|
||||||
|
echo "Verifying Apple Developer Certificate..."
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
|
||||||
|
echo "Certificate Info: $CERT_INFO"
|
||||||
|
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||||
|
echo "Certificate ID: $CERT_ID"
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
||||||
|
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 }}
|
||||||
|
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 }}
|
||||||
|
CI: true
|
||||||
|
with:
|
||||||
|
projectPath: ./frontend/editor
|
||||||
|
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') }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$allSigned = $true
|
||||||
|
|
||||||
|
# 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/"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
} 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"
|
||||||
|
|
||||||
|
# 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 ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "smctl log directory not found at $logDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
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.
|
||||||
|
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" \;
|
||||||
|
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" \;
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
if: always() && steps.digicert-setup.conclusion != 'failure'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
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' }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
pattern: Stirling-PDF-*
|
||||||
|
path: ./artifacts/tauri
|
||||||
|
|
||||||
|
- name: Download JAR artifact (default)
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: jar
|
||||||
|
path: ./artifacts/jars
|
||||||
|
|
||||||
|
- name: Download JAR artifact (with login)
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: jar-with-login
|
||||||
|
path: ./artifacts/jars
|
||||||
|
|
||||||
|
- name: Download JAR artifact (server only)
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: jar-server
|
||||||
|
path: ./artifacts/jars
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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: 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-nightly-${{ github.run_id }}
|
||||||
|
path: frontend/editor/playwright-report/
|
||||||
|
retention-days: 14
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Pre-commit
|
||||||
|
|
||||||
|
# Runs `pre-commit run` for ruff / codespell / gitleaks / EOF / trailing-ws.
|
||||||
|
# Called from build.yml on PRs and merge_group; also runnable on demand via
|
||||||
|
# workflow_dispatch for manual local-equivalent linting.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# Prevents sdist builds → no tar extraction
|
||||||
|
PIP_ONLY_BINARY: ":all:"
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- 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: ./.github/scripts/requirements_pre_commit.txt
|
||||||
|
|
||||||
|
- name: Run Pre-Commit Hooks
|
||||||
|
run: |
|
||||||
|
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_pre_commit.txt
|
||||||
|
|
||||||
|
- name: Run Pre-Commit
|
||||||
|
run: |
|
||||||
|
pre-commit run ruff --all-files -c .pre-commit-config.yaml
|
||||||
|
pre-commit run ruff-format --all-files -c .pre-commit-config.yaml
|
||||||
|
pre-commit run codespell --all-files -c .pre-commit-config.yaml
|
||||||
|
pre-commit run gitleaks --all-files -c .pre-commit-config.yaml
|
||||||
|
pre-commit run end-of-file-fixer --all-files -c .pre-commit-config.yaml
|
||||||
|
pre-commit run trailing-whitespace --all-files -c .pre-commit-config.yaml
|
||||||
|
git diff --exit-code
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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,139 +2,397 @@ name: Push Docker Image with VersionNumber
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
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:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- main
|
- main
|
||||||
|
- V2-master
|
||||||
|
- testMain
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||||
|
runs-on: ubuntu-24.04-8core
|
||||||
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
egress-policy: audit
|
||||||
|
|
||||||
|
- 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"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- name: Cache Gradle dependencies
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
gradle-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Setup Gradle
|
||||||
run: ./gradlew clean build
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
env:
|
with:
|
||||||
DOCKER_ENABLE_SECURITY: false
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
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
|
||||||
|
with:
|
||||||
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
- name: Convert repository owner to lowercase
|
||||||
id: repoowner
|
id: repoowner
|
||||||
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Generate tags
|
- name: Generate tags for latest
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
if: env.RUN_MAIN_APP == 'true'
|
||||||
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||||
|
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/testMain' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push Unified Dockerfile (latest variant)
|
||||||
uses: docker/build-push-action@v5
|
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
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./docker/embedded/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha,scope=stirling-pdf-latest
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: |
|
||||||
|
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
|
BASE_VERSION=1.0.0
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
|
||||||
- name: Generate tags ultra-lite
|
- name: Sign regular images
|
||||||
id: meta2
|
if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-latest.outputs.digest != ''
|
||||||
uses: docker/metadata-action@v5
|
env:
|
||||||
if: github.ref != 'refs/heads/main'
|
DIGEST: ${{ steps.build-push-latest.outputs.digest }}
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||||
|
cosign sign --yes \
|
||||||
|
--key env://COSIGN_PRIVATE_KEY \
|
||||||
|
"${tag}@${DIGEST}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- 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'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||||
|
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Unified Dockerfile (fat variant)
|
||||||
uses: docker/build-push-action@v5
|
id: build-push-fat
|
||||||
if: github.ref != 'refs/heads/main'
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' && steps.meta-fat.outputs.tags != ''
|
||||||
context: .
|
|
||||||
file: ./Dockerfile-ultra-lite
|
|
||||||
push: true
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
tags: ${{ steps.meta2.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta2.outputs.labels }}
|
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
|
||||||
|
|
||||||
|
|
||||||
- name: Generate tags fat
|
|
||||||
id: meta3
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
if: github.ref != 'refs/heads/main'
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
|
||||||
tags: |
|
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
|
||||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
|
||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
if: github.ref != 'refs/heads/main'
|
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile-fat
|
file: ./docker/embedded/Dockerfile.fat
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha,scope=stirling-pdf-fat
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max,scope=stirling-pdf-fat
|
||||||
tags: ${{ steps.meta3.outputs.tags }}
|
tags: ${{ steps.meta-fat.outputs.tags }}
|
||||||
labels: ${{ steps.meta3.outputs.labels }}
|
labels: ${{ steps.meta-fat.outputs.labels }}
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
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 != ''
|
||||||
|
env:
|
||||||
|
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
|
||||||
|
TAGS: ${{ steps.meta-fat.outputs.tags }}
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||||
|
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- 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'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||||
|
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||||
|
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }}
|
||||||
|
|
||||||
|
- 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 != ''
|
||||||
|
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
|
||||||
|
tags: ${{ steps.meta-lite.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-lite.outputs.labels }}
|
||||||
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
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 != ''
|
||||||
|
env:
|
||||||
|
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
|
||||||
|
TAGS: ${{ steps.meta-lite.outputs.tags }}
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
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,75 +0,0 @@
|
|||||||
name: Release Artifacts
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
jobs:
|
|
||||||
push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
enable_security: [true, false]
|
|
||||||
include:
|
|
||||||
- enable_security: true
|
|
||||||
file_suffix: "-with-login"
|
|
||||||
- enable_security: false
|
|
||||||
file_suffix: ""
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "temurin"
|
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
|
||||||
with:
|
|
||||||
gradle-version: 8.7
|
|
||||||
|
|
||||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
|
||||||
run: ./gradlew clean createExe
|
|
||||||
env:
|
|
||||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
|
||||||
|
|
||||||
- name: Get version number
|
|
||||||
id: versionNumber
|
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Rename binarie
|
|
||||||
if: matrix.file_suffix != ''
|
|
||||||
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
|
||||||
|
|
||||||
- name: Upload Assets binarie
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
|
||||||
name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
|
||||||
overwrite: true
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
- name: Upload binaries to release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
|
||||||
|
|
||||||
- name: Rename jar binaries
|
|
||||||
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
|
|
||||||
- name: Upload Assets jar binaries
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
overwrite: true
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload jar binaries to release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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!"
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||||
|
# by a third-party and are governed by separate terms of service, privacy
|
||||||
|
# policy, and support documentation.
|
||||||
|
|
||||||
|
name: Scorecard supply-chain security
|
||||||
|
on:
|
||||||
|
# For Branch-Protection check. Only the default branch is supported. See
|
||||||
|
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||||
|
branch_protection_rule:
|
||||||
|
# To guarantee Maintained check is occasionally updated. See
|
||||||
|
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||||
|
schedule:
|
||||||
|
- cron: "20 7 * * 2"
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||||
|
name: Scorecard analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Needed to upload the results to code-scanning dashboard.
|
||||||
|
security-events: write
|
||||||
|
# Needed to publish results and get a badge (see publish_results below).
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
# To allow GraphQL ListCommits to work
|
||||||
|
issues: read
|
||||||
|
pull-requests: read
|
||||||
|
# To detect SAST tools
|
||||||
|
checks: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: "Checkout code"
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: "Run analysis"
|
||||||
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||||
|
with:
|
||||||
|
results_file: results.sarif
|
||||||
|
results_format: sarif
|
||||||
|
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||||
|
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||||
|
# - you are installing Scorecards on a *private* repository
|
||||||
|
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
||||||
|
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||||
|
|
||||||
|
# Public repositories:
|
||||||
|
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||||
|
# - Allows the repository to include the Scorecard badge.
|
||||||
|
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||||
|
# For private repositories:
|
||||||
|
# - `publish_results` will always be set to `false`, regardless
|
||||||
|
# of the value entered here.
|
||||||
|
publish_results: true
|
||||||
|
|
||||||
|
# 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
|
||||||
|
with:
|
||||||
|
name: SARIF file
|
||||||
|
path: results.sarif
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
|
- name: "Upload to code-scanning"
|
||||||
|
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v3.29.5
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
@@ -5,15 +5,24 @@ on:
|
|||||||
- cron: "30 0 * * *"
|
- cron: "30 0 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 30 days stale issues
|
- name: 30 days stale issues
|
||||||
uses: actions/stale@v9
|
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
@@ -29,4 +38,4 @@ jobs:
|
|||||||
only-issue-labels: "more-info-needed"
|
only-issue-labels: "more-info-needed"
|
||||||
days-before-pr-stale: -1 # Prevents PRs from being marked as stale
|
days-before-pr-stale: -1 # Prevents PRs from being marked as stale
|
||||||
days-before-pr-close: -1 # Prevents PRs from being closed
|
days-before-pr-close: -1 # Prevents PRs from being closed
|
||||||
start-date: '2024-07-06T00:00:00Z' # ISO 8601 Format
|
start-date: "2024-07-06T00:00:00Z" # ISO 8601 Format
|
||||||
|
|||||||
@@ -6,34 +6,75 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
# cancel in-progress jobs if a new job is triggered
|
||||||
push:
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
runs-on: ubuntu-latest
|
# or a pull request is updated.
|
||||||
steps:
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
- uses: actions/checkout@v4
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
- name: Set up JDK 17
|
permissions:
|
||||||
uses: actions/setup-java@v4
|
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 }}
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
egress-policy: audit
|
||||||
|
|
||||||
|
- 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"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||||
|
|
||||||
- name: Upload Swagger Documentation to SwaggerHub
|
- name: Upload Swagger Documentation to SwaggerHub
|
||||||
run: ./gradlew swaggerhubUpload
|
run: ./gradlew swaggerhubUpload
|
||||||
env:
|
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_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
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
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
|
- name: Set API version as published and default on SwaggerHub
|
||||||
run: |
|
run: |
|
||||||
curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/Frooodle/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
|
curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/${SWAGGERHUB_USER}/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
|
||||||
env:
|
env:
|
||||||
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
|
||||||
|
SWAGGERHUB_USER: "Frooodle"
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
name: Sync Files
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "build.gradle"
|
|
||||||
- "src/main/resources/messages_*.properties"
|
|
||||||
- "scripts/ignore_translation.toml"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-versions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pip install pyyaml
|
|
||||||
- name: Sync versions
|
|
||||||
run: python .github/scripts/gradle_to_chart.py
|
|
||||||
- name: Set up git config
|
|
||||||
run: |
|
|
||||||
git config --global user.email "GitHub Action <[email protected]>"
|
|
||||||
git config --global user.name "GitHub Action <[email protected]>"
|
|
||||||
- name: Run git add
|
|
||||||
run: |
|
|
||||||
git add .
|
|
||||||
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
|
|
||||||
> Made via sync_files.yml" || echo "no changes"
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
commit-message: Update files
|
|
||||||
committer: GitHub Action <[email protected]>
|
|
||||||
author: GitHub Action <[email protected]>
|
|
||||||
signoff: true
|
|
||||||
branch: sync_version
|
|
||||||
title: ":floppy_disk: Update Version"
|
|
||||||
body: |
|
|
||||||
Auto-generated by [create-pull-request][1]
|
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
|
||||||
draft: false
|
|
||||||
delete-branch: true
|
|
||||||
labels: github-actions
|
|
||||||
sync-readme:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pip install tomlkit
|
|
||||||
- name: Sync README
|
|
||||||
run: python scripts/counter_translation.py
|
|
||||||
- name: Set up git config
|
|
||||||
run: |
|
|
||||||
git config --global user.email "GitHub Action <[email protected]>"
|
|
||||||
git config --global user.name "GitHub Action <[email protected]>"
|
|
||||||
- name: Run git add
|
|
||||||
run: |
|
|
||||||
git add .
|
|
||||||
git diff --staged --quiet || git commit -m ":memo: Sync README
|
|
||||||
> Made via sync_files.yml" || echo "no changes"
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
commit-message: Update files
|
|
||||||
committer: GitHub Action <[email protected]>
|
|
||||||
author: GitHub Action <[email protected]>
|
|
||||||
signoff: true
|
|
||||||
branch: sync_readme
|
|
||||||
title: ":memo: Update README: Translation Progress Table"
|
|
||||||
body: |
|
|
||||||
Auto-generated by [create-pull-request][1]
|
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
|
||||||
draft: false
|
|
||||||
delete-branch: true
|
|
||||||
labels: Documentation,Translation,github-actions
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: Sync Files (TOML)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "build.gradle"
|
||||||
|
- "app/common/build.gradle"
|
||||||
|
- "app/core/build.gradle"
|
||||||
|
- "app/proprietary/build.gradle"
|
||||||
|
- "README.md"
|
||||||
|
- "frontend/editor/public/locales/*/translation.toml"
|
||||||
|
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||||
|
- "scripts/ignore_translation.toml"
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-files:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.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 -r ./.github/scripts/requirements_pre_commit.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: pre-commit run
|
||||||
|
run: |
|
||||||
|
pre-commit run toml-sort-fix --all-files
|
||||||
|
|
||||||
|
- name: Commit translation files
|
||||||
|
run: |
|
||||||
|
git add frontend/editor/public/locales/*/translation.toml
|
||||||
|
git diff --staged --quiet || git commit -m ":memo: Sync translation files (TOML)" || echo "No changes detected"
|
||||||
|
|
||||||
|
- name: Sync README.md
|
||||||
|
run: |
|
||||||
|
python scripts/counter_translation_v3.py
|
||||||
|
|
||||||
|
- name: Run git add
|
||||||
|
run: |
|
||||||
|
git add README.md scripts/ignore_translation.toml
|
||||||
|
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: always()
|
||||||
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
|
with:
|
||||||
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
commit-message: Update files
|
||||||
|
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||||
|
author: ${{ steps.setup-bot.outputs.committer }}
|
||||||
|
signoff: true
|
||||||
|
branch: sync_readme_v3
|
||||||
|
base: main
|
||||||
|
title: ":globe_with_meridians: Sync Translations + Update README Progress Table"
|
||||||
|
body: |
|
||||||
|
### Description of Changes
|
||||||
|
|
||||||
|
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`.
|
||||||
|
- Ensured consistency and synchronization across all supported language files.
|
||||||
|
- Highlighted any missing or incomplete translations.
|
||||||
|
- **Format**: TOML
|
||||||
|
|
||||||
|
#### **2. Update README.md**
|
||||||
|
- Generated the translation progress table in `README.md` using `counter_translation_v3.py`.
|
||||||
|
- Added a summary of the current translation status for all supported languages.
|
||||||
|
- Included up-to-date statistics on translation coverage.
|
||||||
|
|
||||||
|
#### **Why these changes are necessary**
|
||||||
|
- Keeps translation files aligned with the latest reference updates.
|
||||||
|
- Ensures the documentation reflects the current translation progress.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Auto-generated by [create-pull-request][1].
|
||||||
|
|
||||||
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
|
labels: github-actions
|
||||||
|
sign-commits: true
|
||||||
|
add-paths: |
|
||||||
|
README.md
|
||||||
|
frontend/editor/public/locales/*/translation.toml
|
||||||
|
scripts/ignore_translation.toml
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
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"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: "Platform to build (windows, macos, linux, or all)"
|
||||||
|
required: true
|
||||||
|
default: "all"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- windows
|
||||||
|
- macos
|
||||||
|
- linux
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
determine-matrix:
|
||||||
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
JOINED=$(IFS=','; echo "${ENTRIES[*]}")
|
||||||
|
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: determine-matrix
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
env:
|
||||||
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||||
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||||
|
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: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
|
- name: Setup Task
|
||||||
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||||
|
|
||||||
|
- name: Build universal macOS JRE
|
||||||
|
if: matrix.platform == 'macos-15'
|
||||||
|
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
|
||||||
|
|
||||||
|
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||||
|
- name: Setup DigiCert KeyLocker
|
||||||
|
id: digicert-setup
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||||
|
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||||
|
env:
|
||||||
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||||
|
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
||||||
|
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
|
||||||
|
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||||
|
SM_HOST: ${{ secrets.SM_HOST }}
|
||||||
|
|
||||||
|
- name: Setup DigiCert KeyLocker Certificate
|
||||||
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||||
|
|
||||||
|
# Decode client certificate
|
||||||
|
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
|
||||||
|
$certPath = "D:\Certificate_pkcs12.p12"
|
||||||
|
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
|
||||||
|
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
# Get PKCS11 config path from DigiCert action
|
||||||
|
$pkcs11Config = $env:PKCS11_CONFIG
|
||||||
|
if ($pkcs11Config) {
|
||||||
|
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
|
||||||
|
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
|
||||||
|
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
|
||||||
|
if (Test-Path $defaultPath) {
|
||||||
|
Write-Host "Found config at default path: $defaultPath"
|
||||||
|
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
Write-Host "Warning: Could not find PKCS11 config file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.ref == 'refs/heads/main' }}
|
||||||
|
env:
|
||||||
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
|
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
if ($env:WINDOWS_CERTIFICATE) {
|
||||||
|
Write-Host "Importing Windows Code Signing Certificate..."
|
||||||
|
|
||||||
|
# Decode base64 certificate and save to file
|
||||||
|
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
|
||||||
|
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
|
||||||
|
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
||||||
|
|
||||||
|
# Import certificate to CurrentUser\My store
|
||||||
|
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
|
||||||
|
|
||||||
|
# Extract and set thumbprint as environment variable
|
||||||
|
$thumbprint = $cert.Thumbprint
|
||||||
|
Write-Host "Certificate imported with thumbprint: $thumbprint"
|
||||||
|
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
# Clean up certificate file
|
||||||
|
Remove-Item $certPath
|
||||||
|
|
||||||
|
Write-Host "Windows certificate import completed."
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Import Apple Developer Certificate
|
||||||
|
if: matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "Importing Apple Developer Certificate..."
|
||||||
|
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||||
|
# Create temporary keychain
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
# Import certificate
|
||||||
|
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
# Clean up
|
||||||
|
rm certificate.p12
|
||||||
|
|
||||||
|
- name: Verify Certificate
|
||||||
|
if: matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||||
|
run: |
|
||||||
|
echo "Verifying Apple Developer Certificate..."
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
|
||||||
|
echo "Certificate Info: $CERT_INFO"
|
||||||
|
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||||
|
echo "Certificate ID: $CERT_ID"
|
||||||
|
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||||
|
echo "Certificate imported successfully."
|
||||||
|
|
||||||
|
- name: Check DMG creation dependencies (macOS only)
|
||||||
|
if: matrix.platform == 'macos-15'
|
||||||
|
run: |
|
||||||
|
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||||
|
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||||
|
echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||||
|
echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||||
|
echo "macOS version: $(sw_vers -productVersion)"
|
||||||
|
echo "Available tools:"
|
||||||
|
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||||
|
|
||||||
|
- name: Preflight smctl
|
||||||
|
if: ${{ 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: ${{ 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: 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
|
||||||
|
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
||||||
|
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 }}
|
||||||
|
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 }}
|
||||||
|
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: ${{ (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() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Verify notarization (macOS only)
|
||||||
|
if: matrix.platform == 'macos-15'
|
||||||
|
run: |
|
||||||
|
echo "🔍 Verifying notarization status..."
|
||||||
|
cd ./frontend/editor/src-tauri/target
|
||||||
|
DMG_FILE=$(find . -name "*.dmg" | head -1)
|
||||||
|
if [ -n "$DMG_FILE" ]; then
|
||||||
|
echo "Found DMG: $DMG_FILE"
|
||||||
|
echo "Checking notarization ticket..."
|
||||||
|
spctl -a -vvv -t install "$DMG_FILE" || echo "⚠️ Notarization check failed or not yet complete"
|
||||||
|
stapler validate "$DMG_FILE" || echo "⚠️ No notarization ticket attached"
|
||||||
|
else
|
||||||
|
echo "⚠️ No DMG file found to verify"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
# 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" \;
|
||||||
|
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" \;
|
||||||
|
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: matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$allSigned = $true
|
||||||
|
$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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[ERROR] MSI extraction failed (exit code: $($proc.ExitCode))"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[ERROR] MSI not found at $msiPath"
|
||||||
|
$allSigned = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $allSigned) {
|
||||||
|
Write-Host "[ERROR] Signature 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: Stirling-PDF-${{ matrix.name }}
|
||||||
|
path: ./dist/*
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Verify build artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd ./frontend/editor/src-tauri/target
|
||||||
|
|
||||||
|
# Check for expected artifacts based on platform
|
||||||
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||||
|
echo "Checking for Windows artifacts..."
|
||||||
|
find . -name "*.exe" -o -name "*.msi" | head -5
|
||||||
|
if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then
|
||||||
|
echo "❌ No Windows executable found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||||
|
echo "Checking for macOS artifacts..."
|
||||||
|
find . -name "*.dmg" | head -5
|
||||||
|
if [ $(find . -name "*.dmg" | wc -l) -eq 0 ]; then
|
||||||
|
echo "❌ No macOS artifacts found"
|
||||||
|
exit 1
|
||||||
|
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
|
||||||
|
echo "❌ No Linux artifacts found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Build artifacts found for ${{ matrix.name }}"
|
||||||
|
|
||||||
|
- name: Test artifact sizes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd ./frontend/editor/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
|
||||||
|
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"
|
||||||
|
# Check if file is suspiciously small (less than 1MB)
|
||||||
|
if [ "$size" != "unknown" ] && [ "$size" -lt 1048576 ]; then
|
||||||
|
echo "⚠️ Warning: $file is smaller than 1MB"
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
echo "✅ All Tauri builds completed successfully!"
|
||||||
|
echo "Artifacts are ready for distribution."
|
||||||
|
elif [ "${{ needs.build.result }}" = "skipped" ]; then
|
||||||
|
echo "⏭️ Tauri builds skipped (CI lite mode enabled)"
|
||||||
|
else
|
||||||
|
echo "❌ Some Tauri builds failed."
|
||||||
|
echo "Please check the logs and fix any issues."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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.5.1
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.docker-rev }} (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: ./${{ 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
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
name: Docker Compose Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "src/**"
|
|
||||||
- "**.gradle"
|
|
||||||
- "!src/main/java/resources/messages*"
|
|
||||||
- "exampleYmlFiles/**"
|
|
||||||
- "Dockerfile"
|
|
||||||
- "Dockerfile**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Java 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "adopt"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Install Docker Compose
|
|
||||||
run: |
|
|
||||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/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@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.7"
|
|
||||||
|
|
||||||
- name: Pip requirements
|
|
||||||
run: |
|
|
||||||
pip install -r ./cucumber/requirements.txt
|
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
|
||||||
run: |
|
|
||||||
chmod +x ./test.sh
|
|
||||||
./test.sh
|
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
name: UI test with TestDriverAI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["master", "UITest", "testdriver"]
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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 }}
|
||||||
|
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: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
|
with:
|
||||||
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
||||||
|
env:
|
||||||
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||||
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: true
|
||||||
|
|
||||||
|
- name: Set up 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
|
||||||
|
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||||
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Deploy to VPS
|
||||||
|
run: |
|
||||||
|
cat > docker-compose.yml << EOF
|
||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: stirling-pdf-test-${{ github.sha }}
|
||||||
|
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||||
|
ports:
|
||||||
|
- "1337:8080"
|
||||||
|
volumes:
|
||||||
|
- /stirling/test-${{ github.sha }}/data:/usr/share/tessdata:rw
|
||||||
|
- /stirling/test-${{ github.sha }}/config:/configs:rw
|
||||||
|
- /stirling/test-${{ github.sha }}/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: "Stirling-PDF Test"
|
||||||
|
UI_HOMEDESCRIPTION: "Test Deployment"
|
||||||
|
UI_APPNAMENAVBAR: "Test"
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||||
|
SYSTEM_ENABLEANALYTICS: "false"
|
||||||
|
restart: on-failure:5
|
||||||
|
EOF
|
||||||
|
|
||||||
|
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml
|
||||||
|
|
||||||
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF
|
||||||
|
mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs}
|
||||||
|
mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml
|
||||||
|
cd /stirling/test-${{ github.sha }}
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
EOF
|
||||||
|
|
||||||
|
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' }}
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Check for file changes
|
||||||
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
|
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' }}
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Run TestDriver.ai
|
||||||
|
uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
|
||||||
|
with:
|
||||||
|
key: ${{secrets.TESTDRIVER_API_KEY}}
|
||||||
|
prerun: |
|
||||||
|
choco install go-task -y
|
||||||
|
task frontend:build
|
||||||
|
cd frontend
|
||||||
|
npm install dashcam-chrome --save
|
||||||
|
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337"
|
||||||
|
Start-Sleep -Seconds 20
|
||||||
|
prompt: |
|
||||||
|
1. /run testing/testdriver/test.yml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
FORCE_COLOR: "3"
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
needs: [pick, deploy, test]
|
||||||
|
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }}
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key
|
||||||
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
|
- name: Cleanup deployment
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF
|
||||||
|
cd /stirling/test-${{ github.sha }}
|
||||||
|
docker-compose down
|
||||||
|
cd /stirling
|
||||||
|
rm -rf test-${{ github.sha }}
|
||||||
|
EOF
|
||||||
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||||
+126
-5
@@ -4,6 +4,7 @@ bin/
|
|||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
|
*.exe
|
||||||
*.swp
|
*.swp
|
||||||
*~.nib
|
*~.nib
|
||||||
local.properties
|
local.properties
|
||||||
@@ -12,16 +13,57 @@ local.properties
|
|||||||
.recommenders
|
.recommenders
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
|
*.local.json
|
||||||
version.properties
|
version.properties
|
||||||
pipeline/watchedFolders/
|
|
||||||
pipeline/finishedFolders/
|
|
||||||
#### Stirling-PDF Files ###
|
#### Stirling-PDF Files ###
|
||||||
|
pipeline/
|
||||||
|
!pipeline/.gitkeep
|
||||||
customFiles/
|
customFiles/
|
||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
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
|
||||||
|
app/core/src/main/resources/static/locales/
|
||||||
|
app/core/src/main/resources/static/Login/
|
||||||
|
app/core/src/main/resources/static/classic-logo/
|
||||||
|
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/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
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
|
.gradle-home
|
||||||
.lock
|
.lock
|
||||||
|
|
||||||
# External tool builders
|
# External tool builders
|
||||||
@@ -114,7 +156,14 @@ watchedFolders/
|
|||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.db
|
*.db
|
||||||
/build
|
build
|
||||||
|
app/core/build
|
||||||
|
app/common/build
|
||||||
|
app/proprietary/build
|
||||||
|
common/build
|
||||||
|
proprietary/build
|
||||||
|
stirling-pdf/build
|
||||||
|
frontend/editor/src-tauri/provisioner/target
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -122,7 +171,6 @@ __pycache__/
|
|||||||
*.pyo
|
*.pyo
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.env*
|
|
||||||
.venv*
|
.venv*
|
||||||
env*/
|
env*/
|
||||||
venv*/
|
venv*/
|
||||||
@@ -130,14 +178,19 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Env files (secrets / local overrides). Subproject .gitignore files whitelist any committed defaults.
|
||||||
|
.env*
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
/.vscode/**/*
|
/.vscode/**/*
|
||||||
!/.vscode/settings.json
|
!/.vscode/settings.json
|
||||||
|
!/.vscode/extensions.json
|
||||||
|
|
||||||
# IntelliJ IDEA
|
# IntelliJ IDEA
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
out/
|
out/
|
||||||
|
.junie/
|
||||||
|
|
||||||
# Ignore Mac DS_Store files
|
# Ignore Mac DS_Store files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -146,18 +199,86 @@ out/
|
|||||||
# cucumber
|
# cucumber
|
||||||
/cucumber/reports/**
|
/cucumber/reports/**
|
||||||
|
|
||||||
# Certs
|
# Certs and Security Files
|
||||||
*.p12
|
*.p12
|
||||||
|
*.pk8
|
||||||
*.pem
|
*.pem
|
||||||
*.crt
|
*.crt
|
||||||
*.cer
|
*.cer
|
||||||
|
*.cert
|
||||||
*.der
|
*.der
|
||||||
*.key
|
*.key
|
||||||
*.csr
|
*.csr
|
||||||
|
*.kdbx
|
||||||
|
*.jks
|
||||||
|
*.asc
|
||||||
|
|
||||||
|
# Allow test fixture certificates (synthetic, no real credentials)
|
||||||
|
!frontend/editor/src/core/tests/test-fixtures/certs/**
|
||||||
|
|
||||||
|
# SSH Keys
|
||||||
|
*.pub
|
||||||
|
*.priv
|
||||||
|
id_rsa
|
||||||
|
id_rsa.pub
|
||||||
|
id_ecdsa
|
||||||
|
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
|
||||||
|
.cache
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
.build-cache
|
||||||
|
|
||||||
|
|
||||||
|
**/jcef-bundle/
|
||||||
|
|
||||||
|
# node_modules
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# weasyPrint
|
||||||
|
**/LOCAL_APPDATA_FONTCONFIG_CACHE/**
|
||||||
|
|
||||||
|
# Translation temp files
|
||||||
|
*_compact.json
|
||||||
|
*compact*.json
|
||||||
|
test_batch.json
|
||||||
|
*.backup.*.json
|
||||||
|
frontend/editor/public/locales/*/translation.backup*.json
|
||||||
|
|
||||||
|
# Development/build artifacts
|
||||||
|
.gradle-cache/
|
||||||
|
scripts/pdf-collection/
|
||||||
|
**/tmp/
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Type3 development data
|
||||||
|
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/
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 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
|
||||||
+34
-21
@@ -1,39 +1,52 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.2.1
|
rev: v0.15.14
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
- --fix
|
- --fix
|
||||||
- --line-length=127
|
- --line-length=127
|
||||||
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
|
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
|
||||||
exclude: (split_photos.py)
|
exclude: (split_photos.py)
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
|
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
|
||||||
exclude: (split_photos.py)
|
exclude: (split_photos.py)
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=
|
- --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment
|
||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
files: \.(properties|html|css|js|py|md)$
|
files: \.(html|css|js|py|md)$
|
||||||
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
|
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|frontend/editor/public/vendor|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||||
- repo: local
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.30.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-duplicate-properties-keys
|
- id: gitleaks
|
||||||
name: Check Duplicate Properties Keys
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
entry: python .github/scripts/check_duplicates.py
|
rev: v6.0.0
|
||||||
language: python
|
|
||||||
files: ^(src)/.+\.properties$
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-html-tabs
|
- id: end-of-file-fixer
|
||||||
name: Check HTML for tabs
|
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||||
# args: ["--replace_with= "]
|
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
|
||||||
entry: python .github/scripts/check_tabulator.py
|
- id: trailing-whitespace
|
||||||
language: python
|
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||||
exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
|
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
|
||||||
files: ^.*(\.html|\.css|\.js)$
|
- repo: https://github.com/pappasam/toml-sort
|
||||||
|
rev: v0.24.4
|
||||||
|
hooks:
|
||||||
|
- id: toml-sort-fix
|
||||||
|
files: frontend/editor/public/locales/.*\.toml$
|
||||||
|
args: ['--in-place', '--all', '--ignore-case']
|
||||||
|
# - repo: https://github.com/thibaudcolas/pre-commit-stylelint
|
||||||
|
# rev: v16.21.1
|
||||||
|
# hooks:
|
||||||
|
# - id: stylelint
|
||||||
|
# additional_dependencies:
|
||||||
|
# - [email protected]
|
||||||
|
# - [email protected]
|
||||||
|
# - "@stylistic/[email protected]"
|
||||||
|
# files: \.(css)$
|
||||||
|
# args: [--fix]
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
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_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||||
|
|
||||||
|
dev:proprietary:
|
||||||
|
desc: "Start backend dev server in proprietary mode"
|
||||||
|
ignore_error: true
|
||||||
|
vars:
|
||||||
|
PORT: '{{.PORT | default "8080"}}'
|
||||||
|
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||||
|
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||||
|
env:
|
||||||
|
SERVER_PORT: '{{.PORT}}'
|
||||||
|
cmds:
|
||||||
|
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{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_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||||
|
env:
|
||||||
|
SERVER_PORT: '{{.PORT}}'
|
||||||
|
STIRLING_FLAVOR: saas
|
||||||
|
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||||
|
AIENGINE_ENABLED: '{{if .AIENGINE_URL}}true{{else}}false{{end}}'
|
||||||
|
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]
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
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
|
||||||
|
- |
|
||||||
|
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
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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}}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
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:
|
||||||
|
desc: "Set up dev environment"
|
||||||
|
run: when_changed
|
||||||
|
vars:
|
||||||
|
MODE: '{{.MODE | default ""}}'
|
||||||
|
deps:
|
||||||
|
- task: prepare:env
|
||||||
|
vars: { MODE: '{{.MODE}}' }
|
||||||
|
- prepare:icons
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 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}}' }
|
||||||
|
|
||||||
|
dev:portal:
|
||||||
|
desc: "Start developer portal dev server"
|
||||||
|
deps: [install]
|
||||||
|
cmds:
|
||||||
|
- npx vite portal --port {{.PORT | default "5173"}}{{if .OPEN}} --open{{end}}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 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]
|
||||||
|
cmds:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
build:portal:
|
||||||
|
desc: "Build developer portal"
|
||||||
|
deps: [install]
|
||||||
|
cmds:
|
||||||
|
- npx vite build portal
|
||||||
|
|
||||||
|
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 editor, portal, and the shared design system.
|
||||||
|
- npx dpdm "editor/src/**/*.{ts,tsx}" "portal/src/**/*.{ts,tsx}" "shared/**/*.{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:core:
|
||||||
|
desc: "Typecheck core build variant"
|
||||||
|
deps: [prepare]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/src/core/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:proprietary:
|
||||||
|
desc: "Typecheck proprietary build variant"
|
||||||
|
deps: [prepare]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/src/proprietary/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:saas:
|
||||||
|
desc: "Typecheck SaaS build variant"
|
||||||
|
deps:
|
||||||
|
- task: prepare
|
||||||
|
vars: { MODE: saas }
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/src/saas/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:desktop:
|
||||||
|
desc: "Typecheck desktop build variant"
|
||||||
|
deps:
|
||||||
|
- task: prepare
|
||||||
|
vars: { MODE: desktop }
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/src/desktop/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:cloud:
|
||||||
|
desc: "Typecheck cloud shared layer (standalone)"
|
||||||
|
deps: [prepare]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/src/cloud/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:scripts:
|
||||||
|
desc: "Typecheck scripts"
|
||||||
|
deps: [prepare]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/scripts/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:prototypes:
|
||||||
|
desc: "Typecheck prototypes build variant"
|
||||||
|
deps: [prepare]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project editor/src/prototypes/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:portal:
|
||||||
|
desc: "Typecheck developer portal build variant"
|
||||||
|
deps: [install]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project portal/tsconfig.json
|
||||||
|
|
||||||
|
typecheck:shared:
|
||||||
|
desc: "Typecheck the shared design system"
|
||||||
|
deps: [install]
|
||||||
|
cmds:
|
||||||
|
- npx tsc --noEmit --project shared/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
|
||||||
|
- task: typecheck:shared
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Quality Gate
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
check:
|
||||||
|
desc: "Quick quality gate for local development"
|
||||||
|
cmds:
|
||||||
|
- task: typecheck
|
||||||
|
- task: lint
|
||||||
|
- task: format:check
|
||||||
|
- task: test
|
||||||
|
|
||||||
|
check:all:
|
||||||
|
desc: "Full CI quality gate"
|
||||||
|
cmds:
|
||||||
|
- task: typecheck:all
|
||||||
|
- task: lint
|
||||||
|
- task: format:check
|
||||||
|
- task: build
|
||||||
|
- task: build:portal
|
||||||
|
- task: test
|
||||||
|
- task: storybook:build
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: "Run 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
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
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, dist-portal
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: rm -rf node_modules/.vite editor/dist dist dist-portal
|
||||||
|
platforms: [linux, darwin]
|
||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
|
||||||
|
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
|
||||||
|
"ms-python.black-formatter", // Python code formatter using Black
|
||||||
|
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
|
||||||
|
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
|
||||||
|
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||||
|
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||||
|
"streetsidesoftware.code-spell-checker", // Spell checker for code to avoid typos
|
||||||
|
"vmware.vscode-boot-dev-pack", // Developer tools for Spring Boot by VMware
|
||||||
|
"vscjava.vscode-java-pack", // Java Extension Pack with essential Java tools for VS Code
|
||||||
|
"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.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
|
||||||
|
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||||
|
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
||||||
|
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+142
-49
@@ -1,53 +1,146 @@
|
|||||||
{
|
{
|
||||||
"java.compile.nullAnalysis.mode": "automatic",
|
"editor.wordSegmenterLocales": "",
|
||||||
"files.eol": "auto",
|
|
||||||
"java.configuration.updateBuildConfiguration": "interactive",
|
|
||||||
"black-formatter.args": ["--line-length", "127"],
|
|
||||||
"flake8.args": ["--max-line-length", "127"],
|
|
||||||
"pylint.args": ["max-line-length", "127"],
|
|
||||||
"[java]": {
|
|
||||||
"editor.tabSize": 4,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[python]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[gradle-build]": {
|
|
||||||
"editor.tabSize": 4,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[gradle]": {
|
|
||||||
"editor.tabSize": 4,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[html]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.rulers": [127],
|
|
||||||
"files.trimFinalNewlines": false,
|
|
||||||
"files.insertFinalNewline": false
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[yaml]": {
|
|
||||||
"files.trimFinalNewlines": false,
|
|
||||||
"files.insertFinalNewline": false
|
|
||||||
},
|
|
||||||
"diffEditor.maxComputationTime": 0,
|
|
||||||
"editor.wordSegmenterLocales": null,
|
|
||||||
"editor.guides.bracketPairs": "active",
|
"editor.guides.bracketPairs": "active",
|
||||||
"editor.guides.bracketPairsHorizontal": "active",
|
"editor.guides.bracketPairsHorizontal": "active",
|
||||||
"files.insertFinalNewline": true,
|
"editor.defaultFormatter": "EditorConfig.EditorConfig",
|
||||||
"files.trimFinalNewlines": true,
|
"cSpell.enabled": false,
|
||||||
"files.trimTrailingWhitespace": true,
|
"[feature]": {
|
||||||
"editor.indentSize": "tabSize",
|
"editor.defaultFormatter": "alexkrechik.cucumberautocomplete"
|
||||||
"editor.stickyScroll.enabled": false,
|
},
|
||||||
"editor.minimap.enabled": false,
|
"[java]": {
|
||||||
"editor.formatOnSave": true
|
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "stylelint.vscode-stylelint"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"[gradle-kotlin-dsl]": {
|
||||||
|
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
||||||
|
},
|
||||||
|
"[gradle-build]": {
|
||||||
|
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||||
|
},
|
||||||
|
"[gradle]": {
|
||||||
|
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||||
|
},
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
|
"java.format.enabled": true,
|
||||||
|
"java.format.settings.profile": "GoogleStyle",
|
||||||
|
"java.format.settings.google.version": "1.28.0",
|
||||||
|
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||||
|
// (DE) Aktiviert Kommentare im Java-Format.
|
||||||
|
// (EN) Enables comments in Java formatting.
|
||||||
|
// "java.format.comments.enabled": true,
|
||||||
|
// (DE) Generiert automatisch Kommentare im Code.
|
||||||
|
// (EN) Automatically generates comments in code.
|
||||||
|
// "java.codeGeneration.generateComments": true,
|
||||||
|
// https://github.com/redhat-developer/vscode-java/blob/master/document/_java.learnMoreAboutCleanUps.md#java-clean-ups
|
||||||
|
"java.saveActions.cleanup": true,
|
||||||
|
"java.cleanup.actions": [
|
||||||
|
"invertEquals", // Inverts calls to Object.equals(Object) and String.equalsIgnoreCase(String) to avoid useless null pointer exception.
|
||||||
|
"instanceofPatternMatch" // Replaces instanceof checks with pattern matching.
|
||||||
|
],
|
||||||
|
// (DE) Aktiviert die Code-Vervollständigung für Java.
|
||||||
|
// (EN) Enables code completion for Java.
|
||||||
|
"java.completion.engine": "dom",
|
||||||
|
"java.completion.enabled": true,
|
||||||
|
"java.completion.importOrder": [
|
||||||
|
"java",
|
||||||
|
"javax",
|
||||||
|
"org",
|
||||||
|
"com",
|
||||||
|
"net",
|
||||||
|
"io",
|
||||||
|
"jakarta",
|
||||||
|
"lombok",
|
||||||
|
"me",
|
||||||
|
"stirling",
|
||||||
|
],
|
||||||
|
"java.project.resourceFilters": [
|
||||||
|
".devcontainer/",
|
||||||
|
".git/",
|
||||||
|
".github/",
|
||||||
|
".gradle/",
|
||||||
|
".venv/",
|
||||||
|
".venv*/",
|
||||||
|
".vscode/",
|
||||||
|
"bin/",
|
||||||
|
"app/core/bin/",
|
||||||
|
"app/common/bin/",
|
||||||
|
"app/proprietary/bin/",
|
||||||
|
"build/",
|
||||||
|
"app/core/build/",
|
||||||
|
"app/common/build/",
|
||||||
|
"app/proprietary/build/",
|
||||||
|
"configs/",
|
||||||
|
"app/core/configs/",
|
||||||
|
"customFiles/",
|
||||||
|
"app/core/customFiles/",
|
||||||
|
"docs/",
|
||||||
|
"exampleYmlFiles",
|
||||||
|
"gradle/",
|
||||||
|
"images/",
|
||||||
|
"logs/",
|
||||||
|
"pipeline/",
|
||||||
|
"scripts/",
|
||||||
|
"testings/",
|
||||||
|
".git-blame-ignore-revs",
|
||||||
|
".gitattributes",
|
||||||
|
".gitignore",
|
||||||
|
"app/core/.gitignore",
|
||||||
|
"app/common/.gitignore",
|
||||||
|
"app/proprietary/.gitignore",
|
||||||
|
".pre-commit-config.yaml",
|
||||||
|
],
|
||||||
|
// Enables signature help in Java.
|
||||||
|
"java.signatureHelp.enabled": true,
|
||||||
|
// Enables detailed signature help descriptions.
|
||||||
|
"java.signatureHelp.description.enabled": true,
|
||||||
|
// Downloads sources for Maven dependencies.
|
||||||
|
"java.maven.downloadSources": true,
|
||||||
|
// Enables Gradle project import.
|
||||||
|
"java.import.gradle.enabled": true,
|
||||||
|
// Downloads sources for Eclipse projects.
|
||||||
|
"java.eclipse.downloadSources": true,
|
||||||
|
// Enables import of the Gradle wrapper.
|
||||||
|
"java.import.gradle.wrapper.enabled": true,
|
||||||
|
"spring.initializr.defaultLanguage": "Java",
|
||||||
|
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
|
||||||
|
"spring.initializr.defaultArtifactId": "SPDF",
|
||||||
|
"java.jdt.ls.lombokSupport.enabled": true,
|
||||||
|
"html.format.wrapLineLength": 127,
|
||||||
|
"html.format.enable": true,
|
||||||
|
"html.format.indentInnerHtml": true,
|
||||||
|
"html.format.unformatted": "script,style,textarea",
|
||||||
|
"html.format.contentUnformatted": "pre,code",
|
||||||
|
"html.format.extraLiners": "head,body,/html",
|
||||||
|
"html.format.wrapAttributes": "force",
|
||||||
|
"html.format.wrapAttributesIndentSize": 2,
|
||||||
|
"html.format.indentHandlebars": true,
|
||||||
|
"html.format.preserveNewLines": true,
|
||||||
|
"html.format.maxPreserveNewLines": 2,
|
||||||
|
"stylelint.configFile": "devTools/.stylelintrc.json",
|
||||||
|
"java.project.sourcePaths": [
|
||||||
|
"app/core/src/main/java",
|
||||||
|
"app/common/src/main/java",
|
||||||
|
"app/proprietary/src/main/java"
|
||||||
|
],
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+292
@@ -0,0 +1,292 @@
|
|||||||
|
# Adding New React Tools to Stirling PDF
|
||||||
|
|
||||||
|
This guide covers how to add new PDF tools to the React frontend.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
When adding tools, follow this systematic approach using the established patterns and architecture.
|
||||||
|
|
||||||
|
## 1. Create Tool Structure
|
||||||
|
|
||||||
|
Create these files in the correct directories:
|
||||||
|
```
|
||||||
|
frontend/editor/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]/
|
||||||
|
└── [ToolName]Settings.tsx # Settings UI component (if needed)
|
||||||
|
|
||||||
|
frontend/editor/src/tools/
|
||||||
|
└── [ToolName].tsx # Main tool component
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Implementation Pattern
|
||||||
|
|
||||||
|
Use `useBaseTool` for simplified hook management. This is the recommended approach for all new tools:
|
||||||
|
|
||||||
|
**Parameters Hook** (`use[ToolName]Parameters.ts`):
|
||||||
|
```typescript
|
||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface [ToolName]Parameters extends BaseParameters {
|
||||||
|
// Define your tool-specific parameters here
|
||||||
|
someOption: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: [ToolName]Parameters = {
|
||||||
|
someOption: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const use[ToolName]Parameters = (): BaseParametersHook<[ToolName]Parameters> => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'your-endpoint-name',
|
||||||
|
validateFn: (params) => true, // Add validation logic
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Operation Hook** (`use[ToolName]Operation.ts`):
|
||||||
|
```typescript
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
|
||||||
|
export const build[ToolName]FormData = (parameters: [ToolName]Parameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', file);
|
||||||
|
// Add parameters to formData
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const [toolName]OperationConfig = {
|
||||||
|
toolType: ToolType.singleFile, // or ToolType.multiFile (buildFormData's file parameter will need to be updated)
|
||||||
|
buildFormData: build[ToolName]FormData,
|
||||||
|
operationType: '[toolName]',
|
||||||
|
endpoint: '/api/v1/category/endpoint-name',
|
||||||
|
filePrefix: 'processed_', // Will be overridden with translation
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const use[ToolName]Operation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return useToolOperation({
|
||||||
|
...[toolName]OperationConfig,
|
||||||
|
filePrefix: t('[toolName].filenamePrefix', 'processed') + '_',
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('[toolName].error.failed', 'Operation failed'))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Main Component** (`[ToolName].tsx`):
|
||||||
|
```typescript
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
import { use[ToolName]Parameters } from "../hooks/tools/[toolName]/use[ToolName]Parameters";
|
||||||
|
import { use[ToolName]Operation } from "../hooks/tools/[toolName]/use[ToolName]Operation";
|
||||||
|
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||||
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
|
const [ToolName] = (props: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const base = useBaseTool('[toolName]', use[ToolName]Parameters, use[ToolName]Operation, props);
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles: base.selectedFiles,
|
||||||
|
isCollapsed: base.hasResults,
|
||||||
|
placeholder: t("[toolName].files.placeholder", "Select files to get started"),
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
// Add settings steps if needed
|
||||||
|
],
|
||||||
|
executeButton: {
|
||||||
|
text: t("[toolName].submit", "Process"),
|
||||||
|
isVisible: !base.hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: base.handleExecute,
|
||||||
|
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: base.hasResults,
|
||||||
|
operation: base.operation,
|
||||||
|
title: t("[toolName].results.title", "Results"),
|
||||||
|
onFileClick: base.handleThumbnailClick,
|
||||||
|
onUndo: base.handleUndo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[ToolName].tool = () => use[ToolName]Operation;
|
||||||
|
export default [ToolName] as ToolComponent;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Some existing tools (like AddPassword, Compress) use a legacy pattern with manual hook management. **Always use the Modern Pattern above for new tools** - it's cleaner, more maintainable, and includes automation support.
|
||||||
|
|
||||||
|
## 3. Register Tool in System
|
||||||
|
Update these files to register your new tool:
|
||||||
|
|
||||||
|
**Tool Registry** (`frontend/editor/src/data/useTranslatedToolRegistry.tsx`):
|
||||||
|
1. Add imports at the top:
|
||||||
|
```typescript
|
||||||
|
import [ToolName] from "../tools/[ToolName]";
|
||||||
|
import { [toolName]OperationConfig } from "../hooks/tools/[toolName]/use[ToolName]Operation";
|
||||||
|
import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Settings";
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add tool entry in the `allTools` object:
|
||||||
|
```typescript
|
||||||
|
[toolName]: {
|
||||||
|
icon: <LocalIcon icon="your-icon-name" width="1.5rem" height="1.5rem" />,
|
||||||
|
name: t("home.[toolName].title", "Tool Name"),
|
||||||
|
component: [ToolName],
|
||||||
|
description: t("home.[toolName].desc", "Tool description"),
|
||||||
|
categoryId: ToolCategoryId.STANDARD_TOOLS, // or appropriate category
|
||||||
|
subcategoryId: SubcategoryId.APPROPRIATE_SUBCATEGORY,
|
||||||
|
maxFiles: -1, // or specific number
|
||||||
|
endpoints: ["endpoint-name"],
|
||||||
|
operationConfig: [toolName]OperationConfig,
|
||||||
|
settingsComponent: [ToolName]Settings, // if settings exist
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`):
|
||||||
|
```typescript
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '../../types/tips';
|
||||||
|
|
||||||
|
export const use[ToolName]Tips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("[toolName].tooltip.header.title", "Tool Overview")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("[toolName].tooltip.description.title", "What does this tool do?"),
|
||||||
|
description: t("[toolName].tooltip.description.text", "Simple explanation in everyday language that non-technical users can understand."),
|
||||||
|
bullets: [
|
||||||
|
t("[toolName].tooltip.description.bullet1", "Easy-to-understand benefit 1"),
|
||||||
|
t("[toolName].tooltip.description.bullet2", "Easy-to-understand benefit 2")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// Add more tip sections as needed
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add tooltip to your main component:**
|
||||||
|
```typescript
|
||||||
|
import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
|
||||||
|
|
||||||
|
const [ToolName] = (props: BaseToolProps) => {
|
||||||
|
const tips = use[ToolName]Tips();
|
||||||
|
|
||||||
|
// In your steps array:
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: t("[toolName].steps.settings", "Settings"),
|
||||||
|
tooltip: tips, // Add this line
|
||||||
|
content: <[ToolName]Settings ... />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Add Translations
|
||||||
|
Update translation files. **Important: Only update `en-US` files** - other languages are handled separately.
|
||||||
|
|
||||||
|
**File to update:** `frontend/editor/public/locales/en-US/translation.toml`
|
||||||
|
|
||||||
|
**Required Translation Keys**:
|
||||||
|
```toml
|
||||||
|
{
|
||||||
|
"home": {
|
||||||
|
"[toolName]": {
|
||||||
|
"title": "Tool Name",
|
||||||
|
"desc": "Tool description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"[toolName]": {
|
||||||
|
"title": "Tool Name",
|
||||||
|
"submit": "Process",
|
||||||
|
"filenamePrefix": "processed",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select files to get started"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"title": "Tool Options",
|
||||||
|
"someOption": "Option Label",
|
||||||
|
"someOption.desc": "Option description",
|
||||||
|
"note": "General information about the tool."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Results"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "Operation failed"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"header": {
|
||||||
|
"title": "Tool Overview"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "What does this tool do?",
|
||||||
|
"text": "Simple explanation in everyday language",
|
||||||
|
"bullet1": "Easy-to-understand benefit 1",
|
||||||
|
"bullet2": "Easy-to-understand benefit 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Translation Notes:**
|
||||||
|
- **Only update `en-US/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"
|
||||||
|
- **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
|
||||||
|
- Verify tool appears in UI with correct icon and description
|
||||||
|
- Test with various file sizes and types
|
||||||
|
- Confirm translations work
|
||||||
|
- Check error handling
|
||||||
|
- Test undo functionality
|
||||||
|
- Verify results display correctly
|
||||||
|
|
||||||
|
## Tool Development Patterns
|
||||||
|
|
||||||
|
### Three Tool Patterns:
|
||||||
|
|
||||||
|
**Pattern 1: Single-File Tools** (Individual processing)
|
||||||
|
- Backend processes one file per API call
|
||||||
|
- Set `multiFileEndpoint: false`
|
||||||
|
- Examples: Compress, Rotate
|
||||||
|
|
||||||
|
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||||
|
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||||
|
- Set `multiFileEndpoint: true`
|
||||||
|
- Examples: Split, Merge, Overlay
|
||||||
|
|
||||||
|
**Pattern 3: Complex Tools** (Custom processing)
|
||||||
|
- Tools with complex routing logic or non-standard processing
|
||||||
|
- Provide `customProcessor` for full control
|
||||||
|
- Examples: Convert, OCR
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
# 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, proprietary, and shared vars
|
||||||
|
- `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, see [frontend/editor/DeveloperGuide.md](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.
|
||||||
+32
-7
@@ -15,30 +15,55 @@ 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.
|
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).
|
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:
|
Please make sure your Pull Request adheres to the following guidelines:
|
||||||
|
|
||||||
- Use the PR template provided.
|
- Use the PR template provided.
|
||||||
- Keep your Pull Request title succinct, detailed and to the point.
|
- Keep your Pull Request title succinct, detailed, and to the point.
|
||||||
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
||||||
- Commits should be clear, concise and easy to understand.
|
- Commits should be clear, concise, and easy to understand.
|
||||||
- References to the Issue number in the Pull Request and/or Commit message.
|
- References to the Issue number in the Pull Request and/or Commit message.
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](devGuide/HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
|
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use the "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
|
||||||
|
|
||||||
## Fixing Bugs or Adding a New Feature
|
## Fixing Bugs or Adding a New Feature
|
||||||
|
|
||||||
First, make sure you've read the section [Pull Requests](#pull-requests).
|
First, make sure you've read the section [Pull Requests](#pull-requests).
|
||||||
|
|
||||||
To build from source, please follow this [Guide](LocalRunGuide.md).
|
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||||
|
|
||||||
If, at any point of time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
## Developer Documentation
|
||||||
|
|
||||||
|
For technical guides, setup instructions, and development resources:
|
||||||
|
|
||||||
|
- [Developer Guide](DeveloperGuide.md) - Main setup and architecture guide
|
||||||
|
- [Taskfile.yml](Taskfile.yml) - Unified task runner for all build/dev/test/lint commands
|
||||||
|
- [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/)
|
||||||
|
|
||||||
|
For configuration and usage guides, see:
|
||||||
|
- [Database Guide](DATABASE.md) - Database setup and configuration
|
||||||
|
- [OCR Guide](HowToUseOCR.md) - OCR setup and configuration
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing to this project, you agree that your contributions will be licensed under the [GPL 3 License](LICENSE). You also acknowledge and agree that your contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
|
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
||||||
|
|||||||
+1
-7
@@ -1,17 +1,11 @@
|
|||||||
# New Database Backup and Import Functionality
|
# New Database Backup and Import Functionality
|
||||||
|
|
||||||
**Full activation will take place on approximately January 5th, 2025!**
|
|
||||||
|
|
||||||
Why is the waiting time six months?
|
|
||||||
|
|
||||||
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
|
|
||||||
|
|
||||||
## Functionality Overview
|
## Functionality Overview
|
||||||
|
|
||||||
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
||||||
|
|
||||||
1. Automatic Backup Creation
|
1. Automatic Backup Creation
|
||||||
- The system automatically creates a database backup every day at midnight. This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
- The system automatically creates a database backup on a configurable schedule (default: daily at midnight via `system.databaseBackup.cron`). This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
||||||
2. Manual Backup Export
|
2. Manual Backup Export
|
||||||
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
|
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
|
||||||
3. Importing Database Backups
|
3. Importing Database Backups
|
||||||
|
|||||||
@@ -0,0 +1,610 @@
|
|||||||
|
# Stirling-PDF Developer Guide
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||||
|
|
||||||
|
## 2. Project Overview
|
||||||
|
|
||||||
|
**Stirling 2.0** is built using:
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Spring Boot (requires JDK 25)
|
||||||
|
- PDFBox for core PDF operations
|
||||||
|
- LibreOffice for document conversions
|
||||||
|
- qpdf for PDF optimization
|
||||||
|
- Spring Security (optional, controlled by `DOCKER_ENABLE_SECURITY`)
|
||||||
|
- Lombok for reducing boilerplate code
|
||||||
|
|
||||||
|
**Frontend (React SPA):**
|
||||||
|
- React + TypeScript
|
||||||
|
- Vite for build tooling and development server
|
||||||
|
- Mantine UI component library
|
||||||
|
- TailwindCSS for styling
|
||||||
|
- PDF.js for client-side PDF rendering
|
||||||
|
- PDF-LIB.js for client-side PDF manipulation
|
||||||
|
- IndexedDB for client-side file storage and thumbnails
|
||||||
|
- i18next for internationalization
|
||||||
|
|
||||||
|
**Infrastructure:**
|
||||||
|
- Docker for containerization
|
||||||
|
- Gradle for build management
|
||||||
|
|
||||||
|
**Desktop Application (Tauri):**
|
||||||
|
- Tauri for cross-platform desktop app packaging
|
||||||
|
- Rust backend for system integration
|
||||||
|
- PDF file association support
|
||||||
|
- Self-contained JRE bundling with JLink
|
||||||
|
|
||||||
|
## 3. Development Environment Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Task](https://taskfile.dev/installation/) — unified command runner (recommended)
|
||||||
|
- Docker
|
||||||
|
- Git
|
||||||
|
- Java JDK 25
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
|
||||||
|
cd Stirling-PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install Docker and JDK 25 if not already installed.
|
||||||
|
|
||||||
|
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||||
|
1. Only VSCode
|
||||||
|
1. Open VS Code.
|
||||||
|
2. When prompted, install the recommended extensions.
|
||||||
|
3. Alternatively, open the command palette (`Ctrl + Shift + P` or `Cmd + Shift + P` on macOS) and run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Extensions: Show Recommended Extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install the required extensions from the list.
|
||||||
|
|
||||||
|
4. Lombok Setup
|
||||||
|
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||||
|
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||||
|
|
||||||
|
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.
|
||||||
|
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)
|
||||||
|
3. **Development flow**: The Vite dev server automatically proxies API calls to the backend
|
||||||
|
|
||||||
|
### File Storage Architecture
|
||||||
|
Stirling 2.0 uses client-side file storage:
|
||||||
|
- **IndexedDB**: Stores files locally in the browser with automatic thumbnail generation
|
||||||
|
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||||
|
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
## 5. Project Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||||
|
├── docs/ # Documentation files
|
||||||
|
├── exampleYmlFiles/ # Example YAML configuration files
|
||||||
|
├── images/ # Image assets
|
||||||
|
├── pipeline/ # Pipeline-related files (generated at runtime)
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── main/
|
||||||
|
│ │ ├── java/
|
||||||
|
│ │ │ └── stirling/
|
||||||
|
│ │ │ └── software/
|
||||||
|
│ │ │ └── SPDF/
|
||||||
|
│ │ │ ├── config/
|
||||||
|
│ │ │ ├── controller/
|
||||||
|
│ │ │ ├── model/
|
||||||
|
│ │ │ ├── repository/
|
||||||
|
│ │ │ ├── service/
|
||||||
|
│ │ │ └── utils/
|
||||||
|
│ │ └── resources/
|
||||||
|
│ │ ├── static/ # Legacy static assets (reference only)
|
||||||
|
│ │ │ ├── css/
|
||||||
|
│ │ │ ├── js/
|
||||||
|
│ │ │ └── pdfjs/
|
||||||
|
│ └── test/
|
||||||
|
├── testing/ # Cucumber and integration tests
|
||||||
|
│ └── cucumber/ # Cucumber test files
|
||||||
|
├── build.gradle # Gradle build configuration
|
||||||
|
├── Dockerfile # Main Dockerfile
|
||||||
|
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version
|
||||||
|
├── Dockerfile.fat # Dockerfile for fat version
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Docker-based Development
|
||||||
|
|
||||||
|
Stirling-PDF offers several Docker versions:
|
||||||
|
|
||||||
|
- Full: All features included
|
||||||
|
- Ultra-Lite: Basic PDF operations only
|
||||||
|
- Fat: Includes additional libraries and fonts predownloaded
|
||||||
|
|
||||||
|
### Example Docker Compose Files
|
||||||
|
|
||||||
|
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as:
|
||||||
|
|
||||||
|
- `docker-compose-latest.yml`: Latest version without login and security features
|
||||||
|
- `docker-compose-latest-security.yml`: Latest version with login and security features enabled
|
||||||
|
- `docker-compose-latest-fat-security.yml`: Fat version with login and security features enabled
|
||||||
|
|
||||||
|
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Security
|
||||||
|
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
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'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||||
|
- ./stirling/latest/config:/configs:rw
|
||||||
|
- ./stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
PUID: 1002
|
||||||
|
PGID: 1002
|
||||||
|
UMASK: "022"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
SHOW_SURVEY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
|
```
|
||||||
|
|
||||||
|
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 for to enable login and security features for builds
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task backend:build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the Docker images:
|
||||||
|
|
||||||
|
For the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
For the ultra-lite version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||||
|
```
|
||||||
|
|
||||||
|
For the fat version (with login and security features enabled):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DISABLE_ADDITIONAL_FEATURES=false
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
To run the test script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script performs the following actions:
|
||||||
|
|
||||||
|
1. Builds all Docker images (full, ultra-lite, fat).
|
||||||
|
2. Runs each version to ensure it starts correctly.
|
||||||
|
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged.
|
||||||
|
|
||||||
|
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
||||||
|
|
||||||
|
### Full Testing with Docker
|
||||||
|
|
||||||
|
1. Build and run the Docker container per the above instructions:
|
||||||
|
|
||||||
|
2. Access the application at `http://localhost:8080` and manually test all features developed.
|
||||||
|
|
||||||
|
### Frontend Development Testing (Stirling 2.0)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
### Local Testing (Java and UI Components)
|
||||||
|
|
||||||
|
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
|
||||||
|
|
||||||
|
- Java backend logic
|
||||||
|
- RESTful API endpoints
|
||||||
|
- JavaScript functionality
|
||||||
|
- User interface components and styling
|
||||||
|
|
||||||
|
To run Stirling-PDF locally:
|
||||||
|
|
||||||
|
1. Compile and run the project using built-in IDE methods or by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task backend:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Access the application at `http://localhost:8080` in your web browser.
|
||||||
|
|
||||||
|
3. Manually test the features you're working on through the UI.
|
||||||
|
|
||||||
|
4. For API changes, use tools like Postman or curl to test endpoints directly.
|
||||||
|
|
||||||
|
Important notes:
|
||||||
|
|
||||||
|
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||||
|
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||||
|
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||||
|
|
||||||
|
## 8. Contributing
|
||||||
|
|
||||||
|
1. Fork the repository on GitHub.
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task check # lint + typecheck + test across all components
|
||||||
|
./test.sh # Docker integration tests (builds all variants + Cucumber)
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Push your changes to your fork.
|
||||||
|
7. Submit a pull request to the main repository.
|
||||||
|
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
When you raise a PR:
|
||||||
|
|
||||||
|
- The `test.sh` script will run automatically against your PR.
|
||||||
|
- The PR checks will verify versioning and dependency updates.
|
||||||
|
- Documentation will be automatically updated for dependency changes.
|
||||||
|
- Security issues will be checked using Snyk and PixeeBot.
|
||||||
|
|
||||||
|
Address any issues that arise from these checks before finalizing your pull request.
|
||||||
|
|
||||||
|
## 9. API Documentation
|
||||||
|
|
||||||
|
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
||||||
|
|
||||||
|
## 10. Customization
|
||||||
|
|
||||||
|
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
||||||
|
|
||||||
|
- Application name and branding
|
||||||
|
- Security settings
|
||||||
|
- UI customization
|
||||||
|
- Endpoint management
|
||||||
|
|
||||||
|
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to the main README for a full list of customization options.
|
||||||
|
|
||||||
|
## 11. Language Translations
|
||||||
|
|
||||||
|
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/scripts/replace_translation_line.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script helps you make consistent replacements across language files.
|
||||||
|
|
||||||
|
When contributing translations:
|
||||||
|
|
||||||
|
1. Use the helper script for multi-file changes.
|
||||||
|
2. Ensure all language files are updated consistently.
|
||||||
|
3. The PR checks will verify consistency in language file updates.
|
||||||
|
|
||||||
|
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
|
||||||
|
|
||||||
|
## Code examples
|
||||||
|
|
||||||
|
### React Component Development (Stirling 2.0)
|
||||||
|
|
||||||
|
For Stirling 2.0, new features are built as React components:
|
||||||
|
|
||||||
|
#### Creating a New Tool Component
|
||||||
|
|
||||||
|
1. **Create the React Component:**
|
||||||
|
```typescript
|
||||||
|
// frontend/editor/src/tools/NewTool.tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, FileInput, Container } from '@mantine/core';
|
||||||
|
|
||||||
|
interface NewToolProps {
|
||||||
|
params: Record<string, any>;
|
||||||
|
updateParams: (updates: Record<string, any>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewTool({ params, updateParams }: NewToolProps) {
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
|
const handleProcess = async () => {
|
||||||
|
// Process files using API or client-side logic
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<FileInput
|
||||||
|
multiple
|
||||||
|
accept="application/pdf"
|
||||||
|
onChange={setFiles}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleProcess}>Process</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add API Integration:**
|
||||||
|
```typescript
|
||||||
|
// Use existing API endpoints or create new ones
|
||||||
|
const response = await fetch('/api/v1/new-tool', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Register in Tool Picker:**
|
||||||
|
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||||
|
|
||||||
|
### Adding a New Feature to the Backend (API)
|
||||||
|
|
||||||
|
1. **Create a New Controller:**
|
||||||
|
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
|
||||||
|
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
||||||
|
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/new-feature")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class NewFeatureController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||||
|
public String newFeature() {
|
||||||
|
return "NewFeatureResponse";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Define the Service Layer:** (Not required but often useful)
|
||||||
|
- Create a new service class in the `stirling-pdf/src/main/java/stirling/software/SPDF/service` directory.
|
||||||
|
- Implement the business logic for the new feature.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NewFeatureService {
|
||||||
|
|
||||||
|
public String getNewFeatureData() {
|
||||||
|
// Implement business logic here
|
||||||
|
return "New Feature Data";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2b. **Integrate the Service with the Controller:**
|
||||||
|
|
||||||
|
- Autowire the service class in the controller and use it to handle the API request.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import stirling.software.SPDF.service.NewFeatureService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/new-feature")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class NewFeatureController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NewFeatureService newFeatureService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||||
|
public String newFeature() {
|
||||||
|
return newFeatureService.getNewFeatureData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
### 1. Locate Existing Language Files
|
||||||
|
|
||||||
|
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_fr_FR.properties`
|
||||||
|
- `messages_de_DE.properties`
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
### 2. Add New Translation Entries
|
||||||
|
|
||||||
|
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
|
||||||
|
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
|
||||||
|
you might add:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
pdfSplitter.title=PDF Splitter
|
||||||
|
pdfSplitter.description=Split your PDF into multiple documents
|
||||||
|
pdfSplitter.button.split=Split PDF
|
||||||
|
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.
|
||||||
|
|
||||||
|
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||||
-67
@@ -1,67 +0,0 @@
|
|||||||
# Main stage
|
|
||||||
FROM alpine:3.20.2
|
|
||||||
|
|
||||||
# Copy necessary files
|
|
||||||
COPY scripts /scripts
|
|
||||||
COPY pipeline /pipeline
|
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
|
||||||
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
|
||||||
COPY build/libs/*.jar app.jar
|
|
||||||
|
|
||||||
ARG VERSION_TAG
|
|
||||||
|
|
||||||
# Set Environment Variables
|
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
|
||||||
VERSION_TAG=$VERSION_TAG \
|
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
|
||||||
PUID=1000 \
|
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022
|
|
||||||
|
|
||||||
# JDK for app
|
|
||||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
|
||||||
apk upgrade --no-cache -a && \
|
|
||||||
apk add --no-cache \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
tini \
|
|
||||||
bash \
|
|
||||||
curl \
|
|
||||||
shadow \
|
|
||||||
su-exec \
|
|
||||||
openssl \
|
|
||||||
openssl-dev \
|
|
||||||
openjdk21-jre \
|
|
||||||
# Doc conversion
|
|
||||||
libreoffice \
|
|
||||||
# pdftohtml
|
|
||||||
poppler-utils \
|
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
|
||||||
ocrmypdf \
|
|
||||||
tesseract-ocr-data-eng \
|
|
||||||
# CV
|
|
||||||
py3-opencv \
|
|
||||||
# python3/pip
|
|
||||||
python3 \
|
|
||||||
py3-pip && \
|
|
||||||
# uno unoconv and HTML
|
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
|
||||||
fc-cache -f -v && \
|
|
||||||
chmod +x /scripts/* && \
|
|
||||||
chmod +x /scripts/init.sh && \
|
|
||||||
# User permissions
|
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
|
||||||
tesseract --list-langs
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
|
|
||||||
# Set user and run command
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Build the application
|
|
||||||
FROM gradle:8.7-jdk17 AS build
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the entire project to the working directory
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application with DOCKER_ENABLE_SECURITY=false
|
|
||||||
RUN DOCKER_ENABLE_SECURITY=true \
|
|
||||||
./gradlew clean build
|
|
||||||
|
|
||||||
# Main stage
|
|
||||||
FROM alpine:3.20.2
|
|
||||||
|
|
||||||
# Copy necessary files
|
|
||||||
COPY scripts /scripts
|
|
||||||
COPY pipeline /pipeline
|
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
|
||||||
COPY --from=build /app/build/libs/*.jar app.jar
|
|
||||||
|
|
||||||
ARG VERSION_TAG
|
|
||||||
|
|
||||||
# Set Environment Variables
|
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
|
||||||
VERSION_TAG=$VERSION_TAG \
|
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
|
||||||
PUID=1000 \
|
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022 \
|
|
||||||
FAT_DOCKER=true \
|
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
|
||||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
|
||||||
apk upgrade --no-cache -a && \
|
|
||||||
apk add --no-cache \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
tini \
|
|
||||||
bash \
|
|
||||||
curl \
|
|
||||||
shadow \
|
|
||||||
su-exec \
|
|
||||||
openssl \
|
|
||||||
openssl-dev \
|
|
||||||
openjdk21-jre \
|
|
||||||
# Doc conversion
|
|
||||||
libreoffice \
|
|
||||||
# pdftohtml
|
|
||||||
poppler-utils \
|
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
|
||||||
ocrmypdf \
|
|
||||||
tesseract-ocr-data-eng \
|
|
||||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
|
||||||
# CV
|
|
||||||
py3-opencv \
|
|
||||||
# python3/pip
|
|
||||||
python3 \
|
|
||||||
py3-pip && \
|
|
||||||
# uno unoconv and HTML
|
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
|
||||||
fc-cache -f -v && \
|
|
||||||
chmod +x /scripts/* && \
|
|
||||||
chmod +x /scripts/init.sh && \
|
|
||||||
# User permissions
|
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
|
||||||
tesseract --list-langs
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
|
|
||||||
# Set user and run command
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# use alpine
|
|
||||||
FROM alpine:3.20.2
|
|
||||||
|
|
||||||
ARG VERSION_TAG
|
|
||||||
|
|
||||||
# Set Environment Variables
|
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
|
||||||
VERSION_TAG=$VERSION_TAG \
|
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
|
||||||
PUID=1000 \
|
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022
|
|
||||||
|
|
||||||
# Copy necessary files
|
|
||||||
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
|
||||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
|
||||||
COPY pipeline /pipeline
|
|
||||||
COPY build/libs/*.jar app.jar
|
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
|
||||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
|
||||||
apk upgrade --no-cache -a && \
|
|
||||||
apk add --no-cache \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
tini \
|
|
||||||
bash \
|
|
||||||
curl \
|
|
||||||
shadow \
|
|
||||||
su-exec \
|
|
||||||
openjdk21-jre && \
|
|
||||||
# User permissions
|
|
||||||
mkdir /configs /logs /customFiles && \
|
|
||||||
chmod +x /scripts/*.sh && \
|
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
|
||||||
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- |
|
|
||||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
|
||||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| crop | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
|
||||||
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
|
||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
|
||||||
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
|
||||||
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
|
||||||
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
|
||||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
|
||||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
|
||||||
| compare | | | | ✔️ | | | | | | | ✔️ |
|
|
||||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
|
||||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
|
||||||
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
|
||||||
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
|
||||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
|
||||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
|
||||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
|
||||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
|
||||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
|
||||||
| sign | | | | ✔️ | | | | | | | ✔️ |
|
|
||||||
+444
@@ -0,0 +1,444 @@
|
|||||||
|
# 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`)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user