---
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
