From 78fe011fdd7d9df1ffdcf9378cdfdf6a36d8edd8 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 23 Oct 2026 08:36:40 +0000 Subject: [PATCH] fix(ci): fail Azure deploys on unreadable terraform outputs and show logs after a failed health check The Azure Terraform Apply step read its outputs with `terraform output -raw X 2>/dev/null | grep -v '::' || echo ""`, which hid terraform's error and published an empty step output. The failure then surfaced a job later as "Failed to get app URL" or "Terraform outputs missing", far from its cause. The outputs are now read by scripts/read-tf-outputs.sh, which keeps terraform's stderr, still drops the setup-terraform wrapper's `::` lines, requires exactly one non-empty value line per output and writes nothing to GITHUB_OUTPUT unless every output was read. The "Check Container App logs" step had no condition, so it was skipped exactly when the readiness or smoke gate failed. It now runs with `always()` once the Azure login succeeded, and warns instead of failing when there is no app to read from; an az lookup error still fails it. scripts/test-read-tf-outputs.sh tests the script with a stub terraform and executes the workflow's own Apply and log-step run blocks with stub terraform/az; it runs in CI next to the smoke-gate self-test. test-wait-for-healthy.sh gains the empty-body and non-JSON response cases #217 asked for. Closes #438 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 3 + .github/workflows/deploy-azure.yml | 29 ++- scripts/read-tf-outputs.sh | 91 +++++++ scripts/test-read-tf-outputs.sh | 374 +++++++++++++++++++++++++++++ scripts/test-wait-for-healthy.sh | 15 ++ 5 files changed, 500 insertions(+), 12 deletions(-) create mode 100755 scripts/read-tf-outputs.sh create mode 100755 scripts/test-read-tf-outputs.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8a230650..e827331c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1278,6 +1278,9 @@ jobs: - name: Run smoke-gate self-test run: bash scripts/test-wait-for-healthy.sh + - name: Run Terraform output reader self-test + run: bash scripts/test-read-tf-outputs.sh + # Execute database-migration.yml's down-migration gate (#68): the gate script # over adversarial inputs, then the workflow's own validate and migrate-* run # blocks extracted from the YAML with a stub `migrate`, so a request that diff --git a/.github/workflows/deploy-azure.yml b/.github/workflows/deploy-azure.yml index 1d819cc2c..3ebd975c0 100644 --- a/.github/workflows/deploy-azure.yml +++ b/.github/workflows/deploy-azure.yml @@ -282,16 +282,13 @@ jobs: cd terraform/environments/azure terraform apply -auto-approve tfplan 2>&1 | tee "${RUNNER_TEMP}/tf-apply.log" - # Get app URL - APP_URL=$(terraform output -raw container_app_url 2>/dev/null | grep -v '::' || echo "") - echo "app_url=$APP_URL" >> "$GITHUB_OUTPUT" - - # Get app name - APP_NAME=$(terraform output -raw container_app_name 2>/dev/null | grep -v '::' || echo "") - echo "app_name=$APP_NAME" >> "$GITHUB_OUTPUT" - - RESOURCE_GROUP_NAME=$(terraform output -raw resource_group_name 2>/dev/null | grep -v '::' || echo "") - echo "resource_group_name=$RESOURCE_GROUP_NAME" >> "$GITHUB_OUTPUT" + # A missing or unreadable output fails this step with terraform's own + # error, instead of passing an empty value on to test-deployment + # (#438). The script still drops setup-terraform's `::debug::` lines. + bash "${GITHUB_WORKSPACE}/scripts/read-tf-outputs.sh" \ + app_url=container_app_url \ + app_name=container_app_name \ + resource_group_name=resource_group_name # The provider fails the data read outright when the custom role is absent, # so a Terraform precondition can never run for this case. Issue #1794: the @@ -413,6 +410,7 @@ jobs: steps: - name: Azure Login + id: azure-login uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -472,14 +470,21 @@ jobs: HEALTH_INTERVAL_SECONDS: "5" run: bash scripts/wait-for-healthy.sh "$APP_URL" + # Runs when an earlier step failed too (#438): a failed readiness or smoke + # gate is exactly when the logs are needed. It still needs a working + # Azure login. Missing outputs only warn here, because build-and-deploy + # now fails at the source when it cannot read them, so this job never + # starts without them; failing here would only bury the real error. An + # az lookup error still fails the step (#217). - name: Check Container App logs + if: always() && steps.azure-login.outcome == 'success' env: APP_NAME: ${{ needs.build-and-deploy.outputs.app_name }} RESOURCE_GROUP_NAME: ${{ needs.build-and-deploy.outputs.resource_group_name }} run: | if [ -z "$APP_NAME" ] || [ -z "$RESOURCE_GROUP_NAME" ]; then - echo "::error::Terraform outputs missing: app_name='$APP_NAME' resource_group_name='$RESOURCE_GROUP_NAME'" - exit 1 + echo "::warning::No Container App to read logs from: app_name='$APP_NAME' resource_group_name='$RESOURCE_GROUP_NAME'" + exit 0 fi echo "Recent Container App logs:" az containerapp logs show \ diff --git a/scripts/read-tf-outputs.sh b/scripts/read-tf-outputs.sh new file mode 100755 index 000000000..cc69768b0 --- /dev/null +++ b/scripts/read-tf-outputs.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# read-tf-outputs.sh KEY=OUTPUT_NAME [KEY=OUTPUT_NAME ...] +# +# Reads Terraform outputs from the working directory with +# `terraform output -raw OUTPUT_NAME` and appends them to $GITHUB_OUTPUT as +# KEY=value step outputs. Run it from the Terraform root module directory, +# after the apply. +# +# It fails loudly, at the source, instead of handing an empty value to a later +# job (#438). The inline form it replaces in deploy-azure.yml was +# +# APP_URL=$(terraform output -raw container_app_url 2>/dev/null | grep -v '::' || echo "") +# +# which hid terraform's error message and turned any failure (missing output, +# state read error, renamed output) into an empty step output. The failure then +# surfaced jobs later, as "Failed to get app URL" or "Terraform outputs +# missing", far from its cause. +# +# Wrapper noise: hashicorp/setup-terraform installs a `terraform` wrapper that +# writes `::debug::` workflow-command lines to stdout next to the real output +# (3d0d9dee). Lines containing `::` are dropped, as before; what is left must +# be exactly one non-empty line. Anything else (nothing left, or several lines) +# is an error that prints what terraform returned, rather than a guess. +# +# Every output is read before anything is written, so a failure leaves +# $GITHUB_OUTPUT untouched rather than half-written. +# +# Exit codes: +# 0 every output was read and written +# 1 terraform failed, or an output was empty or not a single line +# 2 usage error (no arguments, malformed KEY=OUTPUT_NAME, GITHUB_OUTPUT unset) + +set -euo pipefail + +usage() { + echo "usage: $(basename "$0") KEY=OUTPUT_NAME [KEY=OUTPUT_NAME ...]" >&2 + echo " appends KEY= to \$GITHUB_OUTPUT" >&2 + exit 2 +} + +[[ $# -ge 1 ]] || usage +if [[ -z "${GITHUB_OUTPUT:-}" ]]; then + echo "::error::GITHUB_OUTPUT is not set" >&2 + exit 2 +fi + +ident='^[A-Za-z_][A-Za-z0-9_]*$' +for pair in "$@"; do + key="${pair%%=*}" + name="${pair#*=}" + if [[ "$pair" != *=* || ! "$key" =~ $ident || ! "$name" =~ $ident ]]; then + echo "::error::malformed argument '${pair}', expected KEY=OUTPUT_NAME" >&2 + exit 2 + fi +done + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +lines=() +for pair in "$@"; do + key="${pair%%=*}" + name="${pair#*=}" + + # stderr is left alone so terraform's own error message reaches the log. + if ! terraform output -raw "$name" > "$tmp/raw"; then + echo "::error::terraform output -raw ${name} failed; see the terraform error above" >&2 + exit 1 + fi + + # `|| true`: grep exits 1 when it selects nothing, which is judged below. + # grep also terminates the last line, which -raw leaves without a newline. + grep -v '::' "$tmp/raw" | grep -v '^[[:space:]]*$' > "$tmp/value" || true + count=$(wc -l < "$tmp/value") + + if ((count != 1)); then + if ((count == 0)); then + reason="is empty" + else + reason="has ${count} lines, expected one" + fi + echo "::error::terraform output ${name} ${reason}. Raw stdout:" >&2 + sed 's/^/ | /' "$tmp/raw" >&2 + exit 1 + fi + + lines+=("${key}=$(cat "$tmp/value")") + echo "Read terraform output ${name} into ${key}" +done + +printf '%s\n' "${lines[@]}" >> "$GITHUB_OUTPUT" diff --git a/scripts/test-read-tf-outputs.sh b/scripts/test-read-tf-outputs.sh new file mode 100755 index 000000000..196601067 --- /dev/null +++ b/scripts/test-read-tf-outputs.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash +# test-read-tf-outputs.sh +# +# Offline test for read-tf-outputs.sh and its wiring into deploy-azure.yml +# (#438). `terraform` and `az` are stubs on PATH; nothing reaches a real +# backend or Azure. +# +# 1. read-tf-outputs.sh itself: clean values, setup-terraform wrapper noise, +# a failing `terraform output`, empty and multi-line values, usage errors, +# and that a failure leaves $GITHUB_OUTPUT untouched. +# 2. deploy-azure.yml's "Terraform Apply" run block, extracted and executed, +# so a missing output is proven to fail the step that reads it (the old +# `2>/dev/null ... || echo ""` form passed with an empty value). +# 3. deploy-azure.yml's "Check Container App logs" step: it runs after a +# failed step (`always()`), skips with a warning when there is no app to +# read from, and still fails on an az lookup error. +# +# WORKFLOW may point at another copy of deploy-azure.yml, e.g. the pre-fix one, +# to show the workflow cases fail there. +# +# Exits 0 when all cases pass; exits 1 on any failure. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +SUT="${SCRIPT_DIR}/read-tf-outputs.sh" +WORKFLOW="${WORKFLOW:-${REPO_ROOT}/.github/workflows/deploy-azure.yml}" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/bin" "$WORK/tf" + +pass=0 +fail=0 + +note_pass() { + echo "PASS: $1" + ((pass++)) || true +} + +note_fail() { + echo "FAIL: $1" + ((fail++)) || true +} + +indent() { + local line + while IFS= read -r line; do printf ' %s\n' "$line"; done <<<"$1" +} + +# Stub terraform. `terraform output -raw NAME` prints $TF_STUB/NAME.out +# verbatim (no newline added, as -raw does), NAME.err to stderr, and exits with +# NAME.rc (default 0). A NAME with no .out file behaves like a missing output. +# Every call is logged to $TF_STUB/calls. `terraform apply` succeeds. +cat > "$WORK/bin/terraform" <<'STUB' +#!/usr/bin/env bash +echo "$*" >> "$TF_STUB/calls" +case "$1" in + apply) echo "Apply complete!"; exit 0 ;; + output) ;; + *) echo "stub terraform: unexpected command $1" >&2; exit 64 ;; +esac +[ "$2" = "-raw" ] || { echo "stub terraform: expected -raw" >&2; exit 64; } +name="$3" +if [ ! -f "$TF_STUB/$name.out" ]; then + printf '\nError: Output "%s" not found\n' "$name" >&2 + exit 1 +fi +cat "$TF_STUB/$name.out" +[ -f "$TF_STUB/$name.err" ] && cat "$TF_STUB/$name.err" >&2 +exit "$(cat "$TF_STUB/$name.rc" 2>/dev/null || echo 0)" +STUB + +# Stub az. Logs its arguments to $AZ_LOG and exits with $AZ_RC (default 0). +cat > "$WORK/bin/az" <<'STUB' +#!/usr/bin/env bash +echo "$*" >> "$AZ_LOG" +if [ "${AZ_RC:-0}" != 0 ]; then + echo "ERROR: (ResourceGroupNotFound) Resource group 'rg-x' could not be found." >&2 + exit "$AZ_RC" +fi +echo "log line" +STUB +chmod +x "$WORK/bin/terraform" "$WORK/bin/az" + +# reset_tf clears the stub's outputs and call log. +reset_tf() { + rm -rf "$WORK/tf" + mkdir -p "$WORK/tf" + : > "$WORK/tf/calls" +} + +# set_output NAME VALUE [RC]: VALUE is written with printf %b, no newline. +set_output() { + printf '%b' "$2" > "$WORK/tf/$1.out" + [ -z "${3:-}" ] || echo "$3" > "$WORK/tf/$1.rc" +} + +# run_sut ARGS...: runs the script, setting RC, OUT (stdout+stderr) and GHO +# (the resulting $GITHUB_OUTPUT contents, which starts with one sentinel line). +run_sut() { + printf 'existing=1\n' > "$WORK/gho" + RC=0 + OUT=$(PATH="$WORK/bin:$PATH" TF_STUB="$WORK/tf" GITHUB_OUTPUT="$WORK/gho" \ + bash "$SUT" "$@" 2>&1) || RC=$? + GHO="$(cat "$WORK/gho")" +} + +# expect LABEL EXPECTED_RC EXPECTED_GHO [OUTPUT_SUBSTRING] +expect() { + local label="$1" want_rc="$2" want_gho="$3" needle="${4:-}" + if [[ "$RC" -ne "$want_rc" ]]; then + note_fail "$label (expected exit $want_rc, got $RC)" + indent "$OUT" + elif [[ "$GHO" != "$want_gho" ]]; then + note_fail "$label (GITHUB_OUTPUT mismatch)" + printf ' want: %q\n got: %q\n' "$want_gho" "$GHO" + elif [[ -n "$needle" && "$OUT" != *"$needle"* ]]; then + note_fail "$label (output lacks '$needle')" + indent "$OUT" + else + note_pass "$label" + fi +} + +# --- 1. read-tf-outputs.sh --------------------------------------------------- + +reset_tf +set_output container_app_url 'https://app.example.azurecontainerapps.io' +set_output container_app_name 'cudly-dev-app' +set_output resource_group_name 'cudly-dev-rg' +run_sut app_url=container_app_url app_name=container_app_name resource_group_name=resource_group_name +expect "three clean outputs are written in order" 0 \ + $'existing=1\napp_url=https://app.example.azurecontainerapps.io\napp_name=cudly-dev-app\nresource_group_name=cudly-dev-rg' +if [[ "$(cat "$WORK/tf/calls")" == $'output -raw container_app_url\noutput -raw container_app_name\noutput -raw resource_group_name' ]]; then + note_pass "terraform is called as 'output -raw NAME' for each output" +else + note_fail "terraform calls were: $(cat "$WORK/tf/calls")" +fi + +# What the setup-terraform wrapper prints around the value. +reset_tf +set_output container_app_url 'https://app.example.azurecontainerapps.io\n::debug::Terraform exited with code 0.\n::debug::stdout: https://app.example.azurecontainerapps.io\n::debug::stderr: \n::debug::exitcode: 0\n' +run_sut app_url=container_app_url +expect "setup-terraform wrapper ::debug:: lines are dropped" 0 \ + $'existing=1\napp_url=https://app.example.azurecontainerapps.io' + +reset_tf +set_output container_app_url 'https://app.example.azurecontainerapps.io\n' +run_sut app_url=container_app_url +expect "a trailing newline is not part of the value" 0 \ + $'existing=1\napp_url=https://app.example.azurecontainerapps.io' + +# The #438 case: the output does not exist. terraform's error must reach the +# log, the script must fail, and nothing must be written, including the +# outputs read successfully before the failing one. +reset_tf +set_output container_app_url 'https://app.example.azurecontainerapps.io' +run_sut app_url=container_app_url app_name=container_app_name +expect "missing output fails with terraform's error" 1 'existing=1' 'Error: Output "container_app_name" not found' +expect "missing output names the failing output" 1 'existing=1' '::error::terraform output -raw container_app_name failed' + +reset_tf +set_output container_app_url '' 1 +printf 'Error: Failed to load state: blob not found\n' > "$WORK/tf/container_app_url.err" +run_sut app_url=container_app_url +expect "state read error fails and keeps the stderr" 1 'existing=1' 'Failed to load state' + +reset_tf +set_output container_app_url '' +run_sut app_url=container_app_url +expect "empty value fails" 1 'existing=1' 'terraform output container_app_url is empty' + +reset_tf +set_output container_app_url '\n \n' +run_sut app_url=container_app_url +expect "whitespace-only value fails" 1 'existing=1' 'is empty' + +reset_tf +set_output container_app_url '::debug::Terraform exited with code 0.\n::debug::exitcode: 0\n' +run_sut app_url=container_app_url +expect "wrapper noise with no value fails and shows the raw stdout" 1 'existing=1' '| ::debug::exitcode: 0' + +reset_tf +set_output container_app_url 'Warning: something\nhttps://app.example.azurecontainerapps.io' +run_sut app_url=container_app_url +expect "two value lines fail instead of guessing" 1 'existing=1' 'has 2 lines, expected one' + +reset_tf +run_sut +expect "no arguments is a usage error" 2 'existing=1' 'usage:' +run_sut app_url +expect "argument without = is a usage error" 2 'existing=1' "malformed argument 'app_url'" +run_sut 'app url=container_app_url' +expect "key with a space is a usage error" 2 'existing=1' 'malformed argument' +run_sut 'app_url=container_app_url;id' +expect "output name with shell metacharacters is a usage error" 2 'existing=1' 'malformed argument' +run_sut app_url= +expect "empty output name is a usage error" 2 'existing=1' 'malformed argument' +if [[ ! -s "$WORK/tf/calls" ]]; then + note_pass "usage errors never call terraform" +else + note_fail "usage errors called terraform: $(cat "$WORK/tf/calls")" +fi + +RC=0 +OUT=$(env -u GITHUB_OUTPUT PATH="$WORK/bin:$PATH" TF_STUB="$WORK/tf" \ + bash "$SUT" app_url=container_app_url 2>&1) || RC=$? +GHO='existing=1' +expect "unset GITHUB_OUTPUT is a usage error" 2 'existing=1' 'GITHUB_OUTPUT is not set' + +# --- Workflow extraction ----------------------------------------------------- + +# extract_step JOB STEP_NAME prints the YAML lines of one step: from its +# `- name: STEP_NAME` line up to the next step or job. Same helper as +# test-migration-down-gate.sh. +extract_step() { + awk -v job=" $1:" -v step="- name: $2" ' + $0 == job { injob = 1; next } + injob && /^ [A-Za-z0-9_-]+:[[:space:]]*$/ { exit } + injob && !instep { + line = $0; sub(/^[[:space:]]+/, "", line) + if (line == step) { instep = 1; match($0, /^ */); ind = RLENGTH; print; next } + } + instep { + if ($0 ~ /^[[:space:]]*-[[:space:]]/) { match($0, /^ */); if (RLENGTH <= ind) exit } + print + } + ' "$WORKFLOW" +} + +# run_block prints the dedented body of the `run: |` block in a step's text. +run_block() { + awk ' + !inrun && /^[[:space:]]+run: \|[[:space:]]*$/ { inrun = 1; match($0, /^ */); ind = RLENGTH; next } + inrun { + if ($0 ~ /^[[:space:]]*$/) { print ""; next } + match($0, /^ */) + if (RLENGTH <= ind) exit + print substr($0, ind + 3) + } + ' +} + +# load_block VAR JOB STEP stores the run block in VAR and the step text in +# STEP_TEXT, failing the test if the block is missing or holds a ${{ }} +# expression this harness cannot evaluate. +load_block() { + local step out + step="$(extract_step "$2" "$3")" + out="$(run_block <<<"$step")" + if [[ -z "$out" ]]; then + note_fail "workflow: no run block found for $2 / $3" + return 1 + fi + if [[ "$out" == *"\${{"* ]]; then + note_fail "workflow: $2 / $3 run block holds a \${{ }} expression; move it to env:" + return 1 + fi + STEP_TEXT="$step" + printf -v "$1" '%s' "$out" +} + +# --- 2. Terraform Apply step ------------------------------------------------- + +# run_apply_block: runs the extracted block the way the runner does (bash -e, +# from the workspace root), against a scratch workspace whose scripts/ is the +# real one. Sets RC, OUT and GHO like run_sut. +run_apply_block() { + local ws="$WORK/ws" + rm -rf "$ws" + mkdir -p "$ws/terraform/environments/azure" "$WORK/runner_temp" + ln -s "$REPO_ROOT/scripts" "$ws/scripts" + printf 'existing=1\n' > "$WORK/gho" + RC=0 + OUT=$(cd "$ws" && PATH="$WORK/bin:$PATH" TF_STUB="$WORK/tf" GITHUB_OUTPUT="$WORK/gho" \ + GITHUB_WORKSPACE="$ws" RUNNER_TEMP="$WORK/runner_temp" \ + bash -e -c "$APPLY_BLOCK" 2>&1) || RC=$? + GHO="$(cat "$WORK/gho")" +} + +STEP_TEXT="" +APPLY_BLOCK="" +if load_block APPLY_BLOCK build-and-deploy "Terraform Apply"; then + reset_tf + set_output container_app_url 'https://app.example.azurecontainerapps.io\n::debug::exitcode: 0\n' + set_output container_app_name 'cudly-dev-app' + set_output resource_group_name 'cudly-dev-rg' + run_apply_block + expect "workflow apply: outputs reach GITHUB_OUTPUT" 0 \ + $'existing=1\napp_url=https://app.example.azurecontainerapps.io\napp_name=cudly-dev-app\nresource_group_name=cudly-dev-rg' + + # Pre-fix this exited 0 and wrote `resource_group_name=` (empty), so the + # failure surfaced later in test-deployment as "Terraform outputs missing". + reset_tf + set_output container_app_url 'https://app.example.azurecontainerapps.io' + set_output container_app_name 'cudly-dev-app' + run_apply_block + if [[ "$RC" -ne 0 && "$OUT" == *'Error: Output "resource_group_name" not found'* ]]; then + note_pass "workflow apply: a missing output fails the step with terraform's error" + else + note_fail "workflow apply: a missing output did not fail the step loudly (exit $RC)" + indent "$OUT" + fi + if [[ "$GHO" != *"resource_group_name="* ]]; then + note_pass "workflow apply: no empty resource_group_name is published" + else + note_fail "workflow apply: published '$(grep resource_group_name= <<<"$GHO")'" + fi +fi + +if grep -nE 'terraform output.*(2>/dev/null|\|\| *echo)' "$WORKFLOW" > "$WORK/swallow"; then + note_fail "workflow: a terraform output error is still swallowed:" + sed 's/^/ /' "$WORK/swallow" +else + note_pass "workflow: no terraform output call discards its error" +fi + +# --- 3. Check Container App logs step ---------------------------------------- + +# run_logs_block APP_NAME RESOURCE_GROUP_NAME [AZ_RC]: sets RC, OUT and AZ. +run_logs_block() { + : > "$WORK/az.log" + RC=0 + OUT=$(PATH="$WORK/bin:$PATH" AZ_LOG="$WORK/az.log" AZ_RC="${3:-0}" \ + APP_NAME="$1" RESOURCE_GROUP_NAME="$2" bash -e -c "$LOGS_BLOCK" 2>&1) || RC=$? + AZ="$(cat "$WORK/az.log")" +} + +LOGS_BLOCK="" +if load_block LOGS_BLOCK test-deployment "Check Container App logs"; then + if_line="$(grep -E '^[[:space:]]+if:' <<<"$STEP_TEXT" || true)" + if [[ "$if_line" == *"always()"* ]]; then + note_pass "workflow logs: step runs after a failed health check (always())" + else + note_fail "workflow logs: step has no always() condition, so it is skipped exactly when the health check fails (if: '${if_line}')" + fi + + # The condition may reference the login step's outcome; that id must exist. + if [[ "$if_line" == *"steps.azure-login."* ]]; then + if extract_step test-deployment "Azure Login" | grep -qE '^[[:space:]]+id: azure-login[[:space:]]*$'; then + note_pass "workflow logs: steps.azure-login refers to the Azure Login step" + else + note_fail "workflow logs: condition uses steps.azure-login but the Azure Login step has no 'id: azure-login'" + fi + fi + + run_logs_block cudly-dev-app cudly-dev-rg + if [[ "$RC" -eq 0 && "$AZ" == "containerapp logs show --name cudly-dev-app --resource-group cudly-dev-rg --tail 50" ]]; then + note_pass "workflow logs: reads the app's logs" + else + note_fail "workflow logs: happy path (exit $RC, az: '$AZ')" + indent "$OUT" + fi + + run_logs_block cudly-dev-app cudly-dev-rg 3 + if [[ "$RC" -ne 0 && "$OUT" == *"ResourceGroupNotFound"* ]]; then + note_pass "workflow logs: an az lookup error still fails the step (#217)" + else + note_fail "workflow logs: an az lookup error was swallowed (exit $RC)" + fi + + run_logs_block "" "" + if [[ "$RC" -eq 0 && -z "$AZ" && "$OUT" == *"::warning::"* ]]; then + note_pass "workflow logs: no deployed app warns and skips az" + else + note_fail "workflow logs: missing outputs (exit $RC, az: '$AZ')" + indent "$OUT" + fi +fi + +echo "passed=$pass failed=$fail" +[[ "$fail" -eq 0 ]] diff --git a/scripts/test-wait-for-healthy.sh b/scripts/test-wait-for-healthy.sh index 89d8d371a..d562f9a35 100755 --- a/scripts/test-wait-for-healthy.sh +++ b/scripts/test-wait-for-healthy.sh @@ -24,6 +24,10 @@ mkdir -p "$WORK/bin" # M = HTTP 301 to https://app.example/ready with an empty body: what an # http:// URL gets from an HTTPS-only ALB listener (#647) # R = connection refused +# E = HTTP 503 with an empty body +# N = HTTP 502 with a non-JSON (HTML) body, as a proxy in front of the app +# answers while no replica is up +# Z = HTTP 200 with an empty body # Models the real curl flags the callers use: -o FILE, -w with %{http_code} and # %{redirect_url}, -f/--fail (exit 22 on >= 400, no body) and --fail-with-body # (exit 22 on >= 400, body still written). Without either, curl exits 0 for any @@ -72,6 +76,9 @@ case "${tokens[$i]}" in D) code=200; body="$degraded" ;; U) code=503; body="$degraded" ;; M) code=301; body=""; location="https://app.example/ready" ;; + E) code=503; body="" ;; + N) code=502; body='

