#!/usr/bin/env bash # No argument preserves the CI build-and-test entry point. Pass an image tag to # test an already-built release without building, pulling, or publishing it. set -euo pipefail if [ "$#" -gt 1 ]; then echo "Usage: $0 [existing-image]" >&2 exit 2 fi script_directory="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repository_directory="$(cd -- "$script_directory/.." && pwd)" image="${1:-magent:ci}" size_limit_mb="${MAGENT_IMAGE_MAX_MB:-350}" managed_mode="${MAGENT_SMOKE_MANAGED:-false}" if [[ "$managed_mode" != true && "$managed_mode" != false ]]; then echo "MAGENT_SMOKE_MANAGED must be true or false." >&2 exit 2 fi if ! [[ "$size_limit_mb" =~ ^[1-9][0-9]*$ ]]; then echo "MAGENT_IMAGE_MAX_MB must be a positive integer (MiB)." >&2 exit 2 fi container_name="magent-ci-${GITHUB_RUN_ID:-local}-$$-${RANDOM}" volume_name="${container_name}-data" network_name="${container_name}-isolated" container_created=false volume_created=false network_created=false cleanup() { result=$? trap - EXIT if [ "$result" -ne 0 ] && [ "$container_created" = true ]; then # Only synthetic credentials/data enter this test container. docker logs --tail 100 "$container_name" >&2 || true fi if [ "$container_created" = true ]; then docker rm -f "$container_name" >/dev/null 2>&1 || true fi if [ "$volume_created" = true ]; then docker volume rm "$volume_name" >/dev/null 2>&1 || true fi if [ "$network_created" = true ]; then docker network rm "$network_name" >/dev/null 2>&1 || true fi exit "$result" } trap cleanup EXIT if [ "$#" -eq 0 ]; then docker build --tag "$image" "$repository_directory" fi image_size="$(docker image inspect --format '{{.Size}}' "$image")" image_id="$(docker image inspect --format '{{.Id}}' "$image")" if [ "$image_size" -gt "$((size_limit_mb * 1024 * 1024))" ]; then echo "Image exceeds ${size_limit_mb} MiB unpacked budget: ${image_size} bytes" >&2 exit 1 fi echo "Image size: ${image_size} bytes (budget ${size_limit_mb} MiB unpacked)" # Inspect the image's original filesystem before tmpfs or volume mounts could # hide accidentally shipped build caches or private files. docker run --rm --pull never --network none --read-only \ --cap-drop ALL --security-opt no-new-privileges:true \ --entrypoint python -i "$image_id" - packaging < "$script_directory/container_smoke.py" # An internal network prevents accidental external integration calls. No host # files, existing volumes, host credentials, or host ports are used. docker network create --internal "$network_name" >/dev/null network_created=true docker volume create "$volume_name" >/dev/null volume_created=true start_container() { local -a secret_environment if [ "$managed_mode" = true ]; then # Exercise the image defaults: no keys, origin or managed-mode variables. secret_environment=() else secret_environment=( --env JWT_SECRET=ci-only-secret-with-at-least-32-characters --env SETTINGS_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= --env SETUP_TOKEN=ci-only-setup-token-with-at-least-32-characters --env AUTH_COOKIE_SECURE=true --env MAGENT_APPLICATION_URL=https://magent-ci.example.test ) fi docker run --detach --name "$container_name" --pull never \ --network "$network_name" \ --read-only --cap-drop ALL --security-opt no-new-privileges:true \ --pids-limit 256 --memory 1g --cpus 2 \ --tmpfs /tmp:rw,noexec,nosuid,size=64m,uid=1000,gid=1000 \ --tmpfs /app/frontend/.next/cache:rw,noexec,nosuid,size=128m,uid=1000,gid=1000 \ --volume "$volume_name:/app/data" \ "${secret_environment[@]}" \ --env ADMIN_PASSWORD= \ --env AUTH_COOKIE_SAMESITE=strict \ --env BACKGROUND_TASKS_ENABLED=false \ --env MAGENT_METRICS_ENABLED=false \ "$image_id" >/dev/null container_created=true } wait_for_health() { local deadline=$((SECONDS + 150)) local status while true; do status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$container_name")" if [ "$status" = healthy ]; then return fi if [ "$status" = missing ] || [ "$SECONDS" -ge "$deadline" ]; then echo "Container did not become healthy within 150 seconds (status: $status)" >&2 return 1 fi if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != true ]; then echo "Container exited before becoming healthy" >&2 return 1 fi sleep 2 done } # Deliberately no root/chown helper: the image must initialize a fresh named # volume with correct ownership for its normal non-root runtime user. start_container wait_for_health docker exec -i "$container_name" python - fresh < "$script_directory/container_smoke.py" docker restart --time 15 "$container_name" >/dev/null wait_for_health docker exec -i "$container_name" python - persisted < "$script_directory/container_smoke.py" # Recreation proves database/configuration are in the volume, not merely in the # container's writable layer. Both test instances use the same immutable image. docker rm -f "$container_name" >/dev/null container_created=false start_container wait_for_health docker exec -i "$container_name" python - persisted < "$script_directory/container_smoke.py" echo "Container smoke passed: fresh install, security headers, assets, login, backup/restore, restart, recreation."