Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/update-readme/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ After saving all docs, report in under 25 lines:
- **Stale references removed** — e.g. "Removed 4 remaining `render.yaml` mentions in Security and ToC"
- **Anything you noticed but did NOT change** — sections that look stale but weren't in scope (surface these for the user to decide)
- **Files NOT touched and why** — if only `docs/CELERY.md` was in scope for the staged diff, say so explicitly
- **A commit messagge with a super brief description of all changes** - e.g. "Created inspirational quote generator, cleaned legacy render config files and prepared everything for googl e cloud migration and updated versioning and precommit make commands"

---

Expand Down
112 changes: 111 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ jobs:
- name: Bandit — security linter
run: PYTHONUTF8=1 uv run bandit -r project/ -c pyproject.toml

- name: pip-audit — dependency CVE scan
run: uv run pip-audit

- name: Hadolint — Dockerfile lint
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile

# ============================================================
# JOB 2: Type check — mypy (no DB needed)
# ============================================================
Expand Down Expand Up @@ -150,7 +158,7 @@ jobs:

- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
files: project/coverage.xml
fail_ci_if_error: false
Expand All @@ -163,3 +171,105 @@ jobs:
name: coverage-report
path: project/coverage.xml
retention-days: 7

# ============================================================
# JOB 4: Docker build — validates Dockerfile builds cleanly
# ============================================================
docker-build:
name: Docker build
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build image (no push)
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: five-a-day:ci
cache-from: type=gha
cache-to: type=gha,mode=max

# ============================================================
# JOB 5: Trivy — filesystem CVE scan → GitHub Security tab
# ============================================================
trivy:
name: Trivy CVE scan
runs-on: ubuntu-latest
permissions:
security-events: write

steps:
- uses: actions/checkout@v4

- name: Trivy — scan Python dependencies and filesystem
uses: aquasecurity/trivy-action@v0.35.0
with:
scan-type: fs
scan-ref: .
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL

- name: Upload results to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif

# ============================================================
# JOB 6: Publish to GHCR + Trivy image scan
# Runs only on push to main or testing (not on PRs).
# Depends on test + docker-build passing first.
# ============================================================
docker-publish:
name: Publish image & scan
runs-on: ubuntu-latest
needs: [test, docker-build]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/testing')
permissions:
contents: read
packages: write
security-events: write

steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push to GHCR
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Trivy — scan Docker image for OS-level CVEs
uses: aquasecurity/trivy-action@v0.35.0
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
format: sarif
output: trivy-image-results.sarif
severity: HIGH,CRITICAL

- name: Upload image scan to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-image-results.sarif
33 changes: 33 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Dependabot auto-merge

# Automatically merges Dependabot PRs once CI passes.
# Only acts on minor/patch updates — major version bumps are left for manual review.
# Requires "Allow auto-merge" enabled in repo Settings → General → Pull Requests.

on: pull_request

permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
name: Enable auto-merge
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'

steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Enable auto-merge for minor/patch updates
if: |
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 changes: 26 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Dependency review

# Runs on every PR and blocks merges that introduce a dependency with a known
# HIGH or CRITICAL CVE. Complements pip-audit (which checks all installed deps)
# by specifically flagging what the PR diff adds or changes.
# GitHub-native — no token or third-party service required.

on: pull_request

permissions:
contents: read
pull-requests: write

jobs:
dependency-review:
name: Dependency review
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Dependency review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
comment-summary-in-pr: always
49 changes: 49 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: OSSF Scorecard

# Grades the repo's supply-chain security posture (branch protection, dependency
# pinning, CI, secret scanning, etc.) and publishes results to the GitHub
# Security tab. Free for public repositories.
# https://securityscorecards.dev

on:
branch_protection_rule:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
push:
branches: [main]

permissions: read-all

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
contents: read
actions: read

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Run Scorecard analysis
uses: ossf/scorecard-action@v2.4.0
with:
results_file: scorecard-results.sarif
results_format: sarif
publish_results: true

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: scorecard-results
path: scorecard-results.sarif
retention-days: 5

- name: Upload results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scorecard-results.sarif
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ All pricing flows through `billing/services/`. The single source of truth is `Si
- **Template names are English** — all email templates were renamed from Spanish (e.g., `matricula_niño.html` → `enrollment_child.html`). Never create templates with Spanish names.
- **Version in four places** — `pyproject.toml`, `project/project/settings.py` (`APP_VERSION` default), the `README.md` header badge URL, and `uv.lock` (the project's own `[[package]]` entry). `make version x.y.z` (positional, with y/N confirmation) updates the first three with `sed` and regenerates `uv.lock` via `uv lock --quiet`. Running `make version` with no argument prints the pyproject and README badge values side-by-side and warns if they've drifted. `make pc-run`'s auto patch-bump does the same four updates, then the existing `git add uv.lock` block at the end of the target stages the regenerated lockfile so the next commit isn't blocked.
- **APP_VERSION in `.env`** — the local `.env` may contain a legacy `APP_VERSION=0.x.y` line. Either remove the line or update it — it silently overrides the default in `settings.py` at runtime.
- **`load_dotenv(override=False)` — Docker env vars take precedence** — `settings.py` calls `load_dotenv(..., override=False)`. This means environment variables already set in the process (i.e. injected by Docker Compose's `environment:` block) are NOT overridden by the `.env` file. This is intentional: it allows compose overlays (`docker-compose.testing.yml`) to inject different credentials without the volume-mounted `.env` file silently overwriting them. If you need the `.env` file to win over Docker's env vars, you'd need `override=True` — but don't do that, it breaks multi-environment setups.
- **`pc-run` renamed** — the old `make pre-commit-run` target is now `make pc-run`. It also auto-bumps the patch version on a clean pass (y/N prompt) and auto-stages `uv.lock` if regenerated.
- **mypy CI job needs `DJANGO_DEBUG=True`** — `django-stubs` imports `project.settings` at load time for the Django plugin. Without `DJANGO_DEBUG=True` + a dummy `DJANGO_SECRET_KEY`, the production guard at the top of `settings.py` raises `ValueError: DJANGO_SECRET_KEY debe ser cambiado en producción`. The CI `mypy` step sets both, plus `PYTHONPATH=project`, as env vars. Any new static-analysis job that imports settings will need the same.
- **Test settings disable production security redirects** — `settings_test.py` explicitly sets `SECURE_SSL_REDIRECT = False`, `SECURE_HSTS_SECONDS = 0`, `SESSION_COOKIE_SECURE = False`, `CSRF_COOKIE_SECURE = False`. Don't remove them — the Django test client speaks HTTP against `testserver`, and inheriting `SECURE_SSL_REDIRECT=True` from `settings.py` (which kicks in when `DEBUG=False`) turns every test request into a 301 to `https://testserver/...`. The overrides keep the test settings self-contained regardless of how CI configures `DJANGO_DEBUG`.
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

# Install system deps needed to compile Python packages
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
postgresql-client \
Expand Down Expand Up @@ -42,6 +43,7 @@ ENV PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"

# Install only runtime system deps
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
libpq-dev \
Expand Down
Loading
Loading