fix(ci): assert the baked API URL in served JS assets (SH-300) (#184)
Some checks are pending
Frontend checks / Build and test (push) Waiting to run
Frontend checks / governance (push) Waiting to run
Frontend checks / Visual regression (push) Waiting to run
Deploy dev content / Deploy shoc-frontend-new-dev through Terraform (push) Waiting to run

Vite puts VITE_API_URL in hashed JS, not index.html. Scan every
referenced /assets file and reject staging or localhost there too.
This commit is contained in:
Adam Moussa 2026-09-11 15:38:07 -04:00 • committed by GitHub
parent f23c60ccc2
commit c30e8aa74b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 105 additions and 24 deletions

View file

@ -8,7 +8,7 @@ CURRENT="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1"
PREVIOUS="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1"
NEW_HASH="1111111111111111111111111111111111111111111111111111111111111111"
OLD_HASH="0000000000000000000000000000000000000000000000000000000000000000"
INDEX_HTML='<!doctype html><html><head></head><body><script src="/assets/app.js"></script>api.dev.seahaven.com</body></html>'
INDEX_HTML='<!doctype html><html><head><script type="module" src="/assets/app.js"></script></head><body></body></html>'
INDEX_HASH="$(printf '%s' "${INDEX_HTML}" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())')"
failures=0
@ -66,8 +66,13 @@ if [[ "${method}" == "OPTIONS" ]]; then
fi
if [[ "${url}" == *"/assets/"* ]]; then
[[ -n "${dump}" ]] && printf 'HTTP/1.1 200 OK\nCache-Control: public,max-age=31536000,immutable\n\n' > "${dump}"
[[ -n "${output}" ]] && printf 'asset' > "${output}"
[[ -z "${output}" ]] && printf 'asset'
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
body="$(cat "${state_dir}/index.html")"
@ -244,6 +249,50 @@ AWS
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

View file

@ -129,6 +129,57 @@ if [[ "${last_status}" != "Deployed" || "${last_hash}" != "${EXPECTED_INDEX_SHA2
exit 1
fi
write_asset_paths() {
python3 -c '
import re, sys
html = open(sys.argv[1], encoding="utf-8").read()
seen = []
for path in re.findall(r"(?:src|href)=\"(/assets/[^\"]+\.(?:js|css))\"", html):
if path not in seen:
seen.append(path)
print(path)
' "$1"
}
assert_baked_api_url() {
local tmp="$1"
if [[ ! -s "${tmp}/asset-paths.txt" ]]; then
echo "FAIL: served index.html has no /assets/ URL to check immutable caching." >&2
exit 1
fi
: > "${tmp}/assets.txt"
local immutable_ok="no"
local asset_path
while IFS= read -r asset_path; do
curl -fsS --max-time 30 "${SITE_URL}${asset_path}" \
-o "${tmp}/asset-body" -D "${tmp}/asset.headers"
cat "${tmp}/asset-body" >> "${tmp}/assets.txt"
if [[ "${asset_path}" == *.js && "${immutable_ok}" == "no" ]]; then
if ! grep -qiE 'cache-control:.*immutable' "${tmp}/asset.headers"; then
echo "FAIL: hashed asset is missing Cache-Control immutable." >&2
exit 1
fi
immutable_ok="yes"
fi
done < "${tmp}/asset-paths.txt"
if [[ "${immutable_ok}" != "yes" ]]; then
echo "FAIL: served index.html has no hashed JS asset to check immutable caching." >&2
exit 1
fi
cat "${tmp}/index.html" "${tmp}/assets.txt" > "${tmp}/served.txt"
local forbidden
for forbidden in api.staging.seahaven.com localhost:5141; do
if grep -Fq "${forbidden}" "${tmp}/served.txt"; then
echo "FAIL: served assets contain forbidden URL ${forbidden}." >&2
exit 1
fi
done
if ! grep -Fq "api.dev.seahaven.com" "${tmp}/served.txt"; then
echo "FAIL: served JS assets are missing the baked dev API URL." >&2
exit 1
fi
}
tmp="$(mktemp -d)"
trap 'rm -rf "${tmp}"' EXIT
@ -139,27 +190,8 @@ if ! grep -qiE 'cache-control:.*no-store' "${tmp}/index.headers"; then
echo "FAIL: HTML Cache-Control is missing no-store." >&2
exit 1
fi
for forbidden in api.staging.seahaven.com localhost:5141; do
if grep -Fq "${forbidden}" "${tmp}/index.html"; then
echo "FAIL: served index contains forbidden URL ${forbidden}." >&2
exit 1
fi
done
if ! grep -Fq "api.dev.seahaven.com" "${tmp}/index.html"; then
echo "FAIL: served index is missing the dev API URL." >&2
exit 1
fi
asset_path="$(python3 -c 'import re,sys; html=open(sys.argv[1],encoding="utf-8").read(); m=re.search(r"(/assets/[^\"'\'']+)", html); print(m.group(1) if m else "")' "${tmp}/index.html")"
if [[ -z "${asset_path}" ]]; then
echo "FAIL: served index.html has no /assets/ URL to check immutable caching." >&2
exit 1
fi
curl -fsS --max-time 30 "${SITE_URL}${asset_path}" -o /dev/null -D "${tmp}/asset.headers"
if ! grep -qiE 'cache-control:.*immutable' "${tmp}/asset.headers"; then
echo "FAIL: hashed asset is missing Cache-Control immutable." >&2
exit 1
fi
write_asset_paths "${tmp}/index.html" > "${tmp}/asset-paths.txt"
assert_baked_api_url "${tmp}"
cors_code="$(curl -sS --max-time 30 -o /dev/null -D "${tmp}/cors.headers" -w '%{http_code}' -X OPTIONS "${API_URL}" \
-H "Origin: ${SITE_URL}" \