Skip to content

Commit b7057c2

Browse files
charliekclaude
andcommitted
fix(cli): resolve doc anchors against zensical.toml, not the deleted mkdocs.yml
The Zensical migration (#353) removed mkdocs.yml, but roost-cli's doc_anchors_resolve test reads it to assert that every URL `roostctl doctor` prints resolves to a page that is published in the site nav and carries a heading matching the anchor. The test has panicked on the missing file ever since, leaving main red for any Rust-touching PR. Ports the nav parser from MkDocs' YAML list entries to Zensical's TOML array of one-key tables, and repoints both the test and its near-miss guard at zensical.toml. The reason CI never noticed is the more important half: rust-build is gated on the `rust` path filter, so a docs-only PR runs no Rust job and ci-success goes green over a broken `cargo test`. A new narrow `docnav` filter (zensical.toml + docs/**) now also triggers rust-build — only that job, since widening `rust` itself would drag four heavy jobs onto every docs typo. Same class as the .mise.toml dotfile miss already noted in that filter block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d
1 parent 3bda60f commit b7057c2

2 files changed

Lines changed: 39 additions & 23 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
ci: ${{ steps.filter.outputs.ci }}
2828
deb: ${{ steps.filter.outputs.deb }}
2929
macbundle: ${{ steps.filter.outputs.macbundle }}
30+
docnav: ${{ steps.filter.outputs.docnav }}
3031
steps:
3132
- uses: actions/checkout@v6
3233
with:
@@ -80,6 +81,16 @@ jobs:
8081
- 'mac/scripts/bundle-lib.sh'
8182
- 'mac/scripts/bundle-iced.sh'
8283
- 'mac/Resources/Info-iced.plist.template'
84+
# roost-cli's `doc_anchors_resolve` test reads the site nav and
85+
# every page it links, so those files are inputs to a RUST test
86+
# even though no Rust file changes when they move. Without this,
87+
# a docs-only PR can delete or rename what the test reads and
88+
# `rust-build` never runs to notice — which is exactly how the
89+
# MkDocs -> Zensical migration left main red while CI was green.
90+
# Same class as the `.mise.toml` dotfile miss noted above.
91+
docnav:
92+
- 'zensical.toml'
93+
- 'docs/**'
8394
ci:
8495
- '.github/workflows/ci.yml'
8596
# Deliberately NOT `linux`: that filter folds in the *rustcore
@@ -192,7 +203,10 @@ jobs:
192203

193204
rust-build:
194205
needs: changes
195-
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.fixtures == 'true' || needs.changes.outputs.ci == 'true'
206+
# `docnav` rides along here only — this is the job that runs `cargo
207+
# test`, and widening `rust` itself would drag four other heavy jobs
208+
# onto every docs typo.
209+
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.fixtures == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.docnav == 'true'
196210
strategy:
197211
fail-fast: false
198212
matrix:

crates/roost-cli/src/doctor.rs

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ macro_rules! docs_base {
6565
/// [`doc`], so the link doctor prints and the anchor the test verifies
6666
/// cannot drift apart.
6767
/// `page` + `anchor` are read only by `doc_anchors_resolve`, which
68-
/// resolves them against `docs/` and `mkdocs.yml`'s nav; `url` is what
68+
/// resolves them against `docs/` and `zensical.toml`'s nav; `url` is what
6969
/// production emits. Carrying all three on one row built by [`doc`] is
7070
/// what makes the printed link and the verified anchor impossible to
7171
/// drift apart — hence the struct-level allow.
@@ -6072,43 +6072,45 @@ mod tests {
60726072
out
60736073
}
60746074

6075-
/// Is `rel` an actual nav entry — `- Title: page.md` or `- page.md` —
6076-
/// rather than a substring of a comment or a longer path?
6075+
/// Is `rel` an actual nav entry — `{ "Title" = "page.md" }` — rather
6076+
/// than a substring of a comment or of a longer path? Zensical's nav
6077+
/// is a TOML array of one-key tables, so the entry is the quoted
6078+
/// value; matching the quotes is what keeps `cli.md` from passing on
6079+
/// `reference/cli.md`.
60776080
fn nav_lists(nav: &str, rel: &str) -> bool {
6081+
let quoted = format!("\"{rel}\"");
60786082
nav.lines().any(|line| {
60796083
let line = line.split('#').next().unwrap_or_default();
6080-
let Some(entry) = line.trim().strip_prefix("- ") else {
6081-
return false;
6082-
};
6083-
let value = entry.rsplit_once(": ").map_or(entry, |(_, v)| v);
6084-
value.trim() == rel
6084+
line.split('=')
6085+
.skip(1)
6086+
.any(|value| value.trim().trim_end_matches([',', '}', ']', ' ']) == quoted)
60856087
})
60866088
}
60876089

6088-
/// mkdocs' `nav:` block, bounded at the next top-level key — `extra:`
6089-
/// follows it today, and its entries are not nav entries.
6090-
fn nav_block(mkdocs: &str) -> String {
6091-
mkdocs
6092-
.split("\nnav:")
6090+
/// Zensical's `nav = [` array, bounded at the line that closes it.
6091+
/// `mkdocs.yml`'s `nav:` block was the pre-Zensical equivalent.
6092+
fn nav_block(zensical: &str) -> String {
6093+
zensical
6094+
.split("\nnav = [")
60936095
.nth(1)
6094-
.expect("mkdocs.yml has a nav:")
6096+
.expect("zensical.toml has a nav = [")
60956097
.lines()
6096-
.take_while(|l| l.trim().is_empty() || l.starts_with([' ', '\t']))
6098+
.take_while(|l| !l.starts_with(']'))
60976099
.collect::<Vec<_>>()
60986100
.join("\n")
60996101
}
61006102

61016103
/// Deliberately one-directional: for each `(page, anchor)` doctor can
6102-
/// emit, assert the page exists, that it is in `mkdocs.yml`'s
6104+
/// emit, assert the page exists, that it is in `zensical.toml`'s
61036105
/// hand-maintained nav (`docs/reference/terminal-queries.md` is the
61046106
/// counterexample on disk today), and that some heading slugifies to
6105-
/// the anchor. It does NOT reimplement mkdocs' slugify over every
6107+
/// the anchor. It does NOT reimplement the site generator's slugify over every
61066108
/// heading in the repo — only these URLs matter.
61076109
#[test]
61086110
fn doc_anchors_resolve() {
61096111
let root = repo_root();
6110-
let mkdocs = std::fs::read_to_string(root.join("mkdocs.yml")).expect("mkdocs.yml");
6111-
let nav = nav_block(&mkdocs);
6112+
let zensical = std::fs::read_to_string(root.join("zensical.toml")).expect("zensical.toml");
6113+
let nav = nav_block(&zensical);
61126114

61136115
let mut targets: Vec<Doc> = DOC_TARGETS.iter().map(|(_, d)| *d).collect();
61146116
targets.push(EXIT_CODES_DOC);
@@ -6125,7 +6127,7 @@ mod tests {
61256127
.unwrap_or_else(|e| panic!("{}: {e}", path.display()));
61266128
assert!(
61276129
nav_lists(&nav, &rel),
6128-
"{rel} is not in mkdocs.yml's nav, so its URL would not publish"
6130+
"{rel} is not in zensical.toml's nav, so its URL would not publish"
61296131
);
61306132
let found = headings(&body)
61316133
.into_iter()
@@ -6145,7 +6147,7 @@ mod tests {
61456147
let body = "# Real\n\n```bash\n# 1. Allow it as a login shell\n```\n\n## Also Real\n";
61466148
assert_eq!(headings(body), vec![" Real", " Also Real"]);
61476149

6148-
let nav = " - CLI: reference/cli.md\n # - Queries: reference/terminal-queries.md\n";
6150+
let nav = " { \"CLI\" = \"reference/cli.md\" },\n # { \"Queries\" = \"reference/terminal-queries.md\" },\n";
61496151
assert!(nav_lists(nav, "reference/cli.md"));
61506152
assert!(
61516153
!nav_lists(nav, "reference/terminal-queries.md"),
@@ -6156,7 +6158,7 @@ mod tests {
61566158
"a suffix of a nav path is not a nav entry"
61576159
);
61586160
assert!(!nav_lists(
6159-
&nav_block(&std::fs::read_to_string(repo_root().join("mkdocs.yml")).unwrap()),
6161+
&nav_block(&std::fs::read_to_string(repo_root().join("zensical.toml")).unwrap()),
61606162
"reference/terminal-queries.md"
61616163
));
61626164
}

0 commit comments

Comments
 (0)