Files
fete/specs/003-cicd-pipeline/plan.md
nitrix 6aeb4b8bca Migrate project artifacts to spec-kit format
- Move cross-cutting docs (personas, design system, implementation phases,
  Ideen.md) to .specify/memory/
- Move cross-cutting research and plans to .specify/memory/research/ and
  .specify/memory/plans/
- Extract 5 setup tasks from spec/setup-tasks.md into individual
  specs/001-005/spec.md files with spec-kit template format
- Extract 20 user stories from spec/userstories.md into individual
  specs/006-026/spec.md files with spec-kit template format
- Relocate feature-specific research and plan docs into specs/[feature]/
- Add spec-kit constitution, templates, scripts, and slash commands
- Slim down CLAUDE.md to Claude-Code-specific config, delegate principles
  to .specify/memory/constitution.md
- Update ralph.sh with stream-json output and per-iteration logging
- Delete old spec/ and docs/agents/ directories
- Gitignore Ralph iteration JSONL logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:19:41 +01:00

11 KiB
Raw Blame History

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
plan
ci-cd
gitea
docker
buildah
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

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-test uses JDK 25 and runs ./mvnw -B verify
  • frontend-test uses Node 24 and runs lint, type-check, tests, and build
  • build-and-publish depends on both test jobs (needs: [backend-test, frontend-test])
  • build-and-publish only runs on SemVer tags (if condition)
  • SemVer parsing correctly extracts major, minor, and full version
  • Registry URL is derived from github.server_url with protocol stripped
  • 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 (skipped — guaranteed by needs dependency, 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):

  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. 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