Files
plex-playlist/scripts/verify-deployable-image-purity.sh
Xlorep DarkHelm c7540e97ad
All checks were successful
CICD Start / Sanity and Base Decision (push) Successful in 17s
feat(ci): enforce runtime-validation image separation (#69)
## Summary

Implements issue #59 by enforcing a hard boundary between CI validation tooling and deployable runtime images.

This PR:

- Adds automated deployable-runtime boundary checks in CI.
- Verifies deployable backend/frontend artifacts are free of CI/development tooling.
- Documents runtime-vs-validation ownership and enforcement behavior.

## What Changed

### CI workflow enforcement

- Updated `.gitea/workflows/docker-build-main.yaml` to:
  - Checkout additional verification inputs (`Dockerfile.backend`, `Dockerfile.frontend`, scripts, backend/frontend directories).
  - Run `scripts/check-dockerfile-boundaries.sh`.
  - Build deployable runtime images (`Dockerfile.backend`, `Dockerfile.frontend --target production`).
  - Run `scripts/verify-deployable-image-purity.sh` against both images before publishing CICD image.

- Updated `.gitea/workflows/cicd-checks.yaml` to add:
  - `dockerfile-boundary-check` job.
  - Boundary validation execution inside the CICD validation image.

### New enforcement scripts

- Added `scripts/check-dockerfile-boundaries.sh`:
  - Ensures deployable Dockerfiles do **not** reference CICD image paths (`cicd-base`, `CICD_BASE_IMAGE`, `Dockerfile.cicd*`, etc.).
  - Ensures deployable Dockerfiles do **not** include disallowed CI-only tooling tokens.
  - Enforces runtime base expectations:
    - Backend: `python:3.14-slim`
    - Frontend production target: `nginx:alpine`

- Added `scripts/verify-deployable-image-purity.sh`:
  - Baseline binary checks for disallowed tooling.
  - Backend-specific deep checks:
    - Python module import probes for disallowed CI/dev modules.
    - `pip show` package metadata checks for disallowed CI/dev packages.
  - Frontend-specific deep checks:
    - OS package metadata checks (`apk`/`dpkg` when available) for disallowed runtime leaks.
    - Directory-based checks for development package trees (`node_modules`, `.venv`, `site-packages`, `dist-packages` in sensitive paths).

## Documentation updates

- Updated `docs/DEVELOPMENT.md`:
  - Clarifies runtime-vs-validation enforcement and where checks run.
  - Notes purity checks include binaries and metadata artifacts.

- Updated `docs/CICD_MULTI_STAGE_BUILD.md`:
  - Adds explicit “Runtime Boundary Enforcement” section.
  - Documents metadata-level purity probes.

- Updated `docs/DEPLOYABLE_RUNTIME_CONTRACT.md`:
  - Replaces future-only language with current enforcement hooks.
  - Documents binary + metadata-level purity enforcement.

## Acceptance Criteria Mapping

1. **Deployable backend/frontend image paths do not require CI-only tool installation**
   - Enforced by:
     - `scripts/check-dockerfile-boundaries.sh`
     - `scripts/verify-deployable-image-purity.sh`
     - `docker-build-main.yaml` pre-publish gates

2. **Checks and tests execute in dedicated validation environment(s)**
   - Reinforced by:
     - `cicd-checks.yaml` boundary-check job running in CICD validation image
     - Existing check/test workflow usage of CICD image

3. **Workflow docs identify runtime vs validation concerns**
   - Addressed via updates to:
     - `docs/DEVELOPMENT.md`
     - `docs/CICD_MULTI_STAGE_BUILD.md`
     - `docs/DEPLOYABLE_RUNTIME_CONTRACT.md`

## Scope / Non-Goals

- Included:
  - Structural separation enforcement
  - Workflow-level guardrails
  - Documentation clarity and traceability

- Not included:
  - Full staging deployment wiring
  - Security policy redesign

## Notes for Reviewers

- Main enforcement path is in `docker-build-main.yaml` before CICD image publish.
- New scripts are intentionally fail-fast and policy-oriented.
- Existing deployable Dockerfiles currently satisfy the new gates.

Co-authored-by: copilotcoder <copilotcoder@darkhelm.org>
Reviewed-on: #69
2026-06-22 12:45:20 -04:00

269 lines
5.3 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
scripts/verify-deployable-image-purity.sh --image <image-ref> --profile <backend|frontend>
USAGE
}
image_ref=""
profile=""
while [[ $# -gt 0 ]]; do
case "$1" in
--image)
image_ref="${2:-}"
shift 2
;;
--profile)
profile="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "${image_ref}" || -z "${profile}" ]]; then
usage
exit 2
fi
case "${profile}" in
backend)
checks=(
ruff
pyright
pytest
pydoclint
xdoctest
pre-commit
yamllint
toml-sort
eslint
prettier
tsc
vitest
playwright
yarn
npm
node
pnpm
)
;;
frontend)
checks=(
ruff
pyright
pytest
pydoclint
xdoctest
pre-commit
yamllint
toml-sort
eslint
prettier
tsc
vitest
playwright
uv
python
python3
pip
pip3
node
npm
yarn
pnpm
)
;;
*)
echo "Unsupported profile: ${profile}" >&2
usage
exit 2
;;
esac
tmp_file="$(mktemp)"
trap 'rm -f "${tmp_file}"' EXIT
printf '%s\n' "${checks[@]}" > "${tmp_file}"
binary_violations="$(docker run --rm --entrypoint /bin/sh "${image_ref}" -c '
set -e
while IFS= read -r cmd; do
if command -v "$cmd" >/dev/null 2>&1; then
printf "%s\n" "$cmd"
fi
done
' < "${tmp_file}")"
if [[ -n "${binary_violations}" ]]; then
echo "❌ Found CI/development tooling binaries in ${profile} image: ${image_ref}" >&2
echo "${binary_violations}" >&2
exit 1
fi
case "${profile}" in
backend)
backend_python_modules=(
pytest
ruff
pyright
pydoclint
xdoctest
pre_commit
yamllint
toml_sort
playwright
)
backend_python_packages=(
pytest
ruff
pyright
pydoclint
xdoctest
pre-commit
yamllint
toml-sort
playwright
)
module_violations="$(docker run --rm --entrypoint /bin/sh \
-e MODULES="${backend_python_modules[*]}" \
"${image_ref}" -c '
set -e
PYTHON_BIN=""
if command -v python3 >/dev/null 2>&1; then
PYTHON_BIN="python3"
elif command -v python >/dev/null 2>&1; then
PYTHON_BIN="python"
fi
if [ -z "$PYTHON_BIN" ]; then
echo "__missing_python_runtime__"
exit 0
fi
for module in ${MODULES}; do
if "$PYTHON_BIN" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec(\"$module\") else 1)" >/dev/null 2>&1; then
printf "%s\n" "$module"
fi
done
')"
if echo "${module_violations}" | grep -q '__missing_python_runtime__'; then
echo "❌ Backend deployable image is missing python runtime: ${image_ref}" >&2
exit 1
fi
if [[ -n "${module_violations}" ]]; then
echo "❌ Found CI/development Python modules importable in backend image: ${image_ref}" >&2
echo "${module_violations}" >&2
exit 1
fi
pip_violations="$(docker run --rm --entrypoint /bin/sh \
-e PACKAGES="${backend_python_packages[*]}" \
"${image_ref}" -c '
set -e
PIP_BIN=""
if command -v pip3 >/dev/null 2>&1; then
PIP_BIN="pip3"
elif command -v pip >/dev/null 2>&1; then
PIP_BIN="pip"
fi
if [ -z "$PIP_BIN" ]; then
exit 0
fi
for package in ${PACKAGES}; do
if "$PIP_BIN" show "$package" >/dev/null 2>&1; then
printf "%s\n" "$package"
fi
done
')"
if [[ -n "${pip_violations}" ]]; then
echo "❌ Found CI/development Python packages in backend image metadata: ${image_ref}" >&2
echo "${pip_violations}" >&2
exit 1
fi
;;
frontend)
frontend_os_packages=(
nodejs
npm
yarn
python3
py3-pip
py3-setuptools
)
os_pkg_violations="$(docker run --rm --entrypoint /bin/sh \
-e PACKAGES="${frontend_os_packages[*]}" \
"${image_ref}" -c '
set -e
if command -v apk >/dev/null 2>&1; then
for package in ${PACKAGES}; do
if apk info -e "$package" >/dev/null 2>&1; then
printf "apk:%s\n" "$package"
fi
done
elif command -v dpkg-query >/dev/null 2>&1; then
for package in ${PACKAGES}; do
if dpkg-query -W -f='"'"'${db:Status-Status}'"'"' "$package" 2>/dev/null | grep -q '^installed$'; then
printf "dpkg:%s\n" "$package"
fi
done
fi
')"
if [[ -n "${os_pkg_violations}" ]]; then
echo "❌ Found CI/development OS packages in frontend runtime image: ${image_ref}" >&2
echo "${os_pkg_violations}" >&2
exit 1
fi
dir_violations="$(docker run --rm --entrypoint /bin/sh "${image_ref}" -c '
set -e
check_dir_tree() {
local base_dir="$1"
[ -d "$base_dir" ] || return 0
find "$base_dir" -maxdepth 5 -type d \
\( -name node_modules -o -name .venv -o -name site-packages -o -name dist-packages \) \
2>/dev/null || true
}
check_dir_tree /app
check_dir_tree /usr/local/lib
check_dir_tree /usr/lib
check_dir_tree /opt
')"
if [[ -n "${dir_violations}" ]]; then
echo "❌ Found development package directories in frontend runtime image: ${image_ref}" >&2
echo "${dir_violations}" >&2
exit 1
fi
;;
esac
echo "${profile} image is clean of disallowed CI/development tooling binaries and metadata"