Phase 0 milestone 0.8: CI pipeline on Gitea Actions

Workflow moved to .gitea/workflows/ci.yml (Gitea is the CI host). Every
gate now passes locally, step-for-step; green on main is pending an
act_runner on gitea.doesworks.net (runs #1-#5 are queued "Waiting").

Gate fixes found by actually running them:
- pip-audit: `--disable-pip` without `-r` was a usage error, so the old
  step could never pass. Now `pip-audit --strict .` audits the runtime
  dependency set from pyproject.
- pip-audit then flagged ecdsa (PYSEC-2026-1325, no fix), a hard dep of
  python-jose. Swapped to PyJWT (HS256 only; 3-line change in
  auth/security.py). Flag to OWC.
- PyJWT warned the test JWT secret was 11 bytes. JWT_SECRET now requires
  >= 32 bytes (RFC 7518 s3.2) at settings load; dev/compose/test defaults
  updated. Test-only value carries an inline gitleaks:allow.
- bandit B101: assert in retention._count_past replaced with a raise.
- gitleaks: gitleaks-action needs a GitHub token/license; replaced with
  the pinned 8.30.1 binary, sha256-verified, scanning full history.

Added:
- SBOM: CycloneDX 1.6 from a clean runtime-only venv (39 components),
  uploaded as a CI artifact. YPMS-POAM-042.
- cyclonedx-bom in dev extras; CI outputs gitignored.

158 tests passing, 95.83% coverage; ruff, mypy, bandit, pip-audit,
gitleaks clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris TonjesandClaude Opus 5.5 committed 2026-10-01 10:57:09 -04:00
1 parent 20a7fdf262
commit 5f2d885fdc
11 files changed
+123 -55

No files matched your search

+6 -2
View File
@@ -11,6 +11,10 @@ NIST 800-63 IAL/AAL semantics: `pre_application` < `standard` <
require `payroll_authorized` or higher (Phase 7+).
pbkdf2_sha256 not bcrypt — see ARCHITECTURE.md §4.6.
JWT library: PyJWT, not OWC's python-jose (milestone 0.8). python-jose hard-
depends on `ecdsa`, which carries an unfixable advisory (PYSEC-2026-1325)
that failed the pip-audit gate. YPMS only signs HS256. Flag to OWC.
"""
from __future__ import annotations
@@ -18,7 +22,7 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
from typing import Any
from jose import JWTError, jwt
import jwt
from passlib.context import CryptContext
from ypms.settings import get_settings
@@ -77,6 +81,6 @@ def decode_token(token: str) -> dict[str, Any]:
s = get_settings()
try:
payload: dict[str, Any] = jwt.decode(token, s.jwt_secret, algorithms=[s.jwt_alg])
except JWTError as e:
except jwt.PyJWTError as e:
raise ValueError("invalid token") from e
return payload