name: Reusable Python Checks on: workflow_call: inputs: python_version: type: string default: "3.14" source_path: type: string default: "app" tests_path: type: string default: "tests" test_command: type: string default: "coverage run -m pytest" coverage_fail_under: type: string default: "80" run_security_scan: type: boolean default: true use_private_index: # Gitea 1.27.x does not reliably propagate boolean inputs through # reusable workflows. Keep this a string until that regression is fixed. type: string default: "false" jobs: check: runs-on: docker container: image: gitea.tech-buddy.at/bitbuddydev/gitea_runner_python314:1.1.0 credentials: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} steps: - name: Checkout uses: actions/checkout@v4 # Keep this step unconditional. A Gitea Actions update can otherwise silently # skip it when a boolean workflow_call input is not propagated as expected; # pip then misleadingly reports that the private package does not exist. - name: Configure and verify private package index env: PRIVATE_INDEX_ENABLED: ${{ inputs.use_private_index }} PRIVATE_INDEX_URL: ${{ secrets.PIP_EXTRA_INDEX_URL }} run: | echo "Private package index requested: ${PRIVATE_INDEX_ENABLED:-}" if [ "${PRIVATE_INDEX_ENABLED}" != "true" ]; then echo "Private package index is not requested for this workflow call." exit 0 fi if [ -z "$PRIVATE_INDEX_URL" ]; then echo "use_private_index=true but the PIP_EXTRA_INDEX_URL secret is empty or not set" exit 1 fi # Probe the configured simple index before pip resolves dependencies. The # probe deliberately prints only scheme/host/path, never URL credentials. # NOTE: Gitea's PyPI package registry does not serve a browsable listing at # the bare /simple/ root (only /simple// resolves per PEP 503), so a # 404 against the configured URL is expected there and is not itself a fault. # This probe therefore only fails hard on auth rejection (401/403) or an # unreachable/erroring host; a plain 404 is logged and deferred to pip's own # resolution, which is the real signal for whether the package is installable. python - "$PRIVATE_INDEX_URL" <<'PY' import base64 import sys from urllib.error import HTTPError, URLError from urllib.parse import unquote, urlsplit, urlunsplit from urllib.request import Request, urlopen configured_url = sys.argv[1] parsed = urlsplit(configured_url) hostname = parsed.hostname or "" port = f":{parsed.port}" if parsed.port else "" safe_url = urlunsplit((parsed.scheme, f"{hostname}{port}", parsed.path, parsed.query, "")) request = Request(safe_url, headers={"Accept": "application/vnd.pypi.simple.v1+json"}) if parsed.username is not None: credentials = f"{unquote(parsed.username)}:{unquote(parsed.password or '')}".encode() request.add_header("Authorization", "Basic " + base64.b64encode(credentials).decode()) try: with urlopen(request, timeout=15) as response: # nosec B310 -- configured CI package index print(f"Private package-index probe: HTTP {response.status} at {safe_url}") except HTTPError as error: print(f"Private package-index probe: HTTP {error.code} at {safe_url}") if error.code in {401, 403}: print("Authentication was rejected by the private package index.") raise SystemExit(1) if error.code == 404: print( "Bare index root returned 404; Gitea's PyPI registry does not " "serve a listing at /simple/ (only /simple// resolves " "per PEP 503). Treating this as expected and deferring to pip's " "own dependency resolution to confirm the index actually works." ) else: raise SystemExit(1) except URLError as error: print(f"Private package-index probe could not reach {safe_url}: {error.reason}") raise SystemExit(1) PY pip config --site set global.extra-index-url "$PRIVATE_INDEX_URL" - name: Install Tools & Deps run: | python -m pip install --upgrade pip setuptools wheel pip install -e ".[dev]" || pip install -e ".[test]" || pip install -e . pip install ruff coverage pip-audit bandit - name: Linting run: ruff check ${{ inputs.source_path }} ${{ inputs.tests_path }} - name: Tests run: | ${{ inputs.test_command }} coverage report --fail-under=${{ inputs.coverage_fail_under }} coverage xml coverage html - name: Security Scan if: ${{ inputs.run_security_scan }} run: | pip freeze | grep -v "git+" > req.txt pip-audit -r req.txt bandit -r ${{ inputs.source_path }} - name: Upload Coverage HTML if: always() uses: actions/upload-artifact@v3 with: name: coverage-html path: htmlcov/ if-no-files-found: warn - name: Upload Coverage XML if: always() uses: actions/upload-artifact@v3 with: name: coverage-xml path: coverage.xml if-no-files-found: warn