diff --git a/scripts/linting/lint-summary.sh b/scripts/linting/lint-summary.sh index 2464b7b..d12895b 100755 --- a/scripts/linting/lint-summary.sh +++ b/scripts/linting/lint-summary.sh @@ -136,9 +136,8 @@ echo "$STACKS_JSON" | jq -r '.[]' | while read -r stack; do echo "" # Show filtered errors (remove environment variable warnings) DOCKER_ERRORS=$(docker compose --env-file "$TEMP_ENV" -f "./$stack/compose.yaml" config 2>&1 | \ - grep -v "WARNING.*interpolat" | \ - grep -v "WARNING.*environment variable" | \ - grep -v "WARNING.*not set" || echo "Configuration errors detected") + grep -viE 'warning.*(interpolat|environment variable|not set)' \ + || echo "Configuration errors detected") if [[ -n "$DOCKER_ERRORS" && "$DOCKER_ERRORS" != "Configuration errors detected" ]]; then # shellcheck disable=SC2001 # sed is appropriate for multi-line prefix addition diff --git a/scripts/linting/validate-image-platforms.sh b/scripts/linting/validate-image-platforms.sh index d4b9477..1ae1465 100755 --- a/scripts/linting/validate-image-platforms.sh +++ b/scripts/linting/validate-image-platforms.sh @@ -83,17 +83,45 @@ echo " Target platforms: ${REQ_PLATFORMS[*]}" print_separator TEMP_ENV=$(mktemp) -trap 'rm -f "$TEMP_ENV"' EXIT +IMAGES_OUT=$(mktemp) +IMAGES_ERR=$(mktemp) +trap 'rm -f "$TEMP_ENV" "$IMAGES_OUT" "$IMAGES_ERR"' EXIT create_temp_env "$COMPOSE_FILE" "$TEMP_ENV" -# `docker compose config --images` emits one fully-resolved image ref per line. +# `docker compose config --images` emits one fully-resolved image ref per line +# on stdout; warnings go to stderr. Capture the two separately and check the +# exit status. +# +# This previously piped straight into the read loop with `2>/dev/null`, which +# discarded the diagnostics AND the exit status: any failure to resolve the +# file yielded an empty list, and the zero-images branch below then reported +# PASS. A check that verified nothing must never report success - that is the +# one way a Compose output change could break this silently rather than loudly. +if ! docker compose --env-file "$TEMP_ENV" -f "$COMPOSE_FILE" config --images \ + >"$IMAGES_OUT" 2>"$IMAGES_ERR"; then + log_error "✗ 'docker compose config --images' failed for $COMPOSE_FILE" + echo " Compose could not resolve the file, so no platform check was possible:" + sed 's/^/ /' "$IMAGES_ERR" + exit 1 +fi + IMAGES=() while IFS= read -r line; do [[ -n "$line" ]] && IMAGES+=("$line") -done < <(docker compose --env-file "$TEMP_ENV" -f "$COMPOSE_FILE" config --images 2>/dev/null | sort -u) +done < <(sort -u "$IMAGES_OUT") if [[ ${#IMAGES[@]} -eq 0 ]]; then - echo "ℹ️ No images resolved from $COMPOSE_FILE — nothing to check." + # Zero images is legitimate only if the file declares no services at all, or + # only `build:` ones. If it declares services yet resolved no image refs, the + # output is not the shape this script expects - fail loudly instead of + # claiming a pass over an empty set. + if docker compose --env-file "$TEMP_ENV" -f "$COMPOSE_FILE" config --services 2>/dev/null | grep -q .; then + log_error "✗ $COMPOSE_FILE declares services but resolved 0 image refs." + log_error " 'config --images' returned nothing parseable - check whether the" + log_error " Compose output format changed. Refusing to pass an unverified stack." + exit 1 + fi + echo "ℹ️ No images resolved from $COMPOSE_FILE — no services declared, nothing to check." exit 0 fi diff --git a/scripts/linting/validate-stack.sh b/scripts/linting/validate-stack.sh index 4c4afbf..e4ada40 100755 --- a/scripts/linting/validate-stack.sh +++ b/scripts/linting/validate-stack.sh @@ -71,12 +71,21 @@ create_temp_env "./$STACK/compose.yaml" "$TEMP_ENV" (set -o pipefail; docker compose --env-file "$TEMP_ENV" -f "./$STACK/compose.yaml" config 2>&1 | tee "$DOCKER_OUTPUT") & DOCKER_PID=$! -# Wait for both processes and capture exit codes -wait "$YAML_PID" -YAML_EXIT=$? - -wait "$DOCKER_PID" -DOCKER_EXIT=$? +# Wait for both processes and capture exit codes. +# +# `|| VAR=$?` is required, not stylistic: this script runs under `set -e` +# (line 14), so a bare `wait` on a failing child aborts the script right there +# - before the assignment, and before every formatted report below. That made +# the entire summary block unreachable for exactly the case it exists to +# serve: a stack that fails validation printed its raw tee'd output and then +# died silently, with no "Issues found", no fix hint and no overall status. +# The job still failed (non-zero exit), so CI verdicts were always correct - +# only the diagnostics were lost, which is why this went unnoticed. +YAML_EXIT=0 +wait "$YAML_PID" || YAML_EXIT=$? + +DOCKER_EXIT=0 +wait "$DOCKER_PID" || DOCKER_EXIT=$? # Filter Docker Compose output to remove environment variable warnings but keep real errors if [ "$DOCKER_EXIT" -eq 0 ]; then @@ -84,9 +93,13 @@ if [ "$DOCKER_EXIT" -eq 0 ]; then cp "$DOCKER_OUTPUT" "$DOCKER_FILTERED" else # If Docker Compose failed, filter out common environment variable warnings but keep errors - grep -v "WARNING.*interpolat" "$DOCKER_OUTPUT" | \ - grep -v "WARNING.*environment variable" | \ - grep -v "WARNING.*not set" > "$DOCKER_FILTERED" || cp "$DOCKER_OUTPUT" "$DOCKER_FILTERED" + # Compose 2.x prefixes these `WARNING: ...`; Compose 5.x emits + # `time="..." level=warning msg="..."` instead. Match case-insensitively on + # the bare token `warning`, which is a substring of both spellings, so the + # filter keeps working across the engine versions in use. The `|| cp` retains + # the original behaviour: if filtering removed every line, show the raw output. + grep -viE 'warning.*(interpolat|environment variable|not set)' \ + "$DOCKER_OUTPUT" > "$DOCKER_FILTERED" || cp "$DOCKER_OUTPUT" "$DOCKER_FILTERED" fi # Cleanup temporary env file