1
0

M01-monorepo-skeleton.md 11 KB

M01 — Monorepo Skeleton & Toolchain

Fresh Claude Code agent prompt. You are starting from an empty repo (or a repo with only SPEC.md and an empty PROGRESS.md). Estimated effort: medium (mostly boilerplate; the goal is solid foundations).

Mission

Create the monorepo layout, set up the toolchain (composer, npm, PHPUnit, PHPStan, php-cs-fixer), and produce buildable Docker images for both the api and ui containers. Both containers start under compose, return placeholder healthchecks, and migrate runs an empty Phinx migration set and exits 0. No business logic in this milestone.

Before you start

  1. Read SPEC.md end-to-end once. Even if it feels long, do it — every later milestone assumes you understand the architecture.
  2. Then re-read these sections carefully: §1 (Project Goals), §2 (Tech Stack), §3 (Architecture), §10 (Docker), §11 (Project Structure), §14 (Coding Conventions).
  3. Verify the working tree:

    ls -la                  # should see SPEC.md and PROGRESS.md
    git status              # clean
    
  4. Confirm tooling: docker --version, docker compose version, php --version (8.3+), composer --version, node --version (20+), npm --version.

Tasks

Execute in this order. Commit nothing until acceptance passes.

1. Repo skeleton

Create the directory structure exactly as in SPEC.md §11. Empty placeholders are fine where files come later (e.g. api/src/Domain/.gitkeep).

2. Root files

  • .env.example — every env var from SPEC.md §9, grouped into "Shared", "API container", "UI container" sections, with comments.
  • .gitignore — sensible defaults: vendor/, node_modules/, public/assets/, .env, .phpunit.cache/, .phpunit.result.cache, data/, IDE files.
  • docker-compose.yml — exactly as in SPEC.md §10.
  • compose.scheduler.yml — exactly as in SPEC.md §10. You don't need to make it work end-to-end yet; you only need it to be valid YAML.
  • README.md — minimal: project name, one-paragraph description, "see SPEC.md and milestones/ for details".

3. api/ subproject

  • api/composer.json — Slim 4, doctrine/dbal, robmorgan/phinx, monolog, php-di, vlucas/phpdotenv (dev), guzzlehttp/psr7. Dev: phpunit ^11, phpstan ^1.10, friendsofphp/php-cs-fixer ^3. Set "type": "project" and PSR-4 autoload mapping App\\src/.
  • api/public/index.php — Slim app bootstrap. Two routes for now: GET /healthz returning {"status": "ok"} (JSON), and a 404 fallback. Wire structured JSON logging via Monolog to stdout.
  • api/config/settings.php — builds a config array from environment variables. Don't read .env in production; do read it in development via phpdotenv.
  • api/config/phinx.php — Phinx config that reads the same env vars. Migrations dir db/migrations, seeds dir db/seeds. Both SQLite and MySQL adapters configured.
  • api/db/migrations/.gitkeep and api/db/seeds/.gitkeep.
  • api/bin/console — minimal Symfony Console app with one command: db:migrate that delegates to vendor/bin/phinx migrate. Make it executable.
  • api/docker/entrypoint.sh — dispatcher script that switches on $1 (api default → starts FrankenPHP serving public/; migrate → runs migrations and exits). Make it executable.
  • api/docker/Caddyfile — FrankenPHP/Caddy config serving on :8081. Configure /internal/* location with the remote_ip matcher from SPEC.md §6 (you don't need any internal routes yet, just the protective Caddy match).
  • api/Dockerfile — multi-stage as described in SPEC.md §10, dunglas/frankenphp:1-php8.3-alpine base. Install pdo_sqlite, pdo_mysql, mbstring, intl, opcache, bcmath extensions.
  • api/phpunit.xml — testsuite includes tests/Unit and tests/Integration.
  • api/phpstan.neon — level 8 on src/.
  • api/.php-cs-fixer.dist.php — PSR-12 + strict types.
  • api/tests/Unit/SmokeTest.php — one trivial assertion ($this->assertTrue(true)) so the suite runs.

4. ui/ subproject

Same shape, but:

  • ui/composer.json — Slim 4, twig/twig, slim/twig-view, guzzlehttp/guzzle, jumbojett/openid-connect-php, monolog, php-di, vlucas/phpdotenv (dev). No DBAL, no Phinx. Same dev deps as api.
  • ui/package.jsontailwindcss ^3, postcss, autoprefixer, alpinejs, htmx.org. Build script: tailwindcss -i resources/css/app.css -o public/assets/app.css --minify.
  • ui/tailwind.config.js — content paths covering resources/views/**/*.twig and resources/js/**/*.js. darkMode: 'class'.
  • ui/postcss.config.js — autoprefixer + tailwindcss.
  • ui/resources/css/app.css — Tailwind directives only.
  • ui/resources/js/app.js — Alpine + htmx imports.
  • ui/resources/views/layout.twig — minimal HTML skeleton with Tailwind classes, <html class="dark:bg-slate-900">, dark-mode toggle button (no JS yet, just the markup).
  • ui/resources/views/pages/hello.twig — extends layout, says "IRDB UI — milestone 1".
  • ui/public/index.php — Slim with Twig. One route GET / renders pages/hello.twig. One route GET /healthz returns {"status":"ok","api_reachable":null,"last_api_check_at":null}.
  • ui/docker/Caddyfile — serves on :8080.
  • ui/docker/entrypoint.sh — single mode (ui).
  • ui/Dockerfile — multi-stage as in SPEC.md §10.
  • ui/phpunit.xml, ui/phpstan.neon, ui/.php-cs-fixer.dist.php, ui/tests/Unit/SmokeTest.php.

