Files
brain-of-reese/.agent/reports/23_containerfile_build/phase_report.md
T

5.9 KiB

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.