fix(build): Containerfile builds again — relative module imports, all four pages and shared assets in the image
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# Phase 23 report — Containerfile: build the whole app image again
|
||||
|
||||
**Date:** 2026-08-24 · **Source:** `TODO.md` L6 ("Fix Containerfile build
|
||||
not working") · **Story:** `.agent/user_stories/containerfile-build.md`
|
||||
|
||||
## Root cause (verified, not guessed — logs in this directory)
|
||||
|
||||
1. **Build failure:** phase 19's absolute `import … from
|
||||
"/assets/header.js"` is resolved by esbuild as the *filesystem* path
|
||||
`/assets/header.js` → `✘ [ERROR] Could not resolve "/assets/header.js"`,
|
||||
reproduced with the exact pinned esbuild **0.25.5**
|
||||
(`repro_esbuild_0.25.5.log`, `repro_podman_build_before_fix.log`).
|
||||
2. **Under-shipped image:** stage 1 bundled only `app.js` +
|
||||
`sources.js` and copied only `index.html` + `sources.html` —
|
||||
`document.html` / `login.html` / `document.js` / `login.js` /
|
||||
`markdown.js` were all missing from the image.
|
||||
3. **Double-evaluation trap:** the four direct `<script
|
||||
src="/assets/header.js">` tags would evaluate the header module
|
||||
twice in the image (bundled copy + raw module) → duplicate sign-out
|
||||
listener, double init.
|
||||
4. **Second blocker (task 01):** a local `npm install` of esbuild left
|
||||
the binary off the next `RUN`'s PATH (`esbuild: not found`) → the
|
||||
stage installs it globally.
|
||||
|
||||
## Fix
|
||||
|
||||
| piece | after |
|
||||
|---|---|
|
||||
| page-script header import | `from "./header.js"` (relative — owner-confirmed over an esbuild alias) |
|
||||
| direct header.js `<script>` tags | **removed from all four pages** (the hoisted import covers evaluation order) |
|
||||
| esbuild install | `npm install --no-audit --no-fund -g esbuild@0.25.5` (pinned, on PATH) |
|
||||
| stage-1 bundles | `app.js`, `sources.js`, `document.js`, `login.js` |
|
||||
| stage-1 minifies | `styles.css` + `markdown.js` (no `--bundle` — classic script) |
|
||||
| stage-1 copies | all four HTML pages into `/out` |
|
||||
|
||||
## Build gate — `podman build` (task 01, `podman_build_after_fix.log`)
|
||||
|
||||
```
|
||||
[1/3] STEP 3/5: RUN npm install --no-audit --no-fund -g esbuild@0.25.5
|
||||
[1/3] STEP 5/5: RUN mkdir -p /out/assets && esbuild ./assets/app.js --bundle … && … && cp ./index.html ./sources.html ./document.html ./login.html /out/
|
||||
../out/assets/app.js 14.3kb ⚡ Done in 2ms
|
||||
../out/assets/sources.js 2.3kb ⚡ Done in 2ms
|
||||
../out/assets/document.js 2.4kb ⚡ Done in 2ms
|
||||
../out/assets/login.js 1.5kb ⚡ Done in 2ms
|
||||
[3/3] COMMIT brain-of-reese/app:phase23test
|
||||
Successfully tagged localhost/brain-of-reese/app:phase23test
|
||||
39f5563a1b101b274d81d86aa06f86a4d6d1bba453e8dba40d6f48f987537019
|
||||
BUILD_EXIT=0
|
||||
```
|
||||
|
||||
## Image smoke (task 01, `image_smoke.log`)
|
||||
|
||||
Throwaway Postgres 17 + pgvector (`bor-smoke-db`, isolated network),
|
||||
production env, teardown done:
|
||||
|
||||
- app boots: migrations applied, uvicorn up, `/api/health` →
|
||||
`{"status":"ok","db":"up",…}`
|
||||
- pages: `GET /`, `/sources.html`, `/document.html`, `/login.html` → **200**
|
||||
- assets: `app.js`, `sources.js`, `document.js`, `login.js`,
|
||||
`markdown.js`, `styles.css` → **200**
|
||||
- single-evaluation: raw `/assets/header.js` → **404** (expected — the
|
||||
header code ships inside each page bundle); all four bundles are
|
||||
minified and contain the header markers (`sign-out-btn`,
|
||||
`/api/logout`)
|
||||
- No CDN rule: `cdn_violations=0` across all four served pages
|
||||
- `direct_header_script_tags=0`
|
||||
- → **`SMOKE_PASS`**
|
||||
|
||||
## Task 02 — hermetic coverage pin + validation
|
||||
|
||||
`tests/integration/test_containerfile_assets.py` (6 tests, no podman /
|
||||
no network / no DB — parses `Containerfile` + `frontend/` as text):
|
||||
|
||||
1. `test_every_html_page_is_copied_into_stage1` — cp set == pages on disk
|
||||
2. `test_every_local_asset_reference_is_produced` — every `assets/`
|
||||
src/href has a stage-1 producer
|
||||
3. `test_page_module_scripts_are_bundled` — HTML module set == esbuild
|
||||
`--bundle` input set
|
||||
4. `test_header_module_is_imported_not_directly_loaded` — no direct
|
||||
header.js tag anywhere; every page script imports `./header.js`
|
||||
relatively
|
||||
5. `test_markdown_js_is_a_produced_classic_script` — minified without
|
||||
`--bundle`; no top-level import/export in the source
|
||||
6. `test_esbuild_stays_pinned` — concrete `esbuild@X.Y.Z`, no floating
|
||||
spec
|
||||
|
||||
Mutation-checked while authoring: removing `login.html` from the cp
|
||||
line (→ test 1 fails), `esbuild@latest` (→ test 6 fails), deleting the
|
||||
`markdown.js` line (→ tests 2+5 fail), deleting the `document.js`
|
||||
bundle line (→ test 3 fails), re-adding a direct header.js tag (→
|
||||
tests 3+4 fail). All pins fire.
|
||||
|
||||
### Test / lint / coverage results (exact commands)
|
||||
|
||||
| command | result |
|
||||
|---|---|
|
||||
| `uv run pytest tests/integration/test_containerfile_assets.py -v --no-cov` | 6 passed |
|
||||
| `uv run pytest tests/e2e/test_smoke.py -v --no-cov` (isolated) | **3 passed** (`e2e_smoke.log`) |
|
||||
| `uv run pytest tests/e2e/test_shared_header.py -v --no-cov` (isolated) | **6 passed** (`e2e_shared_header.log`) |
|
||||
| `uv run pytest tests/e2e/test_chat_persistence.py -v --no-cov` (isolated) | **4 passed** (`e2e_chat_persistence.log`) |
|
||||
| `uv run pytest` | **335 passed** (329 → 335: the six new pins) |
|
||||
| `uv run pytest --cov=app --cov-report=term-missing` | **335 passed; TOTAL 1109 stmts, 14 missed, 99%** (≥ today's 99%; >90% gate) |
|
||||
| `uv run ruff check . && uv run pyright` | **All checks passed! / 0 errors, 0 warnings** |
|
||||
|
||||
Dev-server behavior unchanged: the tag removal is covered by the three
|
||||
isolated E2E regressions above (browser-level header init, sign-out
|
||||
binding exactly once, chat persistence across navigation).
|
||||
|
||||
## Decisions
|
||||
|
||||
- Relative imports over an esbuild alias (owner-confirmed A4-1).
|
||||
- Direct header.js tags removed over shipping a raw header.js
|
||||
(owner-confirmed A4-2 — single module evaluation).
|
||||
- No Playwright suite for this phase (build/infrastructure): the gate
|
||||
is the hermetic integration pin + the real `podman build` + image
|
||||
smoke recorded above + the dev-server E2E regressions (A16 note in
|
||||
the phase plan).
|
||||
- No LOCKED anchor changed (A11 No-CDN and A16 honored).
|
||||
- Build-evidence image `localhost/brain-of-reese/app:phase23test` kept
|
||||
locally.
|
||||
Reference in New Issue
Block a user