Compare commits
4 Commits
958222d0c0
...
0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| a7303aa859 | |||
| 977795ffb1 | |||
| 316137bf1c | |||
| 96ef8656bd |
38
.dockerignore
Normal file
38
.dockerignore
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Build artifacts
|
||||||
|
**/target/
|
||||||
|
**/dist/
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Dependencies (rebuilt inside Docker)
|
||||||
|
**/node_modules/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
**/*.iml
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.gitea/
|
||||||
|
|
||||||
|
# Agent/dev files
|
||||||
|
.claude/
|
||||||
|
.ralph/
|
||||||
|
.rodney/
|
||||||
|
.agent-tests/
|
||||||
|
docs/
|
||||||
|
spec/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Generated files (rebuilt in Docker)
|
||||||
|
frontend/src/api/schema.d.ts
|
||||||
89
.gitea/workflows/ci.yaml
Normal file
89
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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}"
|
||||||
|
docker build -t "${IMAGE}:${{ steps.semver.outputs.full }}" .
|
||||||
|
docker tag "${IMAGE}:${{ steps.semver.outputs.full }}" "${IMAGE}:${{ steps.semver.outputs.minor }}"
|
||||||
|
docker tag "${IMAGE}:${{ steps.semver.outputs.full }}" "${IMAGE}:${{ steps.semver.outputs.major }}"
|
||||||
|
docker tag "${IMAGE}:${{ steps.semver.outputs.full }}" "${IMAGE}:latest"
|
||||||
|
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login -u "${{ github.repository_owner }}" --password-stdin "${IMAGE%%/*}"
|
||||||
|
docker push "${IMAGE}:${{ steps.semver.outputs.full }}"
|
||||||
|
docker push "${IMAGE}:${{ steps.semver.outputs.minor }}"
|
||||||
|
docker push "${IMAGE}:${{ steps.semver.outputs.major }}"
|
||||||
|
docker push "${IMAGE}:latest"
|
||||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Stage 1: Build frontend
|
||||||
|
FROM node:24-alpine AS frontend-build
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
# OpenAPI spec needed for type generation (npm run build runs generate:api)
|
||||||
|
COPY backend/src/main/resources/openapi/api.yaml \
|
||||||
|
../backend/src/main/resources/openapi/api.yaml
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Build backend with frontend assets baked in
|
||||||
|
FROM eclipse-temurin:25-jdk-alpine AS backend-build
|
||||||
|
WORKDIR /app/backend
|
||||||
|
COPY backend/ ./
|
||||||
|
COPY --from=frontend-build /app/frontend/dist src/main/resources/static/
|
||||||
|
RUN ./mvnw -B -DskipTests -Dcheckstyle.skip -Dspotbugs.skip package
|
||||||
|
|
||||||
|
# Stage 3: Runtime
|
||||||
|
FROM eclipse-temurin:25-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=backend-build /app/backend/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
40
backend/src/main/java/de/fete/config/WebConfig.java
Normal file
40
backend/src/main/java/de/fete/config/WebConfig.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package de.fete.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||||
|
|
||||||
|
/** Configures API path prefix and SPA static resource serving. */
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||||
|
configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(RestController.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.resourceChain(true)
|
||||||
|
.addResolver(new PathResourceResolver() {
|
||||||
|
@Override
|
||||||
|
protected Resource getResource(String resourcePath,
|
||||||
|
Resource location) throws IOException {
|
||||||
|
Resource requested = location.createRelative(resourcePath);
|
||||||
|
if (requested.exists() && requested.isReadable()) {
|
||||||
|
return requested;
|
||||||
|
}
|
||||||
|
Resource index = new ClassPathResource("/static/index.html");
|
||||||
|
return (index.exists() && index.isReadable()) ? index : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
spring.application.name=fete
|
spring.application.name=fete
|
||||||
server.servlet.context-path=/api
|
|
||||||
|
|
||||||
management.endpoints.web.exposure.include=health
|
management.endpoints.web.exposure.include=health
|
||||||
management.endpoint.health.show-details=never
|
management.endpoint.health.show-details=never
|
||||||
|
|||||||
33
backend/src/test/java/de/fete/config/WebConfigTest.java
Normal file
33
backend/src/test/java/de/fete/config/WebConfigTest.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package de.fete.config;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
class WebConfigTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void actuatorHealthIsOutsideApiPrefix() throws Exception {
|
||||||
|
mockMvc.perform(get("/actuator/health"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value("UP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void apiPrefixNotAccessibleWithoutIt() throws Exception {
|
||||||
|
// /health without /api prefix should not resolve to the API endpoint
|
||||||
|
mockMvc.perform(get("/health"))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
}
|
||||||
366
docs/agents/plan/2026-03-04-t2-docker-deployment.md
Normal file
366
docs/agents/plan/2026-03-04-t2-docker-deployment.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
---
|
||||||
|
date: 2026-03-04T17:40:16+00:00
|
||||||
|
git_commit: 96ef8656bd87032696ae82198620d99f20d80d3b
|
||||||
|
branch: master
|
||||||
|
topic: "T-2: Docker Deployment Setup"
|
||||||
|
tags: [plan, docker, deployment, spring-boot, spa]
|
||||||
|
status: draft
|
||||||
|
---
|
||||||
|
|
||||||
|
# T-2: Docker Deployment Setup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Create a multi-stage Dockerfile that builds both backend (Spring Boot) and frontend (Vue 3) and produces a single runnable container. Spring Boot serves the SPA's static files directly — one process, one port, one JAR. This requires migrating from `server.servlet.context-path=/api` to `addPathPrefix` and adding SPA forwarding so Vue Router's history mode works.
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
**What exists:**
|
||||||
|
- Backend: Spring Boot 3.5.11, Java 25, Maven with wrapper, Actuator health endpoint
|
||||||
|
- Frontend: Vue 3, Vite, TypeScript, Vue Router (history mode), openapi-fetch client
|
||||||
|
- `server.servlet.context-path=/api` scopes everything under `/api` (including static resources)
|
||||||
|
- Health test at `backend/src/test/java/de/fete/FeteApplicationTest.java:27` uses `get("/actuator/health")`
|
||||||
|
- OpenAPI spec at `backend/src/main/resources/openapi/api.yaml` has `servers: [{url: /api}]`
|
||||||
|
- Frontend client at `frontend/src/api/client.ts` uses `baseUrl: "/api"`
|
||||||
|
- ArchUnit registers `de.fete.config..` as an adapter package — config classes belong there
|
||||||
|
- `package-lock.json` exists (needed for `npm ci`)
|
||||||
|
- Maven wrapper `.properties` present (JAR downloads automatically)
|
||||||
|
- No Dockerfile, no `.dockerignore` exist yet
|
||||||
|
|
||||||
|
**What's missing:**
|
||||||
|
- Dockerfile (multi-stage build)
|
||||||
|
- `.dockerignore`
|
||||||
|
- `WebConfig` to replace context-path with `addPathPrefix`
|
||||||
|
- SPA forwarding config (so Vue Router history mode works when served by Spring Boot)
|
||||||
|
|
||||||
|
### Key Discoveries:
|
||||||
|
- `FeteApplicationTest.java:27`: MockMvc test uses `get("/actuator/health")` — path is relative to context path in MockMvc, so removing context-path does NOT break this test
|
||||||
|
- `api.yaml:11`: OpenAPI `servers: [{url: /api}]` — stays unchanged, `addPathPrefix` produces the same routing
|
||||||
|
- `client.ts:4`: `baseUrl: "/api"` — stays unchanged
|
||||||
|
- `HexagonalArchitectureTest.java:22`: `config` is an adapter in ArchUnit → new config classes go in `de.fete.config`
|
||||||
|
- `router/index.ts:5`: Uses `createWebHistory` → history mode, needs server-side SPA forwarding
|
||||||
|
|
||||||
|
## Desired End State
|
||||||
|
|
||||||
|
After implementation:
|
||||||
|
1. `docker build -t fete .` succeeds at the repo root
|
||||||
|
2. `docker run -p 8080:8080 fete` starts the container
|
||||||
|
3. `curl http://localhost:8080/actuator/health` returns `{"status":"UP"}`
|
||||||
|
4. `curl http://localhost:8080/` returns the Vue SPA's `index.html`
|
||||||
|
5. `curl http://localhost:8080/some/spa/route` also returns `index.html` (SPA forwarding)
|
||||||
|
6. `curl http://localhost:8080/assets/...` returns actual static files
|
||||||
|
7. All existing tests still pass (`./mvnw test` and `npm run test:unit`)
|
||||||
|
|
||||||
|
## What We're NOT Doing
|
||||||
|
|
||||||
|
- Database wiring (`DATABASE_URL`, JPA, Flyway) — deferred to T-4
|
||||||
|
- docker-compose example — deferred to T-4
|
||||||
|
- Environment variable configuration (Unsplash key, max events) — deferred to T-4
|
||||||
|
- README deployment documentation — deferred to T-4
|
||||||
|
- Production hardening (JVM flags, memory limits, graceful shutdown) — not in scope
|
||||||
|
- TLS/HTTPS — hoster's responsibility (reverse proxy)
|
||||||
|
- gzip compression config — premature optimization at this scale
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
Three phases, strictly sequential: first the Spring Boot config changes (testable locally without Docker), then the Dockerfile and `.dockerignore`, then end-to-end verification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Spring Boot Configuration Changes
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Replace `context-path=/api` with `addPathPrefix` so that API endpoints live under `/api/*` but static resources and SPA routes are served at `/`. Add SPA forwarding so non-API, non-static requests return `index.html`.
|
||||||
|
|
||||||
|
### Changes Required:
|
||||||
|
|
||||||
|
#### [x] 1. Remove context-path from application.properties
|
||||||
|
**File**: `backend/src/main/resources/application.properties`
|
||||||
|
**Changes**: Remove `server.servlet.context-path=/api`. Explicitly set Actuator base path to keep it outside `/api`.
|
||||||
|
|
||||||
|
```properties
|
||||||
|
spring.application.name=fete
|
||||||
|
|
||||||
|
management.endpoints.web.exposure.include=health
|
||||||
|
management.endpoint.health.show-details=never
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Actuator defaults to `/actuator` which is exactly where we want it (outside `/api`). No extra config needed.
|
||||||
|
|
||||||
|
#### [x] 2. Create WebConfig with addPathPrefix
|
||||||
|
**File**: `backend/src/main/java/de/fete/config/WebConfig.java`
|
||||||
|
**Changes**: New `@Configuration` class that prefixes only `@RestController` handlers with `/api`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package de.fete.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** Configures API path prefix and SPA static resource serving. */
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||||
|
configurer.addPathPrefix("/api",
|
||||||
|
c -> c.isAnnotationPresent(RestController.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.resourceChain(true)
|
||||||
|
.addResolver(new PathResourceResolver() {
|
||||||
|
@Override
|
||||||
|
protected Resource getResource(String resourcePath,
|
||||||
|
Resource location) throws IOException {
|
||||||
|
Resource requested = location.createRelative(resourcePath);
|
||||||
|
return (requested.exists() && requested.isReadable())
|
||||||
|
? requested
|
||||||
|
: new ClassPathResource("/static/index.html");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This single class handles both concerns:
|
||||||
|
- `configurePathMatch`: prefixes `@RestController` endpoints with `/api`
|
||||||
|
- `addResourceHandlers`: serves static files from `classpath:/static/`, falls back to `index.html` for SPA routes
|
||||||
|
|
||||||
|
The SPA forwarding only activates when `classpath:/static/index.html` exists (i.e. in the Docker image where the frontend is bundled). During local backend development without frontend assets, requests to `/` will simply 404 as before — no behavior change for the dev workflow.
|
||||||
|
|
||||||
|
#### [x] 3. Write test for WebConfig behavior
|
||||||
|
**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java`
|
||||||
|
**Changes**: New test class verifying the `/api` prefix routing and actuator independence.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package de.fete.config;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
class WebConfigTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void actuatorHealthIsOutsideApiPrefix() throws Exception {
|
||||||
|
mockMvc.perform(get("/actuator/health"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value("UP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void apiPrefixNotAccessibleWithoutIt() throws Exception {
|
||||||
|
// /health without /api prefix should not resolve to the API endpoint
|
||||||
|
mockMvc.perform(get("/health"))
|
||||||
|
.andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The existing `FeteApplicationTest.healthEndpointReturns200()` already tests `/actuator/health`. Since we're removing context-path but MockMvc paths are relative to the servlet context anyway, the existing test continues to pass without changes. The new test adds explicit verification that the `/api` prefix isolation works correctly.
|
||||||
|
|
||||||
|
### Success Criteria:
|
||||||
|
|
||||||
|
#### Automated Verification:
|
||||||
|
- [x] `cd backend && ./mvnw test` — all tests pass (existing + new)
|
||||||
|
- [x] `cd backend && ./mvnw checkstyle:check` — no style violations
|
||||||
|
|
||||||
|
#### Manual Verification:
|
||||||
|
- [x] `cd backend && ./mvnw spring-boot:run`, then `curl http://localhost:8080/actuator/health` returns `{"status":"UP"}`
|
||||||
|
|
||||||
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Dockerfile and .dockerignore
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Create the multi-stage Dockerfile (frontend build → backend build with static assets → JRE runtime) and a `.dockerignore` to keep the build context clean.
|
||||||
|
|
||||||
|
### Changes Required:
|
||||||
|
|
||||||
|
#### [x] 1. Create .dockerignore
|
||||||
|
**File**: `.dockerignore`
|
||||||
|
**Changes**: Exclude build artifacts, dependencies, IDE files, and dev-only files from the Docker build context.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Build artifacts
|
||||||
|
**/target/
|
||||||
|
**/dist/
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Dependencies (rebuilt inside Docker)
|
||||||
|
**/node_modules/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
**/*.iml
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.gitea/
|
||||||
|
|
||||||
|
# Agent/dev files
|
||||||
|
.claude/
|
||||||
|
.ralph/
|
||||||
|
.rodney/
|
||||||
|
.agent-tests/
|
||||||
|
docs/
|
||||||
|
spec/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Generated files (rebuilt in Docker)
|
||||||
|
frontend/src/api/schema.d.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
#### [x] 2. Create multi-stage Dockerfile
|
||||||
|
**File**: `Dockerfile`
|
||||||
|
**Changes**: Three-stage build — frontend, backend, runtime.
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Build frontend
|
||||||
|
FROM node:24-alpine AS frontend-build
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
# OpenAPI spec needed for type generation (npm run build runs generate:api)
|
||||||
|
COPY backend/src/main/resources/openapi/api.yaml \
|
||||||
|
../backend/src/main/resources/openapi/api.yaml
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Build backend with frontend assets baked in
|
||||||
|
FROM eclipse-temurin:25-jdk-alpine AS backend-build
|
||||||
|
WORKDIR /app/backend
|
||||||
|
COPY backend/ ./
|
||||||
|
COPY --from=frontend-build /app/frontend/dist src/main/resources/static/
|
||||||
|
RUN ./mvnw -B -DskipTests -Dcheckstyle.skip -Dspotbugs.skip package
|
||||||
|
|
||||||
|
# Stage 3: Runtime
|
||||||
|
FROM eclipse-temurin:25-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=backend-build /app/backend/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Design decisions:
|
||||||
|
- **Frontend first**: `npm run build` triggers `generate:api` which needs `api.yaml` — copied from the build context
|
||||||
|
- **Layer caching**: `npm ci` before `COPY frontend/` so dependency install is cached unless `package*.json` changes
|
||||||
|
- **`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip`**: Tests run in CI (T-3), not during image build
|
||||||
|
- **JRE-only runtime**: No JDK, no Node, no Maven in the final image
|
||||||
|
- **Alpine images**: Minimal size
|
||||||
|
- **HEALTHCHECK directive**: Docker-native health checking via Actuator
|
||||||
|
|
||||||
|
Note on Java 25: `eclipse-temurin:25-jdk-alpine` / `eclipse-temurin:25-jre-alpine` may not yet be published. If unavailable, fall back to `eclipse-temurin:21-jdk-alpine` / `eclipse-temurin:21-jre-alpine` (current LTS). The Java version in the Dockerfile does not need to match the development Java version exactly — Spring Boot 3.5.x runs on both 21 and 25.
|
||||||
|
|
||||||
|
### Success Criteria:
|
||||||
|
|
||||||
|
#### Automated Verification:
|
||||||
|
- [x] `docker build -t fete .` succeeds without errors
|
||||||
|
|
||||||
|
#### Manual Verification:
|
||||||
|
- [x] `docker run --rm -p 8080:8080 fete` starts the container
|
||||||
|
- [x] `curl http://localhost:8080/actuator/health` returns `{"status":"UP"}`
|
||||||
|
- [x] `curl http://localhost:8080/` returns HTML (the Vue SPA's index.html)
|
||||||
|
- [x] `docker images fete --format '{{.Size}}'` shows a reasonable size (< 400MB)
|
||||||
|
|
||||||
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Finalize and Verify
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Run all existing tests to confirm no regressions, check off T-2 acceptance criteria in the spec, and commit.
|
||||||
|
|
||||||
|
### Changes Required:
|
||||||
|
|
||||||
|
#### [x] 1. Check off T-2 acceptance criteria
|
||||||
|
**File**: `spec/setup-tasks.md`
|
||||||
|
**Changes**: Mark completed acceptance criteria for T-2.
|
||||||
|
|
||||||
|
#### [x] 2. Commit the research report
|
||||||
|
**File**: `docs/agents/research/2026-03-04-spa-springboot-docker-patterns.md`
|
||||||
|
**Changes**: Stage and commit the already-written research report alongside the implementation.
|
||||||
|
|
||||||
|
### Success Criteria:
|
||||||
|
|
||||||
|
#### Automated Verification:
|
||||||
|
- [x] `cd backend && ./mvnw test` — all tests pass
|
||||||
|
- [x] `cd frontend && npm run test:unit -- --run` — all tests pass
|
||||||
|
- [x] `cd backend && ./mvnw verify` — full verification including SpotBugs
|
||||||
|
- [x] `docker build -t fete .` — still builds cleanly
|
||||||
|
|
||||||
|
#### Manual Verification:
|
||||||
|
- [x] Container starts and health check responds
|
||||||
|
- [x] SPA is accessible at `http://localhost:8080/`
|
||||||
|
- [x] All T-2 acceptance criteria in `spec/setup-tasks.md` are checked off
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests:
|
||||||
|
- `WebConfigTest.actuatorHealthIsOutsideApiPrefix()` — actuator accessible at `/actuator/health`
|
||||||
|
- `WebConfigTest.apiPrefixNotAccessibleWithoutIt()` — `/health` without prefix returns 404
|
||||||
|
- Existing `FeteApplicationTest.healthEndpointReturns200()` — regression test (unchanged)
|
||||||
|
|
||||||
|
### Integration Tests:
|
||||||
|
- Docker build succeeds end-to-end
|
||||||
|
- Container starts and serves both API and static content
|
||||||
|
|
||||||
|
### Manual Testing Steps:
|
||||||
|
1. `docker build -t fete .`
|
||||||
|
2. `docker run --rm -p 8080:8080 fete`
|
||||||
|
3. Open `http://localhost:8080/` in browser — should see Vue SPA
|
||||||
|
4. Open `http://localhost:8080/actuator/health` — should see `{"status":"UP"}`
|
||||||
|
5. Navigate to a non-existent SPA route like `http://localhost:8080/events/test` — should still see Vue SPA (SPA forwarding)
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- `server.servlet.context-path=/api` is removed → any external clients that previously called `http://host:8080/api/actuator/health` must now call `http://host:8080/actuator/health`. Since the app is not yet deployed, this has zero impact.
|
||||||
|
- The OpenAPI spec's `servers: [{url: /api}]` is unchanged. The `addPathPrefix` produces identical routing to the old context-path for all `@RestController` endpoints.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Research report: `docs/agents/research/2026-03-04-spa-springboot-docker-patterns.md`
|
||||||
|
- T-2 spec: `spec/setup-tasks.md` (lines 24-37)
|
||||||
|
- Existing health test: `backend/src/test/java/de/fete/FeteApplicationTest.java:26-30`
|
||||||
|
- ArchUnit config adapter: `backend/src/test/java/de/fete/HexagonalArchitectureTest.java:22`
|
||||||
|
- Spring PathMatchConfigurer docs: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/path-matching.html
|
||||||
262
docs/agents/plan/2026-03-04-t3-cicd-pipeline.md
Normal file
262
docs/agents/plan/2026-03-04-t3-cicd-pipeline.md
Normal file
@@ -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`
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Single-Container Deployment: Spring Boot + Vue SPA
|
||||||
|
|
||||||
|
**Date:** 2026-03-04
|
||||||
|
**Context:** T-2 research — how to serve a Spring Boot API and Vue SPA from one Docker container.
|
||||||
|
|
||||||
|
## The Three Approaches
|
||||||
|
|
||||||
|
### Approach A: Spring Boot Serves Static Files (Recommended for fete)
|
||||||
|
|
||||||
|
Frontend `dist/` is built by Vite and copied into Spring Boot's `classpath:/static/` during the Docker build. One JAR serves everything.
|
||||||
|
|
||||||
|
**Pros:** One process, one port, one health check. No process manager. The JAR is the single artifact.
|
||||||
|
|
||||||
|
**Cons:** Embedded Tomcat is not optimized for static files (no `sendfile`, no `gzip_static`). Irrelevant at fete's scale (tens to hundreds of users).
|
||||||
|
|
||||||
|
**Real-world examples:** [jonashackt/spring-boot-vuejs](https://github.com/jonashackt/spring-boot-vuejs), [georgwittberger/spring-boot-embedded-spa-example](https://github.com/georgwittberger/spring-boot-embedded-spa-example), [bootify-io/spring-boot-react-example](https://github.com/bootify-io/spring-boot-react-example).
|
||||||
|
|
||||||
|
### Approach B: nginx + Spring Boot (Two Processes)
|
||||||
|
|
||||||
|
nginx serves static files, proxies `/api` to Spring Boot. Needs supervisord or a wrapper script.
|
||||||
|
|
||||||
|
**Pros:** nginx is battle-tested for static files (gzip, caching, HTTP/2). Clean separation.
|
||||||
|
|
||||||
|
**Cons:** Two processes, supervisord complexity, partial-failure detection issues, larger image.
|
||||||
|
|
||||||
|
**When to use:** High-traffic apps where static file performance matters.
|
||||||
|
|
||||||
|
### Approach C: Separate Containers
|
||||||
|
|
||||||
|
Not relevant — violates the single-container requirement.
|
||||||
|
|
||||||
|
## The Context-Path Problem
|
||||||
|
|
||||||
|
Current config: `server.servlet.context-path=/api`. This scopes **everything** under `/api`, including static resources. Frontend at `classpath:/static/` would be served at `/api/index.html`, not `/`.
|
||||||
|
|
||||||
|
### Solution: Remove context-path, use `addPathPrefix`
|
||||||
|
|
||||||
|
Remove `server.servlet.context-path=/api`. Instead, use Spring's `PathMatchConfigurer`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
@Override
|
||||||
|
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||||
|
configurer.addPathPrefix("/api",
|
||||||
|
c -> c.isAnnotationPresent(RestController.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
- API: `/api/...` (only `@RestController` classes)
|
||||||
|
- Static files: `/` (from `classpath:/static/`)
|
||||||
|
- Actuator: `/actuator/health` (outside `/api` — correct for infra health checks)
|
||||||
|
- SPA forwarding controller (`@Controller`, not `@RestController`) is not prefixed
|
||||||
|
|
||||||
|
OpenAPI spec keeps `servers: [{url: /api}]`. Frontend client keeps `baseUrl: "/api"`. No changes needed.
|
||||||
|
|
||||||
|
## SPA Routing (Vue Router History Mode)
|
||||||
|
|
||||||
|
Vue Router in history mode uses real URL paths. The server must return `index.html` for all paths that aren't API endpoints or actual static files.
|
||||||
|
|
||||||
|
### Recommended: Custom `PathResourceResolver`
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
public class SpaConfig implements WebMvcConfigurer {
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.resourceChain(true)
|
||||||
|
.addResolver(new PathResourceResolver() {
|
||||||
|
@Override
|
||||||
|
protected Resource getResource(String resourcePath,
|
||||||
|
Resource location) throws IOException {
|
||||||
|
Resource requested = location.createRelative(resourcePath);
|
||||||
|
return (requested.exists() && requested.isReadable())
|
||||||
|
? requested
|
||||||
|
: new ClassPathResource("/static/index.html");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Requests flow: `@RestController` match → serve API. No match → resource handler → file exists → serve file. No file → serve `index.html` → Vue Router handles route.
|
||||||
|
|
||||||
|
## Multi-Stage Dockerfile Pattern
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Build frontend
|
||||||
|
FROM node:24-alpine AS frontend-build
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
COPY backend/src/main/resources/openapi/api.yaml ../backend/src/main/resources/openapi/api.yaml
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Build backend (with frontend assets)
|
||||||
|
FROM eclipse-temurin:25-jdk-alpine AS backend-build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backend/ backend/
|
||||||
|
COPY --from=frontend-build /app/frontend/dist backend/src/main/resources/static/
|
||||||
|
WORKDIR /app/backend
|
||||||
|
RUN ./mvnw -B -DskipTests package
|
||||||
|
|
||||||
|
# Stage 3: Runtime
|
||||||
|
FROM eclipse-temurin:25-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=backend-build /app/backend/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Key decisions:
|
||||||
|
- Frontend built first (needs `api.yaml` for type generation)
|
||||||
|
- `npm ci` before `COPY frontend/` for Docker layer caching
|
||||||
|
- Frontend output copied into `backend/src/main/resources/static/` before Maven build → ends up in JAR
|
||||||
|
- `-DskipTests` in Docker build (tests run in CI)
|
||||||
|
- JRE-only final image (no JDK, no Node)
|
||||||
|
- Alpine-based for minimal size
|
||||||
|
|
||||||
|
## Open-Source Consensus
|
||||||
|
|
||||||
|
All surveyed Spring Boot + SPA projects use Approach A. None use supervisord + nginx for small-to-medium apps.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [jonashackt/spring-boot-vuejs](https://github.com/jonashackt/spring-boot-vuejs)
|
||||||
|
- [georgwittberger/spring-boot-embedded-spa-example](https://github.com/georgwittberger/spring-boot-embedded-spa-example)
|
||||||
|
- [Bundling React/Vite with Spring Boot (jessym.com)](https://www.jessym.com/articles/bundling-react-vite-with-spring-boot)
|
||||||
|
- [Spring Framework PathMatchConfigurer docs](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/path-matching.html)
|
||||||
|
- [Docker: Multi-service containers](https://docs.docker.com/engine/containers/multi-service_container/)
|
||||||
213
docs/agents/research/2026-03-04-t3-cicd-pipeline.md
Normal file
213
docs/agents/research/2026-03-04-t3-cicd-pipeline.md
Normal file
@@ -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.
|
||||||
@@ -9,10 +9,10 @@ All setup tasks must complete before any user story work begins. T-3 can run in
|
|||||||
| Order | Task | Depends on | Notes |
|
| Order | Task | Depends on | Notes |
|
||||||
|-------|------|------------|-------|
|
|-------|------|------------|-------|
|
||||||
| 1 | T-1: Initialize monorepo structure | — | Scaffolds empty backend + frontend projects |
|
| 1 | T-1: Initialize monorepo structure | — | Scaffolds empty backend + frontend projects |
|
||||||
| 2 | T-2: Deployment setup (Dockerfile + config) | T-1 | Docker build, DB connection, health check |
|
| 2 | T-5: API-first tooling setup | T-1 | OpenAPI spec, codegen plugins, generated types |
|
||||||
| 2* | T-5: API-first tooling setup | T-1 | OpenAPI spec, codegen plugins, generated types — parallelizable with T-2 |
|
| 3 | T-2: Docker deployment setup | T-1, T-5 | Multi-stage Dockerfile — builds backend + frontend into one container |
|
||||||
| 3 | T-4: Development infrastructure | T-2, T-5 | Migrations, router, test infra — gates all user stories |
|
| 4 | T-4: Development infrastructure | T-2, T-5 | Migrations, DB wiring, router, test infra, docker-compose docs — gates all user stories |
|
||||||
| 3* | T-3: CI/CD pipeline | T-1, T-2 | Parallelizable with T-4. Uses Gitea Actions (per Q-5 resolution) |
|
| 4* | T-3: CI/CD pipeline | T-1, T-2 | Parallelizable with T-4. Uses Gitea Actions (per Q-5 resolution) |
|
||||||
|
|
||||||
## Phase 1: Core Event Flow (Vertical Slice)
|
## Phase 1: Core Event Flow (Vertical Slice)
|
||||||
|
|
||||||
@@ -110,9 +110,10 @@ graph TD
|
|||||||
classDef shell fill:#7f8c8d,stroke:#566566,color:#fff
|
classDef shell fill:#7f8c8d,stroke:#566566,color:#fff
|
||||||
|
|
||||||
%% Phase 0: Infrastructure
|
%% Phase 0: Infrastructure
|
||||||
T1(["T-1: Monorepo"]):::infra --> T2(["T-2: Docker & DB"]):::infra
|
T1(["T-1: Monorepo"]):::infra --> T5(["T-5: API-First Tooling"]):::infra
|
||||||
T1 --> T5(["T-5: API-First Tooling"]):::infra
|
T1 --> T2(["T-2: Docker"]):::infra
|
||||||
T2 --> T4(["T-4: Dev Infra"]):::infra
|
T5 --> T2
|
||||||
|
T2 --> T4(["T-4: Dev Infra + DB"]):::infra
|
||||||
T5 --> T4
|
T5 --> T4
|
||||||
T2 --> T3(["T-3: CI/CD"]):::infra
|
T2 --> T3(["T-3: CI/CD"]):::infra
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,20 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### T-2: Deployment setup (Dockerfile + configuration)
|
### T-2: Docker deployment setup
|
||||||
|
|
||||||
**Description:** Create the Docker-based deployment infrastructure so the app can be built and run as a single container connecting to an external PostgreSQL database.
|
**Description:** Create a multi-stage Dockerfile that builds backend and frontend and produces a single runnable container. This task focuses exclusively on the Docker build — database wiring, environment variable configuration, and docker-compose documentation are deferred to T-4 (where JPA and migration tooling are introduced).
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
- [ ] Single multi-stage Dockerfile at repo root that builds backend and frontend and produces one container
|
- [x] Single multi-stage Dockerfile at repo root that builds backend and frontend and produces one container
|
||||||
- [ ] App connects to external PostgreSQL via environment variable (e.g. `DATABASE_URL`)
|
- [x] `.dockerignore` excludes build artifacts, IDE files, and unnecessary files from the build context
|
||||||
- [ ] All runtime configuration via environment variables: database connection, optional Unsplash API key, optional max active events
|
|
||||||
- [x] Health-check endpoint so Docker/orchestrators can verify the app is alive
|
- [x] Health-check endpoint so Docker/orchestrators can verify the app is alive
|
||||||
- [ ] README documents setup with a docker-compose example (app + postgres)
|
- [x] `docker build .` succeeds and produces a working image
|
||||||
- [ ] Container starts and responds to health checks with an empty database (migrations run on startup or are documented)
|
- [x] Container starts and the health-check endpoint responds
|
||||||
|
|
||||||
**Dependencies:** T-1
|
**Dependencies:** T-1, T-5
|
||||||
|
|
||||||
|
**Addendum (2026-03-04):** Scope reduced from original "Dockerfile + configuration" to Docker-only. Database connectivity (`DATABASE_URL`), runtime environment variable configuration (Unsplash API key, max active events), and README docker-compose documentation are deferred to T-4, where JPA and Flyway are introduced and the configuration can be tested end-to-end. Rationale: without JPA and migrations, database wiring cannot be meaningfully verified.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,15 +78,21 @@
|
|||||||
|
|
||||||
### T-4: Development infrastructure setup
|
### T-4: Development infrastructure setup
|
||||||
|
|
||||||
**Description:** Set up the development foundation needed before the first user story can be implemented with TDD (as required by CLAUDE.md). This bridges the gap between project scaffolds and actual feature development.
|
**Description:** Set up the development foundation needed before the first user story can be implemented with TDD (as required by CLAUDE.md). This bridges the gap between project scaffolds and actual feature development. Also includes the database and environment variable configuration deferred from T-2.
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
- [ ] Database migration framework (Flyway or Liquibase) is configured in the backend with a first empty migration that runs successfully against a PostgreSQL instance
|
- [ ] Database migration framework (Flyway or Liquibase) is configured in the backend with a first empty migration that runs successfully against a PostgreSQL instance
|
||||||
|
- [ ] App connects to external PostgreSQL via environment variable (e.g. `DATABASE_URL` or Spring-native `SPRING_DATASOURCE_*`)
|
||||||
|
- [ ] All runtime configuration via environment variables: database connection, optional Unsplash API key, optional max active events
|
||||||
- [ ] SPA router is configured in the Vue frontend (Vue Router) so pages can be navigated by URL path
|
- [ ] SPA router is configured in the Vue frontend (Vue Router) so pages can be navigated by URL path
|
||||||
- [ ] Backend test infrastructure is set up: JUnit 5 with Spring Boot Test, plus integration test support using Testcontainers (PostgreSQL) so tests can run against a real database without external setup
|
- [ ] Backend test infrastructure is set up: JUnit 5 with Spring Boot Test, plus integration test support using Testcontainers (PostgreSQL) so tests can run against a real database without external setup
|
||||||
- [ ] Frontend test infrastructure is set up: Vitest with @vue/test-utils configured and a sample test runs successfully
|
- [ ] Frontend test infrastructure is set up: Vitest with @vue/test-utils configured and a sample test runs successfully
|
||||||
- [ ] Both test suites (backend and frontend) can be executed via their respective build tools (`mvn test` and `npm test` / `npx vitest`)
|
- [ ] Both test suites (backend and frontend) can be executed via their respective build tools (`mvn test` and `npm test` / `npx vitest`)
|
||||||
|
- [ ] README documents deployment setup with a docker-compose example (app + postgres)
|
||||||
|
- [ ] Container starts and responds to health checks with a running PostgreSQL (migrations run on startup)
|
||||||
|
|
||||||
**Dependencies:** T-2, T-5
|
**Dependencies:** T-2, T-5
|
||||||
|
|
||||||
**Notes:** T-4 is the prerequisite for all user story implementation. Without migration tooling, router, and test infrastructure, TDD (the mandated methodology per CLAUDE.md) cannot begin. The API client layer is provided by T-5 (openapi-fetch + generated types). All user stories that previously depended on T-1 and/or T-2 now depend on T-4 instead, since T-4 transitively includes T-1, T-2, and T-5.
|
**Notes:** T-4 is the prerequisite for all user story implementation. Without migration tooling, router, and test infrastructure, TDD (the mandated methodology per CLAUDE.md) cannot begin. The API client layer is provided by T-5 (openapi-fetch + generated types). All user stories that previously depended on T-1 and/or T-2 now depend on T-4 instead, since T-4 transitively includes T-1, T-2, and T-5.
|
||||||
|
|
||||||
|
**Addendum (2026-03-04):** Absorbed database connectivity, environment variable configuration, and docker-compose documentation from T-2 (see T-2 addendum). These criteria require JPA and Flyway to be testable, so they belong here.
|
||||||
|
|||||||
Reference in New Issue
Block a user