Compare commits

..
307 Commits
Author SHA1 Message Date
James Brunton 270af912d4 Fix frontend download links pointing to split Mac binaries (#6325) 2026-05-07 20:25:01 +01:00
Anthony Stirling efd2ed754c Enhance automate to have import and export buttons (#6326) 2026-05-07 20:23:11 +01:00
ConnorYohandJames Brunton 6730ad7cbb Desktop: persist auth token to disk when Credential Manager is restricted (#6303)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-05-07 16:02:48 +01:00
James Brunton 8fb9fced35 Convert to building a universal Mac app (#6302) 2026-05-07 14:26:28 +01:00
Reece BrowneandEthanHealy01 ed56490f93 Fix move button on multitool (#6291)
Hitting move button twice on multitool didn't work

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-05-05 20:46:47 +00:00
dependabot[bot]andAnthony Stirling c892b985e2 build(deps): bump springSecuritySamlVersion from 7.0.4 to 7.0.5 (#6255)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 19:01:06 +01:00
James Brunton 95c39b4648 Fix missing desktop save indicator on files (#6310)
# Description of Changes
Save indicator stopped showing up after #6050, which fixed the missing
truncation on filenames, but accidentally bypassed the save indicator
component at the same time. This PR puts the component back in and makes
it support truncation so we can have both.

<img width="586" height="166" alt="image"
src="https://github.com/user-attachments/assets/529c3dcb-ee00-4a6d-ae53-ef8657204369"
/>
2026-05-05 11:19:11 +00:00
dependabot[bot] 44f2d0d679 build(deps): bump the embedpdf group across 1 directory with 23 updates (#6299)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 12:15:01 +01:00
dependabot[bot]andAnthony Stirling 1a63aa045e build(deps): bump the tauri group across 1 directory with 7 updates (#6305)
Bumps the tauri group with 5 updates in the /frontend/src-tauri
directory:

| Package | From | To |
| --- | --- | --- |
| [tauri](https://github.com/tauri-apps/tauri) | `2.10.2` | `2.10.3` |
| [tauri-plugin-fs](https://github.com/tauri-apps/plugins-workspace) |
`2.4.5` | `2.5.0` |
| [tauri-plugin-dialog](https://github.com/tauri-apps/plugins-workspace)
| `2.6.0` | `2.7.0` |
| [tauri-plugin-http](https://github.com/tauri-apps/plugins-workspace) |
`2.5.7` | `2.5.8` |
|
[tauri-plugin-single-instance](https://github.com/tauri-apps/plugins-workspace)
| `2.4.0` | `2.4.1` |


Updates `tauri` from 2.10.2 to 2.10.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/tauri/releases">tauri's
releases</a>.</em></p>
<blockquote>
<h2>tauri v2.10.3</h2>
<!-- raw HTML omitted -->
<pre><code>Fetching advisory database from
`https://github.com/RustSec/advisory-db.git`
Loaded 941 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (1052 crate dependencies)
Crate:     atk
Version:   0.18.2
Warning:   unmaintained
Title:     gtk-rs GTK3 bindings - no longer maintained
Date:      2024-03-04
ID:        RUSTSEC-2024-0413
URL:       https://rustsec.org/advisories/RUSTSEC-2024-0413
Dependency tree:
atk 0.18.2
└── gtk 0.18.2
    ├── wry 0.54.0
    │   └── tauri-runtime-wry 2.10.1
    │       └── tauri 2.10.3
    │           ├── tauri-utils 2.8.3
    │           │   ├── tauri-schema-generator 0.0.0
    │           │   ├── tauri-runtime-wry 2.10.1
    │           │   ├── tauri-runtime 2.10.1
    │           │   │   ├── tauri-runtime-wry 2.10.1
    │           │   │   └── tauri 2.10.3
    │           │   ├── tauri-plugin 2.5.4
    │           │   │   ├── tauri-plugin-sample 0.1.0
    │           │   │   │   └── api 0.1.0
    │           │   │   └── tauri-plugin-log 2.6.0
    │           │   │       └── api 0.1.0
    │           │   ├── tauri-macros 2.5.5
    │           │   │   └── tauri 2.10.3
    │           │   ├── tauri-codegen 2.5.5
    │           │   │   ├── tauri-macros 2.5.5
    │           │   │   └── tauri-build 2.5.6
    │           │   │       ├── tauri-file-associations-demo 0.1.0
    │           │   │       ├── tauri 2.10.3
    │           │   │       ├── resources 0.1.0
    │           │   │       ├── bench_helloworld 0.1.0
    │           │   │       ├── bench_files_transfer 0.1.0
    │           │   │       ├── bench_cpu_intensive 0.1.0
    │           │   │       └── api 0.1.0
    │           │   ├── tauri-cli 2.10.1
    │           │   │   └── tauri-cli-node 0.0.0
    │           │   ├── tauri-bundler 2.8.1
    │           │   │   └── tauri-cli 2.10.1
    │           │   ├── tauri-build 2.5.6
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/tauri/commit/9b17a7aeae9a83222ffe829aa4e2d8a5ba6bed8c"><code>9b17a7a</code></a>
fix(ci): bump rustsec/audit-check to v2 and ignore time audit (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15030">#15030</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/d86827980d45826bb30749a3dbc11c9326ed8bd9"><code>d868279</code></a>
apply version updates (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14897">#14897</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/3a65cc6885ea61e35dc5be23b229043ab6e92372"><code>3a65cc6</code></a>
fix(test): disable <code>resolve_resource_dir</code> on Android (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15026">#15026</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/52cf195b78e0b78f02d8aae997116c620355b095"><code>52cf195</code></a>
refactor(cli): reduce some nesting code (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14844">#14844</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/c3cbff3f7430161715f80f82128b345a6f7140c9"><code>c3cbff3</code></a>
fix: resource path handles <code>./</code> path differently (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14662">#14662</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/33754ae5e3740d022483b6164511c5c001a3c24b"><code>33754ae</code></a>
fix(cli): unusable empty password private keys (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15022">#15022</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/3935dee121e1dcb4f377c9933233c0ace186b0bb"><code>3935dee</code></a>
Add AI tool policy to contributing guide (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15002">#15002</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/33932a72b27c970dde242da5241f7a340277507d"><code>33932a7</code></a>
chore(deps-dev): bump svelte from 5.51.5 to 5.53.5 (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15015">#15015</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/7d3c7593a9d41ac1cc7f72e959263f22d32bc61d"><code>7d3c759</code></a>
chore(deps): update dependency rollup to v4.59.0 (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15001">#15001</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/f20256bca542dfd6ba4b5e2741a6a9439cc57be8"><code>f20256b</code></a>
chore: fix clippy warnings (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14999">#14999</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/tauri/compare/tauri-v2.10.2...tauri-v2.10.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `tauri-plugin-fs` from 2.4.5 to 2.5.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/plugins-workspace/releases">tauri-plugin-fs's
releases</a>.</em></p>
<blockquote>
<h2>dialog-js v2.5.0</h2>
<h2>[2.5.0]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/dff6fa986a9a05ba98b6ca660fea78ae97251fc2"><code>dff6fa98</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3034">#3034</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../onehumandev"><code>@​onehumandev</code></a>)
Add <code>pickerMode</code> option to file picker (currently only used
on iOS)</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li>Upgraded to <code>fs-js@2.4.5</code></li>
</ul>
<h3>feat</h3>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/c23fa03f07d5c1c220bcf0bca482364513e3f754"><code>c23fa03f</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3098">#3098</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../Lepidopteran"><code>@​Lepidopteran</code></a>)
Add <code>xdg-portal</code> as an optional feature for
<code>rfd</code></li>
</ul>
<!-- raw HTML omitted -->
<pre><code>npm warn Unknown user config &quot;always-auth&quot;. This
will stop working in the next major version of npm.
npm warn publish npm auto-corrected some errors in your package.json
when publishing. Please run &quot;npm pkg fix&quot; to address these
errors.
npm warn publish errors corrected:
npm warn publish &quot;repository&quot; was changed from a string to an
object
npm warn publish &quot;repository.url&quot; was normalized to
&quot;git+https://github.com/tauri-apps/plugins-workspace.git&quot;
npm notice
npm notice 📦  @tauri-apps/plugin-dialog@2.5.0
npm notice Tarball Contents
npm notice 888B LICENSE.spdx
npm notice 3.5kB README.md
npm notice 6.4kB dist-js/index.cjs
npm notice 12.2kB dist-js/index.d.ts
npm notice 6.3kB dist-js/index.js
npm notice 11B dist-js/init.d.ts
npm notice 656B package.json
npm notice Tarball Details
npm notice name: @tauri-apps/plugin-dialog
npm notice version: 2.5.0
npm notice filename: tauri-apps-plugin-dialog-2.5.0.tgz
npm notice package size: 5.8 kB
npm notice unpacked size: 30.0 kB
npm notice shasum: 52057077b52cc51643ac9829d48c2c590e5e1a54
npm notice integrity: sha512-I0R0ygwRd9AN8[...]lxPHD5vDcFjiA==
npm notice total files: 7
npm notice
npm notice Security Notice: Classic tokens have been revoked. Granular
tokens are now limited to 90 days and require 2FA by default. Update
your CI/CD workflows to avoid disruption. Learn more
https://gh.io/all-npm-classic-tokens-revoked
npm notice Publishing to https://registry.npmjs.org/ with tag latest and
public access
npm notice publish Signed provenance statement with source and build
information from GitHub Actions
npm notice publish Provenance statement published to transparency log:
https://search.sigstore.dev/?logIndex=804721691
+ @tauri-apps/plugin-dialog@2.5.0
</code></pre>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/d6a3898001a4bcc659e045f9501498751b77dbe6"><code>d6a3898</code></a>
Publish New Versions (v2) (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3268">#3268</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/2e5bcdf202f98ec0fb0481725dd32f0626c766c8"><code>2e5bcdf</code></a>
chore(deps): fix audits (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3373">#3373</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/4374b4fc1a9a7c0506034d86707fd3288d347f9b"><code>4374b4f</code></a>
chore(notification): remove unused dev-deps (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3372">#3372</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/f75d21db3351d6f12adf585c2c797c20ece94f7f"><code>f75d21d</code></a>
chore(deps): remove used of tauri-utils build feature (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3360">#3360</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/4b95f5e079593d5b2b98220c332f420dc70d18d1"><code>4b95f5e</code></a>
chore(deps): update dependency eslint to v10.1.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3357">#3357</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/99c3e37b54c115e8712f67c2b53ed1f2f0b4aae6"><code>99c3e37</code></a>
chore(deps): bump tar in /plugins/updater/tests/updater-migration/v1-app
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3352">#3352</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/eaac19a5b7e325a06d4c1c6d740e38d86c55cd89"><code>eaac19a</code></a>
chore(deps): update rust crate tar to v0.4.45 [security] (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3353">#3353</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/5183e314cbabc179228e232992ad67025f30272d"><code>5183e31</code></a>
chore(deps): update dependency typescript-eslint to v8.57.1 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3344">#3344</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/2c0883e64e9a53ea18102ed350cd5e65ba7c8f4b"><code>2c0883e</code></a>
chore(deps): update dependency vite to v8 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3346">#3346</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/024ec0c29c20cf94579dab9b79d6be0da61a8daa"><code>024ec0c</code></a>
fix(deep-link): ChromeOS deep link calls filtered and ignored by plugin
(fix ...</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.4.5...fs-v2.5.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `tauri-plugin-dialog` from 2.6.0 to 2.7.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/plugins-workspace/releases">tauri-plugin-dialog's
releases</a>.</em></p>
<blockquote>
<h2>dialog-js v2.7.0</h2>
<h2>[2.7.0]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/24154472a6710a690173df0a121125d1f1b871e8"><code>24154472</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3287">#3287</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../Legend-Master"><code>@​Legend-Master</code></a>)
Re-use <code>message</code> command in Rust side for <code>ask</code>
and <code>confirm</code> commands, <code>allow-ask</code> and
<code>allow-confirm</code> permissions are now aliases to
<code>allow-message</code></li>
</ul>
<h3>Dependencies</h3>
<ul>
<li>Upgraded to <code>fs-js@2.5.0</code></li>
</ul>
<!-- raw HTML omitted -->
<pre><code>npm warn Unknown user config &quot;always-auth&quot;. This
will stop working in the next major version of npm. See `npm help npmrc`
for supported config options.
npm warn publish npm auto-corrected some errors in your package.json
when publishing. Please run &quot;npm pkg fix&quot; to address these
errors.
npm warn publish errors corrected:
npm warn publish &quot;repository&quot; was changed from a string to an
object
npm warn publish &quot;repository.url&quot; was normalized to
&quot;git+https://github.com/tauri-apps/plugins-workspace.git&quot;
npm notice
npm notice 📦  @tauri-apps/plugin-dialog@2.7.0
npm notice Tarball Contents
npm notice 888B LICENSE.spdx
npm notice 3.5kB README.md
npm notice 6.9kB dist-js/index.cjs
npm notice 14.6kB dist-js/index.d.ts
npm notice 6.8kB dist-js/index.js
npm notice 11B dist-js/init.d.ts
npm notice 657B package.json
npm notice Tarball Details
npm notice name: @tauri-apps/plugin-dialog
npm notice version: 2.7.0
npm notice filename: tauri-apps-plugin-dialog-2.7.0.tgz
npm notice package size: 6.7 kB
npm notice unpacked size: 33.3 kB
npm notice shasum: b510ecd42d9900725eaf51f42ec98523c40d29b4
npm notice integrity: sha512-4nS/hfGMGCXiA[...]RtrKXkANKDHvw==
npm notice total files: 7
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and
public access
npm notice publish Signed provenance statement with source and build
information from GitHub Actions
npm notice publish Provenance statement published to transparency log:
https://search.sigstore.dev/?logIndex=1235993203
+ @tauri-apps/plugin-dialog@2.7.0
</code></pre>
<!-- raw HTML omitted -->
<h2>dialog v2.7.0</h2>
<h2>[2.7.0]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/24154472a6710a690173df0a121125d1f1b871e8"><code>24154472</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3287">#3287</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../Legend-Master"><code>@​Legend-Master</code></a>)
Re-use <code>message</code> command in Rust side for <code>ask</code>
and <code>confirm</code> commands, <code>allow-ask</code> and
<code>allow-confirm</code> permissions are now aliases to
<code>allow-message</code></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/51b430be986ed98635c02ba504f6f30a35771c01"><code>51b430b</code></a>
ci: delete .changes/updater-new-bundle-support.md</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/fd439b143e265fc3ab4de435d6d4b641cffc66f1"><code>fd439b1</code></a>
Publish New Versions (v2) (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2964">#2964</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/2522b71f6bcae65c03b24415eb9295c9e7c84ffc"><code>2522b71</code></a>
fix(deep-link): revert the breaking change introduced by <a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2928">#2928</a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2970">#2970</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/9021a732474322073769c1b21f6eee2e4b0dd15e"><code>9021a73</code></a>
chore(deps): update dependency rollup to v4.50.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2966">#2966</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/625bb1c0965394b88522643731f78ccbcca84add"><code>625bb1c</code></a>
feat(log): re-export the log crate (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2965">#2965</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/6215afe0239f815740a2c28150614ca4fef0b486"><code>6215afe</code></a>
chore(deps): update dependency rollup to v4.49.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2962">#2962</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/8cf8eeab02efddd4e44e3bba92b33902a8c00956"><code>8cf8eea</code></a>
feat(updater): inject bundle_type into endpoint url (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2960">#2960</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/509eba8d441c4f6ecf0af77b572cb2afd69a752d"><code>509eba8</code></a>
feat: support message dialogs with 3 buttons (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2641">#2641</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/9ac5fe84e704ef20c437456cb1017b54b101b333"><code>9ac5fe8</code></a>
feat(updater): support bundle-specific targets (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2624">#2624</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/c24741031993894929a9ed00972ff9f6cd9540be"><code>c247410</code></a>
chore(deps): update dependency typescript-eslint to v8.41.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2956">#2956</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/plugins-workspace/compare/log-v2.6.0...log-v2.7.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `tauri-plugin-http` from 2.5.7 to 2.5.8
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/plugins-workspace/releases">tauri-plugin-http's
releases</a>.</em></p>
<blockquote>
<h2>http-js v2.5.8</h2>
<h2>[2.5.8]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/29712892526cfc2d35c9002e0a56925084ae1b73"><code>29712892</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3252">#3252</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../NVolcz"><code>@​NVolcz</code></a>)
Correct Response header initialization to support cloning and ensure
Set-Cookie visibility.</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li>Upgraded to <code>fs-js@2.5.0</code></li>
</ul>
<!-- raw HTML omitted -->
<pre><code>npm warn Unknown user config &quot;always-auth&quot;. This
will stop working in the next major version of npm. See `npm help npmrc`
for supported config options.
npm warn publish npm auto-corrected some errors in your package.json
when publishing. Please run &quot;npm pkg fix&quot; to address these
errors.
npm warn publish errors corrected:
npm warn publish &quot;repository&quot; was changed from a string to an
object
npm warn publish &quot;repository.url&quot; was normalized to
&quot;git+https://github.com/tauri-apps/plugins-workspace.git&quot;
npm notice
npm notice 📦  @tauri-apps/plugin-http@2.5.8
npm notice Tarball Contents
npm notice 888B LICENSE.spdx
npm notice 2.6kB README.md
npm notice 7.1kB dist-js/index.cjs
npm notice 2.4kB dist-js/index.d.ts
npm notice 7.0kB dist-js/index.js
npm notice 655B package.json
npm notice Tarball Details
npm notice name: @tauri-apps/plugin-http
npm notice version: 2.5.8
npm notice filename: tauri-apps-plugin-http-2.5.8.tgz
npm notice package size: 5.1 kB
npm notice unpacked size: 20.5 kB
npm notice shasum: a6e638fd0d1b17563ef0c53745373c0b1eb50260
npm notice integrity: sha512-oxd7oypzQeu8k[...]hKUbhuKWcunRw==
npm notice total files: 6
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and
public access
npm notice publish Signed provenance statement with source and build
information from GitHub Actions
npm notice publish Provenance statement published to transparency log:
https://search.sigstore.dev/?logIndex=1235993387
+ @tauri-apps/plugin-http@2.5.8
</code></pre>
<!-- raw HTML omitted -->
<h2>http v2.5.8</h2>
<h2>[2.5.8]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/29712892526cfc2d35c9002e0a56925084ae1b73"><code>29712892</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3252">#3252</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../NVolcz"><code>@​NVolcz</code></a>)
Correct Response header initialization to support cloning and ensure
Set-Cookie visibility.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/d6a3898001a4bcc659e045f9501498751b77dbe6"><code>d6a3898</code></a>
Publish New Versions (v2) (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3268">#3268</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/2e5bcdf202f98ec0fb0481725dd32f0626c766c8"><code>2e5bcdf</code></a>
chore(deps): fix audits (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3373">#3373</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/4374b4fc1a9a7c0506034d86707fd3288d347f9b"><code>4374b4f</code></a>
chore(notification): remove unused dev-deps (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3372">#3372</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/f75d21db3351d6f12adf585c2c797c20ece94f7f"><code>f75d21d</code></a>
chore(deps): remove used of tauri-utils build feature (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3360">#3360</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/4b95f5e079593d5b2b98220c332f420dc70d18d1"><code>4b95f5e</code></a>
chore(deps): update dependency eslint to v10.1.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3357">#3357</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/99c3e37b54c115e8712f67c2b53ed1f2f0b4aae6"><code>99c3e37</code></a>
chore(deps): bump tar in /plugins/updater/tests/updater-migration/v1-app
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3352">#3352</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/eaac19a5b7e325a06d4c1c6d740e38d86c55cd89"><code>eaac19a</code></a>
chore(deps): update rust crate tar to v0.4.45 [security] (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3353">#3353</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/5183e314cbabc179228e232992ad67025f30272d"><code>5183e31</code></a>
chore(deps): update dependency typescript-eslint to v8.57.1 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3344">#3344</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/2c0883e64e9a53ea18102ed350cd5e65ba7c8f4b"><code>2c0883e</code></a>
chore(deps): update dependency vite to v8 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3346">#3346</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/024ec0c29c20cf94579dab9b79d6be0da61a8daa"><code>024ec0c</code></a>
fix(deep-link): ChromeOS deep link calls filtered and ignored by plugin
(fix ...</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/plugins-workspace/compare/http-v2.5.7...http-v2.5.8">compare
view</a></li>
</ul>
</details>
<br />

Updates `tauri-plugin-single-instance` from 2.4.0 to 2.4.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/plugins-workspace/releases">tauri-plugin-single-instance's
releases</a>.</em></p>
<blockquote>
<h2>store-js v2.4.1</h2>
<h2>[2.4.1]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9"><code>93426f85</code></a>
Fixed an issue that caused docs.rs builds to fail. No user facing
changes.</li>
</ul>
<!-- raw HTML omitted -->
<pre><code>npm warn publish npm auto-corrected some errors in your
package.json when publishing. Please run &quot;npm pkg fix&quot; to
address these errors.
npm warn publish errors corrected:
npm warn publish &quot;repository&quot; was changed from a string to an
object
npm warn publish &quot;repository.url&quot; was normalized to
&quot;git+https://github.com/tauri-apps/plugins-workspace.git&quot;
npm notice
npm notice 📦  @tauri-apps/plugin-store@2.4.1
npm notice Tarball Contents
npm notice 888B LICENSE.spdx
npm notice 4.4kB README.md
npm notice 6.6kB dist-js/index.cjs
npm notice 8.5kB dist-js/index.d.ts
npm notice 6.4kB dist-js/index.js
npm notice 711B package.json
npm notice Tarball Details
npm notice name: @tauri-apps/plugin-store
npm notice version: 2.4.1
npm notice filename: tauri-apps-plugin-store-2.4.1.tgz
npm notice package size: 5.5 kB
npm notice unpacked size: 27.5 kB
npm notice shasum: 5e2d3362e41861d2fa79a3f1a78c091e12963236
npm notice integrity: sha512-ckGSEzZ5Ii4Hf[...]ugpGRDOFOunkA==
npm notice total files: 6
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and
public access
npm notice publish Signed provenance statement with source and build
information from GitHub Actions
npm notice publish Provenance statement published to transparency log:
https://search.sigstore.dev/?logIndex=644610830
+ @tauri-apps/plugin-store@2.4.1
</code></pre>
<!-- raw HTML omitted -->
<h2>store v2.4.1</h2>
<h2>[2.4.1]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9"><code>93426f85</code></a>
Fixed an issue that caused docs.rs builds to fail. No user facing
changes.</li>
</ul>
<!-- raw HTML omitted -->
<pre><code>Updating crates.io index
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/d66aa6ff78cbbeaef1542a1fa930c33399578a25"><code>d66aa6f</code></a>
publish new versions (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2822">#2822</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/6f345870df4e7b187deb869df03b79858e03b4fe"><code>6f34587</code></a>
fix(single-instance): disable dbus name replacement (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2860">#2860</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/708fa4e2b77e847ce554115e80f7eb685f4a322e"><code>708fa4e</code></a>
chore(deps): update dependency eslint-config-prettier to v10.1.8 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2858">#2858</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/b7292030594daa04e78979214478031241b6e38e"><code>b729203</code></a>
fix(upload): fix download() locks main thread on Android (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2838">#2838</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/2f9c71aae77cddd4ca2ffe9d13e9e1f23ee4f478"><code>2f9c71a</code></a>
chore(deps): update dependency rollup to v4.45.1 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2850">#2850</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/80d4d8e128922b022e76049189a657b71ce95a97"><code>80d4d8e</code></a>
chore(deps): update eslint monorepo to v9.31.0 (v2) (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2839">#2839</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/e7a98b0d2e60463ff86a0f031ce3c84c83a21274"><code>e7a98b0</code></a>
chore(deps): update dependency typescript-eslint to v8.37.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2848">#2848</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/44a1f659125a341191420e650608b0b6ff316a0e"><code>44a1f65</code></a>
fix(fs): <code>writeFile</code> create file by default (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2846">#2846</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/6210cd31df00bb3c55642013c74dc1da81fa00f9"><code>6210cd3</code></a>
chore(deps): update dependency rollup to v4.45.0 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2841">#2841</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/467f07b7de4103a653e40fec8badb6f24abcef39"><code>467f07b</code></a>
chore(deps): update dependency vite to v7 (v2) (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/2800">#2800</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/plugins-workspace/compare/fs-v2.4.0...fs-v2.4.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `tauri-plugin-deep-link` from 2.4.7 to 2.4.9
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/plugins-workspace/releases">tauri-plugin-deep-link's
releases</a>.</em></p>
<blockquote>
<h2>deep-link-js v2.4.9</h2>
<h2>[2.4.9]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/e6cdc9f52e2cd975b11b8e4c12879d597f1f76c3"><code>e6cdc9f5</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3396">#3396</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../Legend-Master"><code>@​Legend-Master</code></a>)
Fix broken iOS custom URL schemes</li>
</ul>
<!-- raw HTML omitted -->
<pre><code>npm warn Unknown user config &quot;always-auth&quot;. This
will stop working in the next major version of npm. See `npm help npmrc`
for supported config options.
npm warn publish npm auto-corrected some errors in your package.json
when publishing. Please run &quot;npm pkg fix&quot; to address these
errors.
npm warn publish errors corrected:
npm warn publish &quot;repository&quot; was changed from a string to an
object
npm warn publish &quot;repository.url&quot; was normalized to
&quot;git+https://github.com/tauri-apps/plugins-workspace.git&quot;
npm notice
npm notice 📦  @tauri-apps/plugin-deep-link@2.4.9
npm notice Tarball Contents
npm notice 888B LICENSE.spdx
npm notice 6.2kB README.md
npm notice 3.5kB dist-js/index.cjs
npm notice 2.9kB dist-js/index.d.ts
npm notice 3.4kB dist-js/index.js
npm notice 801B package.json
npm notice Tarball Details
npm notice name: @tauri-apps/plugin-deep-link
npm notice version: 2.4.9
npm notice filename: tauri-apps-plugin-deep-link-2.4.9.tgz
npm notice package size: 4.4 kB
npm notice unpacked size: 17.7 kB
npm notice shasum: ae56d59130380f806b533b3107c3f16654e66a8d
npm notice integrity: sha512-u0SKOUHnJ1wqe[...]hIvqLBRpgHJlA==
npm notice total files: 6
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and
public access
npm notice publish Signed provenance statement with source and build
information from GitHub Actions
npm notice publish Provenance statement published to transparency log:
https://search.sigstore.dev/?logIndex=1429011657
+ @tauri-apps/plugin-deep-link@2.4.9
</code></pre>
<!-- raw HTML omitted -->
<h2>deep-link v2.4.9</h2>
<h2>[2.4.9]</h2>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/e6cdc9f52e2cd975b11b8e4c12879d597f1f76c3"><code>e6cdc9f5</code></a>
(<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/pull/3396">#3396</a>
by <a
href="https://github.com/tauri-apps/plugins-workspace/../../Legend-Master"><code>@​Legend-Master</code></a>)
Fix broken iOS custom URL schemes</li>
</ul>
<!-- raw HTML omitted -->
<pre><code>&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/5c7668b6bb7c9a509f394d584568b3a922161e50"><code>5c7668b</code></a>
publish new versions (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3397">#3397</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/ec0540138bece081e9a87982091947360e61987a"><code>ec05401</code></a>
chore(deps): update rust crate toml to v1 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3323">#3323</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/b86e999beb9fb48ef54adf3d0af631f94c06a5d0"><code>b86e999</code></a>
chore(deps): update tauri packages to 2.11 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3407">#3407</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/c463d8ab1422a4cf44f627a7b4e6ba9d3553f334"><code>c463d8a</code></a>
chore(deps): update rustls-webpki in lockfile, ignore core2 in audit (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3405">#3405</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/1bb7beb3076a8bf76b084223d0e4225bb2e53bc9"><code>1bb7beb</code></a>
chore(deps): bump openssl (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3402">#3402</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/3412fa2741b3b6e7a251e3548a43b21f2c26c635"><code>3412fa2</code></a>
docs(readme): fix platform support matrix (opener supports mobile)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/af81fdac9881d4ed52fbc18b9511bf10f7540ebb"><code>af81fda</code></a>
docs(readme): fix platform support matrix (mobile is supported)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/c1fd33b3a2735f2e25c1d026dc524af932db3315"><code>c1fd33b</code></a>
fix(opener): allow open network share locations (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3343">#3343</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/250857b7b72cd59825a14caa046160dc2ba1bb12"><code>250857b</code></a>
chore(deps): update dependency typescript to v6 (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3363">#3363</a>)</li>
<li><a
href="https://github.com/tauri-apps/plugins-workspace/commit/964e13f124ad1feeb93c10168b265dc4936f738c"><code>964e13f</code></a>
fix(store): dead lock trying to set while exiting (<a
href="https://redirect.github.com/tauri-apps/plugins-workspace/issues/3395">#3395</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/plugins-workspace/compare/deep-link-v2.4.7...deep-link-v2.4.9">compare
view</a></li>
</ul>
</details>
<br />

Updates `tauri-build` from 2.5.5 to 2.6.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tauri-apps/tauri/releases">tauri-build's
releases</a>.</em></p>
<blockquote>
<h2>tauri-build v2.6.0</h2>
<!-- raw HTML omitted -->
<pre><code>Fetching advisory database from
`https://github.com/RustSec/advisory-db.git`
Loaded 1060 security advisories (from /home/runner/.cargo/advisory-db)
    Updating crates.io index
    Scanning Cargo.lock for vulnerabilities (1086 crate dependencies)
Crate:     atk
Version:   0.18.2
Warning:   unmaintained
Title:     gtk-rs GTK3 bindings - no longer maintained
Date:      2024-03-04
ID:        RUSTSEC-2024-0413
URL:       https://rustsec.org/advisories/RUSTSEC-2024-0413
Dependency tree:
atk 0.18.2
└── gtk 0.18.2
    ├── wry 0.55.0
    │   └── tauri-runtime-wry 2.11.0
    │       └── tauri 2.11.0
    │           ├── tauri-utils 2.9.0
    │           │   ├── tauri-schema-generator 0.0.0
    │           │   ├── tauri-runtime-wry 2.11.0
    │           │   ├── tauri-runtime 2.11.0
    │           │   │   ├── tauri-runtime-wry 2.11.0
    │           │   │   └── tauri 2.11.0
    │           │   ├── tauri-plugin 2.6.0
    │           │   │   ├── tauri-plugin-sample 0.1.0
    │           │   │   │   └── api 0.1.0
    │           │   │   └── tauri-plugin-log 2.6.0
    │           │   │       └── api 0.1.0
    │           │   ├── tauri-macros 2.6.0
    │           │   │   └── tauri 2.11.0
    │           │   ├── tauri-codegen 2.6.0
    │           │   │   ├── tauri-macros 2.6.0
    │           │   │   └── tauri-build 2.6.0
    │           │   │       ├── tauri-file-associations-demo 0.1.0
    │           │   │       ├── tauri 2.11.0
    │           │   │       ├── resources 0.1.0
    │           │   │       ├── bench_helloworld 0.1.0
    │           │   │       ├── bench_files_transfer 0.1.0
    │           │   │       ├── bench_cpu_intensive 0.1.0
    │           │   │       └── api 0.1.0
    │           │   ├── tauri-cli 2.11.0
    │           │   │   └── tauri-cli-node 0.0.0
    │           │   ├── tauri-bundler 2.9.0
    │           │   │   └── tauri-cli 2.11.0
    │           │   ├── tauri-build 2.6.0
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tauri-apps/tauri/commit/e60834fc67d87c10e2f44b2568052295cb61c325"><code>e60834f</code></a>
Apply Version Updates From Current Changes (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15041">#15041</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/df05c00563a91fc936bd15c6b10dd2825472f96b"><code>df05c00</code></a>
chore: minor bump for codegen crate</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/13bea1777fed60c75e37fe67d7ded5dde0b2997d"><code>13bea17</code></a>
chore: fmt</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/9808236ebf7755d498d674b614f3fc75eeac1ec4"><code>9808236</code></a>
fix(macOS): correct value for work_area.position.y (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14655">#14655</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/eb0312ea9e493954298ac0b3fdaae7eafb52750e"><code>eb0312e</code></a>
feat(mobile): Propagate tao::Event::Suspended and tao::Event::Resumed to
the ...</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/4ef5797f0fb27fa2df3f39f4a54e48ef319560ec"><code>4ef5797</code></a>
feat(ios): add --no-sign and --archive-only flags to ios build (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15061">#15061</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/110336c88a8c0a04476619db0a5c8f7694d969a5"><code>110336c</code></a>
fix(macOS): fix incorrect window position on multi-monitor setups (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/15250">#15250</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/c00a3dbffccd6e051d3b7332f706b6c63759865d"><code>c00a3db</code></a>
feat(macros): add support for rename command macro in tauri-macros <a
href="https://redirect.github.com/tauri-apps/tauri/issues/14173">#14173</a>
(<a
href="https://redirect.github.com/tauri-apps/tauri/issues/1">#1</a>...</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/764b9139a32de149d8a914a6b5ec6cd1937c64eb"><code>764b913</code></a>
feat(cli): restart Android emulator if it is disconnected from adb (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14313">#14313</a>)</li>
<li><a
href="https://github.com/tauri-apps/tauri/commit/1035f12eeb8b23d9780881606d442d11c786e39e"><code>1035f12</code></a>
fix(windows): tauri-bundler detect arm system (<a
href="https://redirect.github.com/tauri-apps/tauri/issues/14923">#14923</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tauri-apps/tauri/compare/tauri-build-v2.5.5...tauri-build-v2.6.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 12:05:54 +01:00
dependabot[bot] 1ae24e18b5 build(deps): bump eclipse-temurin from a051234 to b27ca47 in /docker/base (#6292)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 11:42:36 +01:00
dependabot[bot]andAnthony Stirling 4764f1931b build(deps): bump sha2 from 0.10.9 to 0.11.0 in /frontend/src-tauri (#6180)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 11:40:58 +01:00
dependabot[bot]andAnthony Stirling 7f00441aab build(deps): bump windows from 0.58.0 to 0.61.3 in /frontend/src-tauri (#6185)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 11:40:46 +01:00
dependabot[bot]andAnthony Stirling 561d07b8b1 build(deps): bump globals from 17.4.0 to 17.5.0 in /frontend (#6285)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 11:40:13 +01:00
dependabot[bot]andAnthony Stirling f39db01919 build(deps): bump step-security/harden-runner from 2.15.1 to 2.19.0 (#6228)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 11:39:59 +01:00
dependabot[bot]andAnthony Stirling 4ab7d3b3ae build(deps): bump the mui group across 1 directory with 2 updates (#6301)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 10:53:16 +01:00
dependabot[bot]andAnthony Stirling d8519a6517 build(deps): bump com.google.guava:guava from 33.5.0-jre to 33.6.0-jre (#6283)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 10:39:53 +01:00
dependabot[bot]andAnthony Stirling 34c9e9bdc5 build(deps): bump actions/setup-node from 6.3.0 to 6.4.0 (#6258)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 10:38:42 +01:00
dependabot[bot]andAnthony Stirling 4663a9a194 build(deps): bump org.springdoc:springdoc-openapi-starter-webmvc-ui from 3.0.2 to 3.0.3 in /app/common (#6286)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 10:35:19 +01:00
dependabot[bot]andAnthony Stirling f89f7d99ba build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#6297)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-05-05 10:34:45 +01:00
dependabot[bot] 84e30cd008 build(deps): bump actions/github-script from 7.1.0 to 9.0.0 (#6298)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 10:33:13 +01:00
dependabot[bot] 69236a89c8 build(deps): bump gradle/actions from 5.0.1 to 6.1.0 (#6294)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 10:32:53 +01:00
dependabot[bot] b66e12d15d build(deps): bump eclipse-temurin from a051234 to b27ca47 in /docker/embedded (#6293)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 10:32:29 +01:00
James BruntonandEthanHealy01 3fe8adc5cb Switch key areas to lazily import to improve Vite chunk size (#6278)
# Description of Changes
Vite currently warns that when it's bundling our code that the chunk
size is way too high because most of the imports are static so it can't
split them into smaller chunks. This PR changes a few key areas to use
lazy imports to try and make the chunks as small as possible with
minimal code changes.

Vite's warnings kick in at minified chunks being >500kB, and we've got a
little way to go still to reach that, but we can keep chipping away at
this and I'd rather get the biggest wins done now. I've also included
Lighthouse scores because there's been discussion about improving ours
recently. It's not the aim of this PR to improve it, but it's nice that
it makes it a little better.

## Current main chunks

Build split into 12 chunks. Largest chunk in build is:

```
[frontend:build] dist/assets/index-B6JiWDxZ.js               5,175.51 kB │ gzip: 1,495.85 kB
```

<img width="1442" height="775" alt="image"
src="https://github.com/user-attachments/assets/b0e8a3fa-4ef3-4ccd-8c1d-bfed2d99bd27"
/>

Lighthouse score:

<img width="423" height="146" alt="before"
src="https://github.com/user-attachments/assets/c62056e8-2e77-49a6-a1ae-f08ec8021fb3"
/>

## This PR's chunks

Build split into 176 chunks. Largest chunk in build is:

```
[frontend:build] dist/assets/index-qCgeCY4B.js                              2,878.54 kB │ gzip:   861.03 kB
```

<img width="1447" height="776" alt="image"
src="https://github.com/user-attachments/assets/8d0c3cf0-cc25-41c3-b114-4940d3e99349"
/>

Lighthouse score:

<img width="402" height="145" alt="after"
src="https://github.com/user-attachments/assets/99a26eb3-bd15-4b92-bf22-82b58b458f52"
/>

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-05-01 15:21:06 +00:00
James BruntonandEthanHealy01 51f5345151 Inform AI engine which endpoints are disabled on the backend (#6251)
# Description of Changes
Have the Java send a list of enabled endpoints to the AI engine so it
can intelligently respond to the user that the tool does exist but is
disabled on the server so it can't acutally run the operation, instead
of the current behaviour where it sends the API call back and then 503
errors because the execution fails when the URL is disabled.

<img width="380" height="208" alt="image"
src="https://github.com/user-attachments/assets/5842fb2e-2e55-45a5-8205-25515636daae"
/>

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-05-01 14:59:53 +00:00
James Brunton 5541dd666c Flesh out RAG system (#6197)
# Description of Changes
Flesh out the RAG system and connect it to the PDF Question Agent so it
can respond to questions about PDFs of an extremely large size.

I'd expect lots more work will need to be done to finish off the RAG
system to really be what we need, but this should be a reasonable start
which will let us connect it to tools and have the ingestion mostly
handled automatically. I'm leaving file deletion and proper file ID
management to be done in a future PR. We also need to consider whether
all tools should retrieve content exclusively via RAG, or whether it's
beneficial to have tools sometimes fetch the direct content and other
times fetch it from RAG.

A diagram of the expected interaction is as follows:

```mermaid
sequenceDiagram
    autonumber
    actor U as User
    participant FE as Frontend<br/>(ChatPanel)
    participant J as Java<br/>(AiWorkflowService)
    participant O as Engine:<br/>OrchestratorAgent
    participant QA as Engine:<br/>PdfQuestionAgent
    participant RAG as Engine:<br/>RagService + SqliteVecStore
    participant V as VoyageAI<br/>(embeddings)
    participant L as LLM<br/>(Claude / etc.)

    U->>FE: types "Summarise this PDF"<br/>(PDF already uploaded)
    FE->>J: POST /api/v1/ai/orchestrate/stream<br/>multipart: fileInputs[], userMessage
    Note over J: ByteHashFileIdStrategy<br/>id = sha256(bytes)[:16]
    J->>O: POST /api/v1/orchestrator<br/>{ files:[{id,name}], userMessage }

    O->>L: route via fast model
    L-->>O: delegate_pdf_question
    O->>QA: PdfQuestionRequest

    loop for each file
        QA->>RAG: has_collection(file.id)
        RAG-->>QA: false
    end
    QA-->>O: NeedIngestResponse(files_to_ingest)
    O-->>J: { outcome:"need_ingest", filesToIngest:[...] }

    Note over J: onNeedIngest
    loop per file
        J->>J: PDFBox: extract page text
        J->>O: POST /api/v1/rag/documents<br/>(long-running timeout)
        O->>RAG: chunk + stage documents
        O->>V: embed_documents (batches of 256)
        V-->>O: embeddings
        O->>RAG: add_documents
        O-->>J: { chunks_indexed: N }
    end

    Note over J: retry with resumeWith=pdf_question
    J->>O: POST /api/v1/orchestrator
    Note over O: fast-path to PdfQuestionAgent

    O->>QA: PdfQuestionRequest
    Note over QA: build RagCapability<br/>pinned to file IDs
    QA->>L: run(prompt) with search_knowledge tool

    loop up to max_searches
        L->>QA: search_knowledge(query)
        QA->>V: embed_query
        V-->>QA: query vector
        QA->>RAG: search(vector, collections=[file.id])
        RAG-->>QA: top-k chunks
        QA-->>L: formatted chunks
    end

    Note over QA: once budget spent,<br/>prepare() hides the tool
    L-->>QA: PdfQuestionAnswerResponse
    QA-->>O: answer
    O-->>J: { outcome:"answer", answer, evidence }
    J-->>FE: SSE "result"
    FE->>U: assistant bubble
```
2026-05-01 14:11:54 +01:00
Anthony Stirling 5605062153 Add Dependabot groups for frontend npm + cargo deps (#6287) 2026-05-01 11:37:07 +01:00
ConnorYohandJames Brunton 86774d556e Pdf comment agent (#6196)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-05-01 10:19:38 +01:00
LudovicandCopilot 2dc5276e8b Fix French translations for various terms (#6218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-30 23:09:57 +01:00
dependabot[bot] c58deb6b55 build(deps): bump tauri-apps/tauri-action from 0.5.25 to 0.6.2 (#6257)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 22:36:15 +01:00
Anthony Stirling 324a7cd25e decouple appimage (#6281) 2026-04-30 21:36:33 +01:00
Anthony Stirling d28ad89f64 gha cleanups (#6275)
# Description of Changes

Adds all pre commit PR checks under single file with job validator to
have a true "required" run step "all-checks-passed" check
Moves all GHAs into workflow helper function calls 

Note if: always() overrides the default skip-on-needs-failure, "Needs"
is added to ensure the validation runs at end of all other tasks

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-04-30 17:08:34 +01:00
Anthony Stirling d8c1ffa8ef fix stubs playwright (#6274) 2026-04-30 12:37:15 +01:00
stirlingbot[bot] b552feaf28 Update Backend 3rd Party Licenses (#6264)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-04-30 10:19:08 +01:00
Peter Dave Hello 890cf16f74 Adjust zh-TW translation ignore list (#6062) 2026-04-29 10:55:51 +01:00
dependabot[bot] 493757bf41 build(deps): bump com.google.guava:guava from 33.5.0-jre to 33.6.0-jre in /app/common (#6260)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 21:42:20 +01:00
dependabot[bot] 99b9b60f50 build(deps-dev): bump @stylistic/stylelint-plugin from 4.0.0 to 5.1.0 in /devTools (#6177)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 21:42:02 +01:00
dependabot[bot] 85aa722f37 build(deps): bump the npm_and_yarn group across 2 directories with 8 updates (#6266)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 21:41:45 +01:00
dependabot[bot] 1c42ce7087 build(deps): bump com.github.junrar:junrar from 7.5.8 to 7.5.10 in /app/common (#6261)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 18:45:49 +01:00
dependabot[bot] 5bd62eeb5c build(deps): bump github/codeql-action from 4.32.6 to 4.35.2 (#6256)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 18:45:38 +01:00
dependabot[bot] 87e5b84324 build(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0 (#6254)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 18:45:24 +01:00
dependabot[bot] 568c6da165 build(deps-dev): bump stylelint from 17.4.0 to 17.8.0 in /devTools (#6253)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 18:45:09 +01:00
Anthony Stirling c818db1042 ci(tauri): skip Linux GPG signing when secret is missing (#6263) 2026-04-28 18:43:52 +01:00
dependabot[bot]andAnthony Stirling cd3a59f077 build(deps): bump com.drewnoakes:metadata-extractor from 2.19.0 to 2.20.0 (#6184)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-04-28 17:40:22 +01:00
dependabot[bot]andAnthony Stirling 05e2ce45d4 build(deps): bump com.drewnoakes:metadata-extractor from 2.19.0 to 2.20.0 in /app/common (#6193)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-04-28 17:40:04 +01:00
stirlingbot[bot] 94cf25b319 Update Backend 3rd Party Licenses (#6243)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-04-28 17:38:00 +01:00
dependabot[bot] 4149276b01 build(deps): bump actions/dependency-review-action from 4.8.2 to 4.9.0 (#6230)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 17:37:43 +01:00
Reece BrowneandEthanHealy01 a26b15b1fe Lazy load open cv (#6236)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-04-28 17:36:34 +01:00
ConnorYoh 4e4918b91e fix(workflow): stop leaking peer share tokens from participant session API (#6241) 2026-04-28 17:36:20 +01:00
Anthony Stirling b966e771a4 add dependabot 7 day cooldown (#6242) 2026-04-28 17:36:09 +01:00
dependabot[bot] 376e49a947 build(deps): bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 (#6229)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 17:35:56 +01:00
dependabot[bot] 128697958b build(deps): bump org.springframework.boot from 4.0.5 to 4.0.6 (#6225)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 17:35:32 +01:00
dependabot[bot] 60c39c6f6e build(deps): bump actions/stale from 10.1.1 to 10.2.0 (#6227)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 17:35:17 +01:00
dependabot[bot] c3c5b08ce6 build(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.1 (#6226)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 17:35:04 +01:00
James Brunton 49f24fa7a9 Have Task choose free ports for dev servers (#6145) 2026-04-28 17:26:04 +01:00
Anthony Stirling 12a00fe9c5 bug fixes for some desktop tools (#6246) 2026-04-28 17:24:56 +01:00
James Brunton 3c09b7f736 Guard PR commenting on pull requests (#6250)
# Description of Changes
Attempt 2 at #6249
2026-04-28 15:17:42 +01:00
James Brunton 5116e6ec43 Set CI to run the same in the merge queue as it does in CI (#6249) 2026-04-28 14:13:55 +01:00
Anthony Stirling 8daee7303d playwright (#6025) 2026-04-27 11:35:50 +01:00
Philip H. 688c3e1e58 Update: AUR PKGBUILDs desktop and server (#6220) 2026-04-25 22:20:05 +01:00
albanobattistella df5d0a9aa8 Fix translation (#6224) 2026-04-25 22:19:53 +01:00
James Brunton 1e3da14081 Change frontend .env files to be committed and have .env.*.local overrides (#6207) 2026-04-25 13:09:59 +01:00
Anthony Stirling 276bbd635c release notes fix for engine (#6219) 2026-04-25 13:09:48 +01:00
Ludy 8cd6377131 build: Add Sync app version to AUR PKGBUILD files (#6222) 2026-04-25 13:09:25 +01:00
Ludy 9890d6a7de fix: skip translation key extraction for files without i18n usage (#6221) 2026-04-25 13:08:24 +01:00
stirlingbot[bot]andLudy 5cfb77314f Update Backend 3rd Party Licenses (#5932)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-04-24 21:34:58 +01:00
stirlingbot[bot] 6ebd85defb 🌐 Sync Translations + Update README Progress Table (#5716)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-04-24 21:30:37 +01:00
dependabot[bot]andAnthony Stirling c16dc1782d build(deps): bump dorny/test-reporter from 2.5.0 to 3.0.0 (#5973)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-04-24 21:24:21 +01:00
dependabot[bot] 87464aca53 build(deps): bump com.github.jk1.dependency-license-report from 3.1.1 to 3.1.2 (#6188)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 21:23:30 +01:00
dependabot[bot] 2ade011106 build(deps): bump com.bucket4j:bucket4j_jdk17-core from 8.17.0 to 8.18.0 (#6189)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 21:21:02 +01:00
dependabot[bot] ac0ef7056e build(deps): bump astral-sh/setup-uv from 4.2.0 to 8.1.0 (#6181)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:26:23 +01:00
dependabot[bot] 7cb0f928bc build(deps): bump docker/login-action from 3.7.0 to 4.1.0 (#6072)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:26:02 +01:00
dependabot[bot] 42978b9ad6 build(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0 (#6021)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:25:41 +01:00
dependabot[bot] c5cdde0b58 build(deps): bump docker/build-push-action from 6.18.0 to 7.1.0 (#6192)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:25:21 +01:00
Anthony Stirling 9f6d7750f2 translation additions (#6208) 2026-04-24 14:02:13 +01:00
Anthony Stirling 7e185bdf8f fix AUR, publish desktop not server for now (#6204)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-04-24 10:45:58 +01:00
EthanHealy01 7c42d8018a make clicking on comments open the comments sidebar and more (#6174)
make clicking on comments open the comments sidebar and add a button to
create comments into the empty state of the comments sidebar
<img width="2056" height="1081" alt="Screenshot 2026-04-23 at 12 39
50 PM"
src="https://github.com/user-attachments/assets/6f15484d-d04f-4900-92c6-b2cc397d6d08"
/>

<img width="627" height="396" alt="Screenshot 2026-04-23 at 12 40 06 PM"
src="https://github.com/user-attachments/assets/509e5526-0082-4fc6-a98f-829bb4c1baf2"
/>
2026-04-23 17:22:47 +01:00
Anthony Stirling 177c776658 Migrate stream to resource for stability (#6160) 2026-04-23 15:56:31 +01:00
Anthony StirlingandEthanHealy01 c294e9b2cb fix file sharing bug (#6161)
# Description of Changes

Fixes share-link navigation for SSO users. Reported on v2.9.2 with
`SSOAutoLogin: true`: clicking a `/share/<token>` link in an email
redirected the user to the home page after SSO instead of the shared
file.

## Root cause

Three compounding issues had to be fixed together; the first was the
initial symptom but the other two only surfaced during live
verification.

1. **Spring Security blocked `/share/<token>` for unauthenticated
users.** The route wasn't in `RequestUriUtils.isPublicAuthEndpoint`, so
the server 302'd straight to `/login` before React could load
`ShareLinkPage`. The share URL was lost because `NullRequestCache` is
configured and never persisted the original destination.

2. **`httpErrorHandler` full-page-redirected to `/login?from=<path>` on
any unhandled 401** (fired by `LicenseContext`, `AppConfig`, etc. during
normal ShareLinkPage mount). That *did* preserve the return path — but
**Spring Security strips query strings from `/login`** (302 to bare
`/login`), so `?from=` never reached React. Confirmed via `curl -i
http://localhost:8080/login?from=xyz` → `Location: /login`.

3. **`AuthCallback.tsx` unconditionally `navigate("/")`** after the
SAML/OAuth round-trip, discarding any intended destination.

## Fix

**Backend** — make `/share/<token>` a public SPA bootstrap, data APIs
stay protected:
- `RequestUriUtils.isPublicAuthEndpoint` — permits `^/share/[^/]+/?$`
(tight regex, single token segment only; `/share/<token>/anything` stays
protected).
- `ReactRoutingController` — dedicated `@GetMapping("/share/{token}")`
mirroring `/auth/callback`.
- `/api/v1/storage/share-links/**` remains behind Spring Security with
its existing `canAccessShareLink` check.

**Frontend** — persist the return path across full-page redirects via
`sessionStorage` (same-origin, survives the SSO round-trip):
- `httpErrorHandler.ts` — stashes current pathname to
`stirling_post_login_path` before the 401 → `/login` redirect.
- `springAuthClient.ts` — new `isSafePostLoginRedirect` /
`setPostLoginRedirectPath` / `consumePostLoginRedirectPath` helpers
(rejects protocol-relative URLs and auth-plumbing paths to guard against
open-redirect abuse).
- `Login.tsx` — on explicit user sign-in, read path from
`location.state` or `?from=` query and stash it; don't clobber an
already-stashed value.
- `AuthCallback.tsx` — consume the stashed path (single-use) and
`navigate(target)` instead of always `/`.


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-04-23 14:52:25 +01:00
James Brunton 3e94157137 Add document context for edit agent (#6152)
# Description of Changes
Adds the ability for the Edit agent to request the content of the
document before it decides which parameters it needs. This makes it able
to process requests like `Split the document after the page containing
the "My Section" section`, allowing for document context-based requests
for all[^1] tools.

I had to make a few changes elsewhere to make this work, including:
- Moving the requesting of content out of the Question Agent and into a
common location
- Added specific API docs for the Split param because the generic ones
were not specific enough for the AI to be able to reliably perform the
correct operation
- Fixed an issue in the tool models generator which caused the Redact
params to only be half-generated (causing Pydantic to crash when the AI
tried to run Redact)
- Added missing logging to a bunch of tools and hooked it up properly so
it'll print to stderr
- Made the limits for the max pages/chars to extract from PDFs
configurable via env var

[^1]: Many of the tools can't actually do anything useful with the
context at this stage, but will just need the tool API to be extended
with new features like page-specific operations to be automatically able
to do smart operations without needing to change the Edit agent itself.
2026-04-23 13:19:27 +00:00
Ludy e087b54cf0 build(docker): pin base container images to immutable digests (#6173) 2026-04-23 13:31:21 +01:00
Ludy 90efb844d9 chore(pre-commit): bump linting and formatting tool versions and ignore Windows DLL artifact (#6165) 2026-04-23 13:30:35 +01:00
Ludy 27ccf6afdd chore(ci): consolidate Dependabot directories and pin GitHub Actions in workflow automation (#6172) 2026-04-23 13:30:10 +01:00
Ludy c5d07e23bf deps(ci): enforce binary-only Python installs and refresh pinned dependency locks (#6157) 2026-04-23 13:28:44 +01:00
Anthony Stirling fed4fd2efb package manager fixes (#6130) 2026-04-23 11:55:36 +01:00
Anthony Stirling 611d7577a3 Version bump to 2.10.0 (#6168) 2026-04-23 11:54:47 +01:00
Anthony Stirling 1d5b47fa9b fix edge translation bug (#6158) 2026-04-22 16:31:43 +01:00
Anthony Stirling d71a2c3d81 FixThumbnailRegeneration (#6134) 2026-04-22 14:33:38 +01:00
Ludy 97e2dc2c68 chore(frontend): replace platform-specific update:minor script with cross-platform Node.js implementation (#6155) 2026-04-22 11:50:01 +01:00
James Brunton 975f135217 Move engine/AGENTS.md into root AGENTS.md because Claude doesn't bother to read it (#6151)
# Description of Changes
Move `engine/AGENTS.md` into root `AGENTS.md` because Claude doesn't
bother to read it half the time.
2026-04-22 11:32:03 +02:00
James Brunton 3b2afe0deb Change engine/.env to be committed and have .env.local override (#6150)
# Description of Changes
We keep adding stuff to `engine/config/.env.example` and have to
manually update `.env` because of it, which is really clunky, especially
when working on multiple worktrees at once. This PR changes it so that
we just have a committed `.env` file and have an `.env.local` override
to put the actual private keys into, which should make it a bit easier
to manage.

> [!warning]
>
> After this goes in, be very careful for a little while not to
accidentally commit any keys that you've got inside your `.env` file!
2026-04-21 16:18:25 +01:00
James Brunton 2a856fbc19 Allow chat history to be sent to AI engine (#6128)
# Description of Changes
Add an extra parameter to every agent to receive the conversation
history in addition to the current message. This will make it possible
to answer followup questions from the AI without needing to give full
context in your message.
2026-04-21 15:03:10 +00:00
Anthony Stirling f779085d75 setup RAG (#6146) 2026-04-21 12:42:33 +01:00
66a75b1f28 Add pixel comparison mode to Compare tool (#6109)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-04-20 19:31:07 +01:00
EthanHealy01andJames Brunton 089e448cf4 allow deploypr:prototypes comment to spin up the prototypes build (#6144)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-04-20 18:58:33 +01:00
e5767ed58b Change AI engine to execute tools in Java instead of on frontend (#6116)
# Description of Changes
Redesign AI engine so that it autogenerates the `tool_models.py` file
from the OpenAPI spec so the Python has access to the Java API
parameters and the full list of Java tools that it can run. CI ensures
that whenever someone modifies a tool endpoint that the AI enigne tool
models get updated as well (the dev gets told to run `task
engine:tool-models`).

There's loads of advantages to having the Java be the one that actually
executes the tools, rather than the frontend as it was previously set up
to theoretically use:
- The AI gets much better descriptions of the params from the API docs
- It'll be usable headless in the future so a Java daemon could run to
execute ops on files in a folder without the need for the UI to run
- The Java already has all the logic it needs to execute the tools 
- We don't need to parse the TypeScript to find the API (which is hard
because the TS wasn't designed to be computer-read to extract the API)

I've also hooked up the prototype frontend to ensure it's working
properly, and have built it in a way that all the tool names can be
translated properly, which was always an issue with previous prototypes
of this.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-04-20 15:57:11 +01:00
James Brunton cc9650e7a3 Fix any type usage in desktop/ (#6033)
# Description of Changes
Follow on from #5949, expanding any type usage ban to the `desktop/`
folder

Also gets rid of a bunch of really verbose desktop logging that I don't
think we really need anymore (or ever needed tbh, most of it doesn't
make sense) because it was using a bunch of `any` typing and wasn't
worth fixing.
2026-04-20 12:42:38 +00:00
Anthony Stirling 308da01d96 Fix form-fill hang when flattening with empty values (#6143) 2026-04-20 13:12:25 +01:00
Anthony Stirling 4e7f435016 Swap thumbnail rendering from PDF.js to PDFium (#6135) 2026-04-20 12:53:56 +01:00
Anthony Stirling b4b196556d Fix compare tool file selection and other files improvements (#6133) 2026-04-20 12:53:37 +01:00
Anthony Stirling 30aff3236f fix tests caused by streaming changes (#6137) 2026-04-19 18:35:51 +01:00
Anthony Stirlingandaikido-pr-checks[bot] ab19cf113b AUR publishing workflow (#6132)
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
2026-04-17 23:12:46 +01:00
Anthony Stirling 3eefabd44b enable AppImage and rpm distrobutions (#6127) 2026-04-17 22:19:16 +01:00
Anthony Stirling 79f4748ea6 package manager GHA init to allow workflow dispatch testing (#6129) 2026-04-17 15:56:04 +01:00
EthanHealy01 bad92a9eae Chore/remove usage of mantine color scheme (#6108)
Remove instances of `colorScheme === "dark" ?` in the app and rely on
the theme.css' light and dark variables instead.
2026-04-17 14:29:37 +01:00
Anthony Stirling a7a5bb2057 Tauri sign fixes for security alerts (#6122) 2026-04-17 11:05:29 +01:00
James Brunton 8ab060a4be Prettier 2: Electric Boogaloo (#6113)
# Description of Changes
When I added Prettier formatting in #6052, my aim was to use just the
default settings in Prettier. Turns out, Prettier looks _really hard_
for any config files if it's not explicitly given one, which means that
if a developer has some sort of Prettier config file lying around on
their system, Prettier might find it and use it. Also, Prettier changes
its defaults based on stuff in `.editorconfig` without any good way of
disabling that behaviour explicitly in its config file.

To solve both of these issues, I've introduced a `.prettierrc` file
which sets Prettier's defaults explicitly, and then reformatted all our
code _again_ in Prettier's actual default settings. This should achieve
the aim of #6052 and remove the possibility for it breaking on different
dev computers.
2026-04-17 09:50:16 +00:00
de8c483054 Feat/math validation agent (#6012)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-04-17 10:36:45 +01:00
James Brunton 688f7f2013 Add streaming to Engine orchestrator (#6094)
# Description of Changes
Adds a streaming endpoint to the Java AI orchestrator
(`/api/v1/ai/orchestrate/stream` in addition to the existing
`/api/v1/ai/orchestrate`). This allows the caller to get updates of what
stage of orchestration is being run at the time so UIs can give the user
feedback.

Also contains some dubious Gradle changes to suppress errors coming from
Spotless, when it crashes in Google stuff. I'm not sure if that's
appropriate to add, feel free to ask for changes in review.
2026-04-17 10:01:08 +01:00
Orel Yosupov 97ca85d878 Fix terms and privacy URLs links in Footer component (#6124)
Fix the issue #6104
2026-04-16 15:55:53 +01:00
Anthony Stirling 60c036e980 thumbnail preview fixes windows (#6074) 2026-04-15 23:25:38 +01:00
Anthony Stirling cc5a0b8def Cleanup work + stream endpoints to reduce memory usage (#6106) 2026-04-15 15:34:17 +01:00
ConnorYohandJames Brunton 702f4e5c2c Add Taskfile for unified dev workflow across all components (#6080)
## Add Taskfile for unified dev workflow

### Summary
- Introduces [Taskfile](https://taskfile.dev/) as the single CLI entry
point for all development workflows across backend, frontend, engine,
Docker, and desktop
- ~80 tasks organized into 6 namespaces: `backend:`, `frontend:`,
`engine:`, `docker:`, `desktop:`, plus root-level composites
- All CI workflows migrated to use Task
- Deletes `engine/Makefile` and `scripts/build-tauri-jlink.{sh,bat}` —
replaced by Task equivalents
- Removes redundant npm scripts (`dev`, `build`, `prep`, `lint`, `test`,
`typecheck:all`) from `package.json`
- Smart dependency caching: `sources`/`status`/`generates`
fingerprinting, CI-aware `npm ci` vs `npm install`, `run: once` for
parallel dep deduplication

### What this does NOT do
- Does not replace Gradle, npm, or Docker — Taskfile is a thin
orchestration wrapper
- Does not change application code or behavior

### Install
```
npm install -g @go-task/cli    # or: brew install go-task, winget install Task.Task
```

### Quick start
```
task --list       # discover all tasks
task install      # install all deps
task dev          # start backend + frontend
task dev:all      # also start AI engine
task test         # run all tests
task check        # quick quality gate (local dev)
task check:all    # full CI quality gate
```

### Test plan
- [ ] Install `task` CLI and run `task --list` — verify all tasks
display
- [ ] Run `task install` — verify frontend + engine deps install
- [ ] Run `task dev` — verify backend + frontend start, Ctrl+C exits
cleanly
- [ ] Run `task frontend:check` — verify typecheck + lint + test pass
- [ ] Run `task desktop:dev` — verify jlink builds are cached on second
run
- [ ] Verify CI passes on all workflows

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-04-15 14:16:57 +00:00
James Brunton 4cf797ab75 Fix Java formatting (#6114)
# Description of Changes
#6069 introduced formatting issues in the Java, this PR fixes them.
2026-04-15 15:12:04 +01:00
Anthony Stirling 07b7c991f0 desktop mobile QR fixes (#6069) 2026-04-15 13:21:45 +01:00
James BruntonandConnorYoh 2bf5f0b18e Add tracking system to support optional PostHog tracking in AI engine (#6040)
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2026-04-14 18:45:47 +01:00
plind 4ada46ca56 Fix encrypted PDF unlock modal missing on IndexedDB restore and large files (#6099) 2026-04-14 00:38:42 +01:00
Reece Browne 76aa5c7e2f Fix encrypted pdf handling (#6088)
Fix and improve encrypted pdf handling
2026-04-13 13:20:43 +01:00
Reece BrowneandClaude Opus 4.6 d53beb9bce Remove duplicate isPanning state (#6086)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:49:09 +01:00
unlair 3d17f0409f Fix healthcheck in Docker files when SYSTEM_ROOTURIPATH is specified (#5954) 2026-04-12 22:44:04 +01:00
James Brunton a3e45bc182 Add frontend autoformatting and set CI to require formatted code for all languages (#6052)
# Description of Changes
Changes the strategy for autoformatting to reject PRs if they are not
formatted correctly instead of allowing them to merge and then spawning
a new PR to fix the formatting. The old strategy just caused more work
for us because we'd have to manually approve the followup PR and get it
merged, which required 2 reviewers so in practice it rarely got done and
just meant everyone's PRs ended up containing reformatting for unrelated
files, which makes code review unnecessarily difficult. If the PR's code
is not formatted correctly after this PR, a comment will be added
automatically to tell the author how to run the formatter script to fix
their code so it can go in.

This also enables autoformatting for the frontend code, using Prettier.
I've enabled it for pretty much everything in the frontend folder, other
than 3rd party files and files it doesn't make sense for. I also
excluded Markdown because it sounds likely to be more annoying to have
to autoformat the Markdown in the frontend folder but nowhere else. Open
to changing this though if people disagree.

> [!note]
> 
> Advice to reviewers: The first commit contains all of the actual logic
I've introduced (CI changes, Prettier config, etc.)
> The second commit is just the reformatting of the entire frontend
folder.
> The first commit needs proper review, the second one just give it a
spot-check that it's doing what you'd expect.
2026-04-10 17:41:19 +01:00
aikido-autofix[bot] 33b2b5827a [Aikido] Fix 16 security issues in fastmcp, aiohttp, cryptography and 1 more (#6091)
Upgrade fastmcp, aiohttp, cryptography, and anthropic to fix critical
SSRF/path traversal, header injection, OAuth confused deputy, and DoS
vulnerabilities.

<details>
<summary> 16 CVEs resolved by this upgrade, including 2 critical 🚨
CVEs</summary>

<br>


This PR will resolve the following CVEs:
| Issue |
Severity&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
Description |
| --- | --- | --- |
|
<pre>[CVE-2026-32871](https://app.aikido.dev/issues/25944204/detail?groupId=70007#CVE-2026-32871)</pre>
| <pre>🚨 CRITICAL</pre> | [fastmcp] Path traversal vulnerability in URL
construction allows attackers to bypass API prefix restrictions and
access arbitrary backend endpoints using unencoded path parameters,
enabling authenticated SSRF attacks. |
|
<pre>[CVE-2026-27124](https://app.aikido.dev/issues/25944204/detail?groupId=70007#CVE-2026-27124)</pre>
| <pre>HIGH</pre> | [fastmcp] OAuthProxy fails to validate user consent
when receiving authorization codes from GitHub, allowing attackers to
exploit GitHub's consent-skipping behavior to gain unauthorized access
to FastMCP servers through a Confused Deputy attack. |
|
<pre>[CVE-2025-64340](https://app.aikido.dev/issues/25944204/detail?groupId=70007#CVE-2025-64340)</pre>
| <pre>MEDIUM</pre> | [fastmcp] Server names with shell metacharacters
can cause command injection on Windows when passed to install commands,
allowing arbitrary code execution through cmd.exe interpretation of .cmd
wrapper files. |
|
<pre>[CVE-2026-34520](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34520)</pre>
| <pre>🚨 CRITICAL</pre> | [aiohttp] is an asynchronous HTTP
client/server framework for asyncio and Python. Prior to version 3.13.4,
the C parser (the default for most installs) accepted null bytes and
control characters in response headers. This issue has been patched in
version 3.13.4. |
|
<pre>[CVE-2026-34516](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34516)</pre>
| <pre>HIGH</pre> | [aiohttp] A response with an excessive number of
multipart headers can consume more memory than intended, leading to a
denial of service (DoS) vulnerability through resource exhaustion. |
|
<pre>[CVE-2026-22815](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-22815)</pre>
| <pre>MEDIUM</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, insufficient
restrictions in header/trailer handling could cause uncapped memory
usage. This issue has been patched in version 3.13.4. |
|
<pre>[CVE-2026-34515](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34515)</pre>
| <pre>MEDIUM</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, on Windows
the static resource handler may expose information about a NTLMv2 remote
path. This issue has been patched in version 3.13.4. |
|
<pre>[CVE-2026-34525](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34525)</pre>
| <pre>MEDIUM</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, multiple Host
headers were allowed in aiohttp. This issue has been patched in version
3.13.4. |
|
<pre>[CVE-2026-34513](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34513)</pre>
| <pre>LOW</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, an unbounded
DNS cache could result in excessive memory usage possibly resulting in a
DoS situation. This issue has been patched in version 3.13.4. |
|
<pre>[CVE-2026-34514](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34514)</pre>
| <pre>LOW</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, an attacker
who controls the content_type parameter in aiohttp could use this to
inject extra headers or similar exploits. This issue has been patched in
version 3.13.4. |
|
<pre>[CVE-2026-34517](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34517)</pre>
| <pre>LOW</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, for some
multipart form fields, aiohttp read the entire field into memory before
checking client_max_size. This issue has been patched in version 3.13.4.
|
|
<pre>[CVE-2026-34518](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34518)</pre>
| <pre>LOW</pre> | [aiohttp] When following redirects to a different
origin, the framework fails to drop the Cookie and Proxy-Authorization
headers alongside the Authorization header, potentially leaking
sensitive authentication credentials to untrusted domains. |
|
<pre>[CVE-2026-34519](https://app.aikido.dev/issues/25944198/detail?groupId=70007#CVE-2026-34519)</pre>
| <pre>LOW</pre> | [aiohttp] is an asynchronous HTTP client/server
framework for asyncio and Python. Prior to version 3.13.4, an attacker
who controls the reason parameter when creating a Response may be able
to inject extra headers or similar exploits. This issue has been patched
in version 3.13.4. |
|
<pre>[CVE-2026-39892](https://app.aikido.dev/issues/25637201/detail?groupId=70007#CVE-2026-39892)</pre>
| <pre>MEDIUM</pre> | [cryptography] Non-contiguous buffers passed to
cryptographic APIs can cause buffer overflows, potentially leading to
memory corruption and arbitrary code execution. |
|
<pre>[CVE-2026-34452](https://app.aikido.dev/issues/25944200/detail?groupId=70007#CVE-2026-34452)</pre>
| <pre>MEDIUM</pre> | [anthropic] A time-of-check-time-of-use (TOCTOU)
vulnerability in the async filesystem memory tool allows local attackers
to escape the sandbox directory via symlink manipulation, enabling
arbitrary file read/write operations outside the intended memory
directory. |
|
<pre>[CVE-2026-34450](https://app.aikido.dev/issues/25944200/detail?groupId=70007#CVE-2026-34450)</pre>
| <pre>MEDIUM</pre> | [anthropic] The local filesystem memory tool
created world-readable and potentially world-writable files, allowing
local attackers to read persisted agent state or modify memory files to
influence model behavior. |


</details>

Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
2026-04-10 08:54:53 +00:00
aikido-autofix[bot] 60cc749e6a [Aikido] Fix critical issue in axios via minor version upgrade from 1.13.6 to 1.15.0 in frontend (#6092)
Upgrade axios to fix critical proxy bypass and SSRF vulnerabilities in
hostname normalization that could allow attackers to reach protected
internal services.

 There are no breaking changes

<details>
<summary> 1 CVE resolved by this upgrade, including 1 critical 🚨
CVE</summary>

<br>


This PR will resolve the following CVEs:
| Issue |
Severity&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |
Description |
| --- | --- | --- |
|
<pre>[CVE-2025-62718](https://app.aikido.dev/issues/26490690/detail?groupId=70007#CVE-2025-62718)</pre>
| <pre>🚨 CRITICAL</pre> | [axios] Axios fails to properly normalize
hostnames when checking NO_PROXY rules, allowing requests to loopback
addresses (localhost., [::1]) to bypass proxy protections and reach
internal services. This enables proxy bypass and SSRF attacks against
protected loopback or internal endpoints. |


</details>

Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
2026-04-10 09:50:05 +01:00
EthanHealy01 11b26755a4 use clean 3 card design for landing page (#6084)
<img width="2056" height="1080" alt="Screenshot 2026-04-08 at 1 26
58 PM"
src="https://github.com/user-attachments/assets/e834988b-c3ab-4633-bf15-9fe0457d0029"
/>

<img width="2056" height="1080" alt="Screenshot 2026-04-08 at 1 27
12 PM"
src="https://github.com/user-attachments/assets/adfebd95-ca59-4de0-9336-b1e2dc1dc5fe"
/>
2026-04-09 12:38:46 +00:00
Vibe Stack a5b259b453 feat(settings): add default startup view and reader zoom preferences (#6073)
## Description of Changes

Adds two new user preferences to the General settings panel, addressing
#5908.

**Default view on launch** - a segmented control (Tools / Reader /
Automate) that controls which left-column tab is active when the app
starts. Previously the app always opened on the Tools tab with no way to
change this. Users who spend most of their time reading PDFs had to
manually switch to the Reader tab on every launch.

**Default reader zoom** - a dropdown (Auto / Fit width / Fit page /
50%–200%) that sets the initial zoom level whenever a PDF is opened in
the reader. Previously the app always applied an automatic
fit-to-viewport calculation.

Both settings are non-breaking. The defaults (`Tools` and `Auto`)
reproduce the existing behaviour exactly, so existing users see no
difference until they change a preference.

### What changed
- `preferencesService.ts` - added `StartupView` and `ViewerZoomSetting`
types plus the two new fields to `UserPreferences` with safe defaults
- `ToolWorkflowContext.tsx` - one-time startup effect that navigates to
the preferred tab on first render (mirrors the existing
`defaultToolPanelMode` sync pattern)
- `ZoomAPIBridge.tsx` - respects the zoom preference before falling back
to auto-zoom logic when a document loads
- `GeneralSection.tsx` - two new controls added below "Default tool
picker mode"; the Select uses `comboboxProps={{ withinPortal: true }}`
so the dropdown renders above the settings modal
- `en-GB/translation.toml` - new keys for labels, descriptions, and
option values

Closes #5908 

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [x] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)
<img width="1023" height="747" alt="Screenshot 2026-04-05 185718"
src="https://github.com/user-attachments/assets/6a8bc35a-d813-4ab8-b303-55bdce747a6a"
/>
<img width="1026" height="755" alt="Screenshot 2026-04-05 185620"
src="https://github.com/user-attachments/assets/d2c45134-ed32-4332-a193-1a96837ba2a3"
/>


### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-04-09 10:30:19 +00:00
James Brunton cc1604a802 Add prototypes folder to test new functionality in (#6081)
# Description of Changes
Add prototypes folder to test new functionality in. This build of the
app is spawnable with `npm run dev:prototypes`.

Currently just contains a very developer-y chat interface to help us
develop & explore the AI backend before we make the frontend for it for
real.
2026-04-09 08:21:07 +00:00
James Brunton b130242688 Add Java orchestrator to connect to the AI engine (#6003)
# Description of Changes
Add Java orchestration layer which can connect and go back and forth
with the AI engine to get results for the user. It's expected that the
AI engine will not be publicly available and this Java layer will always
be in front of it, to manage sessions and auth etc.
2026-04-09 08:04:38 +00:00
fbae819d7c Fix/desktop open with tool access (#6056)
## Description

Fixes #6029 - Additional selection in windows client no longer necessary

## Problem
When opening PDF files in the Windows desktop client using "Open with",
the file displays properly but users had to manually select it again in
the workbench before any PDF tools (merge, compress, crop, compare,
etc.) become functional.

## Root Cause
Files opened via "Open with" were added to FileContext but **not
selected** (missing `selectFiles: true`). Without selection, the file
wasn't marked as active, preventing tool access.

Additionally, `AppInitializer` was placed outside
`ToolWorkflowProvider`, causing a context error.

## Solution

### Changes:
1. **frontend/src/desktop/hooks/useAppInitialization.ts**
   - Added `{ selectFiles: true }` when calling `addFiles()`
   - Files now immediately marked as active in FileContext

2. **frontend/src/core/components/AppProviders.tsx**
   - Moved `AppInitializer` inside `ToolWorkflowProvider`
   - Ensures context availability for initialization

## Testing
- Open PDF via "Open with" on Windows
- File now immediately usable with all tools
- No manual reselection needed

## Screenshot
<img width="1920" height="1080" alt="Screenshot (3)"
src="https://github.com/user-attachments/assets/9ceacadf-eb12-42a6-86f9-bca6188bfbb9"
/>

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2026-04-08 20:39:20 +05:30
Anthony Stirlinganda ebab5a4456 pipeline fixes (#6068)
Co-authored-by: a <a>
2026-04-04 10:19:38 +01:00
Reece Browne 436c8cbed2 Line seperator fix for redaction drift (#6064) 2026-04-03 17:47:48 +01:00
Anthony Stirling 81dc90cd6d possible fix permission issues and fix thread timing issues (#6061)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-04-03 16:49:16 +01:00
EthanHealy01andReece Browne 917edc43b3 Add specific View Scope For Selected Files (#6050)
## Fix 1 — Viewer bug (8 tools)

8 tools called `useFileSelection()` directly instead of routing through
`useBaseTool`. In the viewer, this meant they operated on **all selected
files**
instead of only the one being viewed. For example: 10 files loaded,
viewing
file 3, running Add Stamp — all 10 files got stamped.

**Root cause:** These tools had no view-scope awareness.
`useFileSelection()`
returns the raw workbench selection with no knowledge of which file is
active in
the viewer.

**Fix:** A new hook `useViewScopedFiles` was introduced:

```ts
// Viewer → only the active file
// Everywhere else → all loaded files
const selectedFiles = useViewScopedFiles();
```

The 8 tools were updated to call this instead of `useFileSelection()`.

**Tools fixed:** Add Stamp, Add Watermark, Add Password, Add Page
Numbers,
Add Attachments, Reorganize Pages, OCR, Convert

---

## Fix 2 — Page selector / active files context (all tools)

`useBaseTool` returned `selectedFiles` (checked files only) in
non-viewer
contexts. In the page selector this is typically empty or stale — not
the full
set of loaded files that tools should operate on.

**Fix:** `useBaseTool` was updated to use `useViewScopedFiles`, which
returns
all loaded files in non-viewer contexts. This affected every tool via
`useBaseTool`.

---

## Workarounds for Compare & Merge

Two tools intentionally need all loaded files regardless of view, so
they use
`ignoreViewerScope: true` in `useBaseTool`.

**Compare** — needs exactly 2 files for its Original/Edited slots.
Scoping to
one file would break the comparison entirely. `ignoreViewerScope: true`
is set
and `disableScopeHints: true` hides the "(this file)" button label hint.
The
slot auto-mapping logic was also improved alongside this fix.

**Merge** — needs 2+ files; merging a single file is meaningless. Rather
than
leaving the button silently disabled, Merge now:
- Auto-redirects to the active files view on first open from the viewer
- If the user navigates back to the viewer, shows a disabled button with
a hint
  and a "Go to active files view" shortcut button

---

## How to Test

---

## Fix 1 — 8 tools (viewer scoping)

### Test steps (same for each)
1. Load 3 PDFs into workbench
2. Open viewer, navigate to file 2
3. Open the tool, configure settings, run
4.  Only file 2 is in the results
5.  Button label shows **"[Action] (this file)"**
6.  A note below the button reads **"Only applying to: [filename]"**

| Tool | What to configure |
|---|---|
| **Add Stamp** | Enter any text stamp or upload an image stamp |
| **Add Watermark** | Select text watermark, enter any text |
| **Add Page Numbers** | Leave defaults |
| **Add Password** | Enter any owner + user password |
| **Add Attachments** | Attach any small file |
| **Reorganize Pages** | Enter a page range e.g. `1,2` |
| **OCR** | Leave default language |
| **Convert** | Convert PDF → any format |

---

## Fix 2 — All tools (page selector context)

### Test steps
1. Load 3 PDFs into workbench
2. Open the page selector view 
3. Open any tool from the sidebar, run it
4.  All 3 files are processed (not zero or a stale subset)

---

## Compare (intentionally ignores view scope)

**A — Auto-fill with exactly 2 files**
1. Load exactly 2 PDFs
2. Open Compare from either the viewer or active files view
3.  Both slots are filled automatically (Original + Edited)
4.  No scope hint appears on the button

**B — Manual selection with 3+ files**
1. Load 3+ PDFs
2. Open Compare
3.  The first 2 files fill the slots
4.  A 3rd file does not add a 3rd slot (capped at 2)

**C — File removed mid-session**
1. Load 2 PDFs, let Compare auto-fill both slots
2. Remove one file from the workbench
3.  The corresponding slot clears; the other slot is unchanged

**D — Viewer mode**
1. Load 2 PDFs, open viewer
2. Open Compare from the viewer sidebar
3.  Both files are still available for slot selection (not scoped to
current file)

---

## Merge (intentionally ignores view scope, disabled in viewer)

**A — Auto-redirect on first open from viewer**
1. Load 2+ PDFs, open the viewer
2. Open Merge from the viewer sidebar
3.  Immediately redirected to the active files view

**B — Viewer mode disabled state (after navigating back)**
1. From the active files view, open Merge, then navigate back to the
viewer
2.  Execute button is **disabled** with tooltip "Switch to the file
editor to select multiple files"
3.  A note appears: *"Merge needs 2 or more files. Head to the file
editor to select them."*
4.  A **"Go to active files view"** button is shown; clicking it
navigates back

**C — Active files view works normally**
1. Load 3 PDFs, open Merge from the active files view
2.  All 3 files appear in the merge list
3.  Button shows **"Merge (3 files)"**
4. Run the merge
5.  Output is a single PDF containing all 3 files

---

## Button label behaviour (all tools)

| Context | Expected button text |
|---|---|
| Viewer, 1 file loaded | `[Action]` (no suffix) |
| Viewer, 2+ files loaded | `[Action] (this file)` |
| Active files view, 1 file loaded | `[Action]` (no suffix) |
| Active files view, 2+ files loaded | `[Action] (N files)` |
| Merge in viewer | disabled — no suffix |
| Compare | never shows scope suffix (`disableScopeHints: true`) |

---------

Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2026-04-03 16:04:38 +01:00
Anthony Stirling 3c48740c5e dep updates (#6058) 2026-04-03 13:24:41 +01:00
stirlingbot[bot]andAnthony Stirling 2c940569d1 🤖 format everything with pre-commit by stirlingbot (#6000)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-04-02 18:47:52 +01:00
Thomas BERNARD 7bbb04b594 translate more messages to fr-FR (#6042) 2026-04-02 17:55:17 +01:00
Dexterity fca40e5544 Fix image stamp cropping and align preview with PDF output for add-stamp (#6013) 2026-04-02 17:54:13 +01:00
Anthony Stirling c9a70f3754 removeffmpeg (#6053) 2026-04-02 17:40:02 +01:00
Reece Browne 0adcbeedf1 Fix/redact bug (#6048) 2026-04-02 17:39:45 +01:00
Anthony Stirlinganda de9625942b Pipeline changes and version bump (#6047)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: a <a>
2026-04-02 12:52:22 +01:00
Peter Dave Hello da9327ab1c Restore English search aliases in zh-TW tags (#6039)
# Description of Changes

Preserve the translated zh-TW tags while restoring the English aliases
used by frontend tool search.

This keeps common English technical queries such as permissions or
access control discoverable in the zh-TW locale.
---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [x] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

## GitHub Copilot Pull Reuqest summary

> This pull request significantly expands the keyword tags for a wide
range of PDF-related tools and actions in the Traditional Chinese
(`zh-TW`) translation file. The main goal is to improve searchability
and discoverability of features by including a comprehensive set of
English and Chinese keywords, synonyms, and related phrases for each
tool.
> 
> The most important changes include:
> 
> **Localization and Search Optimization:**
> 
> * Expanded the `tags` fields for all tools and actions under the
`[home.*]` sections in `frontend/public/locales/zh-TW/translation.toml`
to include a broad set of English and Chinese keywords, synonyms, and
common search phrases. This enhances feature discoverability for users
searching in either language.
[[1]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3885-R3925)
[[2]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3934-R4039)
[[3]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4054-R4209)
[[4]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4218-R4218)
> 
> **Consistency and Coverage:**
> 
> * Ensured that each tool/action now has a rich set of tags that cover
various ways users might refer to the feature, including technical
terms, synonyms, and related concepts (e.g., "merge", "combine", "join"
for PDF merging).
[[1]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3885-R3925)
[[2]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3934-R4039)
[[3]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4054-R4209)
[[4]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4218-R4218)
> 
> **Internationalization Improvements:**
> 
> * Added English keywords alongside Chinese ones to support bilingual
search and better serve users who may search using English terms in a
localized interface.
[[1]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3885-R3925)
[[2]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L3934-R4039)
[[3]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4054-R4209)
[[4]](diffhunk://#diff-5979ec7aabfd804ffe625390faee80bfc5b97bdb00f72cc3ce27359f82450e87L4218-R4218)
> 
> These changes collectively make it easier for users to find the
features they need, regardless of the language or terminology they use.
2026-04-02 08:35:38 +00:00
EthanHealy01 61280f758a bump deps (#6041)
bump deps and add a one week buffer to releases that we merge in to
allow for vulnerabilities to be caught.
2026-04-01 18:08:45 +01:00
ConnorYoh 801cc8a5f4 Alpha flag for file storage settings (#6044)
## Summary
- Added "Alpha" badge to the File Storage & Sharing nav item in the
settings sidebar
- Added "Alpha" badge to the File Storage & Sharing page title
- Removed the old inline "(Alpha)" text from the Enable Group Signing
label
- Restructured all toggle cards so the switch is anchored to the right
of each row
- Tightened spacing between cards for a more compact layout
- Extended `ConfigNavItem` interface with optional `badge` and
`badgeColor` fields for reuse elsewhere
<img width="1696" height="1057" alt="image"
src="https://github.com/user-attachments/assets/77ac8276-ed65-4cae-8470-65de8f56dd74"
/>
2026-04-01 17:18:48 +01:00
EthanHealy01 74153b6deb Bug/connection mode fixes (#5998) 2026-04-01 15:33:46 +01:00
Anthony Stirling ecd1d3cad3 fix new line in redact (#6035) 2026-04-01 11:58:38 +01:00
Anthony Stirling 0a098cf7b7 idle cpu fix test (#6015) 2026-04-01 11:58:10 +01:00
Anthony Stirling cfa8d1e5d7 qr split fixes (#6043) 2026-04-01 11:54:33 +01:00
Anthony Stirling 5ffa808c0f Remove gosu (#6036) 2026-04-01 11:54:12 +01:00
Matheus Saito 212f12a81f Added back ctrl+r as rotate if on desktop (#5982) (#5993)
Fix #5982

Behaviour of ctrl+r altered to support rotate on desktop, while the web
version continue to use refresh as default.
2026-04-01 11:48:53 +01:00
James Brunton c31e4253dd Fix any type usage in proprietary/ (#5949)
# Description of Changes
Follow on from #5934, expanding `any` type usage ban to the
`proprietary/` folder
2026-04-01 08:21:26 +00:00
Peter Dave Hello a96b95e198 Update and improve zh-TW Traditional Chinese locale (#6034) 2026-03-30 21:10:49 +01:00
Anthony Stirling a06b6a4bac pdf layer toggle (#6028) 2026-03-30 17:04:53 +01:00
Anthony Stirlinganda cdc288e78d nonpdf-viewer (#6024)
Co-authored-by: a <a>
2026-03-30 16:39:11 +01:00
Anthony Stirling 82a3b8c770 Unlock account (#5984) 2026-03-30 16:07:57 +01:00
ConnorYoh 1e97a32d4b feat(desktop): gate shared signing behind self-hosted auth (#6002)
## Summary

This PR adds full desktop (Tauri) support for the shared signing feature
when connected to a self-hosted server, and fixes several bugs
discovered during that work.

### Feature gating

Shared signing, file sharing, and share links are proprietary server
features that require an authenticated self-hosted session. Previously
these were read directly from `config` with no awareness of connection
mode or auth state, meaning the UI could appear in SaaS/local mode or
when logged out.

- Introduce `useGroupSigningEnabled` and `useSharingEnabled` hooks with
core implementations (web behaviour unchanged) and desktop overrides
that require `selfhosted` mode + an active authenticated session
- Extract shared subscription logic into `useSelfHostedAuth` (connection
mode + auth state + config refetch)
- `QuickAccessBar` now derives all three flags from the hooks instead of
raw config

### Config timing fix

When a user logs in via the SetupWizard, the `jwt-available` event fires
a config fetch *before* the mode is switched to `selfhosted`. This meant
the config was fetched from the local bundled backend (port ~59567)
which has no knowledge of `storageGroupSigningEnabled`, causing the
group signing button to stay hidden until a full page refresh.
`useSelfHostedAuth` detects the mode transition and triggers a fresh
config fetch at the correct moment, after the self-hosted URL is active.

### Bug fixes

**`SignPopout.tsx`** — Manually setting `Content-Type:
multipart/form-data` on two `FormData` POST requests stripped the
auto-generated boundary, causing a `400 bad multipart` from the server.
Removed the explicit headers so Axios sets them correctly.

**`tauriHttpClient.ts`** — `response.json()` was called before
`response.ok` was checked. A plain-text error body from the server (e.g.
`"Cannot sign..."`) caused a `SyntaxError` that fell into the network
error catch block and was reported as `ERR_NETWORK`, hiding the real
failure. The fix checks `response.ok` first, reads error bodies as text,
and handles empty 200 bodies (returning `null` instead of throwing).

---

## Testing

### Prerequisites
- Desktop app running in self-hosted mode pointed at a local
Stirling-PDF instance (`http://localhost:8080`)
- The self-hosted instance has group signing and storage enabled in
settings
- At least two user accounts on the self-hosted instance

### 1. Feature gating — group signing button

| Step | Expected |
|---|---|
| Open the desktop app in **local mode** (no server configured) | Group
signing button absent from QuickAccessBar |
| Switch to self-hosted mode but **do not log in** | Group signing
button absent |
| Log in to the self-hosted server | Group signing button appears
without requiring a page refresh |
| Log out | Group signing button disappears immediately |
| Log back in | Group signing button reappears without a page refresh |

### 2. Feature gating — file sharing

Repeat the same steps above, verifying the share and share-link buttons
in the file manager follow the same visibility rules.

### 3. Create a signing session

1. Log in, open the group signing panel from QuickAccessBar
2. Select a PDF, add a participant, configure signature defaults and
submit
3. Verify the session is created successfully (no `400 bad multipart`
error)

### 4. Participant signing

1. As the invited participant, open the signing request from
QuickAccessBar
2. Upload or draw a signature and submit
3. Verify signing completes successfully (no `ERR_NETWORK` error)

### 5. Error surfacing

1. Attempt an action that the server rejects (e.g. sign a document with
an invalid certificate)
2. Verify the actual server error message is shown rather than a generic
network error
2026-03-30 14:37:45 +00:00
James Brunton 4a6b426651 Only allow Tauri imports in the desktop app (#5995)
# Description of Changes
Adds an eslint rule to disallow importing any Tauri APIs outside the
desktop folder to help hint to developers that they should be following
the frontend architecture.

While doing this, I also discovered that you can provide a custom
message in the `no-restricted-imports` rule, which is nicer than the
comments that I'd previously added to the eslint config file to explain
why they weren't allowed:

```text
/Users/jamesbrunton/Dev/spdf1/frontend/src/core/components/shared/config/configSections/GeneralSection.tsx
  19:1  error  'src/core/contexts/PreferencesContext' import is restricted from being used by a pattern. Use @app/* imports instead of absolute src/ imports              no-restricted-imports
  20:1  error  '../../../../../core/contexts/AppConfigContext' import is restricted from being used by a pattern. Use @app/* imports instead of relative imports          no-restricted-imports
  21:1  error  '@tauri-apps/core' import is restricted from being used by a pattern. Tauri APIs are desktop-only. Review frontend/DeveloperGuide.md for structure advice  no-restricted-imports
```
2026-03-30 14:24:16 +00:00
ConnorYoh 0e29640766 fix: get all Playwright E2E tests loading and expand CI to run full suite (#6009)
## Fix Playwright E2E tests and expand CI to run full suite

### Problem

The full Playwright suite was broken in two ways:

1. **`ConvertE2E.spec.ts` crashed at import time** —
`conversionEndpointDiscovery.ts` imported a React hook at the top level,
which pulled in the entire component tree. That chain eventually
required `material-symbols-icons.json` (a generated file that didn't
exist), crashing module resolution before any tests ran.

2. **CI only ran cert validation tests** — both `build.yml` and
`nightly.yml` hardcoded `src/core/tests/certValidation` as the test
path, silently ignoring everything else.

### Changes

**`ConvertE2E.spec.ts` — complete rewrite**
The old tests were useless in practice: all 9 dynamic conversion tests
were permanently skipped unless a real Spring Boot backend was running
(they called a live `/api/v1/config/endpoints-enabled` endpoint at
module load time). Replaced with 4 focused tests that use `page.route()`
mocking — no backend required, same pattern as
`CertificateValidationE2E`.

New tests cover:
- Convert button absent before a format pair is selected
- Successful PDF→PNG conversion shows a download button (mocked API
response)
- API error surfaces as an error notification
- Convert button appears and is enabled after selecting valid formats

**`conversionEndpointDiscovery.ts` — deleted**
Only existed to support the old tests. The `useConversionEndpoints`
React hook it exported was never imported anywhere else.

**`ReviewToolStep.tsx`**
Added `data-testid="download-result-button"` to the download button —
required for the happy-path test assertion.

**CI workflows (`build.yml`, `nightly.yml`)**
- Added a `Generate icons` step before Playwright runs (`node
scripts/generate-icons.js`) — the icon JSON is generated by `npm run
dev` locally but skipped by `npm ci` in CI
- Removed the `src/core/tests/certValidation` path filter so the full
suite runs
2026-03-30 11:27:55 +01:00
albanobattistella 05b4255751 Update Italian translations (#6014) 2026-03-30 11:04:11 +01:00
dependabot[bot] 1ab07a9027 build(deps): bump crazy-max/ghaction-github-labeler from 5.3.0 to 6.0.0 (#6019)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 11:03:11 +01:00
dependabot[bot] 75421b4223 build(deps): bump qrcode from 8.0 to 8.2 in /testing/cucumber (#6022)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 11:02:30 +01:00
dependabot[bot] a7fe4e9a76 build(deps): bump pypdf from 6.7.5 to 6.9.2 in /testing/cucumber (#6020)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 11:02:12 +01:00
dependabot[bot] 10ab2872f6 build(deps): bump requests from 2.32.5 to 2.33.0 in /testing/cucumber (#6017)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 11:01:56 +01:00
Anthony Stirling 2fdc9c112f test reports for test.sh and fix test.sh deployments (#6027) 2026-03-29 23:35:45 +01:00
ConnorYoh dd44de349c Shared Sign Cert Validation (#5996)
## PR: Certificate Pre-Validation for Document Signing

### Problem

When a participant uploaded a certificate to sign a document, there was
no validation at submission time. If the certificate had the wrong
password, was expired, or was incompatible with the signing algorithm,
the error only surfaced during **finalization** — potentially days
later, after all other participants had signed. At that point the
session is stuck with no way to recover.

Additionally, `buildKeystore` in the finalization service only
recognised `"P12"` as a cert type, causing a `400 Invalid certificate
type: PKCS12` error when the **owner** signed using the standard
`PKCS12` identifier.

---

### What this PR does

#### Backend — Certificate pre-validation service

Adds `CertificateSubmissionValidator`, which validates a keystore before
it is stored by:
1. Loading the keystore with the provided password (catches wrong
password / corrupt file)
2. Checking the certificate's validity dates (catches expired and
not-yet-valid certs)
3. Test-signing a blank PDF using the same `PdfSigningService` code path
as finalization (catches algorithm incompatibilities)

This runs on both the participant submission endpoint
(`WorkflowParticipantController`) and the owner signing endpoint
(`SigningSessionController`), so both flows are protected.

#### Backend — Bug fix

`SigningFinalizationService.buildKeystore` now accepts `"PKCS12"` and
`"PFX"` as aliases for `"P12"`, consistent with how the validator
already handles them. This fixes a `400` error when the owner signed
using the `PKCS12` cert type.

#### Frontend — Real-time validation feedback

`ParticipantView` gains a debounced validation call (600ms) triggered
whenever the cert file or password changes. The UI shows:
- A spinner while validating
- Green "Certificate valid until [date] · [subject name]" on success
- Red error message on failure (wrong password, expired, not yet valid)
- The submit button is disabled while validation is in flight

#### Tests — Three layers

| Layer | File | Coverage |
|---|---|---|
| Service unit | `CertificateSubmissionValidatorTest` | 11 tests — valid
P12/JKS, wrong password, corrupt bytes, expired, not-yet-valid, signing
failure, cert type aliases |
| Controller unit | `WorkflowParticipantValidateCertificateTest` | 4
tests — valid cert, invalid cert, missing file, invalid token |
| Controller integration | `CertificateValidationIntegrationTest` | 6
tests — real `.p12`/`.jks` files through the full controller → validator
stack |
| Frontend E2E | `CertificateValidationE2E.spec.ts` | 7 Playwright tests
— all feedback states, button behaviour, SERVER type bypass |

#### CI

- **PR**: Playwright runs on chromium when frontend files change (~2-3
min)
- **Nightly / on-demand**: All three browsers (chromium, firefox,
webkit) at 2 AM UTC, also manually triggerable via `workflow_dispatch`
2026-03-27 14:01:10 +00:00
James Brunton e10c5f6283 Redesign Python AI engine (#5991)
# Description of Changes
Redesign the Python AI engine to be properly agentic and make use of
`pydantic-ai` instead of `langchain` for correctness and ergonomics.
This should be a good foundation for us to build our AI engine on going
forwards.
2026-03-26 10:35:47 +00:00
Anthony StirlingandClaude Haiku 4.5 9500acd69f Base docker image (#5958)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-25 15:41:58 +00:00
Anthony Stirlinganda bb43e9dcdf dark mode PDF filter init (#5994)
Co-authored-by: a <a>
2026-03-25 15:38:42 +00:00
28613caf8a fileshare (#5414)
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-25 11:00:40 +00:00
Rafael Roseira Machado 47cad0a131 fix pause-rounded icon typos and comments (#5992) 2026-03-24 18:56:51 +00:00
stirlingbot[bot]andAnthony Stirling 4858608162 🤖 format everything with pre-commit by stirlingbot (#5946)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-24 18:55:37 +00:00
OUNZAR AymaneandCopilot a1f03c844b Enhance multi-page PDF layout with advanced customization options (#397, #3655) (#5859)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 17:27:56 +00:00
InstaZDLLandAnthony Stirling 8bbfbd63d7 feat(security): add RFC 3161 PDF timestamp tool (#5855)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-24 17:00:33 +00:00
Anthony Stirling 7b3985e34a FileReadiness (#5985) 2026-03-24 15:25:33 +00:00
Anthony Stirling f03f0d4adb junits (#5988) 2026-03-24 14:12:31 +00:00
Anthony Stirling c3fc200c5d Remove images (#5966) 2026-03-24 14:11:27 +00:00
briosandReece Browne c3530024c4 feat(pdf): replace PdfLib with Pdfium for form handling and general rendering tasks (#5899)
# Description of Changes

Improves PDF rendering in the viewer by adding digital signature field
support,
cleaning up overlay rendering, and migrating the contrast tool off
pdf-lib to PDFium WASM.

### Signature Field Overlay
- Added `SignatureFieldOverlay` component that renders digital signature
form fields
- Renders appearance streams when present; shows a fallback badge for
unsigned fields
- Uses PDFium WASM for bitmap extraction

### Overlay Rendering
- Integrated `SignatureFieldOverlay` and `ButtonAppearanceOverlay` into
`LocalEmbedPDF`
- Overlays are now clipped to page boundaries
- Clarified in `EmbedPdfViewer` that frontend overlays use PDFium WASM,
  backend overlays use PDFBox

### Contrast Tool Migration
- Replaced pdf-lib with PDFium WASM in `useAdjustContrastOperation`
- PDF page creation and image embedding now go through PDFium APIs
directly
- Updated bitmap handling and memory management accordingly

### Cleanup
- Fixed import ordering in viewer components
- Removed stale comments in the contrast operation hook

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2026-03-24 13:34:52 +00:00
Reece Browne 3ea11352e3 Fix/v2/text selection 2 (#5990) 2026-03-24 12:51:52 +00:00
brios 1276e5675e chore(deps): bump pdfbox version to 3.0.7 (#5923) 2026-03-23 19:44:05 +00:00
dependabot[bot] 81c4718954 build(deps): bump sigstore/cosign-installer from 4.0.0 to 4.1.0 (#5975)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:40:01 +00:00
dependabot[bot] 1806b5d3be build(deps): bump actions/cache from 5.0.3 to 5.0.4 (#5976)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:38:07 +00:00
dependabot[bot] 81c0187bf1 build(deps): bump softprops/action-gh-release from 2.5.0 to 2.6.1 (#5979)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:37:37 +00:00
dependabot[bot] 9d51414fbb build(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0 (#5977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 19:37:06 +00:00
EthanHealy01andClaude Sonnet 4.6 2e2b55e87d Desktop/remove hard requirement auth wall on desktop (#5956)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:36:48 +00:00
ConnorYoh 081b1ec49e Invite-link-issues (#5983) 2026-03-23 19:35:41 +00:00
c46156f37f Bump/embed pdfv2.8.0 (#5921)
please merge #5919, alternatively, just push this and delete that PR
because this is a continuation of that.

This PR bumps the embed PDF version to 2.8.0 and also adds comments
functionaliy

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-23 14:35:39 +00:00
Reece Browne 41945543e0 Fix save converted files (#5971)
Fix saving converted files on tauri
2026-03-23 13:51:40 +00:00
James Brunton e5f6180dbe Remove cmd-r override for rotation because it interferes with refresh (#5981)
# Description of Changes
Currently, cmd-r is set to rotate the PDF in the viewer instead of
perform refresh in the browser. This is unintuitive and confusing for
Mac users, and for Windows users (who are less used to doing ctrl-r for
refresh) it only works some of the time, if the Viewer is active, so
removing the override is no great loss.
2026-03-23 13:26:10 +00:00
James Brunton 57c810ab9a Add frontend developer guide describing the path alias architecture (#5964)
# Description of Changes
Add frontend developer guide describing the path alias architecture.
There's probably more needed in here which we should flesh out over
time, but this is a start.
2026-03-23 10:16:52 +00:00
brios b012f18a40 fix(gradle): bump gradle jar version to 9.3.1-bin (#5938) 2026-03-20 12:00:01 +00:00
Anthony Stirling 9e8606cab4 XSS for eml and others (#5967) 2026-03-20 11:55:23 +00:00
Achieve3318andJames Brunton 55bcb92810 Add explicit Save As button for desktop viewer (issue #5928) (#5959)
## Description

Adds an explicit **“Save As”** button to the desktop viewer so users can
always save a copy of the current PDF to a different location, even if
the original file already has a local path.

This complements the existing smart **Save/Download** behavior:
- The existing download button continues to either save back to the
original path (when available) or prompt for a path when needed.
- The new **Save As** button always opens a save dialog to choose a
location/name for a new copy.

## Changes

- **RightRail (viewer controls)**
- Added a new **Save As** action icon in the right rail settings
section.
  - The button:
- Uses `viewerContext.exportActions.saveAsCopy()` to get the current
viewer state as a PDF.
- Calls `downloadFile` without a `localPath`, ensuring the desktop app
shows a **Save As** dialog.
- Picks the first selected file (if any) or the first active file as the
source for the filename.
- **Desktop / Web behavior**
  - In the desktop app (Tauri), clicking **Save As**:
- Opens a native save dialog so the user can choose a different folder
and filename.
- Writes a new copy without changing the existing file’s `localFilePath`
or dirty state.
- In the web app, the button behaves like a standard download of a copy
(browser-controlled save dialog / download).

## Motivation

- Users often want to apply operations on a PDF while **keeping the
original unmodified**.
- The existing smart Save behavior chooses between Save and Save As
automatically, but there was no way to explicitly request **Save As**.
- This change gives desktop users a clear, dedicated **“Save As”**
control while preserving the current Save/Download behavior.

## Notes

- No backend changes.
- No changes to the existing Save / Download button behavior.
- The new button uses existing viewer export and download utilities,
minimizing new logic.

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-03-20 09:32:24 +00:00
Aarón Rosa Díaz a7f2abcb22 Update Spanish translation (translation.toml) (#5965) 2026-03-19 17:15:42 +00:00
Anthony Stirling 3376a87f15 speaking! (#5925) 2026-03-19 14:11:36 +00:00
PandaMan 2b9f03237a Fix non-ASCII characters in headers being rejected (#5377) (#5699) 2026-03-17 19:23:18 +00:00
ConnorYoh 214dc20c2e Hotfix-cant-run-tools-when-no-credits (#5955)
Tested:
* Can sign in on saas -> can run local tools with or without credits->
can run saas only tools (if credits) -> can't run saas only tools
without credits
* Can sign in self-hosted -> can run all tools on remote if available ->
can run local when self-hosted unavailable

Clouds show on saas tools when connected
Tools are disabled when connected to self-hosted but cannot find server.
You also get banner


#cantwaitforplaywritetests
2026-03-17 13:01:08 +00:00
unlair b656e1e2d1 Fix Docker builds on Debian (#5936) 2026-03-16 22:22:16 +00:00
James BruntonandAnthony Stirling 7f9bbebe5b Unify creditCosts.ts files (#5952)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-16 22:05:02 +00:00
James Brunton dbff05814f Fix any type usage in the saas/ folder (#5934)
# Description of Changes
Ages ago I made #4835 to try and fix all the `any` type usage in the
system but never got it finished, and there were just too many to review
and ensure it still worked. There's even more now.

My new tactic is to fix folder by folder. This fixes the `any` typing in
the `saas/` folder, and also enables `no-unnecessary-type-assertion`,
which really helps reduce pointless `as` casts that AI generates when
the type is already known. I hope to expand both of these to the rest of
the folders soon, but one folder is better than none.
2026-03-16 11:51:16 +00:00
Rafael Roseira Machado 1722733802 fix jumping cursor bug (#5937) 2026-03-16 11:44:23 +00:00
dependabot[bot] 85d5bb5dc2 build(deps): bump actions/upload-artifact from 6.0.0 to 7.0.0 (#5939)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:15:15 +00:00
dependabot[bot] 2e64d7cca6 build(deps): bump dorny/paths-filter from 3.0.2 to 4.0.1 (#5943)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:15:12 +00:00
dependabot[bot] 3908e258c8 build(deps): bump github/codeql-action from 4.32.4 to 4.32.6 (#5941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:14:49 +00:00
dependabot[bot] 9b5714277a build(deps): bump srvaroa/labeler from 1.13.0 to 1.14.0 (#5942)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:14:29 +00:00
dependabot[bot] 9df4692648 build(deps): bump actions/cache from 4.3.0 to 5.0.3 (#5940)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 11:14:10 +00:00
James Brunton c58a6092ec Add SaaS AI engine (#5907) 2026-03-16 11:01:50 +00:00
Anthony Stirling cddc8e6df0 Delete code from invalid license (#5947) 2026-03-16 11:01:31 +00:00
James Brunton 971321fb19 Fix printing on Mac desktop (#5920)
# Description of Changes
Fix #5164 

As I mentioned on the bug
https://github.com/Stirling-Tools/Stirling-PDF/issues/5164#issuecomment-4045170827,
it's impossible to print on Mac currently because
`iframe.contentWindow?.print()` silently does nothing in Tauri on Mac,
but [it seems unlikely that this will be
fixed](https://github.com/tauri-apps/tauri/issues/13451#issuecomment-4048075861).

Instead, I've linked directly to the Mac `PDFKit` framework in Rust to
use its printing functionality instead of Safari's. I believe that
`PDFKit` is what `Preview.app` is using and the print UI that it
generates seems to perform identically, so this should solve the issue
on Mac. Hopefully one day the TS iframe print API will be fixed and
we'll be able to get rid of this code, or [there'll be an official Tauri
plugin for printing which we can use
instead](https://github.com/tauri-apps/plugins-workspace/issues/293).

This implementation should be entirely Mac-specific. Windows & Linux
will continue to use their TS printing (which comes from EmbedPDF)
unless we have a good reason to change them to use a native solution as
well.
2026-03-16 10:49:45 +00:00
Balázs Szücs f384e765fb feat(http2): add jetty-alpn-java-server dependency for HTTP/2 support (#5945) 2026-03-15 20:10:34 +00:00
dependabot[bot] 400ee16e83 build(deps): bump actions/download-artifact from 7.0.0 to 8.0.0 (#5887)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:56:25 +00:00
dependabot[bot] f777efdd1c build(deps): bump actions/setup-python from 6.1.0 to 6.2.0 (#5886)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:55:26 +00:00
dependabot[bot] 1d62f7ec23 build(deps): bump docker/metadata-action from 5.10.0 to 6.0.0 (#5889)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:55:12 +00:00
dependabot[bot] c5b202f2a1 build(deps): bump crazy-max/ghaction-github-runtime from 3.1.0 to 4.0.0 (#5890)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:54:58 +00:00
dependabot[bot]andAnthony Stirling a2b0d1122c build(deps): bump step-security/harden-runner from 2.14.0 to 2.15.1 (#5896)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-13 15:39:07 +00:00
stirlingbot[bot] 34c629dcb4 Update Backend 3rd Party Licenses (#5930)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-03-13 14:32:54 +00:00
stirlingbot[bot]andAnthony Stirling 4726f42030 Update Backend 3rd Party Licenses (#5798)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-13 14:29:27 +00:00
EthanHealy01 c9d693f1eb Improve annotations (#5919)
* Text box/notes movement improvements 
* Fix the issue where hiding, then showing annotations looses progress 
* Fix the issue where hidig/showing annotations jumps you back up to the
top of your open document 
* Support ctrl+c and  ctrl+v and backspace to delete 
* Better handling when moving to different tool from annotate 
* Added a color picker eyedropper button  
* Auto-switch to Select after note/text placement, so users can quickly
place and type 
2026-03-13 14:03:27 +00:00
ConnorYoh 44e036da5a Check if saas before blocking credit insufficiencies (#5929)
fixes #5926
2026-03-13 10:28:39 +00:00
albanobattistella 9969fe5a6d Update Italian translations (#5884) 2026-03-12 20:27:03 +00:00
ConnorYohandAnthony Stirling 0545c3f997 Cleanup-conversion-translations (#5906)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-12 20:23:13 +00:00
Reece Browne b68b406a2a Fix rotate failing on large documents (#5917) 2026-03-12 17:57:43 +00:00
James Brunton 8674765528 Add system for managing env vars (#5902)
# Description of Changes
Previously, `VITE_*` environment variables were scattered across the
codebase with hardcoded fallback values inline (e.g.
`import.meta.env.VITE_STRIPE_KEY || 'pk_live_...'`). This made it
unclear which variables
were required, what they were for, and caused real keys to be silently
used in builds where they hadn't been explicitly configured.

## What's changed

I've added `frontend/.env.example` and `frontend/.env.desktop.example`,
which declare every `VITE_*` variable the app uses, with comments
explaining each one and sensible defaults where applicable. These
are the source of truth for what's required.

I've added a setup script which runs before `npm run dev`, `build`,
`tauri-dev`, and all `tauri-build*` commands. It:
- Creates your local `.env` / `.env.desktop` from the example files on
first run, so you don't need to do anything manually
- Errors if you're missing keys that the example defines (e.g. after
pulling changes that added a new variable). These can either be
manually-set env vars, or in your `.env` file (env vars take precedence
over `.env` file vars when running)
- Warns if you have `VITE_*` variables set in your environment that
aren't listed in any example file

I've removed all `|| 'hardcoded-value'` defaults from source files
because they are not necessary in this system, as all variables must be
explicitly set (they can be set to `VITE_ENV_VAR=`, just as long as the
variable actually exists). I think this system will make it really
obvious exactly what you need to set and what's actually running in the
code.

I've added a test that checks that every `import.meta.env.VITE_*`
reference found in source is present in at least one example file, so
new variables can't be added without being documented.

## For contributors

New contributors shouldn't need to do anything - `npm run dev` will
create your `.env` automatically.

If you already have a `.env` file in the `frontend/` folder, you may
well need to update it to make the system happy. Here's an example
output from running `npm run dev` with an old `.env` file:

```
$ npm run dev

> frontend@0.1.0 dev
> npm run prep && vite


> frontend@0.1.0 prep
> tsx scripts/setup-env.ts && npm run generate-icons

setup-env: see frontend/README.md#environment-variables for documentation
setup-env: .env is missing keys from config/.env.example:
  VITE_GOOGLE_DRIVE_CLIENT_ID
  VITE_GOOGLE_DRIVE_API_KEY
  VITE_GOOGLE_DRIVE_APP_ID
  VITE_PUBLIC_POSTHOG_KEY
  VITE_PUBLIC_POSTHOG_HOST
  Add them manually or delete your local file to re-copy from the example.
setup-env: the following VITE_ vars are set but not listed in any example file:
  VITE_DEV_BYPASS_AUTH
  Add them to config/.env.example or config/.env.desktop.example if they are required.
```

If you add a new `VITE_*` variable to the codebase, add it to the
appropriate `frontend/config/.env.example` file or the test will fail.
2026-03-12 13:03:44 +00:00
ConnorYoh d5d03b9ada Manage state of price-lookup calls (#5915)
Now calls stripe-price-lookup once when prices are required rather then
bombarding on every rerender
2026-03-11 13:53:49 +00:00
James Brunton 32cf6866f3 Move AI advice to AGENTS.md and add symlink from CLAUDE.md (#5914)
# Description of Changes
Inspired by https://github.com/pydantic/pydantic-ai/pull/4169, this PR
moves our `CLAUDE.md` advice to the more generic `AGENTS.md` file (which
works on Codex, Gemini, etc). It also adds a symlink from `CLAUDE.md` to
`AGENTS.md`, which Claude follows properly, so all AIs should get the
same advice and we only need to keep one file up-to-date.
2026-03-11 13:43:30 +00:00
James Brunton fa8c52b2be Add SaaS frontend code (#5879)
# Description of Changes
Adds the code for the SaaS frontend as proprietary code to the OSS repo.
This version of the code is adapted from 22/1/2026, which was the last
SaaS version based on the 'V2' design. This will move us closer to being
able to have the OSS products understand whether the user has a SaaS
account, and provide the correct UI in those cases.
2026-03-11 11:53:54 +00:00
ConnorYoh 8bc37bf5ae Desktop: Fallback to local backend if self-hosted server is offline (#5880)
* Adds a fallback mechanism so the desktop app routes tool operations to
the local bundled backend when the user's self-hosted Stirling-PDF
server goes offline, and disables tools in the UI that aren't supported
locally.

* `selfHostedServerMonitor.ts` independently polls the self-hosted
server every 15s and exposes which tool endpoints are unavailable when
it goes offline
* `operationRouter.ts` intercepts operations destined for the
self-hosted server and reroutes them to the local bundled backend when
the monitor reports it offline
* `useSelfHostedToolAvailability.ts` feeds the offline tool set into
useToolManagement, disabling affected tools in the UI with a
selfHostedOffline reason and banner warning

- `SelfHostedOfflineBanner `is a dismissable (session-only) gray bar
shown at the top of the UI when in self-hosted mode and the server goes
offline. It shows:
2026-03-10 10:04:56 +00:00
James Brunton 6d9fc59bc5 Get rid of bad description for file association on Windows (#5905)
# Description of Changes
Explorer currently shows this on Windows:

<img width="380" height="43" alt="image"
src="https://github.com/user-attachments/assets/a892d827-8e0a-4f85-a035-52a454eaadd8"
/>

This PR just removes the description for the file association so we use
the default behaviour. This string is only used on Windows according to
the [Tauri docs](https://v2.tauri.app/reference/config/#description-1).
2026-03-09 15:02:10 +00:00
ConnorYohandJames Brunton ff31b2f9ca Posthog-fixes (#5901)
PostHog is now initialized with persistence: 'memory' so no cookies are
written on first load. Consent is handled in a PostHogConsentSync
component that switches to localStorage+cookie persistence only when the
user accepts, using the official @posthog/react package (cherry-picked
from 14aaf64)

---------

Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-03-09 12:13:09 +00:00
Brian Banerjee 81596f0299 Limit PostHog cookie to Stirling PDF's subdomain only (#5882) 2026-03-08 21:03:10 +00:00
Reece Browne 63d38e382d Chore/v2/transforms as root (#5868)
Any task that changes file type or produces more/fewer files than the
input are now consumed as root files not incremented versions of the
input.
2026-03-06 13:46:40 +00:00
Anthony Stirling a57e336675 Add searchable settings selector to AppConfigModal and improve nav behavior (#5873) 2026-03-06 11:11:17 +00:00
Anthony Stirling 456106195e translations and version bump (#5878) 2026-03-06 11:09:18 +00:00
Anthony Stirling 7c1eb4183b stop enabling english (#5874) 2026-03-06 10:09:51 +00:00
Anthony StirlingandEthanHealy01 7d640e9ce6 option to hide google drive and add settings (#5863)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-03-06 10:09:33 +00:00
ConnorYoh cafcee6c99 Add the production billing portal link for static plan page (#5860) 2026-03-06 10:08:38 +00:00
ConnorYoh 7fdd100abf Fix signatures not showing (#5872) 2026-03-06 00:43:29 +00:00
dependabot[bot] 086b55b0bb build(deps): bump pypdf from 6.7.4 to 6.7.5 in /testing/cucumber in the pip group across 1 directory (#5853)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 00:15:46 +00:00
RenzoandRenzoMXD c77242d943 fix: merge pdf pipeline validation (#5799)
Co-authored-by: RenzoMXD <RenzoMXD@users.noreply.github.com>
2026-03-06 00:14:30 +00:00
dependabot[bot] 30b0924d6b build(deps): bump digicert/ssm-code-signing from 1.2.0 to 1.2.1 (#5692)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 00:13:59 +00:00
dependabot[bot] 2d6f206c36 build(deps): bump actions/setup-node from 4.4.0 to 6.2.0 (#5691)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 00:13:35 +00:00
Anthony Stirling 6c83da6417 Audit fixes and improvements (#5835) 2026-03-05 22:00:44 +00:00
Anthony Stirling 879ffc066f tauri notifications (#5875) 2026-03-05 18:30:20 +00:00
Anthony Stirling 0f7ee5c5b0 settings menu reworks (#5864) 2026-03-05 16:20:20 +00:00
Anthony Stirling ba2d10a75b Persist Tauri window state between launches (#5871) 2026-03-05 16:18:25 +00:00
ConnorYoh 98835ce7b5 Don't build mac if you don't have the secrets (#5861)
Don't build mac if signing secrets unnavailable. 

No point in trying to build without signing as you cannot install it on
a mac without signature.
2026-03-04 15:59:42 +00:00
stirlingbot[bot] 2f2ced321a 🤖 format everything with pre-commit by stirlingbot (#5775)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-03-03 21:51:44 +00:00
Anthony Stirling bfe655fecb Bump version from 2.5.3 to 2.6.0 2026-03-03 21:46:31 +00:00
Balázs Szücs 9ac260ee92 feat(aot): add aot-diagnostics.sh for AOT cache diagnostics and validation (#5848)
# Description of Changes



This pull request makes significant improvements to the Docker build
process for the embedded Stirling-PDF image, focusing on build
efficiency, runtime optimization, and maintainability. Key changes
include upgrading major tool versions, introducing optional stripping of
Calibre's WebEngine to reduce image size, consolidating ImageMagick
layers, and refining the Python environment build process. The runtime
image is now leaner, with clearer separation between build and runtime
dependencies, and improved caching for faster builds and pulls.

**Build and Dependency Management Improvements**
* Upgraded Calibre to version `9.4.0` and added support for the
`TARGETPLATFORM` build argument for multi-platform builds.
* Added an optional `CALIBRE_STRIP_WEBENGINE` build argument to strip
Chromium/WebEngine from Calibre, saving ~80 MB when PDF output via
Calibre is not needed.
* Consolidated ImageMagick outputs into a single staging directory
(`/magick-export`) to reduce Docker layers and improve caching
efficiency.
* Refactored Python virtual environment build: now built in a dedicated
stage with pre-built wheels and copied into the runtime image,
eliminating the need for build tools and pip installs at runtime.

**Runtime Image Optimization**
* Reduced installed system packages to only what is needed at runtime;
Python build tools and dev packages are no longer included.
* Cleaned up unnecessary runtime files, including removal of build-only
Python artifacts and system headers, for a smaller and more secure
image.

**Layer and Copy Optimization**
* Switched to `COPY --link` for all major external tool layers and
application files, enabling independent layer caching and parallel pulls
for faster builds.

**Runtime Configuration and Health**
* Improved runtime directory structure and permissions, added persistent
cache directories for Project Leyden AOT, and wrote the version tag to
`/etc/stirling_version` for easier script access.
* Updated the healthcheck to wait longer for startup and increased
timeout/retries for more robust readiness detection.

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
2026-03-03 19:06:46 +00:00
Anthony Stirling 1b68a513a9 tauri jdk25 and docs (#5814) 2026-03-03 13:49:33 +00:00
Reece BrowneandEthanHealy01 93d7919c4c Fix split tooltips (#5847)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-03-03 13:49:05 +00:00
ConnorYoh 3e4c984fcc Add check for ghostscript before plowing on with removeDataOutsideCrop (#5845) 2026-03-03 12:52:28 +00:00
ConnorYoh c4c43593e6 fallback for /api/v1/config/endpoints-availability (#5842) 2026-03-02 22:03:23 +00:00
fd1b7abc83 refactor(merge,split,json): adopt streaming approach and standardize types, address gradle warnings (#5803)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Balázs <balazs@heim-041-30.jkh.uni-linz.ac.at>
2026-03-02 21:55:07 +00:00
dependabot[bot]andAnthony Stirling 0c46f77179 build(deps): bump com.sun.xml.bind:jaxb-core from 2.3.0.1 to 4.0.6 (#5365)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-03-02 16:16:05 +00:00
ConnorYoh afda066579 Frontend and Desktop audit fixes (#5840) 2026-03-02 15:44:05 +00:00
dependabot[bot] e9148437f6 build(deps-dev): bump stylelint from 16.26.1 to 17.4.0 in /devTools (#5822)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:23:26 +00:00
dependabot[bot] cb835bcce1 build(deps): bump ajv from 8.17.1 to 8.18.0 in /devTools in the npm_and_yarn group across 1 directory (#5774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:17:53 +00:00
dependabot[bot] 161bfef7da build(deps): bump actions/ai-inference from 2.0.5 to 2.0.7 (#5831)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:14:07 +00:00
dependabot[bot] 690eceb548 build(deps): bump github/codeql-action from 4.31.10 to 4.32.4 (#5833)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:13:52 +00:00
dependabot[bot] b53c236234 build(deps): bump pypdf from 6.6.2 to 6.7.4 in /testing/cucumber (#5825)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:12:30 +00:00
Anthony Stirling abb8b1f721 Fix health status checks falling under mettric flag (#5821) 2026-03-02 13:56:51 +00:00
Anthony StirlingandClaude Haiku 4.5 012bd1af92 hardening (#5807)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 13:56:39 +00:00
Anthony Stirling 8b25db37ad fix split cuased by defaultParameters breaking dynamic endpoint tools (#5838) 2026-03-02 13:55:58 +00:00
Balázs Szücs 48dd4154e9 feat(conversion): switch PDF input engine to pdftohtml for improved performance and reduced dependencies (#5820) 2026-03-02 13:55:42 +00:00
StepSecurity Bot cfe040485b [StepSecurity] Apply security best practices (#5830) 2026-03-01 17:16:03 +00:00
Balázs Szücs c15ff1e832 fix(aot): use Spring Boot exploded layer format for aot cache (#5811) 2026-03-01 15:46:02 +00:00
Balázs Szücs c244edf8b7 feat(annotation): add moveAnnotation API for efficient repositioning of annotations, and bump embed to 2.7.0 (#5809) 2026-03-01 15:45:44 +00:00
Anthony Stirling 13d7ee7496 skip certs (#5819)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-27 17:31:40 +00:00
Anthony Stirling 930d7a0df8 open-saml bumps (#5805) 2026-02-27 15:03:12 +00:00
Anthony Stirling 6a1597bb8d ci: provide default desktop env vars in tauri GitHub Actions builds (#5815) 2026-02-27 10:57:50 +00:00
aikido-autofix[bot] d98ff194e4 [Aikido] AI Fix for 3rd party Github Actions should be pinned (#5817)
Co-authored-by: aikido-autofix[bot] <119856028+aikido-autofix[bot]@users.noreply.github.com>
2026-02-27 10:42:51 +00:00
Balázs Szücs 7310b75ee6 chore(deps): update dependencies for security (#5813) 2026-02-27 10:10:40 +00:00
Anthony Stirling 1bac8417af Harden shared signature endpoints (#5806) 2026-02-26 12:53:47 +00:00
Anthony Stirling b21e1313d8 docker cache fix (#5801) 2026-02-25 18:35:22 +00:00
Reece Browne 213f136882 lint (#5802) 2026-02-25 18:20:24 +00:00
Anthony Stirling 9438b8db29 DocumentBuilderFactory limiting (#5797) 2026-02-25 17:25:31 +00:00
Anthony Stirling c9e7d9d6c9 deps (#5796) 2026-02-25 15:42:36 +00:00
Anthony StirlingandEthanHealy01 2bacb4dc81 cleanups (#5795)
Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
2026-02-25 15:19:23 +00:00
James Brunton c9dafc85fd Switch to use ESLint 10 (#5794) 2026-02-25 14:30:40 +00:00
5c39acecd8 Desktop connection SaaS: config, billing, team support (#5768)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: James Brunton <james@stirlingpdf.com>
2026-02-25 14:13:07 +00:00
Anthony StirlingandClaude Haiku 4.5 86072ec91a Cachefixing test (#5793)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-25 13:44:38 +00:00
dependabot[bot] c8081ac7cd build(deps): bump pillow from 12.1.0 to 12.1.1 in /testing/cucumber in the pip group across 1 directory (#5719)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 11:36:42 +00:00
dependabot[bot]andJames Brunton 9d93f20c39 build(deps-dev): bump pillow from 12.1.0 to 12.1.1 in /.github/scripts in the pip group across 1 directory (#5720)
Bumps the pip group with 1 update in the /.github/scripts directory:
[pillow](https://github.com/python-pillow/Pillow).

Updates `pillow` from 12.1.0 to 12.1.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/python-pillow/Pillow/releases">pillow's
releases</a>.</em></p>
<blockquote>
<h2>12.1.1</h2>
<p><a
href="https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html">https://pillow.readthedocs.io/en/stable/releasenotes/12.1.1.html</a></p>
<h2>Dependencies</h2>
<ul>
<li>Patch libavif for svt-av1 4.0 compatibility <a
href="https://redirect.github.com/python-pillow/Pillow/issues/9413">#9413</a>
[<a href="https://github.com/hugovk"><code>@​hugovk</code></a>]</li>
</ul>
<h2>Other changes</h2>
<ul>
<li>Fix OOB Write with invalid tile extents <a
href="https://redirect.github.com/python-pillow/Pillow/issues/9427">#9427</a>
[<a
href="https://github.com/radarhere"><code>@​radarhere</code></a>]</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/python-pillow/Pillow/commit/5158d98c807e719c5938aa3886913ef0ea6814e9"><code>5158d98</code></a>
12.1.1 version bump</li>
<li><a
href="https://github.com/python-pillow/Pillow/commit/9000313cc5d4a31bdcdd6d7f0781101abab553aa"><code>9000313</code></a>
Fix OOB Write with invalid tile extents (<a
href="https://redirect.github.com/python-pillow/Pillow/issues/9427">#9427</a>)</li>
<li><a
href="https://github.com/python-pillow/Pillow/commit/cd0111849fb32c40860e3ee3d57b9b1cee4260cf"><code>cd01118</code></a>
Patch libavif for svt-av1 4.0 compatibility</li>
<li>See full diff in <a
href="https://github.com/python-pillow/Pillow/compare/12.1.0...12.1.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pillow&package-manager=pip&previous-version=12.1.0&new-version=12.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-02-25 10:26:40 +00:00
James BruntonandAnthony Stirling eab84a13d0 Change to use dpdm for circular import scanning (#5788)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-24 23:05:23 +00:00
Anthony Stirling 1b3bfaec20 Remove push event from multiOSReleases workflow
Removed push event trigger for main and V2-master branches.
2026-02-24 21:02:20 +00:00
Anthony Stirling f7299bf89b disable other dockers (#5792) 2026-02-24 20:37:17 +00:00
Anthony Stirling abbd332909 zip and response issues (#5786) 2026-02-24 20:08:18 +00:00
Balázs SzücsandAnthony Stirling 1f9b90ad57 feat(docker): update base images to Java 25, Spring 4, Jackson 3, Gradle 9 and optimize JVM options (Project Lilliput) (#5725)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-24 20:06:32 +00:00
James Brunton 24128dd318 Sync up Tauri versions (#5789) 2026-02-24 18:43:40 +00:00
James Brunton 5b467d19c3 Update frontend minor package versions (#5787) 2026-02-24 14:40:19 +00:00
Balázs SzücsandJames Brunton 6000a2aaed feat(viewer): handle keyboard shortcuts for print, save, undo, etc. (#5748)
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2026-02-23 23:12:23 +00:00
stirlingbot[bot]andAnthony Stirling eaa01a5c23 Update Backend 3rd Party Licenses (#5781)
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-23 23:09:43 +00:00
Reece Browne 4f4d93d028 If in viewer load latest file in viewer (#5784) 2026-02-23 22:33:43 +00:00
Reece Browne d3494e3287 Fix export (#5782) 2026-02-23 22:15:32 +00:00
9b0610b2cc feat: split pdf into small chunks by pdfbox (#5718)
Co-authored-by: Ubuntu <ubuntu@vps-1aebde64.vps.ovh.ca>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-23 21:15:27 +00:00
James Brunton 73213901d1 Fix cookie consent reappearing on desktop builds (#5780)
# Description of Changes
Fix #5779. Cookie consent persistance doesn't work on desktop (on Mac at
least, not sure about Windows) because of permission differences with
Tauri. We are allowed to store things in local storage fine, so this
switches the cookie consent module to store in local storage for
desktop, and leaves it alone for web, where it already worked correctly.
2026-02-23 20:53:31 +00:00
Balázs Szücs 91b4a3484c feat(conversion): add PDF to Excel (XLSX) conversion (#5778) 2026-02-23 20:47:24 +00:00
Balázs Szücs 549f796e47 feat(form-fill): add CSV and XLSX extraction for form fields, improve file ID handling (#5776) 2026-02-23 20:17:58 +00:00
Balázs Szücs 340224b40b refactor(link-layer): migrate to EmbedPDF v2.6.2 annotation state for link rendering and improve link handling (#5760) 2026-02-23 13:34:05 +00:00
Anthony Stirling 30c258ce0b cucumber for days (#5766) 2026-02-21 23:17:28 +00:00
stirlingbot[bot]andAnthony Stirling 7631b222bd 🤖 format everything with pre-commit by stirlingbot (#5675)
Auto-generated by [create-pull-request][1] with **stirlingbot**

[1]: https://github.com/peter-evans/create-pull-request

---------

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-20 22:41:29 +00:00
Anthony Stirling f3f56d1d01 translations ai (please override as you see fit) (#5770)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 22:36:13 +00:00
Anthony Stirling 83169ed0f4 Move Forms location (#5769)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 22:35:35 +00:00
albanobattistellaandAnthony Stirling 46d511b8f6 Translate various terms in Italian localization (#5749)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-02-20 21:33:14 +00:00
Hugomaulwurf fd039b8649 Update de translation.toml (#5736)
Vollständige Übersetzung der fehlenden englischen Texte ins Deutsche:
- Schwärzungsfunktionen (Redactions)
- Mobile Upload/Scanner-Einstellungen
- Telegram-Bot-Konfiguration
- Benutzerkontoeinstellungen
- Stempel-Vorlagen und Tooltips
- API-Fehlermeldungen
- Verschiedene UI-Texte

Insgesamt 146 Strings übersetzt.

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 21:31:31 +00:00
Anthony Stirling 72cc500b3c zipFix (#5762)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 21:27:37 +00:00
Anthony Stirling 9a10fcb590 authclient (#5761)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 12:08:47 +00:00
Anthony Stirling 7959b3f2a4 licensere reTry (#5763)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 12:08:20 +00:00
Anthony Stirling dcda01c2b9 dos fixes (#5759)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-02-20 11:13:50 +00:00
LudyandCopilot 1ded9abb46 build(tauri): enforce Java 17+ requirement in Windows jlink build script (#5684)
# Description of Changes

This pull request improves the `scripts/build-tauri-jlink.bat` build
script by adding a check to ensure that Java 17 or higher is installed
before proceeding. This helps prevent build errors due to incompatible
Java versions.

Build process enhancements:

* Added logic to parse the installed Java version and verify that it is
at least version 17; the script now exits with an error message if an
older version is detected.
* Updated status messages to clearly indicate the Java version being
used and to provide feedback if requirements are not met.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 11:12:27 +00:00
2450 changed files with 301449 additions and 90222 deletions
+97 -62
View File
@@ -1,75 +1,110 @@
# Node modules and build artifacts
node_modules
frontend/node_modules
frontend/dist
frontend/build
frontend/.vite
frontend/.tauri
frontend/src-tauri/target
# Gradle build artifacts
.gradle
build
bin
target
out
# Git
.git
# Version control
.git/
.gitignore
.git-blame-ignore-revs
.gitattributes
# IDE
.vscode
.idea
# Build outputs
build/
*/build/
**/build/
out/
target/
**/target/
bin/
version_builds/
# Gradle caches (local, not what's in the container)
.gradle/
**/.gradle/
.gradle-home/
# Task (go-task) cache
.task/
# Node / frontend
node_modules/
**/node_modules/
frontend/node_modules/
frontend/dist/
frontend/playwright-report/
.npm/
.yarn/
# Tauri/desktop builds
src-tauri/target/
src-tauri/dist/
frontend/src-tauri/target/
frontend/src-tauri/dist/
# IDE and editor
.idea/
.vscode/
.settings/
.settings.zip
.classpath
.project
.devcontainer/
*.iml
*.iws
*.ipr
*.iws
# Logs
# Logs and temp files
*.log
logs
*.tmp
*.pid
.DS_Store
Thumbs.db
logs/
# Environment files
# Docker itself
Dockerfile*
.dockerignore
# CI / CD configs (not needed in build context)
.github/
.circleci/
.gitlab-ci.yml
# Test reports
**/test-results/
**/jacoco/
test_*.pdf
# Testing and documentation (not needed in build)
testing/
docs/
devGuide/
devTools/
*.md
README*
# Separate projects not consumed by the Java/frontend build
commonforms-onnx/
# Runtime mount points used by docker-compose volumes, not build input
stirling/
customFiles/
configs/
# Claude Code workspace
.claude/
# Python caches
.pytest_cache/
.ruff_cache/
__pycache__/
**/__pycache__/
# Local env
.env
.env.*
!.env.example
!engine/.env
# OS files
.DS_Store
Thumbs.db
# Java compiled files
*.class
*.jar
*.war
*.ear
# Test reports
test-results
coverage
# Docker
docker-compose.override.yml
.dockerignore
# Temporary files
tmp
temp
*.tmp
# Misc
*.swp
*.swo
*~
# Runtime database and config files (locked by running app)
app/core/configs/**
stirling/**
stirling-pdf-DB*.mv.db
stirling-pdf-DB*.trace.db
# Documentation
*.md
!README.md
docs
# CI/CD
.github
.gitlab-ci.yml
.DS_Store
.cache/
+2 -1
View File
@@ -14,7 +14,8 @@ indent_size = 4
max_line_length = 100
[*.py]
indent_size = 2
indent_size = 4
max_line_length = 120
[*.gradle]
indent_size = 4
+29
View File
@@ -0,0 +1,29 @@
# Maintainer: Stirling PDF Inc <contact@stirlingpdf.com>
pkgname=stirling-pdf-desktop
pkgver=2.10.1
pkgrel=1
pkgdesc="Locally hosted, web-based PDF manipulation tool (Tauri desktop app, official Stirling PDF Inc build)"
arch=('x86_64')
url="https://www.stirling.com"
license=('MIT' 'LicenseRef-Stirling-PDF-Proprietary')
depends=('gtk3' 'webkit2gtk-4.1' 'libappindicator-gtk3')
provides=('stirling-pdf')
conflicts=('stirling-pdf' 'stirling-pdf-git' 'stirling-pdf-bin')
options=('!strip')
source_x86_64=("${pkgname}-${pkgver}.deb::https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${pkgver}/Stirling-PDF-linux-x86_64.deb")
sha256sums_x86_64=('PLACEHOLDER_DEB_SHA256')
package() {
# Extract the .deb archive
bsdtar -xf data.tar* -C "${pkgdir}"
# Fix permissions
find "${pkgdir}" -type d -exec chmod 755 {} \;
# Install license
install -Dm644 /dev/stdin "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" <<EOF
Copyright (c) 2025 Stirling PDF Inc
All rights reserved. See https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LICENSE
EOF
}
@@ -0,0 +1,77 @@
# Maintainer: Stirling PDF Inc <contact@stirlingpdf.com>
pkgname=stirling-pdf-server-bin
pkgver=2.10.1
pkgrel=1
pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)"
arch=('any')
url="https://www.stirling.com"
license=('MIT' 'LicenseRef-Stirling-PDF-Proprietary')
depends=('java-runtime>=25')
provides=('stirling-pdf-server')
conflicts=('stirling-pdf-server' 'stirling-pdf-server-git')
backup=('etc/stirling-pdf-server/settings.yml')
source=("Stirling-PDF-with-login-${pkgver}.jar::https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${pkgver}/Stirling-PDF-with-login.jar")
sha256sums=('PLACEHOLDER_JAR_SHA256')
package() {
# JAR
install -Dm644 "Stirling-PDF-with-login-${pkgver}.jar" \
"${pkgdir}/usr/share/stirling-pdf-server/stirling-pdf-server.jar"
# Wrapper script
install -Dm755 /dev/stdin "${pkgdir}/usr/bin/stirling-pdf-server" << 'EOF'
#!/bin/sh
exec java $JAVA_OPTS -jar /usr/share/stirling-pdf-server/stirling-pdf-server.jar "$@"
EOF
# systemd unit
install -Dm644 /dev/stdin "${pkgdir}/usr/lib/systemd/system/stirling-pdf-server.service" << 'EOF'
[Unit]
Description=Stirling-PDF Server
After=network.target
[Service]
Type=simple
User=stirling-pdf
Group=stirling-pdf
WorkingDirectory=/var/lib/stirling-pdf-server
ExecStart=/usr/bin/java -jar /usr/share/stirling-pdf-server/stirling-pdf-server.jar
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=stirling-pdf-server
Environment=JAVA_OPTS=-Xmx512m
[Install]
WantedBy=multi-user.target
EOF
# sysusers
install -Dm644 /dev/stdin "${pkgdir}/usr/lib/sysusers.d/stirling-pdf-server.conf" << 'EOF'
u stirling-pdf - "Stirling-PDF Server" /var/lib/stirling-pdf-server -
EOF
# tmpfiles
install -Dm644 /dev/stdin "${pkgdir}/usr/lib/tmpfiles.d/stirling-pdf-server.conf" << 'EOF'
d /var/lib/stirling-pdf-server 0750 stirling-pdf stirling-pdf -
d /var/log/stirling-pdf-server 0750 stirling-pdf stirling-pdf -
EOF
# Default config stub
install -dm755 "${pkgdir}/etc/stirling-pdf-server"
install -Dm644 /dev/stdin "${pkgdir}/etc/stirling-pdf-server/settings.yml" << 'EOF'
# Stirling-PDF Server configuration
# See https://github.com/Stirling-Tools/Stirling-PDF for all options
server:
port: 8080
EOF
# License
install -Dm644 /dev/stdin "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" << 'EOF'
MIT License with proprietary carve-outs (open-core).
SPDX: MIT AND LicenseRef-Stirling-PDF-Proprietary
See https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LICENSE
EOF
}
+50 -6
View File
@@ -6,14 +6,20 @@ openapi: &openapi
- *build
- app/(common|core|proprietary)/src/main/java/**
docker-base: &docker-base
- docker/base/Dockerfile
- ".github/workflows/push-docker-base.yml"
docker: &docker
- Dockerfile
- Dockerfile.fat
- Dockerfile.ultra-lite
- docker/embedded/Dockerfile
- docker/embedded/Dockerfile.fat
- docker/embedded/Dockerfile.ultra-lite
- ".github/workflows/build.yml"
- ".github/workflows/push-docker.yml"
- scripts/init.sh
- scripts/init-without-ocr.sh
- exampleYmlFiles/**
- *docker-base
project: &project
- app/(common|core|proprietary)/src/(main|test)/java/**
@@ -24,6 +30,7 @@ project: &project
- libs/**
- "testing/**/!(requirements*.txt|requirements*.in)*"
- *docker
- *docker-base
- gradle.properties
- gradlew
- gradlew.bat
@@ -39,8 +46,7 @@ frontend: &frontend
- testing/**
- docker/**
- scripts/translations/*.py
- scripts/build-tauri-jlink.bat
- scripts/build-tauri-jlink.sh
- .taskfiles/desktop.yml
- scripts/convert_cff_to_ttf.py
- scripts/harvest_type3_fonts.py
- scripts/ignore_translation.toml
@@ -49,6 +55,25 @@ frontend: &frontend
- scripts/type3_to_cff.py
- scripts/update_type3_library.py
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
# job on changes to any of these.
tauri: &tauri
- frontend/src-tauri/**
- frontend/src/desktop/**
- frontend/tsconfig.desktop.json
- frontend/package.json
- frontend/package-lock.json
- frontend/vite.config.ts
- .github/workflows/tauri-build.yml
# Files that affect the AI engine (Python tool models, fixers, tests). Gate
# the engine validation job on changes to engine sources or to the Java
# tool surfaces it generates models from.
engine: &engine
- engine/**
- app/(common|core|proprietary)/src/main/java/**
- .github/workflows/ai-engine.yml
licenses-frontend: &licenses-frontend
- ".github/workflows/frontend-backend-licenses-update.yml"
- "frontend/package.json"
@@ -57,4 +82,23 @@ licenses-frontend: &licenses-frontend
licenses-backend: &licenses-backend
- ".github/workflows/frontend-backend-licenses-update.yml"
- *build
- *build
# Files that can affect premium / enterprise behaviour. Gate the enterprise
# Playwright job on changes to any of these on PRs.
proprietary: &proprietary
- app/proprietary/**
- frontend/src/proprietary/**
- frontend/src/core/tests/enterprise/**
- testing/compose/docker-compose-keycloak-oauth.yml
- testing/compose/docker-compose-keycloak-saml.yml
- testing/compose/keycloak-realm-oauth.json
- testing/compose/keycloak-realm-saml.json
- testing/compose/start-oauth-test.sh
- testing/compose/start-saml-test.sh
- testing/compose/validate-oauth-test.sh
- testing/compose/validate-saml-test.sh
- configs/settings.yml.template
- build.gradle
- app/proprietary/build.gradle
- .github/workflows/build-enterprise.yml
+119 -27
View File
@@ -13,58 +13,150 @@ updates:
- "/app/proprietary"
schedule:
interval: "weekly"
cooldown:
default-days: 7
rebase-strategy: "auto"
- package-ecosystem: "docker"
directory: "/" # Location of Dockerfile
directories:
- "/" # Location of Dockerfile
- "/docker/backend"
- "/docker/embedded"
- "/docker/frontend"
- "/docker/base"
- "/docker/engine"
schedule:
interval: "weekly"
cooldown:
default-days: 7
rebase-strategy: "auto"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
rebase-strategy: "auto"
- package-ecosystem: npm
directory: /devTools
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: docker
directory: /docker/backend
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: docker
directory: /docker/embedded
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: docker
directory: /docker/frontend
schedule:
interval: "weekly"
rebase-strategy: "auto"
- package-ecosystem: npm
directory: /frontend
directories:
- /devTools
- /frontend
schedule:
interval: "weekly"
cooldown:
default-days: 7
rebase-strategy: "auto"
groups:
embedpdf:
patterns:
- "@embedpdf/*"
mantine:
patterns:
- "@mantine/*"
- "postcss-preset-mantine"
mui:
patterns:
- "@mui/*"
tauri-js:
patterns:
- "@tauri-apps/*"
emotion:
patterns:
- "@emotion/*"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
typescript-eslint:
patterns:
- "@typescript-eslint/*"
- "typescript-eslint"
eslint:
patterns:
- "eslint"
- "@eslint/*"
vite:
patterns:
- "vite"
- "vite-*"
- "@vitejs/*"
vitest:
patterns:
- "vitest"
- "@vitest/*"
testing-library:
patterns:
- "@testing-library/*"
i18next:
patterns:
- "i18next"
- "i18next-*"
- "react-i18next"
iconify:
patterns:
- "@iconify/*"
- "@iconify-json/*"
stripe:
patterns:
- "@stripe/*"
posthog:
patterns:
- "@posthog/*"
- "posthog-js"
supabase:
patterns:
- "@supabase/*"
dnd-kit:
patterns:
- "@dnd-kit/*"
tailwind:
patterns:
- "tailwindcss"
- "@tailwindcss/*"
postcss:
patterns:
- "postcss"
- "postcss-*"
exclude-patterns:
- "postcss-preset-mantine"
- package-ecosystem: cargo
directory: /frontend/src-tauri
directories:
- /frontend/src-tauri
- /frontend/src-tauri/thumbnail-handler
- /frontend/src-tauri/provisioner
schedule:
interval: "weekly"
cooldown:
default-days: 7
rebase-strategy: "auto"
groups:
tauri:
patterns:
- "tauri"
- "tauri-build"
- "tauri-plugin-*"
serde:
patterns:
- "serde"
- "serde_*"
tracing:
patterns:
- "tracing"
- "tracing-*"
tokio:
patterns:
- "tokio"
- "tokio-*"
- package-ecosystem: pip
directory: /testing/cucumber
schedule:
interval: "weekly"
cooldown:
default-days: 7
rebase-strategy: "auto"
+6
View File
@@ -72,6 +72,12 @@ labels:
- 'frontend/src-tauri/**'
- 'frontend/src-tauri/.*'
- label: 'engine'
files:
- 'engine/**'
- 'engine/.*'
- 'engine/**/.*'
- label: 'Java'
files:
- 'app/common/src/main/java/.*.java'
+3 -2
View File
@@ -17,7 +17,7 @@ Closes #(issue_number)
### General
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable)
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable)
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings
@@ -37,4 +37,5 @@ Closes #(issue_number)
### Testing (if applicable)
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
- [ ] I have run `task check` to verify linters, typechecks, and tests pass
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details.
+15 -2
View File
@@ -1,4 +1,11 @@
changelog:
exclude:
labels:
- ignore-for-release
# engine: Python AI engine - not currently live / not yet shipped to users.
# Remove this entry once the engine is released.
- engine
categories:
- title: Breaking Changes
labels:
@@ -6,16 +13,22 @@ changelog:
- title: Bug Fixes
labels:
- Bugfix
- Bug
- title: Enhancements
labels:
- enhancement
- Java
- Front End
- Back End
- Tauri
- title: Minor Enhancements
labels:
- Java
- Front End
- chore
- style
- refactor
- title: Docker Updates
labels:
+278 -270
View File
@@ -101,269 +101,273 @@ cfgv==3.5.0 \
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
# via pre-commit
cssselect2==0.8.0 \
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
--hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a
cssselect2==0.9.0 \
--hash=sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 \
--hash=sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb
# via weasyprint
distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
# via virtualenv
fonttools==4.61.1 \
--hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \
--hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \
--hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \
--hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \
--hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \
--hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \
--hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \
--hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \
--hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \
--hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \
--hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \
--hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \
--hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \
--hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \
--hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \
--hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \
--hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \
--hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \
--hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \
--hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \
--hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \
--hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \
--hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \
--hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \
--hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \
--hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \
--hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \
--hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \
--hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \
--hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \
--hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \
--hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \
--hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \
--hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \
--hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \
--hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \
--hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \
--hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \
--hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \
--hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \
--hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \
--hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \
--hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \
--hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \
--hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \
--hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \
--hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \
--hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \
--hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \
--hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd
filelock==3.29.0 \
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
# via
# python-discovery
# virtualenv
fonttools==4.62.1 \
--hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \
--hash=sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a \
--hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \
--hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \
--hash=sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82 \
--hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \
--hash=sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b \
--hash=sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e \
--hash=sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416 \
--hash=sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae \
--hash=sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069 \
--hash=sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9 \
--hash=sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 \
--hash=sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed \
--hash=sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800 \
--hash=sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e \
--hash=sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9 \
--hash=sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b \
--hash=sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 \
--hash=sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe \
--hash=sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7 \
--hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \
--hash=sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 \
--hash=sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23 \
--hash=sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae \
--hash=sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260 \
--hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \
--hash=sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87 \
--hash=sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24 \
--hash=sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53 \
--hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \
--hash=sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14 \
--hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \
--hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \
--hash=sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1 \
--hash=sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca \
--hash=sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c \
--hash=sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d \
--hash=sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a \
--hash=sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782 \
--hash=sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c \
--hash=sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a \
--hash=sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 \
--hash=sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3 \
--hash=sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7 \
--hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d \
--hash=sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 \
--hash=sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4 \
--hash=sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68 \
--hash=sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca
# via weasyprint
identify==2.6.16 \
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
identify==2.6.19 \
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
# via pre-commit
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
# via pre-commit
numpy==2.4.1 \
--hash=sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c \
--hash=sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba \
--hash=sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5 \
--hash=sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8 \
--hash=sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0 \
--hash=sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d \
--hash=sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574 \
--hash=sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696 \
--hash=sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5 \
--hash=sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505 \
--hash=sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0 \
--hash=sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162 \
--hash=sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844 \
--hash=sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205 \
--hash=sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4 \
--hash=sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc \
--hash=sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d \
--hash=sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93 \
--hash=sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01 \
--hash=sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c \
--hash=sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f \
--hash=sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33 \
--hash=sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82 \
--hash=sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2 \
--hash=sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42 \
--hash=sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509 \
--hash=sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a \
--hash=sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e \
--hash=sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556 \
--hash=sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a \
--hash=sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510 \
--hash=sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295 \
--hash=sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73 \
--hash=sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3 \
--hash=sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9 \
--hash=sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8 \
--hash=sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745 \
--hash=sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2 \
--hash=sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02 \
--hash=sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d \
--hash=sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344 \
--hash=sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f \
--hash=sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be \
--hash=sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425 \
--hash=sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1 \
--hash=sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2 \
--hash=sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2 \
--hash=sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb \
--hash=sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9 \
--hash=sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15 \
--hash=sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690 \
--hash=sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0 \
--hash=sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261 \
--hash=sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a \
--hash=sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc \
--hash=sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f \
--hash=sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5 \
--hash=sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df \
--hash=sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9 \
--hash=sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2 \
--hash=sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8 \
--hash=sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426 \
--hash=sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b \
--hash=sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87 \
--hash=sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220 \
--hash=sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b \
--hash=sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3 \
--hash=sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e \
--hash=sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501 \
--hash=sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee \
--hash=sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7 \
--hash=sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c
numpy==2.4.4 \
--hash=sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed \
--hash=sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50 \
--hash=sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959 \
--hash=sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827 \
--hash=sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd \
--hash=sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233 \
--hash=sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc \
--hash=sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b \
--hash=sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7 \
--hash=sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e \
--hash=sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a \
--hash=sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d \
--hash=sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3 \
--hash=sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e \
--hash=sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb \
--hash=sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a \
--hash=sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0 \
--hash=sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e \
--hash=sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113 \
--hash=sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103 \
--hash=sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93 \
--hash=sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af \
--hash=sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5 \
--hash=sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7 \
--hash=sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392 \
--hash=sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c \
--hash=sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4 \
--hash=sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40 \
--hash=sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf \
--hash=sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44 \
--hash=sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b \
--hash=sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5 \
--hash=sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e \
--hash=sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74 \
--hash=sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0 \
--hash=sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e \
--hash=sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec \
--hash=sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015 \
--hash=sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d \
--hash=sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d \
--hash=sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842 \
--hash=sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150 \
--hash=sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8 \
--hash=sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a \
--hash=sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed \
--hash=sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f \
--hash=sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008 \
--hash=sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e \
--hash=sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0 \
--hash=sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e \
--hash=sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f \
--hash=sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a \
--hash=sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40 \
--hash=sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7 \
--hash=sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83 \
--hash=sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d \
--hash=sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c \
--hash=sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871 \
--hash=sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502 \
--hash=sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252 \
--hash=sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8 \
--hash=sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115 \
--hash=sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f \
--hash=sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e \
--hash=sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d \
--hash=sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0 \
--hash=sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119 \
--hash=sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e \
--hash=sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db \
--hash=sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121 \
--hash=sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d \
--hash=sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e
# via opencv-python-headless
opencv-python-headless==4.13.0.90 \
--hash=sha256:0e0c8c9f620802fddc4fa7f471a1d263c7b0dca16cd9e7e2f996bb8bd2128c0c \
--hash=sha256:12a28674f215542c9bf93338de1b5bffd76996d32da9acb9e739fdb9c8bbd738 \
--hash=sha256:32255203040dc98803be96362e13f9e4bce20146898222d2e5c242f80de50da5 \
--hash=sha256:96060fc57a1abb1144b0b8129e2ff3bfcdd0ccd8e8bd05bd85256ff4ed587d3b \
--hash=sha256:dbc1f4625e5af3a80ebdbd84380227c0f445228588f2521b11af47710caca1ba \
--hash=sha256:e13790342591557050157713af17a7435ac1b50c65282715093c9297fa045d8f \
--hash=sha256:eba38bc255d0b7d1969c5bcc90a060ca2b61a3403b613872c750bfa5dfe9e03b \
--hash=sha256:f46b17ea0aa7e4124ca6ad71143f89233ae9557f61d2326bcdb34329a1ddf9bd
opencv-python-headless==4.13.0.92 \
--hash=sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22 \
--hash=sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e \
--hash=sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209 \
--hash=sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c \
--hash=sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb \
--hash=sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6 \
--hash=sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b \
--hash=sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d
# via -r .github/scripts/requirements_dev.in
pdf2image==1.17.0 \
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
# via -r .github/scripts/requirements_dev.in
pillow==12.1.0 \
--hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \
--hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \
--hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \
--hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \
--hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \
--hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \
--hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \
--hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \
--hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \
--hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \
--hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \
--hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \
--hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \
--hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \
--hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \
--hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \
--hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \
--hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \
--hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \
--hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \
--hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \
--hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \
--hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \
--hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \
--hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \
--hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \
--hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \
--hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \
--hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \
--hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \
--hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \
--hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \
--hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \
--hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \
--hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \
--hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \
--hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \
--hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \
--hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \
--hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \
--hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \
--hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \
--hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \
--hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \
--hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \
--hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \
--hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \
--hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \
--hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \
--hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \
--hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \
--hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \
--hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \
--hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \
--hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \
--hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \
--hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \
--hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \
--hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \
--hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \
--hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \
--hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \
--hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \
--hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \
--hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \
--hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \
--hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \
--hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \
--hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \
--hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \
--hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \
--hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \
--hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \
--hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \
--hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \
--hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \
--hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \
--hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \
--hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \
--hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \
--hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \
--hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \
--hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \
--hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \
--hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \
--hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \
--hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \
--hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \
--hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \
--hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \
--hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd
pillow==12.2.0 \
--hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \
--hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \
--hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \
--hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \
--hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \
--hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \
--hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \
--hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \
--hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \
--hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \
--hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \
--hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \
--hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \
--hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \
--hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \
--hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \
--hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \
--hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \
--hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \
--hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \
--hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \
--hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \
--hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \
--hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \
--hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \
--hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \
--hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \
--hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \
--hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \
--hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \
--hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \
--hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \
--hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \
--hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \
--hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \
--hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \
--hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \
--hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \
--hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \
--hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \
--hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \
--hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \
--hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \
--hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \
--hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \
--hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \
--hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \
--hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \
--hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \
--hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \
--hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \
--hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \
--hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \
--hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \
--hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \
--hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \
--hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \
--hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \
--hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \
--hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \
--hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \
--hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \
--hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \
--hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \
--hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \
--hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \
--hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \
--hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \
--hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \
--hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \
--hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \
--hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \
--hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \
--hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \
--hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \
--hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \
--hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \
--hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \
--hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \
--hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \
--hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \
--hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \
--hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \
--hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \
--hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \
--hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \
--hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \
--hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \
--hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \
--hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \
--hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5
# via
# -r .github/scripts/requirements_dev.in
# pdf2image
# weasyprint
platformdirs==4.5.1 \
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
# via virtualenv
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
platformdirs==4.9.6 \
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
# via
# python-discovery
# virtualenv
pre-commit==4.6.0 \
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
# via -r .github/scripts/requirements_dev.in
pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
@@ -377,6 +381,10 @@ pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
# via weasyprint
python-discovery==1.2.2 \
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
# via virtualenv
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
@@ -458,21 +466,21 @@ tinycss2==1.5.1 \
# via
# cssselect2
# weasyprint
tinyhtml5==2.0.0 \
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
tinyhtml5==2.1.0 \
--hash=sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67 \
--hash=sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a
# via weasyprint
unoserver==3.6 \
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
# via -r .github/scripts/requirements_dev.in
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
virtualenv==21.2.4 \
--hash=sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac \
--hash=sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada
# via pre-commit
weasyprint==68.0 \
--hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \
--hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1
weasyprint==68.1 \
--hash=sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be \
--hash=sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e
# via -r .github/scripts/requirements_dev.in
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
@@ -481,27 +489,27 @@ webencodings==0.5.1 \
# cssselect2
# tinycss2
# tinyhtml5
zopfli==0.4.0 \
--hash=sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1 \
--hash=sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb \
--hash=sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f \
--hash=sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85 \
--hash=sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1 \
--hash=sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6 \
--hash=sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3 \
--hash=sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7 \
--hash=sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941 \
--hash=sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce \
--hash=sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a \
--hash=sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975
zopfli==0.4.1 \
--hash=sha256:02086247dd12fda929f9bfe8b3962b6bcdbfc8c82e99255aebcf367867cf0760 \
--hash=sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e \
--hash=sha256:27823dc1161a4031d1c25925fd45d9868ec0cbc7692341830a7dcfa25063662c \
--hash=sha256:2f992ac7d83cbddd889e1813ace576cbc91a05d5d7a0a21b366e2e5f492e7707 \
--hash=sha256:4238d4d746d1095e29c9125490985e0c12ffd3654f54a24af551e2391e936d54 \
--hash=sha256:5a4c22b6161f47f5bd34637dbaee6735abd287cd64e0d1ce28ef1871bf625f4b \
--hash=sha256:84a31ba9edc921b1d3a4449929394a993888f32d70de3a3617800c428a947b9b \
--hash=sha256:a899eca405662a23ae75054affa3517a060362eae1185d3d791c86a50153c4dd \
--hash=sha256:a93c2ecafff372de6c0aa2212eff18a75f6c71a100372fee7b4b129cc0b6f9a7 \
--hash=sha256:cb136a74d14a4ecfae29cb0fdecece58a6c115abc9a74c12bc6ac62e80f229d7 \
--hash=sha256:d7bcee1b189d64ec33d1e05cfa1b6a1268c29329c382f6ca1bd6245b04925c57 \
--hash=sha256:fdfb7ce9f5de37a5b2f75dd2642fd7717956ef2a72e0387302a36d382440db07
# via fonttools
# The following packages are considered to be unsafe in a requirements file:
pip==26.0 \
--hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \
--hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754
pip==26.0.1 \
--hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \
--hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8
# via -r .github/scripts/requirements_dev.in
setuptools==80.10.2 \
--hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
--hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
setuptools==82.0.1 \
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
# via -r .github/scripts/requirements_dev.in
+25 -17
View File
@@ -12,26 +12,34 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
# via virtualenv
identify==2.6.16 \
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
filelock==3.29.0 \
--hash=sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90 \
--hash=sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258
# via
# python-discovery
# virtualenv
identify==2.6.19 \
--hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \
--hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842
# via pre-commit
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
# via pre-commit
platformdirs==4.5.1 \
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
# via virtualenv
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
platformdirs==4.9.6 \
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
# via
# python-discovery
# virtualenv
pre-commit==4.6.0 \
--hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \
--hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b
# via -r .github/scripts/requirements_pre_commit.in
python-discovery==1.2.2 \
--hash=sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb \
--hash=sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a
# via virtualenv
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
@@ -107,7 +115,7 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
virtualenv==21.2.4 \
--hash=sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac \
--hash=sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada
# via pre-commit
+12 -10
View File
@@ -35,13 +35,13 @@ jobs:
pr_ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Resolve PR info
id: resolve
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
@@ -111,7 +111,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -132,7 +132,7 @@ jobs:
- name: Add deployment started comment
id: deployment-started
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -180,7 +180,7 @@ jobs:
fetch-depth: 0 # Fetch full history for commit hash detection
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Get version number
id: versionNumber
@@ -189,7 +189,7 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -226,11 +226,13 @@ jobs:
- name: Build and push V2 image
if: steps.check-image.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }}
build-args: VERSION_TAG=v2-alpha
platforms: linux/amd64
@@ -307,7 +309,7 @@ jobs:
- name: Post V2 deployment URL to PR
if: success()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -357,7 +359,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -374,7 +376,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Clean up V2 deployment comments
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
+163 -59
View File
@@ -3,6 +3,31 @@ name: PR Deployment via Comment
on:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: "PR number to deploy"
required: true
enable_prototypes:
description: "Build with prototypes frontend"
required: false
type: boolean
default: false
enable_pro:
description: "Enable pro features"
required: false
type: boolean
default: false
enable_enterprise:
description: "Enable enterprise features"
required: false
type: boolean
default: false
disable_security:
description: "Disable security/login"
required: false
type: boolean
default: true
permissions:
contents: read
@@ -14,23 +39,27 @@ jobs:
permissions:
issues: write
if: |
vars.CI_PROFILE != 'lite' &&
github.event.issue.pull_request &&
(
contains(github.event.comment.body, 'prdeploy') ||
contains(github.event.comment.body, 'deploypr')
)
&&
(
github.event.comment.user.login == 'frooodle' ||
github.event.comment.user.login == 'sf298' ||
github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'balazs-szucs' ||
github.event.comment.user.login == 'reecebrowne' ||
github.event.comment.user.login == 'DarioGii' ||
github.event.comment.user.login == 'EthanHealy01' ||
github.event.comment.user.login == 'jbrunton96' ||
github.event.comment.user.login == 'ConnorYoh'
vars.CI_PROFILE != 'lite' && (
github.event_name == 'workflow_dispatch' ||
(
github.event.issue.pull_request &&
(
contains(github.event.comment.body, 'prdeploy') ||
contains(github.event.comment.body, 'deploypr')
)
&&
(
github.event.comment.user.login == 'frooodle' ||
github.event.comment.user.login == 'sf298' ||
github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'balazs-szucs' ||
github.event.comment.user.login == 'reecebrowne' ||
github.event.comment.user.login == 'DarioGii' ||
github.event.comment.user.login == 'EthanHealy01' ||
github.event.comment.user.login == 'jbrunton96' ||
github.event.comment.user.login == 'ConnorYoh'
)
)
)
outputs:
pr_number: ${{ steps.get-pr.outputs.pr_number }}
@@ -38,9 +67,10 @@ jobs:
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -58,10 +88,12 @@ jobs:
- name: Get PR data
id: get-pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const prNumber = context.payload.issue.number;
const prNumber = context.eventName === 'workflow_dispatch'
? context.payload.inputs.pr_number
: context.payload.issue.number;
console.log(`PR Number: ${prNumber}`);
core.setOutput('pr_number', prNumber);
@@ -69,12 +101,14 @@ jobs:
id: check-security-flag
env:
COMMENT_BODY: ${{ github.event.comment.body }}
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
DISPATCH_DISABLE_SECURITY: ${{ inputs.disable_security }}
run: |
if [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
echo "Security flags detected in comment"
if [[ "$IS_DISPATCH" == "true" ]]; then
echo "disable_security=$DISPATCH_DISABLE_SECURITY" >> $GITHUB_OUTPUT
elif [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
echo "disable_security=false" >> $GITHUB_OUTPUT
else
echo "No security flags detected in comment"
echo "disable_security=true" >> $GITHUB_OUTPUT
fi
@@ -82,24 +116,45 @@ jobs:
id: check-pro-flag
env:
COMMENT_BODY: ${{ github.event.comment.body }}
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
DISPATCH_PRO: ${{ inputs.enable_pro }}
DISPATCH_ENTERPRISE: ${{ inputs.enable_enterprise }}
run: |
if [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
echo "pro flags detected in comment"
if [[ "$IS_DISPATCH" == "true" ]]; then
echo "enable_pro=$DISPATCH_PRO" >> $GITHUB_OUTPUT
echo "enable_enterprise=$DISPATCH_ENTERPRISE" >> $GITHUB_OUTPUT
elif [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
echo "enable_pro=true" >> $GITHUB_OUTPUT
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then
echo "enterprise flags detected in comment"
echo "enable_enterprise=true" >> $GITHUB_OUTPUT
echo "enable_pro=true" >> $GITHUB_OUTPUT
else
echo "No pro or enterprise flags detected in comment"
echo "enable_pro=false" >> $GITHUB_OUTPUT
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
fi
- name: Check for prototypes flag
id: check-prototypes-flag
env:
COMMENT_BODY: ${{ github.event.comment.body }}
IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }}
DISPATCH_PROTOTYPES: ${{ inputs.enable_prototypes }}
run: |
if [[ "$IS_DISPATCH" == "true" ]]; then
echo "enable_prototypes=$DISPATCH_PROTOTYPES" >> $GITHUB_OUTPUT
elif [[ "$COMMENT_BODY" == *"prototypes"* ]]; then
echo "Prototypes flag detected in comment"
echo "enable_prototypes=true" >> $GITHUB_OUTPUT
else
echo "No prototypes flag detected in comment"
echo "enable_prototypes=false" >> $GITHUB_OUTPUT
fi
- name: Add 'in_progress' reaction to comment
if: github.event_name == 'issue_comment'
id: add-eyes-reaction
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -128,7 +183,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -150,17 +205,19 @@ jobs:
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
token: ${{ steps.setup-bot.outputs.token }}
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Run Gradle Command
run: |
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
@@ -168,7 +225,7 @@ jobs:
else
export DISABLE_ADDITIONAL_FEATURES=false
fi
./gradlew build
task backend:build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
@@ -176,22 +233,38 @@ jobs:
STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=alpha
build-args: |
VERSION_TAG=alpha
PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }}
platforms: linux/amd64
- name: Build and push engine image
if: needs.check-comment.outputs.enable_prototypes == 'true'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: ./engine
file: ./engine/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-engine
cache-to: type=gha,mode=max,scope=stirling-pdf-engine
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }}
platforms: linux/amd64
- name: Set up SSH
@@ -229,33 +302,64 @@ jobs:
PREMIUM_PROFEATURES_AUDIT_ENABLED="false"
fi
ENABLE_PROTOTYPES="${{ needs.check-comment.outputs.enable_prototypes }}"
PR_NUMBER="${{ needs.check-comment.outputs.pr_number }}"
DOCKER_USER="${{ secrets.DOCKER_HUB_USERNAME }}"
# Build engine env vars for backend (only set when prototypes enabled)
if [ "$ENABLE_PROTOTYPES" == "true" ]; then
AI_ENGINE_VARS="
SYSTEM_AIENGINE_ENABLED: \"true\"
SYSTEM_AIENGINE_URL: \"http://stirling-pdf-engine-pr-${PR_NUMBER}:5001\""
ENGINE_SERVICE="
stirling-pdf-engine:
container_name: stirling-pdf-engine-pr-${PR_NUMBER}
image: ${DOCKER_USER}/test:engine-pr-${PR_NUMBER}
environment:
ANTHROPIC_API_KEY: \"${{ secrets.ANTHROPIC_API_KEY }}\"
networks:
- pr-network
restart: on-failure:5"
NETWORK_SECTION="
networks:
pr-network:"
BACKEND_NETWORK="
networks:
- pr-network"
else
AI_ENGINE_VARS=""
ENGINE_SERVICE=""
NETWORK_SECTION=""
BACKEND_NETWORK=""
fi
# First create the docker-compose content locally
cat > docker-compose.yml << EOF
version: '3.3'
services:
stirling-pdf:
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
container_name: stirling-pdf-pr-${PR_NUMBER}
image: ${DOCKER_USER}/test:pr-${PR_NUMBER}
ports:
- "${{ needs.check-comment.outputs.pr_number }}:8080"
- "${PR_NUMBER}:8080"
volumes:
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
- /stirling/PR-${PR_NUMBER}/data:/usr/share/tessdata:rw
- /stirling/PR-${PR_NUMBER}/config:/configs:rw
- /stirling/PR-${PR_NUMBER}/logs:/logs:rw
environment:
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
UI_APPNAME: "Stirling-PDF PR#${PR_NUMBER}"
UI_HOMEDESCRIPTION: "PR#${PR_NUMBER} for Stirling-PDF Latest"
UI_APPNAMENAVBAR: "PR#${PR_NUMBER}"
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false"
PREMIUM_KEY: "${PREMIUM_KEY}"
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"
restart: on-failure:5
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"${AI_ENGINE_VARS}
restart: on-failure:5${BACKEND_NETWORK}${ENGINE_SERVICE}${NETWORK_SECTION}
EOF
# Then copy the file and execute commands
@@ -263,13 +367,13 @@ jobs:
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH
# Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs}
# Move docker-compose file to correct location
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
mv /tmp/docker-compose.yml /stirling/PR-${PR_NUMBER}/docker-compose.yml
# Start or restart the container
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
cd /stirling/PR-${PR_NUMBER}
docker-compose pull
docker-compose up -d
ENDSSH
@@ -278,8 +382,8 @@ jobs:
echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV
- name: Add success reaction to comment
if: success()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: success() && github.event_name == 'issue_comment'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -313,8 +417,8 @@ jobs:
}
- name: Add failure reaction to comment
if: failure()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
if: failure() && github.event_name == 'issue_comment'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -334,7 +438,7 @@ jobs:
- name: Post deployment URL to PR
if: success()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -370,7 +474,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -385,7 +489,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Apply label commands
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
+4 -3
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -39,7 +39,7 @@ jobs:
- name: Remove 'pr-deployed' label if present
id: remove-label-comment
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -121,8 +121,9 @@ jobs:
# Remove PR-specific directories
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
# Remove the Docker image
# Remove the Docker images
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ github.event.pull_request.number }} || true
echo "PERFORMED_CLEANUP"
else
+115
View File
@@ -0,0 +1,115 @@
name: AI Engine CI
# Validates the Python AI engine: regenerates tool models, runs fixers,
# lint, type-check, and tests. Called from build.yml on PRs and merge_group;
# also runs directly on push to main as a post-merge safety net.
on:
workflow_call:
push:
branches: [main]
permissions:
contents: read
jobs:
engine:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Regenerate tool models
run: task engine:tool-models
- name: Verify tool models are up to date
run: |
if ! git diff --exit-code engine/src/stirling/models/tool_models.py; then
echo "tool_models.py is out of date."
echo "Run 'task engine:tool-models' locally and commit the updated file."
exit 1
fi
- name: Run fixers
run: task engine:fix
- name: Verify fixes are committed
id: fixer_changes
run: |
if ! git diff --quiet; then
git --no-pager diff --stat
echo "::error::There are issues with your Python code that will need to be fixed before they can be merged in. Run 'task engine:fix' to auto-fix what can be fixed automatically, then run 'task engine:check' to see what still needs fixing manually."
exit 1
fi
- name: Comment on fixer failures
if: steps.fixer_changes.outcome == 'failure' && github.event_name == 'pull_request'
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const marker = '<!-- engine-check -->';
const body = [
marker,
'### Engine Check Failed',
'',
'There are issues with your Python code that will need to be fixed before they can be merged in.',
'',
'Run `task engine:fix` to auto-fix what can be fixed automatically, then run `task engine:check` to see what still needs fixing manually.',
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Run linting
run: task engine:lint
- name: Run type checking
run: task engine:typecheck
- name: Run tests
run: task engine:test
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"
@@ -158,7 +158,7 @@ jobs:
- name: Post comment on PR if needed
if: steps.actor.outputs.is_repo_dev == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
continue-on-error: true
with:
github-token: ${{ steps.setup-bot.outputs.token }}
+128
View File
@@ -0,0 +1,128 @@
name: Publish to AUR
on:
release:
types: [released]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 2.9.2 — no v prefix)"
required: true
type: string
dry_run:
description: "Skip the AUR push (safe test)"
type: boolean
default: true
permissions:
contents: read
jobs:
get-release-info:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.info.outputs.version }}
deb_sha256: ${{ steps.hashes.outputs.deb_sha256 }}
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Extract version from tag or manual input
id: info
env:
DISPATCH_VERSION: ${{ inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="$DISPATCH_VERSION"
else
VERSION="$RELEASE_TAG"
fi
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download release assets and compute SHA256
id: hashes
env:
VERSION: ${{ steps.info.outputs.version }}
run: |
BASE="https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${VERSION}"
download_sha256() {
local url="$1"
local file
file=$(basename "$url")
curl -fsSL --retry 3 -o "$file" "$url"
sha256sum "$file" | awk '{print $1}'
}
DEB_SHA=$(download_sha256 "${BASE}/Stirling-PDF-linux-x86_64.deb")
JAR_SHA=$(download_sha256 "${BASE}/Stirling-PDF-with-login.jar")
echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT"
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
publish-aur:
needs: get-release-info
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository (for PKGBUILD templates)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Update stirling-pdf-desktop PKGBUILD
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
DEB_SHA: ${{ needs.get-release-info.outputs.deb_sha256 }}
run: |
PKGBUILD=".github/aur/stirling-pdf-desktop/PKGBUILD"
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
sed -i "s/'PLACEHOLDER_DEB_SHA256'/'${DEB_SHA}'/" "$PKGBUILD"
# Disabled until we sort out the server packaging story.
# The third-party stirling-pdf-bin on AUR currently covers a similar use case.
# - name: Update stirling-pdf-server-bin PKGBUILD
# env:
# VERSION: ${{ needs.get-release-info.outputs.version }}
# JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
# run: |
# PKGBUILD=".github/aur/stirling-pdf-server-bin/PKGBUILD"
# sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
# sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
# sed -i "s/'PLACEHOLDER_JAR_SHA256'/'${JAR_SHA}'/" "$PKGBUILD"
- name: Show updated PKGBUILD (for dry-run visibility)
run: |
echo "--- stirling-pdf-desktop PKGBUILD ---"
cat .github/aur/stirling-pdf-desktop/PKGBUILD
- name: Publish stirling-pdf-desktop to AUR
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
uses: KSXGitHub/github-actions-deploy-aur@v4.1.2
with:
pkgname: stirling-pdf-desktop
pkgbuild: .github/aur/stirling-pdf-desktop/PKGBUILD
commit_username: Stirling PDF Inc
commit_email: contact@stirlingpdf.com
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update to v${{ needs.get-release-info.outputs.version }}"
# Disabled until we sort out the server packaging story.
# - name: Publish stirling-pdf-server-bin to AUR
# if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
# uses: KSXGitHub/github-actions-deploy-aur@2ac5a4c1d7035885d46b10e3193393be8460b6f1 # v4.1.1
# with:
# pkgname: stirling-pdf-server-bin
# pkgbuild: .github/aur/stirling-pdf-server-bin/PKGBUILD
# commit_username: Stirling PDF Inc
# commit_email: contact@stirlingpdf.com
# ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
# commit_message: "Update to v${{ needs.get-release-info.outputs.version }}"
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -29,7 +29,7 @@ jobs:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
- uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0
with:
config_path: .github/labeler-config-srvaroa.yml
use_local_config: false
+175
View File
@@ -0,0 +1,175 @@
name: Backend build, format check, and coverage
# Reusable workflow called from build.yml. Runs the full backend build matrix
# (JDK 21/25 × spring-security on/off), Spotless formatting check, JUnit, and
# posts Jacoco coverage to PRs.
on:
workflow_call:
permissions:
contents: read
actions: read
security-events: write
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
jdk-version: [21, 25]
spring-security: [true, false]
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
cache-disabled: true
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Check Java formatting (Spotless)
if: matrix.jdk-version == 25 && matrix.spring-security == false
id: spotless-check
run: task backend:format:check
continue-on-error: true
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: Comment on Java formatting failure
# Only post a comment on PRs. github-script's PR helpers need an
# issue/PR number, which doesn't exist on merge_group runs.
if: steps.spotless-check.outcome == 'failure' && github.event_name == 'pull_request'
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const marker = '<!-- java-formatting-check -->';
const body = [
marker,
'### Java Formatting Check Failed',
'',
'Your code has formatting issues. Run the following command to fix them:',
'',
'```bash',
'task backend:format',
'```',
'',
'Then commit and push the changes.',
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Fail if Java formatting issues found
if: steps.spotless-check.outcome == 'failure'
run: |
echo "============================================"
echo " Java Formatting Check Failed"
echo "============================================"
echo ""
echo "Your code has formatting issues."
echo "Run the following command to fix them:"
echo ""
echo " task backend:format"
echo ""
echo "Then commit and push the changes."
echo "============================================"
exit 1
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
run: task backend:build:ci
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
- name: Check Test Reports Exist
if: always()
run: |
declare -a dirs=(
"app/core/build/reports/tests/"
"app/core/build/test-results/"
"app/common/build/reports/tests/"
"app/common/build/test-results/"
"app/proprietary/build/reports/tests/"
"app/proprietary/build/test-results/"
)
for dir in "${dirs[@]}"; do
if [ ! -d "$dir" ]; then
echo "Missing $dir"
exit 1
fi
done
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
path: |
app/**/build/reports/jacoco/test
app/**/build/reports/tests/
app/**/build/test-results/
app/**/build/reports/problems/
build/reports/problems/
retention-days: 3
if-no-files-found: warn
- name: Add coverage to PR with spring security ${{ matrix.spring-security }} and JDK ${{ matrix.jdk-version }}
# The action only supports the pull_request event (it posts a PR comment),
# so skip it for merge_group runs and workflow_dispatch.
if: github.event_name == 'pull_request'
id: jacoco
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
with:
paths: |
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 10
min-coverage-changed-files: 0
comment-type: summary
+264
View File
@@ -0,0 +1,264 @@
name: Enterprise E2E (Playwright)
# Enterprise Playwright suite — exercises premium-key gated features (audit,
# teams, analytics) plus full OAuth + SAML logins via the Keycloak compose
# stacks under testing/compose. Slow and secret-gated, so it runs in three
# situations:
#
# - PRs that touch proprietary / premium / SSO compose / enterprise tests
# (driven by build.yml via workflow_call, path-filtered against
# .github/config/.files.yaml `proprietary`),
# - every push to main (post-merge safety net),
# - on a nightly cron schedule (catches Keycloak image drift, license
# expiry, upstream proprietary changes),
# - manual workflow_dispatch.
#
# Auto-skipped when secrets.PREMIUM_KEY_ENTERPRISE is missing (forks, dependabot).
on:
workflow_call:
push:
branches: ["main"]
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
# No `concurrency:` block here on purpose. When this workflow is called via
# workflow_call from build.yml, ${{ github.workflow }}/event_name/pr_number
# resolve to the *caller's* values, producing the same group key as build.yml
# and causing the workflow_call instantiation to self-cancel — the job
# silently fails to spawn while `${{ needs.X.result }}` still reports
# `failure`. Standalone runs (push-to-main, nightly cron, dispatch) don't
# overlap often enough to need explicit concurrency control.
permissions:
contents: read
jobs:
playwright-e2e-enterprise:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
PREMIUM_ENABLED: "true"
SYSTEM_ENABLEANALYTICS: "false"
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.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: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install Playwright (chromium only)
run: task frontend:test:e2e:install -- chromium
- name: Resolve kubernetes.docker.internal to localhost
# The compose stacks set KC_HOSTNAME=kubernetes.docker.internal so
# Keycloak issues redirect URIs against that host. Docker Desktop
# auto-resolves it; GHA runners don't. Map it to 127.0.0.1 so the
# browser-driven OAuth flow lands back on Stirling-PDF correctly.
run: |
echo "127.0.0.1 kubernetes.docker.internal" | sudo tee -a /etc/hosts
# Helper function used by all phases — boots `:stirling-pdf:bootRun`
# with the React frontend baked in (-PbuildWithFrontend=true) so the
# SPA serves on :8080 and OAuth/SAML callbacks land on the same host
# that the browser is interacting with.
- name: Define helpers
run: |
{
echo 'wait_for_backend() {'
echo ' start=$SECONDS'
echo ' for i in $(seq 1 300); do'
echo ' if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then'
echo ' echo "Backend up after $((SECONDS - start))s"; return 0'
echo ' fi; sleep 2'
echo ' done'
echo ' tail -200 /tmp/backend.log || true; return 1'
echo '}'
echo 'stop_backend() {'
echo ' if [ -f /tmp/backend.pid ]; then'
echo ' kill "$(cat /tmp/backend.pid)" 2>/dev/null || true'
echo ' rm -f /tmp/backend.pid'
echo ' fi'
echo ' pkill -f "gradlew :stirling-pdf:bootRun" 2>/dev/null || true'
echo ' for i in $(seq 1 30); do'
echo ' curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1 || return 0'
echo ' sleep 1'
echo ' done'
echo '}'
} > /tmp/helpers.sh
chmod +x /tmp/helpers.sh
# ───────── OAuth round-trip ─────────
- name: Bring up Keycloak (OAuth realm)
working-directory: testing/compose
run: docker compose -f docker-compose-keycloak-oauth.yml up -d --no-deps keycloak-oauth-db keycloak-oauth
- name: Wait for Keycloak (OAuth) ready
working-directory: testing/compose
run: |
for i in $(seq 1 60); do
bash validate-oauth-test.sh 2>/dev/null && exit 0 || true
# validate script also pings stirling on :8080 — accept just the
# keycloak realm as our gate here, stirling boots in the next step
curl -fsS http://localhost:9080/realms/stirling-oauth >/dev/null 2>&1 && exit 0
sleep 5
done
docker compose -f docker-compose-keycloak-oauth.yml logs --tail=200 keycloak-oauth
exit 1
- name: Boot Stirling-PDF (frontend baked in, OAuth env)
env:
SECURITY_ENABLELOGIN: "true"
SECURITY_LOGINMETHOD: "all"
SECURITY_OAUTH2_ENABLED: "true"
SECURITY_OAUTH2_AUTOCREATEUSER: "true"
# Keycloak issues redirect URIs against KC_HOSTNAME, which the
# compose default sets to kubernetes.docker.internal. Match here
# (resolves to localhost via /etc/hosts mapping above).
SECURITY_OAUTH2_CLIENT_KEYCLOAK_ISSUER: "http://kubernetes.docker.internal:9080/realms/stirling-oauth"
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTID: "stirling-pdf-client"
SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTSECRET: "test-client-secret-change-in-production"
SECURITY_OAUTH2_CLIENT_KEYCLOAK_USEASUSERNAME: "email"
SECURITY_OAUTH2_CLIENT_KEYCLOAK_SCOPES: "openid,profile,email"
run: |
source /tmp/helpers.sh
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
echo $! > /tmp/backend.pid
wait_for_backend
- name: Run enterprise OAuth Playwright tests
id: oauth-tests
run: task frontend:test:e2e -- --project=enterprise --grep "OAuth"
- name: Stop backend + tear down OAuth Keycloak
if: always()
run: |
source /tmp/helpers.sh
stop_backend
(cd testing/compose && docker compose -f docker-compose-keycloak-oauth.yml down -v)
# ───────── SAML round-trip ─────────
- name: Bring up Keycloak (SAML realm)
working-directory: testing/compose
run: docker compose -f docker-compose-keycloak-saml.yml up -d --no-deps keycloak-saml-db keycloak-saml
- name: Wait for Keycloak (SAML) ready
working-directory: testing/compose
run: |
for i in $(seq 1 60); do
curl -fsS http://localhost:9080/realms/stirling-saml >/dev/null 2>&1 && exit 0
sleep 5
done
docker compose -f docker-compose-keycloak-saml.yml logs --tail=200 keycloak-saml
exit 1
- name: Generate SAML SP certs + fetch Keycloak IdP cert
# The .pem/.crt/.key files are gitignored (test-only certs); the
# docker-based start-saml-test.sh generates them at runtime, so do
# the same in CI before bootRun reads them.
working-directory: testing/compose
run: |
openssl req -x509 -newkey rsa:2048 \
-keyout saml-private-key.key \
-out saml-public-cert.crt \
-days 3650 -nodes \
-subj "/CN=stirling-pdf-saml-sp" >/dev/null 2>&1
# Fetch Keycloak's SAML signing cert from the realm descriptor
CERT_BODY=$(curl -sf http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor \
| awk 'BEGIN{RS="<[^>]*X509Certificate>|</[^>]*X509Certificate>"} NR==2{gsub(/[[:space:]]+/,""); print; exit}')
{
echo "-----BEGIN CERTIFICATE-----"
echo "$CERT_BODY"
echo "-----END CERTIFICATE-----"
} > keycloak-saml-cert.pem
test -s saml-private-key.key
test -s saml-public-cert.crt
test -s keycloak-saml-cert.pem
echo "✓ SAML certs prepared"
- name: Boot Stirling-PDF (frontend baked in, SAML env)
env:
SECURITY_ENABLELOGIN: "true"
SECURITY_LOGINMETHOD: "all"
SECURITY_SAML2_ENABLED: "true"
SECURITY_SAML2_AUTOCREATEUSER: "true"
SECURITY_SAML2_PROVIDER: "keycloak"
SECURITY_SAML2_REGISTRATIONID: "keycloak"
SECURITY_SAML2_IDP_ISSUER: "http://localhost:9080/realms/stirling-saml"
SECURITY_SAML2_IDP_ENTITYID: "http://localhost:9080/realms/stirling-saml"
SECURITY_SAML2_IDP_METADATAURI: "http://localhost:9080/realms/stirling-saml/protocol/saml/descriptor"
SECURITY_SAML2_IDPSINGLELOGINURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
SECURITY_SAML2_IDPSINGLELOGOUTURL: "http://localhost:9080/realms/stirling-saml/protocol/saml"
SECURITY_SAML2_IDP_CERT: "${{ github.workspace }}/testing/compose/keycloak-saml-cert.pem"
SECURITY_SAML2_PRIVATEKEY: "${{ github.workspace }}/testing/compose/saml-private-key.key"
SECURITY_SAML2_SP_CERT: "${{ github.workspace }}/testing/compose/saml-public-cert.crt"
# Realm registers the SP entity as the metadata URL — see
# keycloak-realm-saml.json `clientId`. Match it here so Keycloak
# accepts the AuthnRequest issuer.
SECURITY_SAML2_SP_ENTITYID: "http://localhost:8080/saml2/service-provider-metadata/keycloak"
SECURITY_SAML2_SP_ACS: "http://localhost:8080/login/saml2/sso/keycloak"
SECURITY_SAML2_SP_SLS: "http://localhost:8080/logout/saml2/slo"
run: |
source /tmp/helpers.sh
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
echo $! > /tmp/backend.pid
wait_for_backend
- name: Run enterprise SAML Playwright tests
id: saml-tests
run: task frontend:test:e2e -- --project=enterprise --grep "SAML"
- name: Stop backend + tear down SAML Keycloak
if: always()
run: |
source /tmp/helpers.sh
stop_backend
(cd testing/compose && docker compose -f docker-compose-keycloak-saml.yml down -v)
# ───────── License-gated feature tests (no IdP needed) ─────────
- name: Wipe DB so InitialSecuritySetup re-runs with admin/adminadmin
# Earlier phases (OAuth, SAML) create the default admin/stirling user.
# InitialSecuritySetup only honours SECURITY_INITIALLOGIN_* when the
# admin user doesn't already exist, so the persisted DB has to be
# cleared between phases for the feature env vars to take effect.
run: |
rm -f app/core/configs/stirling-pdf-DB*.mv.db
rm -rf app/core/configs/backup
- name: Boot Stirling-PDF (frontend baked in, premium only)
env:
SECURITY_INITIALLOGIN_USERNAME: admin
SECURITY_INITIALLOGIN_PASSWORD: adminadmin
SECURITY_ENABLELOGIN: "true"
SECURITY_LOGINMETHOD: "all"
run: |
source /tmp/helpers.sh
nohup ./gradlew :stirling-pdf:bootRun -PbuildWithFrontend=true > /tmp/backend.log 2>&1 &
echo $! > /tmp/backend.pid
wait_for_backend
- name: Run enterprise feature Playwright tests
id: feature-tests
run: task frontend:test:e2e -- --project=enterprise --grep "Enterprise license"
- name: Print backend log on failure
if: failure()
run: |
echo "::group::Enterprise backend log"
tail -500 /tmp/backend.log || true
echo "::endgroup::"
- name: Stop backend (final)
if: always()
run: |
source /tmp/helpers.sh
stop_backend
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-enterprise-${{ github.run_id }}
path: frontend/playwright-report/
retention-days: 7
+163 -318
View File
@@ -1,8 +1,18 @@
name: Build and Test Workflow
# Top-level PR / merge-queue gate. Detects which paths changed and dispatches
# to the dedicated reusable workflows under .github/workflows/. Each child
# workflow keeps its own setup/teardown so this file stays a routing layer.
#
# The final `all-checks-passed` job is the single status check that branch
# protection should require — it succeeds only if every required upstream
# job either succeeded or was legitimately skipped by its path filter.
on:
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
workflow_dispatch:
# cancel in-progress jobs if a new job is triggered
@@ -30,358 +40,193 @@ jobs:
project: ${{ steps.changes.outputs.project }}
openapi: ${{ steps.changes.outputs.openapi }}
frontend: ${{ steps.changes.outputs.frontend }}
docker-base: ${{ steps.changes.outputs.docker-base }}
tauri: ${{ steps.changes.outputs.tauri }}
engine: ${{ steps.changes.outputs.engine }}
proprietary: ${{ steps.changes.outputs.proprietary }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: .github/config/.files.yaml
build:
runs-on: ubuntu-latest
needs: [files-changed]
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
jdk-version: [17, 21]
spring-security: [true, false]
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
run: ./gradlew build -PnoSpotless
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
- name: Check Test Reports Exist
if: always()
run: |
declare -a dirs=(
"app/core/build/reports/tests/"
"app/core/build/test-results/"
"app/common/build/reports/tests/"
"app/common/build/test-results/"
"app/proprietary/build/reports/tests/"
"app/proprietary/build/test-results/"
)
for dir in "${dirs[@]}"; do
if [ ! -d "$dir" ]; then
echo "Missing $dir"
exit 1
fi
done
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
path: |
app/**/build/reports/jacoco/test
app/**/build/reports/tests/
app/**/build/test-results/
app/**/build/reports/problems/
build/reports/problems/
retention-days: 3
if-no-files-found: warn
- name: Add coverage to PR with spring security ${{ matrix.spring-security }} and JDK ${{ matrix.jdk-version }}
id: jacoco
uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2
with:
paths: |
${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 10
min-coverage-changed-files: 0
comment-type: summary
pull-requests: write
uses: ./.github/workflows/backend-build.yml
secrets: inherit
check-generateOpenApiDocs:
if: needs.files-changed.outputs.openapi == 'true'
needs: [files-changed]
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
- name: Generate OpenAPI documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-docs
path: ./SwaggerDoc.json
permissions:
contents: read
uses: ./.github/workflows/check-openapi.yml
secrets: inherit
frontend-validation:
if: needs.files-changed.outputs.frontend == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Type-check frontend
run: cd frontend && npm run prebuild && npm run typecheck:all
- name: Lint frontend
run: cd frontend && npm run lint
- name: Build frontend
run: cd frontend && npm run build
- name: Run frontend tests
run: cd frontend && npm run test -- --run
- name: Upload frontend build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: frontend-build
path: frontend/dist/
retention-days: 3
needs: [files-changed]
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/frontend-validation.yml
secrets: inherit
playwright-e2e:
if: needs.files-changed.outputs.frontend == 'true'
needs: [files-changed]
permissions:
contents: read
uses: ./.github/workflows/e2e-stubbed.yml
secrets: inherit
playwright-e2e-live:
if: needs.files-changed.outputs.frontend == 'true'
needs: [files-changed]
permissions:
contents: read
uses: ./.github/workflows/e2e-live.yml
secrets: inherit
playwright-e2e-enterprise:
if: needs.files-changed.outputs.proprietary == 'true'
needs: [files-changed]
permissions:
contents: read
uses: ./.github/workflows/build-enterprise.yml
secrets: inherit
check-licence:
if: needs.files-changed.outputs.build == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
- name: check the licenses for compatibility
run: ./gradlew checkLicense
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: FAILED - check the licenses for compatibility
if: failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
retention-days: 3
permissions:
contents: read
uses: ./.github/workflows/check-licence.yml
secrets: inherit
docker-compose-tests:
if: needs.files-changed.outputs.project == 'true'
needs: files-changed
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
# (github.event_name == 'pull_request' &&
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
# (
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
# contains(github.event.pull_request.labels.*.name, 'Java') ||
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
# contains(github.event.pull_request.labels.*.name, 'Security') ||
# contains(github.event.pull_request.labels.*.name, 'API') ||
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
# contains(github.event.pull_request.labels.*.name, 'Test')
# )
# )
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
cache-dependency-path: ./testing/cucumber/requirements.txt
- name: Pip requirements
run: |
pip install --require-hashes -r ./testing/cucumber/requirements.txt
- name: Run Docker Compose Tests
run: |
chmod +x ./testing/test_webpages.sh
chmod +x ./testing/test.sh
chmod +x ./testing/test_disabledEndpoints.sh
./testing/test.sh
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
needs: [files-changed]
permissions:
actions: write
contents: read
checks: write
uses: ./.github/workflows/docker-compose-tests.yml
secrets: inherit
with:
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
test-build-docker-images:
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
needs: [files-changed, build, check-generateOpenApiDocs, check-licence]
permissions:
contents: read
packages: read
uses: ./.github/workflows/test-build-docker.yml
secrets: inherit
with:
docker-base-changed: ${{ needs.files-changed.outputs.docker-base }}
tauri-build:
if: needs.files-changed.outputs.tauri == 'true'
needs: [files-changed]
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/tauri-build.yml
secrets: inherit
ai-engine:
if: needs.files-changed.outputs.engine == 'true'
needs: [files-changed]
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/ai-engine.yml
secrets: inherit
pre-commit:
needs: [files-changed]
permissions:
contents: read
uses: ./.github/workflows/pre_commit.yml
secrets: inherit
dependency-review:
needs: [files-changed]
permissions:
contents: read
uses: ./.github/workflows/dependency-review.yml
secrets: inherit
# Single status check that branch protection should mark as required.
# Succeeds when every upstream job is either `success` or `skipped` (path-
# gated jobs that didn't apply this run). Any `failure` or `cancelled`
# result fails the gate. `if: always()` ensures the gate evaluates even
# when an upstream job fails.
all-checks-passed:
name: All checks passed
if: always()
needs:
- files-changed
- build
- check-generateOpenApiDocs
- frontend-validation
- playwright-e2e
- playwright-e2e-live
- playwright-e2e-enterprise
- check-licence
- docker-compose-tests
- test-build-docker-images
- tauri-build
- ai-engine
- pre-commit
- dependency-review
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- docker-rev: docker/embedded/Dockerfile
artifact-suffix: Dockerfile
- docker-rev: docker/embedded/Dockerfile.ultra-lite
artifact-suffix: Dockerfile.ultra-lite
- docker-rev: docker/embedded/Dockerfile.fat
artifact-suffix: Dockerfile.fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Free disk space on runner
run: |
echo "Disk space before cleanup:" && df -h
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
docker system prune -af || true
echo "Disk space after cleanup:" && df -h
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
- name: Build application
run: ./gradlew build
- name: Verify every required job passed (or was legitimately skipped)
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
STIRLING_PDF_DESKTOP_UI: false
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build ${{ matrix.docker-rev }}
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./${{ matrix.docker-rev }}
push: false
cache-from: type=gha,scope=${{ matrix.artifact-suffix }}
cache-to: type=gha,mode=max,scope=${{ matrix.artifact-suffix }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Upload Reports
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: reports-docker-${{ matrix.artifact-suffix }}
path: |
build/reports/tests/
build/test-results/
build/reports/problems/
retention-days: 3
if-no-files-found: warn
RESULTS: |
files-changed=${{ needs.files-changed.result }}
build=${{ needs.build.result }}
check-generateOpenApiDocs=${{ needs.check-generateOpenApiDocs.result }}
frontend-validation=${{ needs.frontend-validation.result }}
playwright-e2e=${{ needs.playwright-e2e.result }}
playwright-e2e-live=${{ needs.playwright-e2e-live.result }}
playwright-e2e-enterprise=${{ needs.playwright-e2e-enterprise.result }}
check-licence=${{ needs.check-licence.result }}
docker-compose-tests=${{ needs.docker-compose-tests.result }}
test-build-docker-images=${{ needs.test-build-docker-images.result }}
tauri-build=${{ needs.tauri-build.result }}
ai-engine=${{ needs.ai-engine.result }}
pre-commit=${{ needs.pre-commit.result }}
dependency-review=${{ needs.dependency-review.result }}
run: |
ok=true
while IFS='=' read -r name result; do
[ -z "$name" ] && continue
case "$result" in
success|skipped) printf ' %-30s %s\n' "$name" "$result" ;;
*) printf '✗ %-30s %s\n' "$name" "$result"; ok=false ;;
esac
done <<< "$RESULTS"
if [ "$ok" != "true" ]; then
echo ""
echo "One or more required checks failed or were cancelled."
exit 1
fi
echo ""
echo "All required checks passed."
+59
View File
@@ -0,0 +1,59 @@
name: License compatibility check
# Reusable workflow called from build.yml when build.gradle / module gradle
# files change. Verifies all transitive dependencies use a permitted license.
on:
workflow_call:
permissions:
contents: read
jobs:
check-licence:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.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 dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
cache-disabled: true
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Check licenses for compatibility
run: task backend:licenses:check
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: FAILED - check the licenses for compatibility
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
retention-days: 3
+59
View File
@@ -0,0 +1,59 @@
name: Generate OpenAPI documentation
# Reusable workflow called from build.yml when backend Java sources change.
# Generates SwaggerDoc.json so we know the doc still builds against the
# current code.
on:
workflow_call:
permissions:
contents: read
jobs:
check-generate-openapi-docs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.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 dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
cache-disabled: true
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Generate OpenAPI documentation
run: task backend:swagger
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Upload OpenAPI Documentation
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: openapi-docs
path: ./SwaggerDoc.json
+7 -6
View File
@@ -27,7 +27,7 @@ jobs:
pull-requests: write # Allow writing to pull requests
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -43,7 +43,7 @@ jobs:
- name: Get PR data
id: get-pr-data
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -87,7 +87,7 @@ jobs:
- name: Determine reference file
id: determine-file
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -196,12 +196,13 @@ jobs:
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install Python dependencies
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
run: |
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
- name: Run Python script to check files
id: run-check
@@ -239,7 +240,7 @@ jobs:
- name: Post comment on PR
if: env.SCRIPT_OUTPUT != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
+9 -12
View File
@@ -1,13 +1,10 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: "Dependency Review"
on: [pull_request]
name: Dependency Review
# Reusable workflow called from build.yml. Scans dependency manifest files
# changing in a PR for known-vulnerable packages, using the rules in
# .github/config/dependency-review-config.yml.
on:
workflow_call:
permissions:
contents: read
@@ -17,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "Dependency Review"
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
config-file: "./.github/config/dependency-review-config.yml"
+9 -5
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Get commit hashes for frontend and backend
id: commit-hashes
@@ -85,18 +85,20 @@ jobs:
fi
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push frontend image
if: steps.check-frontend.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./docker/frontend/Dockerfile
push: true
cache-from: type=gha,scope=stirling-v2-frontend
cache-to: type=gha,mode=max,scope=stirling-v2-frontend
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
@@ -105,11 +107,13 @@ jobs:
- name: Build and push backend image
if: steps.check-backend.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./docker/backend/Dockerfile
push: true
cache-from: type=gha,scope=stirling-v2-backend
cache-to: type=gha,mode=max,scope=stirling-v2-backend
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
+116
View File
@@ -0,0 +1,116 @@
name: Docker Compose Cucumber tests
# Reusable workflow called from build.yml when project / docker / testing
# sources change. Boots the docker-compose stack and runs the cucumber
# scenarios in testing/cucumber.
on:
workflow_call:
inputs:
docker-base-changed:
description: "Whether the docker base image changed (forwarded from files-changed)."
required: false
type: string
default: "false"
permissions:
contents: read
jobs:
docker-compose-tests:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
checks: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.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 dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
cache-disabled: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
- name: Expose GitHub runtime for Buildx cache
uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
cache-dependency-path: ./testing/cucumber/requirements.txt
- name: Pip requirements
run: |
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
- name: Run Docker Compose Tests
run: |
chmod +x ./testing/test_webpages.sh
chmod +x ./testing/test.sh
chmod +x ./testing/test_disabledEndpoints.sh
./testing/test.sh
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DOCKER_BASE_CHANGED: ${{ inputs.docker-base-changed }}
- name: Upload Cucumber Report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cucumber-report
path: testing/cucumber/report.html
retention-days: 7
if-no-files-found: warn
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docker-compose-test-reports
path: testing/reports/
retention-days: 7
if-no-files-found: warn
- name: Cucumber Test Report
if: always()
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
with:
name: Cucumber Tests
path: testing/cucumber/junit/*.xml
reporter: java-junit
fail-on-error: false
+96
View File
@@ -0,0 +1,96 @@
name: Playwright E2E (live backend)
# Reusable workflow called from build.yml. Live-backend Playwright suite —
# boots Spring Boot and runs auth + real tool round-trips against the live
# server.
on:
workflow_call:
permissions:
contents: read
jobs:
playwright-e2e-live:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.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: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install Playwright (chromium only)
run: task frontend:test:e2e:install -- chromium
- name: Start Spring Boot backend (background)
env:
# Suppress the analytics opt-in modal that fires on first admin login when
# enableAnalytics is null (see Onboarding.tsx). The modal renders a Mantine
# overlay that intercepts pointer events on every tool page until dismissed,
# which causes every "click run button" assertion in the live suite to fail.
SYSTEM_ENABLEANALYTICS: "false"
# NOTE: SECURITY_INITIALLOGIN_USERNAME/PASSWORD are intentionally NOT set.
# The live-setup project's bootstrap spec performs the real first-login
# flow against the backend's default admin/stirling user, exercising the
# forced-password-change UI and leaving the DB at admin/adminadmin for
# the rest of the live suite. This is both real coverage of the first-
# login flow and a stronger seed than env-var-driven user creation.
run: |
nohup ./gradlew :stirling-pdf:bootRun > /tmp/backend.log 2>&1 &
echo $! > /tmp/backend.pid
- name: Wait for backend to become ready
run: |
start=$SECONDS
# 300 iterations × 2s = 10 minute ceiling
for i in $(seq 1 300); do
if curl -fsS http://localhost:8080/api/v1/info/status >/dev/null 2>&1; then
echo "Backend up after $((SECONDS - start))s"
exit 0
fi
sleep 2
done
echo "Backend did not become ready in $((SECONDS - start))s"
tail -200 /tmp/backend.log || true
exit 1
- name: Run live E2E tests (chromium)
id: live-tests
run: task frontend:test:e2e -- --project=live
- name: Print backend log on failure
if: failure() && steps.live-tests.conclusion == 'failure'
run: |
echo "::group::Spring Boot backend log (last 500 lines)"
tail -500 /tmp/backend.log || echo "no backend log found"
echo "::endgroup::"
- name: Stop backend
if: always()
run: |
if [ -f /tmp/backend.pid ]; then
kill "$(cat /tmp/backend.pid)" 2>/dev/null || true
fi
- name: Upload backend log
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-log-live-${{ github.run_id }}
path: /tmp/backend.log
retention-days: 7
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-live-${{ github.run_id }}
path: frontend/playwright-report/
retention-days: 7
+40
View File
@@ -0,0 +1,40 @@
name: Playwright E2E (stubbed)
# Reusable workflow called from build.yml. Backend-free Playwright suite —
# fast, no Spring Boot required. Runs against the `stubbed` project which
# mocks API responses in the browser.
on:
workflow_call:
permissions:
contents: read
jobs:
playwright-e2e:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install Playwright (chromium only)
run: task frontend:test:e2e:install -- chromium
- name: Run stubbed E2E tests (chromium)
run: task frontend:test:e2e -- --project=stubbed --workers=3
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-stubbed-${{ github.run_id }}
path: frontend/playwright-report/
retention-days: 7
@@ -7,6 +7,9 @@ on:
pull_request:
branches:
- main
merge_group:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
@@ -25,7 +28,7 @@ jobs:
licenses-backend: ${{ steps.changes.outputs.licenses-backend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -33,7 +36,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: .github/config/.files.yaml
@@ -49,7 +52,7 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -77,7 +80,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
@@ -89,12 +92,13 @@ jobs:
NPM_CONFIG_IGNORE_SCRIPTS: "true"
run: npm ci --ignore-scripts --audit=false --fund=false
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Generate frontend license report (internal PR)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
working-directory: frontend
env:
PR_IS_FORK: "false"
run: npm run generate-licenses
run: task frontend:licenses:generate
- name: Generate frontend license report (fork PRs, pinned)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
@@ -135,7 +139,7 @@ jobs:
# PR Event: Check licenses and comment on PR
- name: Delete previous license check comments
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -188,7 +192,7 @@ jobs:
- name: Comment on PR - License Check Results
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -273,7 +277,7 @@ jobs:
- name: Create Pull Request (Push only)
id: cpr
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Frontend 3rd Party Licenses"
@@ -312,7 +316,7 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -330,21 +334,22 @@ jobs:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Check licenses and generate report
id: license-check
run: |
./gradlew checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
run: task backend:licenses:generate || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
@@ -364,7 +369,7 @@ jobs:
- name: Upload artifact on license issues
if: env.LICENSE_WARNINGS_EXIST == 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-dependencies-without-allowed-license.json
path: build/reports/dependency-license/dependencies-without-allowed-license.json
@@ -377,7 +382,7 @@ jobs:
- name: Delete previous backend license check comments
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -407,7 +412,7 @@ jobs:
- name: Comment on PR - Backend License Check Results
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) && github.actor != 'dependabot[bot]'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -490,7 +495,7 @@ jobs:
- name: Create Pull Request (push only)
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
id: cpr
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: "Update Backend 3rd Party Licenses"
+93
View File
@@ -0,0 +1,93 @@
name: Frontend lint, type-check, and build
# Reusable workflow called from build.yml when frontend / testing sources
# change. Runs the consolidated `task frontend:check:all` (lint, types,
# unit tests, build) and uploads the dist artifact for downstream jobs.
on:
workflow_call:
permissions:
contents: read
pull-requests: write
jobs:
frontend-validation:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Quality-check frontend
id: frontend-check
run: task frontend:check:all
continue-on-error: true
- name: Comment on frontend check failure
# Only post a comment on PRs. github-script's PR helpers need an
# issue/PR number, which doesn't exist on merge_group runs.
if: steps.frontend-check.outcome == 'failure' && github.event_name == 'pull_request'
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const marker = '<!-- frontend-check -->';
const body = [
marker,
'### Frontend Check Failed',
'',
'There are issues with your frontend code that will need to be fixed before they can be merged in.',
'',
'Run `task frontend:fix` to auto-fix what can be fixed automatically, then run `task frontend:check:all` to see what still needs fixing manually.',
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Fail if frontend check failed
if: steps.frontend-check.outcome == 'failure'
run: |
echo "============================================"
echo " Frontend Check Failed"
echo "============================================"
echo ""
echo "There are issues with your frontend code that"
echo "will need to be fixed before they can be merged in."
echo ""
echo "Run 'task frontend:fix' to auto-fix what can be"
echo "fixed automatically, then run 'task frontend:check:all'"
echo "to see what still needs fixing manually."
echo "============================================"
exit 1
- name: Upload frontend build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-build
path: frontend/dist/
retention-days: 3
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d # v6.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml
+262 -202
View File
@@ -21,8 +21,14 @@ on:
- windows
- macos
- linux
push:
branches: [main, V2-master]
sign:
description: "Code sign the binaries (requires signing secrets)"
required: false
default: "true"
type: choice
options:
- "true"
- "false"
release:
types: [created]
@@ -38,28 +44,38 @@ jobs:
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
- name: Cache Gradle dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
gradle-version: 8.14
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Get version number
id: versionNumber
run: |
echo "Running gradlew printVersion..."
./gradlew printVersion --quiet
VERSION=$(./gradlew printVersion --quiet | tail -1)
echo "Extracted version: $VERSION"
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
@@ -77,18 +93,18 @@ jobs:
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"macos")
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"}]}' >> $GITHUB_OUTPUT
;;
"linux")
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
*)
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
esac
else
# For push/release events, build all platforms
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
fi
build-jars:
@@ -111,31 +127,34 @@ jobs:
file_suffix: "-server"
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Setup Node.js
if: matrix.variant.build_frontend == true
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Build JAR
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
env:
@@ -154,7 +173,7 @@ jobs:
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
- name: Upload JAR artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jar${{ matrix.variant.file_suffix }}
path: ./jar-dist/*.jar
@@ -169,11 +188,15 @@ jobs:
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
allowed-endpoints: >
one.digicert.com:443
clientauth.one.digicert.com:443
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -185,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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: "npm"
@@ -195,103 +218,59 @@ jobs:
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
# x86_64 JDK is set up first so the aarch64 step below can leave its
# JAVA_HOME as the active one. The macOS universal JRE build needs
# jmods from both arches; the x64 path is captured into the env
# before the second setup-java overwrites JAVA_HOME.
- name: Set up x86_64 JDK 25 (macOS universal JRE)
if: matrix.platform == 'macos-15'
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
architecture: "x64"
- name: Capture x86_64 JAVA_HOME
if: matrix.platform == 'macos-15'
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
echo "✅ Built JAR: $STIRLING_JAR"
# Build the universal JRE before desktop:prepare so the jlink:runtime
# task short-circuits on its `test -d runtime/jre` status check.
- name: Build universal macOS JRE
if: matrix.platform == 'macos-15'
env:
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
run: task desktop:jlink:universal-mac
# Create Tauri directories
mkdir -p ./frontend/src-tauri/libs
mkdir -p ./frontend/src-tauri/runtime
# Copy JAR to Tauri libs
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
echo "✅ JAR copied to Tauri libs"
# Analyze JAR dependencies for jlink modules
echo "🔍 Analyzing JAR dependencies..."
if command -v jdeps &> /dev/null; then
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
if [ -n "$DETECTED_MODULES" ]; then
echo "📋 jdeps detected modules: $DETECTED_MODULES"
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
else
echo "⚠️ jdeps analysis failed, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
else
echo "⚠️ jdeps not available, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
# Create custom JRE with jlink
echo "🔧 Creating custom JRE with jlink..."
echo "📋 Using modules: $MODULES"
# Remove any existing JRE
rm -rf ./frontend/src-tauri/runtime/jre
# Create the custom JRE
jlink \
--add-modules "$MODULES" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output ./frontend/src-tauri/runtime/jre
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
echo "❌ Failed to create JLink runtime"
exit 1
fi
# Test the bundled runtime
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
else
echo "❌ Custom JRE executable not found"
exit 1
fi
# Calculate runtime size
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
- name: Prepare desktop build
run: task desktop:prepare
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
working-directory: ./frontend
run: npm ci
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
uses: digicert/ssm-code-signing@af7f4b07694120e514fe5b19f0598af3c20256e3 # v1.2.0
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -300,7 +279,7 @@ jobs:
SM_HOST: ${{ secrets.SM_HOST }}
- name: Setup DigiCert KeyLocker Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.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..."
@@ -335,7 +314,7 @@ jobs:
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
- name: Import Windows Code Signing Certificate
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || github.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 }}
@@ -366,7 +345,7 @@ jobs:
}
- name: Import Apple Developer Certificate
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -387,7 +366,7 @@ jobs:
rm certificate.p12
- name: Verify Certificate
if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master')
if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
@@ -398,8 +377,82 @@ jobs:
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported successfully."
# Pre-flight: verify smctl can talk to DigiCert and sync cert before we sign.
# Mirrors the setup from working public Tauri+KeyLocker repos (Labric, Meetily).
# Without this, signCommand failures are opaque (Tauri captures but drops
# smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
- name: Preflight smctl
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
shell: pwsh
env:
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
run: |
& smctl healthcheck
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
& smctl keypair ls
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
Write-Host "[SUCCESS] smctl preflight passed"
# Write platform-specific Tauri config that adds signCommand for Windows.
# Tauri auto-merges tauri.windows.conf.json with tauri.conf.json (RFC 7396).
# Tauri calls this command on every binary BEFORE bundling into the MSI,
# substituting %1 with the file path.
#
# Why OBJECT form (cmd + args) instead of string:
# Tauri's string-form parser does a naive split(' ') with no shell/quote handling.
# Args with spaces or quote characters get mangled. The object form passes each
# arg directly to Rust's Command::arg which handles Windows CreateProcess quoting.
#
# Why --keypair-alias instead of --fingerprint:
# --fingerprint requires smctl windows certsync to have synced the cert to the
# Windows cert store first. --keypair-alias goes direct through PKCS11 and works
# without certsync. All real-world working Tauri+smctl examples use this flag.
#
# smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
# from env (set by prior DigiCert setup step). No --config-file needed.
- name: Configure Windows code signing
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
shell: bash
env:
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
run: |
cat > ./frontend/src-tauri/tauri.windows.conf.json <<EOF
{
"bundle": {
"windows": {
"signCommand": {
"cmd": "smctl",
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
}
}
}
}
EOF
echo "Generated tauri.windows.conf.json (alias masked):"
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/src-tauri/tauri.windows.conf.json
- name: Import release GPG signing key (Linux)
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
run: |
echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
gpg --list-secret-keys --keyid-format=long
- name: Make libjvm discoverable for linuxdeploy (Linux AppImage)
if: matrix.platform == 'ubuntu-22.04'
run: |
JAVA_LIBJVM="$JAVA_HOME/lib/server/libjvm.so"
if [ -f "$JAVA_LIBJVM" ]; then
sudo ln -sf "$JAVA_LIBJVM" /usr/lib/libjvm.so
echo "Linked libjvm from $JAVA_LIBJVM -> /usr/lib/libjvm.so"
else
echo "libjvm not found at $JAVA_LIBJVM"
exit 1
fi
- name: Build Tauri app
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -408,113 +461,116 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
# AppImage signing — three env vars work together:
# SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool
# APPIMAGETOOL_SIGN_PASSPHRASE appimagetool uses this to unlock the GPG key non-interactively
# SIGN_KEY appimagetool picks the key matching this fingerprint
# Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present.
# Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or V2-master, when secret is present.
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on release or V2-master
SIGN: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
# DigiCert KeyLocker env vars consumed by smctl during signCommand
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: ${{ matrix.args }}
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
- name: Clear release GPG key from runner keyring (Linux)
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
env:
RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
run: |
if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then
gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true
gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true
fi
# Verify the MSI (outer wrapper users download) AND the inner exe extracted
# from it (what actually gets installed and what AV scans). We don't check
# target/.../release/stirling-pdf.exe - that's Tauri's intermediate build
# artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
# cargo output unsigned, so checking it produces false negatives.
- name: Verify Windows Code Signature
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
shell: pwsh
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
$allSigned = $true
# Test smctl connectivity first
Write-Host "Testing smctl connection..."
$healthCheck = & smctl healthcheck 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
} else {
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
Write-Host $healthCheck
exit 1
}
Write-Host ""
# Sync certificates to Windows certificate store
Write-Host "Syncing certificates to Windows certificate store..."
$syncOutput = & smctl windows certsync 2>&1
Write-Host "Cert sync result: $syncOutput"
Write-Host ""
# Find only the files we need to sign
$filesToSign = @()
# Main application executable
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
if ($mainExe) { $filesToSign += $mainExe }
# MSI installer
# Check MSI installer (outer wrapper - what users download)
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
$filesToSign += $msiFiles
if ($filesToSign.Count -eq 0) {
Write-Host "[ERROR] No files found to sign"
if ($msiFiles.Count -eq 0) {
Write-Host "[ERROR] No MSI found under target/"
exit 1
}
Write-Host "Found $($filesToSign.Count) files to sign:"
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
Write-Host ""
$signedCount = 0
foreach ($file in $filesToSign) {
Write-Host "Signing: $($file.Name)"
# Get PKCS11 config file path
$pkcs11Config = $env:PKCS11_CONFIG
if (-not $pkcs11Config) {
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
exit 1
foreach ($msi in $msiFiles) {
$sig = Get-AuthenticodeSignature -FilePath $msi.FullName
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
if ($sig.Status -ne "Valid") {
Write-Host "[ERROR] MSI is not signed"
$allSigned = $false
}
Write-Host "Using PKCS11 config: $pkcs11Config"
# Try signing with certificate fingerprint first (if available)
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if ($fingerprint -and $fingerprint -ne "") {
Write-Host "Attempting to sign with certificate fingerprint..."
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
} else {
Write-Host "No fingerprint provided, using keypair alias..."
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
}
Write-Host "Exit code: $exitCode"
Write-Host "Output: $output"
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
Write-Host "[ERROR] Signing failed for $($file.Name)"
exit 1
}
if ($exitCode -ne 0) {
Write-Host "[ERROR] Failed to sign $($file.Name)"
Write-Host "Full error output:"
Write-Host $output
exit 1
}
$signedCount++
Write-Host "[SUCCESS] Signed: $($file.Name)"
Write-Host ""
}
Write-Host "=== Summary ==="
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
# Extract MSI and verify the inner exe (the file that actually gets installed).
# This is the critical check - AV flags the installed exe at runtime.
$msi = $msiFiles[0].FullName
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msi, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
if ($proc.ExitCode -eq 0) {
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
if ($innerExe) {
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
Write-Host "Inner EXE (from MSI): Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
if ($sig.Status -ne "Valid") {
Write-Host "[ERROR] Inner exe extracted from MSI is NOT signed - AV will flag this at runtime"
$allSigned = $false
}
} else {
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
$allSigned = $false
}
} else {
Write-Host "[ERROR] Failed to extract MSI for verification (exit code: $($proc.ExitCode))"
$allSigned = $false
}
if (-not $allSigned) {
Write-Host "[ERROR] Signature verification failed"
exit 1
}
Write-Host "[SUCCESS] MSI and installed exe are properly signed"
# Dump smctl log files on failure. Tauri's signCommand captures smctl output
# but drops stderr when the command exits non-zero, making failures opaque.
# The real errors live in smctl's log files - surface them here for debugging.
- name: Dump smctl logs on failure
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
shell: pwsh
run: |
$logDir = "$env:USERPROFILE\.signingmanager\logs"
if (Test-Path $logDir) {
Get-ChildItem $logDir | ForEach-Object {
Write-Host "=== $($_.FullName) ==="
Get-Content $_.FullName -Tail 200
Write-Host ""
}
} else {
Write-Host "smctl log directory not found at $logDir"
}
# Rename + Upload: use always() so artifacts are still collected when verify
# fails - we need them to manually inspect what actually came out of the build.
- name: Rename artifacts
if: always() && steps.digicert-setup.conclusion != 'failure'
shell: bash
run: |
mkdir -p ./dist
@@ -522,18 +578,21 @@ jobs:
# Find and rename artifacts based on platform
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
# Only ship the MSI installer on Windows. The loose exe and WiX toolset exes
# are not the user-facing installer - the MSI contains the signed inner exe.
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
else
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
find . -name "*.rpm" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.rpm" \;
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
fi
- name: Upload build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always() && steps.digicert-setup.conclusion != 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
@@ -547,30 +606,30 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Download all Tauri artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: Stirling-PDF-*
path: ./artifacts/tauri
- name: Download JAR artifact (default)
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jar
path: ./artifacts/jars
- name: Download JAR artifact (with login)
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jar-with-login
path: ./artifacts/jars
- name: Download JAR artifact (server only)
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jar-server
path: ./artifacts/jars
@@ -579,7 +638,7 @@ jobs:
run: ls -R ./artifacts
- name: Upload binaries to Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
tag_name: v${{ needs.determine-matrix.outputs.version }}
generate_release_notes: true
@@ -588,6 +647,7 @@ jobs:
./artifacts/**/*.msi
./artifacts/**/*.dmg
./artifacts/**/*.deb
./artifacts/**/*.rpm
./artifacts/**/*.AppImage
draft: false
prerelease: false
+49
View File
@@ -0,0 +1,49 @@
name: Nightly E2E Tests
on:
schedule:
- cron: "0 2 * * *" # 2 AM UTC every night
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
playwright-all-browsers:
name: Playwright (chromium + firefox + webkit)
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Install all Playwright browsers
run: task frontend:test:e2e:install
- name: Run E2E tests (all browsers)
run: task frontend:test:e2e
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-nightly-${{ github.run_id }}
path: frontend/playwright-report/
retention-days: 14
+155
View File
@@ -0,0 +1,155 @@
name: Update Package Manager Manifests
on:
release:
types: [ released ]
workflow_dispatch:
inputs:
version:
description: "Version to test (e.g. 2.9.2 — no v prefix)"
required: true
type: string
dry_run:
description: "Skip the git push at the end (safe test)"
type: boolean
default: true
permissions:
contents: read
jobs:
get-release-info:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.info.outputs.version }}
dmg_sha256: ${{ steps.hashes.outputs.dmg_sha256 }}
msi_sha256: ${{ steps.hashes.outputs.msi_sha256 }}
deb_sha256: ${{ steps.hashes.outputs.deb_sha256 }}
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Extract version from tag or manual input
id: info
env:
DISPATCH_VERSION: ${{ inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="$DISPATCH_VERSION"
else
VERSION="$RELEASE_TAG"
fi
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download release assets and compute SHA256
id: hashes
env:
VERSION: ${{ steps.info.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
BASE="https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${VERSION}"
download_sha256() {
local url="$1"
local file
file=$(basename "$url")
curl -fsSL --retry 3 -o "$file" "$url"
sha256sum "$file" | awk '{print $1}'
}
DMG_SHA=$(download_sha256 "${BASE}/Stirling-PDF-macos-universal.dmg")
MSI_SHA=$(download_sha256 "${BASE}/Stirling-PDF-windows-x86_64.msi")
DEB_SHA=$(download_sha256 "${BASE}/Stirling-PDF-linux-x86_64.deb")
JAR_SHA=$(download_sha256 "${BASE}/Stirling-PDF-with-login.jar")
echo "dmg_sha256=$DMG_SHA" >> "$GITHUB_OUTPUT"
echo "msi_sha256=$MSI_SHA" >> "$GITHUB_OUTPUT"
echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT"
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
update-homebrew-and-scoop:
needs: get-release-info
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout homebrew-stirling-pdf tap (also hosts Scoop bucket)
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
repository: Stirling-Tools/homebrew-stirling-pdf
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
# Stirling-PDF now ships a single universal DMG, so the Cask should
# have one sha256 line rather than separate on_arm/on_intel blocks.
# We rewrite every `sha256 "..."` in the Cask to the same value so
# the workflow keeps working through the Cask migration: pre-migration
# both arch blocks get the universal SHA (still correct, since both
# would resolve to the same DMG), post-migration the lone sha256 line
# is updated.
- name: Update Homebrew cask (Casks/stirling-pdf.rb)
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
DMG_SHA: ${{ needs.get-release-info.outputs.dmg_sha256 }}
run: |
CASK="tap/Casks/stirling-pdf.rb"
sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "$CASK"
sed -i "s/sha256 \"[^\"]*\"/sha256 \"${DMG_SHA}\"/g" "$CASK"
- name: Update Homebrew formula (Formula/stirling-pdf-server.rb)
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
run: |
FORMULA="tap/Formula/stirling-pdf-server.rb"
sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "$FORMULA"
sed -i "s/sha256 \"[^\"]*\"/sha256 \"${JAR_SHA}\"/" "$FORMULA"
- name: Update Scoop stirling-pdf.json
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
MSI_SHA: ${{ needs.get-release-info.outputs.msi_sha256 }}
run: |
MANIFEST="tap/scoop/stirling-pdf.json"
jq --arg v "$VERSION" --arg h "$MSI_SHA" \
'.version = $v | .architecture["64bit"].url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-windows-x86_64.msi" | .architecture["64bit"].hash = $h' \
"$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST"
- name: Update Scoop stirling-pdf-server.json
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
run: |
MANIFEST="tap/scoop/stirling-pdf-server.json"
jq --arg v "$VERSION" --arg h "$JAR_SHA" \
'.version = $v | .url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-with-login.jar" | .hash = $h' \
"$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST"
- name: Show tap diff (for dry-run visibility)
working-directory: tap
run: |
echo "--- diff --stat ---"
git diff --stat
echo "--- full diff ---"
git diff
- name: Commit and push all tap updates
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
working-directory: tap
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/stirling-pdf.rb Formula/stirling-pdf-server.rb scoop/stirling-pdf.json scoop/stirling-pdf-server.json
git diff --cached --quiet && echo "No changes" && exit 0
git commit -m "chore: bump Stirling-PDF to v${{ needs.get-release-info.outputs.version }}"
git push
+7 -59
View File
@@ -1,10 +1,11 @@
name: Pre-commit
# Runs `pre-commit run` for ruff / codespell / gitleaks / EOF / trailing-ws.
# Called from build.yml on PRs and merge_group; also runnable on demand via
# workflow_dispatch for manual local-equivalent linting.
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: read
@@ -16,12 +17,9 @@ jobs:
# Prevents sdist builds → no tar extraction
PIP_ONLY_BINARY: ":all:"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
permissions:
contents: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -31,15 +29,8 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Setup GitHub App Bot
id: setup-bot
uses: ./.github/actions/setup-bot
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.12
cache: "pip" # caching pip dependencies
@@ -57,47 +48,4 @@ jobs:
pre-commit run gitleaks --all-files -c .pre-commit-config.yaml
pre-commit run end-of-file-fixer --all-files -c .pre-commit-config.yaml
pre-commit run trailing-whitespace --all-files -c .pre-commit-config.yaml
continue-on-error: true
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 21
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with:
gradle-version: 8.14
- name: Build with Gradle
run: ./gradlew build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
- name: git add
run: |
git add .
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: ":file_folder: pre-commit"
committer: ${{ steps.setup-bot.outputs.committer }}
author: ${{ steps.setup-bot.outputs.committer }}
signoff: true
branch: pre-commit
title: "🤖 format everything with pre-commit by ${{ steps.setup-bot.outputs.app-slug }}"
body: |
Auto-generated by [create-pull-request][1] with **${{ steps.setup-bot.outputs.app-slug }}**
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true
labels: github-actions
sign-commits: true
git diff --exit-code
+122
View File
@@ -0,0 +1,122 @@
name: Push Docker Base Image
on:
push:
branches:
- baseDockerImage
- accessIssueFix
workflow_dispatch:
inputs:
version:
description: 'Base image version (e.g., 1.0.0, 1.0.1)'
required: true
type: string
permissions:
contents: read
jobs:
push-base:
if: ${{ vars.CI_PROFILE != 'lite' && github.actor == 'Frooodle' }}
runs-on: ubuntu-24.04-8core
permissions:
packages: write
id-token: write
steps:
- name: Verify authorized user
run: |
if [ "${{ github.actor }}" != "Frooodle" ]; then
echo "Error: Only Frooodle is authorized to run this workflow"
exit 1
fi
- name: Set version
id: version
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then
VERSION="1.0.3"
else
VERSION="1.0.0"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Generate tags for base image
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf-base
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-base
tags: |
type=raw,value=${{ steps.version.outputs.version }}
- name: Build and push base image
id: build-push-base
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: docker/base
file: ./docker/base/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-base
cache-to: type=gha,mode=max,scope=stirling-pdf-base
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
cosign-release: "v2.4.1"
- name: Sign base images
env:
DIGEST: ${{ steps.build-push-base.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
if [ -n "$COSIGN_PRIVATE_KEY" ]; then
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \
"${tag}@${DIGEST}"
done
else
echo "Warning: COSIGN_PRIVATE_KEY not set, skipping image signing"
fi
+39 -25
View File
@@ -33,27 +33,39 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
- name: Cache Gradle dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
gradle-version: 8.14
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
@@ -64,31 +76,31 @@ jobs:
- name: Install cosign
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
cosign-release: "v2.4.1"
- name: Install cosign
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
with:
cosign-release: "v2.4.1"
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Convert repository owner to lowercase
id: repoowner
@@ -96,7 +108,7 @@ jobs:
- name: Generate tags for latest
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@@ -110,17 +122,19 @@ jobs:
- name: Build and push Unified Dockerfile (latest variant)
id: build-push-latest
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
build-args: |
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
BASE_VERSION=1.0.0
platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
@@ -141,7 +155,7 @@ jobs:
- name: Generate tags for latest-fat
id: meta-fat
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
images: |
@@ -155,15 +169,15 @@ jobs:
- name: Build and push Unified Dockerfile (fat variant)
id: build-push-fat
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.fat
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=stirling-pdf-fat
cache-to: type=gha,mode=max,scope=stirling-pdf-fat
tags: ${{ steps.meta-fat.outputs.tags }}
labels: ${{ steps.meta-fat.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
@@ -185,7 +199,7 @@ jobs:
- name: Generate tags for ultra-lite
id: meta-lite
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
images: |
@@ -199,15 +213,15 @@ jobs:
- name: Build and push Unified Dockerfile (ultra-lite variant)
id: build-push-lite
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain'
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/embedded/Dockerfile.ultra-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=stirling-pdf-ultra-lite
cache-to: type=gha,mode=max,scope=stirling-pdf-ultra-lite
tags: ${{ steps.meta-lite.outputs.tags }}
labels: ${{ steps.meta-lite.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
+93
View File
@@ -0,0 +1,93 @@
name: Rollback Latest Tags to Version
on:
workflow_dispatch:
inputs:
version:
description: "Version to rollback to (e.g. 2.8.0)"
required: true
type: string
permissions:
contents: read
jobs:
rollback:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Install crane
uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Rollback all latest tags to v${{ inputs.version }}
env:
VERSION: ${{ inputs.version }}
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
DOCKER_HUB_ORG_USERNAME: ${{ secrets.DOCKER_HUB_ORG_USERNAME }}
REPO_OWNER: ${{ steps.repoowner.outputs.lowercase }}
run: |
set -euo pipefail
IMAGES=(
"${DOCKER_HUB_USERNAME}/s-pdf"
"ghcr.io/${REPO_OWNER}/s-pdf"
"ghcr.io/${REPO_OWNER}/stirling-pdf"
"${DOCKER_HUB_ORG_USERNAME}/stirling-pdf"
)
VARIANTS=(
"${VERSION}:latest"
"${VERSION}-fat:latest-fat"
"${VERSION}-ultra-lite:latest-ultra-lite"
)
FAILED=0
for image in "${IMAGES[@]}"; do
for variant in "${VARIANTS[@]}"; do
SOURCE_TAG="${variant%%:*}"
TARGET_TAG="${variant##*:}"
echo "::group::${image} — ${SOURCE_TAG} → ${TARGET_TAG}"
if crane manifest "${image}:${SOURCE_TAG}" > /dev/null 2>&1; then
crane cp "${image}:${SOURCE_TAG}" "${image}:${TARGET_TAG}"
echo "✅ ${image}:${TARGET_TAG} now points to ${SOURCE_TAG}"
else
echo "::warning::⚠️ ${image}:${SOURCE_TAG} not found, skipping"
FAILED=1
fi
echo "::endgroup::"
done
done
if [ "$FAILED" -ne 0 ]; then
echo "::warning::Some source tags were not found. This is expected if not all variants exist for this version."
fi
echo ""
echo "🎉 Rollback to ${VERSION} complete!"
+3 -3
View File
@@ -35,7 +35,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -67,7 +67,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
@@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3.29.5
with:
sarif_file: results.sarif
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: 30 days stale issues
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30
+7 -5
View File
@@ -27,22 +27,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Generate Swagger documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
@@ -56,6 +56,8 @@ jobs:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
SWAGGERHUB_USER: "Frooodle"
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
+5 -4
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -51,13 +51,14 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
- name: Install Python dependencies
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt -r ./.github/scripts/requirements_pre_commit.txt
run: |
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt -r ./.github/scripts/requirements_pre_commit.txt
- name: Sync translation TOML files
run: |
@@ -83,7 +84,7 @@ jobs:
- name: Create Pull Request
if: always()
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.setup-bot.outputs.token }}
commit-message: Update files
+240 -332
View File
@@ -1,6 +1,17 @@
name: Build Tauri Applications
# Multi-OS Tauri desktop bundle build matrix (Windows / macOS universal /
# Linux). Called from build.yml on PRs that touch desktop sources (gated
# via the `tauri` filter in .github/config/.files.yaml). Also runnable
# on demand via workflow_dispatch with a per-platform selector.
on:
workflow_call:
inputs:
platform:
description: "Platform to build (windows, macos, linux, or all)."
required: false
type: string
default: "all"
workflow_dispatch:
inputs:
platform:
@@ -13,15 +24,6 @@ on:
- windows
- macos
- linux
pull_request:
branches: [main, V2-tauri-windows]
paths:
- "frontend/src-tauri/**"
- "frontend/src/desktop/**"
- "frontend/tsconfig.desktop.json"
- ".github/workflows/tauri-build.yml"
push:
branches: [main]
permissions:
contents: read
@@ -35,33 +37,42 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Determine build matrix
id: set-matrix
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
PLATFORM: ${{ inputs.platform }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "${{ github.event.inputs.platform }}" in
"windows")
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"macos")
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
;;
"linux")
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
*)
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
;;
esac
else
# For PR/push events, build all platforms
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}'
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"}'
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}'
# Resolve requested platform — populated by either workflow_dispatch
# or workflow_call inputs; both paths default to "all".
case "$PLATFORM" in
windows) ENTRIES=("$WINDOWS") ;;
macos) ENTRIES=("$MACOS") ;;
linux) ENTRIES=("$LINUX") ;;
*) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;;
esac
# Drop macOS entries when Apple certificate secret is unavailable
if [ -z "$APPLE_CERTIFICATE" ]; then
echo "⚠️ APPLE_CERTIFICATE secret not available - skipping macOS builds"
FILTERED=()
for entry in "${ENTRIES[@]}"; do
[[ "$entry" != *'"macos'* ]] && FILTERED+=("$entry")
done
ENTRIES=("${FILTERED[@]}")
fi
JOINED=$(IFS=','; echo "${ENTRIES[*]}")
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
build:
needs: determine-matrix
strategy:
@@ -71,9 +82,11 @@ jobs:
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
@@ -87,7 +100,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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: "npm"
@@ -97,104 +110,59 @@ jobs:
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Set up JDK 21
# x86_64 JDK is set up first so the aarch64 step below can leave its
# JAVA_HOME as the active one. The macOS universal JRE build needs
# jmods from both arches; the x64 path is captured into the env
# before the second setup-java overwrites JAVA_HOME.
- name: Set up x86_64 JDK 25 (macOS universal JRE)
if: matrix.platform == 'macos-15'
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
architecture: "x64"
- name: Capture x86_64 JAVA_HOME
if: matrix.platform == 'macos-15'
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Build Java backend with JLink
working-directory: ./
shell: bash
run: |
chmod +x ./gradlew
echo "🔧 Building Stirling-PDF JAR..."
# STIRLING_PDF_DESKTOP_UI=false ./gradlew bootJar --no-daemon
./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube
- name: Setup Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
# Find the built JAR
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
echo "✅ Built JAR: $STIRLING_JAR"
# Build the universal JRE before desktop:prepare so the jlink:runtime
# task short-circuits on its `test -d runtime/jre` status check.
- name: Build universal macOS JRE
if: matrix.platform == 'macos-15'
env:
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
run: task desktop:jlink:universal-mac
# Create Tauri directories
mkdir -p ./frontend/src-tauri/libs
mkdir -p ./frontend/src-tauri/runtime
# Copy JAR to Tauri libs
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
echo "✅ JAR copied to Tauri libs"
# Analyze JAR dependencies for jlink modules
echo "🔍 Analyzing JAR dependencies..."
if command -v jdeps &> /dev/null; then
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
if [ -n "$DETECTED_MODULES" ]; then
echo "📋 jdeps detected modules: $DETECTED_MODULES"
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
else
echo "⚠️ jdeps analysis failed, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
else
echo "⚠️ jdeps not available, using predefined modules"
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
fi
# Create custom JRE with jlink (always rebuild)
echo "🔧 Creating custom JRE with jlink..."
echo "📋 Using modules: $MODULES"
# Remove any existing JRE
rm -rf ./frontend/src-tauri/runtime/jre
# Create the custom JRE
jlink \
--add-modules "$MODULES" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output ./frontend/src-tauri/runtime/jre
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
echo "❌ Failed to create JLink runtime"
exit 1
fi
# Test the bundled runtime
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
else
echo "❌ Custom JRE executable not found"
exit 1
fi
# Calculate runtime size
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
echo "📊 Custom JRE size: $RUNTIME_SIZE"
- name: Prepare desktop build
run: task desktop:prepare
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
- name: Install frontend dependencies
working-directory: ./frontend
run: npm ci
# DigiCert KeyLocker Setup (Cloud HSM)
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
uses: digicert/ssm-code-signing@af7f4b07694120e514fe5b19f0598af3c20256e3 # v1.2.0
uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -269,7 +237,7 @@ jobs:
}
- name: Import Apple Developer Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
if: matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -290,7 +258,7 @@ jobs:
rm certificate.p12
- name: Verify Certificate
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
if: matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
@@ -302,7 +270,7 @@ jobs:
echo "Certificate imported successfully."
- name: Check DMG creation dependencies (macOS only)
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
if: matrix.platform == 'macos-15'
run: |
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
@@ -312,8 +280,58 @@ jobs:
echo "Available tools:"
ls -la /usr/bin/hd* || echo "No hd* tools found"
- name: Preflight smctl
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
env:
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
run: |
& smctl healthcheck
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
& smctl keypair ls
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
- name: Configure Windows code signing
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: bash
env:
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
run: |
cat > ./frontend/src-tauri/tauri.windows.conf.json <<EOF
{
"bundle": {
"windows": {
"signCommand": {
"cmd": "smctl",
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
}
}
}
}
EOF
- name: Import release GPG signing key (Linux)
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
run: |
echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
gpg --list-secret-keys --keyid-format=long
- name: Make libjvm discoverable for linuxdeploy (Linux AppImage)
if: matrix.platform == 'ubuntu-22.04'
run: |
JAVA_LIBJVM="$JAVA_HOME/lib/server/libjvm.so"
if [ -f "$JAVA_LIBJVM" ]; then
sudo ln -sf "$JAVA_LIBJVM" /usr/lib/libjvm.so
echo "Linked libjvm from $JAVA_LIBJVM -> /usr/lib/libjvm.so"
else
echo "libjvm not found at $JAVA_LIBJVM"
exit 1
fi
- name: Build Tauri app
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -322,180 +340,64 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
# AppImage signing — three env vars work together:
# SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool
# APPIMAGETOOL_SIGN_PASSPHRASE appimagetool uses this to unlock the GPG key non-interactively
# SIGN_KEY appimagetool picks the key matching this fingerprint
# Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present.
# Mirror the Windows/macOS gate: only sign when secret is present AND ref is main (skips PRs from forks/Dependabot).
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
# Only enable Windows signing in Tauri when on main
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: ${{ matrix.args }}
# Linux: build deb+rpm only here. AppImage runs in its own
# continue-on-error step below so its persistent linuxdeploy
# failure (#6127 onwards) does not tank deb/rpm uploads.
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
# Sign with DigiCert KeyLocker (post-build)
- name: Sign Windows binaries with DigiCert KeyLocker
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
shell: pwsh
# AppImage is decoupled so its linuxdeploy run gets a fresh process
# (rpm scratch state torn down) and its failure can't tank deb/rpm.
- name: Build Tauri app (Linux AppImage)
if: matrix.platform == 'ubuntu-22.04'
continue-on-error: true
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }} # gitleaks:allow
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
CI: true
with:
projectPath: ./frontend
tauriScript: npx tauri
args: --bundles appimage
- name: Clear release GPG key from runner keyring (Linux)
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
env:
RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
run: |
Write-Host "=== DigiCert KeyLocker Signing ==="
# Test smctl connectivity first
Write-Host "Testing smctl connection..."
$healthCheck = & smctl healthcheck 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
} else {
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
Write-Host $healthCheck
exit 1
}
Write-Host ""
# Sync certificates to Windows certificate store
Write-Host "Syncing certificates to Windows certificate store..."
$syncOutput = & smctl windows certsync 2>&1
Write-Host "Cert sync result: $syncOutput"
Write-Host ""
# List available certificates and check if they have certificates attached
Write-Host "Checking for available certificates..."
$certList = & smctl keypair ls 2>&1
Write-Host "Keypair list output:"
Write-Host $certList
Write-Host ""
# Parse the output to check certificate status
$lines = $certList -split "`n"
$foundKeypair = $false
$hasCertificate = $false
foreach ($line in $lines) {
if ($line -match "${{ secrets.SM_KEYPAIR_ALIAS }}") {
$foundKeypair = $true
Write-Host "[SUCCESS] Found keypair in list"
# Check if this line has certificate info (not just empty spaces after alias)
$parts = $line -split "\s+"
if ($parts.Count -gt 2 -and $parts[1] -ne "" -and $parts[1] -ne "CERTIFICATE") {
$hasCertificate = $true
Write-Host "[SUCCESS] Certificate is associated with keypair"
}
}
}
if (-not $foundKeypair) {
Write-Host "[ERROR] Keypair not found: ${{ secrets.SM_KEYPAIR_ALIAS }}"
Write-Host "Available keypairs are listed above"
Write-Host ""
Write-Host "Please verify:"
Write-Host " 1. Keypair alias is correct in GitHub secret"
Write-Host " 2. API key has access to this keypair"
exit 1
}
if (-not $hasCertificate) {
Write-Host "[ERROR] No certificate associated with keypair"
Write-Host "This usually means:"
Write-Host " 1. Certificate not yet synced to KeyLocker (run sync manually)"
Write-Host " 2. Certificate is pending approval"
Write-Host " 3. Certificate needs to be attached to the keypair"
Write-Host ""
Write-Host "Try running in DigiCert ONE portal:"
Write-Host " smctl keypair sync"
exit 1
}
Write-Host "[SUCCESS] Certificate check passed"
Write-Host ""
# Find only the files we need to sign (not build scripts)
$filesToSign = @()
# Main application executable
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
if ($mainExe) { $filesToSign += $mainExe }
# MSI installer
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
$filesToSign += $msiFiles
if ($filesToSign.Count -eq 0) {
Write-Host "[ERROR] No files found to sign"
exit 1
}
Write-Host "Found $($filesToSign.Count) files to sign:"
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
Write-Host ""
$signedCount = 0
foreach ($file in $filesToSign) {
Write-Host "Signing: $($file.Name)"
# Get PKCS11 config file path (set by DigiCert action)
$pkcs11Config = $env:PKCS11_CONFIG
if (-not $pkcs11Config) {
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
Write-Host "DigiCert KeyLocker action may not have run correctly"
exit 1
}
Write-Host "Using PKCS11 config: $pkcs11Config"
# Try signing with certificate fingerprint first (if available)
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if ($fingerprint -and $fingerprint -ne "") {
Write-Host "Attempting to sign with certificate fingerprint..."
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
} else {
Write-Host "No fingerprint provided, using keypair alias..."
# Use smctl to sign with keypair alias
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
$exitCode = $LASTEXITCODE
}
Write-Host "Exit code: $exitCode"
Write-Host "Output: $output"
# Check if output contains "FAILED" even with exit code 0
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
Write-Host ""
Write-Host "[ERROR] Signing failed for $($file.Name)"
Write-Host "[ERROR] smctl returned success but output indicates failure"
Write-Host ""
Write-Host "Possible issues:"
Write-Host " 1. Certificate not fully synced to KeyLocker (wait a few minutes)"
Write-Host " 2. Incorrect keypair alias"
Write-Host " 3. API key lacks signing permissions"
Write-Host ""
Write-Host "Please verify in DigiCert ONE portal:"
Write-Host " - Certificate status is 'Issued' (not Pending)"
Write-Host " - Keypair status is 'Online'"
Write-Host " - 'Can sign' is set to 'Yes'"
exit 1
}
if ($exitCode -ne 0) {
Write-Host "[ERROR] Failed to sign $($file.Name)"
Write-Host "Full error output:"
Write-Host $output
exit 1
}
$signedCount++
Write-Host "[SUCCESS] Signed: $($file.Name)"
Write-Host ""
}
Write-Host "=== Summary ==="
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then
gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true
gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true
fi
- name: Verify notarization (macOS only)
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
if: matrix.platform == 'macos-15'
run: |
echo "🔍 Verifying notarization status..."
cd ./frontend/src-tauri/target
@@ -517,77 +419,84 @@ jobs:
# Find and rename artifacts based on platform
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
# Only ship the MSI installer. The loose exe and WiX toolset exes
# are not the user-facing installer - the MSI contains the signed inner exe.
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
else
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
find . -name "*.rpm" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.rpm" \;
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
fi
# Verify the MSI AND the inner exe extracted from it are signed.
# The inner exe is what gets installed on users' machines and what AV scans.
- name: Verify Windows Code Signature
if: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main'
if: matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
shell: pwsh
run: |
Write-Host "Verifying Windows code signatures..."
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}.exe"
$allSigned = $true
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
$allSigned = $true
$usingKeyLocker = "${{ env.SM_API_KEY }}" -ne ""
$usingPfx = "${{ env.WINDOWS_CERTIFICATE }}" -ne ""
# Check EXE signature
if (Test-Path $exePath) {
$exeSig = Get-AuthenticodeSignature -FilePath $exePath
Write-Host "EXE Signature Status: $($exeSig.Status)"
Write-Host "EXE Signer: $($exeSig.SignerCertificate.Subject)"
Write-Host "EXE Timestamp: $($exeSig.TimeStamperCertificate.NotAfter)"
if ($exeSig.Status -ne "Valid") {
Write-Host "[WARNING] EXE is not properly signed (Status: $($exeSig.Status))"
if ($usingKeyLocker -or $usingPfx) {
Write-Host "[ERROR] Certificate was provided but signing failed"
$allSigned = $false
} else {
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
}
} else {
Write-Host "[SUCCESS] EXE is properly signed"
}
}
# Check MSI signature
# Check MSI (outer wrapper)
if (Test-Path $msiPath) {
$msiSig = Get-AuthenticodeSignature -FilePath $msiPath
Write-Host "MSI Signature Status: $($msiSig.Status)"
Write-Host "MSI Signer: $($msiSig.SignerCertificate.Subject)"
Write-Host "MSI Timestamp: $($msiSig.TimeStamperCertificate.NotAfter)"
$sig = Get-AuthenticodeSignature -FilePath $msiPath
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
if ($sig.Status -ne "Valid") {
Write-Host "[ERROR] MSI is not signed"
$allSigned = $false
}
if ($msiSig.Status -ne "Valid") {
Write-Host "[WARNING] MSI is not properly signed (Status: $($msiSig.Status))"
if ($usingKeyLocker -or $usingPfx) {
Write-Host "[ERROR] Certificate was provided but signing failed"
$allSigned = $false
# Extract MSI and verify inner exe
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msiPath, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
if ($proc.ExitCode -eq 0) {
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
if ($innerExe) {
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
Write-Host "Inner EXE: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
if ($sig.Status -ne "Valid") {
Write-Host "[ERROR] Inner exe is NOT signed - AV will flag this at runtime"
$allSigned = $false
}
} else {
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
$allSigned = $false
}
} else {
Write-Host "[SUCCESS] MSI is properly signed"
Write-Host "[ERROR] MSI extraction failed (exit code: $($proc.ExitCode))"
$allSigned = $false
}
} else {
Write-Host "[ERROR] MSI not found at $msiPath"
$allSigned = $false
}
if (($usingKeyLocker -or $usingPfx) -and -not $allSigned) {
Write-Host "[ERROR] Code signing verification failed"
if (-not $allSigned) {
Write-Host "[ERROR] Signature verification failed"
exit 1
}
Write-Host "[SUCCESS] MSI and inner exe are properly signed"
- name: Dump smctl logs on failure
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
shell: pwsh
run: |
$logDir = "$env:USERPROFILE\.signingmanager\logs"
if (Test-Path $logDir) {
Get-ChildItem $logDir | ForEach-Object {
Write-Host "=== $($_.FullName) ==="
Get-Content $_.FullName -Tail 200
Write-Host ""
}
} else {
Write-Host "[SUCCESS] Code signature verification completed"
Write-Host "smctl log directory not found at $logDir"
}
- name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Stirling-PDF-${{ matrix.name }}
path: ./dist/*
@@ -606,7 +515,7 @@ jobs:
echo "❌ No Windows executable found"
exit 1
fi
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
echo "Checking for macOS artifacts..."
find . -name "*.dmg" | head -5
if [ $(find . -name "*.dmg" | wc -l) -eq 0 ]; then
@@ -615,8 +524,8 @@ jobs:
fi
else
echo "Checking for Linux artifacts..."
find . -name "*.deb" -o -name "*.AppImage" | head -5
if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | head -5
if [ $(find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
echo "❌ No Linux artifacts found"
exit 1
fi
@@ -629,7 +538,7 @@ jobs:
run: |
cd ./frontend/src-tauri/target
echo "Artifact sizes for ${{ matrix.name }}:"
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
if [ -f "$file" ]; then
size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown")
echo "$file: $size bytes"
@@ -648,12 +557,12 @@ jobs:
pull-requests: write
steps:
- name: Harden the runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Post/Update PR Comment with Download Links
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
@@ -671,9 +580,8 @@ 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-macos-aarch64': { icon: '🍎', platform: 'macOS ARM64', files: '.dmg' },
'Stirling-PDF-macos-x86_64': { icon: '🍎', platform: 'macOS Intel', files: '.dmg' },
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .AppImage' }
'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' },
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' }
};
let commentBody = `## 📦 Tauri Desktop Builds Ready!\n\n`;
@@ -740,7 +648,7 @@ jobs:
if: always()
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
+162
View File
@@ -0,0 +1,162 @@
name: Build Docker images (PR test)
# Reusable workflow called from build.yml on PRs to verify the three
# embedded Dockerfiles (default, ultra-lite, fat) still build cleanly,
# optionally against a freshly-built base image when the PR touches the
# base Dockerfile.
on:
workflow_call:
inputs:
docker-base-changed:
description: "Whether the docker base image changed (forwarded from files-changed)."
required: false
type: string
default: "false"
permissions:
contents: read
jobs:
# TODO: extract a pre-matrix `prepare` job that runs once and produces
# shared artifacts for the three matrix entries below to consume:
# 1. `task backend:build` — currently runs 3× in parallel with
# identical env (DISABLE_ADDITIONAL_FEATURES=true,
# STIRLING_PDF_DESKTOP_UI=false). Build once, upload the JAR as an
# artifact, matrix entries download.
# 2. The base-image `docker build` (gated on docker-base-changed) —
# currently runs 3× in parallel against the same Dockerfile and
# context. Build once, `docker save` to an artifact, matrix entries
# `docker load` before the embedded build.
# Saves ~2 full backend builds + 2 base-image builds per PR that touches
# docker. May also be reusable from backend-build.yml's jdk-25 +
# spring-security=true matrix entry if `task backend:build` and
# `task backend:build:ci` produce equivalent JARs (verify before wiring).
test-build-docker-images:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- docker-rev: docker/embedded/Dockerfile
artifact-suffix: Dockerfile
cache-scope: stirling-pdf-latest
- docker-rev: docker/embedded/Dockerfile.ultra-lite
artifact-suffix: Dockerfile.ultra-lite
cache-scope: stirling-pdf-ultra-lite
- docker-rev: docker/embedded/Dockerfile.fat
artifact-suffix: Dockerfile.fat
cache-scope: stirling-pdf-fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Free disk space on runner
run: |
echo "Disk space before cleanup:" && df -h
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
docker system prune -af || true
echo "Disk space after cleanup:" && df -h
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "25"
distribution: "temurin"
- name: Cache Gradle dependency artifacts
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
cache-disabled: true
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Build application
run: task backend:build
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
DISABLE_ADDITIONAL_FEATURES: true
STIRLING_PDF_DESKTOP_UI: false
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build base image locally (PR base change only)
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
run: |
docker build -t stirling-pdf-base:pr-test -f docker/base/Dockerfile docker/base
- name: Set base image and platform for this build
id: build-params
# Pass workflow inputs through env vars rather than expanding `${{ }}`
# directly into the shell — defense-in-depth against template injection
# if any upstream provider of these values ever becomes less trusted.
# GITHUB_EVENT_NAME is already provided by the runner.
env:
DOCKER_BASE_CHANGED: ${{ inputs.docker-base-changed }}
run: |
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ "$DOCKER_BASE_CHANGED" = "true" ]; then
echo "base_image=stirling-pdf-base:pr-test" >> "$GITHUB_OUTPUT"
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
else
echo "base_image=stirlingtools/stirling-pdf-base:latest" >> "$GITHUB_OUTPUT"
echo "platforms=linux/amd64,linux/arm64/v8" >> "$GITHUB_OUTPUT"
fi
- name: Build ${{ matrix.docker-rev }}
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./${{ matrix.docker-rev }}
push: false
cache-from: type=gha,scope=${{ matrix.cache-scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
platforms: ${{ steps.build-params.outputs.platforms }}
build-args: |
BASE_IMAGE=${{ steps.build-params.outputs.base_image }}
provenance: true
sbom: true
- name: Upload Reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: reports-docker-${{ matrix.artifact-suffix }}
path: |
build/reports/tests/
build/test-results/
build/reports/problems/
retention-days: 3
if-no-files-found: warn
+17 -15
View File
@@ -25,23 +25,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 8.14
gradle-version: 9.3.1
- name: Build with Gradle
run: ./gradlew build
@@ -52,7 +52,7 @@ jobs:
DISABLE_ADDITIONAL_FEATURES: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Get version number
id: versionNumber
@@ -61,17 +61,19 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push test image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./docker/embedded/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-latest
cache-to: type=gha,mode=max,scope=stirling-pdf-latest
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64
@@ -129,14 +131,14 @@ jobs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
filters: ".github/config/.files.yaml"
@@ -148,14 +150,14 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
@@ -165,9 +167,9 @@ jobs:
with:
key: ${{secrets.TESTDRIVER_API_KEY}}
prerun: |
choco install go-task -y
task frontend:build
cd frontend
npm install
npm run build
npm install dashcam-chrome --save
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337"
Start-Sleep -Seconds 20
@@ -184,7 +186,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit
+27 -1
View File
@@ -29,8 +29,14 @@ clientWebUI/
exampleYmlFiles/stirling/
/stirling/
/testing/file_snapshots
/testing/cucumber/junit/
/testing/cucumber/report.html
/testing/.failed_tests
SwaggerDoc.json
# Runtime storage for uploaded files and user data (not Java source code)
app/core/storage/
# Frontend build artifacts copied to backend static resources
# These are generated by npm build and should not be committed
app/core/src/main/resources/static/assets/
@@ -44,6 +50,7 @@ app/core/src/main/resources/static/samples/
app/core/src/main/resources/static/manifest-classic.json
app/core/src/main/resources/static/robots.txt
app/core/src/main/resources/static/pdfium/
app/core/src/main/resources/static/pdfjs/
app/core/src/main/resources/static/vendor/
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
@@ -157,7 +164,6 @@ __pycache__/
*.pyo
# Virtual environments
.env*
.venv*
env*/
venv*/
@@ -165,6 +171,9 @@ ENV/
env.bak/
venv.bak/
# Env files (secrets / local overrides). Subproject .gitignore files whitelist any committed defaults.
.env*
# VS Code
/.vscode/**/*
!/.vscode/settings.json
@@ -174,6 +183,7 @@ venv.bak/
.idea/
*.iml
out/
.junie/
# Ignore Mac DS_Store files
.DS_Store
@@ -196,6 +206,9 @@ out/
*.jks
*.asc
# Allow test fixture certificates (synthetic, no real credentials)
!frontend/src/core/tests/test-fixtures/certs/**
# SSH Keys
*.pub
*.priv
@@ -206,8 +219,14 @@ id_ecdsa.pub
id_ed25519
id_ed25519.pub
.ssh/
# Allow the published GPG release signing public key (safe to share)
!docs/security/signing-key.pub
*ssh
# Taskfile checksum cache
.task/
# cache
.cache
.ruff_cache
@@ -244,3 +263,10 @@ docs/type3/signatures/
# Type3 sample PDFs (development only)
**/type3/samples/
# Claude
.claude/
# Playwright MCP screenshots / traces
.playwright-mcp/
*.playwright-mcp.png
+4 -4
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
rev: v0.15.11
hooks:
- id: ruff
args:
@@ -12,11 +12,11 @@ repos:
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
exclude: (split_photos.py)
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
rev: v2.4.2
hooks:
- id: codespell
args:
- --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist
- --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment
- --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2
files: \.(html|css|js|py|md)$
@@ -35,7 +35,7 @@ repos:
files: ^.*(\.js|\.java|\.py|\.yml)$
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
- repo: https://github.com/pappasam/toml-sort
rev: v0.24.3
rev: v0.24.4
hooks:
- id: toml-sort-fix
files: frontend/public/locales/.*\.toml$
+122
View File
@@ -0,0 +1,122 @@
version: '3'
tasks:
dev:
desc: "Start backend dev server"
ignore_error: true
vars:
PORT: '{{.PORT | default "8080"}}'
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
env:
SERVER_PORT: '{{.PORT}}'
cmds:
- cmd: '{{if .AIENGINE_URL}}set "AIENGINE_URL={{.AIENGINE_URL}}" && set "AIENGINE_ENABLED=true" && {{end}}cmd /c gradlew.bat :stirling-pdf:bootRun'
platforms: [windows]
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun'
platforms: [linux, darwin]
build:
desc: "Full backend build"
cmds:
- cmd: cmd /c gradlew.bat clean build
platforms: [windows]
- cmd: ./gradlew clean build
platforms: [linux, darwin]
build:fast:
desc: "Build without tests"
cmds:
- cmd: cmd /c gradlew.bat clean build -x test
platforms: [windows]
- cmd: ./gradlew clean build -x test
platforms: [linux, darwin]
build:ci:
desc: "Build for CI (formatting checked separately)"
cmds:
- cmd: cmd /c gradlew.bat build -PnoSpotless
platforms: [windows]
- cmd: ./gradlew build -PnoSpotless
platforms: [linux, darwin]
test:
desc: "Run backend tests"
cmds:
- cmd: cmd /c gradlew.bat test
platforms: [windows]
- cmd: ./gradlew test
platforms: [linux, darwin]
format:
desc: "Auto-fix code formatting"
cmds:
- cmd: cmd /c gradlew.bat spotlessApply
platforms: [windows]
- cmd: ./gradlew spotlessApply
platforms: [linux, darwin]
format:check:
desc: "Check code formatting"
cmds:
- cmd: cmd /c gradlew.bat spotlessCheck
platforms: [windows]
- cmd: ./gradlew spotlessCheck
platforms: [linux, darwin]
fix:
desc: "Auto-fix backend"
cmds:
- task: format
swagger:
desc: "Generate OpenAPI docs"
cmds:
- cmd: cmd /c gradlew.bat :stirling-pdf:copySwaggerDoc
platforms: [windows]
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
platforms: [linux, darwin]
sources:
- app/core/src/main/java/**/*.java
- app/proprietary/src/main/java/**/*.java
- app/common/src/main/java/**/*.java
generates:
- SwaggerDoc.json
check:
desc: "Backend quality gate"
cmds:
- task: format:check
- task: test
version:
desc: "Print project version"
silent: true
cmds:
- cmd: cmd /c gradlew.bat printVersion --quiet | tail -1
platforms: [windows]
- cmd: ./gradlew printVersion --quiet | tail -1
platforms: [linux, darwin]
licenses:check:
desc: "Check dependency licenses"
cmds:
- cmd: cmd /c gradlew.bat checkLicense --no-parallel
platforms: [windows]
- cmd: ./gradlew checkLicense --no-parallel
platforms: [linux, darwin]
licenses:generate:
desc: "Check and generate dependency license report"
cmds:
- cmd: cmd /c gradlew.bat checkLicense generateLicenseReport --no-parallel
platforms: [windows]
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
platforms: [linux, darwin]
clean:
desc: "Clean build artifacts"
cmds:
- cmd: cmd /c gradlew.bat clean
platforms: [windows]
- cmd: ./gradlew clean
platforms: [linux, darwin]
+129
View File
@@ -0,0 +1,129 @@
version: '3'
vars:
# Single source of truth for the jlink module list (also read by
# frontend/scripts/build-universal-mac-jre.sh during CI). The desktop
# taskfile runs with dir: frontend (see Taskfile.yml), so this path
# is relative to that directory.
JLINK_MODULES:
sh: tr -d '\n' < src-tauri/jlink-modules.txt
tasks:
prepare:
desc: "Prepare desktop build dependencies"
deps:
- jlink
- task: ":frontend:prepare"
vars: { MODE: desktop }
- provisioner
provisioner:
desc: "Build installer provisioner"
platforms: [windows]
cmds:
- node scripts/build-provisioner.mjs
dev:
desc: "Start Tauri desktop dev mode"
deps: [prepare]
ignore_error: true
cmds:
- npx tauri dev --no-watch
build:
desc: "Build Tauri desktop app (production)"
deps: [prepare]
cmds:
- npx tauri build
build:dev:
desc: "Build Tauri desktop app (dev, no bundling)"
deps: [prepare]
cmds:
- npx tauri build --no-bundle
build:dev:mac:
desc: "Build Tauri desktop .app bundle (macOS)"
deps: [prepare]
cmds:
- npx tauri build --bundles app
build:dev:windows:
desc: "Build Tauri desktop NSIS installer (Windows)"
deps: [prepare]
cmds:
- npx tauri build --bundles nsis
build:dev:linux:
desc: "Build Tauri desktop AppImage (Linux)"
deps: [prepare]
cmds:
- npx tauri build --bundles appimage
clean:
desc: "Clean Tauri/Cargo build artifacts"
cmds:
- task: jlink:clean
- cd src-tauri && cargo clean
- rm -rf dist build
# ============================================================
# JLink — Build bundled Java runtime for Tauri
# ============================================================
jlink:
desc: "Build backend JAR and create JLink runtime for Tauri"
deps: [jlink:jar, jlink:runtime]
jlink:jar:
desc: "Build backend JAR for Tauri bundling"
run: once
dir: ..
env:
DISABLE_ADDITIONAL_FEATURES: "true"
cmds:
- cmd: cmd /c gradlew.bat bootJar --no-daemon
platforms: [windows]
- cmd: ./gradlew bootJar --no-daemon
platforms: [linux, darwin]
- mkdir -p frontend/src-tauri/libs
- cp app/core/build/libs/stirling-pdf-*.jar frontend/src-tauri/libs/
status:
- test -f frontend/src-tauri/libs/stirling-pdf-*.jar
jlink:runtime:
desc: "Create custom JRE with jlink"
deps: [jlink:jar]
cmds:
- rm -rf src-tauri/runtime/jre
- mkdir -p src-tauri/runtime
- >-
jlink
--add-modules {{.JLINK_MODULES}}
--strip-debug
--compress=zip-6
--no-header-files
--no-man-pages
--output src-tauri/runtime/jre
status:
- test -d src-tauri/runtime/jre
jlink:clean:
desc: "Remove JLink runtime and bundled JARs"
cmds:
- rm -rf src-tauri/libs src-tauri/runtime
# macOS-only. Replaces jlink:runtime's single-arch JRE with a universal
# (arm64 + x86_64) one for the universal Tauri shell. Runs the x86_64
# jlink under Rosetta on Apple Silicon, so it is opt-in and not part of
# the default desktop:build flow. Requires AARCH64_JAVA_HOME and
# X64_JAVA_HOME to point at matching JDK installations with jmods/.
jlink:universal-mac:
desc: "Create universal (arm64+x86_64) JRE for the macOS Tauri build"
deps: [jlink:jar]
platforms: [darwin]
env:
JLINK_MODULES: "{{.JLINK_MODULES}}"
OUTPUT_DIR: src-tauri/runtime/jre
cmds:
- scripts/build-universal-mac-jre.sh
+57
View File
@@ -0,0 +1,57 @@
version: '3'
vars:
COMPOSE_DIR: docker/compose
EMBEDDED_DIR: docker/embedded
tasks:
build:
desc: "Build standard Docker image"
cmds:
- docker build -t stirling-pdf -f {{.EMBEDDED_DIR}}/Dockerfile .
build:fat:
desc: "Build fat Docker image (all features)"
cmds:
- docker build -t stirling-pdf-fat -f {{.EMBEDDED_DIR}}/Dockerfile.fat .
build:ultra-lite:
desc: "Build ultra-lite Docker image"
cmds:
- docker build -t stirling-pdf-ultra-lite -f {{.EMBEDDED_DIR}}/Dockerfile.ultra-lite .
build:frontend:
desc: "Build frontend-only Docker image"
cmds:
- docker build -t stirling-pdf-frontend -f docker/frontend/Dockerfile .
build:engine:
desc: "Build engine Docker image"
dir: engine
cmds:
- docker build -t stirling-pdf-engine .
up:
desc: "Start standard docker compose stack"
cmds:
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml up -d
up:fat:
desc: "Start fat docker compose stack"
cmds:
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.fat.yml up -d
up:ultra-lite:
desc: "Start ultra-lite docker compose stack"
cmds:
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.ultra-lite.yml up -d
down:
desc: "Stop all running docker compose stacks"
cmds:
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml down
logs:
desc: "Tail docker compose logs"
cmds:
- docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml logs -f
+131
View File
@@ -0,0 +1,131 @@
version: '3'
tasks:
install:
desc: "Install engine dependencies"
run: once
cmds:
- uv python install 3.13.8
- uv sync
sources:
- uv.lock
- pyproject.toml
status:
- test -d .venv
prepare:
desc: "Set up engine .env from template"
deps: [install]
cmds:
- uv run scripts/setup_env.py
sources:
- scripts/setup_env.py
generates:
- .env.local
run:
desc: "Run engine server"
deps: [prepare]
ignore_error: true
dir: src
vars:
PORT: '{{.PORT | default "5001"}}'
env:
PYTHONUNBUFFERED: "1"
cmds:
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}}
dev:
desc: "Start engine dev server with hot reload"
deps: [prepare]
ignore_error: true
dir: src
vars:
PORT: '{{.PORT | default "5001"}}'
env:
PYTHONUNBUFFERED: "1"
cmds:
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
lint:
desc: "Run linting"
deps: [install]
cmds:
- uv run ruff check .
lint:fix:
desc: "Auto-fix lint issues"
deps: [install]
cmds:
- uv run ruff check . --fix
format:
desc: "Auto-fix code formatting"
deps: [install]
cmds:
- uv run ruff format .
format:check:
desc: "Check code formatting"
deps: [install]
cmds:
- uv run ruff format . --diff
typecheck:
desc: "Run type checking"
deps: [install]
cmds:
- uv run pyright . --warnings
test:
desc: "Run tests"
deps: [prepare]
cmds:
- uv run pytest tests
fix:
desc: "Auto-fix lint + format"
cmds:
- task: lint:fix
- task: format
check:
desc: "Full engine quality gate"
cmds:
- task: typecheck
- task: lint
- task: format:check
- task: test
tool-models:
desc: "Generate tool_models.py from Java OpenAPI spec (SwaggerDoc.json)"
deps: [install, ":backend:swagger"]
cmds:
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py
sources:
- ../SwaggerDoc.json
- scripts/generate_tool_models.py
generates:
- src/stirling/models/tool_models.py
clean:
desc: "Clean build artifacts"
cmds:
- task: '{{if eq .OS "Windows_NT"}}clean-windows{{else}}clean-unix{{end}}'
clean-unix:
internal: true
desc: "Clean build artifacts"
cmds:
- rm -rf .venv data logs output
# On Windows, use PowerShell as bash failed to delete some dependencies
clean-windows:
internal: true
desc: "Clean build artifacts"
ignore_error: true
cmds:
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue .venv
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue data
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue logs
- powershell rm -Recurse -Force -ErrorAction SilentlyContinue output
+307
View File
@@ -0,0 +1,307 @@
version: '3'
tasks:
install:
desc: "Install dependencies"
run: once
cmds:
- '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}'
sources:
- package-lock.json
- package.json
status:
- test -d node_modules
env:
CI: '{{ .CI | default "false" }}'
prepare:env:
internal: true
run: when_changed
deps: [install]
vars:
MODE: '{{.MODE | default ""}}'
cmds:
- npx tsx scripts/setup-env.ts{{if .MODE}} --{{.MODE}}{{end}}
sources:
- scripts/setup-env.ts
generates:
- .env.local
- .env{{if .MODE}}.{{.MODE}}{{end}}.local
prepare:icons:
internal: true
run: once
deps: [install]
cmds:
- node scripts/generate-icons.js
prepare:
desc: "Set up dev environment"
run: when_changed
vars:
MODE: '{{.MODE | default ""}}'
deps:
- task: prepare:env
vars: { MODE: '{{.MODE}}' }
- prepare:icons
# ============================================================
# Development
# ============================================================
dev:_run:
internal: true
ignore_error: true
vars:
MODE: '{{.MODE}}'
PORT: '{{.PORT | default "5173"}}'
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
OPEN: '{{.OPEN | default ""}}'
env:
BACKEND_URL: '{{.BACKEND_URL}}'
cmds:
- npx vite --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
dev:
desc: "Start frontend dev server"
cmds:
- task: dev:proprietary
vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
dev:core:
desc: "Start frontend dev server in core mode"
deps: [prepare]
cmds:
- task: dev:_run
vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
dev:proprietary:
desc: "Start frontend dev server in proprietary mode"
deps: [prepare]
cmds:
- task: dev:_run
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
dev:saas:
desc: "Start frontend dev server in SaaS mode"
deps:
- task: prepare
vars: { MODE: saas }
cmds:
- task: dev:_run
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
dev:desktop:
desc: "Start frontend dev server in desktop mode"
deps:
- task: prepare
vars: { MODE: desktop }
cmds:
- task: dev:_run
vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
dev:prototypes:
desc: "Start frontend dev server in prototypes mode"
deps: [prepare]
cmds:
- task: dev:_run
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
# ============================================================
# Build
# ============================================================
build:
desc: "Production build (default mode)"
deps: [prepare]
cmds:
- npx vite build
build:core:
desc: "Build for core mode"
deps: [prepare]
cmds:
- npx vite build --mode core
build:proprietary:
desc: "Build for proprietary mode"
deps: [prepare]
cmds:
- npx vite build --mode proprietary
build:saas:
desc: "Build for SaaS mode"
deps:
- task: prepare
vars: { MODE: saas }
cmds:
- npx vite build --mode saas
build:desktop:
desc: "Build for desktop mode"
deps:
- task: prepare
vars: { MODE: desktop }
cmds:
- npx vite build --mode desktop
build:prototypes:
desc: "Build for prototypes mode"
deps: [prepare]
cmds:
- npx vite build --mode prototypes
# ============================================================
# Code quality
# ============================================================
lint:
desc: "Run linting"
deps: [install]
cmds:
- npx eslint --max-warnings=0
- npx dpdm src --circular --no-warning --no-tree --exit-code circular:1
lint:fix:
desc: "Auto-fix lint issues"
deps: [install]
cmds:
- npx eslint --fix
format:
desc: "Auto-fix code formatting"
deps: [install]
cmds:
- npx prettier --write .
format:check:
desc: "Check code formatting"
deps: [install]
cmds:
- npx prettier --check .
fix:
desc: "Auto-fix lint and format"
cmds:
- task: format
- task: lint:fix
typecheck:
desc: "Typecheck default build of the app"
cmds:
- task: typecheck:proprietary
typecheck:core:
desc: "Typecheck core build variant"
deps: [prepare]
cmds:
- npx tsc --noEmit --project src/core/tsconfig.json
typecheck:proprietary:
desc: "Typecheck proprietary build variant"
deps: [prepare]
cmds:
- npx tsc --noEmit --project src/proprietary/tsconfig.json
typecheck:saas:
desc: "Typecheck SaaS build variant"
deps:
- task: prepare
vars: { MODE: saas }
cmds:
- npx tsc --noEmit --project src/saas/tsconfig.json
typecheck:desktop:
desc: "Typecheck desktop build variant"
deps:
- task: prepare
vars: { MODE: desktop }
cmds:
- npx tsc --noEmit --project src/desktop/tsconfig.json
typecheck:scripts:
desc: "Typecheck scripts"
deps: [prepare]
cmds:
- npx tsc --noEmit --project scripts/tsconfig.json
typecheck:prototypes:
desc: "Typecheck prototypes build variant"
deps: [prepare]
cmds:
- npx tsc --noEmit --project src/prototypes/tsconfig.json
typecheck:all:
desc: "Typecheck all build variants"
cmds:
- task: typecheck:core
- task: typecheck:proprietary
- task: typecheck:saas
- task: typecheck:desktop
- task: typecheck:scripts
- task: typecheck:prototypes
# ============================================================
# Quality Gate
# ============================================================
check:
desc: "Quick quality gate for local development"
cmds:
- task: typecheck
- task: lint
- task: format:check
- task: test
check:all:
desc: "Full CI quality gate"
cmds:
- task: typecheck:all
- task: lint
- task: format:check
- task: build
- task: test
# ============================================================
# Test
# ============================================================
test:
desc: "Run tests"
deps: [install]
cmds:
- npx vitest run
test:watch:
desc: "Run tests in watch mode"
deps: [install]
cmds:
- npx vitest --watch
test:coverage:
desc: "Run tests with coverage"
deps: [install]
cmds:
- npx vitest --coverage
test:e2e:
desc: "Run E2E tests"
deps: [prepare]
cmds:
- npx playwright test {{.CLI_ARGS}}
test:e2e:install:
desc: "Install E2E test browsers"
deps: [install]
cmds:
- npx playwright install {{.CLI_ARGS}} --with-deps
# ============================================================
# Code Generation
# ============================================================
licenses:generate:
desc: "Generate frontend license report"
deps: [install]
cmds:
- node scripts/generate-licenses.js
+460
View File
@@ -0,0 +1,460 @@
# AGENTS.md
This file provides guidance to AI Agents when working with code in this repository.
## Taskfile (Recommended)
This project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.
Task `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.
### Quick Reference
- `task install` — install all dependencies
- `task dev` — start backend + frontend concurrently
- `task dev:all` — start backend + frontend + engine concurrently
- `task build` — build all components
- `task test` — run all tests (backend + frontend + engine)
- `task lint` — run all linters
- `task format` — auto-fix formatting across all components
- `task check` — full quality gate (lint + typecheck + test)
- `task clean` — clean all build artifacts
- `task docker:build` — build standard Docker image
- `task docker:up` — start Docker compose stack
## Common Development Commands
### Build and Test
- **Build project**: `task build`
- **Run backend locally**: `task backend:dev`
- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`)
- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
- **Code formatting**: `task format` (or `task backend:format` for Java only)
- **Full quality gate**: `task check` (runs lint + typecheck + test across all components)
After modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`.
### Docker Development
- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`)
- **Build fat version**: `task docker:build:fat`
- **Build ultra-lite**: `task docker:build:ultra-lite`
- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`)
- **Stop compose stack**: `task docker:down`
- **View logs**: `task docker:logs`
- **Example compose files**: Located in `exampleYmlFiles/` directory
### Security Mode Development
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
### Python Development (AI Engine)
The engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.
#### Python Commands
All engine commands run from the repo root using Task:
- `task engine:check` — run all checks (typecheck + lint + format-check + test)
- `task engine:fix` — auto-fix lint + formatting
- `task engine:install` — install Python dependencies via uv
- `task engine:dev` — start FastAPI with hot reload (localhost:5001)
- `task engine:test` — run pytest
- `task engine:lint` — run ruff linting
- `task engine:typecheck` — run pyright
- `task engine:format` — format code with ruff
- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec
The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.
#### Python Code Style
- Keep `task engine:check` passing.
- Use modern Python when it improves clarity.
- Prefer explicit names to cleverness.
- Avoid nested functions and nested classes unless the language construct requires them.
- Prefer composition to inheritance when combining concepts.
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
- Add comments sparingly and only when they explain non-obvious intent.
#### Python Typing and Models
- Deserialize into Pydantic models as early as possible.
- Serialize from Pydantic models as late as possible.
- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.
- Avoid `Any` wherever possible.
- Avoid `cast()` wherever possible (reconsider the structure first).
- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.
- Do not use string literals for any type annotations, including `cast()`.
#### Python Configuration
- Keep application-owned configuration in `stirling.config`.
- Only add `STIRLING_*` environment variables that the engine itself truly owns.
- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.
- Let `pydantic-ai` own provider authentication configuration when possible.
#### Python Architecture
**Package roles:**
- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.
- `stirling.models`: shared model primitives and generated tool models.
- `stirling.agents`: reasoning modules for individual capabilities.
- `stirling.api`: HTTP layer, dependency access, and app startup wiring.
- `stirling.services`: shared runtime and non-AI infrastructure.
- `stirling.config`: application-owned settings.
**Source of truth:**
- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.
- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.
- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.
- If a tool ID must match a parameter model, validate that relationship explicitly in code.
**Boundaries:**
- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.
- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.
- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.
- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.
#### Python AI Usage
- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.
- Use AI for reasoning-heavy outputs, not deterministic glue.
- Do not ask the model to invent data that Python can derive safely.
- Do not fabricate fallback user-facing copy in code to hide incomplete model output.
- AI output schemas should be impossible to instantiate incorrectly.
- Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.
- Prefer Python to derive deterministic follow-up structure from a valid AI result.
- Use `NativeOutput(...)` for structured model outputs.
- Use `ToolOutput(...)` when the model should select and call delegate functions.
#### Python Testing
- Test contracts directly.
- Test agents directly where behaviour matters.
- Test API routes as thin integration points.
- Prefer dependency overrides or startup-state seams to monkeypatching random globals.
### Frontend Development
- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
- **Package Installation**: `task frontend:install`
- **Deployment Options**:
- **Desktop App**: `task desktop:build`
- **Web Server**: `task frontend:build` then serve dist/ folder
- **Development**: `task desktop:dev` for desktop dev mode
#### Environment Variables
- All `VITE_*` variables must be declared in the appropriate committed env file:
- `frontend/.env` — core, proprietary, and shared vars
- `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)
- These files are committed to Git and must not contain private keys
- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top
- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files
- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file
- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks
- See `frontend/README.md#environment-variables` for full documentation
#### Import Paths - CRITICAL
**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.
For a broader explanation of the frontend layering and override architecture, see [frontend/DeveloperGuide.md](frontend/DeveloperGuide.md).
```typescript
// ✅ CORRECT - Use @app/* for all imports
import { AppLayout } from "@app/components/AppLayout";
import { useFileContext } from "@app/contexts/FileContext";
import { FileContext } from "@app/contexts/FileContext";
// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code
import { AppLayout } from "@core/components/AppLayout";
import { useFileContext } from "@proprietary/contexts/FileContext";
```
**Only use explicit aliases when:**
- Building layer-specific override that wraps a lower layer's component
- Example: `import { AppProviders as CoreAppProviders } from "@core/components/AppProviders"` when creating proprietary/AppProviders.tsx that extends the core version
The `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/desktop) and handles the fallback cascade.
#### Component Override Pattern (Stub/Shadow)
Use this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.
**How it works:**
1. Core defines stub component (returns null or no-op)
2. Desktop/proprietary overrides with same path/name
3. Core imports via `@app/*` - higher layer "shadows" core in those builds
4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!
**Example - Desktop-specific footer:**
```typescript
// core/components/rightRail/RightRailFooterExtensions.tsx (stub)
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions(_props: RightRailFooterExtensionsProps) {
return null; // Stub - does nothing in web builds
}
```
```tsx
// desktop/components/rightRail/RightRailFooterExtensions.tsx (real implementation)
import { Box } from '@mantine/core';
import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions({ className }: RightRailFooterExtensionsProps) {
return (
<Box className={className}>
<BackendHealthIndicator />
</Box>
);
}
```
```tsx
// core/components/shared/RightRail.tsx (usage - works in ALL builds)
import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions';
export function RightRail() {
return (
<div>
{/* In web builds: renders nothing (stub returns null) */}
{/* In desktop builds: renders BackendHealthIndicator */}
<RightRailFooterExtensions className="right-rail-footer" />
</div>
);
}
```
**Build resolution:**
- **Core build**: `@app/*``core/*` → Gets stub (returns null)
- **Desktop build**: `@app/*``desktop/*` → Gets real implementation (shadows core)
**Benefits:**
- No runtime checks or feature flags
- Type-safe across all builds
- Clean, readable code
- Build-time optimization (dead code elimination)
#### Multi-Tool Workflow Architecture
Frontend designed for **stateful document processing**:
- Users upload PDFs once, then chain tools (split → merge → compress → view)
- File state and processing results persist across tool switches
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
#### FileContext - Central State Management
**Location**: `frontend/src/core/contexts/FileContext.tsx`
- **Active files**: Currently loaded PDFs and their variants
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
- **IndexedDB persistence**: File storage with thumbnail caching
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
#### Processing Services
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
- **fileStorage**: IndexedDB with LRU cache management
#### Memory Management Strategy
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
- PDF.js documents that need explicit .destroy() calls
- Blob URLs from tool outputs that need revocation
- Web Workers that need termination
Without cleanup: browser crashes with memory leaks.
#### Tool Development
**Architecture**: Modular hook-based system with clear separation of concerns:
- **useToolOperation** (`frontend/src/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
- Coordinates all tool operations with consistent interface
- Integrates with FileContext for operation tracking
- Handles validation, error handling, and UI state management
- **Supporting Hooks**:
- **useToolState**: UI state management (loading, progress, error, files)
- **useToolApiCalls**: HTTP requests and file processing
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
- **Utilities**:
- **toolErrorHandler**: Standardized error extraction and i18n support
- **toolResponseProcessor**: API response handling (single/zip/custom)
- **toolOperationTracker**: FileContext integration utilities
**Three Tool Patterns**:
**Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
```typescript
return useToolOperation({
operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
buildFormData: (params, file: File) => { /* single file */ },
multiFileEndpoint: false,
});
```
**Pattern 2: Multi-File Tools** (Batch processing)
- Backend accepts `MultipartFile[]` arrays in single API call
- Set `multiFileEndpoint: true`
- Examples: Split, Merge, Overlay
```typescript
return useToolOperation({
operationType: 'split',
endpoint: '/api/v1/general/split-pages',
buildFormData: (params, files: File[]) => { /* all files */ },
multiFileEndpoint: true,
filePrefix: 'split_',
});
```
**Pattern 3: Complex Tools** (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide `customProcessor` for full control
- Examples: Convert, OCR
```typescript
return useToolOperation({
operationType: 'convert',
customProcessor: async (params, files) => { /* custom logic */ },
});
```
**Benefits**:
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
- **Consistent**: All tools follow same pattern and interface
- **Maintainable**: Single responsibility hooks, easy to test and modify
- **i18n Ready**: Built-in internationalization support
- **Type Safe**: Full TypeScript support with generic interfaces
- **Memory Safe**: Automatic resource cleanup and blob URL management
## Architecture Overview
### Project Structure
- **Backend**: Spring Boot application
- **Frontend**: React-based SPA in `/frontend` directory
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
- **Internationalization**: JSON-based translations (converted from backend .properties)
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
- **Configuration**: YAML-based configuration with environment variable overrides
### Controller Architecture
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
- Organized by function: converters, security, misc, pipeline
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
### Key Components
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
- **ConfigInitializer**: Handles runtime configuration and settings files
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
- **Security Layer**: Authentication, authorization, and user management (when enabled)
### Frontend Directory Structure
The frontend is organized with a clear separation of concerns:
- **`frontend/src/core/`**: Main application code (shared, production-ready components)
- **`core/components/`**: React components organized by feature
- `core/components/tools/`: Individual PDF tool implementations
- `core/components/viewer/`: PDF viewer components
- `core/components/pageEditor/`: Page manipulation UI
- `core/components/tooltips/`: Help tooltips for tools
- `core/components/shared/`: Reusable UI components
- **`core/contexts/`**: React Context providers
- `FileContext.tsx`: Central file state management
- `file/`: File reducer and selectors
- `toolWorkflow/`: Tool workflow state
- **`core/hooks/`**: Custom React hooks
- `hooks/tools/`: Tool-specific operation hooks (one directory per tool)
- `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)
- **`core/constants/`**: Application constants and configuration
- **`core/data/`**: Static data (tool taxonomy, etc.)
- **`core/services/`**: Business logic services (PDF processing, storage, etc.)
- **`frontend/src/desktop/`**: Desktop-specific (Tauri) code
- **`frontend/src/proprietary/`**: Proprietary/licensed features
- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code
- **`frontend/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)
- **Internationalization**:
- Backend: `messages_*.properties` files
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
- Conversion Script: `scripts/convert_properties_to_json.py`
### Configuration Modes
- **Ultra-lite**: Basic PDF operations only
- **Standard**: Full feature set
- **Fat**: Pre-downloaded dependencies for air-gapped environments
- **Security Mode**: Adds authentication, user management, and enterprise features
### Testing Strategy
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
- **Docker Testing**: `test.sh` validates all Docker variants
- **Manual Testing**: No unit tests currently - relies on UI and API testing
## Development Workflow
1. **Local Development** (using Taskfile):
- Backend + frontend: `task dev`
- All services (including AI engine): `task dev:all`
- Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001)
2. **Quality Gate**: Run `task check` before submitting PRs
3. **Docker Testing**: Use `./test.sh` for full Docker integration tests
4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`)
5. **Translations**:
- Backend: Use helper scripts in `/scripts` for multi-language updates
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
## Frontend Architecture Status
- **Core Status**: React SPA architecture complete with multi-tool workflow support
- **State Management**: FileContext handles all file operations and tool navigation
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
## Translation Rules
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
- Translation files are located in `frontend/public/locales/`
## Important Notes
- **Java Version**: Minimum JDK 21, supports and recommends JDK 25
- **Lombok**: Used extensively - ensure IDE plugin is installed
- **File Persistence**:
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
## Communication Style
- Be direct and to the point
- No apologies or conversational filler
- Answer questions directly without preamble
- Explain reasoning concisely when asked
- Avoid unnecessary elaboration
## Decision Making
- Ask clarifying questions before making assumptions
- Stop and ask when uncertain about project-specific details
- Confirm approach before making structural changes
- Request guidance on preferences (cross-platform vs specific tools, etc.)
- Verify understanding of requirements before proceeding
-337
View File
@@ -1,337 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Common Development Commands
### Build and Test
- **Build project**: `./gradlew clean build`
- **Run locally**: `./gradlew bootRun`
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
### Docker Development
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
- **Example compose files**: Located in `exampleYmlFiles/` directory
### Security Mode Development
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
### Frontend Development
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
- **Package Installation**: DO NOT run npm install commands - package management handled separately
- **Deployment Options**:
- **Desktop App**: `npm run tauri-build` (native desktop application)
- **Web Server**: `npm run build` then serve dist/ folder
- **Development**: `npm run tauri-dev` for desktop dev mode
#### Import Paths - CRITICAL
**ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation.
```typescript
// ✅ CORRECT - Use @app/* for all imports
import { AppLayout } from "@app/components/AppLayout";
import { useFileContext } from "@app/contexts/FileContext";
import { FileContext } from "@app/contexts/FileContext";
// ❌ WRONG - Do not use @core/* or @proprietary/* in normal code
import { AppLayout } from "@core/components/AppLayout";
import { useFileContext } from "@proprietary/contexts/FileContext";
```
**Only use explicit aliases when:**
- Building layer-specific override that wraps a lower layer's component
- Example: `import { AppProviders as CoreAppProviders } from "@core/components/AppProviders"` when creating proprietary/AppProviders.tsx that extends the core version
The `@app/*` alias automatically resolves to the correct layer based on build target (core/proprietary/desktop) and handles the fallback cascade.
#### Component Override Pattern (Stub/Shadow)
Use this pattern for desktop-specific or proprietary-specific features WITHOUT runtime checks or conditionals.
**How it works:**
1. Core defines stub component (returns null or no-op)
2. Desktop/proprietary overrides with same path/name
3. Core imports via `@app/*` - higher layer "shadows" core in those builds
4. No `@ts-ignore`, no `isTauri()` checks, no runtime conditionals!
**Example - Desktop-specific footer:**
```typescript
// core/components/rightRail/RightRailFooterExtensions.tsx (stub)
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions(_props: RightRailFooterExtensionsProps) {
return null; // Stub - does nothing in web builds
}
```
```typescript
// desktop/components/rightRail/RightRailFooterExtensions.tsx (real implementation)
import { Box } from '@mantine/core';
import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator';
interface RightRailFooterExtensionsProps {
className?: string;
}
export function RightRailFooterExtensions({ className }: RightRailFooterExtensionsProps) {
return (
<Box className={className}>
<BackendHealthIndicator />
</Box>
);
}
```
```typescript
// core/components/shared/RightRail.tsx (usage - works in ALL builds)
import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions';
export function RightRail() {
return (
<div>
{/* In web builds: renders nothing (stub returns null) */}
{/* In desktop builds: renders BackendHealthIndicator */}
<RightRailFooterExtensions className="right-rail-footer" />
</div>
);
}
```
**Build resolution:**
- **Core build**: `@app/*``core/*` → Gets stub (returns null)
- **Desktop build**: `@app/*``desktop/*` → Gets real implementation (shadows core)
**Benefits:**
- No runtime checks or feature flags
- Type-safe across all builds
- Clean, readable code
- Build-time optimization (dead code elimination)
#### Multi-Tool Workflow Architecture
Frontend designed for **stateful document processing**:
- Users upload PDFs once, then chain tools (split → merge → compress → view)
- File state and processing results persist across tool switches
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
#### FileContext - Central State Management
**Location**: `frontend/src/core/contexts/FileContext.tsx`
- **Active files**: Currently loaded PDFs and their variants
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
- **IndexedDB persistence**: File storage with thumbnail caching
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
#### Processing Services
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
- **fileStorage**: IndexedDB with LRU cache management
#### Memory Management Strategy
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
- PDF.js documents that need explicit .destroy() calls
- Blob URLs from tool outputs that need revocation
- Web Workers that need termination
Without cleanup: browser crashes with memory leaks.
#### Tool Development
**Architecture**: Modular hook-based system with clear separation of concerns:
- **useToolOperation** (`frontend/src/core/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
- Coordinates all tool operations with consistent interface
- Integrates with FileContext for operation tracking
- Handles validation, error handling, and UI state management
- **Supporting Hooks**:
- **useToolState**: UI state management (loading, progress, error, files)
- **useToolApiCalls**: HTTP requests and file processing
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
- **Utilities**:
- **toolErrorHandler**: Standardized error extraction and i18n support
- **toolResponseProcessor**: API response handling (single/zip/custom)
- **toolOperationTracker**: FileContext integration utilities
**Three Tool Patterns**:
**Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
```typescript
return useToolOperation({
operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
buildFormData: (params, file: File) => { /* single file */ },
multiFileEndpoint: false,
});
```
**Pattern 2: Multi-File Tools** (Batch processing)
- Backend accepts `MultipartFile[]` arrays in single API call
- Set `multiFileEndpoint: true`
- Examples: Split, Merge, Overlay
```typescript
return useToolOperation({
operationType: 'split',
endpoint: '/api/v1/general/split-pages',
buildFormData: (params, files: File[]) => { /* all files */ },
multiFileEndpoint: true,
filePrefix: 'split_',
});
```
**Pattern 3: Complex Tools** (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide `customProcessor` for full control
- Examples: Convert, OCR
```typescript
return useToolOperation({
operationType: 'convert',
customProcessor: async (params, files) => { /* custom logic */ },
});
```
**Benefits**:
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
- **Consistent**: All tools follow same pattern and interface
- **Maintainable**: Single responsibility hooks, easy to test and modify
- **i18n Ready**: Built-in internationalization support
- **Type Safe**: Full TypeScript support with generic interfaces
- **Memory Safe**: Automatic resource cleanup and blob URL management
## Architecture Overview
### Project Structure
- **Backend**: Spring Boot application
- **Frontend**: React-based SPA in `/frontend` directory
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
- **Internationalization**: JSON-based translations (converted from backend .properties)
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
- **Configuration**: YAML-based configuration with environment variable overrides
### Controller Architecture
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
- Organized by function: converters, security, misc, pipeline
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
### Key Components
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
- **ConfigInitializer**: Handles runtime configuration and settings files
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
- **Security Layer**: Authentication, authorization, and user management (when enabled)
### Frontend Directory Structure
The frontend is organized with a clear separation of concerns:
- **`frontend/src/core/`**: Main application code (shared, production-ready components)
- **`core/components/`**: React components organized by feature
- `core/components/tools/`: Individual PDF tool implementations
- `core/components/viewer/`: PDF viewer components
- `core/components/pageEditor/`: Page manipulation UI
- `core/components/tooltips/`: Help tooltips for tools
- `core/components/shared/`: Reusable UI components
- **`core/contexts/`**: React Context providers
- `FileContext.tsx`: Central file state management
- `file/`: File reducer and selectors
- `toolWorkflow/`: Tool workflow state
- **`core/hooks/`**: Custom React hooks
- `hooks/tools/`: Tool-specific operation hooks (one directory per tool)
- `hooks/tools/shared/`: Shared hook utilities (useToolOperation, etc.)
- **`core/constants/`**: Application constants and configuration
- **`core/data/`**: Static data (tool taxonomy, etc.)
- **`core/services/`**: Business logic services (PDF processing, storage, etc.)
- **`frontend/src/desktop/`**: Desktop-specific (Tauri) code
- **`frontend/src/proprietary/`**: Proprietary/licensed features
- **`frontend/src-tauri/`**: Tauri (Rust) native desktop application code
- **`frontend/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)
- **Internationalization**:
- Backend: `messages_*.properties` files
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
- Conversion Script: `scripts/convert_properties_to_json.py`
### Configuration Modes
- **Ultra-lite**: Basic PDF operations only
- **Standard**: Full feature set
- **Fat**: Pre-downloaded dependencies for air-gapped environments
- **Security Mode**: Adds authentication, user management, and enterprise features
### Testing Strategy
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
- **Docker Testing**: `test.sh` validates all Docker variants
- **Manual Testing**: No unit tests currently - relies on UI and API testing
## Development Workflow
1. **Local Development**:
- Backend: `./gradlew bootRun` (runs on localhost:8080)
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
2. **Docker Testing**: Use `./test.sh` before submitting PRs
3. **Code Style**: Spotless enforces Google Java Format automatically
4. **Translations**:
- Backend: Use helper scripts in `/scripts` for multi-language updates
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
## Frontend Architecture Status
- **Core Status**: React SPA architecture complete with multi-tool workflow support
- **State Management**: FileContext handles all file operations and tool navigation
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
## Translation Rules
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
- Translation files are located in `frontend/public/locales/`
## Important Notes
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
- **Lombok**: Used extensively - ensure IDE plugin is installed
- **File Persistence**:
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
- **Import Paths**: ALWAYS use `@app/*` for imports - never use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
## Communication Style
- Be direct and to the point
- No apologies or conversational filler
- Answer questions directly without preamble
- Explain reasoning concisely when asked
- Avoid unnecessary elaboration
## Decision Making
- Ask clarifying questions before making assumptions
- Stop and ask when uncertain about project-specific details
- Confirm approach before making structural changes
- Request guidance on preferences (cross-platform vs specific tools, etc.)
- Verify understanding of requirements before proceeding
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+16 -2
View File
@@ -15,6 +15,19 @@ Before you start working on an issue, please comment on (or create) the issue an
Once you have been assigned an issue, you can start working on it. When you are ready to submit your changes, open a pull request.
For a detailed pull request tutorial, see [this guide](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github).
## Development Quick Start
This project uses [Task](https://taskfile.dev/) as a unified command runner. After cloning:
1. Install the `task` CLI: https://taskfile.dev/installation/
2. Run `task install` to install all dependencies
3. Run `task dev` to start backend + frontend
4. Run `task check` before submitting a PR
Run `task --list` to see all available commands.
## Pull Request Guidelines
Please make sure your Pull Request adheres to the following guidelines:
- Use the PR template provided.
@@ -39,9 +52,10 @@ If, at any point in time, you have a question, please feel free to ask in the sa
## Developer Documentation
For technical guides, setup instructions, and development resources, please see our [Developer Documentation](devGuide/) which includes:
For technical guides, setup instructions, and development resources:
- [Developer Guide](devGuide/DeveloperGuide.md) - Main setup and architecture guide
- [Developer Guide](DeveloperGuide.md) - Main setup and architecture guide
- [Taskfile.yml](Taskfile.yml) - Unified task runner for all build/dev/test/lint commands
- [Exception Handling Guide](devGuide/EXCEPTION_HANDLING_GUIDE.md) - Error handling patterns and i18n
- [Translation Guide](devGuide/HowToAddNewLanguage.md) - Adding new languages
- And more in the [devGuide folder](devGuide/)
+62 -16
View File
@@ -11,7 +11,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
**Stirling 2.0** is built using:
**Backend:**
- Spring Boot (Java 17+, JDK 21 recommended)
- Spring Boot (Java 21+, JDK 25 recommended)
- PDFBox for core PDF operations
- LibreOffice for document conversions
- qpdf for PDF optimization
@@ -42,11 +42,13 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
### Prerequisites
- [Task](https://taskfile.dev/installation/) — unified command runner (recommended)
- Docker
- Git
- Java JDK 17 or later (JDK 21 recommended)
- Java JDK 21 or later (JDK 25 recommended)
- Node.js 18+ and npm (required for frontend development)
- Gradle 7.0 or later (Included within the repo)
- [uv](https://docs.astral.sh/uv/) — Python package manager (required for engine development)
- Rust and Cargo (required for Tauri desktop app development)
- Tauri CLI (install with `cargo install tauri-cli`)
@@ -59,7 +61,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
cd Stirling-PDF
```
2. Install Docker and JDK17 if not already installed.
2. Install Docker and JDK 21 (or JDK 25 recommended) if not already installed.
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
1. Only VSCode
@@ -82,13 +84,29 @@ For local testing, you should generally be testing the full 'Security' version o
5. **Frontend Setup (Required for Stirling 2.0)**
Navigate to the frontend directory and install dependencies using npm.
### Verify Setup
Run `task install` to install all project dependencies (frontend npm packages, engine Python packages). Gradle manages its own dependencies automatically. Then run `task check` to verify everything builds and passes.
## 4. Stirling 2.0 Development Workflow
### Using Taskfile (Recommended)
The fastest way to start developing:
1. **Start developing**: `task dev` (runs backend + frontend concurrently — Ctrl+C to stop)
2. **Or start services individually** in separate terminals:
- `task backend:dev` — Spring Boot on localhost:8080
- `task frontend:dev` — Vite on localhost:5173
- `task engine:dev` — FastAPI on localhost:5001
Run `task --list` to see all available commands.
### Frontend Development (React)
The frontend is a React SPA that runs independently during development:
1. **Start the backend**: Run the Spring Boot application (serves API endpoints on localhost:8080)
2. **Start the frontend dev server**: Navigate to the frontend directory and run the development server (serves UI on localhost:5173)
1. **Start the backend**: `task backend:dev` (serves API endpoints on localhost:8080)
2. **Start the frontend dev server**: `task frontend:dev` (serves UI on localhost:5173)
3. **Development flow**: The Vite dev server automatically proxies API calls to the backend
### File Storage Architecture
@@ -99,7 +117,10 @@ Stirling 2.0 uses client-side file storage:
### Tauri Desktop App Development
Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE.
See [the frontend README](frontend/README.md#tauri) for build instructions.
Using Taskfile: `task desktop:dev` (development) or `task desktop:build` (production build).
See [the frontend README](frontend/README.md#tauri) for detailed build instructions.
## 5. Project Structure
@@ -187,7 +208,7 @@ services:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
@@ -222,6 +243,20 @@ docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
### Building Docker Images
#### Using Taskfile (Recommended)
```bash
task docker:build # standard image
task docker:build:fat # fat image (all features)
task docker:build:ultra-lite # ultra-lite image
task docker:up # start standard compose stack
task docker:up:fat # start fat compose stack
task docker:down # stop all stacks
task docker:logs # tail logs
```
#### Manual Docker Builds
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
1. Set the security environment variable:
@@ -230,10 +265,10 @@ Stirling-PDF uses different Docker images for various configurations. The build
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
```
2. Build the project with Gradle:
2. Build the project:
```bash
./gradlew clean build
task backend:build
```
3. Build the Docker images:
@@ -261,9 +296,18 @@ Note: The `--no-cache` and `--pull` flags ensure that the build process uses the
## 7. Testing
### Quick Testing with Taskfile
Run all unit/integration tests across all components:
```bash
task test # run all tests (backend + frontend + engine)
task check # full quality gate: lint + typecheck + test
```
### Comprehensive Testing Script
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
Stirling-PDF also provides a `test.sh` script in the root directory for Docker integration tests. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
To run the test script:
@@ -289,10 +333,11 @@ Note: The `test.sh` script will run automatically when you raise a PR. However,
For React frontend development:
1. Start the backend: Run the Spring Boot application to serve API endpoints on localhost:8080
2. Start the frontend dev server: Navigate to the frontend directory and run the development server on localhost:5173
1. Start the backend: `task backend:dev` (serves API endpoints on localhost:8080)
2. Start the frontend dev server: `task frontend:dev` (serves UI on localhost:5173)
3. The Vite dev server automatically proxies API calls to the backend
4. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
4. Run frontend tests: `task frontend:test` (or `task frontend:test:watch` for watch mode)
5. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
### Local Testing (Java and UI Components)
@@ -308,7 +353,7 @@ To run Stirling-PDF locally:
1. Compile and run the project using built-in IDE methods or by running:
```bash
./gradlew bootRun
task backend:dev
```
2. Access the application at `http://localhost:8080` in your web browser.
@@ -329,10 +374,11 @@ Important notes:
2. Create a new branch for your feature or bug fix.
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
4. Test your changes thoroughly in the Docker environment.
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
5. Run the quality gate and integration tests:
```bash
./test.sh
task check # lint + typecheck + test across all components
./test.sh # Docker integration tests (builds all variants + Cucumber)
```
6. Push your changes to your fork.
+444
View File
@@ -0,0 +1,444 @@
# File Sharing Feature - Architecture & Workflow
## Overview
The File Sharing feature enables users to store files server-side and share them with other registered users or via token-based share links. Files are stored using a pluggable storage provider (local filesystem or database) with optional quota enforcement.
**Key Capabilities:**
- Server-side file storage (upload, update, download, delete)
- Optional history bundle and audit log attachments per file
- Direct user-to-user sharing with access roles
- Token-based share links (requires `system.frontendUrl`)
- Optional email notifications for shares (requires `mail.enabled`)
- Access audit trail (tracks who accessed a share link and how)
- Automatic share link expiration
- Storage quotas (per-user and total)
- Pluggable storage backend (local filesystem or database BLOB)
- Integration with the Shared Signing workflow
## Architecture
### Database Schema
**`stored_files`**
- One record per uploaded file
- Stores file metadata (name, content type, size, storage key)
- Optionally links to a history bundle and audit log as separate stored objects
- `workflow_session_id` — nullable link to a `WorkflowSession` (signing feature)
- `file_purpose` — enum classifying the file's role: `GENERIC`, `SIGNING_ORIGINAL`, `SIGNING_SIGNED`, `SIGNING_HISTORY`
**`file_shares`**
- One record per sharing relationship
- Two share types, distinguished by which fields are set:
- **User share**: `shared_with_user_id` is set, `share_token` is null
- **Link share**: `share_token` is set (UUID), `shared_with_user_id` is null
- `access_role``EDITOR`, `COMMENTER`, or `VIEWER`
- `expires_at` — nullable expiration for link shares
- `workflow_participant_id` — when set, marks this as a **workflow share** (hidden from the file manager, accessible only via workflow endpoints)
**`file_share_accesses`**
- One record per access event on a share link
- Tracks: user, share link, access type (`VIEW` or `DOWNLOAD`), timestamp
**`storage_cleanup_entries`**
- Queue of storage keys to be deleted asynchronously
- Used when a file is deleted but the physical storage object cleanup is deferred
### Access Roles
| Role | Can Read | Can Write |
|------|----------|-----------|
| `EDITOR` | ✅ | ✅ |
| `COMMENTER` | ✅ | ❌ |
| `VIEWER` | ✅ | ❌ |
Default role when none is specified: `EDITOR`.
Owners always have full access regardless of role.
#### Role Semantics: COMMENTER vs VIEWER
In the file storage layer, `COMMENTER` and `VIEWER` are equivalent — both grant read-only access and neither can replace file content. The distinction is meaningful in the **signing workflow** context:
| Context | COMMENTER | VIEWER |
|---------|-----------|--------|
| File storage | Read only (same as VIEWER) | Read only |
| Signing workflow | Can submit a signing action | Read only |
`WorkflowParticipant.canEdit()` returns `true` for `COMMENTER` (and `EDITOR`) roles, which the signing workflow uses to determine if a participant can still submit a signature. Once a participant has signed or declined, their effective role is automatically downgraded to `VIEWER` regardless of their configured role.
The rationale: "annotating" a document (submitting a signature) is not the same as "replacing" it. COMMENTER grants annotation rights without file-replacement rights.
### Backend Architecture
#### Service Layer
**FileStorageService** (`1137 lines`)
- Core file management service
- Upload, update, download, and delete operations
- User share management (share, revoke, leave)
- Link share management (create, revoke, access)
- Access recording and listing
- Storage quota enforcement
- Configuration feature gate checks
**StorageCleanupService**
- Scheduled daily: deletes orphaned storage keys from `storage_cleanup_entries`
- Scheduled daily: purges expired share links from `file_shares`
- Processes cleanup in batches of 50 entries
#### Storage Providers
**LocalStorageProvider**
- Files stored on the filesystem under `storage.local.basePath` (default: `./storage`)
- Storage key is a path relative to the base directory
**DatabaseStorageProvider**
- Files stored as BLOBs in `stored_file_blobs` table
- No filesystem dependency
Provider is selected at startup via `storage.provider: local | database`.
#### Controller Layer
**FileStorageController** (`/api/v1/storage`)
- All endpoints require authentication
- File CRUD and sharing operations
### Data Flow
```
User uploads file → StorageProvider stores bytes → StoredFile record created
Owner shares file → FileShare record created (user or link)
Recipient accesses file → Access recorded → File bytes streamed
```
## File Operations
### Upload File
```bash
POST /api/v1/storage/files
Content-Type: multipart/form-data
file: document.pdf # Required — main file
historyBundle: history.json # Optional — version history
auditLog: audit.json # Optional — audit trail
```
**Response:**
```json
{
"id": 42,
"fileName": "document.pdf",
"contentType": "application/pdf",
"sizeBytes": 102400,
"owner": "alice",
"ownedByCurrentUser": true,
"accessRole": "editor",
"createdAt": "2025-01-01T12:00:00",
"updatedAt": "2025-01-01T12:00:00",
"sharedWithUsers": [],
"sharedUsers": [],
"shareLinks": []
}
```
### Update File
Replaces the file content. Only the owner can update.
```bash
PUT /api/v1/storage/files/{fileId}
Content-Type: multipart/form-data
file: document_v2.pdf
historyBundle: history.json # Optional
auditLog: audit.json # Optional
```
### List Files
Returns all files owned by or shared with the current user. Workflow-shared files (signing participants) are excluded — those are accessible via signing endpoints only.
```bash
GET /api/v1/storage/files
```
Response is sorted by `createdAt` descending.
### Download File
```bash
GET /api/v1/storage/files/{fileId}/download?inline=false
```
- `inline=false` (default) — `Content-Disposition: attachment`
- `inline=true``Content-Disposition: inline` (for browser preview)
### Delete File
Only the owner can delete. All associated share links and their access records are deleted first, then the database record, then the physical storage object.
```bash
DELETE /api/v1/storage/files/{fileId}
```
## Sharing Operations
### Share with User
```bash
POST /api/v1/storage/files/{fileId}/shares/users
Content-Type: application/json
{
"username": "bob", # Username or email address
"accessRole": "editor" # "editor", "commenter", or "viewer" (default: "editor")
}
```
**Behaviour:**
- If the target user exists: creates/updates a `FileShare` with `sharedWithUser` set
- If `username` is an email address and the user doesn't exist: creates a share link and sends a notification email (requires `sharing.emailEnabled` and `sharing.linkEnabled`)
- If the target user is the owner: returns 400
- If sharing is disabled: returns 403
### Revoke User Share
Only the owner can revoke.
```bash
DELETE /api/v1/storage/files/{fileId}/shares/users/{username}
```
### Leave Shared File
The recipient removes themselves from a shared file.
```bash
DELETE /api/v1/storage/files/{fileId}/shares/self
```
### Create Share Link
Creates a token-based link for anonymous/authenticated access. Requires `sharing.linkEnabled` and `system.frontendUrl` to be configured.
```bash
POST /api/v1/storage/files/{fileId}/shares/links
Content-Type: application/json
{
"accessRole": "viewer" # Optional (default: "editor")
}
```
**Response:**
```json
{
"token": "550e8400-e29b-41d4-a716-446655440000",
"accessRole": "viewer",
"createdAt": "2025-01-01T12:00:00",
"expiresAt": "2025-01-04T12:00:00"
}
```
Expiration is set to `now + sharing.linkExpirationDays` (default: 3 days).
### Revoke Share Link
```bash
DELETE /api/v1/storage/files/{fileId}/shares/links/{token}
```
Also deletes all access records for that token.
## Share Link Access
### Download via Share Link
Authentication is required (even for share links). Anonymous access is not permitted.
```bash
GET /api/v1/storage/share-links/{token}?inline=false
```
- Returns 401 if unauthenticated
- Returns 403 if authenticated but link doesn't permit access
- Returns 410 if the link has expired
- Records a `FileShareAccess` entry on success
> **Token-as-credential semantics:** Any authenticated user who holds the token can access the file — the token is the credential. If you need per-user access control (only a specific person can open it), use "Share with User" instead. Share links are appropriate for broader distribution where possession of the token implies authorization.
### Get Share Link Metadata
```bash
GET /api/v1/storage/share-links/{token}/metadata
```
Returns file name, owner, access role, creation/expiry timestamps, and whether the current user owns the file.
### List Accessed Share Links
Returns the most recent access for each non-expired share link the current user has accessed.
```bash
GET /api/v1/storage/share-links/accessed
```
### List Accesses for a Link (Owner Only)
```bash
GET /api/v1/storage/files/{fileId}/shares/links/{token}/accesses
```
Returns per-user access history (username, VIEW/DOWNLOAD, timestamp), sorted descending by time.
## Workflow Share Integration
Signing workflow participants access documents via their own `WorkflowParticipant.shareToken`. No `FileShare` record is created for participants; access control is self-contained in the `WorkflowParticipant` entity.
The `FileShare.workflow_participant_id` column and the `FileShare.isWorkflowShare()` method are **deprecated**. Legacy data (sessions created before this change) may still have `FileShare` records with `workflow_participant_id` set, which continue to work via the existing token lookup path in `UnifiedAccessControlService`. No new records are created.
`GET /api/v1/storage/files` returns all files owned by or shared with the current user (via `FileShare`). Signing-session PDFs use the `file_purpose` field (`SIGNING_ORIGINAL`, `SIGNING_SIGNED`, etc.) to distinguish them from generic files. The file manager UI can filter on this field if needed.
## API Reference
| Method | Endpoint | Description | Auth |
|--------|----------|-------------|------|
| POST | `/api/v1/storage/files` | Upload file | Required |
| PUT | `/api/v1/storage/files/{id}` | Update file | Required (owner) |
| GET | `/api/v1/storage/files` | List accessible files | Required |
| GET | `/api/v1/storage/files/{id}` | Get file metadata | Required |
| GET | `/api/v1/storage/files/{id}/download` | Download file | Required |
| DELETE | `/api/v1/storage/files/{id}` | Delete file | Required (owner) |
| POST | `/api/v1/storage/files/{id}/shares/users` | Share with user | Required (owner) |
| DELETE | `/api/v1/storage/files/{id}/shares/users/{username}` | Revoke user share | Required (owner) |
| DELETE | `/api/v1/storage/files/{id}/shares/self` | Leave shared file | Required |
| POST | `/api/v1/storage/files/{id}/shares/links` | Create share link | Required (owner) |
| DELETE | `/api/v1/storage/files/{id}/shares/links/{token}` | Revoke share link | Required (owner) |
| GET | `/api/v1/storage/share-links/{token}` | Download via share link | Required |
| GET | `/api/v1/storage/share-links/{token}/metadata` | Get share link metadata | Required |
| GET | `/api/v1/storage/share-links/accessed` | List accessed share links | Required |
| GET | `/api/v1/storage/files/{id}/shares/links/{token}/accesses` | List share accesses | Required (owner) |
## Configuration
All storage settings live under the `storage:` key in `settings.yml`:
```yaml
storage:
enabled: true # Requires security.enableLogin = true
provider: local # 'local' or 'database'
local:
basePath: './storage' # Filesystem base directory (local provider only)
quotas:
maxStorageMbPerUser: -1 # Per-user storage cap in MB; -1 = unlimited
maxStorageMbTotal: -1 # Total storage cap in MB; -1 = unlimited
maxFileMb: -1 # Max size per upload (main + history + audit) in MB; -1 = unlimited
sharing:
enabled: false # Master switch for all sharing (opt-in)
linkEnabled: false # Enable token-based share links (requires system.frontendUrl)
emailEnabled: false # Enable email notifications (requires mail.enabled)
linkExpirationDays: 3 # Days until share links expire
```
**Prerequisites:**
- `storage.enabled` requires `security.enableLogin = true`
- `sharing.linkEnabled` requires `system.frontendUrl` to be set (used to build share link URLs)
- `sharing.emailEnabled` requires `mail.enabled = true`
## Security Considerations
### Access Control
- All endpoints require authentication — there is no anonymous access
- Owner-only operations enforced in service layer (not just controller)
- `requireReadAccess` / `requireEditorAccess` checked on every download
### Share Link Security
- Tokens are UUIDs (random, not guessable)
- Expiration enforced on every access
- Expired links return HTTP 410 Gone
- Revoked links delete all access records
### Quota Enforcement
- Checked before storing (not after)
- Accounts for existing file size when replacing (only the delta counts)
- Covers main file + history bundle + audit log in a single check
## Automatic Cleanup
`StorageCleanupService` runs two scheduled jobs daily:
1. **Orphaned storage cleanup** — processes up to 50 `StorageCleanupEntry` records, deletes the physical storage object, then removes the entry. Failed attempts increment `attemptCount` for retry.
2. **Expired share link cleanup** — deletes all `FileShare` records where `expiresAt` is in the past and `shareToken` is set.
## Troubleshooting
**"Storage is disabled":**
- Check `storage.enabled: true` in settings
- Verify `security.enableLogin: true`
**"Share links are disabled":**
- Check `sharing.linkEnabled: true`
- Verify `system.frontendUrl` is set and non-empty
**"Email sharing is disabled":**
- Check `sharing.emailEnabled: true`
- Verify `mail.enabled: true` and mail configuration
**Signing-session PDF appearing in the general file list:**
- This is expected — signing PDFs are accessible to owners and shared users
- Filter by `file_purpose` (`SIGNING_ORIGINAL`, `SIGNING_SIGNED`) in the UI to distinguish them
**Share link returns 410:**
- Link has expired — check `expires_at` in `file_shares` table
- Owner must create a new link
### Debug Queries
```sql
-- List files and their share counts
SELECT sf.stored_file_id, sf.original_filename, u.username as owner,
COUNT(DISTINCT fs.file_share_id) FILTER (WHERE fs.shared_with_user_id IS NOT NULL) as user_shares,
COUNT(DISTINCT fs.file_share_id) FILTER (WHERE fs.share_token IS NOT NULL) as link_shares
FROM stored_files sf
LEFT JOIN users u ON sf.owner_id = u.user_id
LEFT JOIN file_shares fs ON fs.stored_file_id = sf.stored_file_id
GROUP BY sf.stored_file_id, u.username;
-- Check share link expiration
SELECT share_token, access_role, created_at, expires_at,
expires_at < NOW() as is_expired
FROM file_shares
WHERE share_token IS NOT NULL;
-- Check access history for a share link
SELECT u.username, fsa.access_type, fsa.accessed_at
FROM file_share_accesses fsa
JOIN file_shares fs ON fsa.file_share_id = fs.file_share_id
JOIN users u ON fsa.user_id = u.user_id
WHERE fs.share_token = '{token}'
ORDER BY fsa.accessed_at DESC;
-- Pending cleanup entries
SELECT storage_key, attempt_count, updated_at
FROM storage_cleanup_entries
ORDER BY updated_at ASC;
```
## Summary
The File Sharing feature provides:
- ✅ Server-side file storage with pluggable backend (local/database)
- ✅ History bundle and audit log attachments per file
- ✅ Direct user-to-user sharing with EDITOR/COMMENTER/VIEWER roles
- ✅ Token-based share links with expiration
- ✅ Optional email notifications for shares
- ✅ Per-access audit trail for share links
- ✅ Storage quotas (per-user, total, per-file)
- ✅ Automatic cleanup of expired links and orphaned storage
- ✅ Workflow integration (signing-session PDFs stored via same infrastructure; participant access via `WorkflowParticipant.shareToken`)
+6
View File
@@ -6,10 +6,16 @@ Portions of this software are licensed as follows:
* All content that resides under the "app/proprietary/" directory of this repository,
if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE".
* All content that resides under the "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/proprietary/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/proprietary/LICENSE".
* All content that resides under the "frontend/src/desktop/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE".
* All content that resides under the "frontend/src/saas/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/saas/LICENSE".
* All content that resides under the "frontend/src/prototypes/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/prototypes/LICENSE".
* Content outside of the above mentioned directories or restrictions above is
available under the MIT License as defined below.
+1 -1
View File
@@ -60,7 +60,7 @@ For full installation options (including desktop and Kubernetes), see our [Docum
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
For development setup, see the [Developer Guide](DeveloperGuide.md).
This project uses [Task](https://taskfile.dev/) as a unified command runner for all build, dev, and test commands. Run `task install` to get started, or see the [Developer Guide](DeveloperGuide.md) for full details.
For adding translations, see the [Translation Guide](devGuide/HowToAddNewLanguage.md).
+691
View File
@@ -0,0 +1,691 @@
# Shared Signing Feature - Architecture & Workflow
## Overview
The Shared Signing feature enables collaborative document signing workflows where a document owner can request signatures from multiple participants. Each participant receives a secure token to access the document, submit their digital signature (with optional wet signature overlay), and track the signing progress.
**Key Capabilities:**
- Multi-participant signing sessions
- Digital certificate signatures (P12/PKCS12, JKS, SERVER, USER_CERT, PEM/UPLOAD)
- Visual wet signature overlays (drawn, typed, or uploaded) — multiple per participant
- Token-based participant access (no authentication required for participants)
- Authenticated participant access for registered users via sign-requests API
- Progress tracking for session owners
- Optional signature summary page appended to finalized PDF
- Automatic role downgrade after signing (security)
- GDPR-compliant wet signature metadata cleanup
## Architecture
### Database Schema
#### Core Tables
**`workflow_sessions`**
- Tracks signing sessions created by document owners
- Links to original and processed (signed) PDF files
- Stores session metadata (message, due date, status)
**`workflow_participants`**
- One record per participant per session
- Tracks participant status: PENDING → VIEWED → SIGNED/DECLINED
- `NOTIFIED` status is reserved for a future email notification feature; no current code path sets it
- Stores participant-specific metadata (certificates, wet signatures) as JSONB
- Each participant holds their own `shareToken` (UUID) for token-based access — no separate `FileShare` record is created
- `accessRole` controls what actions the participant can perform. `COMMENTER` (and `EDITOR`) allow submitting a signature; `VIEWER` does not. After signing/declining, effective role is automatically downgraded to `VIEWER`
**`user_server_certificates`**
- Stores auto-generated certificates per user
- Enables "Use My Personal Certificate" option
#### Extended Tables
**`stored_files`**
- Added `workflow_session_id` to link files to signing sessions
- Added `file_purpose` enum (SIGNING_ORIGINAL, SIGNING_SIGNED, etc.)
**`file_shares`**
- Regular file shares are created when the session owner shares the document with other users via the file manager
- The `workflow_participant_id` column is deprecated; participant access is self-contained in `WorkflowParticipant.shareToken`
### Backend Architecture
#### Service Layer
**WorkflowSessionService** (`816 lines`)
- Core workflow management service
- Creates sessions with participants
- Handles participant status updates
- Stores signature metadata (certificates and wet signatures)
- Finalizes sessions by coordinating signing process
Key responsibilities:
- Session lifecycle management (create, list, get details, delete)
- Participant management (add, remove, notify)
- Certificate submission storage
- Wet signature metadata storage
- Session finalization orchestration
**UnifiedAccessControlService**
- Validates participant tokens
- Checks session status and expiration
- Maps participant status to effective access role
- Automatic role downgrade after signing: SIGNED/DECLINED → VIEWER role
**UserServerCertificateService**
- Auto-generates personal certificates for users
- Manages certificate storage and retrieval
- Enables "Use My Personal Certificate" signing option
#### Controller Layer
**SigningSessionController** (Owner-facing + Authenticated participant endpoints)
- `POST /api/v1/security/cert-sign/sessions` - Create signing session
- `GET /api/v1/security/cert-sign/sessions` - List user's sessions
- `GET /api/v1/security/cert-sign/sessions/{id}` - Get session details
- `GET /api/v1/security/cert-sign/sessions/{id}/pdf` - Download original PDF
- `POST /api/v1/security/cert-sign/sessions/{id}/finalize` - Finalize and apply signatures
- `GET /api/v1/security/cert-sign/sessions/{id}/signed-pdf` - Download signed PDF
- `DELETE /api/v1/security/cert-sign/sessions/{id}` - Delete session
- `POST /api/v1/security/cert-sign/sessions/{id}/participants` - Add participants
- `DELETE /api/v1/security/cert-sign/sessions/{id}/participants/{participantId}` - Remove participant
- `GET /api/v1/security/cert-sign/sign-requests` - List sign requests for authenticated user
- `GET /api/v1/security/cert-sign/sign-requests/{id}` - Get sign request details
- `GET /api/v1/security/cert-sign/sign-requests/{id}/document` - Download document for signing
- `POST /api/v1/security/cert-sign/sign-requests/{id}/sign` - Sign document (authenticated)
- `POST /api/v1/security/cert-sign/sign-requests/{id}/decline` - Decline sign request (authenticated)
**WorkflowParticipantController** (Participant-facing, token-based)
- `GET /api/v1/workflow/participant/session?token={token}` - View session details
- `GET /api/v1/workflow/participant/details?token={token}` - Get participant details
- `GET /api/v1/workflow/participant/document?token={token}` - Download PDF
- `POST /api/v1/workflow/participant/submit-signature` - Submit signature
- `POST /api/v1/workflow/participant/decline?token={token}` - Decline to sign
#### Data Flow
```
Owner creates session → Participants receive tokens →
Participants access via token (or authenticated) → Participants submit signatures →
Owner finalizes → System applies signatures → [Optional: append summary page] → Signed PDF generated
```
### Frontend Architecture
#### Quick Access Integration
**SignPopout Component**
- Displays in Quick Access Bar (top navigation)
- Shows active and completed signing sessions
- Auto-refreshes every 15 seconds to show signature progress
- Badge indicator shows count of pending sessions
**ActiveSessionsPanel**
- Lists sessions where user is owner or participant
- Shows signature progress: "X/Y signatures" (e.g., "2/5 signatures")
- Color-coded badges:
- Blue: No signatures yet (0/X)
- Yellow: Partial signatures (X/Y)
- Green: Ready to finalize (X/X)
**CompletedSessionsPanel**
- Lists finalized sessions and declined sign requests
- Allows viewing/downloading signed PDFs
#### Workbench Views
**SignRequestWorkbenchView**
- Full-screen view for participants to sign documents
- Integrated PDF viewer with annotation support
- Certificate selection (Personal/Organization/Custom P12)
- Wet signature input (draw, type, or upload)
- Signature placement on PDF pages
**SessionDetailWorkbenchView**
- Owner's view of session details
- Participant list with status indicators
- Ability to add/remove participants
- Finalize button when all signatures collected
- Download original/signed PDF
#### State Management
**FileContext Integration**
- Signing sessions operate within FileContext workflow
- PDFs loaded once, persist across tool switches
- Memory management for large files (up to 100GB+)
**ToolWorkflowContext**
- Registers custom workbench views
- Manages navigation between viewer and signing tools
- Preserves file state during signing operations
#### Services & Hooks
**workflowService.ts**
- API client for all signing endpoints
- Handles session creation, listing, and management
- Participant operations (submit, decline)
**useWorkflowSession.ts**
- React hook for owner session management
- State management for session list and details
**useParticipantSession.ts**
- React hook for participant signing workflow
- Manages signature submission state
## Signing Workflow Process
### 1. Session Creation (Owner)
```
Owner → Uploads PDF → Selects participants → Creates session
System creates:
- WorkflowSession record
- WorkflowParticipant records (one per participant, each with a unique shareToken)
Participants receive token (via email or share link)
```
**API Call:**
```bash
POST /api/v1/security/cert-sign/sessions
Content-Type: multipart/form-data
file: document.pdf
workflowType: SIGNING
documentName: "contract.pdf" # Optional display name
participantUserIds: [1, 2, 3] # Registered user IDs
participantEmails: ["a@b.com"] # External/unregistered users
participants: [...] # Detailed participant configs (optional)
message: "Please sign this contract"
dueDate: "2025-12-31"
ownerEmail: "owner@example.com" # Optional, for notifications
workflowMetadata: '{"showSignature": false, "showLogo": false, "includeSummaryPage": true}'
```
**Session-level `workflowMetadata` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `showSignature` | boolean | Show visible digital signature block on PDF |
| `pageNumber` | integer | Page to place digital signature on |
| `showLogo` | boolean | Show logo in digital signature block |
| `includeSummaryPage` | boolean | Append a signature summary page before digital signing |
**Response:**
```json
{
"sessionId": "uuid",
"documentName": "contract.pdf",
"participants": [
{
"userId": 1,
"email": "user1@example.com",
"shareToken": "token1",
"status": "PENDING"
}
],
"participantCount": 3,
"signedCount": 0
}
```
### 2. Participant Access
```
Participant → Clicks token link → Views session details
Status changes: PENDING/NOTIFIED → VIEWED
Participant downloads PDF to review
```
**Access URL (unauthenticated):**
```
https://app.example.com/sign?token={participant_token}
```
**Authenticated participants** can also use:
```
GET /api/v1/security/cert-sign/sign-requests
GET /api/v1/security/cert-sign/sign-requests/{sessionId}
GET /api/v1/security/cert-sign/sign-requests/{sessionId}/document
```
**Automatic Status Update:**
- First access: PENDING/NOTIFIED → VIEWED
- Downloads tracked but don't change status
### 3. Signature Submission
```
Participant → Selects certificate type → Uploads certificate (if needed)
→ Draws/uploads wet signatures (optional, multiple supported)
→ Submits signature
System stores:
- Certificate data (P12/JKS keystore as base64)
- Certificate password
- Wet signatures metadata (JSON array: base64 image + coordinates per signature)
Status changes: VIEWED → SIGNED
Access role: EDITOR → VIEWER (automatic downgrade)
```
**API Call (token-based, unauthenticated):**
```bash
POST /api/v1/workflow/participant/submit-signature
Content-Type: multipart/form-data
participantToken: {token}
certType: P12 | JKS | SERVER | USER_CERT
p12File: certificate.p12 (if certType=P12)
jksFile: keystore.jks (if certType=JKS)
password: cert_password
showSignature: false
pageNumber: 1
location: "New York"
reason: "I approve this contract"
showLogo: false
wetSignaturesData: '[{"page":0,"x":100,"y":200,"width":150,"height":50,"type":"IMAGE","data":"base64..."}]'
```
**API Call (authenticated users):**
```bash
POST /api/v1/security/cert-sign/sign-requests/{sessionId}/sign
Content-Type: multipart/form-data
certType: SERVER | USER_CERT | UPLOAD | PEM | PKCS12 | PFX | JKS
p12File: certificate.p12 (if applicable)
password: cert_password
reason: "I approve this contract"
location: "New York"
wetSignaturesData: '[...]'
```
**Metadata Storage (JSONB):**
```json
{
"certificateSubmission": {
"certType": "P12",
"password": "cert_password",
"p12Keystore": "base64_encoded_keystore",
"showSignature": false,
"pageNumber": 1,
"location": "New York",
"reason": "I approve this contract",
"showLogo": false
},
"wetSignatures": [
{
"type": "IMAGE",
"data": "base64_image",
"page": 0,
"x": 100,
"y": 200,
"width": 150,
"height": 50
}
]
}
```
Note: Multiple wet signatures are supported per participant (array).
### 4. Progress Tracking (Owner)
```
Owner → Views session list → Sees "2/5 signatures"
→ Clicks session → Views participant status
Participant list shows:
- user1@example.com: SIGNED ✓
- user2@example.com: SIGNED ✓
- user3@example.com: VIEWED (pending)
- user4@example.com: PENDING
- user5@example.com: DECLINED ✗
Auto-refresh every 15 seconds
```
**Badge Colors:**
- 🔵 Blue: 0/5 signatures (awaiting)
- 🟡 Yellow: 2/5 signatures (partial)
- 🟢 Green: 5/5 signatures (ready to finalize)
### 5. Session Finalization
```
Owner → Clicks "Finalize" → System processes signatures
Processing steps:
1. Apply wet signatures to PDF (visual overlays)
1.5. Append signature summary page (if includeSummaryPage=true)
2. Apply digital certificates in participant order
- Visual signature block suppressed when summary page is enabled
3. Store signed PDF
4. Clear wet signature metadata (GDPR compliance)
Owner downloads signed PDF
```
**Finalization Process:**
1. **Apply Wet Signatures First**
```java
for (WetSignature sig : wetSignatures) {
PDPage page = document.getPage(sig.getPage());
byte[] imageBytes = Base64.decode(sig.getData());
// Convert Y from top-left (UI) to bottom-left (PDF) coordinate system
float pdfY = page.getMediaBox().getHeight() - sig.getY() - sig.getHeight();
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "signature");
contentStream.drawImage(image, sig.getX(), pdfY, sig.getWidth(), sig.getHeight());
}
```
2. **Append Summary Page (optional, before digital signing)**
If `includeSummaryPage=true`, a new A4 page is appended showing:
- Stirling logo and "Signature Summary" title
- Document name and session owner
- Finalization timestamp
- Per-participant: name, email, status, signed timestamp, reason, location, certificate type
- Supports overflow to additional pages
This step occurs **before** digital certificate signing so signatures are not invalidated.
When a summary page is added, the visual digital signature block (`showSignature`) is suppressed — wet signatures (hand-drawn overlays) are unaffected.
3. **Apply Digital Certificates (in participant order)**
```java
for (Participant p : participants) {
if (p.status == SIGNED) {
KeyStore keystore = buildKeystore(p.certificate);
// Reason: participant override > owner default > "Document Signing"
// Location: participant-provided only (no default)
CertSignController.sign(pdfBytes, keystore, password, settings);
}
}
```
4. **Store and Cleanup**
```java
StoredFile signedFile = storeFile(signedPdfBytes, SIGNING_SIGNED);
session.setProcessedFile(signedFile);
session.setFinalized(true);
// GDPR: Clear sensitive metadata after finalization
for (Participant p : participants) {
p.metadata.remove("wetSignatures"); // Clears wet signature image data
p.metadata.remove("certificateSubmission"); // Clears keystore bytes + password
}
```
**API Call:**
```bash
POST /api/v1/security/cert-sign/sessions/{sessionId}/finalize
Authorization: Bearer {owner_token}
```
**Response:** Binary PDF file with Content-Disposition header
## Key Technical Features
### 1. Double JSON Encoding Fix (Recent)
**Problem:** JSONB columns were storing JSON strings instead of JSON objects, requiring double-parsing.
**Solution:** Created `JsonMapConverter` JPA AttributeConverter:
```java
@Convert(converter = JsonMapConverter.class)
@Column(name = "participant_metadata", columnDefinition = "jsonb")
private Map<String, Object> participantMetadata;
```
**Benefits:**
- Single parse on read
- Proper JSON storage in PostgreSQL
- Type-safe Map access
- Backward compatible with legacy data
### 2. Signature Progress Display (Recent)
**Implementation:**
- `WorkflowSessionResponse` includes `participantCount` and `signedCount`
- `WorkflowMapper` calculates counts when converting to DTO
- Frontend displays "X/Y signatures" in session list
- Auto-refresh every 15 seconds keeps counts updated
### 3. Token-Based Security
**No Authentication Required for Participants:**
- Participants access via secure token (UUID)
- Token linked to specific participant and session
- Automatic expiration support
- One-time signing (cannot sign twice)
**Authenticated Participant Access:**
- Registered users can also access sign requests via `/api/v1/security/cert-sign/sign-requests`
- Standard Spring Security authentication required
- Supports additional cert types: UPLOAD, PEM, PKCS12, PFX
**Automatic Role Downgrade:**
- After signing: EDITOR → VIEWER
- After declining: EDITOR → VIEWER
- Prevents modification after action taken
### 4. Storage Integration
**Unified with File Sharing:**
- All PDFs stored via `StorageProvider` (Database or Local)
- Respects storage quotas
- Supports files up to 100GB+ (with Local storage)
- Consistent with existing file sharing infrastructure
### 5. Certificate Types
**P12/PKCS12/PFX:** User uploads PKCS#12 file + password
**JKS:** User uploads Java KeyStore + password
**PEM/UPLOAD:** User uploads PEM certificate + private key
**SERVER:** Uses organization's server certificate (no upload needed)
**USER_CERT:** Uses user's auto-generated personal certificate (one-click)
Note: UPLOAD, PEM, PKCS12, PFX are available on the authenticated (`sign-requests`) path. The token-based path uses P12, JKS, SERVER, USER_CERT.
## Frontend Components Overview
### Owner Workflow Components
1. **CreateSessionPanel** - Form to create new signing session
2. **ActiveSessionsPanel** - List of pending sessions with progress
3. **SessionDetailWorkbenchView** - Full session management interface
4. **CompletedSessionsPanel** - History of finalized sessions
### Participant Workflow Components
1. **SignRequestWorkbenchView** - Main signing interface
2. **SignatureSettingsInput** - Certificate selection and configuration
3. **WetSignatureInput** - Draw/type/upload signature overlay
4. **SignatureSettingsDisplay** - Preview of signature settings
### Shared Components
1. **UserSelector** - Multi-select user picker for participants
2. **LocalEmbedPDFWithAnnotations** - PDF viewer with signature placement
## Configuration
### Backend Configuration
**application.properties:**
```properties
# Database (H2 or PostgreSQL)
spring.jpa.hibernate.ddl-auto=update
# Security
DOCKER_ENABLE_SECURITY=true
# Storage Provider (DATABASE or LOCAL)
storage.provider=LOCAL
storage.maxFileSize=100GB
```
### Frontend Configuration
**Quick Access Bar:**
- Signing popout accessible from top navigation
- Auto-refresh interval: 15 seconds
- Badge shows pending session count
## API Reference Summary
### Owner Endpoints (Authenticated)
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/v1/security/cert-sign/sessions` | Create session |
| GET | `/api/v1/security/cert-sign/sessions` | List sessions |
| GET | `/api/v1/security/cert-sign/sessions/{id}` | Get details |
| POST | `/api/v1/security/cert-sign/sessions/{id}/finalize` | Finalize session |
| GET | `/api/v1/security/cert-sign/sessions/{id}/pdf` | Download original |
| GET | `/api/v1/security/cert-sign/sessions/{id}/signed-pdf` | Download signed |
| DELETE | `/api/v1/security/cert-sign/sessions/{id}` | Delete session |
| POST | `/api/v1/security/cert-sign/sessions/{id}/participants` | Add participants |
| DELETE | `/api/v1/security/cert-sign/sessions/{id}/participants/{pid}` | Remove participant |
### Authenticated Participant Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/security/cert-sign/sign-requests` | List sign requests |
| GET | `/api/v1/security/cert-sign/sign-requests/{id}` | Get sign request details |
| GET | `/api/v1/security/cert-sign/sign-requests/{id}/document` | Download document |
| POST | `/api/v1/security/cert-sign/sign-requests/{id}/sign` | Sign document |
| POST | `/api/v1/security/cert-sign/sign-requests/{id}/decline` | Decline signing |
### Token-Based Participant Endpoints (No Auth Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/workflow/participant/session?token={token}` | View session |
| GET | `/api/v1/workflow/participant/details?token={token}` | Get participant details |
| GET | `/api/v1/workflow/participant/document?token={token}` | Download PDF |
| POST | `/api/v1/workflow/participant/submit-signature` | Submit signature |
| POST | `/api/v1/workflow/participant/decline?token={token}` | Decline signing |
## Security Considerations
### Data Protection
- Wet signature image data cleared after finalization (GDPR compliance)
- Certificate submission data (keystore bytes + password) cleared after finalization (GDPR compliance)
- Certificate passwords are not encrypted at rest while stored (TODO: encrypt at rest)
- Token expiration support
### Access Control
- Owner authentication required for session management
- Participant access via secure UUID tokens (no auth) or standard auth (sign-requests)
- Automatic role downgrade prevents re-signing
- Session status checks prevent unauthorized actions
### Audit Trail
- All participant actions tracked
- FileShare access logged
- Status transitions recorded
- Notification history maintained
## Performance Characteristics
### Scalability
- Supports PDFs up to 100GB+ (with Local storage provider)
- Memory-efficient streaming for large files
- IndexedDB caching on frontend
- Database indexes on session_id, share_token, workflow_session_id
### Response Times
- Session creation: ~500ms (10MB file)
- Session listing: ~100ms
- Token validation: ~50ms
- Finalization: ~2s per MB of PDF (varies by certificate operations)
## Future Enhancements
### Planned Features
- Email notifications for participants
- Reminder system for pending signatures
- Bulk signing operations
- Template-based signing workflows
- Signature validation/verification UI
- Certificate password encryption at rest
- Certificate keystore cleanup after finalization (GDPR)
- Webhook support for external integrations
- Analytics dashboard for signing metrics
### Additional Workflow Types
- **REVIEW** - Document review with comments
- **APPROVAL** - Multi-level approval chains
- **COLLABORATION** - Real-time collaborative editing
## Troubleshooting
### Common Issues
**"Token invalid" error:**
- Check token exists in workflow_participants table
- Verify session is not finalized
- Check expiration date (expires_at)
**Signature not appearing on PDF:**
- Verify certificate type is correct
- Check certificate password
- Review logs for signing errors
- Ensure PDFDocumentFactory is available
**"Awaiting signatures" not updating:**
- Backend should return participantCount and signedCount
- Frontend auto-refresh every 15 seconds
- Check network tab for API errors
**Wet signatures not visible after finalization:**
- Wet signatures are applied first as image overlays (Step 1)
- Check `wetSignaturesData` was sent as valid JSON array
- Verify page index is within document bounds
- Note: wet signatures survive regardless of `includeSummaryPage` setting
### Debug Queries
```sql
-- Check session status
SELECT session_id, status, finalized,
(SELECT COUNT(*) FROM workflow_participants WHERE workflow_session_id = ws.id) as participant_count,
(SELECT COUNT(*) FROM workflow_participants WHERE workflow_session_id = ws.id AND status = 'SIGNED') as signed_count
FROM workflow_sessions ws;
-- Check participant tokens
SELECT email, status, share_token, expires_at
FROM workflow_participants
WHERE workflow_session_id = (SELECT id FROM workflow_sessions WHERE session_id = '{session_id}');
-- Check metadata storage
SELECT email,
participant_metadata->'certificateSubmission'->>'certType' as cert_type,
jsonb_array_length(participant_metadata->'wetSignatures') as wet_sig_count
FROM workflow_participants;
```
## Summary
The Shared Signing feature provides a complete collaborative signing workflow with:
- ✅ Multi-participant support with progress tracking
- ✅ Multiple certificate types (P12/PKCS12/PFX, JKS, PEM, SERVER, USER_CERT)
- ✅ Visual wet signature overlays (multiple per participant)
- ✅ Token-based security for unauthenticated participants
- ✅ Authenticated participant access via sign-requests API
- ✅ Automatic role management
- ✅ Large file support (100GB+)
- ✅ GDPR-compliant wet signature metadata cleanup
- ✅ Real-time progress updates
- ✅ Full frontend integration with Quick Access Bar
- ✅ Optional signature summary page with logo and participant details
The architecture leverages existing file sharing infrastructure while adding workflow-specific features, ensuring consistency and maintainability across the application.
+158
View File
@@ -0,0 +1,158 @@
version: '3'
output: prefixed
vars:
FIND_FREE_PORT_PS: powershell -NoProfile -File scripts\find-free-port.ps1 -Preferred
FIND_FREE_PORT_SH: bash scripts/find-free-port.sh
includes:
backend:
taskfile: .taskfiles/backend.yml
dir: .
frontend:
taskfile: .taskfiles/frontend.yml
dir: frontend
engine:
taskfile: .taskfiles/engine.yml
dir: engine
docker:
taskfile: .taskfiles/docker.yml
dir: .
desktop:
taskfile: .taskfiles/desktop.yml
dir: frontend
tasks:
# ============================================================
# Setup & Prerequisites
# ============================================================
install:
desc: "Install all project dependencies"
cmds:
- task: frontend:install
- task: engine:install
# ============================================================
# Development
# ============================================================
dev:
desc: "Start backend + frontend concurrently on free ports"
vars:
PORTS:
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080,5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
deps:
- task: backend:dev
vars:
PORT: '{{.BACKEND_PORT}}'
- task: frontend:dev
vars:
PORT: '{{.FRONTEND_PORT}}'
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
OPEN: "true"
dev:all:
desc: "Start backend + frontend + engine concurrently on free ports"
vars:
PORTS:
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080,5173,5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
ENGINE_PORT: '{{index (splitList "\n" .PORTS) 2}}'
deps:
- task: engine:dev
vars:
PORT: '{{.ENGINE_PORT}}'
- task: backend:dev
vars:
PORT: '{{.BACKEND_PORT}}'
AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}'
- task: frontend:dev:prototypes
vars:
PORT: '{{.FRONTEND_PORT}}'
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
OPEN: "true"
# ============================================================
# Build
# ============================================================
build:
desc: "Build all components"
cmds:
- task: backend:build
- task: frontend:build
# ============================================================
# Test
# ============================================================
test:
desc: "Run ALL tests (backend + frontend + engine)"
cmds:
- task: backend:test
- task: frontend:test
- task: engine:test
# ============================================================
# Lint & Format
# ============================================================
lint:
desc: "Run all linters"
cmds:
- task: frontend:lint
- task: engine:lint
fix:
desc: "Auto-fix all components"
cmds:
- task: backend:fix
- task: frontend:fix
- task: engine:fix
format:
desc: "Auto-fix formatting across all components"
cmds:
- task: backend:format
- task: frontend:format
- task: engine:format
format:check:
desc: "Check formatting across all components"
cmds:
- task: backend:format:check
- task: frontend:format:check
- task: engine:format:check
# ============================================================
# Quality Gate
# ============================================================
check:
desc: "Quick quality gate for local development"
cmds:
- task: backend:check
- task: frontend:check
- task: engine:check
check:all:
desc: "Full CI quality gate"
cmds:
- task: backend:check
- task: frontend:check:all
- task: engine:check
# ============================================================
# Clean
# ============================================================
clean:
desc: "Clean all build artifacts"
cmds:
- task: backend:clean
- task: engine:clean
+10 -7
View File
@@ -5,10 +5,12 @@ bootRun {
spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
toggleOffOn()
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
@@ -27,22 +29,23 @@ spotless {
}
}
dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-aop'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
api 'com.google.guava:guava:33.6.0-jre'
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api 'org.apache.commons:commons-lang3:3.20.0'
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
api 'com.github.junrar:junrar:7.5.10' // 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:2.8.15"
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
api 'org.simplejavamail:simple-java-mail:8.12.6'
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
@@ -68,6 +68,36 @@ public class EndpointConfiguration {
return endpoint.startsWith("/") ? endpoint.substring(1) : endpoint;
}
/**
* Translate a full request URI like {@code /api/v1/general/remove-pages} into the endpoint key
* used by this configuration ({@code remove-pages}). Convert endpoints are a special case -
* {@code /api/v1/convert/pdf/img} is registered as {@code pdf-to-img}. Returns {@code null} if
* the URI is not an {@code /api/v1/<group>/<endpoint>} path.
*/
public static String endpointKeyForUri(String uri) {
if (uri == null || !uri.contains("/api/v1")) {
return null;
}
String[] parts = uri.split("/");
if (parts.length <= 4) {
return null;
}
if ("convert".equals(parts[3]) && parts.length > 5) {
return parts[4] + "-to-" + parts[5];
}
return parts[4];
}
/**
* Convenience wrapper around {@link #isEndpointEnabled(String)} that accepts a full request URI
* and translates it to the endpoint key. Falls back to treating the URI itself as a key for
* non-{@code /api/v1/...} paths so callers can pass arbitrary URIs.
*/
public boolean isEndpointEnabledForUri(String uri) {
String key = endpointKeyForUri(uri);
return isEndpointEnabled(key != null ? key : uri);
}
public void enableEndpoint(String endpoint) {
String normalized = normalizeEndpoint(endpoint);
endpointStatuses.put(normalized, true);
@@ -103,8 +133,9 @@ public class EndpointConfiguration {
// Rule 2: Functional-group override - check if endpoint belongs to any disabled functional
// group
for (String group : endpointGroups.keySet()) {
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
// Skip tool groups (qpdf, OCRmyPDF, Ghostscript, LibreOffice, etc.)
if (!isToolGroup(group)) {
log.debug(
@@ -131,10 +162,11 @@ public class EndpointConfiguration {
// Rule 4: Single-dependency check - if no alternatives defined, check if endpoint belongs
// to any disabled tool groups
for (String group : endpointGroups.keySet()) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (isToolGroup(group)
&& disabledGroups.contains(group)
&& endpointGroups.get(group).contains(endpoint)) {
&& entry.getValue().contains(endpoint)) {
log.debug(
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
original,
@@ -354,6 +386,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Security", "remove-cert-sign");
addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "timestamp-pdf");
addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Security", "validate-signature");
addEndpointToGroup("Security", "add-stamp");
@@ -371,7 +404,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", REMOVE_BLANKS);
addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "remove-image-pdf");
addEndpointToGroup("Other", "add-attachments");
addEndpointToGroup("Other", "replace-invert-pdf");
addEndpointToGroup("Other", "edit-table-of-contents");
@@ -471,6 +503,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "auto-rename");
addEndpointToGroup("Java", "auto-split-pdf");
addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "timestamp-pdf");
addEndpointToGroup("Java", "crop");
addEndpointToGroup("Java", "get-info-on-pdf");
addEndpointToGroup("Java", "pdf-to-single-page");
@@ -486,7 +519,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", REMOVE_BLANKS);
addEndpointToGroup("Java", "remove-annotations");
addEndpointToGroup("Java", "pdf-to-text");
addEndpointToGroup("Java", "remove-image-pdf");
addEndpointToGroup("Java", "pdf-to-markdown");
addEndpointToGroup("Java", "add-attachments");
addEndpointToGroup("Java", "compress-pdf");
@@ -645,8 +677,9 @@ public class EndpointConfiguration {
}
// Check if endpoint belongs to any disabled functional group
for (String group : endpointGroups.keySet()) {
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
for (Map.Entry<String, Set<String>> entry : endpointGroups.entrySet()) {
String group = entry.getKey();
if (disabledGroups.contains(group) && entry.getValue().contains(endpoint)) {
if (!isToolGroup(group)) {
return false;
}
@@ -2,12 +2,14 @@ package stirling.software.common.aop;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@@ -26,7 +28,7 @@ import stirling.software.common.service.JobExecutorService;
@Component
@RequiredArgsConstructor
@Slf4j
@Order(0) // Highest precedence - executes before audit aspects
@Order(20) // Lower precedence - executes AFTER audit aspects populate MDC
public class AutoJobAspect {
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
@@ -70,26 +72,29 @@ public class AutoJobAspect {
// No retries needed, simple execution
return jobExecutorService.runJobGeneric(
async,
() -> {
try {
// Note: Progress tracking is handled in TaskManager/JobExecutorService
// The trackProgress flag controls whether detailed progress is stored
// for REST API queries, not WebSocket notifications
return joinPoint.proceed(args);
} catch (Throwable ex) {
log.error(
"AutoJobAspect caught exception during job execution: {}",
ex.getMessage(),
ex);
// Rethrow RuntimeException as-is to preserve exception type
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(ex);
}
},
wrapWithMDC(
() -> {
try {
// Note: Progress tracking is handled in
// TaskManager/JobExecutorService
// The trackProgress flag controls whether detailed progress is
// stored
// for REST API queries, not WebSocket notifications
return joinPoint.proceed(args);
} catch (Throwable ex) {
log.error(
"AutoJobAspect caught exception during job execution: {}",
ex.getMessage(),
ex);
// Rethrow RuntimeException as-is to preserve exception type
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(ex);
}
}),
timeout,
queueable,
resourceWeight);
@@ -123,114 +128,108 @@ public class AutoJobAspect {
return jobExecutorService.runJobGeneric(
async,
() -> {
// Use iterative approach instead of recursion to avoid stack overflow
Throwable lastException = null;
wrapWithMDC(
() -> {
// Use iterative approach instead of recursion to avoid stack overflow
Throwable lastException = null;
// Attempt counter starts at 1 for first try
for (int currentAttempt = 1; currentAttempt <= maxRetries; currentAttempt++) {
try {
if (trackProgress && async) {
// Get jobId for progress tracking in TaskManager
// This enables REST API progress queries, not WebSocket
if (jobIdRef.get() == null) {
jobIdRef.set(getJobIdFromContext());
}
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Tracking progress for job {} (attempt {}/{})",
jobId,
// Attempt counter starts at 1 for first try
for (int currentAttempt = 1;
currentAttempt <= maxRetries;
currentAttempt++) {
try {
if (trackProgress && async) {
// Get jobId for progress tracking in TaskManager
// This enables REST API progress queries, not WebSocket
if (jobIdRef.get() == null) {
jobIdRef.set(getJobIdFromContext());
}
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Tracking progress for job {} (attempt {}/{})",
jobId,
currentAttempt,
maxRetries);
// Progress is tracked in TaskManager for REST API
// access
// No WebSocket notifications sent here
}
}
// Attempt to execute the operation
return joinPoint.proceed(args);
} catch (Throwable ex) {
lastException = ex;
log.error(
"AutoJobAspect caught exception during job execution (attempt"
+ " {}/{}): {}",
currentAttempt,
maxRetries);
// Progress is tracked in TaskManager for REST API access
// No WebSocket notifications sent here
}
}
maxRetries,
ex.getMessage(),
ex);
// Attempt to execute the operation
return joinPoint.proceed(args);
// Check if we should retry
if (currentAttempt < maxRetries) {
log.info(
"Retrying operation, attempt {}/{}",
currentAttempt + 1,
maxRetries);
} catch (Throwable ex) {
lastException = ex;
log.error(
"AutoJobAspect caught exception during job execution (attempt"
+ " {}/{}): {}",
currentAttempt,
maxRetries,
ex.getMessage(),
ex);
if (trackProgress && async) {
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Recording retry attempt for job {} in TaskManager",
jobId);
// Retry info is tracked in TaskManager for REST API
// access
}
}
// Check if we should retry
if (currentAttempt < maxRetries) {
log.info(
"Retrying operation, attempt {}/{}",
currentAttempt + 1,
maxRetries);
// Use sleep for retry delay
// For sync jobs, both sleep and async are blocking at this
// point
// For async jobs, the delay occurs in the executor thread
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
if (trackProgress && async) {
String jobId = jobIdRef.get();
if (jobId != null) {
log.debug(
"Recording retry attempt for job {} in TaskManager",
jobId);
// Retry info is tracked in TaskManager for REST API access
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.debug(
"Retry delay interrupted for attempt {}/{}",
currentAttempt,
maxRetries);
break;
}
} else {
// No more retries, we'll throw the exception after the loop
break;
}
}
// Use non-blocking delay for all retry attempts to avoid blocking
// threads
// For sync jobs this avoids starving the tomcat thread pool under
// load
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
// Execute the retry after a delay through the JobExecutorService
// rather than blocking the current thread with sleep
CompletableFuture<Object> delayedRetry = new CompletableFuture<>();
// Use a delayed executor for non-blocking delay
CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
.execute(
() -> {
// Continue the retry loop in the next iteration
// We can't return from here directly since
// we're in a Runnable
delayedRetry.complete(null);
});
// Wait for the delay to complete before continuing
try {
delayedRetry.join();
} catch (Exception e) {
Thread.currentThread().interrupt();
break;
}
} else {
// No more retries, we'll throw the exception after the loop
break;
}
}
}
// If we get here, all retries failed
if (lastException != null) {
// Rethrow RuntimeException as-is to preserve exception type
if (lastException instanceof RuntimeException) {
throw (RuntimeException) lastException;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(
"Job failed after "
+ maxRetries
+ " attempts: "
+ lastException.getMessage(),
lastException);
}
// If we get here, all retries failed
if (lastException != null) {
// Rethrow RuntimeException as-is to preserve exception type
if (lastException instanceof RuntimeException) {
throw (RuntimeException) lastException;
}
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
// BaseAppException
throw new RuntimeException(
"Job failed after "
+ maxRetries
+ " attempts: "
+ lastException.getMessage(),
lastException);
}
// This should never happen if lastException is properly tracked
throw new RuntimeException("Job failed but no exception was recorded");
},
// This should never happen if lastException is properly tracked
throw new RuntimeException("Job failed but no exception was recorded");
}),
timeout,
queueable,
resourceWeight);
@@ -299,4 +298,32 @@ public class AutoJobAspect {
return null;
}
}
/**
* Wraps a supplier to propagate MDC context to background threads. Captures MDC on request
* thread and restores it in the background thread. Ensures proper cleanup to prevent context
* leakage across jobs in thread pools.
*/
private <T> Supplier<T> wrapWithMDC(Supplier<T> supplier) {
final Map<String, String> captured = MDC.getCopyOfContextMap();
return () -> {
final Map<String, String> previous = MDC.getCopyOfContextMap();
try {
// Set the captured context (or clear if none was captured)
if (captured != null) {
MDC.setContextMap(new HashMap<>(captured));
} else {
MDC.clear();
}
return supplier.get();
} finally {
// Restore previous state (or clear if there was none)
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}
}
@@ -14,7 +14,6 @@ public class InstallationPathConfig {
private static final String CUSTOM_FILES_PATH;
private static final String CLIENT_WEBUI_PATH;
private static final String PIPELINE_PATH;
private static final String PLUGINS_PATH;
// Config paths
private static final String SETTINGS_PATH;
@@ -41,7 +40,6 @@ public class InstallationPathConfig {
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
PIPELINE_PATH = BASE_PATH + "pipeline" + File.separator;
PLUGINS_PATH = CUSTOM_FILES_PATH + "plugins" + File.separator;
// Initialize config paths
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
@@ -112,10 +110,6 @@ public class InstallationPathConfig {
return SIGNATURES_PATH;
}
public static String getPluginsPath() {
return PLUGINS_PATH;
}
public static String getPrivateKeyPath() {
return BACKUP_PRIVATE_KEY_PATH;
}
@@ -91,9 +91,9 @@ public class RuntimePathConfig {
// Initialize Operation paths
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
String defaultUnoConvertPath = isDocker ? "/usr/local/bin/unoconvert" : "unoconvert";
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
String defaultOcrMyPdfPath = isDocker ? "/opt/venv/bin/ocrmypdf" : "ocrmypdf";
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
Operations operations = customPaths.getOperations();
@@ -0,0 +1,23 @@
package stirling.software.common.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
/**
* Configures the scheduler used by all {@code @Scheduled} methods. Uses virtual threads so that
* long-running scheduled tasks (e.g. cleanup, license checks, file monitoring) never block each
* other — each runs on its own lightweight virtual thread.
*/
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
scheduler.setVirtualThreads(true);
scheduler.setThreadNamePrefix("scheduled-vt-");
return scheduler;
}
}
@@ -58,6 +58,7 @@ public class ApplicationProperties {
private Legal legal = new Legal();
private Security security = new Security();
private System system = new System();
private Storage storage = new Storage();
private Ui ui = new Ui();
private Endpoints endpoints = new Endpoints();
private Metrics metrics = new Metrics();
@@ -74,6 +75,7 @@ public class ApplicationProperties {
private AutoPipeline autoPipeline = new AutoPipeline();
private ProcessExecutor processExecutor = new ProcessExecutor();
private PdfEditor pdfEditor = new PdfEditor();
private AiEngine aiEngine = new AiEngine();
@Bean
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
@@ -150,6 +152,44 @@ public class ApplicationProperties {
@Data
public static class AutoPipeline {
private String outputFolder;
private FileReadiness fileReadiness = new FileReadiness();
/**
* Configuration for the {@link stirling.software.common.util.FileReadinessChecker}.
* Controls how the pipeline determines whether a file is fully written and stable before
* processing begins.
*/
@Data
public static class FileReadiness {
/**
* Master toggle. When {@code false} every readiness check is skipped and all files are
* considered immediately ready (preserves legacy behaviour).
*/
private boolean enabled = true;
/**
* How long (in milliseconds) a file must remain unmodified before it is considered
* stable. Files modified more recently than this threshold are skipped and retried on
* the next scan cycle. Default: 5 000 ms (5 seconds).
*/
private long settleTimeMillis = 5000;
/**
* How long (in milliseconds) to pause between two consecutive file-size reads when
* checking whether a file is still being written. If the size differs between the two
* reads the file is considered unstable. This catches active copies on Linux/macOS
* where advisory locking alone cannot detect a mid-copy file. Default: 500 ms.
*/
private long sizeCheckDelayMillis = 500;
/**
* Optional list of file extensions (without the leading dot, case-insensitive) that are
* allowed through the readiness check. An empty list means all extensions are accepted.
* Example: {@code ["pdf", "tiff"]} will skip any file whose extension is not {@code
* pdf} or {@code tiff}.
*/
private List<String> allowedExtensions = new java.util.ArrayList<>();
}
}
@Data
@@ -192,6 +232,20 @@ public class ApplicationProperties {
}
}
@Data
public static class AiEngine {
private boolean enabled = false;
private String url = "http://localhost:5001";
private int timeoutSeconds = 120;
/**
* Longer timeout for heavy operations like RAG ingestion, which embeds the whole document
* and can take multiple minutes for large books. Applied per-call when the caller
* explicitly requests it via {@code AiEngineClient.postWithTimeout}.
*/
private int longRunningTimeoutSeconds = 600;
}
@Data
public static class Legal {
private String termsAndConditions;
@@ -213,6 +267,7 @@ public class ApplicationProperties {
private String customGlobalAPIKey;
private Jwt jwt = new Jwt();
private Validation validation = new Validation();
private Timestamp timestamp = new Timestamp();
private String xFrameOptions = "DENY";
public Boolean isAltLogin() {
@@ -365,11 +420,11 @@ public class ApplicationProperties {
}
public boolean isSettingsValid() {
return !ValidationUtils.isStringEmpty(this.getIssuer())
&& !ValidationUtils.isStringEmpty(this.getClientId())
&& !ValidationUtils.isStringEmpty(this.getClientSecret())
&& !ValidationUtils.isCollectionEmpty(this.getScopes())
&& !ValidationUtils.isStringEmpty(this.getUseAsUsername());
return !ValidationUtils.isStringEmpty(this.issuer)
&& !ValidationUtils.isStringEmpty(this.clientId)
&& !ValidationUtils.isStringEmpty(this.clientSecret)
&& !ValidationUtils.isCollectionEmpty(this.scopes)
&& !ValidationUtils.isStringEmpty(this.useAsUsername);
}
@Data
@@ -531,6 +586,12 @@ public class ApplicationProperties {
private boolean hardFail = false;
}
}
@Data
public static class Timestamp {
private String defaultTsaUrl = "http://timestamp.digicert.com";
private List<String> customTsaUrls = new ArrayList<>();
}
}
@Data
@@ -575,19 +636,52 @@ public class ApplicationProperties {
}
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
return this.enableAnalytics != null && this.enableAnalytics;
}
public boolean isPosthogEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
return this.isAnalyticsEnabled() && (this.enablePosthog == null || this.enablePosthog);
}
public boolean isScarfEnabled() {
// Treat null as enabled when analytics is enabled
return this.isAnalyticsEnabled()
&& (this.getEnableScarf() == null || this.getEnableScarf());
return this.isAnalyticsEnabled() && (this.enableScarf == null || this.enableScarf);
}
}
@Data
public static class Storage {
private boolean enabled = false;
private String provider = "local";
private Local local = new Local();
private Quotas quotas = new Quotas();
private Sharing sharing = new Sharing();
private Signing signing = new Signing();
@Data
public static class Local {
private String basePath = InstallationPathConfig.getPath() + "storage";
}
@Data
public static class Sharing {
private boolean enabled = false;
private boolean linkEnabled = false;
private boolean emailEnabled = false;
private int linkExpirationDays = 3;
}
@Data
public static class Quotas {
private long maxStorageMbPerUser = -1;
private long maxStorageMbTotal = -1;
private long maxFileMb = -1;
}
@Data
public static class Signing {
private boolean enabled = false;
}
}
@@ -714,6 +808,7 @@ public class ApplicationProperties {
private String logoStyle = "classic"; // Options: "classic" (default) or "modern"
private boolean defaultHideUnavailableTools = false;
private boolean defaultHideUnavailableConversions = false;
private HideDisabledTools hideDisabledTools = new HideDisabledTools();
public String getAppNameNavbar() {
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
@@ -726,6 +821,12 @@ public class ApplicationProperties {
}
return "classic"; // default
}
@Data
public static class HideDisabledTools {
private boolean googleDrive = false;
private boolean mobileQRScanner = false;
}
}
@Data
@@ -913,6 +1014,15 @@ public class ApplicationProperties {
private boolean ssoAutoLogin;
private boolean database;
private CustomMetadata customMetadata = new CustomMetadata();
private GoogleDrive googleDrive = new GoogleDrive();
@Data
public static class GoogleDrive {
private boolean enabled = false;
private String clientId = "";
private String apiKey = "";
private String appId = "";
}
@Data
public static class CustomMetadata {
@@ -962,6 +1072,12 @@ public class ApplicationProperties {
private boolean enabled = true;
private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE
private int retentionDays = 90;
private boolean captureFileHash =
false; // Capture SHA-256 hash of files (increases processing time)
private boolean capturePdfAuthor =
false; // Capture PDF author metadata (increases processing time)
private boolean captureOperationResults =
false; // Capture operation return values (not recommended, high volume)
}
@Data
@@ -0,0 +1,15 @@
package stirling.software.common.model.api.comments;
/**
* Absolute position of a PDF annotation in the document.
*
* <p>Coordinates are in PDF user-space with the origin at the page's bottom-left, consistent with
* PDFBox's {@code PDRectangle} convention.
*
* @param pageIndex 0-indexed page number the annotation lives on.
* @param x bottom-left x coordinate of the annotation rectangle.
* @param y bottom-left y coordinate of the annotation rectangle.
* @param width width of the annotation rectangle, in user-space units.
* @param height height of the annotation rectangle, in user-space units.
*/
public record AnnotationLocation(int pageIndex, float x, float y, float width, float height) {}
@@ -0,0 +1,15 @@
package stirling.software.common.model.api.comments;
/**
* Description of a single sticky-note (PDF Text) annotation to place on a document.
*
* <p>{@code author} and {@code subject} are optional — callers that pass {@code null} get a default
* author/subject from {@code PdfAnnotationService}.
*
* @param location where to anchor the annotation icon, in PDF user-space.
* @param text the comment body shown in the popup (required, non-blank).
* @param author optional author label shown in the popup; {@code null} → service default.
* @param subject optional subject line shown in the popup; {@code null} → service default.
*/
public record StickyNoteSpec(
AnnotationLocation location, String text, String author, String subject) {}
@@ -0,0 +1,16 @@
package stirling.software.common.model.api.security;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserSummaryDTO {
private Long userId;
private String username;
private String displayName;
private String teamName;
private boolean enabled;
}
@@ -42,7 +42,7 @@ public enum Role {
// Using the fromString method to get the Role enum based on the roleId
Role role = fromString(roleId);
// Return the roleName of the found Role enum
return role.getRoleName();
return role.roleName;
}
// Method to retrieve all role IDs and role names
@@ -50,14 +50,14 @@ public enum Role {
// Using LinkedHashMap to preserve order
Map<String, String> roleDetails = new LinkedHashMap<>();
for (Role role : Role.values()) {
roleDetails.put(role.getRoleId(), role.getRoleName());
roleDetails.put(role.roleId, role.roleName);
}
return roleDetails;
}
public static Role fromString(String roleId) {
for (Role role : Role.values()) {
if (role.getRoleId().equalsIgnoreCase(roleId)) {
if (role.roleId.equalsIgnoreCase(roleId)) {
return role;
}
}
@@ -117,13 +117,13 @@ public class Provider {
+ ", clientName="
+ getClientName()
+ ", clientId="
+ getClientId()
+ clientId
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ (clientSecret != null && !clientSecret.isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ useAsUsername
+ "]";
}
}
@@ -1,34 +0,0 @@
package stirling.software.common.plugins;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Value;
/** Immutable descriptor that represents a loaded plugin. */
@Value
@Builder
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class PluginDescriptor {
String id;
String icon;
String name;
String description;
String version;
String author;
String frontendLabel;
String frontendPath;
String iconPath;
String minHostVersion;
String jarCreatedAt;
@Builder.Default boolean hasFrontend = false;
@Builder.Default List<String> backendEndpoints = Collections.emptyList();
@Builder.Default Map<String, String> metadata = Collections.emptyMap();
}
@@ -1,65 +0,0 @@
package stirling.software.common.plugins;
import java.util.List;
import java.util.Map;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
/**
* API-facing representation of a plugin descriptor with a fully resolved frontend URL.
*
* <p>This DTO is returned to clients so they can render plugin metadata and open plugin UIs.
*/
public class PluginDescriptorResponse {
String id;
String icon;
String name;
String description;
String version;
String author;
String frontendUrl;
String frontendLabel;
String iconPath;
String minHostVersion;
String jarCreatedAt;
boolean hasFrontend;
List<String> backendEndpoints;
Map<String, String> metadata;
/**
* Creates a response object from an internal {@link PluginDescriptor}.
*
* @param descriptor loaded plugin descriptor
* @param baseUrl optional API base URL used to build an absolute frontend URL
* @return normalized response payload for API clients
*/
public static PluginDescriptorResponse from(PluginDescriptor descriptor, String baseUrl) {
String frontendPath = descriptor.getFrontendPath();
String normalizedBase = baseUrl != null ? baseUrl.replaceAll("/+$", "") : "";
String normalizedPath = frontendPath != null ? frontendPath.replaceAll("^/+", "/") : "";
String frontendUrl =
(normalizedBase.isEmpty() || normalizedPath.isEmpty())
? (normalizedPath.isEmpty() ? null : normalizedPath)
: normalizedBase + normalizedPath;
return PluginDescriptorResponse.builder()
.id(descriptor.getId())
.icon(descriptor.getIcon())
.name(descriptor.getName())
.description(descriptor.getDescription())
.version(descriptor.getVersion())
.author(descriptor.getAuthor())
.frontendUrl(frontendUrl)
.frontendLabel(descriptor.getFrontendLabel())
.iconPath(descriptor.getIconPath())
.hasFrontend(descriptor.isHasFrontend())
.backendEndpoints(descriptor.getBackendEndpoints())
.metadata(descriptor.getMetadata())
.minHostVersion(descriptor.getMinHostVersion())
.jarCreatedAt(descriptor.getJarCreatedAt())
.build();
}
}
@@ -1,261 +0,0 @@
package stirling.software.common.plugins;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
/**
* Utility responsible for discovering plugin jars, parsing their metadata, and integrating them
* into the Stirling-PDF runtime.
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class PluginLoader {
private static final String JAR_EXTENSION = ".jar";
private static final String JAR_MIME_TYPE = "application/java-archive";
private static final String METADATA_RESOURCE = "META-INF/stirling-plugin.json";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Discovers plugin jar files in the configured plugins directory.
*
* @return sorted list of valid plugin jar paths
*/
public static List<Path> listPluginJars() {
Path pluginDir = ensurePluginDirectory();
if (!Files.isDirectory(pluginDir)) {
return Collections.emptyList();
}
try (Stream<Path> stream = Files.list(pluginDir)) {
return stream.filter(Files::isRegularFile)
.filter(PluginLoader::looksLikeJarFile)
.filter(PluginLoader::isReadableJarArchive)
.sorted(
Comparator.comparing(
path -> path.getFileName().toString().toLowerCase()))
.collect(Collectors.toList());
} catch (IOException e) {
log.warn("Failed to list plugin directory {}: {}", pluginDir, e.getMessage());
return Collections.emptyList();
}
}
/**
* Converts discovered plugin jar paths into URL entries suitable for class/resource loading.
*
* @return immutable-style list of valid jar URLs
*/
public static List<URL> pluginJarUrls() {
List<Path> jars = listPluginJars();
if (jars.isEmpty()) {
return Collections.emptyList();
}
List<URL> urls = new ArrayList<>(jars.size());
for (Path jar : jars) {
try {
urls.add(jar.toUri().toURL());
} catch (MalformedURLException e) {
log.warn("Skipping plugin jar with invalid URL {}: {}", jar, e.getMessage());
}
}
return urls;
}
/**
* Creates a class loader that can load classes/resources from installed plugins.
*
* @param parent parent class loader
* @return plugin-aware class loader or parent when no plugin jars exist
*/
public static ClassLoader buildPluginClassLoader(ClassLoader parent) {
List<URL> urls = pluginJarUrls();
if (urls.isEmpty()) {
return parent;
}
log.info(
"Scanning {} plugin jars in {}",
urls.size(),
InstallationPathConfig.getPluginsPath());
return new URLClassLoader(urls.toArray(URL[]::new), parent);
}
/**
* Loads descriptors for all discovered plugin jars.
*
* @return immutable list of successfully parsed descriptors
*/
public static List<PluginDescriptor> loadDescriptors() {
List<Path> jars = listPluginJars();
if (jars.isEmpty()) {
return Collections.emptyList();
}
List<PluginDescriptor> descriptors = new ArrayList<>();
for (Path jar : jars) {
PluginDescriptor descriptor = readDescriptorFromJar(jar);
if (descriptor != null) {
descriptors.add(descriptor);
}
}
return Collections.unmodifiableList(descriptors);
}
/**
* Loads metadata for one plugin jar and maps it to a descriptor.
*
* @param jarPath plugin jar path
* @return descriptor when valid metadata exists, otherwise {@code null}
*/
public static PluginDescriptor loadDescriptor(Path jarPath) {
return readDescriptorFromJar(jarPath);
}
private static Path ensurePluginDirectory() {
Path pluginDir = Path.of(InstallationPathConfig.getPluginsPath());
try {
return Files.createDirectories(pluginDir);
} catch (IOException e) {
log.error("Unable to create plugin directory {}", pluginDir, e);
return pluginDir;
}
}
private static PluginDescriptor readDescriptorFromJar(Path jarPath) {
if (!Files.isRegularFile(jarPath)) {
log.warn("Plugin jar {} is not a regular file, skipping", jarPath);
return null;
}
try (JarFile jarFile = new JarFile(jarPath.toFile())) {
JarEntry entry = jarFile.getJarEntry(METADATA_RESOURCE);
if (entry == null) {
log.info("Plugin jar {} does not include {}, skipping", jarPath, METADATA_RESOURCE);
return null;
}
PluginMetadata metadata;
try (InputStream inputStream = jarFile.getInputStream(entry)) {
metadata = OBJECT_MAPPER.readValue(inputStream, PluginMetadata.class);
}
String createdAt = resolveJarTimestamp(jarPath);
if (metadata.getId() == null || metadata.getId().isBlank()) {
log.warn("Plugin metadata in {} is missing required id, ignoring", jarPath);
return null;
}
String pluginId = metadata.getId();
log.info(
"Loaded metadata for plugin '{}': name='{}' version='{}'",
pluginId,
metadata.getName(),
metadata.getVersion());
return buildDescriptor(metadata, createdAt);
} catch (IOException e) {
log.warn("Failed to inspect plugin jar {}: {}", jarPath, e.getMessage());
return null;
}
}
private static PluginDescriptor buildDescriptor(PluginMetadata metadata, String jarCreatedAt) {
PluginMetadata.PluginFrontend frontend = metadata.getFrontend();
String id = metadata.getId();
String icon = metadata.getIcon();
String frontendPath =
(frontend != null
&& frontend.getEntrypoint() != null
&& !frontend.getEntrypoint().isBlank())
? ensureLeadingSlash(frontend.getEntrypoint())
: "/plugins/" + id + "/index.html";
return PluginDescriptor.builder()
.id(id)
.icon(defaultIfEmpty(icon, null))
.name(defaultIfEmpty(metadata.getName(), id))
.description(defaultIfEmpty(metadata.getDescription(), ""))
.version(defaultIfEmpty(metadata.getVersion(), "0.0.0"))
.author(metadata.getAuthor())
.frontendLabel(frontend != null ? frontend.getLabel() : null)
.frontendPath(frontendPath)
.iconPath(frontend != null ? frontend.getIconPath() : null)
.hasFrontend(frontend != null)
.backendEndpoints(
metadata.getBackendEndpoints() == null
? Collections.emptyList()
: metadata.getBackendEndpoints())
.metadata(
metadata.getMetadata() == null
? Collections.emptyMap()
: metadata.getMetadata())
.minHostVersion(defaultIfEmpty(metadata.getMinHostVersion(), null))
.jarCreatedAt(jarCreatedAt)
.build();
}
private static String defaultIfEmpty(String value, String fallback) {
return (value == null || value.isBlank()) ? fallback : value;
}
private static String ensureLeadingSlash(String path) {
if (path == null || path.isBlank()) {
return "/";
}
return path.startsWith("/") ? path : "/" + path;
}
private static boolean looksLikeJarFile(Path path) {
return path.getFileName().toString().toLowerCase().endsWith(JAR_EXTENSION);
}
private static boolean isReadableJarArchive(Path path) {
try {
String mimeType = Files.probeContentType(path);
if (mimeType != null && !JAR_MIME_TYPE.equals(mimeType)) {
log.debug("Ignoring non-jar mime type {} for {}", mimeType, path);
return false;
}
} catch (IOException e) {
log.debug("Unable to probe mime type for {}: {}", path, e.getMessage());
}
try (JarFile ignored = new JarFile(path.toFile())) {
return true;
} catch (IOException e) {
log.warn("Skipping invalid jar archive {}: {}", path, e.getMessage());
return false;
}
}
private static String resolveJarTimestamp(Path jarPath) throws IOException {
BasicFileAttributes attrs = Files.readAttributes(jarPath, BasicFileAttributes.class);
FileTime creationTime = attrs.creationTime();
FileTime lastModifiedTime = attrs.lastModifiedTime();
FileTime preferredTime =
creationTime == null || creationTime.toMillis() <= 0
? lastModifiedTime
: creationTime;
return preferredTime.toInstant().toString();
}
}
@@ -1,36 +0,0 @@
package stirling.software.common.plugins;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
/** Model for deserializing {@code META-INF/stirling-plugin.json} from a plugin jar. */
public class PluginMetadata {
private String id;
private String icon;
private String name;
private String description;
private String version;
private String author;
private String minHostVersion;
private PluginFrontend frontend;
private List<String> backendEndpoints;
private Map<String, String> metadata;
/** Frontend-specific metadata block declared inside plugin metadata JSON. */
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class PluginFrontend {
private String entrypoint;
private String label;
private String iconPath;
}
}
@@ -1,28 +0,0 @@
package stirling.software.common.plugins;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
/** Registers MVC resource handlers that expose static assets from plugin jars. */
public class PluginResourceConfig implements WebMvcConfigurer {
/** Adds {@code /plugins/**} static resource mappings for every discovered plugin jar. */
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
List<String> locations =
PluginLoader.pluginJarUrls().stream()
.map(url -> "jar:" + url + "!/META-INF/resources/plugins/")
.collect(Collectors.toList());
if (!locations.isEmpty()) {
registry.addResourceHandler("/plugins/**")
.addResourceLocations(locations.toArray(String[]::new))
.setCachePeriod(0);
}
}
}
@@ -1,13 +1,19 @@
package stirling.software.common.service;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -21,6 +27,9 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FileStorage {
/** Holds the result of a stream-to-disk store operation: the file ID and the bytes written. */
public record StoredFile(String fileId, long size) {}
@Value("${stirling.tempDir:/tmp/stirling-files}")
private String tempDirPath;
@@ -104,6 +113,93 @@ public class FileStorage {
return Files.readAllBytes(filePath);
}
/**
* Retrieve a file by its ID as a streaming InputStream. The caller is responsible for closing
* the returned stream.
*
* @param fileId The ID of the file to retrieve
* @return A buffered InputStream for the file
* @throws IOException If the file doesn't exist or can't be read
*/
public InputStream retrieveInputStream(String fileId) throws IOException {
Path filePath = getFilePath(fileId);
// Let Files.newInputStream throw NoSuchFileException naturally — avoids TOCTOU race
// between exists-check and open when another thread may delete concurrently.
return new BufferedInputStream(Files.newInputStream(filePath));
}
/**
* Store data from an InputStream as a file and return its unique ID and byte count. Streams
* directly to disk without buffering the entire content in heap.
*
* @param inputStream The input stream to read from
* @param originalName The original name of the file (unused, kept for API symmetry)
* @return A {@link StoredFile} containing the file ID and the number of bytes written
* @throws IOException If there is an error storing the file
*/
public StoredFile storeInputStream(InputStream inputStream, String originalName)
throws IOException {
String fileId = generateFileId();
Path filePath = getFilePath(fileId);
Files.createDirectories(filePath.getParent());
long size = Files.copy(inputStream, filePath);
log.debug("Stored input stream with ID: {}", fileId);
return new StoredFile(fileId, size);
}
public String storeFromStreamingBody(StreamingResponseBody body, String originalName)
throws IOException {
String fileId = generateFileId();
Path filePath = getFilePath(fileId);
Files.createDirectories(filePath.getParent());
boolean success = false;
try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(filePath))) {
body.writeTo(os);
success = true;
} finally {
if (!success) {
try {
Files.deleteIfExists(filePath);
} catch (IOException cleanupEx) {
log.warn(
"Failed to clean up partial file {} after store failure",
filePath,
cleanupEx);
}
}
}
log.debug("Stored StreamingResponseBody with ID: {}", fileId);
return fileId;
}
/**
* Persist a {@link Resource} body to disk, returning the generated file ID. Used by the async
* job pipeline to capture {@code ResponseEntity<Resource>} results produced by controllers.
*/
public String storeFromResource(Resource resource, String originalName) throws IOException {
String fileId = generateFileId();
Path filePath = getFilePath(fileId);
Files.createDirectories(filePath.getParent());
boolean success = false;
try (InputStream in = resource.getInputStream()) {
Files.copy(in, filePath);
success = true;
} finally {
if (!success) {
try {
Files.deleteIfExists(filePath);
} catch (IOException cleanupEx) {
log.warn(
"Failed to clean up partial file {} after store failure",
filePath,
cleanupEx);
}
}
}
log.debug("Stored Resource with ID: {}", fileId);
return fileId;
}
/**
* Delete a file by its ID
*
@@ -0,0 +1,190 @@
package stirling.software.common.service;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.enumeration.Role;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
/**
* Dispatches HTTP POST requests to internal Stirling API endpoints via loopback. Used by
* PipelineProcessor and AiWorkflowService to execute tool operations programmatically without
* leaving the JVM network stack.
*/
@Service
@Slf4j
public class InternalApiClient {
// Allowlist for internal dispatch. Matches fixed namespace prefixes,
// but rejects traversal (..), URL-encoding (%), query/fragment, backslashes, and any other
// character that could alter the resolved endpoint on the local Spring server.
//
// The second alternation carves out `/api/v1/ai/tools/*` specifically — AI tools are
// dispatchable, but the broader `/api/v1/ai/` surface (orchestrate, health, etc.) is
// intentionally NOT permitted to avoid plan steps re-entering the orchestrator.
private static final Pattern ALLOWED_ENDPOINT_PATH =
Pattern.compile(
"^/api/v1/(general|misc|security|convert|filter)(/[A-Za-z0-9_-]+)+$"
+ "|^/api/v1/ai/tools(/[A-Za-z0-9_-]+)+$");
private final ServletContext servletContext;
private final UserServiceInterface userService;
private final TempFileManager tempFileManager;
private final Environment environment;
public InternalApiClient(
ServletContext servletContext,
@Autowired(required = false) UserServiceInterface userService,
TempFileManager tempFileManager,
Environment environment) {
this.servletContext = servletContext;
this.userService = userService;
this.tempFileManager = tempFileManager;
this.environment = environment;
}
/**
* POST to an internal API endpoint. The endpointPath must start with one of the allowed
* prefixes (e.g. {@code /api/v1/misc/compress-pdf}).
*
* @param endpointPath API path (e.g. {@code /api/v1/general/rotate-pdf})
* @param body multipart form body (fileInput + parameters)
* @return response with the result file as a {@link TempFileResource} body
*/
public ResponseEntity<Resource> post(String endpointPath, MultiValueMap<String, Object> body) {
validateUrl(endpointPath);
String url = getBaseUrl() + endpointPath;
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser();
if (apiKey != null && !apiKey.isEmpty()) {
headers.add("X-API-KEY", apiKey);
}
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
RequestCallback requestCallback = restTemplate.httpEntityCallback(entity, Resource.class);
return restTemplate.execute(
url,
HttpMethod.POST,
requestCallback,
response -> {
try {
TempFile tempFile = tempFileManager.createManagedTempFile("internal-api");
Files.copy(
response.getBody(),
tempFile.getPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
String filename = extractFilename(response.getHeaders());
TempFileResource resource = new TempFileResource(tempFile, filename);
return ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders())
.body(resource);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
/**
* Extract the filename from a response's {@code Content-Disposition} header. Returns {@code
* null} if the header is missing or has no filename.
*/
private static String extractFilename(HttpHeaders headers) {
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
if (contentDisposition == null || contentDisposition.isBlank()) {
return null;
}
for (String part : contentDisposition.split(";")) {
String trimmed = part.trim();
if (trimmed.startsWith("filename")) {
String[] kv = trimmed.split("=", 2);
if (kv.length != 2) {
continue;
}
String value = kv[1].trim().replace("\"", "");
return URLDecoder.decode(value, StandardCharsets.UTF_8);
}
}
return null;
}
private String getBaseUrl() {
// Resolve the port lazily so desktop mode (server.port=0, OS-assigned) dispatches to the
// actual bound port. Spring publishes local.server.port once the web server is up; fall
// back to the configured server.port for early calls (tests, non-web contexts).
String port = environment.getProperty("local.server.port");
if (port == null) {
port = environment.getProperty("server.port", "8080");
}
return "http://localhost:" + port + servletContext.getContextPath();
}
private String getApiKeyForUser() {
if (userService == null) return "";
String username = userService.getCurrentUsername();
if (username != null && !username.equals("anonymousUser")) {
return userService.getApiKeyForUser(username);
}
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
private void validateUrl(String endpointPath) {
if (endpointPath == null || !ALLOWED_ENDPOINT_PATH.matcher(endpointPath).matches()) {
log.warn("Blocked internal API request to disallowed path: {}", endpointPath);
throw new SecurityException(
"Internal API dispatch not permitted for endpoint: " + endpointPath);
}
}
/**
* A {@link FileSystemResource} that holds a reference to its backing {@link TempFile}.
*
* <p>If a display filename is supplied (typically parsed from the upstream response's {@code
* Content-Disposition} header), it is returned from {@link #getFilename()} instead of the
* underlying temp file's path-based name.
*/
public static class TempFileResource extends FileSystemResource {
private final TempFile tempFile;
private final String displayFilename;
public TempFileResource(TempFile tempFile) {
this(tempFile, null);
}
public TempFileResource(TempFile tempFile, String displayFilename) {
super(tempFile.getFile());
this.tempFile = tempFile;
this.displayFilename = displayFilename;
}
public TempFile getTempFile() {
return tempFile;
}
@Override
public String getFilename() {
return displayFilename != null ? displayFilename : super.getFilename();
}
}
}
@@ -11,11 +11,13 @@ import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import jakarta.servlet.http.HttpServletRequest;
@@ -35,7 +37,7 @@ public class JobExecutorService {
private final HttpServletRequest request;
private final ResourceMonitor resourceMonitor;
private final JobQueue jobQueue;
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
private final long effectiveTimeoutMs;
@Autowired(required = false)
@@ -254,10 +256,13 @@ public class JobExecutorService {
return ResponseEntity.internalServerError()
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
} catch (RuntimeException e) {
// Check if this is a wrapped typed exception that should be handled by
// GlobalExceptionHandler
// Check if this is a typed exception that should be handled by
// GlobalExceptionHandler (either directly or wrapped)
Throwable cause = e.getCause();
if (cause instanceof stirling.software.common.util.ExceptionUtils.BaseAppException
if (e instanceof IllegalArgumentException
|| cause
instanceof
stirling.software.common.util.ExceptionUtils.BaseAppException
|| cause
instanceof
stirling.software.common.util.ExceptionUtils
@@ -302,33 +307,28 @@ public class JobExecutorService {
Object body = response.getBody();
if (body instanceof byte[]) {
// Extract filename from content-disposition header if available
String filename = "result.pdf";
String contentType = MediaType.APPLICATION_PDF_VALUE;
String filename = extractResponseFilename(response);
String contentType = extractResponseContentType(response);
if (response.getHeaders().getContentDisposition() != null) {
String disposition =
response.getHeaders().getContentDisposition().toString();
if (disposition.contains("filename=")) {
filename =
disposition.substring(
disposition.indexOf("filename=") + 9,
disposition.lastIndexOf("\""));
}
}
MediaType mediaType = response.getHeaders().getContentType();
if (mediaType != null) {
contentType = mediaType.toString();
}
// Store byte array directly to disk
String fileId = fileStorage.storeBytes((byte[]) body, filename);
taskManager.setFileResult(jobId, fileId, filename, contentType);
log.debug("Stored ResponseEntity<byte[]> result with fileId: {}", fileId);
} else if (body instanceof StreamingResponseBody streamingBody) {
String filename = extractResponseFilename(response);
String contentType = extractResponseContentType(response);
// Let the GC handle the memory naturally
String fileId = fileStorage.storeFromStreamingBody(streamingBody, filename);
taskManager.setFileResult(jobId, fileId, filename, contentType);
log.debug(
"Stored ResponseEntity<StreamingResponseBody> result with fileId: {}",
fileId);
} else if (body instanceof Resource resource) {
String filename = extractResponseFilename(response);
String contentType = extractResponseContentType(response);
String fileId = fileStorage.storeFromResource(resource, filename);
taskManager.setFileResult(jobId, fileId, filename, contentType);
log.debug("Stored ResponseEntity<Resource> result with fileId: {}", fileId);
} else {
// Check if the response body contains a fileId
if (body != null && body.toString().contains("fileId")) {
@@ -478,6 +478,21 @@ public class JobExecutorService {
}
}
private static String extractResponseFilename(ResponseEntity<?> response) {
if (response.getHeaders().getContentDisposition() != null) {
String filename = response.getHeaders().getContentDisposition().getFilename();
if (filename != null && !filename.isEmpty()) {
return filename;
}
}
return "result.pdf";
}
private static String extractResponseContentType(ResponseEntity<?> response) {
MediaType mediaType = response.getHeaders().getContentType();
return mediaType != null ? mediaType.toString() : MediaType.APPLICATION_PDF_VALUE;
}
/**
* Parse session timeout string (e.g., "30m", "1h") to milliseconds
*
@@ -44,8 +44,10 @@ public class JobQueue implements SmartLifecycle {
private volatile BlockingQueue<QueuedJob> jobQueue;
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("job-queue-scheduler-", 0).factory());
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualThreadExecutor();
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
private boolean shuttingDown = false;
@@ -399,7 +401,7 @@ public class JobQueue implements SmartLifecycle {
* @throws Exception If there is an execution error
*/
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs) throws Exception {
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier);
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier, jobExecutor);
try {
if (timeoutMs <= 0) {
@@ -129,7 +129,7 @@ public class MobileScannerService {
FILE_EXTENSION_PATTERN.matcher(safeFilename).replaceFirst("");
String ext =
safeFilename.contains(".")
? safeFilename.substring(safeFilename.lastIndexOf("."))
? safeFilename.substring(safeFilename.lastIndexOf('.'))
: "";
safeFilename = nameWithoutExt + "-" + counter + ext;
filePath = sessionDir.resolve(safeFilename).normalize().toAbsolutePath();
@@ -0,0 +1,157 @@
package stirling.software.common.service;
import java.util.Calendar;
import java.util.List;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationText;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.comments.AnnotationLocation;
import stirling.software.common.model.api.comments.StickyNoteSpec;
/**
* Shared primitive for adding sticky-note (PDF Text) annotations to a document.
*
* <p>Used by:
*
* <ul>
* <li>{@code /api/v1/misc/add-comments} — a deterministic, reusable tool.
* <li>AI-agent flows that generate comment specs (e.g. PDF review agent, math auditor review
* mode) and hand them off to this service for deterministic placement.
* </ul>
*/
@Slf4j
@Service
public class PdfAnnotationService {
/** Yellow sticky-note fill colour (R, G, B in 0..1 range). */
private static final float[] STICKY_NOTE_COLOR_RGB = {1f, 0.95f, 0.4f};
/** Opacity for the sticky-note icon. */
private static final float ANNOTATION_OPACITY = 0.9f;
/** PDF Text-annotation icon name — {@code "Comment"} is one of the standard icons. */
private static final String ANNOTATION_ICON_NAME = "Comment";
/** Default subject shown in the annotation popup when a spec does not supply one. */
private static final String DEFAULT_SUBJECT = "Stirling AI Comment";
/** Default author label shown in the annotation popup when a spec does not supply one. */
private static final String DEFAULT_AUTHOR = "Stirling AI";
/**
* Cap on sticky-note text length. PDF annotation bodies can technically be much longer, but
* anything beyond this is almost certainly pathological (accidental document-dump or malicious
* payload) and would bloat the output file.
*/
private static final int MAX_COMMENT_TEXT_LENGTH = 100_000;
/**
* Add a list of sticky notes to {@code doc}. Specs that reference an out-of-range page or
* contain blank text are logged and skipped; this method never throws for a single bad spec.
*
* @return the number of annotations actually applied
*/
public int addStickyNotes(PDDocument doc, List<StickyNoteSpec> specs) {
if (specs == null || specs.isEmpty()) {
return 0;
}
int totalPages = doc.getNumberOfPages();
Calendar now = Calendar.getInstance();
int applied = 0;
for (int i = 0; i < specs.size(); i++) {
StickyNoteSpec spec = specs.get(i);
if (!isValid(spec, totalPages, i)) {
continue;
}
apply(doc, spec, now);
applied++;
}
if (applied < specs.size()) {
log.warn(
"Applied {}/{} sticky notes; {} skipped due to invalid specs.",
applied,
specs.size(),
specs.size() - applied);
}
return applied;
}
/**
* Add a single sticky note. Convenience wrapper; prefer {@link #addStickyNotes(PDDocument,
* List)} when placing multiple annotations so log output is batched.
*/
public void addStickyNote(PDDocument doc, StickyNoteSpec spec) {
addStickyNotes(doc, List.of(spec));
}
private boolean isValid(StickyNoteSpec spec, int totalPages, int index) {
if (spec == null || spec.location() == null) {
log.warn("Skipping sticky-note[{}]: spec or location is null.", index);
return false;
}
if (spec.text() == null || spec.text().isBlank()) {
log.warn("Skipping sticky-note[{}]: text is blank.", index);
return false;
}
if (spec.text().length() > MAX_COMMENT_TEXT_LENGTH) {
log.warn(
"Skipping sticky-note[{}]: text length {} exceeds limit {}.",
index,
spec.text().length(),
MAX_COMMENT_TEXT_LENGTH);
return false;
}
AnnotationLocation loc = spec.location();
if (loc.width() <= 0f || loc.height() <= 0f) {
log.warn(
"Skipping sticky-note[{}]: non-positive dimensions width={} height={}.",
index,
loc.width(),
loc.height());
return false;
}
int page = loc.pageIndex();
if (page < 0 || page >= totalPages) {
log.warn(
"Skipping sticky-note[{}]: pageIndex={} out of range [0, {}).",
index,
page,
totalPages);
return false;
}
return true;
}
private void apply(PDDocument doc, StickyNoteSpec spec, Calendar now) {
AnnotationLocation loc = spec.location();
PDAnnotationText annot = new PDAnnotationText();
annot.setContents(spec.text());
annot.setRectangle(new PDRectangle(loc.x(), loc.y(), loc.width(), loc.height()));
annot.setSubject(nonBlankOr(spec.subject(), DEFAULT_SUBJECT));
annot.setTitlePopup(nonBlankOr(spec.author(), DEFAULT_AUTHOR));
annot.setColor(new PDColor(STICKY_NOTE_COLOR_RGB, PDDeviceRGB.INSTANCE));
annot.setCreationDate(now);
annot.setConstantOpacity(ANNOTATION_OPACITY);
annot.getCOSObject().setName(COSName.NAME, ANNOTATION_ICON_NAME);
try {
doc.getPage(loc.pageIndex()).getAnnotations().add(annot);
} catch (java.io.IOException e) {
log.warn(
"Failed to attach sticky note to page {}: {}", loc.pageIndex(), e.getMessage());
}
}
private static String nonBlankOr(String value, String fallback) {
return value != null && !value.isBlank() ? value : fallback;
}
}
@@ -169,7 +169,10 @@ public class PdfMetadataService {
.getAuthor();
if (userService != null) {
author = author.replace("username", userService.getCurrentUsername());
String username = userService.getCurrentUsername();
if (username != null) {
author = author.replace("username", username);
}
}
}
pdf.getDocumentInformation().setAuthor(author);
@@ -0,0 +1,37 @@
package stirling.software.common.service;
import java.security.KeyStore;
/**
* Abstraction for PDF digital signature operations. Defined in common so that proprietary services
* can use it without creating a circular dependency on core.
*/
public interface PdfSigningService {
/**
* Signs a PDF document using the provided KeyStore.
*
* @param pdfBytes raw PDF bytes to sign
* @param keystore the KeyStore containing the signing key and certificate chain
* @param password keystore password
* @param showSignature whether to render a visible signature block
* @param pageNumber 0-indexed page on which to render the visible signature (may be null)
* @param name signer name embedded in the signature
* @param location location string embedded in the signature
* @param reason reason string embedded in the signature
* @param showLogo whether to include the Stirling-PDF logo in the visible signature
* @return signed PDF bytes
* @throws Exception on any signing failure
*/
byte[] signWithKeystore(
byte[] pdfBytes,
KeyStore keystore,
char[] password,
boolean showSignature,
Integer pageNumber,
String name,
String location,
String reason,
boolean showLogo)
throws Exception;
}
@@ -7,11 +7,8 @@ import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.management.ThreadMXBean;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -94,21 +91,12 @@ public class PostHogService {
metrics.put("os_name", System.getProperty("os.name"));
metrics.put("os_version", System.getProperty("os.version"));
metrics.put("java_version", System.getProperty("java.version"));
metrics.put("user_name", System.getProperty("user.name"));
metrics.put("user_home", System.getProperty("user.home"));
metrics.put("user_dir", System.getProperty("user.dir"));
// CPU and Memory
metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors());
metrics.put("total_memory", Runtime.getRuntime().totalMemory());
metrics.put("free_memory", Runtime.getRuntime().freeMemory());
// Network and Server Identity
InetAddress localHost = InetAddress.getLocalHost();
metrics.put("ip_address", localHost.getHostAddress());
metrics.put("hostname", localHost.getHostName());
metrics.put("mac_address", getMacAddress());
// JVM info
metrics.put("jvm_vendor", System.getProperty("java.vendor"));
metrics.put("jvm_version", System.getProperty("java.vm.version"));
@@ -153,9 +141,6 @@ public class PostHogService {
metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime());
}
// Network interfaces
metrics.put("network_interfaces", getNetworkInterfacesInfo());
// Docker detection and stats
boolean isDocker = isRunningInDocker();
if (isDocker) {
@@ -353,30 +338,6 @@ public class PostHogService {
.getProFeatures()
.getCustomMetadata()
.isAutoUpdateMetadata());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_author",
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getAuthor());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_creator",
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getCreator());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_producer",
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getProducer());
}
// Capture AutoPipeline properties
addIfNotEmpty(
@@ -386,39 +347,4 @@ public class PostHogService {
return properties;
}
private String getMacAddress() {
try {
Enumeration<NetworkInterface> networkInterfaces =
NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface ni = networkInterfaces.nextElement();
byte[] hardwareAddress = ni.getHardwareAddress();
if (hardwareAddress != null) {
String[] hexadecimal = new String[hardwareAddress.length];
for (int i = 0; i < hardwareAddress.length; i++) {
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
}
return String.join("-", hexadecimal);
}
}
} catch (Exception e) {
// Handle exception
}
return "Unknown";
}
private Map<String, String> getNetworkInterfacesInfo() {
Map<String, String> interfacesInfo = new HashMap<>();
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface netint = nets.nextElement();
interfacesInfo.put(netint.getName(), netint.getDisplayName());
}
} catch (Exception e) {
interfacesInfo.put("error", e.getMessage());
}
return interfacesInfo;
}
}
@@ -43,7 +43,9 @@ public class ResourceMonitor {
@Value("${stirling.resource.monitor.interval-ms:60000}")
private long monitorIntervalMs = 60000; // 60 seconds
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("resource-monitor-", 0).factory());
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
@@ -226,10 +226,11 @@ public class SsrfProtectionService {
}
private boolean isPrivateIPv4Range(String ip) {
// Includes RFC1918, loopback, link-local, and unspecified addresses
// Includes RFC1918, RFC6598, loopback, link-local, and unspecified addresses
return ip.startsWith("10.")
|| ip.startsWith("192.168.")
|| (ip.startsWith("172.") && isInRange172(ip))
|| (ip.startsWith("100.") && isInRange100(ip))
|| ip.startsWith("169.254.")
|| ip.startsWith("127.")
|| "0.0.0.0".equals(ip);
@@ -247,6 +248,18 @@ public class SsrfProtectionService {
return false;
}
private boolean isInRange100(String ip) {
String[] parts = ip.split("\\.");
if (parts.length >= 2) {
try {
int secondOctet = Integer.parseInt(parts[1]);
return secondOctet >= 64 && secondOctet <= 127;
} catch (NumberFormatException e) {
}
}
return false;
}
private boolean isCloudMetadataAddress(String ip) {
String normalizedIp = normalizeIpv4MappedAddress(ip);
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
@@ -1,8 +1,8 @@
package stirling.software.common.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -19,7 +19,6 @@ import java.util.zip.ZipInputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
@@ -42,7 +41,8 @@ public class TaskManager {
private final FileStorage fileStorage;
private final ScheduledExecutorService cleanupExecutor =
Executors.newSingleThreadScheduledExecutor();
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("task-cleanup-", 0).factory());
/** Initialize the task manager and start the cleanup scheduler */
public TaskManager(FileStorage fileStorage) {
@@ -362,39 +362,29 @@ public class TaskManager {
String zipFileId, String originalZipFileName) throws IOException {
List<ResultFile> extractedFiles = new ArrayList<>();
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(zipFile.getBytes()))) {
try (InputStream fileStream = fileStorage.retrieveInputStream(zipFileId);
ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new BufferedInputStream(fileStream))) {
ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {
// Use buffered reading for memory safety
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = zipIn.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
byte[] fileContent = out.toByteArray();
String contentType = determineContentType(entry.getName());
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
// storeInputStream returns the fileId and byte count — no extra stat needed
FileStorage.StoredFile stored =
fileStorage.storeInputStream(zipIn, entry.getName());
ResultFile resultFile =
ResultFile.builder()
.fileId(individualFileId)
.fileId(stored.fileId())
.fileName(entry.getName())
.contentType(contentType)
.fileSize(fileContent.length)
.fileSize(stored.size())
.build();
extractedFiles.add(resultFile);
log.debug(
"Extracted file: {} (size: {} bytes)",
entry.getName(),
fileContent.length);
"Extracted file: {} (size: {} bytes)", entry.getName(), stored.size());
}
zipIn.closeEntry();
}
@@ -466,4 +456,24 @@ public class TaskManager {
}
return null;
}
/**
* Find the job key that owns a given file ID.
*
* @param fileId file identifier to look up
* @return scoped job key if found, otherwise null
*/
public String findJobKeyByFileId(String fileId) {
for (Map.Entry<String, JobResult> entry : jobResults.entrySet()) {
JobResult jobResult = entry.getValue();
if (jobResult.hasFiles()) {
for (ResultFile resultFile : jobResult.getAllResultFiles()) {
if (fileId.equals(resultFile.getFileId())) {
return entry.getKey();
}
}
}
}
return null;
}
}
@@ -0,0 +1,18 @@
package stirling.software.common.service;
/** Provides metadata about tool endpoints for internal dispatch. */
public interface ToolMetadataService {
/** Returns true if the given operation path accepts multiple input files. */
boolean isMultiInput(String operationPath);
/**
* 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 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);
}
@@ -5,6 +5,8 @@ public interface UserServiceInterface {
String getCurrentUsername();
String getCurrentUserApiKey();
long getTotalUsersCount();
boolean isCurrentUserAdmin();
@@ -4,6 +4,7 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
@@ -30,15 +31,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
@UtilityClass
public class CbzUtils {
public byte[] convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException {
return convertCbzToPdf(cbzFile, pdfDocumentFactory, tempFileManager, false);
}
public byte[] convertCbzToPdf(
public TempFile convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager,
@@ -64,70 +57,90 @@ public class CbzUtils {
try (PDDocument document = pdfDocumentFactory.createNewDocument();
ZipFile zipFile = new ZipFile(tempFile.getFile())) {
// Pass 1: collect sorted image names (cheap just strings, no image data)
List<String> sortedImageNames = new ArrayList<>();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
List<ImageEntryData> imageEntries = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && isImageFile(entry.getName())) {
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
is.transferTo(baos);
imageEntries.add(
new ImageEntryData(entry.getName(), baos.toByteArray()));
} catch (IOException e) {
log.warn("Error reading image {}: {}", entry.getName(), e.getMessage());
}
sortedImageNames.add(entry.getName());
}
}
sortedImageNames.sort(new NaturalOrderComparator());
imageEntries.sort(
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
if (imageEntries.isEmpty()) {
if (sortedImageNames.isEmpty()) {
throw ExceptionUtils.createCbzNoImagesException();
}
for (ImageEntryData imageEntry : imageEntries) {
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageEntry.data(), imageEntry.name());
PDPage page =
new PDPage(
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
// Pass 2: load ONE image at a time peak memory = max(single image)
for (String imageName : sortedImageNames) {
ZipEntry entry = zipFile.getEntry(imageName);
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream imgBaos = new ByteArrayOutputStream();
is.transferTo(imgBaos);
byte[] imageBytes = imgBaos.toByteArray();
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageBytes, imageName);
PDPage page =
new PDPage(
new PDRectangle(
pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
page,
PDPageContentStream.AppendMode.OVERWRITE,
true,
true)) {
contentStream.drawImage(pdImage, 0, 0);
}
} catch (IOException e) {
log.warn("Error processing image {}: {}", imageName, e.getMessage());
}
// imageBytes eligible for GC after each iteration
} catch (IOException e) {
log.warn(
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
log.warn("Error reading image {}: {}", imageName, e.getMessage());
}
}
if (document.getNumberOfPages() == 0) {
throw ExceptionUtils.createCbzCorruptedImagesException();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] pdfBytes = baos.toByteArray();
// Apply Ghostscript optimization if requested
if (optimizeForEbook) {
try {
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
} catch (IOException e) {
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
// Write to TempFile (not BAOS)
TempFile pdfTempFile = new TempFile(tempFileManager, ".pdf");
try {
document.save(pdfTempFile.getFile());
if (optimizeForEbook) {
try {
byte[] pdfBytes = Files.readAllBytes(pdfTempFile.getPath());
byte[] optimized = GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
pdfTempFile.close();
TempFile optimizedFile = new TempFile(tempFileManager, ".pdf");
try {
Files.write(optimizedFile.getPath(), optimized);
return optimizedFile;
} catch (Exception e) {
optimizedFile.close();
throw e;
}
} catch (IOException e) {
log.warn(
"Ghostscript optimization failed, returning unoptimized PDF",
e);
}
}
}
return pdfBytes;
return pdfTempFile;
} catch (Exception e) {
pdfTempFile.close();
throw e;
}
}
}
}
@@ -175,8 +188,6 @@ public class CbzUtils {
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
}
private record ImageEntryData(String name, byte[] data) {}
private class NaturalOrderComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
@@ -13,11 +13,18 @@ public class EmlToPdf {
public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request)
throws IOException {
return convertEmlToHtml(emlBytes, request, null);
}
public static String convertEmlToHtml(
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
throws IOException {
EmlProcessingUtils.validateEmlInput(emlBytes);
EmlParser.EmailContent emailContent =
EmlParser.extractEmailContent(emlBytes, request, null);
return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null);
EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer);
return EmlProcessingUtils.generateEnhancedEmailHtml(
emailContent, request, customHtmlSanitizer);
}
public static byte[] convertEmlToPdf(
@@ -2,30 +2,28 @@ package stirling.software.common.util;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import lombok.extern.slf4j.Slf4j;
/**
* Factory for creating executors backed by virtual threads (Java 21+). Virtual threads are
* lightweight, managed by the JVM, and ideal for I/O-bound tasks. They eliminate the need for
* thread pool sizing since thousands can run concurrently with minimal overhead.
*/
public final class ExecutorFactory {
@Slf4j
public class ExecutorFactory {
private ExecutorFactory() {}
/** Creates an {@link ExecutorService} that starts a new virtual thread for each task. */
public static ExecutorService newVirtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
/**
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
* cached thread pool on older Java versions.
* Creates a {@link ScheduledExecutorService} backed by a single virtual thread. Useful for
* periodic/delayed tasks that should not pin a platform thread.
*/
public static ExecutorService newVirtualOrCachedThreadExecutor() {
try {
ExecutorService executor =
(ExecutorService)
Executors.class
.getMethod("newVirtualThreadPerTaskExecutor")
.invoke(null);
return executor;
} catch (NoSuchMethodException e) {
log.debug("Virtual threads not available; falling back to cached thread pool.");
} catch (Exception e) {
log.debug("Error initializing virtual thread executor: {}", e.getMessage(), e);
}
return Executors.newCachedThreadPool();
public static ScheduledExecutorService newSingleVirtualThreadScheduledExecutor() {
return Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("scheduled-vt-", 0).factory());
}
}
@@ -68,6 +68,17 @@ public class FileMonitor {
if (this.rootDirs.isEmpty()) {
log.error("No valid directories to monitor - FileMonitor will not function");
} else {
// Register directories eagerly so the first @Scheduled tick does not warn.
for (Path rootDir : this.rootDirs) {
if (Files.exists(rootDir)) {
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("Failed to register monitoring for {}", rootDir, e);
}
}
}
}
}
@@ -101,8 +112,6 @@ public class FileMonitor {
All files observed changes in the last iteration will be considered as staging files.
If those files are not modified in current iteration, they will be considered as ready for processing.
*/
stagingFiles = new HashSet<>(newlyDiscoveredFiles);
readyForProcessingFiles.clear();
if (path2KeyMapping.isEmpty()) {
log.warn("Not monitoring any directories; attempting to re-register root paths.");
@@ -118,8 +127,19 @@ public class FileMonitor {
}
}
WatchKey key;
while ((key = watchService.poll()) != null) {
// Skip expensive collection work when there is nothing to track
WatchKey firstKey = watchService.poll();
if (firstKey == null
&& newlyDiscoveredFiles.isEmpty()
&& readyForProcessingFiles.isEmpty()) {
return;
}
stagingFiles = new HashSet<>(newlyDiscoveredFiles);
readyForProcessingFiles.clear();
WatchKey key = firstKey;
while (key != null) {
final Path watchingDir = (Path) key.watchable();
key.pollEvents()
.forEach(
@@ -156,6 +176,7 @@ public class FileMonitor {
if (!isKeyValid) { // key is invalid when the directory itself is no longer exists
path2KeyMapping.remove((Path) key.watchable());
}
key = watchService.poll();
}
readyForProcessingFiles.addAll(stagingFiles);
}
@@ -0,0 +1,217 @@
package stirling.software.common.util;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.AutoPipeline.FileReadiness;
/**
* Stateless safety checker that decides whether a file is stable and ready for pipeline processing.
* Call {@link #isReady(Path)} before moving or processing any file picked up from a watched folder.
*
* <p>A file is considered ready when ALL of the following hold:
*
* <ol>
* <li>The file exists on disk.
* <li>The path refers to a regular file, not a directory.
* <li>The file's extension matches the configured allow-list (if one is set).
* <li>The file has not been modified within the configured settle window ({@code
* settleTimeMillis}), meaning it is no longer being written.
* <li>The file size is stable: two reads separated by {@code sizeCheckDelayMillis} return the
* same value. This catches active copies on Linux/macOS where advisory file locking alone
* cannot detect a mid-copy file.
* <li>An exclusive file-system lock can be acquired, confirming no other process holds it.
* </ol>
*
* <p>All behaviour is controlled through {@link FileReadiness} inside {@link
* ApplicationProperties.AutoPipeline}. Setting {@code enabled: false} makes every call return
* {@code true} so the checker is a no-op drop-in.
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class FileReadinessChecker {
private final ApplicationProperties applicationProperties;
/**
* Returns {@code true} when the file at {@code path} passes every readiness check and is safe
* to hand off to the pipeline for processing. Returns {@code false} when any check fails; the
* caller should skip the file and retry on the next scan cycle.
*/
public boolean isReady(Path path) {
FileReadiness config = applicationProperties.getAutoPipeline().getFileReadiness();
if (!config.isEnabled()) {
return true;
}
if (!existsAsRegularFile(path)) {
return false;
}
if (!isExtensionAllowed(path, config.getAllowedExtensions())) {
return false;
}
if (!hasSettled(path, config.getSettleTimeMillis())) {
return false;
}
if (!hasSizeStabilized(path, config.getSizeCheckDelayMillis())) {
return false;
}
if (isLocked(path)) {
return false;
}
return true;
}
// -------------------------------------------------------------------------
// Individual checks
// -------------------------------------------------------------------------
private boolean existsAsRegularFile(Path path) {
if (!Files.exists(path)) {
log.debug("File does not exist, skipping: {}", path);
return false;
}
if (!Files.isRegularFile(path)) {
log.debug("Path is not a regular file (directory or symlink?), skipping: {}", path);
return false;
}
return true;
}
/**
* Returns {@code true} when {@code allowedExtensions} is empty (no filter) or when the file's
* extension (case-insensitive) appears in the list.
*/
private boolean isExtensionAllowed(Path path, List<String> allowedExtensions) {
if (allowedExtensions == null || allowedExtensions.isEmpty()) {
return true;
}
String filename = path.getFileName().toString();
String extension =
filename.contains(".")
? filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT)
: "";
boolean allowed =
allowedExtensions.stream().anyMatch(ext -> ext.equalsIgnoreCase(extension));
if (!allowed) {
log.debug(
"File '{}' has extension '{}' which is not in the allowed list {}, skipping",
filename,
extension,
allowedExtensions);
}
return allowed;
}
/**
* Returns {@code true} when the file's last-modified timestamp is at least {@code
* settleTimeMillis} milliseconds in the past, indicating the write has completed and the file
* has "settled".
*/
private boolean hasSettled(Path path, long settleTimeMillis) {
try {
long lastModified = Files.getLastModifiedTime(path).toMillis();
long ageMillis = System.currentTimeMillis() - lastModified;
boolean settled = ageMillis >= settleTimeMillis;
if (!settled) {
log.debug(
"File '{}' was modified {}ms ago (settle threshold: {}ms), not yet ready",
path.getFileName(),
ageMillis,
settleTimeMillis);
}
return settled;
} catch (IOException e) {
log.warn(
"Could not read last-modified time for '{}', treating as not settled: {}",
path,
e.getMessage());
return false;
}
}
/**
* Returns {@code true} when the file size is the same before and after a short pause of {@code
* sizeCheckDelayMillis} milliseconds. A size change indicates another process is still
* appending to the file. This is the primary write-detection mechanism on Linux/macOS, where
* mandatory file locking is not enforced by the OS.
*/
private boolean hasSizeStabilized(Path path, long sizeCheckDelayMillis) {
try {
long sizeBefore = Files.size(path);
Thread.sleep(sizeCheckDelayMillis);
long sizeAfter = Files.size(path);
boolean stable = sizeBefore == sizeAfter;
if (!stable) {
log.debug(
"File '{}' size changed from {} to {} bytes during stability check,"
+ " not yet ready",
path.getFileName(),
sizeBefore,
sizeAfter);
}
return stable;
} catch (IOException e) {
log.warn(
"Could not read file size for '{}', treating as unstable: {}",
path,
e.getMessage());
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn(
"Size stability check interrupted for '{}', treating as unstable",
path.getFileName());
return false;
}
}
/**
* Returns {@code true} when an exclusive file-system lock cannot be acquired, which indicates
* another process still holds the file open for writing.
*
* <p>{@link OverlappingFileLockException} is also treated as locked: the JVM already holds a
* lock on this file (e.g. from another thread), so it is unsafe to process.
*/
private boolean isLocked(Path path) {
try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw");
FileChannel channel = raf.getChannel()) {
FileLock lock = channel.tryLock();
if (lock == null) {
log.debug("File '{}' is locked by another process", path.getFileName());
return true;
}
lock.release();
return false;
} catch (OverlappingFileLockException e) {
log.debug("File '{}' is already locked by this JVM", path.getFileName());
return true;
} catch (IOException e) {
log.debug(
"Could not acquire lock on '{}', treating as locked: {}",
path.getFileName(),
e.getMessage());
return true;
}
}
}

Some files were not shown because too many files have changed in this diff Show More