feat(ci): enforce runtime-validation image separation
Some checks failed
CICD Start / Sanity and Base Decision (pull_request) Failing after 10m34s
Some checks failed
CICD Start / Sanity and Base Decision (pull_request) Failing after 10m34s
Add Dockerfile boundary checks and deployable image purity validation for backend/frontend runtime artifacts. Wire enforcement into CI workflows and document runtime-vs-validation ownership.
This commit is contained in:
@@ -106,6 +106,59 @@ jobs:
|
|||||||
echo "=== Kernel Tail ==="
|
echo "=== Kernel Tail ==="
|
||||||
dmesg | tail -n 120 || true
|
dmesg | tail -n 120 || true
|
||||||
|
|
||||||
|
dockerfile-boundary-check:
|
||||||
|
name: Dockerfile Runtime Boundary Check
|
||||||
|
runs-on: ubuntu-act
|
||||||
|
timeout-minutes: 10
|
||||||
|
needs: setup
|
||||||
|
steps:
|
||||||
|
- name: Configure registry host resolution
|
||||||
|
run: |
|
||||||
|
if ! grep -q "${GITEA_REGISTRY_HOST}" /etc/hosts; then
|
||||||
|
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Ensure CICD image is available
|
||||||
|
env:
|
||||||
|
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
|
||||||
|
run: |
|
||||||
|
IMAGE="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd:${HEAD_SHA}"
|
||||||
|
if docker image inspect "${IMAGE}" >/dev/null 2>&1; then
|
||||||
|
echo "Using cached CICD image: ${IMAGE}"
|
||||||
|
else
|
||||||
|
echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | docker login "http://${GITEA_REGISTRY}" -u "${{ github.actor }}" --password-stdin
|
||||||
|
pulled=false
|
||||||
|
for i in 1 2 3; do
|
||||||
|
echo "Pull attempt ${i}/3 for ${IMAGE}"
|
||||||
|
if docker pull "${IMAGE}"; then
|
||||||
|
pulled=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "${i}" -lt 3 ]; then
|
||||||
|
sleep_seconds=$((5 * i))
|
||||||
|
echo "Pull failed; retrying in ${sleep_seconds}s"
|
||||||
|
sleep "${sleep_seconds}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${pulled}" != "true" ]; then
|
||||||
|
echo "❌ Failed to pull CICD image after 3 attempts: ${IMAGE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Validate runtime Dockerfile boundaries in validation environment
|
||||||
|
env:
|
||||||
|
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
docker run --rm --entrypoint /bin/bash "${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd:${HEAD_SHA}" -c "
|
||||||
|
cd /workspace &&
|
||||||
|
bash scripts/check-dockerfile-boundaries.sh
|
||||||
|
"
|
||||||
|
|
||||||
|
- *failure_diagnostics_step
|
||||||
|
|
||||||
run-check:
|
run-check:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
# Run checks across the full act runner pool for maximum parallelism.
|
# Run checks across the full act runner pool for maximum parallelism.
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
RESOLVED_HEAD_SHA="${HEAD_SHA_INPUT:-${HEAD_SHA_FALLBACK}}"
|
RESOLVED_HEAD_SHA="${HEAD_SHA_INPUT:-${HEAD_SHA_FALLBACK}}"
|
||||||
echo "head_sha=${RESOLVED_HEAD_SHA}" >> "$GITHUB_OUTPUT"
|
echo "head_sha=${RESOLVED_HEAD_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Minimal checkout for build inputs
|
- name: Minimal checkout for build and verification inputs
|
||||||
env:
|
env:
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
HEAD_SHA: ${{ steps.meta.outputs.head_sha }}
|
HEAD_SHA: ${{ steps.meta.outputs.head_sha }}
|
||||||
@@ -212,9 +212,45 @@ jobs:
|
|||||||
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
|
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
|
||||||
git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1 || true
|
git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
git checkout FETCH_HEAD -- Dockerfile.cicd Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh
|
git checkout FETCH_HEAD -- \
|
||||||
|
.dockerignore \
|
||||||
|
Dockerfile.backend \
|
||||||
|
Dockerfile.frontend \
|
||||||
|
Dockerfile.cicd \
|
||||||
|
Dockerfile.cicd-base \
|
||||||
|
backend \
|
||||||
|
frontend \
|
||||||
|
scripts/compute-cicd-base-hash.sh \
|
||||||
|
scripts/check-dockerfile-boundaries.sh \
|
||||||
|
scripts/verify-deployable-image-purity.sh
|
||||||
chmod +x scripts/compute-cicd-base-hash.sh
|
chmod +x scripts/compute-cicd-base-hash.sh
|
||||||
|
|
||||||
|
- name: Verify deployable runtime boundaries
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
bash ./scripts/check-dockerfile-boundaries.sh
|
||||||
|
|
||||||
|
- name: Build and verify deployable runtime image purity
|
||||||
|
env:
|
||||||
|
HEAD_SHA: ${{ steps.meta.outputs.head_sha }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
docker build -f Dockerfile.backend \
|
||||||
|
-t deployable-backend:"${HEAD_SHA}" .
|
||||||
|
|
||||||
|
docker build -f Dockerfile.frontend \
|
||||||
|
--target production \
|
||||||
|
-t deployable-frontend:"${HEAD_SHA}" .
|
||||||
|
|
||||||
|
bash ./scripts/verify-deployable-image-purity.sh \
|
||||||
|
--image deployable-backend:"${HEAD_SHA}" \
|
||||||
|
--profile backend
|
||||||
|
|
||||||
|
bash ./scripts/verify-deployable-image-purity.sh \
|
||||||
|
--image deployable-frontend:"${HEAD_SHA}" \
|
||||||
|
--profile frontend
|
||||||
|
|
||||||
- name: Build and push complete CICD image
|
- name: Build and push complete CICD image
|
||||||
env:
|
env:
|
||||||
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
|
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
|
||||||
|
|||||||
@@ -148,6 +148,32 @@ jobs:
|
|||||||
`.gitea/workflows/cicd-tests.yaml` own CI validation and checks.
|
`.gitea/workflows/cicd-tests.yaml` own CI validation and checks.
|
||||||
- Main CI never rebuilds the base image locally.
|
- Main CI never rebuilds the base image locally.
|
||||||
|
|
||||||
|
### Runtime Boundary Enforcement
|
||||||
|
|
||||||
|
The main build workflow now enforces separation between validation environments
|
||||||
|
and deployable runtime artifacts before publishing the complete CICD image.
|
||||||
|
|
||||||
|
- Dockerfile boundary check:
|
||||||
|
- Script: `scripts/check-dockerfile-boundaries.sh`
|
||||||
|
- Validates `Dockerfile.backend` and `Dockerfile.frontend` do not reference
|
||||||
|
CICD image paths (`cicd-base`, `CICD_BASE_IMAGE`, `Dockerfile.cicd*`) and
|
||||||
|
do not install disallowed CI-only tooling.
|
||||||
|
- Deployable image purity check:
|
||||||
|
- Script: `scripts/verify-deployable-image-purity.sh`
|
||||||
|
- Builds deployable backend and frontend production images and verifies known
|
||||||
|
CI/development binaries are absent from final runtime artifacts.
|
||||||
|
- Performs profile-specific metadata checks:
|
||||||
|
- Backend: Python module import probes and pip metadata checks for
|
||||||
|
disallowed CI/development packages.
|
||||||
|
- Frontend: OS package metadata checks (apk/dpkg when available) and
|
||||||
|
development package directory detection.
|
||||||
|
|
||||||
|
Workflow location:
|
||||||
|
|
||||||
|
- `.gitea/workflows/docker-build-main.yaml`
|
||||||
|
- `Verify deployable runtime boundaries`
|
||||||
|
- `Build and verify deployable runtime image purity`
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
### Building Base Image
|
### Building Base Image
|
||||||
|
|||||||
@@ -145,12 +145,20 @@ Note:
|
|||||||
definition.
|
definition.
|
||||||
- Covered by: "Disallowed Tooling Classes in Deployable Runtime Images".
|
- Covered by: "Disallowed Tooling Classes in Deployable Runtime Images".
|
||||||
|
|
||||||
## Future Enforcement Hooks (Out of Scope for PP-58)
|
## Enforcement Hooks
|
||||||
|
|
||||||
Potential follow-up automation under epic #66:
|
Current enforcement implemented in CI:
|
||||||
|
|
||||||
|
- Dockerfile target boundary checks:
|
||||||
|
- Script: `scripts/check-dockerfile-boundaries.sh`
|
||||||
|
- Workflow: `.gitea/workflows/docker-build-main.yaml`
|
||||||
|
- Deployable runtime image purity checks:
|
||||||
|
- Script: `scripts/verify-deployable-image-purity.sh`
|
||||||
|
- Workflow: `.gitea/workflows/docker-build-main.yaml`
|
||||||
|
- Checks include binary presence and profile-specific package metadata probes
|
||||||
|
to detect CI/development tooling leakage.
|
||||||
|
|
||||||
|
Future follow-up automation under epic #66 may expand this with:
|
||||||
|
|
||||||
- Policy checks validating final image layers do not include disallowed tooling
|
|
||||||
classes.
|
|
||||||
- Contract tests that assert documented health/startup behavior.
|
- Contract tests that assert documented health/startup behavior.
|
||||||
- CI checks that verify Dockerfile target boundaries remain aligned with this
|
- Additional policy checks for drift detection and broader runtime compliance.
|
||||||
contract.
|
|
||||||
|
|||||||
@@ -34,9 +34,34 @@ When changing deployment behavior or image composition, treat
|
|||||||
|
|
||||||
Scope boundary:
|
Scope boundary:
|
||||||
|
|
||||||
- This repository separates contract definition from enforcement mechanics.
|
- Contract definition lives in `DEPLOYABLE_RUNTIME_CONTRACT.md`.
|
||||||
- CI workflow rewiring and test execution redesign are out of scope for PP-58
|
- CI enforcement now includes Dockerfile boundary checks and deployable image
|
||||||
and belong to follow-up work under epic #66.
|
purity checks in `.gitea/workflows/docker-build-main.yaml`.
|
||||||
|
- Broader workflow redesign and deployment wiring remain out of scope for this
|
||||||
|
repository's runtime contract document and belong to follow-up work under
|
||||||
|
epic #66.
|
||||||
|
|
||||||
|
## Runtime vs Validation Enforcement
|
||||||
|
|
||||||
|
Deployable runtime artifacts and validation environments are intentionally
|
||||||
|
separated.
|
||||||
|
|
||||||
|
- Validation tools (lint/typecheck/test/browser tooling) belong to CICD image
|
||||||
|
paths (`Dockerfile.cicd-base`, `Dockerfile.cicd`) and CI validation
|
||||||
|
workflows.
|
||||||
|
- Deployable runtime paths (`Dockerfile.backend`, `Dockerfile.frontend`
|
||||||
|
production target) must remain independent of CI-only tooling layers.
|
||||||
|
|
||||||
|
Automated checks:
|
||||||
|
|
||||||
|
- `scripts/check-dockerfile-boundaries.sh` validates deployable Dockerfiles do
|
||||||
|
not depend on CICD images or install disallowed CI-only tools.
|
||||||
|
- `scripts/verify-deployable-image-purity.sh` inspects built deployable images
|
||||||
|
and fails if CI/development binaries or package metadata artifacts are
|
||||||
|
present.
|
||||||
|
|
||||||
|
These checks run in `.gitea/workflows/docker-build-main.yaml` before publishing
|
||||||
|
the complete CICD image.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
72
scripts/check-dockerfile-boundaries.sh
Normal file
72
scripts/check-dockerfile-boundaries.sh
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
backend_file="Dockerfile.backend"
|
||||||
|
frontend_file="Dockerfile.frontend"
|
||||||
|
|
||||||
|
declare -a disallowed_references=(
|
||||||
|
"cicd-base"
|
||||||
|
"CICD_BASE_IMAGE"
|
||||||
|
"Dockerfile.cicd"
|
||||||
|
"Dockerfile.cicd-base"
|
||||||
|
"plex-playlist-cicd"
|
||||||
|
)
|
||||||
|
|
||||||
|
declare -a disallowed_runtime_tools=(
|
||||||
|
"ruff"
|
||||||
|
"pyright"
|
||||||
|
"pytest"
|
||||||
|
"pydoclint"
|
||||||
|
"xdoctest"
|
||||||
|
"pre-commit"
|
||||||
|
"yamllint"
|
||||||
|
"toml-sort"
|
||||||
|
"eslint"
|
||||||
|
"prettier"
|
||||||
|
"typescript"
|
||||||
|
"vitest"
|
||||||
|
"playwright"
|
||||||
|
)
|
||||||
|
|
||||||
|
check_absent() {
|
||||||
|
local file="$1"
|
||||||
|
local token="$2"
|
||||||
|
|
||||||
|
if grep -Eiv '^[[:space:]]*#' "${file}" | grep -Eiq "${token}"; then
|
||||||
|
echo "❌ Found disallowed token '${token}' in ${file}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
status=0
|
||||||
|
|
||||||
|
for token in "${disallowed_references[@]}"; do
|
||||||
|
check_absent "${backend_file}" "${token}" || status=1
|
||||||
|
check_absent "${frontend_file}" "${token}" || status=1
|
||||||
|
done
|
||||||
|
|
||||||
|
for token in "${disallowed_runtime_tools[@]}"; do
|
||||||
|
check_absent "${backend_file}" "${token}" || status=1
|
||||||
|
check_absent "${frontend_file}" "${token}" || status=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! grep -Eq '^FROM[[:space:]]+python:3\.14-slim' "${backend_file}"; then
|
||||||
|
echo "❌ ${backend_file} must use python:3.14-slim as runtime base" >&2
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^FROM[[:space:]]+nginx:alpine[[:space:]]+AS[[:space:]]+production' "${frontend_file}"; then
|
||||||
|
echo "❌ ${frontend_file} must use nginx:alpine for production target" >&2
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${status}" -ne 0 ]]; then
|
||||||
|
exit "${status}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Dockerfile runtime boundaries are valid"
|
||||||
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