mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
Compare commits
2 commits
7716b4afc8
...
c30e8aa74b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c30e8aa74b | ||
|
|
f23c60ccc2 |
7 changed files with 350 additions and 79 deletions
56
.github/workflows/deploy.yml
vendored
56
.github/workflows/deploy.yml
vendored
|
|
@ -1,13 +1,14 @@
|
|||
name: Validate and deploy
|
||||
name: Deploy dev content
|
||||
|
||||
# Dev content CD through Terraform (SH-300). GitHub uploads an immutable
|
||||
# releases/<sha>-<run>-<attempt>/ prefix. Terraform owns the pointer, origin
|
||||
# group, and invalidation. Push-to-dev stays off until
|
||||
# vars.TERRAFORM_CONTENT_CD_ENABLED is the string true.
|
||||
#
|
||||
# Quality gates live in Frontend checks (`ci.yaml`). This workflow does not
|
||||
# re-run those gates on pull requests, pushes, or workflow_dispatch.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
push:
|
||||
branches: [dev]
|
||||
paths-ignore:
|
||||
|
|
@ -18,61 +19,12 @@ permissions:
|
|||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate production build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- name: Set up Terraform
|
||||
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||
with:
|
||||
terraform_version: "1.16.0"
|
||||
terraform_wrapper: false
|
||||
- name: Install actionlint
|
||||
env:
|
||||
ACTIONLINT_VERSION: "1.7.12"
|
||||
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o actionlint.tar.gz \
|
||||
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
||||
echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum -c -
|
||||
tar -xzf actionlint.tar.gz actionlint
|
||||
sudo mv actionlint /usr/local/bin/actionlint
|
||||
- name: Quality gates
|
||||
run: npm ci && npm run verify
|
||||
env:
|
||||
GOVERNANCE_BASE: ${{ github.event.pull_request.base.sha || 'origin/dev' }}
|
||||
- name: Build with pinned API URL
|
||||
env:
|
||||
VITE_API_URL: https://api.dev.seahaven.com/api
|
||||
VITE_APP_COMMIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm run build
|
||||
if grep -Rq "api.staging.seahaven.com" dist/; then
|
||||
echo "::error::Built assets contain the staging API URL." >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Rq "localhost:5141" dist/; then
|
||||
echo "::error::Built assets contain the Vite proxy target localhost:5141." >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -Rq "api.dev.seahaven.com" dist/
|
||||
|
||||
deploy-dev:
|
||||
name: Deploy shoc-frontend-new-dev through Terraform
|
||||
if: >
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/dev' &&
|
||||
vars.TERRAFORM_CONTENT_CD_ENABLED == 'true') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
permissions:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
Accepts exactly:
|
||||
- an update of the release pointer (content, plus computed etag/version_id)
|
||||
- an update of the distribution with only origin[*].origin_path changed
|
||||
(response_completion_timeout 0, null, and a missing key are equivalent)
|
||||
- exactly one action invocation for the CloudFront invalidation
|
||||
|
||||
after origin_path values must match the expected labels. before origin_path
|
||||
|
|
@ -52,6 +53,11 @@ DISTRIBUTION_UNKNOWN_ATTRIBUTES = frozenset(
|
|||
"in_progress_validation_batches",
|
||||
}
|
||||
)
|
||||
# Not an after_unknown allowlist. AWS returns 0 when the timeout is unset;
|
||||
# the provider writes null on origin_path updates. Treat 0, null, and a
|
||||
# missing key as the same. Any other value still fails closed.
|
||||
ORIGIN_RESPONSE_COMPLETION_TIMEOUT = "response_completion_timeout"
|
||||
ORIGIN_TIMEOUT_UNSET = frozenset({0, None})
|
||||
REDIRECT_STATUSES = {301, 302, 303, 307, 308}
|
||||
|
||||
UrlOpen = Callable[..., Any]
|
||||
|
|
@ -302,10 +308,16 @@ def _validate_pointer(
|
|||
return violations
|
||||
|
||||
|
||||
def _origin_fields_for_compare(origin: dict[str, Any]) -> dict[str, Any]:
|
||||
rest = {key: value for key, value in origin.items() if key != "origin_path"}
|
||||
timeout = rest.get(ORIGIN_RESPONSE_COMPLETION_TIMEOUT)
|
||||
if timeout in ORIGIN_TIMEOUT_UNSET:
|
||||
rest.pop(ORIGIN_RESPONSE_COMPLETION_TIMEOUT, None)
|
||||
return rest
|
||||
|
||||
|
||||
def _origin_non_path_fields_changed(before: dict[str, Any], after: dict[str, Any]) -> bool:
|
||||
before_rest = {key: value for key, value in before.items() if key != "origin_path"}
|
||||
after_rest = {key: value for key, value in after.items() if key != "origin_path"}
|
||||
return before_rest != after_rest
|
||||
return _origin_fields_for_compare(before) != _origin_fields_for_compare(after)
|
||||
|
||||
|
||||
def _validate_distribution(
|
||||
|
|
|
|||
|
|
@ -291,12 +291,29 @@ def test_deploy_workflow_confirms_prefix_with_head_object() -> list[str]:
|
|||
)
|
||||
if "aws s3 ls" in workflow:
|
||||
failures.append("deploy.yml must not list the prefix with aws s3 ls")
|
||||
if any(
|
||||
line.lstrip().startswith("run:") and "npm run verify" in line
|
||||
for line in workflow.splitlines()
|
||||
):
|
||||
failures.append(
|
||||
"deploy.yml must not re-run npm run verify; Frontend checks owns that gate"
|
||||
)
|
||||
if any(line.lstrip().startswith("pull_request:") for line in workflow.splitlines()):
|
||||
failures.append(
|
||||
"deploy.yml must not run on pull_request; Frontend checks owns PR verify"
|
||||
)
|
||||
return failures
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cases = [
|
||||
("version-only", run_case("version-only.json"), 0),
|
||||
(
|
||||
"origin-timeout-normalization",
|
||||
run_case("origin-timeout-normalization.json"),
|
||||
0,
|
||||
),
|
||||
("origin-timeout-change", run_case("origin-timeout-change.json"), 1),
|
||||
("wrong-label", run_case("wrong-label.json"), 1),
|
||||
("wrong-before", run_case("wrong-before.json"), 1),
|
||||
("extra-origin-change", run_case("extra-origin-change.json"), 1),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
104
scripts/testdata/terraform-release-plans/origin-timeout-change.json
vendored
Normal file
104
scripts/testdata/terraform-release-plans/origin-timeout-change.json
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "module.environment_owned.aws_iam_role_policy.github_deploy",
|
||||
"mode": "managed",
|
||||
"type": "aws_iam_role_policy",
|
||||
"change": {
|
||||
"actions": ["no-op"],
|
||||
"before": {
|
||||
"name": "policy"
|
||||
},
|
||||
"after": {
|
||||
"name": "policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "module.environment_owned.aws_s3_object.release_pointer",
|
||||
"mode": "managed",
|
||||
"type": "aws_s3_object",
|
||||
"change": {
|
||||
"actions": ["update"],
|
||||
"before": {
|
||||
"content": "{\"current\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\",\"previous\":\"0000000000000000000000000000000000000000-1-1\"}",
|
||||
"key": ".release/current"
|
||||
},
|
||||
"after": {
|
||||
"content": "{\"current\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1\",\"previous\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\"}",
|
||||
"key": ".release/current"
|
||||
},
|
||||
"after_unknown": {
|
||||
"etag": true,
|
||||
"version_id": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "module.environment_owned.aws_cloudfront_distribution.site",
|
||||
"mode": "managed",
|
||||
"type": "aws_cloudfront_distribution",
|
||||
"change": {
|
||||
"actions": ["update"],
|
||||
"before": {
|
||||
"origin": [
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1",
|
||||
"response_completion_timeout": 10
|
||||
},
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||
"origin_path": "/releases/0000000000000000000000000000000000000000-1-1"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"comment": "SeaHaven SHOC frontend (dev)"
|
||||
},
|
||||
"after": {
|
||||
"origin": [
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||
"origin_path": "/releases/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1",
|
||||
"response_completion_timeout": 60
|
||||
},
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"comment": "SeaHaven SHOC frontend (dev)"
|
||||
},
|
||||
"after_unknown": {
|
||||
"etag": true,
|
||||
"last_modified_time": true,
|
||||
"status": true,
|
||||
"in_progress_validation_batches": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"action_invocations": [
|
||||
{
|
||||
"address": "module.environment_owned.action.aws_cloudfront_create_invalidation.release",
|
||||
"type": "aws_cloudfront_create_invalidation"
|
||||
}
|
||||
]
|
||||
}
|
||||
105
scripts/testdata/terraform-release-plans/origin-timeout-normalization.json
vendored
Normal file
105
scripts/testdata/terraform-release-plans/origin-timeout-normalization.json
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "module.environment_owned.aws_iam_role_policy.github_deploy",
|
||||
"mode": "managed",
|
||||
"type": "aws_iam_role_policy",
|
||||
"change": {
|
||||
"actions": ["no-op"],
|
||||
"before": {
|
||||
"name": "policy"
|
||||
},
|
||||
"after": {
|
||||
"name": "policy"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "module.environment_owned.aws_s3_object.release_pointer",
|
||||
"mode": "managed",
|
||||
"type": "aws_s3_object",
|
||||
"change": {
|
||||
"actions": ["update"],
|
||||
"before": {
|
||||
"content": "{\"current\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\",\"previous\":\"0000000000000000000000000000000000000000-1-1\"}",
|
||||
"key": ".release/current"
|
||||
},
|
||||
"after": {
|
||||
"content": "{\"current\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1\",\"previous\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\"}",
|
||||
"key": ".release/current"
|
||||
},
|
||||
"after_unknown": {
|
||||
"etag": true,
|
||||
"version_id": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "module.environment_owned.aws_cloudfront_distribution.site",
|
||||
"mode": "managed",
|
||||
"type": "aws_cloudfront_distribution",
|
||||
"change": {
|
||||
"actions": ["update"],
|
||||
"before": {
|
||||
"origin": [
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1",
|
||||
"response_completion_timeout": 0
|
||||
},
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||
"origin_path": "/releases/0000000000000000000000000000000000000000-1-1",
|
||||
"response_completion_timeout": 0
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"comment": "SeaHaven SHOC frontend (dev)"
|
||||
},
|
||||
"after": {
|
||||
"origin": [
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||
"origin_path": "/releases/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1",
|
||||
"response_completion_timeout": null
|
||||
},
|
||||
{
|
||||
"connection_attempts": 3,
|
||||
"connection_timeout": 10,
|
||||
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||
"origin_access_control_id": "E30VSIK87N8H64",
|
||||
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"comment": "SeaHaven SHOC frontend (dev)"
|
||||
},
|
||||
"after_unknown": {
|
||||
"etag": true,
|
||||
"last_modified_time": true,
|
||||
"status": true,
|
||||
"in_progress_validation_batches": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"action_invocations": [
|
||||
{
|
||||
"address": "module.environment_owned.action.aws_cloudfront_create_invalidation.release",
|
||||
"type": "aws_cloudfront_create_invalidation"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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}" \
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue