CI/CD Pipeline
This guide covers the continuous integration workflows under .forgejo/workflows/. Each
scope (backend, frontend, android, web, cli) has its own path-filtered ci-*.yml file, so a
change to one scope only runs that scope’s checks.
ci-security.yml is unscoped and always runs, since a leaked secret or a vulnerable dependency can land in any scope.
Release workflows (backend.yml, frontend.yml, web.yml, android.yml) reuse
these same ci-*.yml files as their pre-publish gate — see Releases.
Trigger and Concurrency
Section titled “Trigger and Concurrency”| Property | Value |
|---|---|
| Triggers | push to main, pull_request targeting main — path-filtered per scope (ci-security.yml is unscoped) |
| Runner | Self-hosted rocky (ships Node.js 24 natively — no per-job install step needed), except ci-android.yml which runs on ubuntu for the Android SDK |
| Concurrency group | ci-<scope>-${{ github.ref }}, one per workflow file |
| Cancel in progress | true — a new push to the same ref cancels the previous run |
ci-backend.yml
Section titled “ci-backend.yml”Path-filtered to backend/**, frontend/container/nginx.conf.template (pinned by tests/test_security_headers.py), android/app/src/main/java/com/patrimoine/app/data/remote/ApiErrorCodes.kt (tests/test_android_error_contract.py) and the workflow file. Four jobs: lint, test (three shards, matrix shard: [1, 2, 3], each running the test files assigned to it, no coverage), coverage (whole suite with --cov=app, pushes to main only, non-blocking, failures announced through ntfy; the fail_under gate in backend/pyproject.toml applies here) and schema (OpenAPI check and migration round-trip), each with its own checkout and dependency install.
| Step | Command |
|---|---|
| Install uv | pip install uv |
| Install dependencies | uv sync --frozen --python 3.13 --extra dev |
| Lint | uv run ruff check . |
| Test | TMPDIR=/dev/shm uv run pytest $files -v -rfE -n 4 on the shard’s test files — four workers (not one per core) so a loaded runner does not lose one; no coverage |
Coverage (coverage job) |
uv run pytest tests/ -q -n 4 --cov=app --cov-report=term on the whole suite |
| OpenAPI schema check | uv run python scripts/export_openapi.py --check |
| Migration test (upgrade) | uv run alembic upgrade head against a scratch sqlite+aiosqlite DB |
| Migration test (downgrade) | uv run alembic downgrade base against the same scratch DB |
The migration round-trip test is a CI-only sanity check — it does not touch the real database and is not the same operation as a production rollback (see Updating a Deployment).
ci-frontend.yml
Section titled “ci-frontend.yml”Path-filtered to frontend/**. Five jobs run in parallel: static (lint and type check), test-1, test-2 and test-3 (the vitest suite split with --shard=N/3) and build (build, size limit and bundle checks). The shards are explicit jobs rather than a matrix, and run without coverage; ci-local.sh runs the whole suite with coverage.
| Step | Command |
|---|---|
| Install dependencies | npm ci |
| Generate version metadata | node scripts/generate-version.mjs |
| Lint | oxlint |
| Type check | tsc -b --noEmit |
| Test | vitest run --shard=N/3 — one third of the suite per job |
| Build | npm run build |
| Bundle size check | size-limit |
| Bundle checks | node scripts/verify-bundle.mjs dist — no PDF or non-default language chunk in the service worker precache, no dnd-kit preload on the entry page, the language preload map present, and the gzip size of the eagerly loaded login files within budget |
ci-android.yml
Section titled “ci-android.yml”Path-filtered to android/** and backend/openapi.json (the schema the DTO contract test reads). Runs on ubuntu for the Android SDK.
| Step | Command |
|---|---|
| Set up JDK 25 + Android SDK | actions/setup-java, android-actions/setup-android |
| Release script tests | bash android/scripts/release.test.sh && bash android/scripts/release-notes.test.sh |
| Lint | ./gradlew :app:lintDebug |
| Test | ./gradlew :app:testDebugUnitTest — includes the OpenAPI contract test |
| Build | ./gradlew :app:assembleDebug — a debug APK, never signed or published |
ci-web.yml
Section titled “ci-web.yml”Path-filtered to web/documentation/**, web/container/** (the nginx config and entrypoint the site is served with), backend/openapi.json (the API reference source) and frontend/src/locales/** (the bold-UI-label parity check). A single docs job on the one Astro project.
| Job | Steps |
|---|---|
docs (web/documentation) |
install, npm test (the node --test suites for the nginx redirects, the locale tooling and the build output checks), verify:locale-parity, check (astro check), build, verify:build, verify:no-leakage |
ci-cli.yml
Section titled “ci-cli.yml”Path-filtered to cli/**, backend/openapi.json and the workflow file itself. The schema is in the filter because cli/tests/test_contract.py checks every endpoint the command-line client calls against it, so an API change that breaks the client fails here. A single check job. There is no release workflow: the client is installed from a checkout with uv tool install ./cli.
| Step | Command |
|---|---|
| Install uv | pip install uv |
| Install dependencies | cd cli && uv sync --frozen --python 3.13 --extra dev |
| Lint | cd cli && uv run ruff check . |
| Test | cd cli && uv run pytest -q |
ci-security.yml
Section titled “ci-security.yml”Unscoped — no path filter, runs on every push to main and every pull request targeting main.
| Job | Step | Command |
|---|---|---|
security |
Backend audit | pip-audit --strict (against the locked backend dependency set, dev extra included; a known vulnerability or a package that cannot be audited fails the job) |
security |
Frontend audit | npm audit --omit=dev |
security |
Secret scan | gitleaks detect --source . --no-banner (full git history); the gitleaks download is verified against a pinned SHA-256 |
stale-deps (PR only) |
Backend outdated check | uv lock --upgrade --dry-run — warns (non-blocking ::warning::) if updates are available |
stale-deps (PR only) |
Frontend outdated check | npm outdated --omit=dev — non-blocking (|| true) |