From a00a3395620f60433404eac4af4705dca253d7ab Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 10:19:07 +1000 Subject: [PATCH 1/7] fix: load Pagefind without eval so search works in Chromium MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search loaded pagefind.js via window.eval("import(...)"), which requires CSP 'unsafe-eval'. Under Chromium's stricter enforcement the eval was blocked, throwing an importError that surfaced the generic "Unable to load search functionality" message — and the real error was swallowed, making it hard to diagnose. Replace the eval hack with a real dynamic import marked webpackIgnore/ turbopackIgnore (which is what the eval was working around) using an origin-qualified URL, and log the underlying error instead of discarding it. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/search-docs/search.tsx | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/search-docs/search.tsx b/src/components/search-docs/search.tsx index 6ab765f0..c6ad369b 100644 --- a/src/components/search-docs/search.tsx +++ b/src/components/search-docs/search.tsx @@ -52,15 +52,25 @@ export function Search({ className }: { className?: string }) { if (typeof window !== "undefined") { let pagefindModule: any; try { - // Using eval to import pagefind.js is a workaround since the script isn't available during the build process. - // This also improves performance by loading the script only when needed, reducing initial page load time. - // A direct import would require committing the file with the codebase, which would change frequently - // with every content update. - - pagefindModule = await (window as any).eval( - `import("${pagefindPath}/pagefind.js")` + // pagefind.js is generated at build time and lives outside the bundle. The + // webpackIgnore/turbopackIgnore comments tell both bundlers to leave this dynamic + // import alone (the reason the old code resorted to `window.eval`), emitting it as a + // runtime import. Avoiding `eval` means no CSP 'unsafe-eval' is required, which is + // what was breaking search under Chromium's stricter enforcement. + const pagefindUrl = new URL( + `${pagefindPath}/pagefind.js`, + window.location.origin + ).href; + + pagefindModule = await import( + /* webpackIgnore: true */ + /* turbopackIgnore: true */ + pagefindUrl ); } catch (importError) { + // Surface the real cause (CSP, MIME type, or 404) instead of swallowing it. + // biome-ignore lint/suspicious/noConsole: needed to diagnose load failures in prod + console.error("Pagefind failed to load:", importError); setError( "Unable to load search functionality. For more information, please check this README: https://github.com/tinacms/tina-docs?tab=readme-ov-file#search-functionality and refresh the page." ); From 301165c405c5eed392d6d117275e244f75cedd26 Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 12:43:01 +1000 Subject: [PATCH 2/7] ci: bound search-test runtime so a stuck install can't hang for hours The search-tests job had no timeout, so when the "Install Playwright browsers" step hung it sat in_progress for ~2h (GitHub's 6h default before auto-fail), holding the PR check pending. Add a 15-min job timeout and an 8-min cap on the install step so a hang fails fast and visibly instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/search-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/search-test.yml b/.github/workflows/search-test.yml index 17e12ef2..c3ad9458 100644 --- a/.github/workflows/search-test.yml +++ b/.github/workflows/search-test.yml @@ -23,6 +23,7 @@ jobs: test-search: name: Test Search Functionality runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout code @@ -33,6 +34,7 @@ jobs: uses: ./.github/actions/setup-deps - name: Install Playwright browsers + timeout-minutes: 8 run: npx playwright install chromium --with-deps - name: Run search tests From 19a047f48ddbc1c93b74e59bbc796f1830f4cc2e Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 12:50:45 +1000 Subject: [PATCH 3/7] ci: make Playwright browser install hang-proof with per-attempt timeout + retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install step wedges intermittently when `--with-deps` blocks on the runner's apt/dpkg lock. A plain retry can't help because a hang is not a non-zero exit. Wrap each attempt in `timeout 180` so a stuck apt is killed, then retry up to 3 times to ride out the transient lock — all inside the existing 8-minute step budget. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/search-test.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/search-test.yml b/.github/workflows/search-test.yml index c3ad9458..a8d99f78 100644 --- a/.github/workflows/search-test.yml +++ b/.github/workflows/search-test.yml @@ -35,7 +35,23 @@ jobs: - name: Install Playwright browsers timeout-minutes: 8 - run: npx playwright install chromium --with-deps + # `--with-deps` shells out to apt, which intermittently wedges on the + # runner's dpkg lock. Cap each attempt with `timeout` so a hang is killed + # (a hang is not a non-zero exit, so a plain retry would never fire) and + # retry a few times to ride out the transient lock. + run: | + for attempt in 1 2 3; do + echo "::group::playwright install (attempt $attempt)" + if timeout 180 npx playwright install chromium --with-deps; then + echo "::endgroup::" + exit 0 + fi + echo "::endgroup::" + echo "Attempt $attempt failed or timed out; retrying in 15s..." + sleep 15 + done + echo "Playwright browser install failed after 3 attempts." + exit 1 - name: Run search tests run: ${{ steps.setup.outputs.runner }} test From 74c767a697a4b61a104fc92b8572dfac75eba05e Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 14:28:10 +1000 Subject: [PATCH 4/7] ci: force apt non-interactive so Playwright dep install can't hang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit playwright install --with-deps fetched and unpacked fine, then hung in the apt configure phase with zero output until the timeout fired — every attempt, deterministically. The cause is apt blocking on an interactive frontend (needrestart/debconf) with no TTY, so the previous per-attempt-timeout-plus- retry just repeated the identical stall and then overran the step cap. Set the debconf frontend to Noninteractive in the debconf database before install so the sudo apt-get Playwright spawns reads it and never prompts. Drop the retry loop (the failure was deterministic, not transient). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/search-test.yml | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/search-test.yml b/.github/workflows/search-test.yml index a8d99f78..9a9de06f 100644 --- a/.github/workflows/search-test.yml +++ b/.github/workflows/search-test.yml @@ -34,24 +34,19 @@ jobs: uses: ./.github/actions/setup-deps - name: Install Playwright browsers - timeout-minutes: 8 - # `--with-deps` shells out to apt, which intermittently wedges on the - # runner's dpkg lock. Cap each attempt with `timeout` so a hang is killed - # (a hang is not a non-zero exit, so a plain retry would never fire) and - # retry a few times to ride out the transient lock. + timeout-minutes: 10 + # `--with-deps` runs `sudo apt-get`, which fetched and unpacked fine but + # then hung deterministically in the configure phase — zero output for the + # full timeout on every attempt. The cause is apt blocking on an + # interactive frontend (needrestart/debconf) with no TTY to answer it, so + # the old per-attempt-timeout-plus-retry just repeated the same stall and + # then blew the step cap. Force the debconf frontend non-interactive in the + # debconf database so the sudo apt-get Playwright spawns reads it and never + # prompts. One attempt, generous cap — no retry, because the failure was + # deterministic, not transient. run: | - for attempt in 1 2 3; do - echo "::group::playwright install (attempt $attempt)" - if timeout 180 npx playwright install chromium --with-deps; then - echo "::endgroup::" - exit 0 - fi - echo "::endgroup::" - echo "Attempt $attempt failed or timed out; retrying in 15s..." - sleep 15 - done - echo "Playwright browser install failed after 3 attempts." - exit 1 + echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections + npx playwright install chromium --with-deps - name: Run search tests run: ${{ steps.setup.outputs.runner }} test From 8faa740641c7675c98977b9ac7f1433a47fdbab2 Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 14:43:05 +1000 Subject: [PATCH 5/7] ci: split Playwright deps from browser install, retry the browser download The debconf fix resolved the apt hang (apt now completes in ~3s), which revealed a second, distinct hang: the chromium archive downloads to 100% and then Playwright stalls with no output until the step cap. Split the working apt step from the browser download. Wrap only the browser install in a per-attempt timeout + retry (sized to fit under the step cap, so a stall is killed and retried rather than blowing the whole budget), and turn on DEBUG=pw:install to capture what Playwright does if it stalls again. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/search-test.yml | 42 ++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/.github/workflows/search-test.yml b/.github/workflows/search-test.yml index 9a9de06f..7a41b528 100644 --- a/.github/workflows/search-test.yml +++ b/.github/workflows/search-test.yml @@ -33,20 +33,38 @@ jobs: id: setup uses: ./.github/actions/setup-deps - - name: Install Playwright browsers - timeout-minutes: 10 - # `--with-deps` runs `sudo apt-get`, which fetched and unpacked fine but - # then hung deterministically in the configure phase — zero output for the - # full timeout on every attempt. The cause is apt blocking on an - # interactive frontend (needrestart/debconf) with no TTY to answer it, so - # the old per-attempt-timeout-plus-retry just repeated the same stall and - # then blew the step cap. Force the debconf frontend non-interactive in the - # debconf database so the sudo apt-get Playwright spawns reads it and never - # prompts. One attempt, generous cap — no retry, because the failure was - # deterministic, not transient. + # System deps (apt) and the browser download are split because they fail + # for different reasons. apt previously hung on an interactive frontend, so + # force debconf non-interactive in the db before the sudo apt-get Playwright + # runs — this now completes in seconds. + - name: Install Playwright system dependencies + timeout-minutes: 6 run: | echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections - npx playwright install chromium --with-deps + npx playwright install-deps chromium + + # The browser archive downloads fully (hits 100%) and then Playwright stalls + # indefinitely with no output. Cap each attempt with `timeout` so a stall is + # killed (a hang is not a non-zero exit) and retry; per-attempt cap × attempts + # stays under the step timeout. DEBUG=pw:install surfaces exactly what + # Playwright is doing if it stalls again. + - name: Install Playwright browser + timeout-minutes: 10 + env: + DEBUG: pw:install + run: | + for attempt in 1 2 3; do + echo "::group::playwright browser install (attempt $attempt)" + if timeout 150 npx playwright install chromium; then + echo "::endgroup::" + exit 0 + fi + echo "::endgroup::" + echo "Attempt $attempt stalled or failed; retrying in 10s..." + sleep 10 + done + echo "Playwright browser install failed after 3 attempts." + exit 1 - name: Run search tests run: ${{ steps.setup.outputs.runner }} test From 11b2e54f2f49fe85d4f299eb23e69c0a79762966 Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 14:56:35 +1000 Subject: [PATCH 6/7] ci: pin search tests to Node 22 to fix Playwright extraction hang The browser install hung deterministically at "extracting archive" (download hit 100%, then silence until the timeout). Root cause: .nvmrc is `lts/*`, which now resolves to Node 24, and Playwright 1.54's unzip path hangs on Node 24. Add an optional node-version override to the setup-deps action and pin the search-test job to Node 22 (previous LTS). Other workflows are unaffected (the input defaults to empty, falling back to node-version-file). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/actions/setup-deps/action.yml | 5 +++++ .github/workflows/search-test.yml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 2b0938e4..e9a50a1e 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -5,6 +5,10 @@ inputs: node-version-file: description: File to read Node.js version from default: .nvmrc + node-version: + description: Explicit Node.js version; takes precedence over node-version-file when set + required: false + default: "" pnpm-version: description: pnpm version to use if pnpm is detected default: "9.15.2" @@ -47,6 +51,7 @@ runs: - name: Setup Node.js uses: actions/setup-node@v4 with: + node-version: ${{ inputs.node-version }} node-version-file: ${{ inputs.node-version-file }} cache: ${{ steps.detect.outputs.manager }} diff --git a/.github/workflows/search-test.yml b/.github/workflows/search-test.yml index 7a41b528..b9ad071e 100644 --- a/.github/workflows/search-test.yml +++ b/.github/workflows/search-test.yml @@ -32,6 +32,12 @@ jobs: - name: Setup dependencies id: setup uses: ./.github/actions/setup-deps + with: + # .nvmrc is `lts/*`, which now resolves to Node 24 — and Playwright + # 1.54's browser-archive extraction hangs deterministically on Node 24. + # Pin this job to Node 22 (previous LTS, known-good) so the install can + # extract chromium. Scoped to this workflow only. + node-version: "22" # System deps (apt) and the browser download are split because they fail # for different reasons. apt previously hung on an interactive frontend, so From bca6390eebdf167a1814aa2275ebdca72e65c409 Mon Sep 17 00:00:00 2001 From: Josh Berman Date: Fri, 19 Jun 2026 15:08:58 +1000 Subject: [PATCH 7/7] ci: allow NEXT_PUBLIC_BASE_PATH on manual search-test dispatch The site is served under a base path (/tinadocs), so navigateToDocs needs it to reach /tinadocs/docs. workflow_call already accepted it; add the same input to workflow_dispatch so manual runs can target a real preview correctly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/search-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/search-test.yml b/.github/workflows/search-test.yml index b9ad071e..ce9198d6 100644 --- a/.github/workflows/search-test.yml +++ b/.github/workflows/search-test.yml @@ -7,6 +7,11 @@ on: description: 'Base URL to test (required)' required: true type: string + NEXT_PUBLIC_BASE_PATH: + description: 'Base path for the site (e.g. /tinadocs)' + required: false + default: '' + type: string workflow_call: inputs: base_url: