fix(docs): missing vitepress route for api-docs#1278
Conversation
Apparently a newer version of bun adds a configVersion key to the lock file.
The button on the homepage links to /api-docs/ is intented to be populated by rustdoc output during CI. However, VitePress has no page at that route, so its router intercepts the click and renders a 404 without even though an index.html exists. Add a placeholder at `docs/api-docs/index.md` so VitePress registers the route. When publishing docs we overwrite this and instead serve the real API documentation.
Binary Size Analysis (Agent Data Plane)Target: f40ce45 (baseline) vs 00f180c (comparison) diff
|
| Module | File Size | Symbols |
|---|
Detailed Symbol Changes
FILE SIZE VM SIZE
-------------- --------------
[ = ] 0 [ = ] 0 TOTAL
Regression Detector (Agent Data Plane)Regression Detector ResultsRun ID: 8fad7f25-76ea-439e-acdb-9f8764307789 Baseline: f40ce45 Optimization Goals: ✅ No significant changes detected
|
| perf | experiment | goal | Δ mean % | Δ mean % CI | trials | links |
|---|---|---|---|---|---|---|
| ❌ | otlp_ingest_logs_5mb_memory | memory utilization | +7.56 | [+6.89, +8.23] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_cpu | % cpu utilization | +0.30 | [-4.64, +5.23] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_throughput | ingress throughput | -0.03 | [-0.16, +0.10] | 1 | (metrics) (profiles) (logs) |
Fine details of change detection per experiment
| perf | experiment | goal | Δ mean % | Δ mean % CI | trials | links |
|---|---|---|---|---|---|---|
| ➖ | dsd_uds_1mb_3k_contexts_cpu | % cpu utilization | +10.26 | [-47.06, +67.59] | 1 | (metrics) (profiles) (logs) |
| ❌ | otlp_ingest_logs_5mb_memory | memory utilization | +7.56 | [+6.89, +8.23] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_metrics_5mb_memory | memory utilization | +3.31 | [+3.09, +3.54] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_500mb_3k_contexts_throughput | ingress throughput | +3.23 | [+3.03, +3.43] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_500mb_3k_contexts_memory | memory utilization | +0.55 | [+0.38, +0.72] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_5mb_cpu | % cpu utilization | +0.44 | [-1.57, +2.44] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_low | memory utilization | +0.40 | [+0.20, +0.60] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_cpu | % cpu utilization | +0.30 | [-4.64, +5.23] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_100mb_3k_contexts_cpu | % cpu utilization | +0.27 | [-5.79, +6.33] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_filtering_5mb_memory | memory utilization | +0.24 | [-0.10, +0.58] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_10mb_3k_contexts_memory | memory utilization | +0.09 | [-0.09, +0.28] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_500mb_3k_contexts_cpu | % cpu utilization | +0.03 | [-1.26, +1.31] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_10mb_3k_contexts_throughput | ingress throughput | +0.02 | [-0.11, +0.15] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_5mb_memory | memory utilization | +0.01 | [-0.25, +0.27] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_100mb_3k_contexts_throughput | ingress throughput | +0.01 | [-0.03, +0.04] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_512kb_3k_contexts_throughput | ingress throughput | +0.01 | [-0.05, +0.06] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_1mb_3k_contexts_throughput | ingress throughput | +0.00 | [-0.05, +0.06] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_filtering_5mb_throughput | ingress throughput | -0.00 | [-0.02, +0.02] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_5mb_throughput | ingress throughput | -0.00 | [-0.02, +0.02] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_transform_5mb_throughput | ingress throughput | -0.00 | [-0.02, +0.02] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_metrics_5mb_throughput | ingress throughput | -0.01 | [-0.13, +0.11] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_throughput | ingress throughput | -0.03 | [-0.16, +0.10] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_100mb_3k_contexts_memory | memory utilization | -0.10 | [-0.28, +0.08] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_medium | memory utilization | -0.10 | [-0.30, +0.09] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_transform_5mb_memory | memory utilization | -0.11 | [-0.36, +0.14] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_idle | memory utilization | -0.19 | [-0.23, -0.15] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_ultraheavy | memory utilization | -0.29 | [-0.41, -0.17] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_transform_5mb_cpu | % cpu utilization | -0.39 | [-2.22, +1.43] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_512kb_3k_contexts_memory | memory utilization | -0.40 | [-0.57, -0.23] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_heavy | memory utilization | -0.50 | [-0.64, -0.37] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_1mb_3k_contexts_memory | memory utilization | -0.61 | [-0.79, -0.44] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_metrics_5mb_cpu | % cpu utilization | -0.90 | [-7.69, +5.89] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_filtering_5mb_cpu | % cpu utilization | -1.12 | [-3.42, +1.17] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_512kb_3k_contexts_cpu | % cpu utilization | -4.38 | [-60.65, +51.90] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_10mb_3k_contexts_cpu | % cpu utilization | -5.70 | [-36.52, +25.12] | 1 | (metrics) (profiles) (logs) |
Bounds Checks: ✅ Passed
| perf | experiment | bounds_check_name | replicates_passed | observed_value | links |
|---|---|---|---|---|---|
| ✅ | quality_gates_rss_dsd_heavy | memory_usage | 10/10 | 113.38MiB ≤ 140MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_dsd_low | memory_usage | 10/10 | 33.71MiB ≤ 50MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_dsd_medium | memory_usage | 10/10 | 53.30MiB ≤ 75MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_dsd_ultraheavy | memory_usage | 10/10 | 164.48MiB ≤ 200MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_idle | memory_usage | 10/10 | 20.88MiB ≤ 40MiB | (metrics) (profiles) (logs) |
Explanation
Confidence level: 90.00%
Effect size tolerance: |Δ mean %| ≥ 5.00%
Performance changes are noted in the perf column of each table:
- ✅ = significantly better comparison variant performance
- ❌ = significantly worse comparison variant performance
- ➖ = no significant change in performance
A regression test is an A/B test of target performance in a repeatable rig, where "performance" is measured as "comparison variant minus baseline variant" for an optimization goal (e.g., ingress throughput). Due to intrinsic variability in measuring that goal, we can only estimate its mean value for each experiment; we report uncertainty in that value as a 90.00% confidence interval denoted "Δ mean % CI".
For each experiment, we decide whether a change in performance is a "regression" -- a change worth investigating further -- if all of the following criteria are true:
-
Its estimated |Δ mean %| ≥ 5.00%, indicating the change is big enough to merit a closer look.
-
Its 90.00% confidence interval "Δ mean % CI" does not contain zero, indicating that if our statistical model is accurate, there is at least a 90.00% chance there is a difference in performance between baseline and comparison variants.
-
Its configuration does not mark it "erratic".
tobz
left a comment
There was a problem hiding this comment.
Yeah, doesn't hurt to give an explainer for local dev on why there's no actual docs. 👍🏻
That button doesn't work on the live site either and I think this may fix it. |
## Summary ### TL;DR This link button is broken and I think this PR will fix it 🤞 <img width="787" height="574" alt="image" src="https://github.com/user-attachments/assets/33cf11ea-8eed-4ed5-96cb-15b7e7d52cb8" /> ### Too Short; Need More Vitepress is using some sort of router that doesn't have an entry for `api-docs/index.html` because there isn't something there when we do the vitepress build step. If we put a placeholder Markdown file there then the router will have an entry. In our CI publish step we write the output of our Rust docs build to that location, so we will serve the Rust docs as intended. Claude is highly convinced this is going to work. ### Long-Winded AI Explaination Add a placeholder file at `docs/api-docs/index.md`. This gives VitePress a page at that route, so the SPA router resolves it instead of showing 404. In production, `docs.yml` runs two jobs: `generate-docs-site` (VitePress build) and `generate-api-docs` (`cargo +nightly doc`). The `publish` job extracts VitePress to `/tmp/docs/`, then extracts rustdoc to `/tmp/docs/api-docs/`, overwriting the placeholder `index.html` with the real rustdoc output. Locally, `make run-docs` serves only VitePress, so developers see the placeholder explaining what belongs there and how to generate API docs with `cargo doc`. #### Detailed Reasoning **Step 1: VitePress builds the docs site (including our placeholder)** `.github/workflows/docs.yml:17-22` — the `generate-docs-site` job: ```yaml - name: Build documentation run: | yarn install yarn run docs:build - name: Compress generated documentation assets run: tar -C ./docs/.vitepress/dist -c -z -f docs.tar.gz . ``` `yarn run docs:build` invokes VitePress, which compiles every `.md` file under `docs/` into static HTML. Our new file `docs/api-docs/index.md` becomes `docs/.vitepress/dist/api-docs/index.html` — the placeholder. VitePress also adds `api-docs_index.md` to its `hashmap.json`, which is the SPA router's page lookup table. This is the key part of the fix: without an entry in the hashmap, VitePress's client-side JavaScript intercepts clicks to `/api-docs/`, finds no match, and renders "404 page not found" without ever making a server request. With the entry, VitePress routes to it normally. The job tars the entire `docs/.vitepress/dist/` directory (including `api-docs/index.html`) into `docs.tar.gz` and uploads it as an artifact. **Step 2: CI generates rustdoc** `.github/workflows/docs.yml:42-48` — the `generate-api-docs` job: ```yaml - name: Generate API documentation run: | RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --no-deps -Zrustdoc-map --lib - name: Compress generated API documentation assets run: tar -C ./target/doc -c -z -f api-docs.tar.gz . ``` This runs `cargo doc` on nightly, generating rustdoc HTML into `target/doc/`. The `--enable-index-page` flag produces an `index.html` listing all crates. The job tars `target/doc/` into `api-docs.tar.gz` and uploads it. **Step 3: Publish composites them — rustdoc overwrites the placeholder** `.github/workflows/docs.yml:78-83` — the `publish` job extracts both artifacts sequentially: ```yaml - name: Create extract directory run: mkdir -p /tmp/docs/api-docs - name: Uncompress generated documentation assets run: tar -C /tmp/docs -x -z -f ./docs.tar.gz && rm ./docs.tar.gz - name: Uncompress generated API documentation assets run: tar -C /tmp/docs/api-docs -x -z -f ./api-docs.tar.gz && rm ./api-docs.tar.gz ``` Line 81 extracts VitePress into `/tmp/docs/`. This puts our placeholder at `/tmp/docs/api-docs/index.html`. Line 83 then extracts rustdoc into `/tmp/docs/api-docs/`, overwriting `/tmp/docs/api-docs/index.html` with rustdoc's crate index page. The order matters: VitePress first, rustdoc second. The entire `/tmp/docs/` directory is then published to `gh-pages` (line 84-91). **Why the placeholder never appears in production** The rustdoc `index.html` (a full standalone HTML page listing all Saluki crates) replaces the VitePress-generated `index.html` (our placeholder) byte-for-byte on disk before publish. GitHub Pages serves the rustdoc version. The placeholder only exists transiently during CI and locally when running `make run-docs`. ## Change Type - [X] Bug fix - [ ] New feature - [X] Non-functional (chore, refactoring, docs) - [ ] Performance ## How did you test this PR? Hard to know for sure if this will fix the issue in production, but I tried to replicate what happens in production locally like this (and it worked): ```bash #!/usr/bin/env bash set -euo pipefail cd ~/repos/saluki # ─── generate-docs-site job (.github/workflows/docs.yml:17-22) ─── # Corresponds to: yarn run docs:build # VitePress compiles all .md files under docs/ into static HTML. # Our placeholder at docs/api-docs/index.md becomes dist/api-docs/index.html. bun run docs:build # Corresponds to: tar -C ./docs/.vitepress/dist -c -z -f docs.tar.gz . tar -C ./docs/.vitepress/dist -c -z -f /tmp/docs.tar.gz . # ─── generate-api-docs job (.github/workflows/docs.yml:42-48) ─── # Corresponds to: cargo +nightly doc (generates rustdoc into target/doc/) RUSTDOCFLAGS="--enable-index-page -Zunstable-options" \ cargo +nightly doc --no-deps -Zrustdoc-map --lib # Corresponds to: tar -C ./target/doc -c -z -f api-docs.tar.gz . tar -C ./target/doc -c -z -f /tmp/api-docs.tar.gz . # ─── publish job (.github/workflows/docs.yml:78-83) ─── # Corresponds to: mkdir -p /tmp/docs/api-docs rm -rf /tmp/docs mkdir -p /tmp/docs/api-docs # Corresponds to: tar -C /tmp/docs -x -z -f ./docs.tar.gz # Extracts VitePress output. Placeholder lands at /tmp/docs/api-docs/index.html. tar -C /tmp/docs -x -z -f /tmp/docs.tar.gz # Corresponds to: tar -C /tmp/docs/api-docs -x -z -f ./api-docs.tar.gz # Extracts rustdoc. Overwrites /tmp/docs/api-docs/index.html with rustdoc's crate index. tar -C /tmp/docs/api-docs -x -z -f /tmp/api-docs.tar.gz # ─── Verify the overwrite ─── echo "--- Checking /tmp/docs/api-docs/index.html ---" if grep -q 'rustdoc' /tmp/docs/api-docs/index.html; then echo "PASS: rustdoc replaced the placeholder" else echo "FAIL: placeholder was NOT overwritten" fi # ─── Serve locally ─── # Note: the site expects to be served under /saluki/ (see docs/.vitepress/config.mts base option). # Create the expected path structure for the local server. rm -rf /tmp/docs-serve mkdir -p /tmp/docs-serve/saluki cp -r /tmp/docs/* /tmp/docs-serve/saluki/ echo "" echo "Serving at http://localhost:8080/saluki/" echo " Homepage: http://localhost:8080/saluki/" echo " API Documentation: http://localhost:8080/saluki/api-docs/" echo "" cd /tmp/docs-serve && python3 -m http.server 8080 ``` ## References Closes #1276 7d03456
Summary
TL;DR
This link button is broken and I think this PR will fix it 🤞
Too Short; Need More
Vitepress is using some sort of router that doesn't have an entry for
api-docs/index.htmlbecause there isn't something there when we do the vitepress build step. If we put a placeholder Markdown file there then the router will have an entry. In our CI publish step we write the output of our Rust docs build to that location, so we will serve the Rust docs as intended. Claude is highly convinced this is going to work.Long-Winded AI Explaination
Add a placeholder file at
docs/api-docs/index.md. This gives VitePress a page at that route,so the SPA router resolves it instead of showing 404.
In production,
docs.ymlruns two jobs:generate-docs-site(VitePress build) andgenerate-api-docs(cargo +nightly doc). Thepublishjob extracts VitePress to/tmp/docs/,then extracts rustdoc to
/tmp/docs/api-docs/, overwriting the placeholderindex.htmlwiththe real rustdoc output. Locally,
make run-docsserves only VitePress, so developers see theplaceholder explaining what belongs there and how to generate API docs with
cargo doc.Detailed Reasoning
Step 1: VitePress builds the docs site (including our placeholder)
.github/workflows/docs.yml:17-22— thegenerate-docs-sitejob:yarn run docs:buildinvokes VitePress, which compiles every.mdfile underdocs/intostatic HTML. Our new file
docs/api-docs/index.mdbecomesdocs/.vitepress/dist/api-docs/index.html— the placeholder. VitePress also addsapi-docs_index.mdto itshashmap.json, which is the SPA router's page lookup table. This isthe key part of the fix: without an entry in the hashmap, VitePress's client-side JavaScript
intercepts clicks to
/api-docs/, finds no match, and renders "404 page not found" without evermaking a server request. With the entry, VitePress routes to it normally.
The job tars the entire
docs/.vitepress/dist/directory (includingapi-docs/index.html) intodocs.tar.gzand uploads it as an artifact.Step 2: CI generates rustdoc
.github/workflows/docs.yml:42-48— thegenerate-api-docsjob:This runs
cargo docon nightly, generating rustdoc HTML intotarget/doc/. The--enable-index-pageflag produces anindex.htmllisting all crates. The job tarstarget/doc/into
api-docs.tar.gzand uploads it.Step 3: Publish composites them — rustdoc overwrites the placeholder
.github/workflows/docs.yml:78-83— thepublishjob extracts both artifacts sequentially:Line 81 extracts VitePress into
/tmp/docs/. This puts our placeholder at/tmp/docs/api-docs/index.html. Line 83 then extracts rustdoc into/tmp/docs/api-docs/,overwriting
/tmp/docs/api-docs/index.htmlwith rustdoc's crate index page. The order matters:VitePress first, rustdoc second. The entire
/tmp/docs/directory is then published togh-pages(line 84-91).
Why the placeholder never appears in production
The rustdoc
index.html(a full standalone HTML page listing all Saluki crates) replaces theVitePress-generated
index.html(our placeholder) byte-for-byte on disk before publish. GitHubPages serves the rustdoc version. The placeholder only exists transiently during CI and locally
when running
make run-docs.Change Type
How did you test this PR?
Hard to know for sure if this will fix the issue in production, but I tried to replicate what happens in production locally like this (and it worked):
References
Closes #1276