--- phase: 16-ci-dependency-audit-and-security-checks plan: "06" subsystem: ci tags: [ci, docker, image-hygiene, boot-smoke, security, publish] dependency_graph: requires: ["16-01", "16-04"] provides: ["IMG-03"] affects: [".gitea/workflows/publish.yml"] tech_stack: added: [] patterns: - "Static assertion step: grep-based structural checks in shell before docker push" - "Boot-smoke: docker run with forbidden env combo + timeout + exit-code semantics" key_files: created: [] modified: - .gitea/workflows/publish.yml decisions: - "D-10 (16-06): Static assertions grep for 7 forbidden .dockerignore patterns + --target production pin; grep is substring-safe (apps/api/scripts matches apps/api/scripts/seed-credential.mjs)" - "D-10 (16-06): Boot-smoke treats EXIT==0 and EXIT==124 as failures; any other non-zero is a PASS — covers the D-08 guard's process.exit(1) path" - "D-10 (16-06): Both hygiene steps ordered strictly after build and before push; set -euo pipefail ensures any failure stops the job before push runs" metrics: duration: "1 minute" completed: "2026-06-13" tasks_completed: 2 files_modified: 1 requirements: [IMG-03] --- # Phase 16 Plan 06: Image Hygiene CI Assertions Summary **One-liner:** Publish-time CI assertions that block docker push when .dockerignore is incomplete, --target production is dropped, or the production image tolerates DEV_AUTH_BYPASS=true (D-10 / IMG-03). ## What Was Built Two CI assertion steps added to `.gitea/workflows/publish.yml`, inserted strictly between the `Build production image` step and the `Push image` step: **Step 1 — "Image hygiene — static assertions"** (`set -euo pipefail`): - Fails if `.dockerignore` is absent - Loops over 7 forbidden patterns (`.env`, `node_modules`, `apps/api/scripts`, `.git`, `.planning`, `apps/api/tests`, `apps/pwa/e2e`) and fails if any is missing from `.dockerignore` - Fails if `--target production` is no longer grep-able in `publish.yml` itself - Catches config drift that would ship secrets or dev files (T-16-19) **Step 2 — "Image hygiene — boot-smoke (must refuse dev-bypass in production)"** (`set -euo pipefail`): - Runs the freshly-built `sha_tag` image with `NODE_ENV=production DEV_AUTH_BYPASS=true` - `timeout 15` caps the run (T-16-21: prevents the job hanging if the guard does not fire) - EXIT==0 → image started → guard NOT working → FAIL - EXIT==124 → timeout → guard not firing → FAIL - Any other non-zero exit → image refused boot → PASS - Proves `assertNotDevBypassInProduction()` (16-01 / D-08) fires in the actual shipped image (T-16-18) The step order in the publish job is now: 1. Checkout 2. Compute image tags 3. Docker login 4. Build production image 5. Image hygiene — static assertions ← new 6. Image hygiene — boot-smoke ← new 7. Push image 8. Docker logout (if: always()) ## Task Commits | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | Split publish build and push into separate steps | bc00f3e | .gitea/workflows/publish.yml | | 2 | Add static image-hygiene assertion + boot-smoke before push | 27046db | .gitea/workflows/publish.yml | ## Deviations from Plan None — plan executed exactly as written. ## Verification - YAML parses cleanly (python3 yaml.safe_load confirmed) - Step ordering verified: assertions at indices 4-5, push at index 6 (between Build at 3 and Push at 6) - All 7 forbidden .dockerignore patterns confirmed present in .dockerignore - `--target production` grep confirmed to match within publish.yml - Dockerfile `ENV NODE_ENV=production` confirmed in production stage (line 45) — precondition for the smoke to be meaningful - `bootGuards.ts` `process.exit(1)` confirmed fires when `NODE_ENV=production AND DEV_AUTH_BYPASS=true` — that is what the smoke exercises ## Threat Coverage | Threat ID | Description | Mitigated By | |-----------|-------------|--------------| | T-16-18 | Regressed image that tolerates DEV_AUTH_BYPASS=true published | Boot-smoke step: fails push if image starts under forbidden combo | | T-16-19 | Future change removes .dockerignore or drops --target production | Static assertions step: fails push on any missing pattern or pin | | T-16-20 | Assertions run after push (bad image already published) | Ordering: both steps are before Push image step | | T-16-21 | Boot-smoke hangs if guard does not fire | `timeout 15`; EXIT==124 treated as failure | ## Known Stubs None. ## Self-Check Files modified: - `.gitea/workflows/publish.yml` — modified (confirmed by git log) Commits: - `bc00f3e` — ci(16-06): split publish build and push into separate steps - `27046db` — ci(16-06): add static image-hygiene assertion + boot-smoke before push ## Self-Check: PASSED