Skip to content

Fix/bug audit high medium #1352

Fix/bug audit high medium

Fix/bug audit high medium #1352

Workflow file for this run

name: CI
on:
pull_request:
push:
branches:
- "**"
permissions:
contents: read
jobs:
commits:
name: Commit Messages (Conventional Commits)
runs-on: ubuntu-latest
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install commitlint
run: npm ci --ignore-scripts
- name: Lint commits (PR)
if: ${{ github.event_name == 'pull_request' }}
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.sha }} --verbose
- name: Lint commits (push)
if: ${{ github.event_name == 'push' && github.ref != 'refs/heads/main' && github.event.before != '0000000000000000000000000000000000000000' }}
# github.event.before can be a commit no longer reachable from github.sha (e.g. after a
# rebase or force-push rewrites history), which makes the range invalid and this step
# fail even though nothing is actually wrong with the commit messages. When that happens,
# skip rather than fail: if this branch has an open PR, the pull_request-triggered lint
# job above already covers the same commits against a stable base.sha.
run: |
if git merge-base --is-ancestor ${{ github.event.before }} ${{ github.sha }} 2>/dev/null; then
npx commitlint --from ${{ github.event.before }} --to ${{ github.sha }} --verbose
else
echo "::notice::${{ github.event.before }} is not an ancestor of ${{ github.sha }} (likely a rebase/force-push) — skipping push-triggered lint for this range."
fi
ui:
name: UI Typecheck + Test + Build
runs-on: ubuntu-latest
steps:
- name: Checkout (full history for diff coverage)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install UI deps
working-directory: apps/ui
run: bun install --frozen-lockfile
- name: Typecheck
working-directory: apps/ui
run: bun run typecheck
- name: Lint UI
working-directory: apps/ui
run: bun run lint
# Runs all tests and enforces the global coverage floor (apps/ui/vitest.config.ts
# test.coverage.thresholds) — a regression guard, not an aspirational target; most
# app/** page components aren't unit-tested yet. New/changed lines are held to a much
# higher bar by the diff-coverage step below.
- name: Test UI (with coverage)
working-directory: apps/ui
run: bun run test:coverage
- name: Build UI
working-directory: apps/ui
run: bun run build
- name: Setup Python (for diff-cover)
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install diff-cover
run: python -m pip install diff-cover==9.4.1
# vitest's lcov report has SF: paths relative to apps/ui (its configured `root`), but
# diff-cover matches against `git diff` paths, which are repo-root-relative. Rewrite the
# paths to be repo-root-relative before handing off, then run diff-cover from the repo
# root so its own path resolution lines up. (Verified locally: without this rewrite,
# diff-cover silently reports "No lines with coverage information" instead of failing.)
- name: Diff coverage (new/changed UI lines must be tested)
run: |
sed -E 's#^SF:(.*)$#SF:apps/ui/\1#' apps/ui/coverage/lcov.info | tr '\134' '/' > apps/ui/coverage/lcov-root-relative.info
diff-cover apps/ui/coverage/lcov-root-relative.info --compare-branch=origin/main --fail-under=90
python:
name: Python Tests + Compile
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: clevis
POSTGRES_USER: clevis
POSTGRES_PASSWORD: clevis
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U clevis"
--health-interval 10s
--health-timeout 5s
--health-retries 5
# Issue #191/S3: webhook ingestion queue. Not used by any test yet in this PR
# (infra scaffolding only), but required for Settings() to construct (redis_url
# has no default) and wired up now so PR 3's Redis-backed tests don't need a
# separate CI change.
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
# Superuser URL: used for migrations and every step except "Run Python tests" below,
# which overrides DATABASE_URL to the clevis_api role instead (issue #330/#190 --
# proves RLS actually enforces tenant isolation under the real non-superuser runtime
# role, not just a hand-built throwaway test role).
DATABASE_URL: postgresql+psycopg://clevis:clevis@localhost:5432/clevis
API_DB_PASSWORD: ci-dummy-api-db-password-not-for-production-use-0
JOB_SECRET_KEY: ${{ secrets.JOB_SECRET_KEY || 'ci-dummy-secret-key-not-for-production-use-00' }}
AUTH_SECRET: ${{ secrets.AUTH_SECRET || 'ci-dummy-auth-secret-not-for-production-use-000' }}
REDIS_URL: redis://localhost:6379/0
steps:
- name: Checkout (full history for diff coverage)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install API/Worker + test requirements
run: |
python -m pip install --upgrade pip
python -m pip install -r apps/api/requirements.txt
python -m pip install -r apps/worker/requirements.txt
python -m pip install -e packages/checks
python -m pip install -r requirements-test.txt
- name: Run migrations
working-directory: apps/api
run: python -m alembic upgrade head
- name: Check for model / migration drift
working-directory: apps/api
run: python -m alembic check
# Reuses the exact same script an operator runs by hand for an existing deployment
# (docs/self-hosting.md) instead of duplicating its grant SQL here -- keeps CI's
# provisioning and the documented manual path from drifting apart. Connects over TCP
# to the service container (not a docker exec local-socket connection like the
# script's usual home), so auth needs PGPASSWORD/PGHOST/PGPORT explicitly.
- name: Provision clevis_api role (issue #330 -- non-superuser runtime role)
env:
PGHOST: localhost
PGPORT: 5432
PGPASSWORD: clevis
POSTGRES_USER: clevis
POSTGRES_DB: clevis
run: sh docker/provision-api-role-existing-deployment.sh
# Global floor (.coveragerc / --cov-fail-under) is a regression guard measured against
# the current baseline (~87%), not an aspirational target — it stops overall coverage
# from silently eroding. New/changed lines are held to a much higher bar by the diff
# coverage step below.
#
# DATABASE_URL is overridden here (not at job level) to the clevis_api role
# provisioned above -- issue #330/#190: this is what actually exercises the RLS
# policies (migrations 0030/0031) against a real non-superuser, non-owner role, the
# same posture as production when API_DB_PASSWORD is configured. Every other step
# in this job (migrations, drift check, coverage upload) keeps using the job-level
# superuser DATABASE_URL, since Alembic needs DDL/GRANT privilege this role
# intentionally lacks.
- name: Run Python tests (with coverage)
env:
DATABASE_URL: postgresql+psycopg://clevis_api:${{ env.API_DB_PASSWORD }}@localhost:5432/clevis
run: >
python -m pytest -q
--cov=apps/api/src --cov=apps/worker/src --cov=packages/checks/src
--cov-report=xml --cov-report=term --cov-fail-under=85
- name: Diff coverage (new/changed Python lines must be tested)
run: diff-cover coverage.xml --compare-branch=origin/main --fail-under=90
- name: Compile Python sources
run: python -m compileall apps/api/src apps/worker/src
e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Dummy secrets, same pattern as the "python" job — this stack is thrown away at the
# end of the job, nothing here is a real credential.
- name: Write CI env file
run: |
cat > .env <<'EOF'
DB_USER=clevis
DB_PASSWORD=clevis
DB_NAME=clevis
JOB_SECRET_KEY=ci-e2e-secret-key-not-for-production-use-0000
AUTH_SECRET=ci-e2e-auth-secret-not-for-production-use-00000
NEXT_PUBLIC_API_BASE=http://localhost:8080
NEXT_PUBLIC_GITHUB_APP_SLUG=
REDIS_PASSWORD=ci-e2e-redis-password-not-for-production-use-000
EOF
# Full docker-compose stack (not direct processes) so this exercises the real images —
# the same ones verified in Docker Build Verification — not just app code in isolation.
- name: Start stack
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile backend --profile frontend up --build -d
- name: Wait for API and UI to be reachable
run: |
for i in $(seq 1 60); do
api_ok=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/healthz || echo "000")
ui_ok=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/login || echo "000")
if [ "$api_ok" = "200" ] && [ "$ui_ok" = "200" ]; then
echo "API and UI are up"
exit 0
fi
sleep 2
done
echo "Timed out waiting for API/UI"
docker compose -f docker-compose.yml -f docker-compose.ci.yml logs
exit 1
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install UI deps
working-directory: apps/ui
run: bun install --frozen-lockfile
- name: Install Playwright browser (Chromium only)
working-directory: apps/ui
run: bunx playwright install --with-deps chromium
- name: Run E2E tests
working-directory: apps/ui
env:
E2E_BASE_URL: http://localhost:3000
E2E_API_BASE: http://localhost:8080
CI: "true"
run: bun run test:e2e
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: apps/ui/playwright-report/
retention-days: 7
- name: Stack logs (on failure)
if: failure()
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml logs
- name: Tear down stack
if: always()
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v
docker:
name: Docker Build Verification
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build API image
run: docker build -t clevis-api -f apps/api/Dockerfile .
- name: Build Worker image
run: docker build -t clevis-worker -f apps/worker/Dockerfile .
- name: Build UI image
run: docker build -t clevis-ui -f apps/ui/Dockerfile apps/ui
# Building an image only proves the Dockerfile's instructions succeed — it does not
# prove the app can actually import/start (e.g. a missing runtime dependency like
# packages/checks not being installed in an image). Smoke-test by importing the
# entrypoint module directly, bypassing entrypoint.sh so no live DB is needed.
- name: Smoke-test API image
run: |
docker run --rm --entrypoint python \
-e DATABASE_URL=postgresql+psycopg://smoke:smoke@localhost:5432/smoke \
-e JOB_SECRET_KEY=ci-smoke-test-key-not-for-production-use-000 \
-e AUTH_SECRET=ci-smoke-test-secret-not-for-production-use-00 \
-e REDIS_URL=redis://smoke:6379/0 \
clevis-api -c "import src.main"
- name: Smoke-test Worker image
run: |
docker run --rm --entrypoint python \
-e DATABASE_URL=postgresql://smoke:smoke@localhost:5432/smoke \
-e JOB_SECRET_KEY=ci-smoke-test-key-not-for-production-use-000 \
-e REDIS_URL=redis://smoke:6379/0 \
clevis-worker -c "import worker"