mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 21:30:14 +03:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c600c0d89 | ||
|
|
5ef2947c01 | ||
|
|
ca3b87e7fd | ||
|
|
7435b4f7ed | ||
|
|
a4f1696aba | ||
|
|
9eb8e2b7fd | ||
|
|
08d670cfb3 | ||
|
|
a9c50f2501 | ||
|
|
0b953f26b3 | ||
|
|
6b4e20b705 | ||
|
|
4e73d62530 | ||
|
|
8f50e373c4 |
@@ -32,7 +32,7 @@ vs `main`. Flags: `--html` (also emit a rendered HTML twin), `--no-screens`.
|
||||
- Read the PR description / commit messages for stated intent. Do **not** invent
|
||||
history or motivation that isn't evidenced (state current behavior in present tense).
|
||||
- Classify touched files by layer:
|
||||
- **Frontend**: tools (`frontend/src/editor/core/components/tools/*` or `.../core/tools/*`),
|
||||
- **Frontend**: tools (`frontend/editor/src/core/components/tools/*` or `.../core/tools/*`),
|
||||
hooks (`core/hooks/tools/*`, `useToolOperation`), contexts, routes, i18n
|
||||
(`public/locales/en-US`).
|
||||
- **Java backend**: controllers (`.../controller/api/...`), services, models, config.
|
||||
|
||||
@@ -53,11 +53,11 @@ gh pr diff <pr> --name-only # or: git diff --name-only <base>...HEAD
|
||||
Map changed frontend files to URLs generically:
|
||||
- **Tools**: a changed `components/tools/<toolDir>/…` or `hooks/tools/<tool>/…` →
|
||||
toolId → URL via the repo's own rule `getToolUrlPath` in
|
||||
[toolsTaxonomy.ts:200](frontend/src/editor/core/data/toolsTaxonomy.ts): `/` + the
|
||||
[toolsTaxonomy.ts:200](frontend/editor/src/core/data/toolsTaxonomy.ts): `/` + the
|
||||
id kebab-cased (`addPageNumbers` → `/add-page-numbers`).
|
||||
- **Pages/routes**: changed `filesPage/*` → `/files`, etc.
|
||||
- `--all`: enumerate every tool in the registry instead of just changed ones.
|
||||
Write `frontend/screenshots/ui-diff/targets.json` =
|
||||
Write `frontend/editor/screenshots/ui-diff/targets.json` =
|
||||
`[{ "id":"compress", "url":"/compress", "name":"Compress" }]`. This is what makes
|
||||
it generic - the spec never names a tool.
|
||||
|
||||
@@ -69,12 +69,12 @@ the full viewport - or the `--scope` container if given). Ensure the harness is
|
||||
(node_modules + icons).
|
||||
```
|
||||
# after = current head
|
||||
cd frontend && PR_SHOT_SIDE=after PR_SHOT_THEME=light \
|
||||
cd frontend/editor && PR_SHOT_SIDE=after PR_SHOT_THEME=light \
|
||||
npx playwright test --project=stubbed ui-before-after.spec.ts
|
||||
# before = base, in an isolated worktree (copy the spec + targets.json in)
|
||||
git worktree add ../ba-base origin/<baseRefName> # or the merge-base
|
||||
# set up its frontend, copy spec + screenshots/ui-diff/targets.json across, then:
|
||||
cd ../ba-base/frontend && PR_SHOT_SIDE=before PR_SHOT_THEME=light \
|
||||
cd ../ba-base/frontend/editor && PR_SHOT_SIDE=before PR_SHOT_THEME=light \
|
||||
npx playwright test --project=stubbed ui-before-after.spec.ts
|
||||
# copy its screenshots/ui-diff/before/ back next to after/. Repeat with
|
||||
# PR_SHOT_THEME=dark if --theme includes dark. Remove worktree when done.
|
||||
@@ -82,7 +82,7 @@ cd ../ba-base/frontend && PR_SHOT_SIDE=before PR_SHOT_THEME=light \
|
||||
|
||||
### 4. Auto-diff (surface what changed)
|
||||
```
|
||||
cd frontend && node <skill>/diff-shots.mjs \
|
||||
cd frontend/editor && node <skill>/diff-shots.mjs \
|
||||
screenshots/ui-diff/before screenshots/ui-diff/after screenshots/ui-diff
|
||||
```
|
||||
Produces `diff-report.json` classifying each view `unchanged | changed | added |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// unchanged | changed | added | removed, and CROP each changed pair to the
|
||||
// affected region (bounding box of differing pixels + padding) - unless the
|
||||
// change spans most of the page, in which case the full frame is kept.
|
||||
// Run from frontend (so deps resolve):
|
||||
// Run from frontend/editor (so deps resolve):
|
||||
// node <skill>/diff-shots.mjs <beforeDir> <afterDir> [outDir]
|
||||
// Env:
|
||||
// DIFF_THRESHOLD min fraction of differing pixels to count as changed (default 0.001)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Render each .tab-section of a montage HTML into its own PNG (the PR-ready image).
|
||||
// Run from frontend (so @playwright/test resolves):
|
||||
// Run from frontend/editor (so @playwright/test resolves):
|
||||
// node <skill>/shoot-sections.mjs <montage.html> <outDir>
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
@@ -28,13 +28,13 @@ current branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),
|
||||
## What this repo gives you (use it, don't reinvent)
|
||||
|
||||
- **Stubbed Playwright project** = backend-free screenshots via `page.route()` mocks.
|
||||
Reference implementation: `frontend/src/editor/core/tests/stubbed/files-page-screenshots.spec.ts`.
|
||||
Reference implementation: `frontend/editor/src/core/tests/stubbed/files-page-screenshots.spec.ts`.
|
||||
It already shows the light / **dark** / **RTL** passes, JWT seeding, IndexedDB
|
||||
seeding, and dumping PNGs to a `screenshots/<area>/` folder. Copy its shape.
|
||||
- Helpers: `frontend/src/editor/core/tests/helpers/ui-helpers.ts`
|
||||
- Helpers: `frontend/editor/src/core/tests/helpers/ui-helpers.ts`
|
||||
(`uploadFiles`, `openSettings`, `waitForModalOpen`, `dismissTourTooltip`, …)
|
||||
and the `stub-test-base` fixtures (`autoGoto`, `seedJwt`, `viewport`).
|
||||
- Config: `frontend/playwright.config.ts` (run from `frontend/`).
|
||||
- Config: `frontend/editor/playwright.config.ts` (run from `frontend/editor/`).
|
||||
- Report template: [report-template.html](report-template.html) - self-contained,
|
||||
one big image at a time, a global light/dark slider that flips every shot,
|
||||
thumbnail rail, prev/next + arrow keys, and a Findings tab.
|
||||
@@ -54,13 +54,13 @@ current branch diff. Flags: `--fix`, `--theme light|dark|both` (default both),
|
||||
Worktrees have no `node_modules` and no generated icons. From repo root:
|
||||
```
|
||||
cd frontend && npm ci # or junction main's node_modules (see memory)
|
||||
cd frontend && node scripts/generate-icons.js
|
||||
cd frontend/editor && node scripts/generate-icons.js
|
||||
```
|
||||
Kill any stale dev server first (it serves old modules):
|
||||
`Get-NetTCPConnection -LocalPort 5173 -State Listen | %{ Stop-Process -Id $_.OwningProcess -Force }`
|
||||
|
||||
### 3. Write the capture spec
|
||||
Create `frontend/src/editor/core/tests/stubbed/<feature>-walkthrough.spec.ts`,
|
||||
Create `frontend/editor/src/core/tests/stubbed/<feature>-walkthrough.spec.ts`,
|
||||
modeled on `files-page-screenshots.spec.ts`. For each enumerated view:
|
||||
- stub the APIs it needs, drive the UI to that state, wait on a real locator
|
||||
(not a fixed sleep), `await settle(page)` for Mantine portals, then
|
||||
@@ -71,7 +71,7 @@ modeled on `files-page-screenshots.spec.ts`. For each enumerated view:
|
||||
- Name shots `NN_<view>_<theme>.png` so light/dark pair up by suffix.
|
||||
- Prefer **stable test-ids** over translated accessible names (RTL/i18n breaks text locators).
|
||||
|
||||
Run it: `cd frontend && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.
|
||||
Run it: `cd frontend/editor && npx playwright test --project=stubbed <feature>-walkthrough.spec.ts`.
|
||||
Add `--project=stubbed-firefox`/`-webkit` only if cross-browser layout matters.
|
||||
|
||||
### 4. Build the report
|
||||
|
||||
+4
-4
@@ -26,16 +26,16 @@ version_builds/
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/playwright-report/
|
||||
frontend/editor/dist/
|
||||
frontend/editor/playwright-report/
|
||||
.npm/
|
||||
.yarn/
|
||||
|
||||
# Tauri/desktop builds
|
||||
src-tauri/target/
|
||||
src-tauri/dist/
|
||||
frontend/src-tauri/target/
|
||||
frontend/src-tauri/dist/
|
||||
frontend/editor/src-tauri/target/
|
||||
frontend/editor/src-tauri/dist/
|
||||
|
||||
# IDE and editor
|
||||
.idea/
|
||||
|
||||
@@ -92,12 +92,12 @@ frontend: &frontend
|
||||
# job on changes to any of these.
|
||||
tauri: &tauri
|
||||
- *ci
|
||||
- frontend/src-tauri/**
|
||||
- frontend/src/editor/desktop/**
|
||||
- frontend/tsconfig.desktop.vite.json
|
||||
- frontend/editor/src-tauri/**
|
||||
- frontend/editor/src/desktop/**
|
||||
- frontend/editor/tsconfig.desktop.vite.json
|
||||
- frontend/package.json
|
||||
- frontend/package-lock.json
|
||||
- frontend/vite.config.ts
|
||||
- frontend/editor/vite.config.ts
|
||||
- .github/workflows/tauri-build.yml
|
||||
- Taskfile.yml
|
||||
- .taskfiles/desktop.yml
|
||||
@@ -121,12 +121,10 @@ engine: &engine
|
||||
generated-models: &generated-models
|
||||
- *ci
|
||||
- *openapi
|
||||
- frontend/scripts/generate-tool-api-types.mts
|
||||
- frontend/src/editor/core/types/toolApiTypes.ts
|
||||
- frontend/src/editor/core/types/toolIO.ts
|
||||
- frontend/editor/scripts/generate-tool-api-types.mts
|
||||
- frontend/editor/src/core/types/toolApiTypes.ts
|
||||
- engine/scripts/generate_tool_models.py
|
||||
- engine/src/stirling/models/tool_models.py
|
||||
- engine/src/stirling/models/tool_io.py
|
||||
- .taskfiles/frontend.yml
|
||||
- .taskfiles/engine.yml
|
||||
- .github/workflows/check-generated-models.yml
|
||||
@@ -135,7 +133,7 @@ licenses-frontend: &licenses-frontend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
- "frontend/editor/scripts/generate-licenses.js"
|
||||
|
||||
licenses-backend: &licenses-backend
|
||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||
@@ -146,8 +144,8 @@ licenses-backend: &licenses-backend
|
||||
proprietary: &proprietary
|
||||
- *ci
|
||||
- app/proprietary/**
|
||||
- frontend/src/editor/proprietary/**
|
||||
- frontend/src/editor/core/tests/enterprise/**
|
||||
- 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
|
||||
|
||||
@@ -128,9 +128,9 @@ updates:
|
||||
|
||||
- package-ecosystem: cargo
|
||||
directories:
|
||||
- /frontend/src-tauri
|
||||
- /frontend/src-tauri/thumbnail-handler
|
||||
- /frontend/src-tauri/provisioner
|
||||
- /frontend/editor/src-tauri
|
||||
- /frontend/editor/src-tauri/thumbnail-handler
|
||||
- /frontend/editor/src-tauri/provisioner
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
|
||||
@@ -51,7 +51,7 @@ labels:
|
||||
|
||||
- label: 'Translation'
|
||||
files:
|
||||
- 'frontend/public/locales/[a-zA-Z]{2}-[a-zA-Z\-]{2,7}/translation.toml'
|
||||
- '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'
|
||||
@@ -70,8 +70,8 @@ labels:
|
||||
|
||||
- label: 'Tauri'
|
||||
files:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src-tauri/.*'
|
||||
- 'frontend/editor/src-tauri/**'
|
||||
- 'frontend/editor/src-tauri/.*'
|
||||
|
||||
- label: 'engine'
|
||||
files:
|
||||
|
||||
@@ -13,7 +13,7 @@ Usage:
|
||||
"""
|
||||
|
||||
# Sample for Windows:
|
||||
# python .github/scripts/check_language_toml.py --reference-file frontend/public/locales/en-US/translation.toml --branch "" --files frontend/public/locales/de-DE/translation.toml frontend/public/locales/fr-FR/translation.toml
|
||||
# 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
|
||||
@@ -308,7 +308,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-US/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/public/locales/en-US/translation.toml)"
|
||||
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_**")
|
||||
|
||||
@@ -14,7 +14,9 @@ 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/src-tauri/tauri.conf.json")
|
||||
CONF = Path(
|
||||
sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json"
|
||||
)
|
||||
|
||||
|
||||
def load_pubkey():
|
||||
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
|
||||
- name: Set up Node.js for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
@@ -211,16 +211,10 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: ai-engine
|
||||
|
||||
@@ -40,16 +40,20 @@ jobs:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
@@ -60,18 +60,8 @@ jobs:
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -294,7 +284,7 @@ jobs:
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: >
|
||||
npx tsx scripts/report-flaky-tests.mts
|
||||
npx tsx editor/scripts/report-flaky-tests.mts
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-oauth.json"
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-saml.json"
|
||||
"${{ github.workspace }}/frontend/playwright-report/results-feature.json"
|
||||
|
||||
+11
-48
@@ -60,43 +60,8 @@ jobs:
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
gradle-cache-prime:
|
||||
name: Prime shared Gradle cache
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
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 User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
- name: Resolve backend dependencies
|
||||
run: ./gradlew :stirling-pdf:classes -PnoSpotless --no-daemon
|
||||
env:
|
||||
STIRLING_FLAVOR: saas
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
|
||||
build:
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -111,7 +76,7 @@ jobs:
|
||||
# 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, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/db-migration-test.yml
|
||||
@@ -119,7 +84,7 @@ jobs:
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/check-openapi.yml
|
||||
@@ -155,7 +120,7 @@ jobs:
|
||||
|
||||
playwright-e2e-live:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/e2e-live.yml
|
||||
@@ -163,7 +128,7 @@ jobs:
|
||||
|
||||
playwright-e2e-enterprise:
|
||||
if: needs.files-changed.outputs.proprietary == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build-enterprise.yml
|
||||
@@ -171,7 +136,7 @@ jobs:
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build, gradle-cache-prime]
|
||||
needs: [files-changed, build]
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/check-licence.yml
|
||||
@@ -179,7 +144,7 @@ jobs:
|
||||
|
||||
docker-compose-tests:
|
||||
if: needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
@@ -191,7 +156,7 @@ jobs:
|
||||
|
||||
test-build-docker-images:
|
||||
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
||||
needs: [files-changed, build, check-generateOpenApiDocs, check-licence, gradle-cache-prime]
|
||||
needs: [files-changed, build, check-generateOpenApiDocs, check-licence]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
@@ -209,14 +174,12 @@ jobs:
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/tauri-build.yml
|
||||
secrets: inherit
|
||||
# PR smoke build: macOS + Windows (the platforms our developers use).
|
||||
# sign: true only reaches macOS - tauri-build's per-platform gate keeps
|
||||
# Windows/Linux signing on main, and an unsigned .dmg cannot be opened.
|
||||
# PR smoke build: macOS + Windows (the platforms our developers use).
|
||||
# The full signed multi-OS matrix runs on release;
|
||||
# nightly still warms the Rust cache with all-OS defaults.
|
||||
with:
|
||||
platform: windows-macos
|
||||
sign: true
|
||||
sign: false
|
||||
|
||||
ai-engine:
|
||||
if: needs.files-changed.outputs.engine == 'true'
|
||||
@@ -234,7 +197,7 @@ jobs:
|
||||
# frontend filter, so a CSS-only PR does not pay for a backend build.
|
||||
generated-models:
|
||||
if: needs.files-changed.outputs.generated-models == 'true'
|
||||
needs: [files-changed, gradle-cache-prime]
|
||||
needs: [files-changed]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name: Check generated models
|
||||
|
||||
# Verifies the committed generated files are still in sync with the Java OpenAPI
|
||||
# spec: the request models (toolApiTypes.ts, tool_models.py) and the tool I/O
|
||||
# tables saying what each endpoint accepts and produces (toolIO.ts, tool_io.py).
|
||||
# Regenerates them all with the single top-level `task tool-models` and fails if
|
||||
# any committed file is out of date. Called from build.yml when the
|
||||
# backend Java, frontend, or engine changes; also runs on push to main as a
|
||||
# post-merge safety net.
|
||||
# Verifies the committed generated API models are still in sync with the Java
|
||||
# OpenAPI spec: the frontend tool API types
|
||||
# (frontend/editor/src/core/types/toolApiTypes.ts) and the engine tool
|
||||
# models (engine/src/stirling/models/tool_models.py). Regenerates both with the
|
||||
# single top-level `task tool-models` and fails if either committed file is
|
||||
# out of date. Called from build.yml when the backend Java, frontend, or engine
|
||||
# changes; also runs on push to main as a post-merge safety net.
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: generated-models
|
||||
@@ -42,19 +42,13 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -63,10 +57,18 @@ jobs:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
# Rebuilds the OpenAPI spec from the current Java and regenerates both the
|
||||
# frontend types and the engine tool models from it.
|
||||
- name: Regenerate generated models
|
||||
run: task tool-models
|
||||
|
||||
- name: Verify generated models are up to date
|
||||
id: models-check
|
||||
continue-on-error: true
|
||||
run: task tool-models:check
|
||||
run: |
|
||||
git diff --exit-code \
|
||||
frontend/editor/src/core/types/toolApiTypes.ts \
|
||||
engine/src/stirling/models/tool_models.py
|
||||
|
||||
- name: Comment on generated models check failure
|
||||
# Only post a comment on PRs. github-script's PR helpers need an
|
||||
@@ -81,9 +83,9 @@ jobs:
|
||||
marker,
|
||||
'### Generated Models Check Failed',
|
||||
'',
|
||||
'One or more generated files are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.',
|
||||
'The generated `frontend/editor/src/core/types/toolApiTypes.ts` and/or `engine/src/stirling/models/tool_models.py` are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.',
|
||||
'',
|
||||
'Run `task tool-models` to regenerate them, then commit the updated files.',
|
||||
'Run `task tool-models` to regenerate both, then commit the updated files.',
|
||||
].join('\n');
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
@@ -114,10 +116,11 @@ jobs:
|
||||
echo " Generated Models Check Failed"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "One or more generated files are out of date with the Java"
|
||||
echo "OpenAPI spec and will need to be regenerated before merging."
|
||||
echo "The generated frontend API types and/or engine tool"
|
||||
echo "models are out of date with the Java OpenAPI spec and"
|
||||
echo "will need to be regenerated before they can be merged in."
|
||||
echo ""
|
||||
echo "Run 'task tool-models' to regenerate them, then"
|
||||
echo "Run 'task tool-models' to regenerate both, then"
|
||||
echo "commit the updated files."
|
||||
echo "============================================"
|
||||
exit 1
|
||||
|
||||
@@ -26,16 +26,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
@@ -27,16 +27,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
- "frontend/editor/public/locales/*/translation.toml"
|
||||
- ".github/scripts/check_language_toml.py"
|
||||
- ".github/workflows/check_toml.yml"
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for TOML translation files
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/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"
|
||||
@@ -166,12 +166,12 @@ jobs:
|
||||
|
||||
// Determine reference file
|
||||
let referenceFilePath;
|
||||
if (changedFiles.includes("frontend/public/locales/en-US/translation.toml")) {
|
||||
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/public/locales/en-US/translation.toml",
|
||||
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||
ref: branch,
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
path: "frontend/public/locales/en-US/translation.toml",
|
||||
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||
ref: "main",
|
||||
});
|
||||
|
||||
|
||||
@@ -46,16 +46,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -77,7 +81,7 @@ jobs:
|
||||
# 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
pattern: jacoco-exec-*
|
||||
path: coverage-execs/
|
||||
@@ -202,7 +206,7 @@ jobs:
|
||||
# absence on backend-only runs by skipping the download entirely
|
||||
# when the producer job was not part of this workflow run.
|
||||
if: inputs.frontend-validation-result == 'success'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: frontend-coverage
|
||||
path: matrix-inputs/vitest/
|
||||
@@ -212,7 +216,7 @@ jobs:
|
||||
# e2e-live uploads the artifact with a stable name. Skip the
|
||||
# download entirely when the producer job did not run.
|
||||
if: inputs.playwright-e2e-live-result == 'success'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0
|
||||
with:
|
||||
name: playwright-frontend-coverage
|
||||
path: matrix-inputs/playwright/
|
||||
|
||||
@@ -30,19 +30,23 @@ jobs:
|
||||
java-version: 25
|
||||
distribution: temurin
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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') }}
|
||||
|
||||
# Keep the normal formatting path here so this smoke test exercises the
|
||||
# same Gradle configuration as the backend build.
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.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 }}
|
||||
|
||||
@@ -38,16 +38,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
# When the PR changes the base image, test.sh builds it locally
|
||||
# (stirling-pdf-base:local) into the daemon image store. A buildx
|
||||
|
||||
@@ -25,36 +25,8 @@ jobs:
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
# Gradle does not retry 429s, and a cold cache resolving the buildscript
|
||||
# classpath is exactly where Maven Central rate-limits us. Retry it here,
|
||||
# where a failure is cheap, instead of inside the backgrounded bootRun.
|
||||
- name: Prime Gradle dependencies
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
if ./gradlew --quiet -PnoSpotless :stirling-pdf:classes; then
|
||||
exit 0
|
||||
fi
|
||||
echo "::warning::Gradle dependency resolution failed (attempt $attempt of 3)"
|
||||
sleep $((attempt * 30))
|
||||
done
|
||||
echo "::error::Gradle could not resolve dependencies after 3 attempts"
|
||||
exit 1
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -81,11 +53,6 @@ jobs:
|
||||
# to aggregate. Chromium-only - other engines silently skip.
|
||||
PW_COVERAGE: "1"
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
# Internal mirror, as in backend-build.yml. Empty on Dependabot and
|
||||
# fork PRs, where the build falls back to Maven Central.
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
run: task e2e:live
|
||||
- name: Flag flaky tests
|
||||
# Runs regardless of the test outcome: a flaky test (passed on retry)
|
||||
@@ -93,16 +60,12 @@ jobs:
|
||||
# ::warning:: annotations + a job summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: npx tsx scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
run: npx tsx editor/scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
- name: Generate JaCoCo report from e2e:live .exec
|
||||
if: always()
|
||||
id: live-coverage
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
# `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
|
||||
@@ -230,8 +193,8 @@ jobs:
|
||||
run: |
|
||||
echo "::group::Playwright output dirs"
|
||||
# Playwright anchors its default outputDir + HTML report to the
|
||||
# nearest package.json, which is frontend/ (frontend has
|
||||
# none), so artifacts land under frontend/, not frontend/.
|
||||
# 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 \
|
||||
@@ -246,7 +209,7 @@ jobs:
|
||||
# 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 has none).
|
||||
# package.json, which is frontend/; frontend/editor has none).
|
||||
path: |
|
||||
frontend/playwright-report/
|
||||
frontend/test-results/
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
# ::warning:: annotations + a job summary; never fails the job.
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: npx tsx scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
run: npx tsx editor/scripts/report-flaky-tests.mts "$PLAYWRIGHT_JSON_OUTPUT_FILE"
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
|
||||
- name: Upload Playwright report
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
# Need the base branch too, to diff against it.
|
||||
fetch-depth: 0
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -113,31 +113,31 @@ jobs:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
working-directory: frontend
|
||||
run: |
|
||||
mkdir -p src/assets
|
||||
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
|
||||
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/scripts/generate-licenses.js \
|
||||
--input frontend/src/assets/3rdPartyLicenses.json
|
||||
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/src/assets
|
||||
if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then
|
||||
cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json
|
||||
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/src/assets/license-warnings.json" ]; then
|
||||
cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json
|
||||
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/src/assets/license-warnings.json" ]; then
|
||||
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
|
||||
@@ -185,10 +185,10 @@ jobs:
|
||||
echo ""
|
||||
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
|
||||
echo "❌ **Failed** – incompatible or unknown licenses found."
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
if [ -f "frontend/editor/src/assets/license-warnings.json" ]; then
|
||||
echo ""
|
||||
echo "### Warnings"
|
||||
jq -r '.warnings[] | "- \(.message)"' frontend/src/assets/license-warnings.json || true
|
||||
jq -r '.warnings[] | "- \(.message)"' frontend/editor/src/assets/license-warnings.json || true
|
||||
fi
|
||||
else
|
||||
echo "✅ **Passed** – no license warnings detected."
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
const fs = require('fs');
|
||||
let warningDetails = '';
|
||||
try {
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
|
||||
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';
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
- name: Commit changes (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
git add frontend/src/assets/3rdPartyLicenses.json
|
||||
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
|
||||
|
||||
@@ -272,7 +272,7 @@ jobs:
|
||||
|
||||
The following licenses may require review for corporate compatibility:
|
||||
|
||||
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
$(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
|
||||
@@ -350,16 +350,10 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -130,19 +130,19 @@ jobs:
|
||||
run: |
|
||||
python scripts/coverage-summary.py \
|
||||
--title "Frontend Vitest coverage" \
|
||||
--vitest frontend/coverage/coverage-summary.json \
|
||||
--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/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/dist/
|
||||
path: frontend/editor/dist/
|
||||
retention-days: 3
|
||||
|
||||
@@ -12,14 +12,13 @@ on:
|
||||
- "true"
|
||||
- "false"
|
||||
platform:
|
||||
description: "Platform to build (windows, windows-arm64, macos, linux, or all)"
|
||||
description: "Platform to build (windows, macos, linux, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- windows
|
||||
- windows-arm64
|
||||
- macos
|
||||
- linux
|
||||
sign:
|
||||
@@ -57,16 +56,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -84,35 +87,24 @@ jobs:
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
# windows-arm64: NSIS only (WiX MSI has no arm64 support in Tauri) and no
|
||||
# JPDFium natives yet - flip to windows-arm64 once JPDFium ships them.
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
||||
WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}'
|
||||
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"}'
|
||||
ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX"
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"windows-arm64")
|
||||
echo "matrix={\"include\":[$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
||||
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\":[$MACOS]}" >> $GITHUB_OUTPUT
|
||||
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\":[$LINUX]}" >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo "matrix={\"include\":[$ALL]}" >> $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\":[$ALL]}" >> $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
|
||||
fi
|
||||
|
||||
build-jars:
|
||||
@@ -147,20 +139,14 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.variant.build_frontend == true
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -222,7 +208,7 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -250,23 +236,16 @@ jobs:
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -292,7 +271,7 @@ jobs:
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ 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 }}
|
||||
@@ -302,7 +281,7 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ 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..."
|
||||
@@ -337,7 +316,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
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 }}
|
||||
@@ -405,7 +384,7 @@ jobs:
|
||||
# Without this, signCommand failures are opaque (Tauri captures but drops
|
||||
# smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
|
||||
- name: Preflight smctl
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ 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 }}
|
||||
@@ -436,12 +415,12 @@ jobs:
|
||||
# smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
|
||||
# from env (set by prior DigiCert setup step). No --config-file needed.
|
||||
- name: Configure Windows code signing
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ 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/src-tauri/tauri.windows.conf.json <<EOF
|
||||
cat > ./frontend/editor/src-tauri/tauri.windows.conf.json <<EOF
|
||||
{
|
||||
"bundle": {
|
||||
"windows": {
|
||||
@@ -454,7 +433,7 @@ jobs:
|
||||
}
|
||||
EOF
|
||||
echo "Generated tauri.windows.conf.json (alias masked):"
|
||||
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/src-tauri/tauri.windows.conf.json
|
||||
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')
|
||||
@@ -502,59 +481,11 @@ jobs:
|
||||
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
args: ${{ matrix.args }}
|
||||
updaterJsonKeepUniversal: true
|
||||
|
||||
# Bundled libwayland conflicts with the host's on some distros (Fedora
|
||||
# Wayland: EGL_BAD_PARAMETER, blank window - #6878). Repack without it,
|
||||
# then regenerate the updater .sig (repack invalidates the original) and
|
||||
# GPG-sign again when release signing is on.
|
||||
- name: Strip bundled Wayland libs from AppImage
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
continue-on-error: true
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
|
||||
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AI=$(find "$PWD/frontend/src-tauri/target" -name "*.AppImage" | head -1)
|
||||
if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi
|
||||
chmod +x "$AI"
|
||||
WORK=$(mktemp -d)
|
||||
(cd "$WORK" && "$AI" --appimage-extract >/dev/null)
|
||||
if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then
|
||||
echo "No bundled libwayland - nothing to strip"
|
||||
rm -rf "$WORK"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$WORK/squashfs-root/usr/lib/"libwayland-*
|
||||
curl -fsSL -o "$WORK/appimagetool" \
|
||||
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
# Pinned checksum: never execute an unverified downloaded binary. On
|
||||
# mismatch (upstream rebuilt continuous) the step aborts and the
|
||||
# original AppImage ships unchanged - update the pin deliberately.
|
||||
echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c -
|
||||
chmod +x "$WORK/appimagetool"
|
||||
SIGN_ARGS=()
|
||||
if [ "$GPG_SIGN" = "1" ] && [ -n "${SIGN_KEY:-}" ]; then
|
||||
SIGN_ARGS=(--sign --sign-key "$SIGN_KEY")
|
||||
fi
|
||||
"$WORK/appimagetool" --appimage-extract-and-run "${SIGN_ARGS[@]}" "$WORK/squashfs-root" "$AI.new"
|
||||
# Updater payload signature must match the repacked bytes. The CLI
|
||||
# reads the key/password from env - never pass secrets as argv.
|
||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||||
(cd frontend && npx tauri signer sign "$AI.new")
|
||||
mv "$AI.new.sig" "$AI.sig"
|
||||
fi
|
||||
mv "$AI.new" "$AI"
|
||||
rm -rf "$WORK"
|
||||
echo "Stripped bundled libwayland from $(basename "$AI")"
|
||||
|
||||
- name: Clear release GPG key from runner keyring (Linux)
|
||||
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||
env:
|
||||
@@ -571,35 +502,14 @@ jobs:
|
||||
# artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
|
||||
# cargo output unsigned, so checking it produces false negatives.
|
||||
- name: Verify Windows Code Signature
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
||||
if: ${{ 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') }}
|
||||
timeout-minutes: 15
|
||||
shell: pwsh
|
||||
run: |
|
||||
# arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the
|
||||
# inner exe before packing and the setup exe after, so verifying the setup
|
||||
# exe is the arm64 equivalent of the MSI + inner-exe check below.
|
||||
if ("${{ matrix.platform }}" -eq "windows-11-arm") {
|
||||
$setupExes = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*-setup.exe" -Recurse -File
|
||||
if ($setupExes.Count -eq 0) {
|
||||
Write-Host "[ERROR] No NSIS installer found under target/"
|
||||
exit 1
|
||||
}
|
||||
foreach ($exe in $setupExes) {
|
||||
$sig = Get-AuthenticodeSignature -FilePath $exe.FullName
|
||||
Write-Host "NSIS installer: $($exe.Name) Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] NSIS installer is not signed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
Write-Host "[SUCCESS] NSIS installer is properly signed"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$allSigned = $true
|
||||
|
||||
# Check MSI installer (outer wrapper - what users download)
|
||||
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
||||
$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
|
||||
@@ -659,7 +569,7 @@ jobs:
|
||||
# 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() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }}
|
||||
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||
@@ -682,7 +592,7 @@ jobs:
|
||||
# Absolute dist path so the cd below can't break the copy targets.
|
||||
DIST="$GITHUB_WORKSPACE/dist"
|
||||
mkdir -p "$DIST"
|
||||
cd ./frontend/src-tauri/target
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
|
||||
echo "=== tauri bundle artifacts ==="
|
||||
find . -path "*/bundle/*" \( -name "*.msi" -o -name "*.deb" \
|
||||
@@ -695,11 +605,6 @@ jobs:
|
||||
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 }}" = "windows-11-arm" ]; then
|
||||
# arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri).
|
||||
# The setup exe is also its own updater payload (-> sibling .sig).
|
||||
find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \;
|
||||
find . -name "*-setup.exe.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe.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).
|
||||
@@ -743,7 +648,7 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts/verify-updater-signatures.py
|
||||
frontend/src-tauri/tauri.conf.json
|
||||
frontend/editor/src-tauri/tauri.conf.json
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Download all Tauri artifacts
|
||||
@@ -814,10 +719,6 @@ jobs:
|
||||
'bundles': ['Stirling-PDF-windows-x86_64.msi'],
|
||||
'targets': ['windows-x86_64-msi', 'windows-x86_64'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-windows-arm64-setup.exe'],
|
||||
'targets': ['windows-aarch64-nsis', 'windows-aarch64'],
|
||||
},
|
||||
{
|
||||
'bundles': ['Stirling-PDF-macos-universal.app.tar.gz'],
|
||||
'targets': ['darwin-x86_64', 'darwin-aarch64'],
|
||||
@@ -892,7 +793,7 @@ jobs:
|
||||
run: |
|
||||
python3 -m pip install --quiet 'cryptography==44.0.0'
|
||||
python3 .github/scripts/verify-updater-signatures.py \
|
||||
./artifacts/tauri frontend/src-tauri/tauri.conf.json
|
||||
./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
|
||||
@@ -902,7 +803,7 @@ jobs:
|
||||
# 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@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||
# Don't regenerate/append notes on re-runs, and don't force this into the
|
||||
@@ -916,7 +817,6 @@ jobs:
|
||||
files: |
|
||||
./artifacts/**/*.jar
|
||||
./artifacts/**/*.msi
|
||||
./artifacts/**/*-setup.exe
|
||||
./artifacts/**/*.dmg
|
||||
./artifacts/**/*.app.tar.gz
|
||||
./artifacts/**/*.deb
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: pre-commit
|
||||
|
||||
@@ -65,16 +65,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-${{ runner.os }}-
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -39,16 +39,10 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
@@ -84,12 +84,12 @@ jobs:
|
||||
body: |
|
||||
Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot].
|
||||
|
||||
Regenerates `frontend/src/processor/proprietary/generated/docsManifest.json`
|
||||
Regenerates `frontend/editor/src/portal/generated/docsManifest.json`
|
||||
from the Stirling docs repo via `npm run docs:sync`.
|
||||
labels: |
|
||||
Documentation
|
||||
github-actions
|
||||
Front End
|
||||
add-paths: frontend/src/processor/proprietary/generated/docsManifest.json
|
||||
add-paths: frontend/editor/src/portal/generated/docsManifest.json
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
- "app/proprietary/build.gradle"
|
||||
- "gradle/spotless.gradle"
|
||||
- "README.md"
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
- "frontend/editor/public/locales/*/translation.toml"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-suffix: sync-files
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/public/locales/en-US/translation.toml" --branch main
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||
|
||||
- name: Sort translation TOML files
|
||||
run: |
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add frontend/public/locales/*/translation.toml
|
||||
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
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`frontend/public/locales/*/translation.toml`) to reflect changes in the reference file `en-US/translation.toml`.
|
||||
- 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
|
||||
@@ -135,5 +135,5 @@ jobs:
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
frontend/public/locales/*/translation.toml
|
||||
frontend/editor/public/locales/*/translation.toml
|
||||
scripts/ignore_translation.toml
|
||||
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)."
|
||||
description: "Platform to build (windows, macos, linux, windows-macos, or all)."
|
||||
required: false
|
||||
type: string
|
||||
default: "all"
|
||||
@@ -29,14 +29,13 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)"
|
||||
description: "Platform to build (windows, macos, linux, windows-macos, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- windows
|
||||
- windows-arm64
|
||||
- macos
|
||||
- linux
|
||||
- windows-macos
|
||||
@@ -74,19 +73,15 @@ jobs:
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
||||
# ARM64: NSIS only (WiX MSI has no arm64 support in Tauri) and no JPDFium
|
||||
# natives yet - flip jpdfium_platforms to windows-arm64 once JPDFium ships it.
|
||||
WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}'
|
||||
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" "$WINDOWS_ARM64") ;;
|
||||
windows-arm64) ENTRIES=("$WINDOWS_ARM64") ;;
|
||||
windows) ENTRIES=("$WINDOWS") ;;
|
||||
macos) ENTRIES=("$MACOS") ;;
|
||||
linux) ENTRIES=("$LINUX") ;;
|
||||
windows-macos) ENTRIES=("$WINDOWS" "$MACOS") ;;
|
||||
*) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64" "$MACOS" "$LINUX") ;;
|
||||
*) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;;
|
||||
esac
|
||||
|
||||
# Drop macOS entries when Apple certificate secret is unavailable
|
||||
@@ -113,11 +108,6 @@ jobs:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
# Per-platform sign gate. macOS signs on any run with the cert available,
|
||||
# PRs included: Gatekeeper blocks an unsigned .dmg, so an unsigned macOS
|
||||
# PR build is not testable. Windows and Linux stay main-only, matching the
|
||||
# gates on their own signing steps below.
|
||||
SIGN_BUNDLE: ${{ inputs.sign && (matrix.platform == 'macos-15' && secrets.APPLE_CERTIFICATE != '' || github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
@@ -134,7 +124,7 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -152,7 +142,7 @@ jobs:
|
||||
- name: Cache Rust build
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: frontend/src-tauri
|
||||
workspaces: frontend/editor/src-tauri
|
||||
# Stable key shared across workflows so the nightly warmer.
|
||||
# rust-cache still appends OS + rustc + Cargo.lock.
|
||||
shared-key: tauri-${{ matrix.name }}
|
||||
@@ -172,23 +162,16 @@ jobs:
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||||
|
||||
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
@@ -215,7 +198,7 @@ jobs:
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
id: digicert-setup
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
@@ -225,7 +208,7 @@ jobs:
|
||||
SM_HOST: ${{ secrets.SM_HOST }}
|
||||
|
||||
- name: Setup DigiCert KeyLocker Certificate
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Setting up DigiCert KeyLocker environment..."
|
||||
@@ -260,7 +243,7 @@ jobs:
|
||||
|
||||
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
||||
- name: Import Windows Code Signing Certificate
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && 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 }}
|
||||
@@ -291,7 +274,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -312,7 +295,7 @@ jobs:
|
||||
rm certificate.p12
|
||||
|
||||
- name: Verify Certificate
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
@@ -335,7 +318,7 @@ jobs:
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Preflight smctl
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
@@ -348,12 +331,12 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
||||
|
||||
- name: Configure Windows code signing
|
||||
if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
||||
run: |
|
||||
cat > ./frontend/src-tauri/tauri.windows.conf.json <<EOF
|
||||
cat > ./frontend/editor/src-tauri/tauri.windows.conf.json <<EOF
|
||||
{
|
||||
"bundle": {
|
||||
"windows": {
|
||||
@@ -385,7 +368,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build Tauri app (signed)
|
||||
if: env.SIGN_BUNDLE == 'true'
|
||||
if: inputs.sign
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -412,7 +395,7 @@ jobs:
|
||||
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
|
||||
# AppImage runs in its own continue-on-error step below so its
|
||||
@@ -420,7 +403,7 @@ jobs:
|
||||
args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
|
||||
|
||||
- name: Build Tauri app (unsigned)
|
||||
if: env.SIGN_BUNDLE != 'true'
|
||||
if: ${{ !inputs.sign }}
|
||||
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -432,7 +415,7 @@ jobs:
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
|
||||
# AppImage runs in its own continue-on-error step below so its
|
||||
@@ -460,41 +443,10 @@ jobs:
|
||||
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
projectPath: ./frontend/editor
|
||||
tauriScript: npx tauri
|
||||
args: --bundles appimage
|
||||
|
||||
# Bundled libwayland conflicts with the host's on some distros (Fedora
|
||||
# Wayland: EGL_BAD_PARAMETER, blank window - #6878). The AppImage
|
||||
# ecosystem excludelist agrees these libs must come from the system.
|
||||
- name: Strip bundled Wayland libs from AppImage
|
||||
if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AI=$(find "$PWD/frontend/src-tauri/target" -name "*.AppImage" | head -1)
|
||||
if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi
|
||||
chmod +x "$AI"
|
||||
WORK=$(mktemp -d)
|
||||
(cd "$WORK" && "$AI" --appimage-extract >/dev/null)
|
||||
if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then
|
||||
echo "No bundled libwayland - nothing to strip"
|
||||
rm -rf "$WORK"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$WORK/squashfs-root/usr/lib/"libwayland-*
|
||||
curl -fsSL -o "$WORK/appimagetool" \
|
||||
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
# Pinned checksum: never execute an unverified downloaded binary. On
|
||||
# mismatch (upstream rebuilt continuous) the step aborts and the
|
||||
# original AppImage ships unchanged - update the pin deliberately.
|
||||
echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c -
|
||||
chmod +x "$WORK/appimagetool"
|
||||
"$WORK/appimagetool" --appimage-extract-and-run "$WORK/squashfs-root" "$AI.new"
|
||||
mv "$AI.new" "$AI"
|
||||
rm -rf "$WORK"
|
||||
echo "Stripped bundled libwayland from $(basename "$AI")"
|
||||
|
||||
- name: Clear release GPG key from runner keyring (Linux)
|
||||
if: always() && inputs.sign && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
@@ -506,10 +458,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Verify notarization (macOS only)
|
||||
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
|
||||
if: inputs.sign && matrix.platform == 'macos-15'
|
||||
run: |
|
||||
echo "🔍 Verifying notarization status..."
|
||||
cd ./frontend/src-tauri/target
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
DMG_FILE=$(find . -name "*.dmg" | head -1)
|
||||
if [ -n "$DMG_FILE" ]; then
|
||||
echo "Found DMG: $DMG_FILE"
|
||||
@@ -526,16 +478,13 @@ jobs:
|
||||
# Absolute dist path so the cd below can't break the copy targets.
|
||||
DIST="$GITHUB_WORKSPACE/dist"
|
||||
mkdir -p "$DIST"
|
||||
cd ./frontend/src-tauri/target
|
||||
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 }}" = "windows-11-arm" ]; then
|
||||
# arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri).
|
||||
find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
else
|
||||
@@ -547,28 +496,9 @@ jobs:
|
||||
# Verify the MSI AND the inner exe extracted from it are signed.
|
||||
# The inner exe is what gets installed on users' machines and what AV scans.
|
||||
- name: Verify Windows Code Signature
|
||||
if: inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||
if: inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the
|
||||
# inner exe before packing and the setup exe after, so verifying the setup
|
||||
# exe is the arm64 equivalent of the MSI + inner-exe check below.
|
||||
if ("${{ matrix.platform }}" -eq "windows-11-arm") {
|
||||
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}-setup.exe"
|
||||
if (-not (Test-Path $exePath)) {
|
||||
Write-Host "[ERROR] NSIS installer not found at $exePath"
|
||||
exit 1
|
||||
}
|
||||
$sig = Get-AuthenticodeSignature -FilePath $exePath
|
||||
Write-Host "NSIS installer: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
||||
if ($sig.Status -ne "Valid") {
|
||||
Write-Host "[ERROR] NSIS installer is not signed"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[SUCCESS] NSIS installer is properly signed"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$allSigned = $true
|
||||
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
||||
|
||||
@@ -614,7 +544,7 @@ jobs:
|
||||
Write-Host "[SUCCESS] MSI and inner exe are properly signed"
|
||||
|
||||
- name: Dump smctl logs on failure
|
||||
if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }}
|
||||
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
||||
@@ -638,10 +568,10 @@ jobs:
|
||||
- name: Verify build artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
cd ./frontend/editor/src-tauri/target
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ] || [ "${{ matrix.platform }}" = "windows-11-arm" ]; then
|
||||
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
|
||||
@@ -669,7 +599,7 @@ jobs:
|
||||
- name: Test artifact sizes
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
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
|
||||
@@ -720,7 +650,6 @@ jobs:
|
||||
// Map of expected artifact names to display info
|
||||
const artifactMap = {
|
||||
'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' },
|
||||
'Stirling-PDF-windows-arm64': { icon: '🪟', platform: 'Windows ARM64', files: '-setup.exe (NSIS)' },
|
||||
'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' },
|
||||
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' }
|
||||
};
|
||||
|
||||
@@ -84,16 +84,20 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
- name: Cache Gradle dependency artifacts
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
~/.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@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
gradle-version: 9.6.1
|
||||
cache-disabled: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
|
||||
@@ -38,16 +38,10 @@ jobs:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Cache Gradle User Home
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
||||
gradle-${{ runner.os }}-${{ runner.arch }}-
|
||||
gradle-version: 9.6.1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
@@ -162,7 +156,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
+4
-7
@@ -22,9 +22,6 @@ pipeline/
|
||||
customFiles/
|
||||
configs/
|
||||
watchedFolders/
|
||||
# The rule above targets the app's runtime watched-folders working dir, but it
|
||||
# also matches this frontend source component dir; keep the source tracked.
|
||||
!frontend/src/editor/proprietary/components/watchedFolders/
|
||||
clientWebUI/
|
||||
# Scratch dir used by local fixture-regeneration runs (see
|
||||
# app/proprietary/src/test/resources/db-migration-fixtures/README.md).
|
||||
@@ -174,7 +171,7 @@ app/proprietary/build
|
||||
common/build
|
||||
proprietary/build
|
||||
stirling-pdf/build
|
||||
frontend/src-tauri/provisioner/target
|
||||
frontend/editor/src-tauri/provisioner/target
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -225,7 +222,7 @@ out/
|
||||
*.asc
|
||||
|
||||
# Allow test fixture certificates (synthetic, no real credentials)
|
||||
!frontend/src/editor/core/tests/test-fixtures/certs/**
|
||||
!frontend/editor/src/core/tests/test-fixtures/certs/**
|
||||
|
||||
# SSH Keys
|
||||
*.pub
|
||||
@@ -267,7 +264,7 @@ node_modules/
|
||||
*compact*.json
|
||||
test_batch.json
|
||||
*.backup.*.json
|
||||
frontend/public/locales/*/translation.backup*.json
|
||||
frontend/editor/public/locales/*/translation.backup*.json
|
||||
|
||||
# Development/build artifacts
|
||||
.gradle-cache/
|
||||
@@ -292,4 +289,4 @@ docs/type3/signatures/
|
||||
*.playwright-mcp.png
|
||||
|
||||
# Local screenshot artifacts from *-screenshots.spec.ts
|
||||
frontend/screenshots/
|
||||
frontend/editor/screenshots/
|
||||
|
||||
+2
-2
@@ -15,10 +15,10 @@ testing/compose/validate-mcp-test.sh:curl-auth-header:92
|
||||
testing/compose/validate-mcp-test.sh:curl-auth-header:116
|
||||
|
||||
# Storybook example showing curl with a fake Bearer token placeholder (sk_live_a3f8...).
|
||||
frontend/src/editor/proprietary/ui/CodeBlock.stories.tsx:curl-auth-header:5
|
||||
frontend/editor/src/proprietary/ui/CodeBlock.stories.tsx:curl-auth-header:5
|
||||
|
||||
# Truncated placeholder API key in portal docs example (sk_live_8f2c...e10) - not a real secret.
|
||||
frontend/src/processor/proprietary/components/docs/GettingStartedSection.tsx:generic-api-key:30
|
||||
frontend/editor/src/portal/components/docs/GettingStartedSection.tsx:generic-api-key:30
|
||||
|
||||
# False positive: generic-api-key matches the Java type name "X509Certificate"
|
||||
# in a method signature (CreateSignatureBase.resolveSignatureAlgorithm) - not a secret.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"ignoredFiles": [
|
||||
"frontend/src-tauri/icons/icon.png"
|
||||
"frontend/editor/src-tauri/icons/icon.png"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -114,15 +114,6 @@ tasks:
|
||||
- cmd: ./gradlew test
|
||||
platforms: [linux, darwin]
|
||||
|
||||
test:force:
|
||||
desc: "Run backend tests, ignoring cached results"
|
||||
aliases: [test:no-cache]
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat cleanTest test --no-build-cache"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew cleanTest test --no-build-cache
|
||||
platforms: [linux, darwin]
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
cmds:
|
||||
@@ -146,7 +137,6 @@ tasks:
|
||||
|
||||
swagger:
|
||||
desc: "Generate OpenAPI docs"
|
||||
run: once
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc"
|
||||
platforms: [windows]
|
||||
|
||||
+16
-7
@@ -22,7 +22,6 @@ vars:
|
||||
linux-amd64) echo "linux-x64";;
|
||||
linux-arm64) echo "linux-arm64";;
|
||||
windows-amd64) echo "windows-x64";;
|
||||
windows-arm64) echo "none";; # no JPDFium windows-arm64 natives published yet
|
||||
*) echo "all";;
|
||||
esac
|
||||
fi
|
||||
@@ -39,6 +38,7 @@ tasks:
|
||||
provisioner:
|
||||
desc: "Build installer provisioner"
|
||||
platforms: [windows]
|
||||
dir: editor
|
||||
cmds:
|
||||
- node scripts/build-provisioner.mjs
|
||||
|
||||
@@ -46,48 +46,55 @@ tasks:
|
||||
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: src-tauri
|
||||
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
|
||||
@@ -109,6 +116,7 @@ tasks:
|
||||
|
||||
jlink:verify:
|
||||
desc: "Fail the build if the bundled JRE is older than the app JAR requires"
|
||||
dir: editor
|
||||
env:
|
||||
REQUIRED_JAVA: "{{.REQUIRED_JAVA}}"
|
||||
cmds:
|
||||
@@ -126,15 +134,15 @@ tasks:
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
platforms: [linux, darwin]
|
||||
- mkdir -p frontend/src-tauri/libs
|
||||
- cp app/core/build/libs/stirling-pdf-*.jar frontend/src-tauri/libs/
|
||||
- mkdir -p frontend/editor/src-tauri/libs
|
||||
- cp app/core/build/libs/stirling-pdf-*.jar frontend/editor/src-tauri/libs/
|
||||
status:
|
||||
- test -f frontend/src-tauri/libs/stirling-pdf-*.jar
|
||||
- test -f frontend/editor/src-tauri/libs/stirling-pdf-*.jar
|
||||
|
||||
jlink:runtime:
|
||||
desc: "Create custom JRE with jlink"
|
||||
deps: [jlink:jar]
|
||||
dir: src-tauri
|
||||
dir: editor/src-tauri
|
||||
cmds:
|
||||
- rm -rf runtime/jre
|
||||
- mkdir -p runtime
|
||||
@@ -193,7 +201,7 @@ tasks:
|
||||
|
||||
jlink:clean:
|
||||
desc: "Remove JLink runtime and bundled JARs"
|
||||
dir: src-tauri
|
||||
dir: editor/src-tauri
|
||||
cmds:
|
||||
- rm -rf libs runtime
|
||||
|
||||
@@ -206,6 +214,7 @@ tasks:
|
||||
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
|
||||
|
||||
+5
-5
@@ -3,14 +3,14 @@ version: '3'
|
||||
tasks:
|
||||
install:
|
||||
desc: "Install Playwright browsers"
|
||||
dir: frontend
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:install' ]
|
||||
cmds:
|
||||
- npx playwright install {{.CLI_ARGS}} --with-deps
|
||||
|
||||
stubbed:
|
||||
desc: "Run stubbed E2E tests"
|
||||
dir: frontend
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:prepare' ]
|
||||
cmds:
|
||||
- npx playwright test --project=stubbed {{.CLI_ARGS}}
|
||||
@@ -82,7 +82,7 @@ tasks:
|
||||
live:runner:
|
||||
internal: true
|
||||
deps: [ ':frontend:prepare' ]
|
||||
dir: frontend
|
||||
dir: frontend/editor
|
||||
vars:
|
||||
BASE_DIR: '{{.ROOT_DIR}}/.test-state/playwright'
|
||||
cmds:
|
||||
@@ -151,14 +151,14 @@ tasks:
|
||||
up first with:
|
||||
task e2e:oauth:up
|
||||
task e2e:saml:up
|
||||
dir: frontend
|
||||
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
|
||||
dir: frontend/editor
|
||||
deps: [ ':frontend:prepare' ]
|
||||
cmds:
|
||||
- npx playwright test --project=stubbed --project=stubbed-firefox --project=stubbed-webkit {{.CLI_ARGS}}
|
||||
|
||||
@@ -102,19 +102,12 @@ tasks:
|
||||
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 --io-output src/stirling/models/tool_io.py
|
||||
- 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
|
||||
- src/stirling/models/tool_io.py
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if the committed tool models are out of date"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py --check
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
|
||||
+48
-49
@@ -3,7 +3,7 @@ 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/ without each task needing a cd.
|
||||
# under frontend/editor/ without each task needing a cd.
|
||||
|
||||
tasks:
|
||||
install:
|
||||
@@ -26,34 +26,34 @@ tasks:
|
||||
vars:
|
||||
MODE: '{{.MODE | default ""}}'
|
||||
cmds:
|
||||
- npx tsx scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}}
|
||||
- npx tsx editor/scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}}
|
||||
sources:
|
||||
- scripts/setup-env.mts
|
||||
- editor/scripts/setup-env.mts
|
||||
generates:
|
||||
- .env.local
|
||||
- .env{{if .MODE}}.{{.MODE}}{{end}}.local
|
||||
- editor/.env.local
|
||||
- editor/.env{{if .MODE}}.{{.MODE}}{{end}}.local
|
||||
|
||||
prepare:icons:
|
||||
internal: true
|
||||
run: once
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node scripts/generate-icons.js
|
||||
- node editor/scripts/generate-icons.js
|
||||
|
||||
prepare:og:
|
||||
internal: true
|
||||
run: when_changed
|
||||
desc: "Regenerate OG/social-preview metadata from the tool registry"
|
||||
cmds:
|
||||
- node scripts/generate-og-metadata.mjs
|
||||
- node editor/scripts/generate-og-metadata.mjs
|
||||
sources:
|
||||
- src/editor/core/types/toolId.ts
|
||||
- src/editor/core/utils/urlMapping.ts
|
||||
- src/editor/core/data/useTranslatedToolRegistry.tsx
|
||||
- public/og_images/*.png
|
||||
- editor/src/core/types/toolId.ts
|
||||
- editor/src/core/utils/urlMapping.ts
|
||||
- editor/src/core/data/useTranslatedToolRegistry.tsx
|
||||
- editor/public/og_images/*.png
|
||||
generates:
|
||||
- src/editor/core/data/ogImageMap.json
|
||||
- public/og-metadata.json
|
||||
- editor/src/core/data/ogImageMap.json
|
||||
- editor/public/og-metadata.json
|
||||
|
||||
prepare:
|
||||
desc: "Set up dev environment"
|
||||
@@ -88,7 +88,7 @@ tasks:
|
||||
sh: >-
|
||||
{{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}}
|
||||
cmds:
|
||||
- npx vite --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
dev:
|
||||
desc: "Start frontend dev server"
|
||||
@@ -143,13 +143,13 @@ tasks:
|
||||
desc: "Production build (default mode)"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build
|
||||
- npx vite build editor
|
||||
|
||||
build:core:
|
||||
desc: "Build for core mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build --mode core
|
||||
- npx vite build editor --mode core
|
||||
|
||||
build:proprietary:
|
||||
desc: "Build for proprietary mode"
|
||||
@@ -157,7 +157,7 @@ tasks:
|
||||
vars:
|
||||
PREVIEW: '{{.PREVIEW | default ""}}'
|
||||
cmds:
|
||||
- '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build --mode proprietary'
|
||||
- '{{if .PREVIEW}}VITE_BUILD_FOR_PREVIEW=1 {{end}}npx vite build editor --mode proprietary'
|
||||
|
||||
build:saas:
|
||||
desc: "Build for SaaS mode"
|
||||
@@ -165,7 +165,7 @@ tasks:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- npx vite build --mode saas
|
||||
- npx vite build editor --mode saas
|
||||
|
||||
build:desktop:
|
||||
desc: "Build for desktop mode"
|
||||
@@ -173,13 +173,13 @@ tasks:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- npx vite build --mode desktop
|
||||
- npx vite build editor --mode desktop
|
||||
|
||||
build:prototypes:
|
||||
desc: "Build for prototypes mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vite build --mode prototypes
|
||||
- npx vite build editor --mode prototypes
|
||||
|
||||
|
||||
storybook:
|
||||
@@ -272,23 +272,23 @@ tasks:
|
||||
# Covers the whole editor tree, including the portal/processor layer and
|
||||
# public/css. Vendored CSS and build output are excluded via ignoreFiles
|
||||
# in stylelint.config.mjs.
|
||||
- npx stylelint "src/**/*.css"
|
||||
- npx stylelint "editor/**/*.css"
|
||||
|
||||
lint:colors:
|
||||
desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components"
|
||||
aliases: [lint:colours]
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node scripts/lint/theme-lint.mjs
|
||||
- node scripts/lint/theme-lint.mjs css-colors
|
||||
- node scripts/lint/theme-lint.mjs code-colors
|
||||
- node scripts/lint/theme-lint.mjs no-primitives
|
||||
- node editor/scripts/lint/theme-lint.mjs
|
||||
- node editor/scripts/lint/theme-lint.mjs css-colors
|
||||
- node editor/scripts/lint/theme-lint.mjs code-colors
|
||||
- node editor/scripts/lint/theme-lint.mjs no-primitives
|
||||
|
||||
contrast:
|
||||
desc: "Report low-contrast theme token pairs (warning only, never blocks)"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node scripts/lint/theme-lint.mjs contrast
|
||||
- node editor/scripts/lint/theme-lint.mjs contrast
|
||||
|
||||
lint:eslint:
|
||||
desc: "Run ESLint linting"
|
||||
@@ -302,7 +302,7 @@ tasks:
|
||||
cmds:
|
||||
# Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is
|
||||
# shell-agnostic. Covers the whole editor tree, including the portal layer.
|
||||
- npx dpdm "src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1
|
||||
- npx dpdm "editor/src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1
|
||||
|
||||
lint:fix:
|
||||
desc: "Auto-fix lint issues"
|
||||
@@ -343,14 +343,14 @@ tasks:
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/editor/core/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/core/tsconfig.json }
|
||||
|
||||
typecheck:proprietary:
|
||||
desc: "Typecheck proprietary build variant"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/editor/proprietary/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/proprietary/tsconfig.json }
|
||||
|
||||
typecheck:saas:
|
||||
desc: "Typecheck SaaS build variant"
|
||||
@@ -359,7 +359,7 @@ tasks:
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/editor/saas/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/saas/tsconfig.json }
|
||||
|
||||
typecheck:desktop:
|
||||
desc: "Typecheck desktop build variant"
|
||||
@@ -368,35 +368,35 @@ tasks:
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/editor/desktop/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/desktop/tsconfig.json }
|
||||
|
||||
typecheck:cloud:
|
||||
desc: "Typecheck cloud shared layer (standalone)"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/editor/cloud/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/cloud/tsconfig.json }
|
||||
|
||||
typecheck:scripts:
|
||||
desc: "Typecheck scripts"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: scripts/tsconfig.json }
|
||||
vars: { PROJECT: editor/scripts/tsconfig.json }
|
||||
|
||||
typecheck:prototypes:
|
||||
desc: "Typecheck prototypes build variant"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/editor/prototypes/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/prototypes/tsconfig.json }
|
||||
|
||||
typecheck:portal:
|
||||
desc: "Typecheck developer portal build variant"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- task: typecheck:_run
|
||||
vars: { PROJECT: src/processor/proprietary/tsconfig.json }
|
||||
vars: { PROJECT: editor/src/portal/tsconfig.json }
|
||||
|
||||
typecheck:storybook:
|
||||
desc: "Typecheck Storybook config and stories"
|
||||
@@ -433,7 +433,7 @@ tasks:
|
||||
og:check:
|
||||
desc: "Fail if committed OG/social-preview metadata is out of date"
|
||||
cmds:
|
||||
- node scripts/generate-og-metadata.mjs --check
|
||||
- node editor/scripts/generate-og-metadata.mjs --check
|
||||
|
||||
check:all:
|
||||
desc: "Full CI quality gate"
|
||||
@@ -461,13 +461,13 @@ tasks:
|
||||
desc: "Run editor tests"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vitest run --root .
|
||||
- npx vitest run --root editor
|
||||
|
||||
test:watch:
|
||||
desc: "Run tests in watch mode"
|
||||
deps: [prepare]
|
||||
cmds:
|
||||
- npx vitest --watch --root .
|
||||
- npx vitest --watch --root editor
|
||||
|
||||
test:coverage:
|
||||
desc: "Run tests with coverage (one-shot; CI-friendly)."
|
||||
@@ -478,12 +478,12 @@ tasks:
|
||||
# coverage-summary.py helper consumes; html/text are kept for humans.
|
||||
#
|
||||
# reportsDirectory is pinned to ./coverage relative to vitest's root
|
||||
# (--root .), so output lands at frontend/coverage/. The
|
||||
# (--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/coverage;
|
||||
# `./editor/coverage` double-nested into frontend/editor/editor/coverage;
|
||||
# pinning future-proofs against vitest changing the default.
|
||||
- >
|
||||
npx vitest run --root . --coverage
|
||||
npx vitest run --root editor --coverage
|
||||
--coverage.provider=v8
|
||||
--coverage.reporter=text-summary
|
||||
--coverage.reporter=json-summary
|
||||
@@ -498,25 +498,24 @@ tasks:
|
||||
desc: "Generate tool API types from the Java OpenAPI spec"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- npx tsx scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output src/editor/core/types/toolApiTypes.ts --io-output src/editor/core/types/toolIO.ts
|
||||
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts
|
||||
sources:
|
||||
- scripts/generate-tool-api-types.mts
|
||||
- editor/scripts/generate-tool-api-types.mts
|
||||
- ../SwaggerDoc.json
|
||||
generates:
|
||||
- src/editor/core/types/toolApiTypes.ts
|
||||
- src/editor/core/types/toolIO.ts
|
||||
- editor/src/core/types/toolApiTypes.ts
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if committed tool API types are out of date"
|
||||
deps: [install, ":backend:swagger"]
|
||||
cmds:
|
||||
- npx tsx scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output src/editor/core/types/toolApiTypes.ts --io-output src/editor/core/types/toolIO.ts --check
|
||||
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --check
|
||||
|
||||
licenses:generate:
|
||||
desc: "Generate frontend license report"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node scripts/generate-licenses.js
|
||||
- node editor/scripts/generate-licenses.js
|
||||
|
||||
# ============================================================
|
||||
# Clean
|
||||
@@ -525,7 +524,7 @@ tasks:
|
||||
clean:
|
||||
desc: "Clean build artifacts and caches"
|
||||
cmds:
|
||||
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, dist, dist
|
||||
- cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist
|
||||
platforms: [windows]
|
||||
- cmd: rm -rf node_modules/.vite dist dist
|
||||
- cmd: rm -rf node_modules/.vite editor/dist dist
|
||||
platforms: [linux, darwin]
|
||||
|
||||
@@ -21,7 +21,7 @@ vars:
|
||||
':(exclude).devcontainer/*'
|
||||
':(exclude)app/core/src/main/resources/*'
|
||||
':(exclude)app/proprietary/src/main/resources/*'
|
||||
':(exclude)frontend/public/vendor/*'
|
||||
':(exclude)frontend/editor/public/vendor/*'
|
||||
':(exclude)*Dockerfile*'
|
||||
':(exclude)*pdfjs*'
|
||||
':(exclude)*thirdParty*'
|
||||
@@ -39,7 +39,7 @@ vars:
|
||||
':(exclude)*.min.*'
|
||||
':(exclude)*diff.js'
|
||||
':(exclude).github/workflows/*'
|
||||
LOCALE_TOML: 'frontend/public/locales/*/translation.toml'
|
||||
LOCALE_TOML: 'frontend/editor/public/locales/*/translation.toml'
|
||||
|
||||
# gitleaks is pinned + checksum-verified by scripts/pre-commit/install_gitleaks.py,
|
||||
# which owns the version and caches the binary here.
|
||||
|
||||
+6
-6
@@ -10,14 +10,14 @@ When adding tools, follow this systematic approach using the established pattern
|
||||
|
||||
Create these files in the correct directories:
|
||||
```
|
||||
frontend/src/hooks/tools/[toolName]/
|
||||
frontend/editor/src/hooks/tools/[toolName]/
|
||||
├── use[ToolName]Parameters.ts # Parameter definitions and validation
|
||||
└── use[ToolName]Operation.ts # Tool operation logic using useToolOperation
|
||||
|
||||
frontend/src/components/tools/[toolName]/
|
||||
frontend/editor/src/components/tools/[toolName]/
|
||||
└── [ToolName]Settings.tsx # Settings UI component (if needed)
|
||||
|
||||
frontend/src/tools/
|
||||
frontend/editor/src/tools/
|
||||
└── [ToolName].tsx # Main tool component
|
||||
```
|
||||
|
||||
@@ -128,7 +128,7 @@ export default [ToolName] as ToolComponent;
|
||||
## 3. Register Tool in System
|
||||
Update these files to register your new tool:
|
||||
|
||||
**Tool Registry** (`frontend/src/data/useTranslatedToolRegistry.tsx`):
|
||||
**Tool Registry** (`frontend/editor/src/data/useTranslatedToolRegistry.tsx`):
|
||||
1. Add imports at the top:
|
||||
```typescript
|
||||
import [ToolName] from "../tools/[ToolName]";
|
||||
@@ -155,7 +155,7 @@ import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Setting
|
||||
## 4. Add Tooltips (Optional but Recommended)
|
||||
Create user-friendly tooltips to help non-technical users understand your tool. **Use simple, clear language - avoid technical jargon:**
|
||||
|
||||
**Tooltip Hook** (`frontend/src/components/tooltips/use[ToolName]Tips.ts`):
|
||||
**Tooltip Hook** (`frontend/editor/src/components/tooltips/use[ToolName]Tips.ts`):
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
@@ -202,7 +202,7 @@ const [ToolName] = (props: BaseToolProps) => {
|
||||
## 5. Add Translations
|
||||
Update translation files. **Important: Only update `en-US` files** - other languages are handled separately.
|
||||
|
||||
**File to update:** `frontend/public/locales/en-US/translation.toml`
|
||||
**File to update:** `frontend/editor/public/locales/en-US/translation.toml`
|
||||
|
||||
**Required Translation Keys**:
|
||||
```toml
|
||||
|
||||
@@ -139,10 +139,10 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
|
||||
#### Environment Variables
|
||||
- All `VITE_*` variables must be declared in the appropriate committed env file:
|
||||
- `frontend/.env` — core and shared vars (base, loaded in every mode)
|
||||
- `frontend/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)
|
||||
- `frontend/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)
|
||||
- `frontend/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)
|
||||
- `frontend/editor/.env` — core and shared vars (base, loaded in every mode)
|
||||
- `frontend/editor/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode)
|
||||
- `frontend/editor/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode)
|
||||
- `frontend/editor/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode)
|
||||
- These files are committed to Git and must not contain private keys
|
||||
- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top
|
||||
- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files
|
||||
@@ -153,9 +153,9 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
#### Import Paths - CRITICAL
|
||||
**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.
|
||||
|
||||
For a broader explanation of the frontend layering and override architecture, read @frontend/DeveloperGuide.md
|
||||
For a broader explanation of the frontend layering and override architecture, read @frontend/editor/DeveloperGuide.md
|
||||
|
||||
Before touching colours or theming (tokens, dark mode, accent colours), read @frontend/src/editor/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.
|
||||
Before touching colours or theming (tokens, dark mode, accent colours), read @frontend/editor/src/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use @app/* for all imports
|
||||
@@ -270,7 +270,7 @@ Frontend designed for **stateful document processing**:
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `frontend/src/editor/core/contexts/FileContext.tsx`
|
||||
**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
|
||||
@@ -295,7 +295,7 @@ Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/src/editor/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- **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
|
||||
@@ -383,7 +383,7 @@ return useToolOperation({
|
||||
### Frontend Directory Structure
|
||||
The frontend is organized with a clear separation of concerns:
|
||||
|
||||
- **`frontend/src/editor/core/`**: Main application code (shared, production-ready components)
|
||||
- **`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
|
||||
@@ -401,17 +401,17 @@ The frontend is organized with a clear separation of concerns:
|
||||
- **`core/data/`**: Static data (tool taxonomy, etc.)
|
||||
- **`core/services/`**: Business logic services (PDF processing, storage, etc.)
|
||||
|
||||
- **`frontend/src/editor/desktop/`**: Desktop-specific (Tauri) code
|
||||
- **`frontend/src/editor/proprietary/`**: Proprietary/licensed features
|
||||
- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code
|
||||
- **`frontend/public/`**: Static assets served directly
|
||||
- **`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/public/` (modern)
|
||||
- **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/public/locales/` (converted from .properties)
|
||||
- Frontend: JSON files in `frontend/editor/public/locales/` (converted from .properties)
|
||||
- Conversion Script: `scripts/convert_properties_to_json.py`
|
||||
|
||||
### Configuration Modes
|
||||
@@ -436,7 +436,7 @@ The frontend is organized with a clear separation of concerns:
|
||||
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/public/locales/` or use conversion script
|
||||
- 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
|
||||
@@ -454,7 +454,7 @@ The frontend is organized with a clear separation of concerns:
|
||||
## 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/public/locales/`
|
||||
- Translation files are located in `frontend/editor/public/locales/`
|
||||
- After changing any translation file, run `task pre-commit:fix`
|
||||
|
||||
## Important Notes
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ This project uses [Task](https://taskfile.dev/) as a unified command runner. Aft
|
||||
|
||||
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 or `task desktop:dev` to start the desktop application
|
||||
3. Run `task dev` to start backend + frontend
|
||||
4. Run `task check` before submitting a PR
|
||||
|
||||
Run `task --list` to see all available commands.
|
||||
|
||||
+2
-35
@@ -458,7 +458,7 @@ For Stirling 2.0, new features are built as React components:
|
||||
|
||||
1. **Create the React Component:**
|
||||
```typescript
|
||||
// frontend/src/tools/NewTool.tsx
|
||||
// frontend/editor/src/tools/NewTool.tsx
|
||||
import { useState } from 'react';
|
||||
import { Button, FileInput, Container } from '@mantine/core';
|
||||
|
||||
@@ -504,8 +504,7 @@ For Stirling 2.0, new features are built as React components:
|
||||
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.")`.
|
||||
- If the endpoint transforms a document, declare what it accepts and produces with `@ToolIO`, for example `@ToolIO(produces = ToolFormat.PDF)`. This is what lets a pipeline containing the step be checked before it runs, so a chain that cannot work is caught in the builder rather than part-way through a job. Endpoints under the tool namespaces are required to carry it - `ToolIODeclarationCoverageTest` fails the build otherwise. See [Declaring tool inputs and outputs](#declaring-tool-inputs-and-outputs).
|
||||
- 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;
|
||||
@@ -579,38 +578,6 @@ For Stirling 2.0, new features are built as React components:
|
||||
}
|
||||
```
|
||||
|
||||
### Declaring tool inputs and outputs
|
||||
|
||||
An endpoint that transforms a document declares what it accepts and produces with `@ToolIO`. This is the single source of truth: it is published into the OpenAPI spec as an `x-stirling-io` extension, and generated from there into the frontend (`toolIO.ts`) and the AI engine (`tool_io.py`). A pipeline can therefore be checked while it is being edited, instead of failing part-way through a job.
|
||||
|
||||
```java
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
```
|
||||
|
||||
`accepts` defaults to `{ ToolFormat.PDF }` and `arity` to `ToolArity.SISO`, so most tools only declare what they produce.
|
||||
|
||||
- **`ToolFormat`** is the kind of file: `PDF`, `PDF_ENCRYPTED`, `IMAGE`, `ZIP`, `WORD`, `PPT`, `EXCEL`, `CSV`, `HTML`, `XML`, `JSON`, `TEXT`, `MARKDOWN`, `JAVASCRIPT`, `EBOOK`, `EMAIL`, `POSTSCRIPT`, `VIDEO`, `CBZ`, `CBR`, plus `ANY` (accepts or produces anything) and `NONE` (returns a report, not a file). Encryption is a format rather than a flag, so the default `accepts = PDF` means an endpoint rejects an encrypted PDF unless it opts in.
|
||||
- **`ToolArity`** is how many files go in and out: `SISO`, `SIMO`, `MISO`, `MIMO`. This axis carries ZIP-as-transport. A splitter is `produces = PDF, arity = SIMO`, and the caller unpacks the archive; an endpoint whose deliverable really is an archive declares `produces = ZIP` with a single-output arity and stays packed.
|
||||
|
||||
When the output depends on a parameter, declare the exception as a case rather than picking one answer. Add Password produces an encrypted PDF unless both passwords are blank, in which case it has only set permissions:
|
||||
|
||||
```java
|
||||
@ToolIO(
|
||||
produces = ToolFormat.PDF_ENCRYPTED,
|
||||
cases =
|
||||
@ToolIOCase(
|
||||
when = {
|
||||
@ToolIOWhen(param = "password", matches = ""),
|
||||
@ToolIOWhen(param = "ownerPassword", matches = "")
|
||||
},
|
||||
produces = ToolFormat.PDF,
|
||||
arity = ToolArity.SISO))
|
||||
```
|
||||
|
||||
Every condition in a `when` must hold for the case to apply, and `matches` is compared as a string, case-insensitively, with an empty string matching an absent or blank value. If a case reads a parameter that is not set yet, the output is reported as uncertain and the chain warns rather than erroring.
|
||||
|
||||
Endpoints under the tool namespaces must carry a declaration; `ToolIODeclarationCoverageTest` fails the build for any that does not, with a short allowlist for endpoints that manage a session, a device or a stored resource rather than transforming a document. The matching rules are implemented three times (Java `ToolChainValidator`, `toolIOCompat.ts`, `tool_io_compat.py`) and pinned to the same answers by the shared fixtures in `testing/tool-io-cases.json`, so a behaviour change belongs in that file first.
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -10,18 +10,20 @@ if that directory exists, is licensed under the license defined in "app/propriet
|
||||
if that directory exists, is licensed under the license defined in "app/saas/LICENSE".
|
||||
* All content that resides under the "engine/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "engine/LICENSE".
|
||||
* All content that resides under the "frontend/src/editor/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/editor/proprietary/LICENSE".
|
||||
* All content that resides under the "frontend/src/editor/desktop/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/editor/desktop/LICENSE".
|
||||
* All content that resides under the "frontend/src/editor/saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/editor/saas/LICENSE".
|
||||
* All content that resides under the "frontend/src/editor/cloud/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/editor/cloud/LICENSE".
|
||||
* All content that resides under the "frontend/src/editor/prototypes/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/editor/prototypes/LICENSE".
|
||||
* All content that resides under the "frontend/src/processor/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/src/processor/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/proprietary/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/desktop/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/desktop/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/saas/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/cloud/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/cloud/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/prototypes/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/prototypes/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/portal/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/portal/LICENSE".
|
||||
* All content that resides under the "frontend/editor/src/portal-saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "frontend/editor/src/portal-saas/LICENSE".
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the MIT License as defined below.
|
||||
|
||||
|
||||
@@ -194,12 +194,6 @@ tasks:
|
||||
- task: frontend:tool-models
|
||||
- task: engine:tool-models
|
||||
|
||||
tool-models:check:
|
||||
desc: "Fail if any committed API model is out of date"
|
||||
cmds:
|
||||
- task: frontend:tool-models:check
|
||||
- task: engine:tool-models:check
|
||||
|
||||
# ============================================================
|
||||
# Quality Gate
|
||||
# ============================================================
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ If you're using Tauri's built-in updater feature:
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### 1. Tauri Configuration (frontend/src-tauri/tauri.conf.json)
|
||||
### 1. Tauri Configuration (frontend/editor/src-tauri/tauri.conf.json)
|
||||
|
||||
The Windows signing configuration is already set up:
|
||||
|
||||
|
||||
@@ -32,14 +32,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "BSD-4 License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Revised BSD"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "ISC"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT"
|
||||
@@ -56,10 +48,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT-0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "MIT license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.github.jai-imageio:jai-imageio-core",
|
||||
"moduleLicense": "LICENSE.txt"
|
||||
@@ -156,14 +144,6 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "GNU Lesser Public License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The GNU Lesser General Public License"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.martiansoftware:jsap",
|
||||
"moduleLicense": "LGPL"
|
||||
@@ -232,6 +212,14 @@
|
||||
"moduleName": "com.google.re2j:re2j",
|
||||
"moduleLicense": "Go License"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.hubspot:algebra",
|
||||
"moduleLicense": null
|
||||
},
|
||||
{
|
||||
"moduleName": "com.hubspot.immutables:immutables-exceptions",
|
||||
"moduleLicense": null
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "UnRar License"
|
||||
|
||||
+7
-13
@@ -16,7 +16,7 @@ dependencies {
|
||||
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:8.0.0' // RAR archive support for CBR files
|
||||
api 'com.github.junrar:junrar:7.6.0' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
|
||||
@@ -36,24 +36,18 @@ dependencies {
|
||||
|
||||
api "com.stirling:jpdfium:${jpdfiumVersion}"
|
||||
|
||||
// -PjpdfiumPlatforms=all|none|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
// 'none' skips natives entirely (windows-arm64 builds, until JPDFium ships that platform).
|
||||
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
def jpdfiumPlatforms
|
||||
if (jpdfiumPlatformsProp == 'all') {
|
||||
jpdfiumPlatforms = jpdfiumAllPlatforms
|
||||
} else if (jpdfiumPlatformsProp == 'none') {
|
||||
jpdfiumPlatforms = []
|
||||
} else {
|
||||
jpdfiumPlatforms = jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it }
|
||||
}
|
||||
def jpdfiumPlatforms = jpdfiumPlatformsProp == 'all'
|
||||
? jpdfiumAllPlatforms
|
||||
: jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it }
|
||||
def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) }
|
||||
if (jpdfiumInvalid) {
|
||||
throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " +
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')}, 'all' or 'none'.")
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')} or 'all'.")
|
||||
}
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms ? jpdfiumPlatforms.join(', ') : 'none'}")
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
|
||||
jpdfiumPlatforms.each { platform ->
|
||||
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
|
||||
}
|
||||
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
package stirling.software.common.config.swagger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springdoc.core.customizers.GlobalOperationCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Operation;
|
||||
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.model.tool.ToolIOCase;
|
||||
import stirling.software.common.model.tool.ToolIOWhen;
|
||||
|
||||
/**
|
||||
* Publishes each {@link ToolIO} into the spec as {@code x-stirling-io}, which is how the frontend
|
||||
* and the AI engine get it.
|
||||
*
|
||||
* <p>Also appends the {@code Input:/Output:/Type:} line the docs used to carry by hand, so the
|
||||
* published text is unchanged without anyone maintaining it.
|
||||
*/
|
||||
@Component
|
||||
public class ToolIOOperationCustomizer
|
||||
implements GlobalOperationCustomizer, GlobalOpenApiCustomizer {
|
||||
|
||||
public static final String EXTENSION_NAME = "x-stirling-io";
|
||||
public static final String VOCABULARY_EXTENSION_NAME = "x-stirling-io-vocabulary";
|
||||
|
||||
// Published separately from the declarations: generators need the full vocabulary for their
|
||||
// enums, and deriving it from what is present would shrink it when an endpoint is disabled.
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
Map<String, Object> vocabulary = new LinkedHashMap<>();
|
||||
vocabulary.put("formats", names(ToolFormat.values()));
|
||||
vocabulary.put("arities", names(ToolArity.values()));
|
||||
openApi.addExtension(VOCABULARY_EXTENSION_NAME, vocabulary);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
|
||||
ToolIO declaration = handlerMethod.getMethodAnnotation(ToolIO.class);
|
||||
if (declaration == null) {
|
||||
return operation;
|
||||
}
|
||||
operation.addExtension(EXTENSION_NAME, toExtension(declaration));
|
||||
operation.setDescription(appendSummaryLine(operation.getDescription(), declaration));
|
||||
return operation;
|
||||
}
|
||||
|
||||
private static Map<String, Object> toExtension(ToolIO declaration) {
|
||||
Map<String, Object> extension = new LinkedHashMap<>();
|
||||
extension.put("accepts", names(declaration.accepts()));
|
||||
extension.put("produces", declaration.produces().name());
|
||||
extension.put("arity", declaration.arity().name());
|
||||
if (declaration.cases().length > 0) {
|
||||
extension.put("cases", cases(declaration));
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> cases(ToolIO declaration) {
|
||||
return Arrays.stream(declaration.cases()).map(ToolIOOperationCustomizer::toCase).toList();
|
||||
}
|
||||
|
||||
private static Map<String, Object> toCase(ToolIOCase rule) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put(
|
||||
"when",
|
||||
Arrays.stream(rule.when()).map(ToolIOOperationCustomizer::toCondition).toList());
|
||||
entry.put("produces", rule.produces().name());
|
||||
entry.put("arity", rule.arity().name());
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static Map<String, Object> toCondition(ToolIOWhen condition) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("param", condition.param());
|
||||
entry.put("matches", List.of(condition.matches()));
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static List<String> names(Enum<?>[] values) {
|
||||
return Arrays.stream(values).map(Enum::name).toList();
|
||||
}
|
||||
|
||||
private static String appendSummaryLine(String description, ToolIO declaration) {
|
||||
String summary =
|
||||
"Input:"
|
||||
+ String.join("/", names(declaration.accepts()))
|
||||
+ " Output:"
|
||||
+ declaration.produces().name()
|
||||
+ " Type:"
|
||||
+ declaration.arity().name();
|
||||
return description == null || description.isBlank()
|
||||
? summary
|
||||
: description.trim() + " " + summary;
|
||||
}
|
||||
}
|
||||
@@ -251,21 +251,6 @@ public class ApplicationProperties {
|
||||
*/
|
||||
private boolean allowPrivateS3Endpoints = false;
|
||||
|
||||
/**
|
||||
* Whether a network source's host (SFTP, FTP, or SMB) may resolve to a loopback,
|
||||
* link-local, or private address. Off by default so a connection cannot be pointed at
|
||||
* internal services; enable for an on-network file server (e.g. an internal SFTP drop or a
|
||||
* Samba share).
|
||||
*/
|
||||
private boolean allowPrivateNetworkSources = false;
|
||||
|
||||
/**
|
||||
* Hostnames (exact, case-insensitive) that a network source may use even when they resolve
|
||||
* to a private or local address and {@code allowPrivateNetworkSources} is off. Lets shared
|
||||
* infra allow one named on-prem file server without opening every internal host.
|
||||
*/
|
||||
private List<String> allowedPrivateNetworkHosts = new java.util.ArrayList<>();
|
||||
|
||||
/**
|
||||
* Whether an API/Purview/ConsignO integration's base URL may resolve to a loopback,
|
||||
* link-local, or private address. Off by default: unlike S3 connections, any user may
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
/**
|
||||
* How many files an endpoint consumes and produces (Single/Multiple In, Single/Multiple Out).
|
||||
*
|
||||
* <p>This axis carries ZIP-as-transport: a multi-output endpoint returns its results zipped and the
|
||||
* caller unpacks them, so {@code split-pages} is {@code produces = PDF, arity = SIMO} rather than
|
||||
* naming a ZIP-of-PDF format. An endpoint whose deliverable really is an archive declares {@link
|
||||
* ToolFormat#ZIP} with a single-output arity and stays packed.
|
||||
*/
|
||||
public enum ToolArity {
|
||||
SISO,
|
||||
SIMO,
|
||||
MISO,
|
||||
MIMO;
|
||||
|
||||
public boolean isMultiInput() {
|
||||
return this == MISO || this == MIMO;
|
||||
}
|
||||
|
||||
public boolean isMultiOutput() {
|
||||
return this == SIMO || this == MIMO;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
/**
|
||||
* One problem found while checking a chain, against the step that cannot run. {@code code} is
|
||||
* stable so the frontend can pick its own wording; {@code message} is an English fallback.
|
||||
*/
|
||||
public record ToolDiagnostic(int stepIndex, Severity severity, String code, String message) {
|
||||
|
||||
public enum Severity {
|
||||
/** The chain cannot run as configured. Only this should block a save. */
|
||||
ERROR,
|
||||
/** May not run, depending on configuration or file content. */
|
||||
WARN,
|
||||
/** Worth knowing but not a problem, such as a step running once per file. */
|
||||
INFO
|
||||
}
|
||||
|
||||
/** The step declares no {@link ToolIO}, so nothing past it can be checked. */
|
||||
public static final String UNDECLARED = "undeclared-operation";
|
||||
|
||||
/** The previous step's output is not a format this step accepts. */
|
||||
public static final String FORMAT_MISMATCH = "format-mismatch";
|
||||
|
||||
/** The previous step's output depends on a parameter that is not set yet. */
|
||||
public static final String OUTPUT_UNCERTAIN = "output-uncertain";
|
||||
|
||||
/** The pipeline's input files are not a format the first step accepts. */
|
||||
public static final String SOURCE_MISMATCH = "source-mismatch";
|
||||
|
||||
/** The previous step emits several files and this one runs once per file. */
|
||||
public static final String FAN_OUT = "fan-out";
|
||||
|
||||
/** The previous step emits several files and this one consumes them in a single call. */
|
||||
public static final String FAN_IN = "fan-in";
|
||||
|
||||
public static ToolDiagnostic error(int stepIndex, String code, String message) {
|
||||
return new ToolDiagnostic(stepIndex, Severity.ERROR, code, message);
|
||||
}
|
||||
|
||||
public static ToolDiagnostic warn(int stepIndex, String code, String message) {
|
||||
return new ToolDiagnostic(stepIndex, Severity.WARN, code, message);
|
||||
}
|
||||
|
||||
public static ToolDiagnostic info(int stepIndex, String code, String message) {
|
||||
return new ToolDiagnostic(stepIndex, Severity.INFO, code, message);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* The kind of file a tool endpoint consumes or produces.
|
||||
*
|
||||
* <p>Encryption is its own format rather than a separate attribute, so the endpoints accepting only
|
||||
* {@link #PDF} reject an encrypted one without declaring anything.
|
||||
*
|
||||
* <p>Extensions are a lossy projection used for run-time file checks: {@link #PDF} and {@link
|
||||
* #PDF_ENCRYPTED} share {@code pdf}, because a filename cannot tell you whether a PDF is encrypted.
|
||||
*/
|
||||
@Getter
|
||||
public enum ToolFormat {
|
||||
PDF("pdf"),
|
||||
PDF_ENCRYPTED("pdf"),
|
||||
|
||||
// Vector formats are folded in: the extension set has always included svg/eps, and splitting
|
||||
// them out would make chains that run fine today report as broken.
|
||||
IMAGE("png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff", "svg", "psd", "ai", "eps"),
|
||||
|
||||
/**
|
||||
* An archive that is itself the deliverable. Multiple results use {@link ToolArity} instead.
|
||||
*/
|
||||
ZIP("zip", "rar", "7z", "tar", "gz", "bz2", "xz", "lz", "lzma", "z"),
|
||||
|
||||
WORD("doc", "docx", "odt", "rtf"),
|
||||
PPT("ppt", "pptx", "odp"),
|
||||
EXCEL("xls", "xlsx", "ods"),
|
||||
CSV("csv"),
|
||||
HTML("html", "htm", "xhtml"),
|
||||
XML("xml", "xsd", "xsl"),
|
||||
JSON("json"),
|
||||
TEXT("txt", "text", "md", "markdown"),
|
||||
MARKDOWN("md", "markdown"),
|
||||
JAVASCRIPT("js", "jsx"),
|
||||
EBOOK("epub", "mobi", "azw3", "fb2", "txt", "docx"),
|
||||
EMAIL("eml", "msg"),
|
||||
POSTSCRIPT("ps", "eps"),
|
||||
|
||||
PCL("pcl", "pxl"),
|
||||
XPS("xps", "oxps"),
|
||||
|
||||
VIDEO("mp4", "webm", "avi", "mov", "mkv"),
|
||||
CBZ("cbz"),
|
||||
CBR("cbr"),
|
||||
|
||||
/** Never reported as incompatible. */
|
||||
ANY(),
|
||||
|
||||
/** A report or a status rather than a document. */
|
||||
NONE();
|
||||
|
||||
private final List<String> extensions;
|
||||
|
||||
ToolFormat(String... extensions) {
|
||||
this.extensions = List.of(extensions);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* What a tool endpoint consumes and produces, so a chain of steps can be checked before it runs.
|
||||
*
|
||||
* <p>The single source of truth: read off the handler method by {@code ToolIORegistry}, published
|
||||
* into the OpenAPI spec as {@code x-stirling-io}, and generated from there into the frontend and
|
||||
* the AI engine.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ToolIO {
|
||||
|
||||
/** Defaulting to a plain PDF is what makes an ordinary endpoint reject an encrypted one. */
|
||||
ToolFormat[] accepts() default {ToolFormat.PDF};
|
||||
|
||||
ToolFormat produces();
|
||||
|
||||
ToolArity arity() default ToolArity.SISO;
|
||||
|
||||
/** Overrides for an output that depends on a parameter; first match wins. */
|
||||
ToolIOCase[] cases() default {};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* An output that applies when every condition in {@link #when()} holds.
|
||||
*
|
||||
* <p>Conditions are ANDed because the interesting branches turn on more than one parameter: Add
|
||||
* Password only leaves the document unencrypted when both passwords are absent.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ToolIOCase {
|
||||
|
||||
ToolIOWhen[] when();
|
||||
|
||||
ToolFormat produces();
|
||||
|
||||
ToolArity arity();
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Supplies the {@link ToolIO} declaration for an endpoint path. An interface so a chain can be
|
||||
* checked against a fixed set of declarations without standing up an application context.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ToolIOSource {
|
||||
|
||||
Optional<ToolIOSpec> find(String operationPath);
|
||||
|
||||
static ToolIOSource of(Map<String, ToolIOSpec> specs) {
|
||||
Map<String, ToolIOSpec> copy = Map.copyOf(specs);
|
||||
return path -> Optional.ofNullable(copy.get(path));
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** The runtime form of a {@link ToolIO} declaration, read off a handler method once at startup. */
|
||||
public record ToolIOSpec(
|
||||
Set<ToolFormat> accepts, ToolFormat produces, ToolArity arity, List<Case> cases) {
|
||||
|
||||
public record When(String param, List<String> matches) {
|
||||
|
||||
boolean holdsFor(Object value) {
|
||||
String normalised = normalise(value);
|
||||
return matches.stream().anyMatch(match -> normalise(match).equals(normalised));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Both sides of a condition are normalised at comparison, not at construction: the declaration
|
||||
* reaches the frontend and the engine as published data, and normalising only one side there
|
||||
* would silently disagree with this one.
|
||||
*/
|
||||
public static String normalise(Object value) {
|
||||
return value == null ? "" : String.valueOf(value).trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public record Case(List<When> when, ToolFormat produces, ToolArity arity) {
|
||||
|
||||
public Case {
|
||||
when = List.copyOf(when);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code certain} is false when a {@link Case} keys on a parameter whose value is unknown. */
|
||||
public record Output(ToolFormat format, ToolArity arity, boolean certain) {}
|
||||
|
||||
public ToolIOSpec {
|
||||
accepts = Set.copyOf(accepts);
|
||||
cases = List.copyOf(cases);
|
||||
}
|
||||
|
||||
public static ToolIOSpec from(ToolIO annotation) {
|
||||
return new ToolIOSpec(
|
||||
new LinkedHashSet<>(Arrays.asList(annotation.accepts())),
|
||||
annotation.produces(),
|
||||
annotation.arity(),
|
||||
Arrays.stream(annotation.cases()).map(ToolIOSpec::toCase).toList());
|
||||
}
|
||||
|
||||
private static Case toCase(ToolIOCase rule) {
|
||||
List<When> when = Arrays.stream(rule.when()).map(ToolIOSpec::toWhen).toList();
|
||||
return new Case(when, rule.produces(), rule.arity());
|
||||
}
|
||||
|
||||
private static When toWhen(ToolIOWhen condition) {
|
||||
return new When(condition.param(), List.of(condition.matches()));
|
||||
}
|
||||
|
||||
/**
|
||||
* First matching {@link Case} wins. If none match but one reads a parameter we cannot see, the
|
||||
* declared output comes back uncertain: a value we never saw might have picked another branch.
|
||||
*
|
||||
* @param parameters the step's configured parameters, or null when not known
|
||||
*/
|
||||
public Output resolveOutput(Map<String, Object> parameters) {
|
||||
boolean sawUnknownParam = false;
|
||||
for (Case rule : cases) {
|
||||
boolean allHold = true;
|
||||
for (When condition : rule.when()) {
|
||||
if (parameters == null || !parameters.containsKey(condition.param())) {
|
||||
sawUnknownParam = true;
|
||||
allHold = false;
|
||||
continue;
|
||||
}
|
||||
allHold &= condition.holdsFor(parameters.get(condition.param()));
|
||||
}
|
||||
if (allHold) {
|
||||
return new Output(rule.produces(), rule.arity(), true);
|
||||
}
|
||||
}
|
||||
return new Output(produces, arity, !sawUnknownParam);
|
||||
}
|
||||
|
||||
public Output resolveOutput() {
|
||||
return resolveOutput(null);
|
||||
}
|
||||
|
||||
public boolean acceptsFormat(ToolFormat format) {
|
||||
return format == ToolFormat.ANY
|
||||
|| accepts.contains(ToolFormat.ANY)
|
||||
|| accepts.contains(format);
|
||||
}
|
||||
|
||||
/** For run-time file checks. Empty means anything is accepted. */
|
||||
public List<String> acceptedExtensions() {
|
||||
if (accepts.contains(ToolFormat.ANY)) {
|
||||
return List.of();
|
||||
}
|
||||
return accepts.stream()
|
||||
.flatMap(format -> format.getExtensions().stream())
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package stirling.software.common.model.tool;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/** One condition on a request parameter, guarding a {@link ToolIOCase}. */
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ToolIOWhen {
|
||||
|
||||
String param();
|
||||
|
||||
/**
|
||||
* Compared as strings, case-insensitively. An empty string matches an absent or blank value.
|
||||
*/
|
||||
String[] matches();
|
||||
}
|
||||
@@ -983,7 +983,7 @@ public class PdfMarkdownConverter {
|
||||
ordered.sort(Comparator.comparingDouble((Line l) -> l.y).reversed());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Line l : ordered) {
|
||||
if (!sb.isEmpty()) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(l.text);
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -64,21 +63,6 @@ public class JobExecutorService {
|
||||
"Job executor configured with effective timeout of {} ms", this.effectiveTimeoutMs);
|
||||
}
|
||||
|
||||
/** Stop the service-owned executor when the application context is closed or restarted. */
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.debug("Shutting down job executor");
|
||||
executor.shutdown();
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
public ResponseEntity<?> runJobGeneric(boolean async, Supplier<Object> work) {
|
||||
return runJobGeneric(async, work, -1);
|
||||
}
|
||||
|
||||
+13
-15
@@ -225,21 +225,19 @@ public class MobileScannerService {
|
||||
Path sessionDir = getSafeSessionDirectory(sessionId);
|
||||
if (Files.exists(sessionDir)) {
|
||||
// Delete all files in session directory
|
||||
try (var paths = Files.walk(sessionDir)) {
|
||||
paths.sorted(
|
||||
(a, b) ->
|
||||
-a.compareTo(
|
||||
b)) // Reverse order to delete files before
|
||||
// directory
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete file: {}", path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Files.walk(sessionDir)
|
||||
.sorted(
|
||||
(a, b) ->
|
||||
-a.compareTo(b)) // Reverse order to delete files before
|
||||
// directory
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete file: {}", path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
log.info("Deleted session: {}", sessionId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
@@ -48,10 +48,6 @@ public class TaskManager {
|
||||
@Value("${stirling.jobResultExpiryMinutes:30}")
|
||||
private int jobResultExpiryMinutes = 30;
|
||||
|
||||
/** Maximum age of a task that never reached a terminal state. */
|
||||
@Value("${stirling.job.pendingExpiryMinutes:1440}")
|
||||
private int pendingJobExpiryMinutes = 1440;
|
||||
|
||||
private final FileStorage fileStorage;
|
||||
private final JobStore jobStore;
|
||||
private final ClusterBackplane clusterBackplane;
|
||||
@@ -336,32 +332,19 @@ public class TaskManager {
|
||||
}
|
||||
LocalDateTime expiryThreshold =
|
||||
LocalDateTime.now().minus(jobResultExpiryMinutes, ChronoUnit.MINUTES);
|
||||
LocalDateTime pendingExpiryThreshold =
|
||||
LocalDateTime.now().minus(pendingJobExpiryMinutes, ChronoUnit.MINUTES);
|
||||
int removedCount = 0;
|
||||
|
||||
try {
|
||||
for (Map.Entry<String, JobResult> entry : jobResults.entrySet()) {
|
||||
JobResult result = entry.getValue();
|
||||
|
||||
boolean expiredCompletedJob =
|
||||
result.isComplete()
|
||||
&& result.getCompletedAt() != null
|
||||
&& result.getCompletedAt().isBefore(expiryThreshold);
|
||||
boolean abandonedPendingJob =
|
||||
!result.isComplete()
|
||||
&& result.getCreatedAt() != null
|
||||
&& result.getCreatedAt().isBefore(pendingExpiryThreshold);
|
||||
|
||||
// Remove old terminal results and abandoned pending jobs. Without the second
|
||||
// branch, a client that starts a task and never completes it keeps its result in
|
||||
// memory forever.
|
||||
if (expiredCompletedJob || abandonedPendingJob) {
|
||||
// Remove completed jobs that are older than the expiry threshold
|
||||
if (result.isComplete()
|
||||
&& result.getCompletedAt() != null
|
||||
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||
|
||||
// Clean up file results
|
||||
if (expiredCompletedJob) {
|
||||
cleanupJobFiles(result, entry.getKey());
|
||||
}
|
||||
cleanupJobFiles(result, entry.getKey());
|
||||
|
||||
// Remove the job result
|
||||
jobResults.remove(entry.getKey());
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.tool.ToolDiagnostic;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIOSource;
|
||||
import stirling.software.common.model.tool.ToolIOSpec;
|
||||
|
||||
/**
|
||||
* Whether a chain of steps can run: what each produces against what the next accepts.
|
||||
*
|
||||
* <p>The frontend and the AI engine implement the same rules against their generated copies, so a
|
||||
* chain can be checked without a round trip. {@code testing/tool-io-cases.json} pins all three to
|
||||
* the same answers.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ToolChainValidator {
|
||||
|
||||
/** {@code parameters} may be null; only used to resolve an output that depends on one. */
|
||||
public record Step(String operation, Map<String, Object> parameters) {}
|
||||
|
||||
private final ToolIOSource toolIO;
|
||||
|
||||
public List<ToolDiagnostic> validate(List<Step> steps) {
|
||||
return validate(steps, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sourceFormat the format entering step one, or null when unknown
|
||||
*/
|
||||
public List<ToolDiagnostic> validate(List<Step> steps, ToolFormat sourceFormat) {
|
||||
List<ToolDiagnostic> diagnostics = new ArrayList<>();
|
||||
ToolIOSpec.Output carried = null;
|
||||
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
Step step = steps.get(i);
|
||||
Optional<ToolIOSpec> found = toolIO.find(step.operation());
|
||||
if (found.isEmpty()) {
|
||||
diagnostics.add(
|
||||
ToolDiagnostic.warn(
|
||||
i,
|
||||
ToolDiagnostic.UNDECLARED,
|
||||
"Step "
|
||||
+ step.operation()
|
||||
+ " does not declare what it accepts or produces, so the"
|
||||
+ " rest of the chain cannot be checked."));
|
||||
// Nothing is known past an undeclared step.
|
||||
carried = null;
|
||||
continue;
|
||||
}
|
||||
ToolIOSpec spec = found.get();
|
||||
|
||||
// Only the first step is handed the pipeline's input. Every later step is handed the
|
||||
// previous step's output, which is simply unknown once an undeclared step intervened -
|
||||
// checking it against the input again would judge it on a format it never receives.
|
||||
if (i == 0) {
|
||||
checkSource(diagnostics, i, step, spec, sourceFormat);
|
||||
} else if (carried != null) {
|
||||
checkTransition(diagnostics, i, step, spec, carried);
|
||||
}
|
||||
carried = spec.resolveOutput(step.parameters());
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
public static boolean hasErrors(List<ToolDiagnostic> diagnostics) {
|
||||
return diagnostics.stream().anyMatch(d -> d.severity() == ToolDiagnostic.Severity.ERROR);
|
||||
}
|
||||
|
||||
private static void checkSource(
|
||||
List<ToolDiagnostic> diagnostics,
|
||||
int index,
|
||||
Step step,
|
||||
ToolIOSpec spec,
|
||||
ToolFormat sourceFormat) {
|
||||
if (sourceFormat == null || spec.acceptsFormat(sourceFormat)) {
|
||||
return;
|
||||
}
|
||||
diagnostics.add(
|
||||
ToolDiagnostic.error(
|
||||
index,
|
||||
ToolDiagnostic.SOURCE_MISMATCH,
|
||||
"Step "
|
||||
+ step.operation()
|
||||
+ " accepts "
|
||||
+ describe(spec)
|
||||
+ " but the pipeline's input is "
|
||||
+ sourceFormat
|
||||
+ "."));
|
||||
}
|
||||
|
||||
private static void checkTransition(
|
||||
List<ToolDiagnostic> diagnostics,
|
||||
int index,
|
||||
Step step,
|
||||
ToolIOSpec spec,
|
||||
ToolIOSpec.Output previous) {
|
||||
|
||||
if (previous.format() == ToolFormat.NONE) {
|
||||
diagnostics.add(
|
||||
ToolDiagnostic.error(
|
||||
index,
|
||||
ToolDiagnostic.FORMAT_MISMATCH,
|
||||
"The previous step returns a report rather than a file, so "
|
||||
+ step.operation()
|
||||
+ " has nothing to run on."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!spec.acceptsFormat(previous.format())) {
|
||||
String message =
|
||||
"Step "
|
||||
+ step.operation()
|
||||
+ " accepts "
|
||||
+ describe(spec)
|
||||
+ " but the previous step produces "
|
||||
+ previous.format()
|
||||
+ ".";
|
||||
diagnostics.add(
|
||||
previous.certain()
|
||||
? ToolDiagnostic.error(index, ToolDiagnostic.FORMAT_MISMATCH, message)
|
||||
// Unresolved output: may yet be fine once the step is configured.
|
||||
: ToolDiagnostic.warn(index, ToolDiagnostic.OUTPUT_UNCERTAIN, message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!previous.certain()) {
|
||||
diagnostics.add(
|
||||
ToolDiagnostic.warn(
|
||||
index,
|
||||
ToolDiagnostic.OUTPUT_UNCERTAIN,
|
||||
"The previous step's output depends on how it is configured, so this"
|
||||
+ " step may not be able to run."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (previous.arity().isMultiOutput()) {
|
||||
diagnostics.add(
|
||||
spec.arity().isMultiInput()
|
||||
? ToolDiagnostic.info(
|
||||
index,
|
||||
ToolDiagnostic.FAN_IN,
|
||||
"This step combines every file the previous step produced.")
|
||||
: ToolDiagnostic.info(
|
||||
index,
|
||||
ToolDiagnostic.FAN_OUT,
|
||||
"This step runs once for each file the previous step"
|
||||
+ " produced."));
|
||||
}
|
||||
}
|
||||
|
||||
private static String describe(ToolIOSpec spec) {
|
||||
return spec.accepts().stream()
|
||||
.map(Enum::name)
|
||||
.sorted()
|
||||
.reduce((a, b) -> a + " or " + b)
|
||||
.orElse("nothing");
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.model.tool.ToolIOSource;
|
||||
import stirling.software.common.model.tool.ToolIOSpec;
|
||||
|
||||
/**
|
||||
* Reads every {@link ToolIO} declaration off its handler method at startup and serves it by
|
||||
* endpoint path. Replaces parsing the same information out of the description prose, which meant
|
||||
* fetching our own {@code /v1/api-docs} over HTTP first.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ToolIORegistry implements ToolMetadataService, ToolIOSource {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
// Written on the startup thread, read on request threads. Spring's lifecycle establishes
|
||||
// happens-before, so no volatile (same as AiEngineEndpointResolver).
|
||||
private Map<String, ToolIOSpec> specsByPath = Map.of();
|
||||
|
||||
// Keep this the only constructor: with two, Spring falls back to a no-arg one that isn't here.
|
||||
public ToolIORegistry(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/** A registry over known declarations rather than ones discovered from the context. */
|
||||
static ToolIORegistry forSpecs(Map<String, ToolIOSpec> specs) {
|
||||
ToolIORegistry registry = new ToolIORegistry(null);
|
||||
registry.specsByPath = Map.copyOf(specs);
|
||||
return registry;
|
||||
}
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void discoverToolIO() {
|
||||
Map<String, ToolIOSpec> discovered = new TreeMap<>();
|
||||
for (RequestMappingHandlerMapping mapping :
|
||||
applicationContext.getBeansOfType(RequestMappingHandlerMapping.class).values()) {
|
||||
mapping.getHandlerMethods()
|
||||
.forEach((info, handler) -> register(discovered, info, handler));
|
||||
}
|
||||
specsByPath = Map.copyOf(discovered);
|
||||
log.debug("Discovered {} endpoints declaring @ToolIO", specsByPath.size());
|
||||
}
|
||||
|
||||
private static void register(
|
||||
Map<String, ToolIOSpec> target, RequestMappingInfo info, HandlerMethod handler) {
|
||||
ToolIO annotation = handler.getMethodAnnotation(ToolIO.class);
|
||||
if (annotation == null) {
|
||||
return;
|
||||
}
|
||||
ToolIOSpec spec = ToolIOSpec.from(annotation);
|
||||
for (String pattern : extractPatterns(info)) {
|
||||
target.put(pattern, spec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ToolIOSpec> find(String operationPath) {
|
||||
return Optional.ofNullable(specsByPath.get(operationPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMultiInput(String operationPath) {
|
||||
return find(operationPath).map(spec -> spec.arity().isMultiInput()).orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getExtensionTypes(boolean output, String operationPath) {
|
||||
Optional<ToolIOSpec> spec = find(operationPath);
|
||||
if (spec.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<String> extensions =
|
||||
output
|
||||
? spec.get().resolveOutput().format().getExtensions()
|
||||
: spec.get().acceptedExtensions();
|
||||
// Callers express "no restriction" as null.
|
||||
return extensions.isEmpty() ? null : extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldUnpackZipResponse(String operationPath) {
|
||||
// Multi-output zips purely as transport. A single-output ZIP is the deliverable
|
||||
// (extract-attachments) and stays packed.
|
||||
return find(operationPath)
|
||||
.map(spec -> spec.resolveOutput().arity().isMultiOutput())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static Set<String> extractPatterns(RequestMappingInfo info) {
|
||||
try {
|
||||
Method getDirectPaths = info.getClass().getMethod("getDirectPaths");
|
||||
Object result = getDirectPaths.invoke(info);
|
||||
if (result instanceof Set<?> set) {
|
||||
Set<String> patterns = new HashSet<>();
|
||||
for (Object value : set) {
|
||||
if (value instanceof String s) {
|
||||
patterns.add(s);
|
||||
}
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.trace("getDirectPaths unavailable on RequestMappingInfo", e);
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,12 @@ public interface ToolMetadataService {
|
||||
List<String> getExtensionTypes(boolean output, String operationPath);
|
||||
|
||||
/**
|
||||
* Returns true when the endpoint's ZIP response is a transport for several results and should
|
||||
* be unpacked, which is exactly the multi-output endpoints (a {@code SIMO} or {@code MIMO}
|
||||
* arity).
|
||||
* Returns true when the endpoint's ZIP response is a transport for multiple typed results and
|
||||
* should be unpacked: multi-output endpoints (Type:SIMO / Type:MIMO) and wrapper declarations
|
||||
* such as {@code Output:ZIP-PDF} or {@code Output:IMAGE/ZIP}.
|
||||
*
|
||||
* <p>Returns false for an endpoint whose declared output is an archive in its own right (for
|
||||
* example {@code extract-attachments}), where unpacking would discard the deliverable.
|
||||
* <p>Returns false for a bare {@code Output:ZIP} (e.g. {@code get-attachments}), where the
|
||||
* archive itself is the deliverable and should be kept packed.
|
||||
*/
|
||||
boolean shouldUnpackZipResponse(String operationPath);
|
||||
}
|
||||
|
||||
@@ -941,7 +941,7 @@ public class GeneralUtils {
|
||||
}
|
||||
|
||||
// If no MAC address found, use hostname as fallback
|
||||
if (sb.isEmpty()) {
|
||||
if (sb.length() == 0) {
|
||||
String hostname = InetAddress.getLocalHost().getHostName();
|
||||
sb.append(hostname != null ? hostname : "unknown-host");
|
||||
log.warn("No MAC address found, using hostname for fingerprint generation");
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public final class RegexPatternUtils {
|
||||
|
||||
private static final RegexPatternUtils INSTANCE = new RegexPatternUtils();
|
||||
private static final long MAX_CACHED_PATTERNS = 512;
|
||||
private final Cache<PatternKey, Pattern> patternCache =
|
||||
CacheBuilder.newBuilder().maximumSize(MAX_CACHED_PATTERNS).build();
|
||||
private final ConcurrentHashMap<PatternKey, Pattern> patternCache = new ConcurrentHashMap<>();
|
||||
|
||||
private static final String WHITESPACE_REGEX = "\\s++";
|
||||
private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$";
|
||||
@@ -56,7 +51,7 @@ public final class RegexPatternUtils {
|
||||
throw new IllegalArgumentException("Regex pattern cannot be null");
|
||||
}
|
||||
|
||||
return getOrCompile(new PatternKey(regex, 0));
|
||||
return patternCache.computeIfAbsent(new PatternKey(regex, 0), this::compilePattern);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +77,7 @@ public final class RegexPatternUtils {
|
||||
throw new IllegalArgumentException("Regex pattern cannot be null");
|
||||
}
|
||||
|
||||
return getOrCompile(new PatternKey(regex, flags));
|
||||
return patternCache.computeIfAbsent(new PatternKey(regex, flags), this::compilePattern);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +98,7 @@ public final class RegexPatternUtils {
|
||||
* @return true if pattern is cached, false otherwise
|
||||
*/
|
||||
public boolean isCached(String regex, int flags) {
|
||||
return regex != null && patternCache.getIfPresent(new PatternKey(regex, flags)) != null;
|
||||
return regex != null && patternCache.containsKey(new PatternKey(regex, flags));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +107,7 @@ public final class RegexPatternUtils {
|
||||
* @return number of patterns currently cached
|
||||
*/
|
||||
public int getCacheSize() {
|
||||
return (int) patternCache.size();
|
||||
return patternCache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +115,7 @@ public final class RegexPatternUtils {
|
||||
* useful for testing or memory cleanup in long-running applications.
|
||||
*/
|
||||
public void clearCache() {
|
||||
patternCache.invalidateAll();
|
||||
patternCache.clear();
|
||||
log.debug("Regex pattern cache cleared");
|
||||
}
|
||||
|
||||
@@ -146,32 +141,13 @@ public final class RegexPatternUtils {
|
||||
return false;
|
||||
}
|
||||
PatternKey key = new PatternKey(regex, flags);
|
||||
boolean removed = patternCache.getIfPresent(key) != null;
|
||||
patternCache.invalidate(key);
|
||||
boolean removed = patternCache.remove(key) != null;
|
||||
if (removed) {
|
||||
log.debug("Removed regex pattern from cache: {} (flags: {})", regex, flags);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private Pattern getOrCompile(PatternKey key) {
|
||||
try {
|
||||
return patternCache.get(key, () -> compilePattern(key));
|
||||
} catch (UncheckedExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof PatternSyntaxException patternSyntaxException) {
|
||||
throw patternSyntaxException;
|
||||
}
|
||||
throw e;
|
||||
} catch (java.util.concurrent.ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof PatternSyntaxException patternSyntaxException) {
|
||||
throw patternSyntaxException;
|
||||
}
|
||||
throw new IllegalStateException("Failed to compile regex pattern", cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to compile a pattern and handle errors consistently.
|
||||
*
|
||||
@@ -561,6 +537,10 @@ public final class RegexPatternUtils {
|
||||
getPattern("[/\\\\?%*:|\"<>]"); // Unsafe filename characters
|
||||
getPattern("[^a-zA-Z0-9 ]"); // Input sanitization
|
||||
getPattern("[^a-zA-Z0-9]"); // Filename sanitization
|
||||
// API doc patterns
|
||||
getPattern("Output:\\s*(\\w+)");
|
||||
getPattern("Input:\\s*(\\w+)");
|
||||
getPattern("Type:\\s*(\\w+)");
|
||||
log.debug("Pre-compiled {} common regex patterns", patternCache.size());
|
||||
}
|
||||
|
||||
@@ -570,6 +550,23 @@ public final class RegexPatternUtils {
|
||||
"^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*(?:\\.[A-Za-z]{2,})$");
|
||||
}
|
||||
|
||||
/* Pattern for matching Output:<TYPE> in API descriptions */
|
||||
public Pattern getApiDocOutputTypePattern() {
|
||||
return getPattern("Output:\\s*(\\w+)");
|
||||
}
|
||||
|
||||
/* Pattern for matching Input:<TYPE> in API descriptions */
|
||||
public Pattern getApiDocInputTypePattern() {
|
||||
return getPattern("Input:\\s*(\\w+)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern for matching Type:<CODE> in API descriptions
|
||||
*/
|
||||
public Pattern getApiDocTypePattern() {
|
||||
return getPattern("Type:\\s*(\\w+)");
|
||||
}
|
||||
|
||||
/* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */
|
||||
public Pattern getFileExtensionValidationPattern() {
|
||||
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@@ -154,7 +154,7 @@ class PdfMarkdownConverterTest {
|
||||
|| isTableSeparatorRow(line)) {
|
||||
continue;
|
||||
}
|
||||
if (!sb.isEmpty()) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(line);
|
||||
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolDiagnostic;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIOSource;
|
||||
import stirling.software.common.model.tool.ToolIOSpec;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
/** The shared cases in {@code testing/tool-io-cases.json}, which all three implementations run. */
|
||||
class ToolChainValidatorConformanceTest {
|
||||
|
||||
private static final JsonMapper MAPPER = JsonMapper.builder().build();
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> sharedCases() throws IOException {
|
||||
JsonNode root = MAPPER.readTree(Files.readString(casesFile()));
|
||||
Map<String, ToolIOSpec> specs = readSpecs(root.get("specs"));
|
||||
|
||||
List<DynamicTest> tests = new ArrayList<>();
|
||||
for (JsonNode testCase : root.get("cases")) {
|
||||
tests.add(
|
||||
DynamicTest.dynamicTest(
|
||||
testCase.get("name").asString(), () -> runCase(testCase, specs)));
|
||||
}
|
||||
return tests.stream();
|
||||
}
|
||||
|
||||
private static void runCase(JsonNode testCase, Map<String, ToolIOSpec> specs) {
|
||||
Map<String, ToolIOSpec> registry = new HashMap<>();
|
||||
List<ToolChainValidator.Step> steps = new ArrayList<>();
|
||||
|
||||
int index = 0;
|
||||
for (JsonNode stepNode : testCase.get("steps")) {
|
||||
// Each step gets its own path so the same spec can appear twice in a chain.
|
||||
String operation = "/op/" + index++;
|
||||
JsonNode specName = stepNode.get("spec");
|
||||
if (specName != null && !specName.isNull()) {
|
||||
registry.put(operation, specs.get(specName.asString()));
|
||||
}
|
||||
steps.add(new ToolChainValidator.Step(operation, readParameters(stepNode)));
|
||||
}
|
||||
|
||||
JsonNode sourceNode = testCase.get("sourceFormat");
|
||||
ToolFormat sourceFormat =
|
||||
sourceNode == null || sourceNode.isNull()
|
||||
? null
|
||||
: ToolFormat.valueOf(sourceNode.asString());
|
||||
|
||||
List<ToolDiagnostic> actual =
|
||||
new ToolChainValidator(ToolIOSource.of(registry)).validate(steps, sourceFormat);
|
||||
|
||||
assertEquals(summarise(testCase.get("expected")), summarise(actual), describe(actual));
|
||||
}
|
||||
|
||||
private static Map<String, Object> readParameters(JsonNode stepNode) {
|
||||
JsonNode parameters = stepNode.get("parameters");
|
||||
if (parameters == null || parameters.isNull()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
parameters.propertyStream().forEach(e -> values.put(e.getKey(), e.getValue().asString()));
|
||||
return values;
|
||||
}
|
||||
|
||||
private static Map<String, ToolIOSpec> readSpecs(JsonNode node) {
|
||||
Map<String, ToolIOSpec> specs = new HashMap<>();
|
||||
node.propertyStream()
|
||||
.forEach(entry -> specs.put(entry.getKey(), readSpec(entry.getValue())));
|
||||
return specs;
|
||||
}
|
||||
|
||||
private static ToolIOSpec readSpec(JsonNode node) {
|
||||
Set<ToolFormat> accepts = new LinkedHashSet<>();
|
||||
for (JsonNode format : node.get("accepts")) {
|
||||
accepts.add(ToolFormat.valueOf(format.asString()));
|
||||
}
|
||||
List<ToolIOSpec.Case> cases = new ArrayList<>();
|
||||
for (JsonNode rule : node.get("cases")) {
|
||||
List<ToolIOSpec.When> when = new ArrayList<>();
|
||||
for (JsonNode condition : rule.get("when")) {
|
||||
List<String> matches = new ArrayList<>();
|
||||
for (JsonNode match : condition.get("matches")) {
|
||||
matches.add(match.asString());
|
||||
}
|
||||
when.add(new ToolIOSpec.When(condition.get("param").asString(), matches));
|
||||
}
|
||||
cases.add(
|
||||
new ToolIOSpec.Case(
|
||||
when,
|
||||
ToolFormat.valueOf(rule.get("produces").asString()),
|
||||
ToolArity.valueOf(rule.get("arity").asString())));
|
||||
}
|
||||
return new ToolIOSpec(
|
||||
accepts,
|
||||
ToolFormat.valueOf(node.get("produces").asString()),
|
||||
ToolArity.valueOf(node.get("arity").asString()),
|
||||
cases);
|
||||
}
|
||||
|
||||
/** Messages are free text, so compare only the contractual parts. */
|
||||
private static List<String> summarise(List<ToolDiagnostic> diagnostics) {
|
||||
return diagnostics.stream()
|
||||
.map(d -> d.stepIndex() + ":" + d.severity() + ":" + d.code())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<String> summarise(JsonNode expected) {
|
||||
List<String> summary = new ArrayList<>();
|
||||
for (JsonNode node : expected) {
|
||||
summary.add(
|
||||
node.get("stepIndex").asInt()
|
||||
+ ":"
|
||||
+ node.get("severity").asString()
|
||||
+ ":"
|
||||
+ node.get("code").asString());
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
private static String describe(List<ToolDiagnostic> actual) {
|
||||
return actual.stream()
|
||||
.map(ToolDiagnostic::message)
|
||||
.reduce((a, b) -> a + " | " + b)
|
||||
.orElse("no diagnostics");
|
||||
}
|
||||
|
||||
/** Shared with the frontend and engine, so it lives at the repo root. */
|
||||
private static Path casesFile() {
|
||||
Path current = Path.of("").toAbsolutePath();
|
||||
while (current != null) {
|
||||
Path candidate = current.resolve("testing/tool-io-cases.json");
|
||||
if (Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"testing/tool-io-cases.json not found above the working directory");
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.model.tool.ToolIOCase;
|
||||
import stirling.software.common.model.tool.ToolIOSpec;
|
||||
import stirling.software.common.model.tool.ToolIOWhen;
|
||||
|
||||
/**
|
||||
* The registry reads real {@code @ToolIO} annotations off real handler mappings. Everything else
|
||||
* checks the logic against a hand-built map, which would still pass if discovery silently found
|
||||
* nothing.
|
||||
*/
|
||||
class ToolIODiscoveryTest {
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/fixture")
|
||||
static class FixtureController {
|
||||
|
||||
@PostMapping("/rotate")
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
public String rotate() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@PostMapping("/split")
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
|
||||
public String split() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@PostMapping("/add-password")
|
||||
@ToolIO(
|
||||
produces = ToolFormat.PDF_ENCRYPTED,
|
||||
cases =
|
||||
@ToolIOCase(
|
||||
when = {
|
||||
@ToolIOWhen(param = "password", matches = ""),
|
||||
@ToolIOWhen(param = "ownerPassword", matches = "")
|
||||
},
|
||||
produces = ToolFormat.PDF,
|
||||
arity = ToolArity.SISO))
|
||||
public String addPassword() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@PostMapping("/undeclared")
|
||||
public String undeclared() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static ToolIORegistry registry;
|
||||
|
||||
@BeforeAll
|
||||
static void discover() {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
context.registerSingleton("fixtureController", FixtureController.class);
|
||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
||||
mapping.setApplicationContext(context);
|
||||
mapping.afterPropertiesSet();
|
||||
context.getBeanFactory().registerSingleton("requestMappingHandlerMapping", mapping);
|
||||
|
||||
registry = new ToolIORegistry(context);
|
||||
registry.discoverToolIO();
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsDeclarationsOffHandlerMethods() {
|
||||
ToolIOSpec rotate = registry.find("/api/v1/fixture/rotate").orElseThrow();
|
||||
assertEquals(ToolFormat.PDF, rotate.produces());
|
||||
assertEquals(ToolArity.SISO, rotate.arity());
|
||||
assertTrue(rotate.acceptsFormat(ToolFormat.PDF));
|
||||
assertFalse(rotate.acceptsFormat(ToolFormat.PDF_ENCRYPTED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipsMethodsWithNoDeclaration() {
|
||||
assertTrue(registry.find("/api/v1/fixture/undeclared").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void carriesArityThroughToTheUnpackDecision() {
|
||||
assertTrue(registry.shouldUnpackZipResponse("/api/v1/fixture/split"));
|
||||
assertFalse(registry.shouldUnpackZipResponse("/api/v1/fixture/rotate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void carriesCasesThroughToOutputResolution() {
|
||||
ToolIOSpec spec = registry.find("/api/v1/fixture/add-password").orElseThrow();
|
||||
assertEquals(
|
||||
ToolFormat.PDF_ENCRYPTED,
|
||||
spec.resolveOutput(Map.of("password", "x", "ownerPassword", "")).format());
|
||||
assertEquals(
|
||||
ToolFormat.PDF,
|
||||
spec.resolveOutput(Map.of("password", "", "ownerPassword", "")).format());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesInputExtensionsForRunTimeFileChecks() {
|
||||
assertEquals(List.of("pdf"), registry.getExtensionTypes(false, "/api/v1/fixture/rotate"));
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIOSpec;
|
||||
|
||||
/** The {@link ToolMetadataService} behaviour the pipeline executors depend on. */
|
||||
class ToolIORegistryTest {
|
||||
|
||||
private static final String SPLIT = "/api/v1/general/split-pages";
|
||||
private static final String MERGE = "/api/v1/general/merge-pdfs";
|
||||
private static final String ROTATE = "/api/v1/general/rotate-pdf";
|
||||
private static final String ATTACHMENTS = "/api/v1/security/get-attachments";
|
||||
private static final String EXTRACT_IMAGES = "/api/v1/misc/extract-images";
|
||||
private static final String CONVERT_ANY = "/api/v1/convert/file/pdf";
|
||||
private static final String UNKNOWN = "/api/v1/general/does-not-exist";
|
||||
|
||||
private static ToolIOSpec spec(ToolFormat accepts, ToolFormat produces, ToolArity arity) {
|
||||
return new ToolIOSpec(Set.of(accepts), produces, arity, List.of());
|
||||
}
|
||||
|
||||
private final ToolIORegistry registry =
|
||||
ToolIORegistry.forSpecs(
|
||||
Map.of(
|
||||
SPLIT, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.SIMO),
|
||||
MERGE, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.MISO),
|
||||
ROTATE, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.SISO),
|
||||
ATTACHMENTS, spec(ToolFormat.PDF, ToolFormat.ZIP, ToolArity.SISO),
|
||||
EXTRACT_IMAGES, spec(ToolFormat.PDF, ToolFormat.IMAGE, ToolArity.SIMO),
|
||||
CONVERT_ANY, spec(ToolFormat.ANY, ToolFormat.PDF, ToolArity.SISO)));
|
||||
|
||||
@Test
|
||||
void multiInputFollowsArity() {
|
||||
assertTrue(registry.isMultiInput(MERGE));
|
||||
assertFalse(registry.isMultiInput(SPLIT));
|
||||
assertFalse(registry.isMultiInput(ROTATE));
|
||||
assertFalse(registry.isMultiInput(UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inputExtensionsComeFromAcceptedFormats() {
|
||||
assertEquals(List.of("pdf"), registry.getExtensionTypes(false, ROTATE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputExtensionsComeFromTheProducedFormat() {
|
||||
assertEquals(List.of("pdf"), registry.getExtensionTypes(true, ROTATE));
|
||||
assertTrue(registry.getExtensionTypes(true, EXTRACT_IMAGES).contains("png"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noRestrictionIsReportedAsNull() {
|
||||
// Callers treat null as "any type accepted".
|
||||
assertNull(registry.getExtensionTypes(false, CONVERT_ANY));
|
||||
assertNull(registry.getExtensionTypes(false, UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutputResponsesAreUnpacked() {
|
||||
assertTrue(registry.shouldUnpackZipResponse(SPLIT));
|
||||
assertTrue(registry.shouldUnpackZipResponse(EXTRACT_IMAGES));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anArchiveDeliverableStaysPacked() {
|
||||
// The archive is the deliverable; unpacking would lose it.
|
||||
assertFalse(registry.shouldUnpackZipResponse(ATTACHMENTS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleOutputResponsesAreNotUnpacked() {
|
||||
assertFalse(registry.shouldUnpackZipResponse(ROTATE));
|
||||
assertFalse(registry.shouldUnpackZipResponse(MERGE));
|
||||
assertFalse(registry.shouldUnpackZipResponse(UNKNOWN));
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -325,8 +325,11 @@ class RegexPatternUtilsMoreTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageModePattern() {
|
||||
void pageModeAndApiDocPatterns() {
|
||||
assertTrue(utils.getPageModePattern().matcher("a/b").find());
|
||||
assertTrue(utils.getApiDocOutputTypePattern().matcher("Output: PDF").find());
|
||||
assertTrue(utils.getApiDocInputTypePattern().matcher("Input: PDF").find());
|
||||
assertTrue(utils.getApiDocTypePattern().matcher("Type: WEB").find());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -57,11 +57,11 @@ dependencies {
|
||||
// veraPDF still uses javax.xml.bind, not the new jakarta namespace
|
||||
implementation 'javax.xml.bind:jaxb-api:2.3.1'
|
||||
implementation 'com.sun.xml.bind:jaxb-impl:2.3.9'
|
||||
implementation 'com.sun.xml.bind:jaxb-core:4.0.9'
|
||||
implementation 'com.sun.xml.bind:jaxb-core:4.0.7'
|
||||
|
||||
// CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7)
|
||||
implementation "com.google.code.gson:gson:${gsonVersion}"
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.5'
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||
implementation 'org.apache.poi:poi-ooxml:5.5.1'
|
||||
|
||||
@@ -156,7 +156,7 @@ if (buildWithPortal) {
|
||||
}
|
||||
// Workspace root holds package.json and node_modules (shared across editor /
|
||||
// future portal). Editor-specific paths (src, public, dist, tauri) live one
|
||||
// level deeper under frontend/.
|
||||
// level deeper under frontend/editor/.
|
||||
|
||||
// Vite mode: -PprototypesMode > -PfrontendMode > enableSaas > disableAdditional > proprietary.
|
||||
def frontendModeOverride = project.findProperty('frontendMode')?.toString()?.toLowerCase()
|
||||
@@ -176,11 +176,11 @@ def frontendBuildTask = "frontend:build:${frontendMode}"
|
||||
|
||||
// Workspace root holds package.json and node_modules (shared across editor /
|
||||
// future portal). Editor-specific paths (src, public, dist, tauri) live one
|
||||
// level deeper under frontend/. When the portal lands as an embedded
|
||||
// level deeper under frontend/editor/. When the portal lands as an embedded
|
||||
// app, add a sibling frontendPortalDir / frontendPortalDistDir alongside.
|
||||
def frontendDir = file('../../frontend')
|
||||
def frontendEditorDir = file('../../frontend')
|
||||
def frontendEditorDistDir = file('../../frontend/dist')
|
||||
def frontendEditorDir = file('../../frontend/editor')
|
||||
def frontendEditorDistDir = file('../../frontend/editor/dist')
|
||||
def resourcesStaticDir = file('src/main/resources/static')
|
||||
def generatedFrontendPaths = [
|
||||
'assets',
|
||||
|
||||
@@ -321,7 +321,7 @@ public class ExternalAppDepConfig {
|
||||
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (!sb.isEmpty()) sb.append('\n');
|
||||
if (sb.length() > 0) sb.append('\n');
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -40,7 +40,7 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get PDF page count",
|
||||
description = "Returns total number of pages in PDF.")
|
||||
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getPageCount(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
return ResponseEntity.ok(Map.of("pageCount", document.getNumberOfPages()));
|
||||
@@ -54,7 +54,7 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get basic PDF information",
|
||||
description = "Returns page count, version, file size.")
|
||||
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getBasicInfo(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
@@ -72,7 +72,7 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get PDF document properties",
|
||||
description = "Returns title, author, subject, etc.")
|
||||
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getDocumentProperties(@ModelAttribute PDFFile file)
|
||||
throws IOException {
|
||||
// Load the document in read-only mode to prevent modifications and ensure the integrity of
|
||||
@@ -105,7 +105,7 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get page dimensions for all pages",
|
||||
description = "Returns width and height of each page.")
|
||||
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getPageDimensions(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
List<Map<String, Float>> dimensions = new ArrayList<>();
|
||||
@@ -128,7 +128,8 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get form field information",
|
||||
description = "Returns count and details of form fields.")
|
||||
description =
|
||||
"Returns count and details of form fields. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getFormFields(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
Map<String, Object> formInfo = new HashMap<>();
|
||||
@@ -154,7 +155,7 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get annotation information",
|
||||
description = "Returns count and types of annotations.")
|
||||
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getAnnotationInfo(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
Map<String, Object> annotInfo = new HashMap<>();
|
||||
@@ -182,7 +183,8 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get font information",
|
||||
description = "Returns list of fonts used in the document.")
|
||||
description =
|
||||
"Returns list of fonts used in the document. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getFontInfo(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
Map<String, Object> fontInfo = new HashMap<>();
|
||||
@@ -210,7 +212,8 @@ public class AnalysisController {
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get security information",
|
||||
description = "Returns encryption and permission details.")
|
||||
description =
|
||||
"Returns encryption and permission details. Input:PDF Output:JSON Type:SISO")
|
||||
public ResponseEntity<?> getSecurityInfo(@ModelAttribute PDFFile file) throws IOException {
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
|
||||
Map<String, Object> securityInfo = new HashMap<>();
|
||||
|
||||
+3
-6
@@ -29,8 +29,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -49,13 +47,12 @@ public class BookletImpositionController {
|
||||
value = "/booklet-imposition",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Create a booklet with proper page imposition",
|
||||
description =
|
||||
"This operation combines page reordering for booklet printing with multi-page"
|
||||
+ " layout. It rearranges pages in the correct order for booklet printing and"
|
||||
+ " places multiple pages on each sheet for proper folding and binding.")
|
||||
"This operation combines page reordering for booklet printing with multi-page layout. "
|
||||
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
|
||||
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> createBookletImposition(
|
||||
@ModelAttribute BookletImpositionRequest request) throws IOException {
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -133,12 +131,11 @@ public class CropController {
|
||||
value = "/crop",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Crops a PDF document",
|
||||
description =
|
||||
"This operation takes an input PDF file and crops it according to the given"
|
||||
+ " coordinates.")
|
||||
+ " coordinates. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> cropPdf(@ModelAttribute CropPdfForm request)
|
||||
throws IOException {
|
||||
if (request.isAutoCrop()) {
|
||||
|
||||
-4
@@ -26,8 +26,6 @@ import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -49,7 +47,6 @@ public class EditTableOfContentsController {
|
||||
value = "/extract-bookmarks",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@ToolIO(produces = ToolFormat.JSON)
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@@ -154,7 +151,6 @@ public class EditTableOfContentsController {
|
||||
value = "/edit-table-of-contents",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Edit Table of Contents",
|
||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||
|
||||
+9
-10
@@ -34,8 +34,6 @@ import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.general.EditTextOperation;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
@@ -85,18 +83,19 @@ public class EditTextController {
|
||||
value = "/edit-text",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@StandardPdfResponse
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Edit text in a PDF via find and replace",
|
||||
description =
|
||||
"Applies an ordered list of find/replace operations to the text in a PDF and"
|
||||
+ " returns the edited PDF. Useful for find-and-replace, bulk renames (e.g."
|
||||
+ " updating a company name throughout a document), and copy editing where the AI"
|
||||
+ " agent has identified specific replacements. Matching is performed against the"
|
||||
+ " joined text of each page, so find strings can span multiple visual runs"
|
||||
+ " (titles split per word, kerning-broken phrases). Cross-element matches are"
|
||||
+ " written as a single replacement run anchored at the leftmost matched position;"
|
||||
+ " centered or tracked text may shift left when its content changes.")
|
||||
+ " returns the edited PDF. Useful for find-and-replace, bulk renames"
|
||||
+ " (e.g. updating a company name throughout a document), and copy"
|
||||
+ " editing where the AI agent has identified specific replacements."
|
||||
+ " Matching is performed against the joined text of each page, so"
|
||||
+ " find strings can span multiple visual runs (titles split per word,"
|
||||
+ " kerning-broken phrases). Cross-element matches are written as a"
|
||||
+ " single replacement run anchored at the leftmost matched position;"
|
||||
+ " centered or tracked text may shift left when its content changes."
|
||||
+ " Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> editText(@ModelAttribute EditTextRequest request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
|
||||
@@ -41,9 +41,6 @@ import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -268,13 +265,12 @@ public class MergeController {
|
||||
value = "/merge-pdfs",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@StandardPdfResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.MISO)
|
||||
@Operation(
|
||||
summary = "Merge multiple PDF files into one",
|
||||
description =
|
||||
"This endpoint merges multiple PDF files into a single PDF file. The merged"
|
||||
+ " file will contain all pages from the input files in the order they were"
|
||||
+ " provided.")
|
||||
+ " provided. Input:PDF Output:PDF Type:MISO")
|
||||
public ResponseEntity<Resource> mergePdfs(
|
||||
@ModelAttribute MergePdfsRequest request,
|
||||
@RequestParam(value = "fileOrder", required = false) String fileOrder)
|
||||
|
||||
+1
-4
@@ -25,8 +25,6 @@ import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralFormCopyUtils;
|
||||
@@ -46,12 +44,11 @@ public class MultiPageLayoutController {
|
||||
value = "/multi-page-layout",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Merge multiple pages of a PDF document into a single page",
|
||||
description =
|
||||
"This operation takes an input PDF file and the number of pages to merge into a"
|
||||
+ " single sheet in the output PDF file.")
|
||||
+ " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> mergeMultiplePagesIntoOne(
|
||||
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
|
||||
|
||||
|
||||
+1
-5
@@ -27,9 +27,6 @@ import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -49,12 +46,11 @@ public class PdfOverlayController {
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@StandardPdfResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.MISO)
|
||||
@Operation(
|
||||
summary = "Overlay PDF files in various modes",
|
||||
description =
|
||||
"Overlay PDF files onto a base PDF with different modes: Sequential,"
|
||||
+ " Interleaved, or Fixed Repeat.")
|
||||
+ " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
|
||||
public ResponseEntity<Resource> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
|
||||
throws IOException {
|
||||
MultipartFile baseFile = request.getFileInput();
|
||||
|
||||
+4
-7
@@ -29,9 +29,6 @@ import stirling.software.SPDF.model.api.general.PosterPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -52,13 +49,13 @@ public class PosterPdfController {
|
||||
consumes = "multipart/form-data",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@MultiFileResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
|
||||
@Operation(
|
||||
summary = "Split large PDF pages into smaller printable chunks",
|
||||
description =
|
||||
"This endpoint splits large or oddly-sized PDF pages into smaller chunks"
|
||||
+ " suitable for printing on standard paper sizes (e.g., A4, Letter). Divides each"
|
||||
+ " page into a grid of smaller pages using Apache PDFBox.")
|
||||
"This endpoint splits large or oddly-sized PDF pages into smaller chunks "
|
||||
+ "suitable for printing on standard paper sizes (e.g., A4, Letter). "
|
||||
+ "Divides each page into a grid of smaller pages using Apache PDFBox. "
|
||||
+ "Input: PDF Output: ZIP-PDF Type: SISO")
|
||||
public ResponseEntity<Resource> posterPdf(@ModelAttribute PosterPdfRequest request)
|
||||
throws Exception {
|
||||
|
||||
|
||||
+5
-7
@@ -30,8 +30,6 @@ import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
@@ -52,12 +50,12 @@ public class RearrangePagesPDFController {
|
||||
value = "/remove-pages",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@StandardPdfResponse
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Remove pages from a PDF file",
|
||||
description =
|
||||
"This endpoint removes specified pages from a given PDF file. Users can provide"
|
||||
+ " a comma-separated list of page numbers or ranges to delete.")
|
||||
+ " a comma-separated list of page numbers or ranges to delete. Input:PDF"
|
||||
+ " Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> deletePages(@ModelAttribute PDFWithPageNums request)
|
||||
throws IOException {
|
||||
|
||||
@@ -236,13 +234,13 @@ public class RearrangePagesPDFController {
|
||||
value = "/rearrange-pages",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@StandardPdfResponse
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Rearrange pages in a PDF file",
|
||||
description =
|
||||
"This endpoint rearranges pages in a given PDF file based on the specified page"
|
||||
+ " order or custom mode. Users can provide a page order as a comma-separated list"
|
||||
+ " of page numbers or page ranges, or a custom mode.")
|
||||
+ " order or custom mode. Users can provide a page order as a"
|
||||
+ " comma-separated list of page numbers or page ranges, or a custom mode."
|
||||
+ " Input:PDF Output:PDF")
|
||||
public ResponseEntity<Resource> rearrangePages(@ModelAttribute RearrangePagesRequest request)
|
||||
throws IOException {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
|
||||
+1
-4
@@ -20,8 +20,6 @@ import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -40,12 +38,11 @@ public class RotationController {
|
||||
value = "/rotate-pdf",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@StandardPdfResponse
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Rotate a PDF file",
|
||||
description =
|
||||
"This endpoint rotates a given PDF file by a specified angle. The angle must be"
|
||||
+ " a multiple of 90.")
|
||||
+ " a multiple of 90. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> rotatePDF(@ModelAttribute RotatePDFRequest request)
|
||||
throws IOException {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
|
||||
+1
-4
@@ -26,8 +26,6 @@ import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -89,12 +87,11 @@ public class ScalePagesController {
|
||||
value = "/scale-pages",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@ToolIO(produces = ToolFormat.PDF)
|
||||
@Operation(
|
||||
summary = "Change the size of a PDF page/document",
|
||||
description =
|
||||
"This operation takes an input PDF file and the size to scale the pages to in"
|
||||
+ " the output PDF file.")
|
||||
+ " the output PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<Resource> scalePages(@ModelAttribute ScalePagesRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
+3
-6
@@ -30,9 +30,6 @@ import stirling.software.SPDF.model.api.SplitPagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -55,13 +52,13 @@ public class SplitPDFController {
|
||||
value = "/split-pages",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@MultiFileResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
|
||||
@Operation(
|
||||
summary = "Split a PDF file into separate documents",
|
||||
description =
|
||||
"This endpoint splits a given PDF file into separate documents based on the"
|
||||
+ " specified page numbers or ranges. Users can specify pages using individual"
|
||||
+ " numbers, ranges, or 'all' for every page.")
|
||||
+ " specified page numbers or ranges. Users can specify pages using"
|
||||
+ " individual numbers, ranges, or 'all' for every page. Input:PDF"
|
||||
+ " Output:PDF Type:SIMO")
|
||||
public ResponseEntity<Resource> splitPdf(@ModelAttribute SplitPagesRequest request)
|
||||
throws IOException {
|
||||
|
||||
|
||||
+3
-5
@@ -34,9 +34,6 @@ import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -96,10 +93,11 @@ public class SplitPdfByChaptersController {
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@MultiFileResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
|
||||
@Operation(
|
||||
summary = "Split PDFs by Chapters",
|
||||
description = "Splits a PDF into chapters and returns a ZIP file.")
|
||||
description =
|
||||
"Splits a PDF into chapters and returns a ZIP file. Input:PDF Output:ZIP-PDF"
|
||||
+ " Type:SISO")
|
||||
public ResponseEntity<Resource> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
|
||||
throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
+3
-6
@@ -34,9 +34,6 @@ import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -57,13 +54,13 @@ public class SplitPdfBySectionsController {
|
||||
value = "/split-pdf-by-sections",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@MultiFileResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
|
||||
@Operation(
|
||||
summary = "Split PDF pages into smaller sections",
|
||||
description =
|
||||
"Split each page of a PDF into smaller sections based on the user's choice"
|
||||
+ " which page to split, and how to split ( halves, thirds, quarters, etc.), both"
|
||||
+ " vertically and horizontally.")
|
||||
+ " which page to split, and how to split"
|
||||
+ " ( halves, thirds, quarters, etc.), both vertically and horizontally."
|
||||
+ " Input:PDF Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<Resource> splitPdf(
|
||||
@Valid @ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
+4
-7
@@ -29,9 +29,6 @@ import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.tool.ToolArity;
|
||||
import stirling.software.common.model.tool.ToolFormat;
|
||||
import stirling.software.common.model.tool.ToolIO;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
@@ -55,14 +52,14 @@ public class SplitPdfBySizeController {
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@MultiFileResponse
|
||||
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||
description =
|
||||
"split PDF into multiple paged documents based on size/count, ie if 20 pages"
|
||||
+ " and split into 5, it does 5 documents each 4 pages\r\n if 10MB and each page"
|
||||
+ " is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts"
|
||||
+ " 1.9MB but not 2.1MB)")
|
||||
+ " and split into 5, it does 5 documents each 4 pages\r\n"
|
||||
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB"
|
||||
+ " (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF"
|
||||
+ " Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<Resource> autoSplitPdf(
|
||||
@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user