Add the Gitea Actions workflows

Ports n3xd's four: pre-commit, backend tests, sharded Playwright and a
compose smoke test. Everything comes from the committed .env.example, so
no repository secrets are needed; Actions still has to be enabled per
repository and a runner registered with the ubuntu-latest label.

container_name, published ports and image tags are all daemon-global, so
two shards on one runner would fight over them. compose.ci.yml resets
them and tags per project. test-backend keeps the fixed names because it
reaches Postgres from the runner host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
This commit is contained in:
Melvin Strobl
2026-08-15 21:20:05 +02:00
co-authored by Claude Opus 5
parent 9cede8dbb5
commit 8a221bfe50
5 changed files with 342 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
name: Playwright Tests
on:
push:
branches:
- main
paths:
- backend/**
- frontend/**
- docker/compose*.yml
- .env.example
- .gitea/workflows/playwright.yml
pull_request:
types:
- opened
- synchronize
paths:
- backend/**
- frontend/**
- docker/compose*.yml
- .env.example
- .gitea/workflows/playwright.yml
env:
# The playwright runner service comes from the dev overlay. It loads the SPA
# from the nginx `frontend` service and reaches the API as http://backend:8000
# on the project network — both wired up in compose.ci.yml.
COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml:docker/compose.ci.yml
# frontend/playwright.config.ts keys the blob reporter, the retries and
# forbidOnly off CI, and compose.dev.yml forwards it into the container.
CI: "true"
jobs:
test-playwright:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# Two spec files (tests/flows.spec.ts, tests/admin.spec.ts) plus
# tests/auth.setup.ts, which every shard reruns as its setup project.
# More shards than that would only pay container startup twice over.
shardIndex: [1, 2]
shardTotal: [2]
env:
# One stack per shard, so a `down -v` cannot reach the other shard's
# volumes. compose.ci.yml above drops the fixed container_names and host
# ports, which the project name does not cover — without it two shards on
# the same runner host collide on the daemon-global names.
COMPOSE_PROJECT_NAME: fluksio-app-ci-pw-${{ matrix.shardIndex }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Write .env
# Not committed; compose interpolates it and the runner container reads
# FIRST_SUPERUSER / FIRST_SUPERUSER_PASSWORD out of it (tests/config.ts).
run: cp .env.example .env
- run: docker compose build backend frontend playwright
- run: docker compose down -v --remove-orphans
# PLAYWRIGHT_BASE_URL points the browser at `frontend`, which is not a
# dependency of the playwright service, so bring it up explicitly.
- run: docker compose up -d --wait backend frontend
- name: Run Playwright tests
run: >-
docker compose run --rm playwright
bunx playwright test --trace=retain-on-failure
--shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Tear down
if: always()
run: docker compose down -v --remove-orphans
- name: Upload blob report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: frontend/blob-report
include-hidden-files: true
retention-days: 1
merge-reports:
needs:
- test-playwright
# Merge even when a shard failed — that report is the one worth reading.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Download blob reports
uses: actions/download-artifact@v4
with:
path: frontend/all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into an HTML report
run: bunx playwright merge-reports --reporter html ./all-blob-reports
working-directory: frontend
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: frontend/playwright-report
include-hidden-files: true
retention-days: 7
+53
View File
@@ -0,0 +1,53 @@
name: pre-commit
# Gitea Actions. `uses:` is resolved against the instance's action mirror, the
# same convention the sibling N3XD repositories use — plain `owner/action@tag`.
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
# Matches backend/Dockerfile, so the gate type-checks and formats against
# the interpreter the image ships.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: |
pyproject.toml
backend/pyproject.toml
uv.lock
- name: Write .env
# Not committed, and the generate-frontend-sdk hook imports the backend
# settings, which read ../.env from backend/.
run: cp .env.example .env
# The hooks call `uv run ruff` from the repo root, so the workspace .venv
# (pyproject.toml → [tool.uv.workspace]) is what has to exist.
- name: Install backend dependencies
run: uv sync
- name: Install frontend dependencies
run: bun install --frozen-lockfile
# The formatting hooks rewrite files; pre-commit exits non-zero when they
# do, which fails the run. No auto-commit branch here: that needs a push
# token, and pre-commit-ci/lite-action talks to a GitHub-only service.
- name: Run pre-commit
run: uvx pre-commit run --all-files --show-diff-on-failure
# The hooks cover ruff and biome but not mypy, which `make lint-backend`
# runs and which is strict for everything outside the flow prototypes.
- name: Typecheck backend
run: uv run mypy app
working-directory: backend
+58
View File
@@ -0,0 +1,58 @@
name: Test Backend
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
env:
# The compose files live in docker/, so a bare `docker compose` from the repo
# root finds nothing. With COMPOSE_FILE set, compose reads .env from the
# working directory — the same file the backend settings load as ../.env.
# The CI-only project name keeps the `down -v` steps away from a dev stack.
COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml
COMPOSE_PROJECT_NAME: fluksio-app-ci
jobs:
test-backend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: |
pyproject.toml
backend/pyproject.toml
uv.lock
- name: Write .env
# Not committed, and both compose and the backend settings need it.
# POSTGRES_SERVER=localhost there matches the port db publishes in the
# dev overlay, so the suite runs on the runner against the container.
run: cp .env.example .env
- run: docker compose down -v --remove-orphans
# Postgres is the only service the suite needs: the mail paths are
# patched in tests/, and the flow engine falls back to in-memory state
# while REDIS_HOST is empty.
- run: docker compose up -d --wait db
- run: uv sync
- name: Migrate DB and seed the superuser
run: uv run bash scripts/prestart.sh
working-directory: backend
# scripts/tests-start.sh waits for the DB, then runs pytest under
# coverage and prints the report (see backend/scripts/test.sh).
- name: Run tests with coverage
run: uv run bash scripts/tests-start.sh "Coverage for ${{ github.sha }}"
working-directory: backend
- name: Tear down
if: always()
run: docker compose down -v --remove-orphans
+63
View File
@@ -0,0 +1,63 @@
name: Compose Smoke Test
# Lives in this repository rather than the workspace root: the stack it starts
# is entirely app-local (docker/compose*.yml + .env.example). A root workflow
# would additionally need a token to check out the private submodules and would
# have to run scripts/setup.sh to generate secrets, for the same coverage.
on:
push:
branches:
- main
paths:
- backend/**
- frontend/**
- docker/compose*.yml
- .env.example
- .gitea/workflows/test-compose.yml
pull_request:
types:
- opened
- synchronize
paths:
- backend/**
- frontend/**
- docker/compose*.yml
- .env.example
- .gitea/workflows/test-compose.yml
env:
# compose.ci.yml drops the fixed container_names and host ports so this job
# can share a runner host with the Playwright shards.
COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml:docker/compose.ci.yml
# Own project name so this job's `down -v` cannot reach another stack.
COMPOSE_PROJECT_NAME: fluksio-app-ci-compose
jobs:
test-compose:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Write .env
run: cp .env.example .env
# The Playwright job runs the SPA off a Vite server inside its own
# container, so the nginx image is only ever built there. This job is what
# covers it — the SPA fallback in frontend/nginx.conf is the part that
# breaks silently.
- run: docker compose build backend frontend
- run: docker compose down -v --remove-orphans
# db and prestart come along as depends_on; --wait blocks on the backend
# healthcheck in docker/compose.yml.
- run: docker compose up -d --wait backend frontend
# Nothing is published on the host (compose.ci.yml), so the checks run
# from inside the backend container against the project network.
- name: Backend is up
run: docker compose exec -T backend curl -f http://localhost:8000/api/v1/utils/health-check/
- name: Frontend is up
run: docker compose exec -T backend curl -f http://frontend/
- name: An authenticated route serves the SPA shell
run: docker compose exec -T backend curl -f http://frontend/flows
- name: Tear down
if: always()
run: docker compose down -v --remove-orphans
+61
View File
@@ -0,0 +1,61 @@
# CI isolation overrides — layered on top of compose.yml + compose.dev.yml by
# the Playwright shards and the compose smoke job in .gitea/workflows/.
#
# A self-hosted runner can put several jobs on the same host. A per-job
# COMPOSE_PROJECT_NAME keeps their containers, networks and volumes apart, but
# it does not cover the two things that are global to the Docker daemon:
#
# 1. `container_name` ignores the project name, so a second job fails with
# "the container name /fluksio-db is already in use".
# 2. Published host ports are first come, first served.
#
# Neither is needed in CI: everything reaches everything else by compose
# service name on the project network.
services:
proxy:
ports: !reset []
db:
container_name: !reset null
ports: !reset []
redis:
container_name: !reset null
adminer:
container_name: !reset null
ports: !reset []
mailcatcher:
ports: !reset []
prestart:
container_name: !reset null
backend:
container_name: !reset null
ports: !reset []
frontend:
container_name: !reset null
ports: !reset []
# Baked at build time. compose.dev.yml bakes http://localhost:8000, which
# resolves to the playwright container itself; the browser has to reach the
# API by service name on the project network instead.
build:
args:
- VITE_API_URL=http://backend:8000
# Image tags are daemon-global too, and this one no longer holds the same
# bytes as the smoke job's: without a per-job tag the two jobs overwrite
# each other's `fluksio-frontend:latest` and the SPA calls the wrong API.
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${COMPOSE_PROJECT_NAME:-ci}'
playwright:
ports: !reset []
# frontend/playwright.config.ts defaults baseURL to http://app.localhost,
# which is right for a local run against the integrated stack but resolves
# to 127.0.0.1 in here. In CI the nginx `frontend` service serves the SPA.
# This origin must stay in BACKEND_CORS_ORIGINS (.env.example).
environment:
- PLAYWRIGHT_BASE_URL=http://frontend