131 lines
4.8 KiB
YAML
131 lines
4.8 KiB
YAML
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:-<empty>}"
|
|
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.
|
|
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 "<missing host>"
|
|
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)
|
|
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
|