e43428c1a2
Gitea Actions CI / detect (push) Successful in 3s
Gitea Actions CI / Compose & nginx validation (push) Failing after 20s
Gitea Actions CI / Frontend (lint / type / test / build) (push) Failing after 24s
Gitea Actions CI / Backend (lint / type / test / build) (push) Failing after 1m34s
187 lines
7.6 KiB
YAML
187 lines
7.6 KiB
YAML
name: Gitea Actions CI
|
|
|
|
on:
|
|
push:
|
|
branches: [master, main]
|
|
pull_request:
|
|
|
|
# Single persistent vm-runner with host-network job containers: serialize runs
|
|
# so PostgreSQL/Redis service ports never collide across concurrent runs.
|
|
concurrency:
|
|
group: gitea-actions-ci
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Jobs activate only when their locked dependency manifests exist.
|
|
detect:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
backend: ${{ steps.dirs.outputs.backend }}
|
|
frontend: ${{ steps.dirs.outputs.frontend }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- id: dirs
|
|
run: |
|
|
echo "backend=$([ -f backend/uv.lock ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
echo "frontend=$([ -f frontend/pnpm-lock.yaml ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
|
|
backend:
|
|
name: Backend (lint / type / test / build)
|
|
needs: detect
|
|
if: needs.detect.outputs.backend == 'true'
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: backend
|
|
services:
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
env:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: ${{ secrets.CI_POSTGRES_PASSWORD }}
|
|
POSTGRES_DB: finance_test
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd "pg_isready -U postgres"
|
|
--health-interval 5s
|
|
--health-timeout 5s
|
|
--health-retries 12
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- 6379:6379
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 5s
|
|
--health-timeout 5s
|
|
--health-retries 12
|
|
env:
|
|
DATABASE_URL: postgresql+psycopg://postgres:${{ secrets.CI_POSTGRES_PASSWORD }}@localhost:5432/finance_test
|
|
REDIS_URL: redis://localhost:6379/0
|
|
CELERY_BROKER_URL: redis://localhost:6379/1
|
|
IMPORTS_KEK_B64: ${{ secrets.CI_IMPORTS_KEK_B64 }}
|
|
CI_RLS_DATABASE_PASSWORD: ${{ secrets.CI_RLS_DATABASE_PASSWORD }}
|
|
CI_MINIO_ROOT_PASSWORD: ${{ secrets.CI_MINIO_ROOT_PASSWORD }}
|
|
RLS_DATABASE_URL: postgresql+psycopg://finance_app_test:${{ secrets.CI_RLS_DATABASE_PASSWORD }}@localhost:5432/finance_test
|
|
S3_ENDPOINT_URL: http://localhost:9000
|
|
S3_REGION: us-east-1
|
|
S3_ACCESS_KEY_ID: minioadmin
|
|
S3_SECRET_ACCESS_KEY: ${{ secrets.CI_MINIO_ROOT_PASSWORD }}
|
|
S3_BUCKET_STATEMENTS: finance-statements
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: astral-sh/setup-uv@v5
|
|
with:
|
|
python-version: "3.14"
|
|
enable-cache: true
|
|
cache-python: true
|
|
cache-dependency-glob: backend/uv.lock
|
|
- name: Install dependencies
|
|
run: uv sync --locked
|
|
- name: Lint (ruff check)
|
|
run: uv run ruff check .
|
|
- name: Format (ruff format)
|
|
run: uv run ruff format --check .
|
|
- name: Type check (mypy)
|
|
run: uv run mypy .
|
|
- name: Create restricted app role and migrate
|
|
run: |
|
|
uv run python -c "import os; from psycopg import sql; from sqlalchemy import create_engine,text; e=create_engine(os.environ['DATABASE_URL']); c=e.connect(); raw=c.connection.driver_connection; role=sql.SQL('CREATE ROLE finance_app_test LOGIN PASSWORD {} NOSUPERUSER NOBYPASSRLS').format(sql.Literal(os.environ['CI_RLS_DATABASE_PASSWORD'])).as_string(raw); c.exec_driver_sql(role); c.execute(text('GRANT CONNECT ON DATABASE finance_test TO finance_app_test')); c.execute(text('GRANT USAGE ON SCHEMA public TO finance_app_test')); c.execute(text('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO finance_app_test')); c.commit(); c.close(); e.dispose()"
|
|
uv run alembic upgrade head
|
|
# docker.io denies minio/minio from this network (run 246); quay.io serves it.
|
|
# Unique container name keeps parallel/retried runs off each other.
|
|
- name: Start MinIO (S3-compatible test storage)
|
|
run: |
|
|
docker run -d --name minio-ci-${{ github.run_id }}-${{ github.run_attempt }} -p 9000:9000 \
|
|
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD \
|
|
quay.io/minio/minio:latest server /data
|
|
for i in $(seq 1 30); do
|
|
if curl -sf http://localhost:9000/minio/health/live > /dev/null; then
|
|
exit 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
echo "MinIO did not become healthy" && exit 1
|
|
- name: Test (pytest)
|
|
run: uv run pytest -ra
|
|
- name: Build provider-neutral image
|
|
run: docker build -f ../infra/docker/Dockerfile.backend -t finance-api:ci-${{ github.run_id }}-${{ github.run_attempt }} .
|
|
- name: Cleanup Docker resources
|
|
if: always()
|
|
run: |
|
|
docker rm -f minio-ci-${{ github.run_id }}-${{ github.run_attempt }} || true
|
|
docker rmi -f finance-api:ci-${{ github.run_id }}-${{ github.run_attempt }} || true
|
|
|
|
frontend:
|
|
name: Frontend (lint / type / test / build)
|
|
needs: detect
|
|
if: needs.detect.outputs.frontend == 'true'
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: frontend
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
# pnpm 10 is required: frontend/pnpm-workspace.yaml is config-only
|
|
# (allowBuilds) and pnpm 9 rejects it with "packages field missing or empty".
|
|
- uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 10
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: pnpm
|
|
cache-dependency-path: frontend/pnpm-lock.yaml
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Lint
|
|
run: pnpm lint
|
|
- name: Type check
|
|
run: pnpm typecheck
|
|
- name: Test (vitest, non-watch)
|
|
run: pnpm test
|
|
- name: Build
|
|
run: pnpm build
|
|
|
|
compose:
|
|
name: Compose & nginx validation
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
# Unique project name per run: this runner reuses the host, and the
|
|
# compose file pins "name: finance", which would collide across runs.
|
|
COMPOSE_PROJECT_NAME: finance-ci-${{ github.run_id }}-${{ github.run_attempt }}
|
|
# Backend job binds 5432/6379/9000 on the shared host network in the
|
|
# same run, so compose services use non-overlapping host ports.
|
|
POSTGRES_HOST_PORT: "15432"
|
|
REDIS_HOST_PORT: "16379"
|
|
MINIO_API_HOST_PORT: "19000"
|
|
MINIO_CONSOLE_HOST_PORT: "19001"
|
|
MAILPIT_SMTP_HOST_PORT: "11025"
|
|
MAILPIT_UI_HOST_PORT: "18025"
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Prepare env from example (placeholders only)
|
|
run: cp infra/.env.example infra/.env
|
|
- name: Validate compose file
|
|
run: docker compose -f infra/docker-compose.yml config -q
|
|
# docker.io denies minio/minio from this network (run 246); pre-pull from
|
|
# quay.io and retag so compose finds the image locally and never pulls
|
|
# the blocked reference.
|
|
- name: Pre-pull MinIO via quay.io
|
|
run: |
|
|
docker pull quay.io/minio/minio:latest
|
|
docker tag quay.io/minio/minio:latest minio/minio:latest
|
|
- name: Boot infra services and wait for healthchecks
|
|
run: docker compose -f infra/docker-compose.yml up -d --wait --wait-timeout 180 postgres redis minio mailpit
|
|
- name: Show service status
|
|
run: docker compose -f infra/docker-compose.yml ps
|
|
- name: Validate nginx config
|
|
run: |
|
|
docker run --rm --network none --add-host api:127.0.0.1 \
|
|
-v "$PWD/infra/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
|
|
nginx:1.27-alpine nginx -t
|
|
- name: Teardown
|
|
if: always()
|
|
run: docker compose -f infra/docker-compose.yml down -v --remove-orphans
|