--- date: "2026-03-04T18:46:12.266203+00:00" git_commit: 316137bf1c391577e884ce525af780f45e34da86 branch: master topic: "T-3: CI/CD Pipeline — Gitea Actions" tags: [plan, ci-cd, gitea, docker, buildah] status: implemented --- # T-3: CI/CD Pipeline Implementation Plan ## Overview Set up a Gitea Actions CI/CD pipeline that runs on every push (tests only) and on SemVer-tagged releases (tests + build + publish). Single workflow file, three jobs. ## Current State Analysis - All dependencies completed: T-1 (monorepo), T-2 (Dockerfile), T-5 (API-first tooling) - Multi-stage Dockerfile exists and works (`Dockerfile:1-26`) - Dockerfile skips tests (`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip`) by design — quality gates belong in CI - `.dockerignore` already excludes `.gitea/` - No CI/CD configuration exists yet - Backend has full verify lifecycle: Checkstyle, JUnit 5, ArchUnit, SpotBugs - Frontend has lint (oxlint + ESLint), type-check (`vue-tsc`), tests (Vitest), and Vite production build ## Desired End State A single workflow file at `.gitea/workflows/ci.yaml` that: 1. Triggers on every push (branches and tags) 2. Runs backend and frontend quality gates in parallel 3. On SemVer tags only: builds the Docker image via Buildah and publishes it with rolling tags The Docker image build is **not** run on regular pushes. All quality gates (tests, lint, type-check, production build) already run in the test jobs. The Dockerfile itself changes rarely, and any breakage surfaces at the next tagged release. This avoids a redundant image build on every push. ### Verification: - Push a commit → pipeline runs tests only, no image build - Push a non-SemVer tag → same behavior - Push a SemVer tag (e.g. `1.0.0`) → tests + build + publish with 4 tags ### Pipeline Flow: ``` git push (any branch/tag) │ ┌───────────┴───────────┐ ▼ ▼ ┌─────────────────┐ ┌─────────────────────┐ │ backend-test │ │ frontend-test │ │ │ │ │ │ JDK 25 │ │ Node 24 │ │ ./mvnw -B │ │ npm ci │ │ verify │ │ npm run lint │ │ │ │ npm run type-check │ │ │ │ npm run test:unit │ │ │ │ -- --run │ │ │ │ npm run build │ └────────┬─────────┘ └──────────┬───────────┘ │ │ └───────────┬─────────────┘ │ ▼ ┌───────────────────┐ │ SemVer-Tag? │── nein ──► DONE └────────┬──────────┘ │ ja ▼ ┌─────────────────────┐ │ build-and-publish │ │ │ │ buildah bud │ │ buildah tag ×4 │ │ buildah push ×4 │ └─────────────────────┘ ``` ## What We're NOT Doing - No deployment automation (how/where to deploy is the hoster's responsibility) - No branch protection rules (Gitea admin concern, not pipeline scope) - No automated versioning (no release-please, no semantic-release — manual `git tag`) - No caching optimization (can be added later if runner time becomes a concern) - No separate staging/production pipelines - No notifications (Slack, email, etc.) ## Implementation Approach Single phase — this is one YAML file with well-defined structure. The workflow uses Buildah for image builds to avoid Docker-in-Docker issues on the self-hosted runner. ## Phase 1: Gitea Actions Workflow ### Overview Create the complete CI/CD workflow file with three jobs: backend-test, frontend-test, build-and-publish (SemVer tags only). ### Changes Required: #### [x] 1. Create workflow directory **Action**: `mkdir -p .gitea/workflows/` #### [x] 2. Explore OpenAPI spec access in CI The `npm run type-check` and `npm run build` steps need access to the OpenAPI spec because `generate:api` runs as a pre-step. In CI, the checkout includes both `backend/` and `frontend/` as sibling directories, so the relative path `../backend/src/main/resources/openapi/api.yaml` from `frontend/` should resolve correctly. **Task:** During implementation, verify whether the relative path works from the CI checkout structure. If it does, no `cp` step is needed. If it doesn't, add an explicit copy step. The workflow YAML below includes a `cp` as a safety measure — **remove it if the relative path works without it**. **Resolution:** The relative path works. The `cp` was removed. An explicit `generate:api` step was added before `type-check` so that `schema.d.ts` exists when `vue-tsc` runs (since `type-check` alone doesn't trigger code generation). #### [x] 3. Create workflow file **File**: `.gitea/workflows/ci.yaml` ```yaml name: CI on: push: jobs: backend-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: temurin java-version: 25 - name: Run backend verify run: cd backend && ./mvnw -B verify frontend-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node 24 uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: cd frontend && npm ci - name: Lint run: cd frontend && npm run lint - name: Type check run: | cp backend/src/main/resources/openapi/api.yaml frontend/ cd frontend && npm run type-check - name: Unit tests run: cd frontend && npm run test:unit -- --run - name: Production build run: cd frontend && npm run build build-and-publish: needs: [backend-test, frontend-test] if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Parse SemVer tag id: semver run: | TAG="${{ github.ref_name }}" if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Not a valid SemVer tag: $TAG" exit 1 fi MAJOR="${TAG%%.*}" MINOR="${TAG%.*}" echo "full=$TAG" >> "$GITHUB_OUTPUT" echo "minor=$MINOR" >> "$GITHUB_OUTPUT" echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - name: Build image run: | REGISTRY="${{ github.server_url }}" REGISTRY="${REGISTRY#https://}" REGISTRY="${REGISTRY#http://}" REPO="${{ github.repository }}" IMAGE="${REGISTRY}/${REPO}" buildah bud -t "${IMAGE}:${{ steps.semver.outputs.full }}" . buildah tag "${IMAGE}:${{ steps.semver.outputs.full }}" \ "${IMAGE}:${{ steps.semver.outputs.minor }}" \ "${IMAGE}:${{ steps.semver.outputs.major }}" \ "${IMAGE}:latest" echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" - name: Push to registry run: | buildah login -u "${{ github.repository_owner }}" \ -p "${{ secrets.REGISTRY_TOKEN }}" \ "${IMAGE%%/*}" buildah push "${IMAGE}:${{ steps.semver.outputs.full }}" buildah push "${IMAGE}:${{ steps.semver.outputs.minor }}" buildah push "${IMAGE}:${{ steps.semver.outputs.major }}" buildah push "${IMAGE}:latest" ``` ### Success Criteria: #### Automated Verification: - [x] YAML is valid: `python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/ci.yaml'))"` - [x] File is in the correct directory: `.gitea/workflows/ci.yaml` - [x] Workflow triggers on `push` (all branches and tags) - [x] `backend-test` uses JDK 25 and runs `./mvnw -B verify` - [x] `frontend-test` uses Node 24 and runs lint, type-check, tests, and build - [x] `build-and-publish` depends on both test jobs (`needs: [backend-test, frontend-test]`) - [x] `build-and-publish` only runs on SemVer tags (`if` condition) - [x] SemVer parsing correctly extracts major, minor, and full version - [x] Registry URL is derived from `github.server_url` with protocol stripped - [x] Authentication uses `secrets.REGISTRY_TOKEN` (not the built-in token) #### Manual Verification: - [ ] Push a commit to a branch → pipeline runs `backend-test` and `frontend-test` only — no image build - [ ] Push a SemVer tag → pipeline runs all three jobs, image appears in Gitea container registry with 4 tags - [ ] Break a test intentionally → pipeline fails, `build-and-publish` does not run - [ ] Push a non-SemVer tag → pipeline runs tests only, no image build **Implementation Note**: After creating the workflow file and passing automated verification, the manual verification requires pushing to the actual Gitea instance. Pause here for the human to test on the real runner. --- ## Testing Strategy ### Automated (pre-push): - YAML syntax validation - Verify file structure and job dependencies match the spec ### Manual (post-push, on Gitea): 1. Push a normal commit → verify only test jobs run, no image build 2. Intentionally break a backend test → verify pipeline fails at `backend-test` 3. Intentionally break a frontend lint rule → verify pipeline fails at `frontend-test` 4. Fix and push a SemVer tag (e.g. `0.1.0`) → verify all 3 jobs run, image published with rolling tags 5. Verify image is pullable: `docker pull {registry}/{owner}/fete:0.1.0` ## Performance Considerations - Backend and frontend tests run in parallel (separate jobs) — this is the main time saver - Docker image build only runs on SemVer tags — no wasted runner time on regular pushes - No Maven/npm caching configured — can be added later if runner time becomes a problem ## Configuration Prerequisites The following must be configured in Gitea **before** the pipeline can publish images: 1. **Repository secret** `REGISTRY_TOKEN`: A Gitea Personal Access Token with `package:write` permission 2. **Buildah** must be installed on the runner (standard on most Linux runners) ## References - Research: `docs/agents/research/2026-03-04-t3-cicd-pipeline.md` - Spec: `spec/setup-tasks.md:41-55` - Dockerfile: `Dockerfile:1-26` - Frontend scripts: `frontend/package.json:6-17` - Backend plugins: `backend/pom.xml:56-168`