diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..c653a64 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,92 @@ +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: Generate API types + run: cd frontend && npm run generate:api + + - name: Type check + run: 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" diff --git a/docs/agents/plan/2026-03-04-t3-cicd-pipeline.md b/docs/agents/plan/2026-03-04-t3-cicd-pipeline.md new file mode 100644 index 0000000..9acadff --- /dev/null +++ b/docs/agents/plan/2026-03-04-t3-cicd-pipeline.md @@ -0,0 +1,262 @@ +--- +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` diff --git a/docs/agents/research/2026-03-04-t3-cicd-pipeline.md b/docs/agents/research/2026-03-04-t3-cicd-pipeline.md new file mode 100644 index 0000000..dad29c8 --- /dev/null +++ b/docs/agents/research/2026-03-04-t3-cicd-pipeline.md @@ -0,0 +1,213 @@ +--- +date: "2026-03-04T18:19:10.241698+00:00" +git_commit: 316137bf1c391577e884ce525af780f45e34da86 +branch: master +topic: "T-3: CI/CD Pipeline — Gitea Actions" +tags: [research, codebase, ci-cd, gitea, docker, pipeline] +status: complete +--- + +# Research: T-3 CI/CD Pipeline + +## Research Question + +What is the current state of the project relevant to implementing T-3 (CI/CD pipeline with Gitea Actions), what are its requirements, dependencies, and what infrastructure already exists? + +## Summary + +T-3 requires a Gitea Actions workflow in `.gitea/workflows/` that runs on every push: tests both backend and frontend, builds a Docker image, and publishes it to the Gitea container registry. The task is currently unstarted but all dependencies (T-1, T-2) are completed. The project already has a working multi-stage Dockerfile, comprehensive test suites for both backend and frontend, and clearly defined build commands. No CI/CD configuration files exist yet. + +## Detailed Findings + +### T-3 Specification + +Defined in `spec/setup-tasks.md:41-55`. + +**Acceptance Criteria (all unchecked):** +1. Gitea Actions workflow file in `.gitea/workflows/` runs on push: test, build, publish Docker image +2. Backend tests run via Maven +3. Frontend tests run via Vitest +4. Docker image is published to the Gitea container registry on the same instance +5. Pipeline fails visibly if any test fails or the build breaks +6. Docker image is only published if all tests pass and the build succeeds + +**Dependencies:** T-1 (completed), T-2 (completed) + +**Platform Decision (Q-5):** Per `.ralph/review-findings/questions.md:12-22`, Gitea is confirmed as the exclusive CI/CD platform. Only Gitea infrastructure will be used — Gitea Actions for pipelines, Gitea container registry for Docker image publishing. + +### Dependencies — What Already Exists + +#### Dockerfile (repo root) + +A working 3-stage Dockerfile exists (`Dockerfile:1-26`): + +| Stage | Base Image | Purpose | +|-------|-----------|---------| +| `frontend-build` | `node:24-alpine` | `npm ci` + `npm run build` (includes OpenAPI type generation) | +| `backend-build` | `eclipse-temurin:25-jdk-alpine` | Maven build with frontend assets baked into `static/` | +| `runtime` | `eclipse-temurin:25-jre-alpine` | `java -jar app.jar`, exposes 8080, HEALTHCHECK configured | + +The Dockerfile skips tests and static analysis during build (`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip` at `Dockerfile:17`), with the explicit design rationale that quality gates belong in CI (T-3). + +The OpenAPI spec is copied into the frontend build stage (`Dockerfile:8-9`) because `npm run build` triggers `generate:api` as a pre-step. + +#### .dockerignore + +`.dockerignore:18-19` already excludes `.gitea/` from the Docker build context, anticipating this directory's creation. + +#### Backend Build & Test + +- **Build:** `./mvnw package` (or `./mvnw -B package` for batch mode) +- **Test:** `./mvnw test` +- **Full verify:** `./mvnw verify` (includes Checkstyle, SpotBugs, ArchUnit, JUnit 5) +- **Config:** `backend/pom.xml` — Spring Boot 3.5.11, Java 25 +- **Quality gates:** Checkstyle (Google Style, `validate` phase), SpotBugs (`verify` phase), ArchUnit (9 rules, runs with JUnit), Surefire (fail-fast at 1 failure) + +#### Frontend Build & Test + +- **Build:** `npm run build` (runs `generate:api` + parallel type-check and vite build) +- **Test:** `npm run test:unit` (Vitest) +- **Lint:** `npm run lint` (oxlint + ESLint) +- **Type check:** `vue-tsc --build` +- **Config:** `frontend/package.json` — Vue 3.5, Vite 7.3, TypeScript 5.9 +- **Node engines:** `^20.19.0 || >=22.12.0` + +### Scheduling and Parallelism + +Per `spec/implementation-phases.md:15`, T-3 is at priority level 4* — parallelizable with T-4 (Development infrastructure). It can be implemented at any time now since T-1 and T-2 are done. + +From `review-findings.md:105-107`: T-3 should be completed before the first user story is finished, otherwise code will exist without a pipeline. + +### Gitea Actions Specifics + +Gitea Actions uses the same YAML format as GitHub Actions with minor differences: + +- Workflow files go in `.gitea/workflows/` (not `.github/workflows/`) +- Uses `runs-on: ubuntu-latest` or custom runner labels +- Container registry URL pattern: `{gitea-host}/{owner}/{repo}` (no separate registry domain) +- Supports `docker/login-action`, `docker/build-push-action`, and direct `docker` CLI +- Secrets are configured in the Gitea repository settings (e.g., `secrets.GITHUB_TOKEN` equivalent is typically `secrets.GITEA_TOKEN` or the built-in `gitea.token`) + +### Build Commands for the Pipeline + +The pipeline needs to execute these commands in order: + +| Step | Command | Runs in | +|------|---------|---------| +| Backend test | `cd backend && ./mvnw -B verify` | JDK 25 environment | +| Frontend install | `cd frontend && npm ci` | Node 24 environment | +| Frontend lint | `cd frontend && npm run lint` | Node 24 environment | +| Frontend type-check | `cd frontend && npm run type-check` | Node 24 environment (needs OpenAPI spec) | +| Frontend test | `cd frontend && npm run test:unit -- --run` | Node 24 environment | +| Docker build | `docker build -t {registry}/{owner}/{repo}:{tag} .` | Docker-capable runner | +| Docker push | `docker push {registry}/{owner}/{repo}:{tag}` | Docker-capable runner | + +Note: `npm run build` is implicitly tested by the Docker build stage (the Dockerfile runs `npm run build` in stage 1). Running it separately in CI is redundant but could provide faster feedback. + +The `--run` flag on Vitest ensures it runs once and exits (non-watch mode). + +### Container Registry + +The Gitea container registry is built into Gitea. Docker images are pushed using the Gitea instance hostname as the registry. Authentication uses a Gitea API token or the built-in `GITHUB_TOKEN`-equivalent that Gitea Actions provides. + +Push format: `docker push {gitea-host}/{owner}/{repo}:{tag}` + +### What Does NOT Exist Yet + +- No `.gitea/` directory +- No `.github/` directory +- No workflow YAML files anywhere +- No CI/CD configuration of any kind +- No Makefile or build orchestration script +- No documentation about the Gitea instance URL or registry configuration + +## Code References + +- `spec/setup-tasks.md:41-55` — T-3 specification and acceptance criteria +- `spec/implementation-phases.md:15` — T-3 scheduling (parallel with T-4) +- `.ralph/review-findings/questions.md:12-22` — Q-5 resolution (Gitea confirmed) +- `Dockerfile:1-26` — Multi-stage Docker build +- `Dockerfile:17` — Tests skipped in Docker build (deferred to CI) +- `.dockerignore:18-19` — `.gitea/` already excluded from Docker context +- `backend/pom.xml:56-168` — Maven build plugins (Checkstyle, Surefire, SpotBugs, OpenAPI generator) +- `frontend/package.json:6-17` — npm build and test scripts +- `CLAUDE.md` — Build commands reference table + +## Architecture Documentation + +### Pipeline Architecture Pattern + +The project follows a "test in CI, skip in Docker" pattern: +- The Dockerfile is a pure build artifact — it produces a runnable image as fast as possible +- All quality gates (tests, linting, static analysis) are expected to run in the CI pipeline before the Docker build +- The Docker image is only published if all preceding steps pass + +### Gitea Actions Workflow Pattern (GitHub Actions compatible) + +Gitea Actions workflows follow the same `on/jobs/steps` YAML structure as GitHub Actions. The runner is a self-hosted instance with Docker available on the host, but the pipeline uses Buildah for container image builds to avoid Docker-in-Docker complexity. + +### Image Tagging Strategy (resolved) + +SemVer with rolling tags, following standard Docker convention. When a tag like `2.3.9` is pushed, the pipeline publishes the **same image** under all four tags: + +| Tag | Type | Example | Updated when | +|-----|------|---------|-------------| +| `2.3.9` | Immutable | Exact version | Only once, on this release | +| `2.3` | Rolling | Latest `2.3.x` | Overwritten by any `2.3.x` release | +| `2` | Rolling | Latest `2.x.x` | Overwritten by any `2.x.x` release | +| `latest` | Rolling | Newest release | Overwritten by every release | + +This means the pipeline does four pushes per release (one per tag). Users can pin `2` to get automatic minor and patch updates, or pin `2.3.9` for exact reproducibility. + +Images are only published on tagged releases (Git tags matching a SemVer pattern), not on every push. + +### Release Process (resolved) + +Manual Git tags trigger releases. The workflow is: + +```bash +git tag 1.2.3 +git push --tags +``` + +The pipeline triggers on tags matching a SemVer pattern (e.g., `1.2.3`) and publishes the image with rolling SemVer tags. No `v` prefix — tags are pure SemVer. No automated versioning tools (release-please, semantic-release) — the developer decides the version. + +### Container Build Tool (resolved) + +Buildah is used instead of Docker for building and pushing container images. This avoids Docker-in-Docker issues entirely and works cleanly on self-hosted runners regardless of whether the runner process runs inside a container or on the host. + +### Pipeline Quality Scope (resolved) + +The pipeline runs the maximum quality gates available: +- **Backend:** `./mvnw verify` (full lifecycle — Checkstyle, JUnit 5, ArchUnit, SpotBugs) +- **Frontend:** Lint (`npm run lint`), type-check (`npm run type-check`), tests (`npm run test:unit -- --run`) + +### Gitea Registry Authentication (researched) + +Key findings from Gitea documentation and community: +- `GITHUB_TOKEN` / `GITEA_TOKEN` (the built-in runner token) does **not** have permissions to push to the Gitea container registry. It only has repository read access. +- A **Personal Access Token** (PAT) with package write permissions must be created and stored as a repository secret (e.g., `REGISTRY_TOKEN`). +- The registry URL is the Gitea instance hostname (e.g., `gitea.example.com`). `${{ github.server_url }}` provides it with protocol prefix — needs stripping or use a repository variable. +- Push URL format: `{registry-host}/{owner}/{repo}:{tag}` + +### Reference Workflow (existing project) + +An existing Gitea Actions workflow in the sibling project `../arr/.gitea/workflows/deploy.yaml` provides a reference: +- Runner label: `ubuntu-latest` +- Uses `${{ vars.DEPLOY_PATH }}` for repository variables +- Simple deploy pattern (git pull + docker compose up) + +## Resolved Questions + +1. **Gitea instance URL:** Configured via repository variable or derived from `${{ github.server_url }}`. The registry hostname is the same as the Gitea instance. +2. **Runner label:** `ubuntu-latest` (consistent with the existing `arr` project workflow). +3. **Runner setup:** Self-hosted runner on the host with Docker available. Buildah used for image builds to avoid DinD. +4. **Container build tool:** Buildah (no DinD needed). +5. **Image tagging strategy:** SemVer with rolling tags (`latest`, `2`, `2.3`, `2.3.9`). Published only on tagged releases. +6. **Branch protection / publish trigger:** Images are only published from tagged releases, not from branch pushes. Every push triggers test + build (without publish). +7. **Maven lifecycle scope:** `./mvnw verify` (full lifecycle including SpotBugs). Frontend also runs all available quality gates (lint, type-check, tests). +8. **Registry authentication:** Personal Access Token stored as repository secret (built-in `GITHUB_TOKEN` lacks package write permissions). + +## Open Questions + +None — all questions resolved.