From 9788005073214e2f07c068771d3be401705318a6 Mon Sep 17 00:00:00 2001 From: Chris Garber Date: Tue, 10 Nov 2026 13:21:09 -0500 Subject: [PATCH 1/6] ci: validate Fern links and modernize docs previews --- .github/required-checks.ruleset.json | 23 +++++++++++ .github/workflows/check-live-links.yml | 44 ++++++++++++++++++++ .github/workflows/check.yml | 9 ++++- .github/workflows/preview-docs.yml | 56 ++++++++++++++++++++++++-- README.md | 38 +++++++++++++++++ fern/docs.yml | 5 +++ fern/quickstart/introduction.mdx | 2 +- scripts/fern/README.md | 15 ++++++- 8 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 .github/required-checks.ruleset.json create mode 100644 .github/workflows/check-live-links.yml diff --git a/.github/required-checks.ruleset.json b/.github/required-checks.ruleset.json new file mode 100644 index 000000000..1daa6b61e --- /dev/null +++ b/.github/required-checks.ruleset.json @@ -0,0 +1,23 @@ +{ + "name": "Documentation CI", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "required_status_checks": [ + { "context": "fern-check", "integration_id": 15368 }, + { "context": "Docs Preview", "integration_id": 15368 } + ] + } + } + ] +} diff --git a/.github/workflows/check-live-links.yml b/.github/workflows/check-live-links.yml new file mode 100644 index 000000000..8025d1b11 --- /dev/null +++ b/.github/workflows/check-live-links.yml @@ -0,0 +1,44 @@ +name: Check Live Docs Links + +on: + schedule: + - cron: "0 8 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: check-live-docs-links + cancel-in-progress: false + +jobs: + check-links: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-fern + + - name: Check published internal and external links + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: | + node "$FERN_CLI_PATH" docs link check --url https://docs.vapi.ai --output json > "$RUNNER_TEMP/fern-links.json" + node - <<'NODE' + const fs = require('node:fs'); + const report = JSON.parse(fs.readFileSync(`${process.env.RUNNER_TEMP}/fern-links.json`, 'utf8')); + if (report.summary.status !== 'complete') { + throw new Error('Fern returned an incomplete link scan; rerun the workflow.'); + } + NODE + + - name: Save link report + if: always() + uses: actions/upload-artifact@v4 + with: + name: fern-live-links + path: ${{ runner.temp }}/fern-links.json + retention-days: 14 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b8f63ce4b..b1591506d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + jobs: fern-check: runs-on: ubuntu-latest @@ -15,5 +18,7 @@ jobs: - uses: ./.github/actions/setup-fern - - name: Check API is valid - run: node scripts/fern/run.cjs check + - name: Validate docs, links, and API configuration + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: node scripts/fern/run.cjs check --warnings diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index d7b61f5d6..7959a7717 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -1,11 +1,26 @@ name: Preview Docs -on: pull_request +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, closed] + +concurrency: + group: docs-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read jobs: run: + name: Docs Preview + if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest - permissions: write-all + permissions: + contents: read + pull-requests: write + env: + PREVIEW_ID: pr-${{ github.event.pull_request.number }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -19,8 +34,12 @@ jobs: POSTHOG_PROJECT_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }} run: | set -euo pipefail - node scripts/fern/run.cjs generate --docs --preview --log-level debug 2>&1 | tee "$RUNNER_TEMP/fern-preview.log" - URL=$(grep -oP 'Published docs to \K.*(?= \()' "$RUNNER_TEMP/fern-preview.log") + node scripts/fern/run.cjs generate --docs --preview --id "$PREVIEW_ID" --no-prompt --log-level debug 2>&1 | tee "$RUNNER_TEMP/fern-preview.log" + URL=$(sed -nE 's/.*Published docs to (https?:\/\/[^[:space:]]+).*/\1/p' "$RUNNER_TEMP/fern-preview.log" | tail -n 1) + if [ -z "$URL" ]; then + echo "::error::Fern did not return a preview URL." + exit 1 + fi echo "Preview URL: $URL" echo "🌿 Preview your docs: $URL" > preview_url.txt @@ -28,3 +47,32 @@ jobs: uses: thollander/actions-comment-pull-request@v2.4.3 with: filePath: preview_url.txt + comment_tag: preview-docs + mode: upsert + + cleanup: + name: Delete Docs Preview + if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + + - uses: ./.github/actions/setup-fern + + - name: Delete preview for the closed PR + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + PREVIEW_ID: pr-${{ github.event.pull_request.number }} + run: | + set -euo pipefail + if node "$FERN_CLI_PATH" docs preview delete --id "$PREVIEW_ID" 2>&1 | tee "$RUNNER_TEMP/fern-preview-delete.log"; then + exit 0 + fi + if grep -q 'Preview site not found:' "$RUNNER_TEMP/fern-preview-delete.log"; then + echo "No preview exists for this PR; nothing to delete." + else + exit 1 + fi diff --git a/README.md b/README.md index f5fdbeae5..75fd5db2a 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,41 @@ To preview your documentation, run fern generate --docs --preview ``` The repository contains GitHub workflows that will automatically run these commands for you. For example, when you make a PR a preview link will be auto-generated and when you merge to main the docs site will update. + +### Documentation checks + +Every pull request runs `fern check --warnings` with the pinned CLI and the +example-cache adapter. Internal broken links fail validation and publication. +API example validation and missing redirects are warnings while existing findings +are reviewed. Missing-redirect detection compares against the published site and +needs the `FERN_TOKEN` repository secret; fork PRs still receive local validation. +See [the cache setup and verification commands](scripts/fern/README.md). + +Same-repository PRs receive a stable preview named `pr-`. Subsequent pushes +update the preview and its PR comment, and closing or merging the PR deletes the +preview. Fork PRs skip hosted previews because repository secrets are unavailable. + +The **Check Live Docs Links** workflow scans the published site daily at 08:00 UTC +and can also be run manually. Broken links and incomplete scans fail the job. +Blocked URLs (such as HTTP 403 responses) are reported separately by Fern and do +not fail the scan. Download the `fern-live-links` artifact for the JSON report, +including source pages. This workflow monitors external URLs as well as deployed +internal links; it is separate from PR validation. + +### Required checks on main + +After merging the CI configuration and confirming that **fern-check** and +**Docs Preview** appear on a PR, a repository administrator should apply the +additive [Documentation CI ruleset](.github/required-checks.ruleset.json): + +```sh +gh api --method POST repos/VapiAI/docs/rulesets \ + --input .github/required-checks.ruleset.json +``` + +Apply this once. If a `Documentation CI` ruleset already exists, update that ruleset +instead of creating a duplicate. It requires both checks from GitHub Actions on +an up-to-date branch and leaves the existing review rules intact. **Docs Preview** +is skipped successfully for forks; their **fern-check** must still pass. SDK +preview jobs run only for matching paths, and the live scan runs on a schedule, so +neither is a required check in this ruleset. diff --git a/fern/docs.yml b/fern/docs.yml index 97c0dfb48..2a954c162 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -10,6 +10,11 @@ instances: branch: main title: Vapi +check: + rules: + broken-links: error + example-validation: warn + missing-redirects: warn favicon: static/images/favicon.ico logo: light: static/images/logo/logo-light.svg diff --git a/fern/quickstart/introduction.mdx b/fern/quickstart/introduction.mdx index 1ad99a1e3..70ce6783f 100644 --- a/fern/quickstart/introduction.mdx +++ b/fern/quickstart/introduction.mdx @@ -166,7 +166,7 @@ Work with Vapi from your terminal, or manage your whole configuration as code: -To manage Vapi configuration as code (assistants, squads, tools, structured outputs, simulations and evals as files in git, across one or more orgs or environments), use the open-source repository at https://github.com/VapiAI/gitops and read https://docs.vapi.ai/config-as-code.md before setting it up. +To manage Vapi configuration as code (assistants, squads, tools, structured outputs, simulations and evals as files in git, across one or more orgs or environments), use the open-source repository at https://github.com/VapiAI/gitops and read the [config-as-code guide](/config-as-code) before setting it up. ### Agent skills diff --git a/scripts/fern/README.md b/scripts/fern/README.md index ee197a375..d13f2e4d0 100644 --- a/scripts/fern/README.md +++ b/scripts/fern/README.md @@ -52,7 +52,7 @@ original Fern commands also removes this optimization. npm install --prefix /tmp/vapi-fern --no-save fern-api@5.151.1 export FERN_CLI_PATH=/tmp/vapi-fern/node_modules/fern-api/cli.cjs node --test scripts/fern/example-cache.test.cjs -node scripts/fern/run.cjs check --local --log-level debug +node scripts/fern/run.cjs check --local --warnings --log-level debug node scripts/fern/run.cjs generate --docs --preview --log-level debug ``` @@ -60,6 +60,19 @@ Only use `--preview` for verification. Production publishing remains in the existing main-branch workflow. Logs print cache hits, misses, and uncached diagnostic-producing calls at exit. +The required internal-link rule in `fern/docs.yml` also builds the API navigation. +This can be expensive on a cold cache. Keep it enabled when measuring cache +performance; a check that skips links does not exercise the same work as a docs +build. + +When upgrading Fern, update the pinned version in `fern/fern.config.json`, +`.github/actions/setup-fern/action.yml`, and `run.cjs` together. Review the new +bundle before updating its checksum and function markers. Run the safeguard tests, +compare the unmodified CLI with cold and warm adapter checks (including warnings), +and verify a preview publication before merging. Confirm the preview deletion and +live-link commands still work with the new CLI. Do not update only the checksum +to bypass an unexpected-bundle failure. + ## Measured validation results On the same machine and API inputs, the original generators took 697 seconds From 09fd9d3f06eee605fa8dd587a91e750cb3af8ec5 Mon Sep 17 00:00:00 2001 From: Chris Garber Date: Tue, 10 Nov 2026 13:22:57 -0500 Subject: [PATCH 2/6] ci: verify live-link workflow changes on PRs --- .github/workflows/check-live-links.yml | 4 ++++ README.md | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-live-links.yml b/.github/workflows/check-live-links.yml index 8025d1b11..2f3a926d7 100644 --- a/.github/workflows/check-live-links.yml +++ b/.github/workflows/check-live-links.yml @@ -4,6 +4,9 @@ on: schedule: - cron: "0 8 * * *" workflow_dispatch: + pull_request: + paths: + - .github/workflows/check-live-links.yml permissions: contents: read @@ -14,6 +17,7 @@ concurrency: jobs: check-links: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 30 steps: diff --git a/README.md b/README.md index 75fd5db2a..19a5000fe 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,13 @@ update the preview and its PR comment, and closing or merging the PR deletes the preview. Fork PRs skip hosted previews because repository secrets are unavailable. The **Check Live Docs Links** workflow scans the published site daily at 08:00 UTC -and can also be run manually. Broken links and incomplete scans fail the job. +and can also be run manually. Changes to that workflow run the scan on +same-repository PRs to verify its configuration before merge. Broken links and +incomplete scans fail the job. Blocked URLs (such as HTTP 403 responses) are reported separately by Fern and do not fail the scan. Download the `fern-live-links` artifact for the JSON report, including source pages. This workflow monitors external URLs as well as deployed -internal links; it is separate from PR validation. +internal links; ordinary docs PRs use the local link validator. ### Required checks on main From ae1482f5be74ed79b2e8aa374b5ea8c1743a6646 Mon Sep 17 00:00:00 2001 From: Chris Garber Date: Tue, 10 Nov 2026 13:44:46 -0500 Subject: [PATCH 3/6] ci: summarize live link findings in workflow runs --- .github/workflows/check-live-links.yml | 29 +++++++++++++++++++++++--- README.md | 7 ++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-live-links.yml b/.github/workflows/check-live-links.yml index 2f3a926d7..f83723642 100644 --- a/.github/workflows/check-live-links.yml +++ b/.github/workflows/check-live-links.yml @@ -29,12 +29,35 @@ jobs: - name: Check published internal and external links env: FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: node "$FERN_CLI_PATH" docs link check --url https://docs.vapi.ai --output json > "$RUNNER_TEMP/fern-links.json" + + - name: Summarize link findings + if: always() run: | - node "$FERN_CLI_PATH" docs link check --url https://docs.vapi.ai --output json > "$RUNNER_TEMP/fern-links.json" node - <<'NODE' const fs = require('node:fs'); - const report = JSON.parse(fs.readFileSync(`${process.env.RUNNER_TEMP}/fern-links.json`, 'utf8')); - if (report.summary.status !== 'complete') { + const path = `${process.env.RUNNER_TEMP}/fern-links.json`; + if (!fs.existsSync(path) || fs.statSync(path).size === 0) { + throw new Error('Fern did not produce a link report; see the scan logs.'); + } + const report = JSON.parse(fs.readFileSync(path, 'utf8')); + const { summary, brokenLinks, blockedLinks } = report; + const lines = [ + '## Live docs link scan', + '', + `Status: ${summary.status}. Scanned ${summary.totalPages} pages and ${summary.totalLinks} links.`, + `Reported ${summary.brokenLinks} broken and ${summary.blockedLinks} blocked links.`, + ]; + for (const [label, links] of [['Broken links', brokenLinks], ['Blocked links (non-blocking)', blockedLinks]]) { + lines.push('', `### ${label}`, ''); + for (const link of links) { + const sources = link.references.map(ref => ref.filePath ?? ref.slug).filter(Boolean).join(', '); + lines.push(`- ${link.statusCode ?? 'No response'}: ${link.url} — ${sources}`); + } + if (links.length === 0) lines.push('None.'); + } + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join('\n') + '\n'); + if (summary.status !== 'complete') { throw new Error('Fern returned an incomplete link scan; rerun the workflow.'); } NODE diff --git a/README.md b/README.md index 19a5000fe..fd00df5e5 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ and can also be run manually. Changes to that workflow run the scan on same-repository PRs to verify its configuration before merge. Broken links and incomplete scans fail the job. Blocked URLs (such as HTTP 403 responses) are reported separately by Fern and do -not fail the scan. Download the `fern-live-links` artifact for the JSON report, -including source pages. This workflow monitors external URLs as well as deployed -internal links; ordinary docs PRs use the local link validator. +not fail the scan. The run summary lists findings and source pages; download the +`fern-live-links` artifact for the JSON report. This workflow monitors external +URLs as well as deployed internal links; ordinary docs PRs use the local link +validator. ### Required checks on main From 7ef4f5d37b979e25a28b94f85efd125d6df75674 Mon Sep 17 00:00:00 2001 From: Chris Garber Date: Tue, 10 Nov 2026 13:59:23 -0500 Subject: [PATCH 4/6] ci: name the published link check check-external-links --- .github/workflows/check-live-links.yml | 8 ++++---- README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-live-links.yml b/.github/workflows/check-live-links.yml index f83723642..b7f107f31 100644 --- a/.github/workflows/check-live-links.yml +++ b/.github/workflows/check-live-links.yml @@ -1,4 +1,4 @@ -name: Check Live Docs Links +name: check-external-links on: schedule: @@ -16,7 +16,7 @@ concurrency: cancel-in-progress: false jobs: - check-links: + check-external-links: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 30 @@ -26,7 +26,7 @@ jobs: - uses: ./.github/actions/setup-fern - - name: Check published internal and external links + - name: Check external and internal links on published docs env: FERN_TOKEN: ${{ secrets.FERN_TOKEN }} run: node "$FERN_CLI_PATH" docs link check --url https://docs.vapi.ai --output json > "$RUNNER_TEMP/fern-links.json" @@ -43,7 +43,7 @@ jobs: const report = JSON.parse(fs.readFileSync(path, 'utf8')); const { summary, brokenLinks, blockedLinks } = report; const lines = [ - '## Live docs link scan', + '## External and internal link scan', '', `Status: ${summary.status}. Scanned ${summary.totalPages} pages and ${summary.totalLinks} links.`, `Reported ${summary.brokenLinks} broken and ${summary.blockedLinks} blocked links.`, diff --git a/README.md b/README.md index fd00df5e5..6c5c400fa 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Same-repository PRs receive a stable preview named `pr-`. Subsequent pus update the preview and its PR comment, and closing or merging the PR deletes the preview. Fork PRs skip hosted previews because repository secrets are unavailable. -The **Check Live Docs Links** workflow scans the published site daily at 08:00 UTC +The **check-external-links** workflow scans the published site daily at 08:00 UTC and can also be run manually. Changes to that workflow run the scan on same-repository PRs to verify its configuration before merge. Broken links and incomplete scans fail the job. From 1a69d91b197f2e939b0226469f412fac404b324d Mon Sep 17 00:00:00 2001 From: Chris Garber Date: Tue, 10 Nov 2026 14:01:03 -0500 Subject: [PATCH 5/6] ci: rename the external link workflow file --- .../{check-live-links.yml => check-external-links.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{check-live-links.yml => check-external-links.yml} (98%) diff --git a/.github/workflows/check-live-links.yml b/.github/workflows/check-external-links.yml similarity index 98% rename from .github/workflows/check-live-links.yml rename to .github/workflows/check-external-links.yml index b7f107f31..5495a8b56 100644 --- a/.github/workflows/check-live-links.yml +++ b/.github/workflows/check-external-links.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: pull_request: paths: - - .github/workflows/check-live-links.yml + - .github/workflows/check-external-links.yml permissions: contents: read From aa3356b08902e292f421ef6c1d33ca1267af1fdc Mon Sep 17 00:00:00 2001 From: Chris Garber Date: Tue, 10 Nov 2026 14:04:37 -0500 Subject: [PATCH 6/6] docs: keep ruleset activation instructions in the PR --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/README.md b/README.md index 6c5c400fa..180de92cc 100644 --- a/README.md +++ b/README.md @@ -67,21 +67,3 @@ not fail the scan. The run summary lists findings and source pages; download the `fern-live-links` artifact for the JSON report. This workflow monitors external URLs as well as deployed internal links; ordinary docs PRs use the local link validator. - -### Required checks on main - -After merging the CI configuration and confirming that **fern-check** and -**Docs Preview** appear on a PR, a repository administrator should apply the -additive [Documentation CI ruleset](.github/required-checks.ruleset.json): - -```sh -gh api --method POST repos/VapiAI/docs/rulesets \ - --input .github/required-checks.ruleset.json -``` - -Apply this once. If a `Documentation CI` ruleset already exists, update that ruleset -instead of creating a duplicate. It requires both checks from GitHub Actions on -an up-to-date branch and leaves the existing review rules intact. **Docs Preview** -is skipped successfully for forks; their **fern-check** must still pass. SDK -preview jobs run only for matching paths, and the live scan runs on a schedule, so -neither is a required check in this ruleset.