All manual verification passed: - Branch push: tests only, no image build - Non-SemVer tag: tests only, no image build - SemVer tag (0.0.1): all jobs green, 4 tags in Gitea registry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
date, git_commit, branch, topic, tags, status
| date | git_commit | branch | topic | tags | status | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| 2026-03-04T18:46:12.266203+00:00 | 316137bf1c |
master | T-3: CI/CD Pipeline — Gitea Actions |
|
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 .dockerignorealready 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:
- Triggers on every push (branches and tags)
- Runs backend and frontend quality gates in parallel
- 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
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:
- YAML is valid:
python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/ci.yaml'))" - File is in the correct directory:
.gitea/workflows/ci.yaml - Workflow triggers on
push(all branches and tags) backend-testuses JDK 25 and runs./mvnw -B verifyfrontend-testuses Node 24 and runs lint, type-check, tests, and buildbuild-and-publishdepends on both test jobs (needs: [backend-test, frontend-test])build-and-publishonly runs on SemVer tags (ifcondition)- SemVer parsing correctly extracts major, minor, and full version
- Registry URL is derived from
github.server_urlwith protocol stripped - Authentication uses
secrets.REGISTRY_TOKEN(not the built-in token)
Manual Verification:
- Push a commit to a branch → pipeline runs
backend-testandfrontend-testonly — 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-publishdoes not run (skipped — guaranteed byneedsdependency, verified implicitly) - 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):
- Push a normal commit → verify only test jobs run, no image build
- Intentionally break a backend test → verify pipeline fails at
backend-test - Intentionally break a frontend lint rule → verify pipeline fails at
frontend-test - Fix and push a SemVer tag (e.g.
0.1.0) → verify all 3 jobs run, image published with rolling tags - 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:
- Repository secret
REGISTRY_TOKEN: A Gitea Personal Access Token withpackage:writepermission - Docker must be available on the runner (act_runner provides this via socket forwarding)
Addendum: Buildah → Docker pivot
Buildah was the original choice to avoid Docker-in-Docker issues. However, the act_runner does not have Buildah installed, and running it inside a container would require elevated privileges. Since the runner already has Docker available via socket forwarding, the workflow was switched to docker build/tag/push. This is not classic DinD — it uses the host Docker daemon directly.
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