Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions automation/source-repo-templates/api-docs.rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,17 @@ jobs:
def parse_crate_meta(cargo_toml: pathlib.Path) -> dict:
text = cargo_toml.read_text(encoding="utf-8")
pkg = section(text, "package")
lib = section(text, "lib")
pkg_name = field_in(pkg, "name")
# Cargo's `[lib].name` overrides the default snake_case
# of the package name (e.g. resq-bin's lib is named
# `bin_explorer`). Use the explicit lib name when set;
# otherwise convert the package name `-` → `_` per
# Cargo's default.
lib_name = field_in(lib, "name") or pkg_name.replace("-", "_")
return {
"name": field_in(pkg, "name"),
"name": pkg_name,
"lib_name": lib_name,
"version": field_in(pkg, "version") or ws_version,
"description": field_in(pkg, "description"),
# Cargo allows `license.workspace = true` for
Expand Down Expand Up @@ -216,8 +225,7 @@ jobs:
if not meta["name"]:
continue

snake_name = meta["name"].replace("-", "_")
rustdoc_src = rustdoc_md_dir / snake_name
rustdoc_src = rustdoc_md_dir / meta["lib_name"]
if rustdoc_src.is_dir():
# Rich rustdoc output exists. Copy the whole tree
# into OUTPUT_DIR/<crate>/ and inject the version
Expand Down Expand Up @@ -288,6 +296,45 @@ jobs:
raise SystemExit("no crates found; aborting empty doc PR")
PY

- name: Prefix bare relative .md links with ./
# cargo-doc-md emits cross-references as bare relative paths
# (`config.md`, `commands/audit.md`). Mintlify only resolves
# explicitly-relative paths (`./`, `../`) and absolute paths
# (`/...`) — bare paths are flagged as broken.
#
# Same logic as the TypeScript template's prefix step: skip
# targets that are already qualified, prefix everything else
# with `./`.
working-directory: ${{ env.OUTPUT_DIR }}
run: |
python3 - <<'PY'
import pathlib
import re

link_re = re.compile(r'(?<!\!)\[([^\]]*)\]\(([^)]+)\)')

def is_qualified(target: str) -> bool:
t = target.strip().split(None, 1)[0]
if t.startswith(("./", "../", "/", "#")):
return True
if ":" in t.split("/", 1)[0]:
return True
return False

for f in pathlib.Path(".").rglob("*.md"):
text = f.read_text(encoding="utf-8")
def fix(m: re.Match) -> str:
label, target = m.group(1), m.group(2)
if is_qualified(target):
return m.group(0)
if not target.endswith(".md") and ".md#" not in target:
return m.group(0)
return f"[{label}](./{target})"
new = link_re.sub(fix, text)
if new != text:
f.write_text(new, encoding="utf-8")
Comment on lines +314 to +335
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The link prefixing logic has a few issues that could lead to incorrect documentation or script failures:

  1. Greedy Regex: The regex ([^)]+) for the link target is greedy. On lines with multiple links (e.g., [a](b.md) and [c](d.md)), it will match from the first [ to the last ), causing the rewriter to fail or produce corrupted output. Using a non-greedy quantifier +? is necessary.
  2. Potential IndexError: In is_qualified, target.strip().split(None, 1)[0] will raise an IndexError if the link target is empty or contains only whitespace (e.g., []()). While rare in generated docs, the script should handle this gracefully.
  3. Extension Check Robustness: The fix function checks target.endswith(".md"). If a link includes a title (e.g., [label](file.md "title")) or trailing whitespace, this check will fail, and the link won't be prefixed with ./, leaving it broken in Mintlify.
  4. Automated Discovery: The script uses rglob("*.md") to discover files. Per repository guidelines, prefer explicit project lists over automated discovery in CI workflows if the repository structure contains duplicate project configurations (such as shadow copies) or if manual oversight of the public API surface is necessary to distinguish between public and internal components.

Note that these same patterns appear to be present in the TypeScript template as well.

          link_re = re.compile(r'(?<!\!)\[([^\]]*)\]\(([^)]+?)\)')

          def is_qualified(target: str) -> bool:
              parts = target.strip().split(None, 1)
              if not parts:
                  return True
              t = parts[0]
              if t.startswith(("./", "../", "/", "#")):
                  return True
              if ":" in t.split("/", 1)[0]:
                  return True
              return False

          for f in pathlib.Path(".").rglob("*.md"):
              text = f.read_text(encoding="utf-8")
              def fix(m: re.Match) -> str:
                  label, target = m.group(1), m.group(2)
                  if is_qualified(target):
                      return m.group(0)
                  url_part = target.strip().split(None, 1)[0]
                  if not url_part.endswith(".md") and ".md#" not in url_part:
                      return m.group(0)
                  return f"[{label}](./{target})"
              new = link_re.sub(fix, text)
              if new != text:
                  f.write_text(new, encoding="utf-8")
References
  1. Prefer explicit project lists over automated discovery (e.g., via grep) in CI workflows if the repository structure contains duplicate project configurations (such as shadow copies) or if manual oversight of the public API surface is necessary to distinguish between public and internal components.

PY

- name: Escape MDX-special characters outside code regions
# Mintlify parses .md as MDX. Literal `{ ... }` in prose is
# interpreted as a JSX expression; literal `<X>` (e.g.
Expand Down
Loading