diff --git a/.github/workflows/terraform-deploy-auto-retry.yml b/.github/workflows/terraform-deploy-auto-retry.yml index dd46b68..91e72f5 100644 --- a/.github/workflows/terraform-deploy-auto-retry.yml +++ b/.github/workflows/terraform-deploy-auto-retry.yml @@ -4,73 +4,151 @@ on: workflow_run: workflows: - Terraform Deploy + branches: + - dev + - prod types: - completed + schedule: + - cron: "*/5 * * * *" + workflow_dispatch: permissions: actions: write contents: read jobs: - rerun-on-tfc-discovery-timeout: - if: ${{ github.event.workflow_run.conclusion == 'failure' }} + retry-until-success: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: + - dev + - prod + concurrency: + group: terraform-deploy-auto-retry-${{ matrix.branch }} + cancel-in-progress: false steps: - - name: Evaluate Terraform Deploy failure and optionally rerun + - name: Retry failed Terraform deploys until successful env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.ACTIONS_BOT_TOKEN || github.token }} OWNER: ${{ github.repository_owner }} REPO: ${{ github.event.repository.name }} - RUN_ID: ${{ github.event.workflow_run.id }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + EVENT_NAME: ${{ github.event_name }} + BRANCH: ${{ matrix.branch }} + WORKFLOW_RUN_BRANCH: ${{ github.event.workflow_run.head_branch }} + WORKFLOW_RUN_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + TARGET_WORKFLOW_NAME: "Terraform Deploy" + TARGET_WORKFLOW_FILE: "terraform-deploy.yml" run: | set -euo pipefail - echo "Terraform Deploy run id: ${RUN_ID}" - echo "Failed run branch: ${HEAD_BRANCH}" + should_monitor_branch() { + if [ "${EVENT_NAME}" != "workflow_run" ]; then + return 0 + fi - workdir="$(mktemp -d)" - trap 'rm -rf "${workdir}"' EXIT - logs_zip="${workdir}/logs.zip" - logs_dir="${workdir}/logs" + if [ "${WORKFLOW_RUN_BRANCH}" != "${BRANCH}" ]; then + echo "workflow_run was for '${WORKFLOW_RUN_BRANCH}', this job is '${BRANCH}'. Skipping." + return 1 + fi - downloaded="false" - for i in 1 2 3 4 5 6; do - if gh api "/repos/${OWNER}/${REPO}/actions/runs/${RUN_ID}/logs" > "${logs_zip}"; then - downloaded="true" - break + if [ "${WORKFLOW_RUN_CONCLUSION}" != "failure" ]; then + echo "workflow_run conclusion for '${BRANCH}' was '${WORKFLOW_RUN_CONCLUSION}'. Nothing to retry." + return 1 fi - echo "Run logs not ready yet, retrying in 10s (${i}/6)." - sleep 10 - done - if [ "${downloaded}" != "true" ]; then - echo "Could not download logs for failed run; skipping automatic rerun." - exit 0 - fi + return 0 + } - unzip -q "${logs_zip}" -d "${logs_dir}" + latest_run_json() { + gh run list -R "${OWNER}/${REPO}" \ + --workflow "${TARGET_WORKFLOW_NAME}" \ + --branch "${BRANCH}" \ + --limit 1 \ + --json databaseId,status,conclusion,createdAt,url,event || echo "[]" + } - if rg -n -F "Failed to request discovery document" "${logs_dir}" >/dev/null \ - || rg -n -F "https://app.terraform.io/.well-known/terraform.json" "${logs_dir}" >/dev/null \ - || rg -n -F "context deadline exceeded (Client.Timeout exceeded while awaiting headers)" "${logs_dir}" >/dev/null; then - if [ -z "${HEAD_BRANCH}" ]; then - echo "Head branch is empty; cannot dispatch Terraform Deploy retry." - exit 0 - fi + dispatch_branch() { + echo "Dispatching ${TARGET_WORKFLOW_NAME} on branch '${BRANCH}'." + gh api --method POST "/repos/${OWNER}/${REPO}/actions/workflows/${TARGET_WORKFLOW_FILE}/dispatches" -f ref="${BRANCH}" >/dev/null + } - if [ "${HEAD_BRANCH}" != "dev" ] && [ "${HEAD_BRANCH}" != "prod" ]; then - echo "Head branch '${HEAD_BRANCH}' is not a deploy branch; skipping retry dispatch." - exit 0 - fi + monitor_branch_until_success() { + local latest_json + local run_id + local latest_status + local run_conclusion + local previous_failed_run_id="" + local wait_count=0 + + while true; do + latest_json="$(latest_run_json)" + if [ -z "${latest_json}" ] || [ "$(echo "${latest_json}" | jq 'length')" -eq 0 ]; then + echo "No ${TARGET_WORKFLOW_NAME} runs found for '${BRANCH}'. Waiting 20s." + sleep 20 + continue + fi + + run_id="$(echo "${latest_json}" | jq -r '.[0].databaseId')" + latest_status="$(echo "${latest_json}" | jq -r '.[0].status')" + run_conclusion="$(echo "${latest_json}" | jq -r '.[0].conclusion // ""')" + + echo "Branch '${BRANCH}' latest run=${run_id} status=${latest_status} conclusion=${run_conclusion}" + + if [ "${latest_status}" = "in_progress" ] || [ "${latest_status}" = "queued" ]; then + sleep 20 + continue + fi + + if [ "${run_conclusion}" = "success" ]; then + echo "Branch '${BRANCH}' latest run succeeded. Retry loop complete." + return 0 + fi + + if [ "${run_conclusion}" = "failure" ] || [ "${run_conclusion}" = "cancelled" ] || [ "${run_conclusion}" = "timed_out" ] || [ "${run_conclusion}" = "startup_failure" ]; then + if [ "${previous_failed_run_id}" = "${run_id}" ]; then + sleep 20 + continue + fi + + previous_failed_run_id="${run_id}" + dispatch_branch + wait_count=0 + + while true; do + sleep 15 + latest_json="$(latest_run_json)" + if [ -z "${latest_json}" ] || [ "$(echo "${latest_json}" | jq 'length')" -eq 0 ]; then + continue + fi + + run_id="$(echo "${latest_json}" | jq -r '.[0].databaseId')" + if [ "${run_id}" != "${previous_failed_run_id}" ]; then + echo "Detected new ${TARGET_WORKFLOW_NAME} run ${run_id} on '${BRANCH}'." + break + fi + + wait_count=$((wait_count + 1)) + if [ "${wait_count}" -ge 40 ]; then + echo "Dispatched run not visible yet on '${BRANCH}'. Re-dispatching." + dispatch_branch + wait_count=0 + fi + done + continue + fi - echo "Detected Terraform Cloud discovery timeout. Waiting 120s before retry dispatch." - sleep 120 + echo "Branch '${BRANCH}' has non-retryable conclusion '${run_conclusion}'. Waiting 20s." + sleep 20 + done + } - echo "Dispatching Terraform Deploy on branch '${HEAD_BRANCH}'." - gh api --method POST "/repos/${OWNER}/${REPO}/actions/workflows/terraform-deploy.yml/dispatches" -f ref="${HEAD_BRANCH}" + if ! should_monitor_branch; then exit 0 fi - echo "Failure reason did not match Terraform Cloud discovery timeout; no rerun requested." + echo "Starting retry monitor for branch '${BRANCH}' (event=${EVENT_NAME})." + monitor_branch_until_success diff --git a/README.md b/README.md index 0f2f7df..8ced598 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ renders slot placeholders (`{{ slot:main }}`, etc.), and serves CMS HTML directl ## Browser Cache Versioning -- HTML responses default to `Cache-Control: public, max-age=31536000, immutable`. -- Static assets default to `Cache-Control: public, max-age=31536000, immutable`. +- HTML responses default to `Cache-Control: public, max-age=0, must-revalidate` (override with `app_html_cache_control`). +- Next build assets under `/_next/static/*` use `Cache-Control: public, max-age=31536000, immutable` via [`public/_headers`](public/_headers). +- Worker-handled static routes default to `Cache-Control: public, max-age=31536000, immutable` (override with `app_static_cache_control`). - API routes default to `Cache-Control: no-store, no-cache, must-revalidate`. - The page includes a content hash (`data-content-hash`) and checks `/api/cache/version`. - If the hash changes after a Directus refresh/deploy, the client reloads with `?cmsv=`. diff --git a/package-lock.json b/package-lock.json index 7a8579c..a39739e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "@reduxjs/toolkit": "^2.2.7", "drizzle-orm": "^0.35.3", "lodash": "^4.17.23", - "next": "16.1.6", + "next": "16.1.7", "next-pwa": "^5.6.0", "next-themes": "^0.3.0", "node": "^20", @@ -84,7 +84,7 @@ "tailwindcss": "^3.4.14", "ts-loader": "^9.5.1", "typescript": "^5.6.3", - "wrangler": "^4.70.0" + "wrangler": "^4.75.0" } }, "node_modules/@alloc/quick-lru": { @@ -3098,14 +3098,14 @@ } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.14.0.tgz", - "integrity": "sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.15.0.tgz", + "integrity": "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { "unenv": "2.0.0-rc.24", - "workerd": "^1.20260218.0" + "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "peerDependenciesMeta": { "workerd": { @@ -3114,9 +3114,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260301.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260301.1.tgz", - "integrity": "sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ==", + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz", + "integrity": "sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==", "cpu": [ "x64" ], @@ -3131,9 +3131,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260301.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260301.1.tgz", - "integrity": "sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ==", + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz", + "integrity": "sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==", "cpu": [ "arm64" ], @@ -3148,9 +3148,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260301.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260301.1.tgz", - "integrity": "sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ==", + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz", + "integrity": "sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==", "cpu": [ "x64" ], @@ -3165,9 +3165,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260301.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260301.1.tgz", - "integrity": "sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw==", + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz", + "integrity": "sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==", "cpu": [ "arm64" ], @@ -3182,9 +3182,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260301.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260301.1.tgz", - "integrity": "sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A==", + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz", + "integrity": "sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==", "cpu": [ "x64" ], @@ -3199,9 +3199,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20260301.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260301.1.tgz", - "integrity": "sha512-klKnECMb5A4GtVF0P5NH6rCjtyjqIEKJaz6kEtx9YPHhfFO2HUEarO+MI4F8WPchgeZqpGlEpDhRapzrOTw51Q==", + "version": "4.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz", + "integrity": "sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==", "devOptional": true, "license": "MIT OR Apache-2.0" }, @@ -4883,9 +4883,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", + "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -4899,9 +4899,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", + "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", "cpu": [ "arm64" ], @@ -4915,9 +4915,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", + "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", "cpu": [ "x64" ], @@ -4931,9 +4931,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", + "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", "cpu": [ "arm64" ], @@ -4947,9 +4947,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", + "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", "cpu": [ "arm64" ], @@ -4963,9 +4963,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", + "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", "cpu": [ "x64" ], @@ -4979,9 +4979,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", + "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", "cpu": [ "x64" ], @@ -4995,9 +4995,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", + "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", "cpu": [ "arm64" ], @@ -5028,9 +5028,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", + "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", "cpu": [ "x64" ], @@ -18600,16 +18600,16 @@ } }, "node_modules/miniflare": { - "version": "4.20260301.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260301.1.tgz", - "integrity": "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog==", + "version": "4.20260317.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.0.tgz", + "integrity": "sha512-xuwk5Kjv+shi5iUBAdCrRl9IaWSGnTU8WuTQzsUS2GlSDIMCJuu8DiF/d9ExjMXYiQG5ml+k9SVKnMj8cRkq0w==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", - "undici": "7.18.2", - "workerd": "1.20260301.1", + "undici": "7.24.4", + "workerd": "1.20260317.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, @@ -18807,14 +18807,14 @@ "peer": true }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", + "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.1.7", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -18826,14 +18826,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", + "@next/swc-darwin-arm64": "16.1.7", + "@next/swc-darwin-x64": "16.1.7", + "@next/swc-linux-arm64-gnu": "16.1.7", + "@next/swc-linux-arm64-musl": "16.1.7", + "@next/swc-linux-x64-gnu": "16.1.7", + "@next/swc-linux-x64-musl": "16.1.7", + "@next/swc-win32-arm64-msvc": "16.1.7", + "@next/swc-win32-x64-msvc": "16.1.7", "sharp": "^0.34.4" }, "peerDependencies": { @@ -21544,9 +21544,9 @@ } }, "node_modules/tar": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", - "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -22162,9 +22162,9 @@ } }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "dev": true, "license": "MIT", "engines": { @@ -23326,9 +23326,9 @@ "license": "MIT" }, "node_modules/workerd": { - "version": "1.20260301.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260301.1.tgz", - "integrity": "sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw==", + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz", + "integrity": "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -23339,28 +23339,28 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260301.1", - "@cloudflare/workerd-darwin-arm64": "1.20260301.1", - "@cloudflare/workerd-linux-64": "1.20260301.1", - "@cloudflare/workerd-linux-arm64": "1.20260301.1", - "@cloudflare/workerd-windows-64": "1.20260301.1" + "@cloudflare/workerd-darwin-64": "1.20260317.1", + "@cloudflare/workerd-darwin-arm64": "1.20260317.1", + "@cloudflare/workerd-linux-64": "1.20260317.1", + "@cloudflare/workerd-linux-arm64": "1.20260317.1", + "@cloudflare/workerd-windows-64": "1.20260317.1" } }, "node_modules/wrangler": { - "version": "4.70.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.70.0.tgz", - "integrity": "sha512-PNDZ9o4e+B5x+1bUbz62Hmwz6G9lw+I9pnYe/AguLddJFjfIyt2cmFOUOb3eOZSoXsrhcEPUg2YidYIbVwUkfw==", + "version": "4.75.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.75.0.tgz", + "integrity": "sha512-Efk1tcnm4eduBYpH1sSjMYydXMnIFPns/qABI3+fsbDrUk5GksNYX8nYGVP4sFygvGPO7kJc36YJKB5ooA7JAg==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.14.0", + "@cloudflare/unenv-preset": "2.15.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", - "miniflare": "4.20260301.1", + "miniflare": "4.20260317.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260301.1" + "workerd": "1.20260317.1" }, "bin": { "wrangler": "bin/wrangler.js", @@ -23373,7 +23373,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260226.1" + "@cloudflare/workers-types": "^4.20260317.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index 17da895..88ced7f 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@reduxjs/toolkit": "^2.2.7", "drizzle-orm": "^0.35.3", "lodash": "^4.17.23", - "next": "16.1.6", + "next": "16.1.7", "next-pwa": "^5.6.0", "next-themes": "^0.3.0", "node": "^20", @@ -97,7 +97,7 @@ "tailwindcss": "^3.4.14", "ts-loader": "^9.5.1", "typescript": "^5.6.3", - "wrangler": "^4.70.0" + "wrangler": "^4.75.0" }, "license": "https://themeforest.net/licenses/standard" } diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..c846ea7 --- /dev/null +++ b/public/_headers @@ -0,0 +1,2 @@ +/_next/static/* + Cache-Control: public, max-age=31536000, immutable diff --git a/src/app/page.tsx b/src/app/page.tsx index 78148d3..7f646e4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,6 +12,19 @@ import ThemeModeSelector from "./ThemeModeSelector"; export const dynamic = "force-dynamic"; +const DEFAULT_GRAPHQL_HTTP_URLS = { + dev: "https://cf-suncoast-graphql-proxy.dev.suncoast.systems/graphql", + prod: "https://cf-suncoast-graphql-proxy.prod.suncoast.systems/graphql", +} as const; + +const GRAPHQL_AUTH_TOKEN_STORAGE_KEYS = [ + "suncoast.auth.access_token", + "suncoast.auth.token", + "suncoast.auth.bearer_token", + "mfe.preview.authToken", + "access_token", +] as const; + function hasRenderableContent(content: AppShellContent): boolean { return content.renderedHtml.trim().length > 0; } @@ -26,6 +39,153 @@ function allowRuntimeOriginFallback(): boolean { return ["1", "true", "yes", "on"].includes(normalized); } +function normalizeRuntimeEnvironment(value: string | undefined): "dev" | "prod" { + const normalized = (value ?? "").trim().toLowerCase(); + if (normalized === "prod" || normalized === "production") { + return "prod"; + } + return "dev"; +} + +function toWebSocketUrl(httpUrl: string): string { + const normalized = httpUrl.trim(); + if (!normalized) { + return ""; + } + + try { + const parsed = new URL(normalized); + parsed.protocol = parsed.protocol === "http:" ? "ws:" : "wss:"; + return parsed.toString(); + } catch { + if (normalized.startsWith("https://")) { + return `wss://${normalized.slice("https://".length)}`; + } + if (normalized.startsWith("http://")) { + return `ws://${normalized.slice("http://".length)}`; + } + return normalized; + } +} + +function resolveRuntimeGraphqlHttpUrl(): string { + const explicit = asNonEmptyString(process.env.APP_GRAPHQL_HTTP_URL); + if (explicit) { + return explicit; + } + + const runtimeEnvironment = normalizeRuntimeEnvironment(process.env.ENVIRONMENT); + return runtimeEnvironment === "dev" + ? DEFAULT_GRAPHQL_HTTP_URLS.dev + : DEFAULT_GRAPHQL_HTTP_URLS.prod; +} + +function resolveRuntimeGraphqlWsUrl(httpUrl: string): string { + const explicit = asNonEmptyString(process.env.APP_GRAPHQL_WS_URL); + if (explicit) { + return explicit; + } + return toWebSocketUrl(httpUrl); +} + +function buildModuleRuntimeBootstrapScript(): string { + const tokenKeysJson = JSON.stringify(GRAPHQL_AUTH_TOKEN_STORAGE_KEYS); + return `(function () { + var root = document.getElementById("cms-root"); + if (!root || !root.dataset) return; + + var tokenStorageKeys = ${tokenKeysJson}; + + function normalize(value) { + return typeof value === "string" ? value.trim() : ""; + } + + function readFromStorage(storage) { + if (!storage) return ""; + for (var i = 0; i < tokenStorageKeys.length; i += 1) { + var key = tokenStorageKeys[i]; + try { + var value = normalize(storage.getItem(key)); + if (value) return value; + } catch (_error) {} + } + return ""; + } + + function readToken() { + var globalAuth = window.__SUNCOAST_AUTH__; + if (globalAuth && typeof globalAuth.getAccessToken === "function") { + try { + var dynamicToken = normalize(globalAuth.getAccessToken()); + if (dynamicToken) return dynamicToken; + } catch (_error) {} + } + if (globalAuth) { + var staticToken = normalize(globalAuth.accessToken); + if (staticToken) return staticToken; + } + var sessionToken = ""; + var localToken = ""; + try { + sessionToken = readFromStorage(window.sessionStorage); + } catch (_error) {} + if (sessionToken) { + return sessionToken; + } + try { + localToken = readFromStorage(window.localStorage); + } catch (_error) {} + return localToken; + } + + function ensureRuntimeObject() { + if (!window.__SUNCOAST_RUNTIME__ || typeof window.__SUNCOAST_RUNTIME__ !== "object") { + window.__SUNCOAST_RUNTIME__ = {}; + } + return window.__SUNCOAST_RUNTIME__; + } + + function applyToken(token) { + var normalizedToken = normalize(token); + if (normalizedToken) { + root.dataset.graphqlAuthToken = normalizedToken; + } else { + delete root.dataset.graphqlAuthToken; + } + + var runtime = ensureRuntimeObject(); + var graphql = runtime.graphql && typeof runtime.graphql === "object" ? runtime.graphql : {}; + graphql.httpUrl = normalize(root.dataset.graphqlHttpUrl); + graphql.wsUrl = normalize(root.dataset.graphqlWsUrl); + graphql.authToken = normalizedToken; + runtime.graphql = graphql; + } + + function refreshToken() { + applyToken(readToken()); + } + + refreshToken(); + window.addEventListener("focus", refreshToken); + window.addEventListener("storage", function (event) { + if (!event || !event.key) return; + if (tokenStorageKeys.indexOf(event.key) >= 0) { + refreshToken(); + } + }); + window.addEventListener("suncoast-auth-token", function (event) { + var token = + event && + event.detail && + typeof event.detail.token === "string" ? event.detail.token : ""; + applyToken(token); + }); + window.__SUNCOAST_SET_AUTH_TOKEN__ = function (token) { + applyToken(typeof token === "string" ? token : ""); + }; +})();`; +} + function resolveThemeStyleMedia(mode: "auto" | "light" | "dark"): { light: string; dark: string; @@ -310,6 +470,9 @@ export default async function HomePage() { openObserveRumScriptUrl && openObserveRumConfig ? buildOpenObserveRumInitScript(openObserveRumConfig) : ""; + const runtimeGraphqlHttpUrl = resolveRuntimeGraphqlHttpUrl(); + const runtimeGraphqlWsUrl = resolveRuntimeGraphqlWsUrl(runtimeGraphqlHttpUrl); + const moduleRuntimeBootstrapScript = buildModuleRuntimeBootstrapScript(); return ( <> @@ -336,6 +499,8 @@ export default async function HomePage() { data-cache-state={content.cacheState} data-cache-key={content.cacheKey} data-content-hash={content.contentHash} + data-graphql-http-url={runtimeGraphqlHttpUrl} + data-graphql-ws-url={runtimeGraphqlWsUrl} data-theme-mode-default={defaultThemeMode} data-theme-switcher-position={themeSwitcherPosition} data-theme-switcher-dock-direction={themeSwitcherDockDirection} @@ -346,6 +511,11 @@ export default async function HomePage() { ) : null} +