diff --git a/.claude/skills/start-app-server/SKILL.md b/.claude/skills/start-app-server/SKILL.md new file mode 100644 index 0000000..c9ad00e --- /dev/null +++ b/.claude/skills/start-app-server/SKILL.md @@ -0,0 +1,132 @@ +--- +name: start-app-server +description: "Start the django-react-intro app server in a Docker container and validate it responds. Uses a pre-validated recipe — no discovery, no guessing. Triggers on: 'spin up the server', 'run my app', 'start the server', 'verify app server', 'test my server', 'run app server', 'spin up the app'." +user_invocable: true +codepress_generated: true +--- + +# Start App Server — django-react-intro + +Fast-path skill for starting django-react-intro's app server. All discovery was done during bootstrap on 2026-07-07T19:27:32Z — just execute. + +The recipe lives at `.codepress/start-app-server/recipe.json`. This skill is the human-readable companion. + +## Tools + +These tools are pre-loaded — call them directly: + +- `build_and_start_app_server` — build image + start container in one step +- `forward_app_request` — HTTP request into the running container +- `get_app_server_logs` — container stdout/stderr +- `stop_app_server` — clean up when done + +## Static Context (from bootstrap) + +- **Stack**: Two-app monorepo — Django 1.11 backend (`server/`, SQLite, Pipfile/Pipfile.lock) serving a Create React App frontend (`web/`, react-scripts 1.0.10) that's built and copied into `server/static/build` at image build time. Single container, no sibling app. +- **Dockerfile**: `Dockerfile` +- **Port**: 8000 +- **Validation**: `GET /` → status in [200, 301, 302, 404] +- **Services**: none +- **Required secrets**: none + +## Step 1: Drift Check + +Verify the recipe hasn't gone stale: + +```bash +git hash-object Dockerfile # compare to recipe.input_checksums.dockerfile (79ae4e91373124fd) +git hash-object server/Pipfile # compare to recipe.input_checksums.dependency_manifest (a4af380b1d34c482) +``` + +If either checksum mismatches, the file has been edited since bootstrap. Proceed anyway — the agent will catch any actual breakage at build time — but flag the drift in your final report so the user knows the recipe may need updating. + +## Step 2: Fetch Secrets + +This app needs no vault secrets. Skip to Step 3. + +## Step 3: Build and Start + +```text +build_and_start_app_server( + workspaceDir=, + port=8000, + dockerfilePath="Dockerfile", + envVars={}, +) +``` + +If retrying after a fix in the same session, pass `existingContainerId=` so the old container is stopped on a successful rebuild. On the first attempt there is no previous container — omit the argument. + +## Step 4: Validate + +If the tool returns `health_check: "ready"` → go to Step 5. + +If it returns `health_check: "timed_out"`, poll up to 12 rounds × 5s each (default 12 rounds = 60 seconds; use `recipe.validation_max_polls` if set): + +```text +for attempt in 1..12: + r = forward_app_request(containerId, path="/", method="GET") + if r.status in [200, 301, 302, 404]: break + sleep 5 +``` + +If the poll is about to exhaust without success, check `get_app_server_logs` once. If the logs show a still-progressing startup step (running migrations, compiling assets, warming caches), extend with up to another 12 rounds before giving up. Apps with long migrations or asset builds commonly need 60–120 seconds. + +Success = any status in [200, 301, 302, 404]. A definite crash in the logs, or true exhaustion after extension, counts as failure and jumps to "Repair on Failure" below. + +## Step 5: Report + +Tell the user: + +- Container ID: `` +- Port: 8000 +- How to hit it: `forward_app_request(containerId, path="...")` +- How to stop: `stop_app_server(containerId)` + +If the user only asked to start / spin up the server, **STOP HERE**. Do not proceed to verification unless they asked to verify or test changes. + +## Known Fixes (from bootstrap) + +These issues came up during the initial bootstrap — try these first if a rebuild is needed: + +- web/package.json's postbuild script (`cp -r build/ ../server/static/build/`) fails if server/static doesn't exist yet — added `RUN mkdir -p /app/server/static` before `yarn build` in the frontend build stage. +- Container runs at runtime as uid/gid 65534 (nobody), but files land as root during the Docker build — SQLite couldn't create db.sqlite3 in /app/server. Added `RUN chown -R 65534:65534 /app/server` in the runtime stage. + +## Repair on Failure + +If `build_and_start_app_server` returns a genuine build/start error (not just a health timeout): + +**Before attempting repair, check `recipe.repair_count`.** If it is already at the `>= 3` ceiling, do not attempt in-session repair — at three or more prior repairs the recipe has stopped converging on the customer's actual repo state, and another rebuild would just consume more agent sessions without resolving the underlying drift. Tell the user the recipe is likely stale and suggest they re-invoke `/codepress-bootstrap-app-server` to regenerate it from scratch, then stop. + +Otherwise: + +1. Read `get_app_server_logs` AND the tool's error string in full +2. Match the error against the Known Fixes list above — apply that fix first if it matches +3. Identify ALL issues in the error output — do not rebuild after fixing just one +4. Edit `Dockerfile` (and, if needed, `recipe.json` for port/services changes) +5. Rebuild with `existingContainerId` set to the previous container's ID +6. If still failing after 3 in-session retries: + - Bump `recipe.json.repair_count` by 1 + - Set `recipe.json.origin` to `"repair"` + - Update `recipe.json.bootstrapped_at` to the current ISO timestamp + - Recompute `input_checksums` for the files you changed + - Append a one-line summary of what you fixed to `recipe.json.known_fixes` + - Also append that line to the "Known Fixes" section above in this SKILL.md + - Commit: + ```bash + git add Dockerfile .codepress/start-app-server/recipe.json .claude/skills/start-app-server/ + git commit -m "fix: update start-app-server recipe (repair attempt )" + ``` + - In ECS mode, also push: `git push origin HEAD` + +If you can't get it running after all that, stop the container if one is up, tell the user what you tried, and leave the recipe unchanged. + +## Cleanup + +**Do NOT stop a successfully started container.** The caller (user or `app-server-verify` Step 0) delegated here to leave a live server they can hit with `forward_app_request`. Leave it running and just report the container ID. + +Only call `stop_app_server(containerId)` when: + +- The user explicitly asks to stop or tear down the server, OR +- A build/start attempt failed and left a dead container behind (stop that failed container so it doesn't leak), OR +- You started a container as part of verification and the user asked to clean up afterward diff --git a/.codepress/start-app-server/recipe.json b/.codepress/start-app-server/recipe.json new file mode 100644 index 0000000..ea3f1fa --- /dev/null +++ b/.codepress/start-app-server/recipe.json @@ -0,0 +1,23 @@ +{ + "schema_version": 1, + "bootstrapped_at": "2026-07-07T19:27:32Z", + "origin": "bootstrap", + "repair_count": 0, + + "dockerfile_path": "Dockerfile", + "port": 8000, + + "validation_status_codes": [200, 301, 302, 404], + + "input_checksums": { + "dockerfile": "79ae4e91373124fd", + "dependency_manifest": "a4af380b1d34c482" + }, + + "system_packages": [], + "known_fixes": [ + "web/package.json's postbuild script (`cp -r build/ ../server/static/build/`) fails if server/static doesn't exist yet — added `RUN mkdir -p /app/server/static` before `yarn build` in the frontend build stage.", + "Container runs at runtime as uid/gid 65534 (nobody), but files land as root during the Docker build — SQLite couldn't create db.sqlite3 in /app/server. Added `RUN chown -R 65534:65534 /app/server` in the runtime stage." + ], + "discovery_notes": "Two-app monorepo: a Django 1.11 backend at server/ (SQLite, no external services, Pipfile/Pipfile.lock via pipenv) and a Create React App frontend at web/ (react-scripts 1.0.10, yarn.lock) with no API calls of its own. The repo's own build wiring (web's postbuild script) copies the React production build into server/static/build, and Django serves that directory plus a catch-all index view — so the single canonical app server is the Django backend serving the built frontend, started via a two-stage Dockerfile (node:10 builder + python:3.6-slim runtime). No vault secrets or sidecars needed: DEBUG=True, hardcoded dev SECRET_KEY, SQLite embedded, ALLOWED_HOSTS=[] which Django's dev special-case already permits for the default 'localhost' Host header forward_app_request sends." +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6a3df07 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github + +# Python +__pycache__ +*.pyc +*.egg-info +.venv +server/db.sqlite3 + +# Node +node_modules +web/build +server/static/build +.cache +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..79ae4e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Generated by CodePress bootstrap-app-server. +# Edits are preserved, but running /codepress-bootstrap-app-server again may overwrite them. + +# ---- Stage 1: build the React frontend, then let its postbuild script copy +# the production build into server/static/build (this repo's Django app +# serves that directory directly, see server/server/server/settings.py) ---- +FROM node:10 AS web-builder +WORKDIR /app +COPY server/ ./server/ +COPY web/ ./web/ +WORKDIR /app/web +RUN yarn install --frozen-lockfile +RUN mkdir -p /app/server/static +RUN yarn build + +# ---- Stage 2: Django app server ---- +FROM python:3.6-slim AS runtime +WORKDIR /app/server +COPY server/Pipfile server/Pipfile.lock ./ +RUN pip install --no-cache-dir pipenv \ + && pipenv install --system --deploy --ignore-pipfile +COPY server/ ./ +COPY --from=web-builder /app/server/static ./static +# The runtime sandbox executes the container as uid/gid 65534 (nobody), which +# needs write access to this directory so Django/SQLite can create db.sqlite3. +RUN chown -R 65534:65534 /app/server +EXPOSE 8000 +CMD ["sh", "-c", "python manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000"]