name: Backend Tests on: push: branches-ignore: - 'translations**' pull_request: branches-ignore: - 'translations**' workflow_dispatch: concurrency: group: backend-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: DEFAULT_UV_VERSION: "0.10.x" NLTK_DATA: "/usr/share/nltk_data" permissions: {} jobs: changes: name: Detect Backend Changes runs-on: ubuntu-slim permissions: contents: read outputs: backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Decide run mode id: force env: EVENT_NAME: ${{ github.event_name }} REF_NAME: ${{ github.ref_name }} run: | if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then echo "run_all=true" >> "$GITHUB_OUTPUT" elif [[ "${EVENT_NAME}" == "push" && ( "${REF_NAME}" == "main" || "${REF_NAME}" == "dev" ) ]]; then echo "run_all=true" >> "$GITHUB_OUTPUT" else echo "run_all=false" >> "$GITHUB_OUTPUT" fi - name: Set diff range id: range if: steps.force.outputs.run_all != 'true' env: BEFORE_SHA: ${{ github.event.before }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} EVENT_CREATED: ${{ github.event.created }} EVENT_NAME: ${{ github.event_name }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} SHA: ${{ github.sha }} run: | if [[ "${EVENT_NAME}" == "pull_request" ]]; then echo "base=${PR_BASE_SHA}" >> "$GITHUB_OUTPUT" elif [[ "${EVENT_CREATED}" == "true" ]]; then echo "base=${DEFAULT_BRANCH}" >> "$GITHUB_OUTPUT" else echo "base=${BEFORE_SHA}" >> "$GITHUB_OUTPUT" fi echo "ref=${SHA}" >> "$GITHUB_OUTPUT" - name: Detect changes id: filter if: steps.force.outputs.run_all != 'true' uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 with: base: ${{ steps.range.outputs.base }} ref: ${{ steps.range.outputs.ref }} filters: | backend: - 'src/**' - 'pyproject.toml' - 'uv.lock' - 'docker/compose/docker-compose.ci-test.yml' - '.github/workflows/ci-backend.yml' test: needs: changes if: needs.changes.outputs.backend_changed == 'true' name: "Python ${{ matrix.python-version }}" runs-on: ubuntu-24.04 permissions: contents: read strategy: matrix: python-version: ['3.11', '3.12', '3.13', '3.14'] fail-fast: false steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Start containers run: | docker compose --file docker/compose/docker-compose.ci-test.yml pull --quiet docker compose --file docker/compose/docker-compose.ci-test.yml up --detach - name: Set up Python id: setup-python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "${{ matrix.python-version }}" - name: Install uv uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: version: ${{ env.DEFAULT_UV_VERSION }} enable-cache: true python-version: ${{ steps.setup-python.outputs.python-version }} - name: Install system dependencies run: | sudo apt-get update -qq sudo apt-get install -qq --no-install-recommends \ unpaper tesseract-ocr imagemagick ghostscript poppler-utils - name: Configure ImageMagick run: | sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml - name: Install Python dependencies env: PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} run: | uv sync \ --python "${PYTHON_VERSION}" \ --group testing \ --frozen - name: List installed Python dependencies run: | uv pip list - name: Install NLTK data run: | uv run python -m nltk.downloader punkt punkt_tab snowball_data stopwords -d "${NLTK_DATA}" - name: Run tests env: NLTK_DATA: ${{ env.NLTK_DATA }} PAPERLESS_CI_TEST: 1 PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} run: | uv run \ --python "${PYTHON_VERSION}" \ --dev \ --frozen \ pytest - name: Upload test results to Codecov if: always() uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: backend-python-${{ matrix.python-version }} files: junit.xml report_type: test_results - name: Upload coverage to Codecov uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: backend-python-${{ matrix.python-version }} files: coverage.xml report_type: coverage - name: Stop containers if: always() run: | docker compose --file docker/compose/docker-compose.ci-test.yml logs docker compose --file docker/compose/docker-compose.ci-test.yml down typing: needs: changes if: needs.changes.outputs.backend_changed == 'true' name: Check project typing runs-on: ubuntu-24.04 permissions: contents: read env: DEFAULT_PYTHON: "3.12" PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret" steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python id: setup-python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "${{ env.DEFAULT_PYTHON }}" - name: Install uv uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: version: ${{ env.DEFAULT_UV_VERSION }} enable-cache: true python-version: ${{ steps.setup-python.outputs.python-version }} - name: Install Python dependencies env: PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} run: | uv sync \ --python "${PYTHON_VERSION}" \ --group testing \ --group typing \ --frozen - name: List installed Python dependencies run: | uv pip list - name: Check typing (pyrefly) continue-on-error: true run: | uv run pyrefly \ check \ src/ - name: Cache Mypy uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .mypy_cache # Keyed by OS, Python version, and dependency hashes key: ${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-${{ hashFiles('pyproject.toml', 'uv.lock') }} restore-keys: | ${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}- ${{ runner.os }}-mypy- - name: Check typing (mypy) continue-on-error: true run: | uv run mypy \ --show-error-codes \ --warn-unused-configs \ src/ | uv run mypy-baseline filter gate: name: Backend CI Gate needs: [changes, test, typing] if: always() runs-on: ubuntu-slim steps: - name: Check gate env: BACKEND_CHANGED: ${{ needs.changes.outputs.backend_changed }} TEST_RESULT: ${{ needs.test.result }} TYPING_RESULT: ${{ needs.typing.result }} run: | if [[ "${BACKEND_CHANGED}" != "true" ]]; then echo "No backend-relevant changes detected." exit 0 fi if [[ "${TEST_RESULT}" != "success" ]]; then echo "::error::Backend test job result: ${TEST_RESULT}" exit 1 fi if [[ "${TYPING_RESULT}" != "success" ]]; then echo "::error::Backend typing job result: ${TYPING_RESULT}" exit 1 fi echo "Backend checks passed."