Gitea's PyPI registry only resolves /simple/<package>/, not the bare /simple/ index root, so the v1.11.2 probe added to give visible diagnostics for the private-index setup step was always 404ing even when the index and credentials were correct. This broke installs of private Python packages during builds. The probe now only fails hard on 401/403 (auth rejected) or an unreachable host; a plain 404 is logged and left to pip's own resolution to confirm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
146 lines
5.8 KiB
YAML
146 lines
5.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.
|
|
# NOTE: Gitea's PyPI package registry does not serve a browsable listing at
|
|
# the bare /simple/ root (only /simple/<package>/ 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 "<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)
|
|
if error.code == 404:
|
|
print(
|
|
"Bare index root returned 404; Gitea's PyPI registry does not "
|
|
"serve a listing at /simple/ (only /simple/<package>/ 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
|