CI/CD
Three GitHub Actions workflows run automatically.
ci.yml
Runs on every push to main and every pull request.
build ──┬──→ lint ──┐
│ ├──→ release (main, approval required) ──→ docker: build → smoke → Trivy → SARIF → push → sign
└──→ test ──┘
dependency-review (PRs only)
Jobs
| Job | Tool | Needs | Runs on |
|---|---|---|---|
build |
pip install (cached venv), pip-audit, import check | — | all events |
dependency-review |
actions/dependency-review-action | — | PRs only |
lint |
black → ruff (auto-fix) → pylint → bandit → mypy | build |
all events |
test |
pytest matrix (Python 3.11, 3.12, 3.13) + Codecov | build |
all events |
release |
python-semantic-release | lint, test |
push to main only |
docker |
multi-stage build, smoke test, Trivy, SARIF, SBOM, Cosign, GHCR push | lint, test, release |
all events |
Build
- Creates a
.venvand installsrequirements.txt+requirements-dev.txtinto it. - Saves the venv to
actions/cacheunder keyvenv-{os}-py3.12-{hash(requirements files)}. - Runs
pip check(dependency consistency),pip-audit(CVE scan), andpython -c "import app".
Downstream jobs restore the cache and skip pip install entirely on a cache hit. The Python 3.12 test matrix entry always hits the same cache. Python 3.11 and 3.13 build their own caches on first run and hit on subsequent runs.
Dependency Review (PRs only)
actions/dependency-review-action compares the dependency graph before and after the PR. Blocks merge if any newly introduced package has a known vulnerability.
Lint
Restores the venv from cache, then runs in sequence:
- black — enforces consistent formatting
- ruff check --fix + ruff format — lints and formats; changes are committed back to the branch automatically (skipped for fork PRs)
- pylint — static analysis
- bandit — security-focused static analysis
- mypy — type checking
Test
Runs in parallel with lint. Matrix across Python 3.11, 3.12, and 3.13 (fail-fast: false):
- Each matrix entry restores its own cached venv (key includes the Python version).
pytest --cov=app --cov-fail-under=80— build fails if coverage drops below 80%.pytest-github-actions-annotate-failuresis installed and auto-activates — failed tests appear as inline annotations on PR diffs.- Coverage XML uploaded to Codecov from the 3.12 run only.
Release
Protected by the production GitHub Environment. Before the job starts, GitHub pauses and waits for a required reviewer to approve. This gates both the version tag/release and (because docker depends on release) the GHCR push behind a human approval.
Configure reviewers at Settings → Environments → production.
Uses python-semantic-release with the Angular commit convention — see Automatic versioning below.
Docker
- Build test stage — runs ruff, mypy, and pytest inside the container
- Build runtime image — loaded locally for smoke testing and scanning
- Smoke test — starts the container, hits key endpoints with
curl - Trivy scan (table) — fails the build on unfixed
CRITICALorHIGHvulnerabilities - Trivy SARIF upload — sends findings to Security → Code scanning
- Generate SBOM — Trivy produces an SPDX JSON file, uploaded as a workflow artifact (90-day retention)
- Push to GHCR — only on
main; re-uses GHA layer cache so it is near-instant - Cosign sign — keyless signature stored in the registry alongside the image
- Cosign attest — SBOM attached as a verifiable attestation to the image digest
codeql.yml
Runs on push to main, PRs, and weekly (Monday 08:00 UTC).
Performs source-level security analysis on app.py using the security-and-quality
query suite. Findings are uploaded to Security → Code scanning as SARIF.
Python does not require a build step for CodeQL — the action analyses the source directly.
scorecard.yml
Runs on push to main and weekly (Monday 09:00 UTC).
Evaluates the repository against OSSF Scorecard best practices, including:
- Branch protection rules
- Required code review
- Dependency version pinning
- CI test coverage
- Signed releases
Results are uploaded to Security → Code scanning and published to the OpenSSF
REST API to power the public scorecard badge (publish_results: true).
Automatic versioning
Versions are bumped automatically from commit messages using the Angular convention:
| Commit prefix | Bump | Example |
|---|---|---|
fix: |
patch | 0.1.0 → 0.1.1 |
feat: |
minor | 0.1.0 → 0.2.0 |
feat!: / BREAKING CHANGE: footer |
major | 0.1.0 → 1.0.0 |
chore:, docs:, style: and similar prefixes do not trigger a release.
The version is stored in pyproject.toml under [project] version and tagged
as v<version> in git. Images are tagged <version>, sha-<short-sha>, and latest.
Image registry
Images are published to:
| Tag | When |
|---|---|
1.2.3 |
On a versioned release |
sha-abc1234 |
Every push to main |
latest |
Every push to main |