5. examples/ and doc/ placeholders

  • examples/scheduler/host.crontab — a comment-only stub for now (real content lands in M13).
  • doc/.gitkeep — empty (docs land in M13).

6. CI (local)

Do not create any GitHub Actions workflow. CI runs locally on this server (which has Docker installed). Create scripts/ci.sh that an operator can invoke manually:

  • Make it executable (chmod +x scripts/ci.sh) and start with #!/usr/bin/env bash and set -euo pipefail.
  • Runs each stage in order, fails fast on the first non-zero exit, and prints a clear banner before each stage.
  • Stages:
    1. api/: composer install --no-interaction --prefer-dist, then composer stan, composer cs, composer test.
    2. ui/: composer install --no-interaction --prefer-dist, then composer stan, composer cs, composer test.
    3. ui/: npm ci then npm run build; assert ui/public/assets/app.css exists.
    4. docker compose build from the repo root to verify both images build.
  • DB driver matrix: accept an env var DB_DRIVERS (default "sqlite mysql") and loop the api test stage once per driver, exporting DB_DRIVER so phpunit can pick it up. For mysql, skip gracefully with a warning if no local MySQL is reachable (this milestone has no DB-touching tests yet, so the loop is mostly scaffolding for later milestones).
  • At the end, print a green "CI OK" line. On failure, the failing command's exit code propagates.

Also add a one-line invocation note in README.md under a "Local CI" heading: ./scripts/ci.sh.

Implementation notes

  • FrankenPHP entrypoints: the entrypoint script must exec the FrankenPHP process for proper signal handling. exec frankenphp run --config /etc/Caddyfile.
  • Caddy :8081 for api, :8080 for ui — match SPEC.md §10. Don't expose the api on 8080.
  • healthz format: api returns {"status":"ok"} for now; full payload (db, jobs) lands in later milestones. Comment in the code where additional fields will go.
  • Service token bootstrap: do not implement yet. SPEC says the api ensures UI_SERVICE_TOKEN exists on startup — that lands in M03 when api_tokens table exists.
  • Volume mount: api's entrypoint.sh should mkdir -p /data before launching, in case the SQLite path is set but the dir wasn't created.
  • Composer scripts: add convenience scripts in each subproject's composer.json: test (phpunit), stan (phpstan), cs (cs-fixer), cs-fix (cs-fixer fix). Lets the agent in later milestones run composer test instead of remembering paths.

Out of scope (DO NOT)

  • Do not create any database tables or migrations beyond an empty migrations dir. Tables come in M02.
  • Do not implement any auth, tokens, RBAC, sessions. That's M03/M08.
  • Do not install any deps not listed above without strong reason. Note any addition in PROGRESS.md.
  • Do not write business logic. The api should respond only to /healthz. The ui should render only the hello page and /healthz.
  • Do not wire OIDC, MaxMind, or any external service.
  • Do not create files in doc/ beyond .gitkeep.
  • Do not touch SPEC.md. It is read-only for milestone agents.

Acceptance

Run all of these. Every one must pass. If any fails, fix and re-run; do not commit until all green.

# 1. Static analysis and style
cd api && composer install && composer stan && composer cs && composer test && cd ..
cd ui  && composer install && composer stan && composer cs && composer test && cd ..

# 2. Frontend build
cd ui && npm ci && npm run build && test -f public/assets/app.css && cd ..

# 3. Compose build
docker compose build

# 4. Compose up — migrate exits 0, api and ui become healthy
cp .env.example .env
# Set required secrets in .env so containers start (use placeholder values for now):
#   APP_SECRET, UI_SECRET, UI_SERVICE_TOKEN, INTERNAL_JOB_TOKEN — any 32 hex chars each.
docker compose up -d
sleep 15

# Migrate must have exited 0:
test "$(docker compose ps -a --format '{{.Service}} {{.State}} {{.ExitCode}}' | grep migrate | awk '{print $3}')" = "0"

# Healthchecks (poll up to 60s):
for i in {1..30}; do
  curl -sf http://localhost:8081/healthz && curl -sf http://localhost:8080/healthz && break
  sleep 2
done
curl -sf http://localhost:8081/healthz | grep -q '"status":"ok"'
curl -sf http://localhost:8080/healthz | grep -q '"status":"ok"'

# Hello page renders:
curl -sf http://localhost:8080/ | grep -q "milestone 1"

docker compose down -v

Handoff

  1. Commit:

    feat(M01): monorepo skeleton, toolchain, docker compose builds clean
    
    - api/ and ui/ subprojects with composer + slim 4
    - frankenphp dockerfiles, multi-stage builds
    - phpunit, phpstan level 8, php-cs-fixer wired into composer scripts
    - tailwind build pipeline in ui/
    - empty phinx migrations directory
    - scripts/ci.sh runs the full test/lint matrix locally (no GitHub Actions)
    
  2. Append to PROGRESS.md:

    ## M01 — Monorepo skeleton (done)
    
    **Built:** repo layout per SPEC §11, both Dockerfiles, compose stack, toolchain.
    
    **Notes for next milestone:**
    - DB schema empty; M02 owns all tables and seeds.
    - `entrypoint.sh` for api supports `migrate` mode and calls `vendor/bin/phinx`.
    - Healthcheck payloads are stubs; later milestones extend them.
    - Service-token bootstrap deferred to M03 (needs `api_tokens` table first).
    
    **Deviations from SPEC:** none.
    **Added dependencies beyond SPEC §2:** none.
    
  3. Stop. Do not start M02.