chore(ci): migrate Python tooling to uv and standardize workflow execution (#7386)

# Description of Changes

This PR modernizes the project's Python tooling across GitHub Actions by
migrating CI workflows from pip-based dependency management to `uv` and
aligning Python execution with the engine project's managed environment.

### What was changed

- Replaced `actions/setup-python` and ad-hoc `pip install` steps with
`astral-sh/setup-uv` across CI workflows.
- Configured shared `uv` dependency caching using
`engine/pyproject.toml` and `engine/uv.lock`.
- Updated Python script execution to use `uv run --project engine
--locked` for a consistent runtime environment.
- Replaced package installation steps with `uv sync` for the required
dependency groups (e.g. `tools` and `cucumber`).
- Added Docker image build validation for both production and
development AI engine images.
- Updated workflow cache configuration and Docker build context where
required.
- Removed obsolete Python requirements files that are no longer needed
after the migration.
- Applied minor Python code modernizations, including import cleanup,
modern built-in generic type annotations (`list[...]`, `tuple[...]`,
`float | None`), and small style improvements.
- Removed unnecessary Python formatter/linter extensions from the
development container configuration.

### Why the change was made

- Standardize Python dependency management across the repository.
- Reduce duplicated dependency installation logic in CI.
- Improve workflow performance through shared dependency caching.
- Ensure all Python utilities execute against the same locked dependency
set managed by the engine project.
- Simplify long-term maintenance by eliminating legacy requirements
files and pip-specific workflow steps.


---

## 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.

---------

Signed-off-by: Carsten Drewes <c.drewes@stud.uni-hannover.de>
Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com>
Co-authored-by: kastenherri <116314318+kastenherri@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
Ludy
2026-08-11 08:09:21 +00:00
committed by GitHub
co-authored by albanobattistella kastenherri Anthony Stirling Copilot James Brunton
parent bbaff8d6c4
commit 05eb74022a
64 changed files with 1684 additions and 3258 deletions
@@ -264,7 +264,7 @@ jobs:
if: needs.check-comment.outputs.enable_prototypes == 'true'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: ./engine
context: .
file: ./engine/Dockerfile
push: true
cache-from: type=gha,scope=stirling-pdf-engine
+11
View File
@@ -31,6 +31,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
cache-suffix: ai-engine
- name: Install Task
@@ -96,6 +99,14 @@ jobs:
echo "============================================"
exit 1
- name: Build engine production image
if: always()
run: docker build --file engine/Dockerfile --tag stirling-pdf-engine:ci .
- name: Build engine development image
if: always()
run: docker build --file engine/Dockerfile.dev --tag stirling-pdf-engine-dev:ci .
- name: Remove engine check comment on success
if: steps.engine-check.outcome == 'success' && github.event_name == 'pull_request'
continue-on-error: true
+8 -6
View File
@@ -192,12 +192,14 @@ jobs:
retention-days: 3
if-no-files-found: warn
- name: Install defusedxml for coverage summary
# coverage-summary.py parses JaCoCo XML through defusedxml to
# silence security scanners that pattern-match on the stdlib
# xml.etree.ElementTree.parse call.
- name: Install uv
if: always() && matrix.flavor == 'saas'
run: python -m pip install --quiet defusedxml
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: JaCoCo coverage step summary
# Only the saas leg posts the JUnit summary - it's a strict
@@ -206,7 +208,7 @@ jobs:
# near-identical tables crowding out the aggregate report.
if: always() && matrix.flavor == 'saas'
run: |
python scripts/coverage-summary.py \
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Backend JUnit coverage (JDK ${{ matrix.jdk-version }})" \
--jacoco "common=app/common/build/reports/jacoco/test/jacocoTestReport.xml" \
--jacoco "core=app/core/build/reports/jacoco/test/jacocoTestReport.xml" \
+9 -8
View File
@@ -329,15 +329,16 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: ./testing/cucumber/requirements.txt
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: Install behave test deps
run: |
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
uv sync --project engine --locked --group cucumber
- name: Build the multi-node image
working-directory: testing/compose
run: docker compose -f "$MN_COMPOSE" build
@@ -360,10 +361,10 @@ jobs:
- name: Run multi-node regression (implemented guarantees)
working-directory: testing/cucumber
# -e overrides behave.ini's exclusion of features/multinode; ~@known_gap skips any tracked-gap scenarios.
run: python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain
run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain
- name: Run multi-node failover (destructive)
working-directory: testing/cucumber
run: python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain
run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain
- name: Dump node logs on failure
if: failure()
working-directory: testing/compose
@@ -34,6 +34,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
cache-suffix: generated-models
- name: Set up JDK 25
+8 -5
View File
@@ -195,20 +195,23 @@ jobs:
console.log(`Reference file path: ${referenceFilePath}`);
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: Install Python dependencies
run: |
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
uv sync --project engine --locked --group tools
- name: Run Python script to check files
id: run-check
run: |
echo "Running Python script to check TOML files..."
python .github/scripts/check_language_toml.py \
uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py \
--actor ${{ github.event.pull_request.user.login }} \
--reference-file "${REFERENCE_FILE}" \
--branch "pr-branch" \
+9 -11
View File
@@ -57,15 +57,13 @@ jobs:
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
gradle-${{ runner.os }}-${{ runner.arch }}-
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- name: Install defusedxml for coverage scripts
# Both coverage-summary.py and coverage-matrix.py parse JaCoCo
# XML through defusedxml - see the script headers for context.
run: python -m pip install --quiet defusedxml
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
# Pattern matches every artifact this PR's producers might upload:
# jacoco-exec-junit-jdk-25 (uploaded only by the saas
@@ -158,7 +156,7 @@ jobs:
# ("how much of the backend do real user flows cover?").
if: steps.inventory.outputs.found_e2e == 'true'
run: |
python scripts/coverage-summary.py \
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Real user-flow backend coverage (e2e:live + cucumber)" \
--jacoco "merged=build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml" \
--github-step-summary
@@ -169,7 +167,7 @@ jobs:
# is meaningless when one is a strict superset of the other.
if: steps.inventory.outputs.found_all == 'true'
run: |
python scripts/coverage-summary.py \
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Combined backend coverage (JUnit + e2e:live + cucumber)" \
--jacoco "merged=build/reports/jacoco/aggregate-all/jacocoTestReport.xml" \
--github-step-summary
@@ -224,7 +222,7 @@ jobs:
# generated above) plus whichever frontend artifacts landed.
# Every input is optional; missing ones render as "-".
run: |
python scripts/coverage-matrix.py \
uv run --project engine --locked --group tools python scripts/coverage-matrix.py \
${{ steps.inventory.outputs.found_all == 'true' && '--jacoco-all build/reports/jacoco/aggregate-all/jacocoTestReport.xml' || '' }} \
${{ steps.inventory.outputs.found_e2e == 'true' && '--jacoco-e2e build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml' || '' }} \
--vitest matrix-inputs/vitest/coverage-summary.json \
+9 -14
View File
@@ -69,16 +69,17 @@ jobs:
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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
cache-dependency-path: ./testing/cucumber/requirements.txt
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: Pip requirements
- name: Install Cucumber and coverage dependencies
run: |
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
uv sync --project engine --locked --group cucumber --group tools
- name: Extract JaCoCo agent for cucumber coverage
# Stages build/jacoco/jacocoagent.jar where the coverage override
@@ -121,16 +122,10 @@ jobs:
echo "report=false" >> "$GITHUB_OUTPUT"
fi
- name: Install defusedxml for coverage summary
# coverage-summary.py parses JaCoCo XML through defusedxml -
# see the script header for context.
if: always() && steps.cucumber-coverage.outputs.report == 'true'
run: python -m pip install --quiet defusedxml
- name: Cucumber coverage step summary
if: always() && steps.cucumber-coverage.outputs.report == 'true'
run: |
python scripts/coverage-summary.py \
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Cucumber (docker) JaCoCo coverage" \
--jacoco "cucumber=build/reports/jacoco/cucumber/jacocoTestReport.xml" \
--github-step-summary
+13 -32
View File
@@ -119,20 +119,18 @@ jobs:
echo "::warning::No e2e:live .exec found at .test-state/playwright/jacoco.exec; skipping report"
echo "report=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Python for coverage summary
if: always() && steps.live-coverage.outputs.report == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install uv
if: always()
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- name: Install defusedxml for coverage summary
# coverage-summary.py uses defusedxml instead of stdlib xml.etree
# to dodge XXE / billion-laughs scanner findings.
if: always() && steps.live-coverage.outputs.report == 'true'
run: python -m pip install --quiet defusedxml
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: e2e:live coverage step summary
if: always() && steps.live-coverage.outputs.report == 'true'
run: |
python scripts/coverage-summary.py \
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Playwright (live backend) JaCoCo coverage" \
--jacoco "e2e-live=build/reports/jacoco/e2e-live/jacocoTestReport.xml" \
--github-step-summary
@@ -155,23 +153,6 @@ jobs:
retention-days: 7
if-no-files-found: warn
- name: Set up Python for frontend coverage summary
# Separate from the backend-coverage python step because the
# frontend path doesn't depend on a JaCoCo report - it produces
# a summary even on backend failure, as long as some Playwright
# tests ran far enough to dump V8 coverage.
if: always()
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install defusedxml for frontend coverage summary
# Idempotent re-install: the backend-coverage step may have
# installed it already, but this leg can run on its own when the
# backend report step skips (e.g. .exec missing).
if: always()
run: python -m pip install --quiet defusedxml
- name: Aggregate Playwright frontend (V8) coverage
# Rolls per-test V8 dumps from the test-base fixture into one
# vitest-shaped coverage-summary.json. Tolerates a missing dump
@@ -182,7 +163,7 @@ jobs:
run: |
if [ -d .test-state/playwright/coverage-pw ] && \
find .test-state/playwright/coverage-pw -name '*.json' -type f | grep -q .; then
python scripts/playwright-coverage-summary.py \
uv run --project engine --locked --group tools python scripts/playwright-coverage-summary.py \
.test-state/playwright/coverage-pw \
--out .test-state/playwright/coverage-pw-summary/coverage-summary.json
echo "summary=true" >> "$GITHUB_OUTPUT"
@@ -194,10 +175,10 @@ jobs:
- name: Playwright frontend coverage step summary
if: always() && steps.pw-frontend-coverage.outputs.summary == 'true'
run: |
python scripts/coverage-summary.py \
--title "Playwright (live) frontend coverage" \
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
--github-step-summary
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Playwright (live) frontend coverage" \
--vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \
--github-step-summary
- name: Upload Playwright frontend coverage
# Bundle both the aggregated summary and the raw V8 dumps so
+7 -9
View File
@@ -115,20 +115,18 @@ jobs:
id: frontend-coverage
continue-on-error: true
run: task frontend:test:coverage
- name: Set up Python for coverage summary
- name: Install uv
if: always()
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
python-version: "3.12"
- name: Install defusedxml for coverage summary
# See coverage-summary.py header - it parses XML through defusedxml
# to dodge the stdlib parser's exposure to XXE / billion-laughs.
if: always()
run: python -m pip install --quiet defusedxml
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: Vitest coverage step summary
if: always()
run: |
python scripts/coverage-summary.py \
uv run --project engine --locked --group tools python scripts/coverage-summary.py \
--title "Frontend Vitest coverage" \
--vitest frontend/editor/coverage/coverage-summary.json \
--github-step-summary
+8 -2
View File
@@ -887,10 +887,16 @@ jobs:
# Gate publish on valid updater sigs. Runs after the review upload (so
# artifacts survive for debugging) and before action-gh-release.
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
- name: Verify updater signatures
run: |
python3 -m pip install --quiet 'cryptography==44.0.0'
python3 .github/scripts/verify-updater-signatures.py \
uv run --project engine --locked --only-group updater-signatures python .github/scripts/verify-updater-signatures.py \
./artifacts/tauri frontend/editor/src-tauri/tauri.conf.json
# workflow_dispatch path requires platform=='all' so a single-platform
+3
View File
@@ -28,6 +28,9 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
cache-suffix: pre-commit
- name: Install Task
+9 -12
View File
@@ -51,28 +51,25 @@ jobs:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: "pip" # caching pip dependencies
- name: Install Python dependencies
run: |
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-dependency-glob: |
engine/pyproject.toml
engine/uv.lock
cache-suffix: sync-files
- name: Install Python dependencies
run: |
uv sync --project engine --locked --group tools
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
- name: Sync translation TOML files
run: |
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
- name: Sort translation TOML files
run: |
@@ -85,7 +82,7 @@ jobs:
- name: Sync README.md
run: |
python scripts/counter_translation_v3.py
uv run --project engine --locked --group tools python scripts/counter_translation_v3.py
- name: Run git add
run: |