From 3f1fc59d237941f4bf4564c1b0921e8c269703a7 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Feb 2026 17:21:15 +0100 Subject: [PATCH] docs(01): create phase 1 foundation plans Phase 01: Foundation - 3 plans in 3 waves (sequential dependency) - Plan 01: Mill build + ZIO HTTP server - Plan 02: PostgreSQL + Quill + Flyway migrations - Plan 03: Repository pattern + Pulumi/Besom deployment Ready for execution --- .planning/ROADMAP.md | 12 +- .planning/phases/01-foundation/01-01-PLAN.md | 208 ++++++++++++ .planning/phases/01-foundation/01-02-PLAN.md | 228 +++++++++++++ .planning/phases/01-foundation/01-03-PLAN.md | 325 +++++++++++++++++++ 4 files changed, 767 insertions(+), 6 deletions(-) create mode 100644 .planning/phases/01-foundation/01-01-PLAN.md create mode 100644 .planning/phases/01-foundation/01-02-PLAN.md create mode 100644 .planning/phases/01-foundation/01-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index dc1063e..5914a34 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -32,15 +32,15 @@ Decimal phases appear between their surrounding integers in numeric order. **Success Criteria** (what must be TRUE): 1. Developer can run `mill run` and see HTTP server responding to requests 2. Database queries execute with compile-time SQL validation via Quill - 3. Database migrations run automatically on application startup + 3. Database migrations run via Migrator service 4. Models use Repository pattern with ZIO effects for data access 5. Pulumi configuration deploys the application to cloud infrastructure -**Plans**: TBD +**Plans**: 3 plans Plans: -- [ ] 01-01: Mill build setup and ZIO HTTP server -- [ ] 01-02: PostgreSQL integration with Quill and migrations -- [ ] 01-03: Repository pattern and Pulumi deployment +- [ ] 01-01-PLAN.md - Mill build setup and ZIO HTTP server with health endpoint +- [ ] 01-02-PLAN.md - PostgreSQL integration with Quill and Flyway migrations +- [ ] 01-03-PLAN.md - Repository pattern and Pulumi/Besom deployment config ### Phase 2: Plugin System **Goal**: Establish the plugin architecture that all other features build upon @@ -206,7 +206,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Foundation | 0/3 | Not started | - | +| 1. Foundation | 0/3 | Planned | - | | 2. Plugin System | 0/3 | Not started | - | | 3. Component System | 0/2 | Not started | - | | 4. Theme Engine | 0/2 | Not started | - | diff --git a/.planning/phases/01-foundation/01-01-PLAN.md b/.planning/phases/01-foundation/01-01-PLAN.md new file mode 100644 index 0000000..cdf6ebb --- /dev/null +++ b/.planning/phases/01-foundation/01-01-PLAN.md @@ -0,0 +1,208 @@ +--- +phase: 01-foundation +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - build.mill + - src/Main.scala + - src/config/AppConfig.scala + - src/api/Routes.scala + - src/api/HealthRoutes.scala + - resources/application.conf +autonomous: true + +must_haves: + truths: + - "Developer can run `mill run` and server starts on port 8080" + - "GET /health returns 200 with 'ok' body" + - "Server reads configuration from application.conf" + - "Environment variables override HOCON values" + artifacts: + - path: "build.mill" + provides: "Mill build configuration with ZIO dependencies" + contains: "zio-http" + - path: "src/Main.scala" + provides: "Application entry point extending ZIOAppDefault" + exports: ["Main"] + - path: "src/config/AppConfig.scala" + provides: "Configuration case classes" + exports: ["AppConfig", "ServerConfig", "DatabaseConfig"] + - path: "src/api/Routes.scala" + provides: "Route composition" + exports: ["Routes"] + - path: "src/api/HealthRoutes.scala" + provides: "Health check endpoint" + contains: "/health" + - path: "resources/application.conf" + provides: "HOCON configuration with env overrides" + contains: "server.port" + key_links: + - from: "src/Main.scala" + to: "src/api/Routes.scala" + via: "Server.serve(routes)" + pattern: "Server\\.serve" + - from: "src/Main.scala" + to: "resources/application.conf" + via: "ConfigProvider.fromResourcePath" + pattern: "ConfigProvider" +--- + + +Set up the Mill build system and create a working ZIO HTTP server with health endpoints and HOCON configuration. + +Purpose: Establish the foundational build infrastructure and prove the ZIO HTTP stack works before adding database complexity. +Output: Running HTTP server accessible at localhost:8080 with /health endpoint. + + + +@/home/jin/.claude/get-shit-done/workflows/execute-plan.md +@/home/jin/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/01-foundation/01-CONTEXT.md +@.planning/phases/01-foundation/01-RESEARCH.md + + + + + + Task 1: Create Mill build configuration + build.mill + +Create Mill build file with Scala 3 and ZIO dependencies. + +Use programmatic Mill config (build.mill not YAML) for assembly support: +- Scala version: 3.3.4 (LTS, better library compatibility than 3.8.x) +- Dependencies: + - dev.zio::zio:2.1.14 + - dev.zio::zio-http:3.0.1 (stable release, not 3.8.x which is unreleased) + - dev.zio::zio-config:4.0.2 + - dev.zio::zio-config-typesafe:4.0.2 + - dev.zio::zio-config-magnolia:4.0.2 + - io.getquill::quill-jdbc-zio:4.8.5 + - org.postgresql:postgresql:42.7.4 + - org.flywaydb:flyway-core:10.21.0 + - org.flywaydb:flyway-database-postgresql:10.21.0 + +Include assembly configuration for fat JAR with: +- Service file merging for META-INF/services +- Exclude signature files (*.SF, *.DSA, *.RSA) + +Note: Research suggested newer versions but use stable releases that exist on Maven Central. + + Run `mill resolve _` to verify Mill parses the build file correctly + build.mill exists and Mill can parse all dependencies + + + + Task 2: Create project structure and configuration + + src/config/AppConfig.scala + resources/application.conf + + +Create configuration infrastructure: + +1. src/config/AppConfig.scala: +- Case class AppConfig with server and database nested configs +- Case class ServerConfig(host: String, port: Int) +- Case class DatabaseConfig(host: String, port: Int, database: String, user: String, password: String) +- Use zio-config-magnolia for automatic derivation + +2. resources/application.conf (HOCON): +```hocon +server { + host = "0.0.0.0" + host = ${?SERVER_HOST} + port = 8080 + port = ${?SERVER_PORT} +} + +database { + host = "localhost" + host = ${?DB_HOST} + port = 5432 + port = ${?DB_PORT} + database = "summercms" + database = ${?DB_NAME} + user = "summercms" + user = ${?DB_USER} + password = "summercms" + password = ${?DB_PASSWORD} +} +``` + +Pattern: Environment variables override defaults using HOCON substitution syntax. + + Files exist at correct paths + AppConfig case classes defined, application.conf has all database and server settings with env overrides + + + + Task 3: Create HTTP routes and Main entry point + + src/api/HealthRoutes.scala + src/api/Routes.scala + src/Main.scala + + +Create the HTTP layer: + +1. src/api/HealthRoutes.scala: +- Object HealthRoutes with `routes` val returning Routes +- GET /health -> Response.text("ok") +- Keep simple for now; /ready endpoint added when database exists + +2. src/api/Routes.scala: +- Object Routes that composes all route modules +- For now, just re-exports HealthRoutes.routes +- This is the composition point for future route modules + +3. src/Main.scala: +- Object Main extends ZIOAppDefault +- Override bootstrap to set ConfigProvider.fromResourcePath() +- In run: Server.serve(Routes.routes).provide(Server.defaultWithPort(8080)) +- Print ASCII art banner on startup (SummerCMS with sun motif) + +ASCII banner example: +``` + \\ | // + \\ | // + ___\\###//___ + / SUMMER \\ + \\ CMS / + \\__________/ + + Starting on port 8080... +``` + + Run `mill run` and curl localhost:8080/health returns "ok" + Server starts, /health returns 200 with "ok" body, banner displays + + + + + +1. `mill resolve _` succeeds (build file valid) +2. `mill compile` succeeds (code compiles) +3. `mill run` starts server (process runs) +4. `curl http://localhost:8080/health` returns "ok" (endpoint works) +5. Server logs show startup banner + + + +- Mill build configuration exists with all ZIO dependencies +- Server starts on port 8080 via `mill run` +- GET /health returns 200 OK with "ok" body +- Configuration loads from application.conf +- ASCII SummerCMS banner displays on startup + + + +After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-foundation/01-02-PLAN.md b/.planning/phases/01-foundation/01-02-PLAN.md new file mode 100644 index 0000000..608b80d --- /dev/null +++ b/.planning/phases/01-foundation/01-02-PLAN.md @@ -0,0 +1,228 @@ +--- +phase: 01-foundation +plan: 02 +type: execute +wave: 2 +depends_on: ["01-01"] +files_modified: + - src/db/QuillContext.scala + - src/db/Migrator.scala + - resources/db/migration/V1__create_summer_users.sql + - src/api/HealthRoutes.scala +autonomous: true + +must_haves: + truths: + - "Quill compiles SQL queries at compile time (type errors for bad queries)" + - "Flyway migrations run via ZIO effect" + - "Migration creates summer_users table in PostgreSQL" + - "GET /ready returns 200 when database is connected, 503 when not" + artifacts: + - path: "src/db/QuillContext.scala" + provides: "Quill PostgreSQL context with ZIO integration" + exports: ["quillLayer", "dataSourceLayer"] + - path: "src/db/Migrator.scala" + provides: "Flyway wrapper as ZIO service" + exports: ["Migrator", "MigrationStatus"] + - path: "resources/db/migration/V1__create_summer_users.sql" + provides: "Initial database schema" + contains: "CREATE TABLE summer_users" + key_links: + - from: "src/db/QuillContext.scala" + to: "resources/application.conf" + via: "Quill.DataSource.fromPrefix" + pattern: "fromPrefix.*database" + - from: "src/db/Migrator.scala" + to: "src/db/QuillContext.scala" + via: "ZLayer dependency on DataSource" + pattern: "DataSource" + - from: "src/api/HealthRoutes.scala" + to: "src/db/QuillContext.scala" + via: "/ready endpoint checks connection" + pattern: "/ready" +--- + + +Integrate PostgreSQL via Quill with compile-time SQL validation and Flyway migrations wrapped in ZIO effects. + +Purpose: Establish database connectivity with type-safe queries and version-controlled schema changes. +Output: Working database layer with migrations and a /ready endpoint that verifies connectivity. + + + +@/home/jin/.claude/get-shit-done/workflows/execute-plan.md +@/home/jin/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/01-foundation/01-CONTEXT.md +@.planning/phases/01-foundation/01-RESEARCH.md +@.planning/phases/01-foundation/01-01-SUMMARY.md + + + + + + Task 1: Create Quill PostgreSQL context + src/db/QuillContext.scala + +Create Quill context for PostgreSQL with ZIO integration. + +src/db/QuillContext.scala: +- Import io.getquill.* and io.getquill.jdbczio.Quill +- Create dataSourceLayer: ZLayer[Any, Throwable, javax.sql.DataSource] + - Use Quill.DataSource.fromPrefix("database") to read from HOCON config +- Create quillLayer: ZLayer[javax.sql.DataSource, Nothing, Quill.Postgres[SnakeCase]] + - Use Quill.Postgres.fromNamingStrategy(SnakeCase) +- Export combined layer: dataSourceLayer >>> quillLayer + +IMPORTANT: Never use "io" as a variable name in this file (Quill package conflict). + +Configuration note: Quill reads HikariCP config from the "database" prefix in application.conf. +The existing database config structure should work, but may need adjustment to match HikariCP's expected format: +```hocon +database { + dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" + dataSource { + serverName = "localhost" + portNumber = 5432 + databaseName = "summercms" + user = "summercms" + password = "summercms" + } +} +``` + +Update resources/application.conf to use HikariCP-compatible format if needed. + + `mill compile` succeeds with Quill context + QuillContext.scala exports dataSourceLayer and quillLayer, compiles without errors + + + + Task 2: Create Flyway migrator service + + src/db/Migrator.scala + resources/db/migration/V1__create_summer_users.sql + + +Create Flyway wrapper as ZIO service and initial migration. + +1. src/db/Migrator.scala: +- Case class MigrationStatus(current: Option[String], pending: Int) +- Trait Migrator with methods: + - def migrate: Task[Int] (returns count of migrations run) + - def status: Task[MigrationStatus] +- Object Migrator with: + - val live: ZLayer[javax.sql.DataSource, Nothing, Migrator] + - Implementation configures Flyway with: + - dataSource from layer + - locations: "classpath:db/migration" + - table: "summer_migrations" (not default flyway_schema_history) + - baselineOnMigrate: true (for existing databases) + +2. resources/db/migration/V1__create_summer_users.sql: +```sql +-- Summer CMS initial schema +-- Creates the base users table for future auth + +CREATE TABLE summer_users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_summer_users_email ON summer_users(email); + +-- Trigger for updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_summer_users_updated_at + BEFORE UPDATE ON summer_users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); +``` + +Naming follows conventions from CONTEXT.md: +- snake_case columns +- summer_ prefix for core tables +- id, created_at, updated_at by default + + `mill compile` succeeds, migration file exists at correct path + Migrator service defined with migrate/status methods, V1 migration creates summer_users + + + + Task 3: Add /ready endpoint with database check + src/api/HealthRoutes.scala + +Update HealthRoutes to include database connectivity check. + +Modify src/api/HealthRoutes.scala: +- Keep existing GET /health -> Response.text("ok") (always 200) +- Add GET /ready endpoint: + - Requires DataSource from ZLayer + - Attempts: ZIO.attempt(ds.getConnection.close()) + - Success: Response.text("ready") with 200 + - Failure: Response.status(Status.ServiceUnavailable) with 503 + +Pattern: +```scala +Method.GET / "ready" -> handler { + ZIO.serviceWithZIO[javax.sql.DataSource] { ds => + ZIO.attempt { + val conn = ds.getConnection + conn.close() + }.as(Response.text("ready")) + .catchAll(_ => ZIO.succeed(Response.status(Status.ServiceUnavailable))) + } +} +``` + +Update Main.scala to: +- Provide dataSourceLayer to the server +- On startup, run Migrator.migrate and log result +- Startup sequence: migrate -> start server + +Note: The /ready endpoint will return 503 if no database is running, which is correct behavior. + + +With PostgreSQL running: `curl localhost:8080/ready` returns "ready" +Without PostgreSQL: `curl localhost:8080/ready` returns 503 + + /ready endpoint exists, returns 200 when DB connected, 503 when not + + + + + +1. `mill compile` succeeds (Quill context compiles) +2. With PostgreSQL running: + - `mill run` starts and runs migrations + - Logs show "Applied N migrations" + - `curl localhost:8080/ready` returns "ready" +3. Check database: `psql -d summercms -c "\\dt"` shows summer_users table +4. Check migrations table: `psql -d summercms -c "SELECT * FROM summer_migrations"` + + + +- Quill context exists with PostgreSQL configuration +- Flyway migrator runs as ZIO effect on startup +- V1 migration creates summer_users table with proper schema +- /ready endpoint verifies database connectivity +- Compile-time SQL validation works (intentional bad query causes compile error) + + + +After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` + diff --git a/.planning/phases/01-foundation/01-03-PLAN.md b/.planning/phases/01-foundation/01-03-PLAN.md new file mode 100644 index 0000000..d9899ea --- /dev/null +++ b/.planning/phases/01-foundation/01-03-PLAN.md @@ -0,0 +1,325 @@ +--- +phase: 01-foundation +plan: 03 +type: execute +wave: 3 +depends_on: ["01-02"] +files_modified: + - src/repository/RepositoryError.scala + - src/repository/UserRepository.scala + - src/model/User.scala + - infra/project.scala + - infra/Main.scala + - Dockerfile +autonomous: true + +must_haves: + truths: + - "Repository trait defines CRUD operations with typed ZIO errors" + - "UserRepositoryLive implements queries using Quill context" + - "Besom/Pulumi infrastructure code compiles and can run pulumi preview" + - "Dockerfile builds fat JAR into runnable container" + artifacts: + - path: "src/repository/RepositoryError.scala" + provides: "Typed error ADT for repository operations" + exports: ["RepositoryError", "NotFound", "Conflict", "DatabaseError"] + - path: "src/repository/UserRepository.scala" + provides: "User repository trait and implementation" + exports: ["UserRepository", "UserRepositoryLive"] + - path: "src/model/User.scala" + provides: "User domain model" + exports: ["User"] + - path: "infra/Main.scala" + provides: "Pulumi infrastructure definition" + contains: "Pulumi.run" + - path: "Dockerfile" + provides: "Container build configuration" + contains: "FROM" + key_links: + - from: "src/repository/UserRepository.scala" + to: "src/db/QuillContext.scala" + via: "Quill.Postgres dependency" + pattern: "Quill\\.Postgres" + - from: "src/repository/UserRepository.scala" + to: "src/model/User.scala" + via: "CRUD operations on User" + pattern: "User" + - from: "src/repository/UserRepository.scala" + to: "src/repository/RepositoryError.scala" + via: "IO[RepositoryError, _] return type" + pattern: "RepositoryError" +--- + + +Implement the Repository pattern with typed errors and create Pulumi infrastructure configuration using Besom. + +Purpose: Establish the data access pattern that all future models will follow, and prepare deployment infrastructure. +Output: Working UserRepository with compile-time validated queries, Pulumi config for cloud deployment, Dockerfile for containerization. + + + +@/home/jin/.claude/get-shit-done/workflows/execute-plan.md +@/home/jin/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/01-foundation/01-CONTEXT.md +@.planning/phases/01-foundation/01-RESEARCH.md +@.planning/phases/01-foundation/01-01-SUMMARY.md +@.planning/phases/01-foundation/01-02-SUMMARY.md + + + + + + Task 1: Create Repository pattern with typed errors + + src/model/User.scala + src/repository/RepositoryError.scala + src/repository/UserRepository.scala + + +Implement the Repository pattern following ZIO best practices. + +1. src/model/User.scala: +```scala +package summercms.model + +import java.time.Instant + +case class User( + id: Long, + email: String, + passwordHash: String, + createdAt: Instant, + updatedAt: Instant +) +``` + +2. src/repository/RepositoryError.scala: +```scala +package summercms.repository + +sealed trait RepositoryError + +object RepositoryError: + case class NotFound(entity: String, id: Long) extends RepositoryError + case class NotFoundByField(entity: String, field: String, value: String) extends RepositoryError + case class Conflict(entity: String, message: String) extends RepositoryError + case class ValidationError(errors: List[String]) extends RepositoryError + case class DatabaseError(message: String, cause: Option[Throwable] = None) extends RepositoryError +``` + +3. src/repository/UserRepository.scala: +- Trait UserRepository with methods: + - def findById(id: Long): IO[RepositoryError, Option[User]] + - def findByEmail(email: String): IO[RepositoryError, Option[User]] + - def create(email: String, passwordHash: String): IO[RepositoryError, User] + - def update(user: User): IO[RepositoryError, User] + - def delete(id: Long): IO[RepositoryError, Unit] + +- Object UserRepository with: + - val live: ZLayer[Quill.Postgres[SnakeCase], Nothing, UserRepository] + +- Class UserRepositoryLive(quill: Quill.Postgres[SnakeCase]) extends UserRepository: + - import quill.* + - inline def users = quote(querySchema[User]("summer_users")) + - Implement all methods using Quill queries + - Use refineOrDie to convert SQLException to RepositoryError.DatabaseError + - Handle unique constraint violations as Conflict errors + +Pattern for error handling: +```scala +def create(email: String, passwordHash: String): IO[RepositoryError, User] = + val now = Instant.now() + val user = User(0, email, passwordHash, now, now) + run(users.insertValue(lift(user)).returningGenerated(_.id)) + .map(id => user.copy(id = id)) + .refineOrDie { + case e: java.sql.SQLException if e.getSQLState == "23505" => + RepositoryError.Conflict("User", s"Email $email already exists") + case e: java.sql.SQLException => + RepositoryError.DatabaseError(e.getMessage, Some(e)) + } +``` + +IMPORTANT: Do not use "io" as a variable name (Quill package conflict). + + `mill compile` succeeds, repository pattern established + User model, RepositoryError ADT, and UserRepository trait+implementation exist and compile + + + + Task 2: Create Pulumi infrastructure with Besom + + infra/project.scala + infra/Main.scala + + +Create Pulumi infrastructure using Besom (Scala Pulumi SDK). + +IMPORTANT: Besom does NOT support Mill. Use Scala CLI for the infra/ directory. + +1. infra/project.scala (Scala CLI project config): +```scala +//> using scala 3.3.4 +//> using dep org.virtuslab::besom-core:0.4.0 +//> using dep org.virtuslab::besom-aws:6.56.0 +``` + +Note: Use Besom 0.4.0 (latest stable on Maven Central, not 0.5.0 from research). + +2. infra/Main.scala: +```scala +import besom.* +import besom.api.aws + +@main def main = Pulumi.run { + // Configuration + val config = besom.Config() + val environment = config.get("environment").getOrElse("dev") + + // S3 bucket for assets (optional CDN origin) + val assetsBucket = aws.s3.Bucket( + s"summercms-assets-$environment", + aws.s3.BucketArgs() + ) + + // RDS PostgreSQL instance + val db = aws.rds.Instance( + s"summercms-db-$environment", + aws.rds.InstanceArgs( + engine = "postgres", + engineVersion = "16.4", + instanceClass = "db.t3.micro", + allocatedStorage = 20, + dbName = "summercms", + username = "summercms", + password = config.requireSecret("dbPassword"), + skipFinalSnapshot = environment == "dev", + publiclyAccessible = environment == "dev" + ) + ) + + // ECS for container deployment (placeholder for now) + // Full ECS setup deferred to when we need actual deployment + + Stack.exports( + assetsBucketName = assetsBucket.bucket, + dbEndpoint = db.endpoint, + dbPort = db.port + ) +} +``` + +This provides: +- Multi-environment support via Pulumi config +- S3 bucket for future asset storage +- RDS PostgreSQL for database +- Secrets management for DB password +- Outputs for connecting the application + +Note: Full ECS/container deployment is deferred. This establishes the pattern. + + +Run from infra/ directory: +- `scala-cli compile .` compiles +- `pulumi preview` shows resources (requires Pulumi CLI and AWS credentials) + + Besom infrastructure code compiles, defines RDS and S3 resources + + + + Task 3: Create Dockerfile for containerization + Dockerfile + +Create multi-stage Dockerfile for building and running SummerCMS. + +Dockerfile: +```dockerfile +# Stage 1: Build +FROM eclipse-temurin:21-jdk AS builder + +# Install Mill +RUN curl -L https://github.com/com-lihaoyi/mill/releases/download/0.12.3/0.12.3 > /usr/local/bin/mill && \ + chmod +x /usr/local/bin/mill + +WORKDIR /app +COPY . . + +# Build fat JAR +RUN mill assembly + +# Stage 2: Runtime +FROM eclipse-temurin:21-jre + +WORKDIR /app + +# Copy the fat JAR from builder +COPY --from=builder /app/out/assembly.dest/out.jar /app/summercms.jar + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Install curl for health check +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +EXPOSE 8080 + +# Run the application +ENTRYPOINT ["java", "-jar", "/app/summercms.jar"] +``` + +Features: +- Multi-stage build (builder has JDK+Mill, runtime has only JRE) +- Uses Eclipse Temurin JDK 21 (LTS) +- Health check using /health endpoint +- Minimal runtime image size +- Environment variables configure the app (via application.conf ${?VAR} substitution) + +Usage: +```bash +docker build -t summercms . +docker run -p 8080:8080 \ + -e DB_HOST=host.docker.internal \ + -e DB_PASSWORD=secret \ + summercms +``` + + +`docker build -t summercms .` succeeds +`docker run -p 8080:8080 summercms` starts (will fail at DB connect without DB, that's expected) + + Dockerfile exists with multi-stage build, health check, and env var configuration + + + + + +1. `mill compile` succeeds (repository code compiles) +2. Repository pattern test (manual in mill console or separate test): + - UserRepository.findById returns IO[RepositoryError, Option[User]] + - Type errors when using wrong types +3. Infrastructure: + - `cd infra && scala-cli compile .` succeeds + - `cd infra && pulumi preview` shows planned resources (requires setup) +4. Docker: + - `docker build -t summercms .` builds successfully + - Image contains /app/summercms.jar + + + +- User model and RepositoryError ADT defined +- UserRepository trait with CRUD methods, typed errors +- UserRepositoryLive implements queries with Quill +- Besom infrastructure defines RDS and S3 for AWS +- Dockerfile builds fat JAR in multi-stage build +- All code compiles without errors + + + +After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md` +