From 601bc736347743e4cb7cc37926366f8b3397c99f Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Fri, 18 Sep 2026 09:55:18 -0700 Subject: [PATCH 1/2] fix(ci): find the correct Vercel deployment in link checker workflow patrickedqvist/wait-for-vercel-preview hardcodes actorName to vercel[bot], but this repo's own vercel-preview.yml/vercel-production.yml create GitHub Deployments as github-actions[bot], so the action always timed out with "no vercel deployment found, exiting...". That action also targets the deprecated node20 runtime. Replace it with an inline actions/github-script (node24) step that queries the Deployments API directly by commit sha + environment ("Preview (GitHub Actions)" for PRs, "Production" for pushes to main) and reads the environment_url off a successful deployment status, mirroring the equivalent step in authzed/web's test.yml. Also bump actions/checkout@v3 (node16) to the node24 v6 pin already used elsewhere in this repo. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/link-checker-full.yaml | 2 +- .github/workflows/link-checker.yaml | 66 +++++++++++++++++++++--- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/.github/workflows/link-checker-full.yaml b/.github/workflows/link-checker-full.yaml index e7ab021d..d33ffb95 100644 --- a/.github/workflows/link-checker-full.yaml +++ b/.github/workflows/link-checker-full.yaml @@ -12,7 +12,7 @@ jobs: runs-on: "ubuntu-latest" timeout-minutes: 15 steps: - - uses: "actions/checkout@v3" + - uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6 - name: "Link Checker" uses: "filiph/linkcheck@3.0.0" with: diff --git a/.github/workflows/link-checker.yaml b/.github/workflows/link-checker.yaml index af27c842..d8abd60a 100644 --- a/.github/workflows/link-checker.yaml +++ b/.github/workflows/link-checker.yaml @@ -14,13 +14,67 @@ jobs: preview_url: "${{ steps.waitForVercelPreviewDeployment.outputs.url }}" steps: - name: "Wait for Vercel preview deployment to be ready" - uses: "patrickedqvist/wait-for-vercel-preview@v1.3.1" + uses: "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3" # v9 id: "waitForVercelPreviewDeployment" with: - token: "${{ secrets.GITHUB_TOKEN }}" - check_interval: 30 - max_timeout: 600 - allow_inactive: true + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const isPullRequest = Boolean(context.payload.pull_request); + + // The docs repo deploys via its own workflows (vercel-preview.yml, + // vercel-production.yml), which create the GitHub Deployment as + // github-actions[bot], not vercel[bot]. + const sha = isPullRequest + ? context.payload.pull_request.head.sha + : context.sha; + const environment = isPullRequest + ? "Preview (GitHub Actions)" + : "Production"; + + const checkIntervalSeconds = 30; + const maxTimeoutSeconds = 600; + const iterations = Math.floor(maxTimeoutSeconds / checkIntervalSeconds); + + core.info("Waiting 30 seconds for deployment workflow to start..."); + await new Promise((resolve) => setTimeout(resolve, 30000)); + + for (let i = 0; i < iterations; i++) { + const { data: deployments } = await github.rest.repos.listDeployments({ + owner, + repo, + sha, + environment, + }); + + const deployment = deployments.sort((a, b) => b.id - a.id)[0]; + + if (deployment) { + const { data: statuses } = await github.rest.repos.listDeploymentStatuses({ + owner, + repo, + deployment_id: deployment.id, + }); + + const status = statuses[0]; + + if (status && status.state === "success" && status.environment_url) { + core.info(`Deployment successful! URL: ${status.environment_url}`); + core.setOutput("url", status.environment_url); + return; + } + + if (status && ["failure", "error", "inactive", "cancelled"].includes(status.state)) { + core.setFailed(`Vercel deployment ${deployment.id} reported state "${status.state}"`); + return; + } + } + + core.info(`Waiting for Vercel deployment (attempt ${i + 1} / ${iterations})`); + await new Promise((resolve) => setTimeout(resolve, checkIntervalSeconds * 1000)); + } + + core.setFailed(`Timed out waiting for a successful Vercel deployment on ${sha}`); link_checker: name: "Link Checker" @@ -28,7 +82,7 @@ jobs: needs: "preview" timeout-minutes: 15 steps: - - uses: "actions/checkout@v3" + - uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6 - name: "Link Checker" uses: "filiph/linkcheck@3.0.0" with: From 9ef31f2ef9967c20bf67abec3dd13b451ed13d09 Mon Sep 17 00:00:00 2001 From: Sam Kim Date: Fri, 18 Sep 2026 10:29:05 -0700 Subject: [PATCH 2/2] fix(docs): redirect broken breadcrumb links, quiet anchor warnings Nextra's breadcrumb component links a top-level section's crumb (e.g. "SpiceDB") to its first child folder's bare route when the section has no index page of its own, rather than resolving to a real leaf page. Since spicedb/getting-started, authzed/guides, and materialize/getting-started also have no index page, every page in those sections rendered a breadcrumb crumb pointing at a 404. No content ever links to the bare paths directly, so redirect them in next.config.mjs instead of adding index pages. Also add --no-check-anchors to link-checker.yaml: Nextra doesn't render heading ids into server-rendered HTML, so the crawler flagged every #fragment link as a false-positive missing anchor. link-checker-full.yaml already carries this flag for the same reason. Document both quirks, plus the wait-for-vercel-preview fix from the prior commit, in a new CLAUDE.md "Known Quirks" section. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/link-checker.yaml | 7 ++++++- CLAUDE.md | 6 ++++++ next.config.mjs | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/link-checker.yaml b/.github/workflows/link-checker.yaml index d8abd60a..0091d4a7 100644 --- a/.github/workflows/link-checker.yaml +++ b/.github/workflows/link-checker.yaml @@ -86,4 +86,9 @@ jobs: - name: "Link Checker" uses: "filiph/linkcheck@3.0.0" with: - arguments: "--skip-file /github/workspace/linkcheck-skip.txt ${{ needs.preview.outputs.preview_url }}" + # --no-check-anchors: Nextra doesn't render heading `id`s into the + # static HTML server-side (they're attached client-side after + # hydration), so the crawler flags every #fragment link as a + # missing anchor. link-checker-full.yaml already disables this for + # the same reason. + arguments: "--skip-file /github/workspace/linkcheck-skip.txt --no-check-anchors ${{ needs.preview.outputs.preview_url }}" diff --git a/CLAUDE.md b/CLAUDE.md index 6e68e755..e9ab5365 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,12 @@ pnpm format # Format code pnpm lint:markdown # Lint markdown files ``` +## Known Quirks + +- **Nextra breadcrumb 404s on section index paths.** `nextra-theme-docs`'s breadcrumb component links a top-level section's crumb (e.g. "SpiceDB", from `app/_meta.ts`) to `item.children[0].route` when that section has no index page of its own — the bare path of its first child folder (per that folder's `_meta.ts` order), not a recursively-resolved real page. If that first child also has no index page (true for `spicedb/getting-started`, `authzed/guides`, `materialize/getting-started`), every page in the section renders a breadcrumb crumb pointing at a 404. No content ever links to these bare paths directly — only the breadcrumb does, dynamically, on every page under that section — so the fix is a permanent redirect in `next.config.mjs` from the bare path to the intended real page, not an index page or a content edit. Watch for this again if a new top-level section (or a new first-listed subsection) is added without its own index page. +- **Link checker needs `--no-check-anchors`.** Nextra doesn't render heading `id`s into server-rendered HTML — they're attached client-side after hydration — so a static-HTML crawler (`filiph/linkcheck`, used by `link-checker.yaml`/`link-checker-full.yaml`) flags every `#fragment` link as a false-positive missing anchor without this flag. Both link-checker workflows pass it. +- **The Vercel preview-deploy wait step doesn't use `patrickedqvist/wait-for-vercel-preview`.** That action hardcodes `actorName: 'vercel[bot]'`, but this repo's own `vercel-preview.yml`/`vercel-production.yml` create GitHub Deployments as `github-actions[bot]` (no native Vercel GitHub App integration), so the action always timed out. `link-checker.yaml`'s `preview` job instead queries the Deployments API directly by commit sha + environment (`"Preview (GitHub Actions)"` for PRs, `"Production"` for pushes to main) via an inline `actions/github-script` step, mirroring the equivalent step in `authzed/web`'s `test.yml`. + ## Notes - Markdown linting is lenient (most rules disabled, custom sentence-per-line rule) diff --git a/next.config.mjs b/next.config.mjs index 102ee3aa..76933028 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -77,6 +77,26 @@ export default withNextra({ destination: "/spicedb/integrations/langchain-spicedb", permanent: true, }, + // /spicedb/getting-started, /authzed/guides, and /materialize/getting-started + // are section folders with no index page of their own. Nextra's breadcrumb + // component links a section's top-level crumb (e.g. "SpiceDB") to its first + // child folder's bare route rather than resolving to a real leaf page, so + // without these redirects those crumbs 404. + { + source: "/spicedb/getting-started", + destination: "/spicedb/getting-started/discovering-spicedb", + permanent: true, + }, + { + source: "/authzed/guides", + destination: "/authzed/guides/cloud", + permanent: true, + }, + { + source: "/materialize/getting-started", + destination: "/materialize/getting-started/overview", + permanent: true, + }, ]; }, // This is necessary because we're using CDN domains.