shoc-frontend-new/scripts/test-verify-cloudfront-release.sh
Adam Moussa 29aecff2bb
Some checks failed
Frontend checks / Build and test (push) Has been cancelled
Frontend checks / governance (push) Has been cancelled
Frontend checks / Visual regression (push) Has been cancelled
Deploy dev content / Deploy shoc-frontend-new-dev through Terraform (push) Has been cancelled
fix(ci): hash the served index.html byte stream in CloudFront verify (SH-300) (#186)
Capturing the curl body in "$(...)" strips the trailing newline, so the
served sha256 never matched dist/index.html and every release and rollback
verify polled to the budget and failed. Hash the response stream directly
and give the test fixture a trailing newline so the suite covers it.
2026-09-11 18:10:51 -03:00

302 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
# Stubbed aws/curl tests for scripts/verify-cloudfront-release.sh.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
VERIFY="${ROOT}/scripts/verify-cloudfront-release.sh"
CURRENT="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1"
PREVIOUS="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1"
NEW_HASH="1111111111111111111111111111111111111111111111111111111111111111"
OLD_HASH="0000000000000000000000000000000000000000000000000000000000000000"
# Vite writes index.html with a trailing newline. Keep it in the fixture so
# the expected hash covers every served byte, exactly like dist/index.html.
INDEX_HTML=$'<!doctype html><html><head><script type="module" src="/assets/app.js"></script></head><body></body></html>\n'
INDEX_HASH="$(printf '%s' "${INDEX_HTML}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())')"
failures=0
assert_exit() {
local name="$1" expected="$2" got="$3" log="$4"
if [[ "${got}" != "${expected}" ]]; then
echo "FAIL: ${name}: expected exit ${expected}, got ${got}" >&2
sed -n '1,80p' "${log}" >&2
failures=$((failures + 1))
else
echo "PASS: ${name}"
fi
}
make_stubs() {
local bin="$1"
mkdir -p "${bin}"
cat > "${bin}/aws" << 'AWS'
#!/usr/bin/env bash
set -euo pipefail
state_dir="${STUB_STATE}"
if [[ "${1:-}" == "s3" ]]; then
cat "${state_dir}/pointer.json"
exit 0
fi
cat "${state_dir}/distribution.json"
AWS
cat > "${bin}/curl" << 'CURL'
#!/usr/bin/env bash
set -euo pipefail
state_dir="${STUB_STATE}"
method="GET"
url=""
dump=""
output=""
write_out=""
args=("$@")
i=0
while [[ $i -lt ${#args[@]} ]]; do
arg="${args[$i]}"
case "${arg}" in
-X) i=$((i + 1)); method="${args[$i]}" ;;
-D) i=$((i + 1)); dump="${args[$i]}" ;;
-o) i=$((i + 1)); output="${args[$i]}" ;;
-w) i=$((i + 1)); write_out="${args[$i]}" ;;
-H|--max-time|-s|-S|-f|-fsS|-sS) ;;
http*) url="${arg}" ;;
esac
i=$((i + 1))
done
if [[ "${method}" == "OPTIONS" ]]; then
[[ -n "${dump}" ]] && printf 'HTTP/1.1 204 No Content\nAccess-Control-Allow-Origin: https://dev.seahaven.com\n\n' > "${dump}"
[[ -n "${write_out}" ]] && printf '204'
exit 0
fi
if [[ "${url}" == *"/assets/"* ]]; then
[[ -n "${dump}" ]] && printf 'HTTP/1.1 200 OK\nCache-Control: public,max-age=31536000,immutable\n\n' > "${dump}"
if [[ -f "${state_dir}/asset.js" ]]; then
body="$(cat "${state_dir}/asset.js")"
else
body='const api="https://api.dev.seahaven.com/api";'
fi
[[ -n "${output}" ]] && printf '%s' "${body}" > "${output}"
[[ -z "${output}" ]] && printf '%s' "${body}"
exit 0
fi
# Serve index.html byte-for-byte, trailing newline included, like real curl.
[[ -n "${dump}" ]] && printf 'HTTP/1.1 200 OK\nCache-Control: no-cache,no-store,must-revalidate\n\n' > "${dump}"
if [[ -n "${output}" ]]; then
cat "${state_dir}/index.html" > "${output}"
else
cat "${state_dir}/index.html"
fi
exit 0
CURL
chmod +x "${bin}/aws" "${bin}/curl"
}
dist_json() {
local status="$1" current_path="$2"
python3 -c 'import json,sys
status, path = sys.argv[1], sys.argv[2]
print(json.dumps({
"Distribution": {
"Status": status,
"DistributionConfig": {
"Origins": {"Items": [
{"Id": "current", "OriginPath": path},
{"Id": "previous", "OriginPath": ""},
]}
}
}
}))' "${status}" "${current_path}"
}
pointer_json() {
python3 -c 'import json,sys; print(json.dumps({"current": sys.argv[1], "previous": sys.argv[2]}))' "$1" "$2"
}
run_case() {
local name="$1"
local dir
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
export STUB_STATE="${dir}"
export PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P"
export EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${NEW_HASH}"
export PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com"
export SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=3
export INTERVAL=0
local log="${dir}/log.txt"
set +e
bash "${VERIFY}" > "${log}" 2>&1
local code=$?
set -e
assert_exit "${name}" "$2" "${code}" "${log}"
rm -rf "${dir}"
}
# 1. Right config, then propagates (InProgress -> Deployed, hash already matches).
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
pointer_json "${CURRENT}" "${PREVIOUS}" > "${dir}/pointer.json"
printf 'InProgress\n' > "${dir}/status"
cat > "${dir}/bin/aws" << AWS
#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == "s3" ]]; then
cat "${dir}/pointer.json"
exit 0
fi
status="\$(cat "${dir}/status")"
python3 -c 'import json,sys; print(json.dumps({"Distribution":{"Status":sys.argv[1],"DistributionConfig":{"Origins":{"Items":[{"Id":"current","OriginPath":"/releases/${CURRENT}"},{"Id":"previous","OriginPath":""}]}}}}))' "\${status}"
echo Deployed > "${dir}/status"
AWS
chmod +x "${dir}/bin/aws"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${INDEX_HASH}" PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=5 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "right-config-then-propagates" 0 "${code}" "${dir}/log.txt"
rm -rf "${dir}"
}
# 2. Right config never propagates (Deployed, stale hash).
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
pointer_json "${CURRENT}" "${PREVIOUS}" > "${dir}/pointer.json"
dist_json "Deployed" "/releases/${CURRENT}" > "${dir}/distribution.json"
printf 'stale' > "${dir}/index.html"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${NEW_HASH}" PREVIOUS_INDEX_SHA256="$(printf 'stale' | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())')"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=2 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "right-config-never-propagates" 1 "${code}" "${dir}/log.txt"
grep -q "last observed" "${dir}/log.txt" || { echo "FAIL: timeout missing last observed state" >&2; failures=$((failures + 1)); }
rm -rf "${dir}"
}
# 3. Wrong origin path fails fast.
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
pointer_json "${CURRENT}" "${PREVIOUS}" > "${dir}/pointer.json"
dist_json "Deployed" "/releases/${PREVIOUS}" > "${dir}/distribution.json"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${NEW_HASH}" PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=2 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "wrong-origin-path" 1 "${code}" "${dir}/log.txt"
grep -q "origin_path" "${dir}/log.txt" || { echo "FAIL: wrong origin path did not name origin_path" >&2; failures=$((failures + 1)); }
rm -rf "${dir}"
}
# 4. Wrong pointer fails fast.
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
pointer_json "${PREVIOUS}" "${PREVIOUS}" > "${dir}/pointer.json"
dist_json "Deployed" "/releases/${CURRENT}" > "${dir}/distribution.json"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${NEW_HASH}" PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=2 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "wrong-pointer" 1 "${code}" "${dir}/log.txt"
grep -q "pointer current" "${dir}/log.txt" || { echo "FAIL: wrong pointer did not name pointer current" >&2; failures=$((failures + 1)); }
rm -rf "${dir}"
}
# 5. Never Deployed.
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
pointer_json "${CURRENT}" "${PREVIOUS}" > "${dir}/pointer.json"
dist_json "InProgress" "/releases/${CURRENT}" > "${dir}/distribution.json"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${NEW_HASH}" PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=2 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "never-deployed" 1 "${code}" "${dir}/log.txt"
grep -q "last observed" "${dir}/log.txt" || { echo "FAIL: never-deployed missing last observed state" >&2; failures=$((failures + 1)); }
rm -rf "${dir}"
}
# 6. Hash-matched Deployed release whose JS assets omit the baked API URL.
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
pointer_json "${CURRENT}" "${PREVIOUS}" > "${dir}/pointer.json"
dist_json "Deployed" "/releases/${CURRENT}" > "${dir}/distribution.json"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
printf 'const x=1;' > "${dir}/asset.js"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${INDEX_HASH}" PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=2 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "missing-baked-api-url" 1 "${code}" "${dir}/log.txt"
grep -q "baked dev API URL" "${dir}/log.txt" || { echo "FAIL: missing API URL did not name baked dev API URL" >&2; failures=$((failures + 1)); }
rm -rf "${dir}"
}
# 7. Hash-matched Deployed release whose JS assets contain the staging API URL.
{
dir="$(mktemp -d)"
make_stubs "${dir}/bin"
pointer_json "${CURRENT}" "${PREVIOUS}" > "${dir}/pointer.json"
dist_json "Deployed" "/releases/${CURRENT}" > "${dir}/distribution.json"
printf '%s' "${INDEX_HTML}" > "${dir}/index.html"
printf 'const api="https://api.staging.seahaven.com/api";' > "${dir}/asset.js"
export STUB_STATE="${dir}" PATH="${dir}/bin:${PATH}"
export DISTRIBUTION_ID="E2CWLM1AFB964P" EXPECTED_LABEL="${CURRENT}"
export EXPECTED_INDEX_SHA256="${INDEX_HASH}" PREVIOUS_INDEX_SHA256="${OLD_HASH}"
export SITE_URL="https://dev.seahaven.com" SITE_BUCKET="seahaven-shoc-frontend-dev"
export BUDGET=2 INTERVAL=0
set +e
bash "${VERIFY}" > "${dir}/log.txt" 2>&1
code=$?
set -e
assert_exit "forbidden-staging-api-url" 1 "${code}" "${dir}/log.txt"
grep -q "forbidden URL api.staging.seahaven.com" "${dir}/log.txt" || { echo "FAIL: staging API URL did not name forbidden URL" >&2; failures=$((failures + 1)); }
rm -rf "${dir}"
}
if [[ "${failures}" -ne 0 ]]; then
echo "FAIL: ${failures} verify-cloudfront-release cases failed" >&2
exit 1
fi
echo "PASS: CloudFront release verify checks"