502 Bad Gateway

' ;; + Z) code=200; body="" ;; R) echo "curl: (7) Failed to connect" >&2 [ -n "$wfmt" ] && printf '000 ' exit 7 ;; @@ -132,6 +139,14 @@ run_case "301 redirect forever fails and names the redirect target" 1 "M" "HTTP run_case "301 mid-streak resets it (H H M H H U, must not pass)" 1 "H H M H H U" "auth store not initialized" run_case "503 failure keeps the status and the body" 1 "U" "HTTP 503: {\"status\":\"degraded\"" +# #438: the remaining response shapes from #217. The gate judges the status +# code only, so a body that is empty or not JSON neither breaks it nor changes +# the verdict. +run_case "503 with an empty body fails and says the body was empty" 1 "E" "HTTP 503: " +run_case "502 with a non-JSON body fails and prints the body" 1 "N" "HTTP 502:

502 Bad Gateway

" +run_case "non-JSON 502 mid-streak resets it (H H N H H E, must not pass)" 1 "H H N H H E" "HTTP 503: " +run_case "200 with an empty body counts as ready (status code is the contract)" 0 "Z Z Z" "Service is ready." + # run_env_case LABEL EXPECTED_EXIT SCENARIO OUTPUT_SUBSTRING [ENV=VAL ...] # Runs the SUT with ONLY the given env overrides (plus a zero sleep interval), # so the script's own defaults (streak 3, 30 attempts) are exercised.