feat(ci): enforce runtime-validation image separation (#69)
All checks were successful
CICD Start / Sanity and Base Decision (push) Successful in 17s
All checks were successful
CICD Start / Sanity and Base Decision (push) Successful in 17s
## 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
This commit was merged in pull request #69.
This commit is contained in:
268
scripts/verify-deployable-image-purity.sh
Normal file
268
scripts/verify-deployable-image-purity.sh
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user