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>
267 lines
11 KiB
Markdown
267 lines
11 KiB
Markdown
---
|
||
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:
|
||
- [x] Push a commit to a branch → pipeline runs `backend-test` and `frontend-test` only — no image build
|
||
- [x] 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)
|
||
- [x] 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`
|