[ci][doc] Fail when a BUILD exclude, CODEOWNERS entry, or rule names a missing doc path - #66462
Draft
dstrodtman wants to merge 3 commits into
Draft
dstrodtman wants to merge 3 commits into
dstrodtman wants to merge 3 commits into
Conversation
…er exist A glob exclude naming a missing file is silently a no-op, so these entries stopped applying when their files moved: - doctest[serve] excluded deploy-many-models/multi-app.md and production-guide/deploy-vm.md. They now live at serve/multi-app.md and serve/advanced-guides/deploy-vm.md, inside the target's glob again. Neither has testcode or a >>> prompt, so nothing runs from them either way. - The serve doc_code test excluded doc_code/llm/llm_yaml_config_example.py and doc_code/llm/qwen_example.py, which moved to source/llm/doc_code/serve/qwen/, outside that target's include. No change to what any target runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Douglas Strodtman <douglas@anyscale.com>
…heir files Both entries name files that have moved, so each has silently stopped applying: - CODEOWNERS names doc/source/data/working-with-llms.rst, which #66375 converted to working-with-llms.md. ray-project/ray-llm hasn't been requested on the page since. - test.rules.txt routes doc/test_llms_txt.py to `doc` alone, but the test moved to doc/source/_ext/ in #65461. There it matches the _ext/ directory rule and also emits doc_api, which runs both API-consistency checks for a hermetic Sphinx test that imports no Ray code. It now gets its own rule ahead of the directory entry, the same way api_sidebar.py does, and the test.rules.test.txt case follows it to the new path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Douglas Strodtman <douglas@anyscale.com>
…a missing doc path Three config surfaces name doc files by literal path and ignore a path that no longer exists instead of erroring: glob `exclude` lists in the BUILD files under doc/, .github/CODEOWNERS, and the .buildkite rules files. A page rename, move, or .rst-to-.md conversion changes the path without touching them, so the exclusion, ownership, or routing quietly stops applying. Four dead doc/BUILD.bazel excludes and the two entries fixed in the previous commit accumulated that way. doc/test_literal_paths.py checks every wildcard-free entry under doc/ in those three surfaces and fails on any that doesn't exist, with an ALLOWLIST for paths written ahead of their files. It reads BUILD files with ast, needs no Bazel or Ray install, and checks the tree rather than a diff. It runs in the `always` lint matrix next to doc_no_new_rst, because a prose-only rename can fail it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Douglas Strodtman <douglas@anyscale.com>
dstrodtman
added a commit
that referenced
this pull request
Sep 28, 2026
…ervability pages to MyST (#66461) ## Why are these changes needed? Converts the last non-API-reference RST pages under `ray-core/`, `data/`, and `rllib/` to MyST Markdown, plus the `train/` and `ray-observability/` pages that `doc/BUILD.bazel` names by path. The API reference trees (`*/api/`, `rllib/package_ref/`, and `ray-core/compiled-graph/compiled-graph-api`) stay RST for now. Each page gets its own commit, so you can review one file at a time. Every commit renames the page's literal `.rst` paths in `doc/BUILD.bazel` to `.md`. These paths are in `exclude` lists and in the explicit `files` lists of `doctest[data-gpu]` and `doctest[train-gpu]`, and they don't glob. Without the edit, an excluded page would silently join its target once it's `.md`, and an included page would silently drop out of it. Conversions are format-only: labels, headings, and prose are unchanged. A final whitespace-only commit soft-wraps the prose. `ray-soft-wrap`'s `verify.py` confirms non-whitespace bytes, rendered HTML, and idempotence for all 11 files. ### `rllib/getting-started`: doctest target removed The first commit deletes `doctest[rllib2]`, the only target over `rllib/getting-started.rst`, before the page converts. The target is tagged `manual` with a hang TODO, so no premerge or postmerge step has run it, and nothing else references it (`git grep rllib2` is empty). Its 15 `testcode` blocks had no `testoutput`, so they asserted nothing. The 12 visible blocks become display-only `python` code blocks. The three `:hide:` blocks held `.stop()` cleanups that never rendered, so they're deleted. `doctest[rllib]` still excludes the page. If these examples should be validated later, a `doc_code/` script is the better route than reviving the hanging target. cc @elliot-barn, who owns the TODO. This commit isn't buildable on its own. It turns a `.. testcode::` that had no blank line before its body into a `.. code-block:: python`, still with no blank line, and docutils reads the body as extra arguments to the directive. The later conversion commit fixes it in the `.md`, and a squash merge never puts the intermediate commit on master. ### Doctest coverage Docs-only PRs don't run doctests premerge. This PR edits `doc/BUILD.bazel`, so every library's docs example step runs, but premerge can't show the conversion kept the same blocks. For that, I parsed each `.md` and the `.rst` it replaced with Ray's `pytest-sphinx` fork, the same parser the doctest macro runs, and asserted identical sections (directive, body, `:options:`, `:skipif:`) and examples. | Page | BUILD role | Examples before → after | | --- | --- | --- | | `ray-core/handling-dependencies` | `doctest[core]` exclude | 17 → 17 | | `ray-core/tasks/nested-tasks` | `doctest[core]` exclude | 0 → 0 (`literalinclude` of a tested `doc_code/` file) | | `data/batch-inference` | `doctest_each` exclude, `doctest[data-gpu]` files | 7 → 7 | | `data/transforming-data` | `doctest_each` exclude, `doctest[data-gpu]` files | 24 → 24 | | `data/contributing/contributing` | `doctest_each` glob | 0 → 0 (toctree only) | | `rllib/getting-started` | `doctest[rllib]` exclude, `doctest[rllib2]` removed | 15 → 0, by the removal commit | | `ray-observability/user-guides/cli-sdk` | top-level exclude | 16 → 16 | | `ray-observability/user-guides/ray-tracing` | top-level exclude | 3 → 3 | | `train/horovod` | `doctest[train]` exclude | 2 → 2 | | `train/user-guides/data-loading-preprocessing` | `doctest[train]` exclude, `doctest[train-gpu]` files | 6 → 6 | | `train/user-guides/using-accelerators` | `doctest[train]` exclude, `doctest[train-gpu]` files | 10 → 10 | To show the check would catch a broken conversion, I mutated each page with examples three ways: dropping a `testcode` block, altering a line in one, and turning one into a four-backtick fence. The last renders fine but is never collected. The check failed on all 24 mutations. ### Rendering changes I ran `render_diff.py` on all 11 pages against `/en/master`. Six render identically: `nested-tasks`, `data/contributing/contributing`, `cli-sdk`, `ray-tracing`, `horovod`, and `using-accelerators`. Every difference on the other five is listed here. - `rllib/getting-started`: on master, one `.. testcode::` line had no blank line after it. docutils read the block's first six lines, the `best_result = results.get_best_result(...)` call, as the directive's argument and dropped them. So master shows code that uses `best_result` without showing where it comes from. The converted page renders all six lines. This is the one place the conversion deliberately doesn't reproduce master. - `rllib/getting-started`: the "Farama gymnasium" link now has `https://`. On master it's a relative link to `gymnasium.farama.org` that 404s. - `ray-core/handling-dependencies`: MyST drops the apostrophe when it slugifies the "can't import the packages" heading, so the section id changes. That heading had no label, so the page adds a target carrying the old docutils slug, and existing `#...-can-t-import-...` links still resolve. An auto-numbered span id, which nothing links to, also changes from `id4` to `id2`. - `data/batch-inference` and `train/user-guides/data-loading-preprocessing`: each renders an RST comment as an HTML comment, which readers never see. - `data/transforming-data`: the "When using" definition list's `<dl>` gains the unstyled `myst` class. The first preview diff also caught three regressions that the green build didn't. MyST's `replacements` extension turned two `(c)` list markers into `©` and an `etc..` into `etc…`. The anchor above had changed too. A follow-up commit fixes all three. A sweep of the 11 pages found no other text that extension rewrites. ### Dead `doc/BUILD.bazel` excludes A final commit drops four `exclude` entries that name files that have moved: two in `doctest[serve]` and two in the serve `doc_code` test. A glob exclude naming a missing file excludes nothing, so these had silently stopped applying. This doesn't change what any target runs. #66462 carries the same commit, along with a lint check that fails on this class of stale path, so the two PRs merge cleanly in either order. ### Open PRs that edit these files Each of these open PRs edits one of the converted `.rst` files, so each will need to move its change to the `.md`: #66279, #66191, #65956, #65933, and #65830 (`handling-dependencies`), #65693 (`cli-sdk`), and #66419, #66420, #66422, and #66423 (`data/batch-inference`, `train/user-guides/data-loading-preprocessing`). #66096, #64053, and #63881 also edit `doc/BUILD.bazel`, and only #66096 edits nearby lines. ## Related issue number None. No open PR converts these pages. I checked `gh pr list` for open PRs touching each file. ## Checks - [x] I've signed off every commit (`git commit -s`). - [x] Pre-commit hooks passed on every commit. - Doctest parity: `python3 doctest_parity.py <the 11 .md files>` with `pytest==7.4.4` and `pytest-sphinx @ git+https://github.com/ray-project/pytest-sphinx`. All OK, run again after the soft-wrap commit. - Static checks on every `.md`: fences balance, no leftover RST roles or directives, every label from the `.rst` is present, and directive counts match. - Read the Docs preview builds green under `fail_on_warning`, and the render diff against `/en/master` is in Rendering changes. The first build hit the known autosummary-import segfault and passed on rebuild. - Local doctest run: not done. The GPU targets need hardware, and the CPU targets run in this PR's premerge because of the `BUILD.bazel` change. AI assistance (Claude Code) was used for the conversion and the verification scripts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Douglas Strodtman <douglas@anyscale.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why are these changes needed?
Three config surfaces name doc files by literal path. In each of them, a path that no longer exists is silently ignored instead of raising an error:
excludein a BUILD file underdoc/. A missing path excludes nothing, so a moved page silently rejoins the doctest or example target it was excluded from..github/CODEOWNERS. A missing path matches no file, so the owning team stops being requested on the moved page..buildkite/*.rules.txt. A missing path matches no changed file, so the moved file falls through to a later, usually broader, rule.A page rename, a directory move, or an
.rst-to-.mdconversion changes a path without touching any of these. The docs are in the middle of a full RST-to-MyST conversion, with a round of page and directory renames coming next, so this failure mode is about to get much more common. It has already happened six times on master:doctest[serve]excludesdeploy-many-models/multi-app.mdandproduction-guide/deploy-vm.mdtestcodeor a>>>prompt today, so nothing runs from them.doc_codetest excludesdoc_code/llm/llm_yaml_config_example.pyanddoc_code/llm/qwen_example.pysource/llm/doc_code/serve/qwen/, outside the target's include, so the entries do nothing.doc/source/data/working-with-llms.rst.md, so@ray-project/ray-llmhasn't been requested on it since.test.rules.txtroutesdoc/test_llms_txt.pytodocdoc/source/_ext/in #65461. It now matches the_ext/directory rule and also emitsdoc_api, which runs both API-consistency checks for a hermetic Sphinx test that imports no Ray code.This PR fixes all six and adds a check that makes the next one fail CI.
The check
doc/test_literal_paths.pycollects every wildcard-free entry underdoc/from those three surfaces and fails on any path that doesn't exist. It currently checks 129 entries. Details:ast, not regexes, so inline comments and nestedglob()calls don't confuse it.excludelists. A missing path insrcs,main, orfilesalready fails the Bazel build.ALLOWLIST. It's empty today, because the one pre-routed pair intest.rules.txt([doc] Add in-repo llms.txt generator, replacing sphinx-llms-txt #64458) has since landed.It runs in the
alwayslint matrix, next todoc_no_new_rst. That matrix is where checks a prose-only diff can fail belong, and a rename PR is prose-only, so it triggers no test steps. The script routes todocintest.rules.txtalongsidetest_no_new_rst.py.It's scoped to paths under
doc/. Outsidedoc/,test.rules.txthas 14 entries that name missing CI files (.buildkite/pipeline.*.yml,ci/docker/*.wanda.yaml,examples/, and others), and CODEOWNERS has one for/python/ray/_private/usage/. Those belong to the CI owners, and some may be deliberate, so this PR leaves them alone.Commits
doc/BUILD.bazelexcludes. This doesn't change what any target runs. The same commit is in [doc] Convert the doctest-wired Ray Core, Data, RLlib, Train, and observability pages to MyST #66461, so the two PRs merge cleanly in either order.working-with-llms.md. Givedoc/source/_ext/test_llms_txt.pyits owndocrule ahead of the_ext/directory entry, the same wayapi_sidebar.pyhas one, and move itstest.rules.test.txtcase to the new path.Related issue number
None. No open PR adds a check like this (
gh pr list --search "CODEOWNERS stale path"and"BUILD exclude").Checks
git commit -s).python doc/test_literal_paths.pypasses on this branch. It failed on master with exactly the six entries above.doctest[serve]excludes, converting adoctest[data-gpu]page from.rstto.md, moving the CODEOWNERS directorydoc/source/serve/llm/, and moving the rules-routeddoc/rtd_doctor.py.rayci test-rules, built fromray-project/rayciatc890d24, passes. With only thetest.rules.test.txtcase moved to the new path, it failed with+doc_api (unexpected), which confirms the rule change is what fixes it../ci/lint/lint.sh doc_literal_pathspasses.AI assistance (Claude Code) was used to write the check and find the stale entries.
🤖 Generated with Claude Code