From 7fe36692b44f53773f4e6373c4c3c7ae0a6a3f70 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Sat, 9 May 2026 01:45:07 -0400 Subject: [PATCH 1/4] feat(docs): add api-docs template for pypi (lazydocs) Mirrors the TypeScript and dotnet pipelines for Python. Iterates over packages declared in the PUBLIC_PACKAGES env (resq-mcp, resq-dsa today), installs each editable into a uv-backed venv plus lazydocs, then runs lazydocs against each package's import-name to produce a per-package Markdown tree. Reuses the shared Mintlify-safety post-processing (./ prefix on bare-filename links, MDX curly-brace escape outside code regions) and opens a PR via peter-evans/create-pull-request authored as resq-sw. Picked lazydocs over mkdocstrings because mkdocstrings expects a mkdocs site config; lazydocs is a single CLI invocation that takes a module name and emits Markdown directly. Keeps the template shape consistent with the other two languages. --- automation/source-repo-templates/README.md | 2 +- .../source-repo-templates/api-docs.python.yml | 266 ++++++++++++++++++ 2 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 automation/source-repo-templates/api-docs.python.yml diff --git a/automation/source-repo-templates/README.md b/automation/source-repo-templates/README.md index 055fe976..3186317a 100644 --- a/automation/source-repo-templates/README.md +++ b/automation/source-repo-templates/README.md @@ -65,7 +65,7 @@ it ships its own rustdoc pipeline today. | -------------------------- | ------ | ------------------------ | ---------------------------- | | `resq-software/npm` | TS | TypeDoc + markdown plug. | `api-docs.typescript.yml` | | `resq-software/dotnet-sdk` | C# | DefaultDocumentation | `api-docs.dotnet.yml` | -| `resq-software/pypi` | Python | mkdocstrings | _TODO_ | +| `resq-software/pypi` | Python | lazydocs | `api-docs.python.yml` | | `resq-software/programs` | Rust | rustdoc + cargo-readme | _TODO_ | | `resq-software/vcpkg` | C++ | Doxygen + moxygen | _TODO_ | | `resq-software/viz` | C#/web | DefaultDocumentation | _TODO_ | diff --git a/automation/source-repo-templates/api-docs.python.yml b/automation/source-repo-templates/api-docs.python.yml new file mode 100644 index 00000000..b692624b --- /dev/null +++ b/automation/source-repo-templates/api-docs.python.yml @@ -0,0 +1,266 @@ +# Copyright 2026 ResQ Software +# SPDX-License-Identifier: Apache-2.0 +# +# Source-of-truth template lives in resq-software/docs: +# automation/source-repo-templates/api-docs.python.yml +# +# Copy this file to resq-software/pypi at: +# .github/workflows/api-docs.yml +# +# Renders Markdown API reference for every Python package in the +# monorepo and opens a PR in resq-software/docs with generated +# content under sdks/python/api/. +# +# Pipeline: +# 1. uv pip install -e + lazydocs +# 2. lazydocs -> per-module Markdown files +# 3. Mintlify-safety post-processing (./ prefix on bare links; +# MDX curly-brace escape outside code regions) +# 4. peter-evans/create-pull-request opens a PR in the docs repo + +name: api-docs + +on: + push: + # pypi monorepo uses single-versioned bare semver tags (v1.3.4, + # v1.3.3, ...) covering both packages at once, so a single + # tag pattern is enough. + tags: + - 'v*' + workflow_dispatch: + inputs: + ref: + description: 'Source ref to document (defaults to current ref)' + required: false + type: string + +permissions: + contents: read + +concurrency: + # Each run force-pushes the same auto/python-api- branch in + # the docs repo. Cancel any earlier run still in flight so we do + # not race on that push. + group: api-docs-${{ inputs.ref || github.ref }} + cancel-in-progress: true + +jobs: + generate: + runs-on: ubuntu-latest + timeout-minutes: 15 + + env: + PYTHON_VERSION: '3.13' + OUTPUT_DIR: generated-docs + DOCS_TARGET: sdks/python/api + # Public packages to document. Format: :. + # The pyproject directory is the path under packages/; the + # import name is what gets passed to `lazydocs` (typically the + # underscore form of the distribution name). + PUBLIC_PACKAGES: >- + resq-mcp:resq_mcp + resq-dsa:resq_dsa + + steps: + - name: Resolve ref metadata + # Single source of truth for the ref this run documents. + # workflow_dispatch can pass an alternate ref via inputs.ref; + # fall back to github.ref_name (already stripped of refs/...). + # DOCS_REF_SLUG is branch-safe for use in PR/branch names. + run: | + raw='${{ inputs.ref || github.ref_name }}' + raw="${raw#refs/tags/}" + raw="${raw#refs/heads/}" + slug="${raw//\//-}" + echo "DOCS_REF_NAME=$raw" >> "$GITHUB_ENV" + echo "DOCS_REF_SLUG=$slug" >> "$GITHUB_ENV" + + - name: Checkout source repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref || github.ref }} + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 + + - name: Install packages + lazydocs into a venv + # lazydocs imports the packages it documents, so they must + # be installed in the active Python environment. uv pip + # install -e per package; the editable install resolves all + # transitive runtime deps the same way `pip install` would. + run: | + set -euo pipefail + uv venv .venv --python "$PYTHON_VERSION" + # shellcheck disable=SC1091 + source .venv/bin/activate + for entry in $PUBLIC_PACKAGES; do + pkg_dir="${entry%%:*}" + uv pip install -e "packages/$pkg_dir" + done + uv pip install lazydocs + + - name: Generate per-package Markdown + # lazydocs walks the import graph of each module and emits + # one .md per public submodule + an overview.md index. + # --src-base-url points at the source on GitHub for the ref + # we are documenting, so "View source" links resolve. + run: | + set -euo pipefail + # shellcheck disable=SC1091 + source .venv/bin/activate + rm -rf "$OUTPUT_DIR" + mkdir -p "$OUTPUT_DIR" + missing=0 + generated=0 + for entry in $PUBLIC_PACKAGES; do + pkg_dir="${entry%%:*}" + mod_name="${entry##*:}" + out="$OUTPUT_DIR/$pkg_dir" + mkdir -p "$out" + # Confirm the module is importable before invoking + # lazydocs so the failure mode is clear. + if ! python -c "import ${mod_name}" 2>/dev/null; then + echo "::error ::cannot import ${mod_name} (entry ${entry})" + missing=1 + continue + fi + lazydocs \ + --output-path "$out" \ + --src-base-url "https://github.com/${{ github.repository }}/blob/${DOCS_REF_NAME}/" \ + --no-watermark \ + --validate \ + "$mod_name" + generated=$((generated + 1)) + done + if [ "$missing" -ne 0 ] || [ "$generated" -eq 0 ]; then + echo "::error ::aborting to avoid syncing partial/empty docs output" + exit 1 + fi + + - name: Write top-level index + # lazydocs emits an overview.md per package. Stitch a small + # README.md at the top of the api/ folder linking to each + # so a single nav entry can drive into all packages. + run: | + { + echo "# ResQ Python SDK" + echo "" + echo "Auto-generated reference for the public packages in" + echo "[${{ github.repository }}](https://github.com/${{ github.repository }})." + echo "" + echo "## Packages" + echo "" + for entry in $PUBLIC_PACKAGES; do + pkg_dir="${entry%%:*}" + if [ -d "$OUTPUT_DIR/$pkg_dir" ]; then + echo "- [$pkg_dir]($pkg_dir/overview)" + fi + done + } > "$OUTPUT_DIR/README.md" + + - name: Prefix bare-filename intra-page links with ./ + # Mintlify rejects bare-filename .md links as broken; + # prefixing with ./ makes the resolver treat them as + # relative paths. Same pattern as the TypeScript template. + working-directory: ${{ env.OUTPUT_DIR }} + run: | + find . -type f -name '*.md' -print0 | while IFS= read -r -d '' f; do + sed -E -i \ + 's|\]\(([A-Za-z0-9][^)/]*\.md(#[^)]*)?)\)|](./\1)|g' \ + "$f" + done + + - name: Escape curly braces outside code regions (MDX safety) + # Mintlify parses .md as MDX. Any literal `{ ... }` in prose + # is interpreted as a JSX expression and trips the + # broken-links check. Awk tracks both triple-backtick fences + # and single-backtick inline spans, and rewrites curly + # braces to HTML entities only on prose outside any code + # region. + working-directory: ${{ env.OUTPUT_DIR }} + run: | + find . -type f -name '*.md' -print0 | while IFS= read -r -d '' f; do + awk ' + BEGIN { in_fence = 0 } + /^```/ { in_fence = !in_fence; print; next } + { + if (in_fence) { print; next } + out = "" + in_inline_code = 0 + n = length($0) + for (i = 1; i <= n; i++) { + c = substr($0, i, 1) + if (c == "`") { + in_inline_code = !in_inline_code + out = out c + } else if (!in_inline_code && c == "{") { + out = out "{" + } else if (!in_inline_code && c == "}") { + out = out "}" + } else { + out = out c + } + } + print out + } + ' "$f" > "$f.tmp" && mv "$f.tmp" "$f" + done + + - name: Build pages index + # Flat JSON array of generated Markdown paths (without + # extension) so the docs repo can later splice them into + # docs.json automatically. + working-directory: ${{ env.OUTPUT_DIR }} + run: | + find . -name '*.md' -type f \ + | sed 's|^\./||; s|\.md$||' \ + | sort > _pages.txt + jq -R -s 'split("\n") | map(select(length > 0))' _pages.txt > _pages.json + rm _pages.txt + + - name: Checkout docs repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: resq-software/docs + path: docs-checkout + token: ${{ secrets.DOCS_REPO_PR_TOKEN }} + persist-credentials: false + + - name: Sync generated Markdown into docs checkout + run: | + target="docs-checkout/${DOCS_TARGET}" + mkdir -p "$target" + rm -rf "${target:?}"/* + cp -R "${OUTPUT_DIR}/." "$target/" + + - name: Open PR in docs repo + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + with: + path: docs-checkout + token: ${{ secrets.DOCS_REPO_PR_TOKEN }} + author: 'resq-sw ' + committer: 'resq-sw ' + commit-message: | + docs(python): sync API reference for ${{ env.DOCS_REF_NAME }} + title: 'docs(python): API reference ${{ env.DOCS_REF_NAME }}' + body: | + Auto-generated by `${{ github.workflow }}` in + `${{ github.repository }}` for ref `${{ env.DOCS_REF_NAME }}` + (run: ${{ github.run_id }}). + + Regenerated files under `sdks/python/api/`. Review the + diff for unintended exports and merge to publish. + branch: auto/python-api-${{ env.DOCS_REF_SLUG }} + base: main + delete-branch: true + add-paths: sdks/python/api/** + labels: | + automated + docs:api-ref + language:python From a52bb9fef637cefbe667263b0ee095c29936a1c4 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Sat, 9 May 2026 09:13:38 -0400 Subject: [PATCH 2/4] chore(template): address pr#18 review feedback * lazydocs --src-base-url: prefix the URL with packages/${pkg_dir}/. When the package is installed editable, lazydocs records source paths relative to the package's pyproject root, so generated GitHub source links land on the wrong directory unless the URL base names that subpath. Without this, every "View source" anchor 404s. * Curly-brace escape awk: rewrite the inline-code detector to count backtick run length and scan for a matching-length closer rather than toggling on every backtick. Single-backtick spans and multi-backtick spans (e.g. ``code with ` inside``) now preserve braces correctly. --- .../source-repo-templates/api-docs.python.yml | 68 ++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/automation/source-repo-templates/api-docs.python.yml b/automation/source-repo-templates/api-docs.python.yml index b692624b..2d46d464 100644 --- a/automation/source-repo-templates/api-docs.python.yml +++ b/automation/source-repo-templates/api-docs.python.yml @@ -130,9 +130,16 @@ jobs: missing=1 continue fi + # --src-base-url must point at the package root, not the + # repo root: lazydocs (run against an editable install) + # records source paths relative to the package's pyproject + # location, so prepending packages// produces + # correct GitHub source links. Without this prefix the + # "View source" anchors land on the repo root + a relative + # path that does not exist (404). lazydocs \ --output-path "$out" \ - --src-base-url "https://github.com/${{ github.repository }}/blob/${DOCS_REF_NAME}/" \ + --src-base-url "https://github.com/${{ github.repository }}/blob/${DOCS_REF_NAME}/packages/${pkg_dir}/" \ --no-watermark \ --validate \ "$mod_name" @@ -179,10 +186,18 @@ jobs: - name: Escape curly braces outside code regions (MDX safety) # Mintlify parses .md as MDX. Any literal `{ ... }` in prose # is interpreted as a JSX expression and trips the - # broken-links check. Awk tracks both triple-backtick fences - # and single-backtick inline spans, and rewrites curly - # braces to HTML entities only on prose outside any code - # region. + # broken-links check. + # + # CommonMark inline code: a span opens with N consecutive + # backticks and closes with the same number. The earlier + # implementation toggled state on every single backtick, + # which mishandled spans like ``code with ` inside`` (used + # to embed a literal backtick) — the inner backtick flipped + # state mid-span and the closing pair landed wrong. The + # walker below counts the opening run, scans ahead for a + # matching-length closing run, and copies the entire span + # verbatim. Triple-backtick fences are still detected at + # line level above the per-character loop. working-directory: ${{ env.OUTPUT_DIR }} run: | find . -type f -name '*.md' -print0 | while IFS= read -r -d '' f; do @@ -191,20 +206,41 @@ jobs: /^```/ { in_fence = !in_fence; print; next } { if (in_fence) { print; next } + s = $0 out = "" - in_inline_code = 0 - n = length($0) - for (i = 1; i <= n; i++) { - c = substr($0, i, 1) + n = length(s) + i = 1 + while (i <= n) { + c = substr(s, i, 1) if (c == "`") { - in_inline_code = !in_inline_code - out = out c - } else if (!in_inline_code && c == "{") { - out = out "{" - } else if (!in_inline_code && c == "}") { - out = out "}" + runlen = 0 + while (i + runlen <= n && substr(s, i + runlen, 1) == "`") runlen++ + j = i + runlen + close_at = 0 + while (j <= n) { + if (substr(s, j, 1) == "`") { + k = 0 + while (j + k <= n && substr(s, j + k, 1) == "`") k++ + if (k == runlen) { close_at = j; break } + j += k + } else { + j++ + } + } + if (close_at > 0) { + end = close_at + runlen - 1 + out = out substr(s, i, end - i + 1) + i = end + 1 + } else { + out = out substr(s, i, runlen) + i += runlen + } + } else if (c == "{") { + out = out "{"; i++ + } else if (c == "}") { + out = out "}"; i++ } else { - out = out c + out = out c; i++ } } print out From 15b4ab0aaeda1ea4334a0465d1e9e4b4118473c7 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Sat, 9 May 2026 09:25:13 -0400 Subject: [PATCH 3/4] fix(template): switch lazydocs -> pydoc-markdown + active-voice README Two issues uncovered while resolving review on docs/PR#19: 1. lazydocs is broken on Python 3.12+. It uses pkgutil's deprecated find_module() API which was removed in Python 3.12; on the runner's Python 3.13, every submodule walk fails with: AttributeError: 'FileFinder' object has no attribute 'find_module' The first run produced only README.md and an empty _pages.json because every per-module emit failed silently and the script only fell over once both packages had zero output. Switch to pydoc-markdown, which is on the find_spec() API and works on 3.13. 2. README intro previously read as a fragment in third person. Per docs#19 review (active voice + code formatting), rewrite the generator to emit "You can use this auto-generated..." and wrap the repo / package paths in backticks. The docs PR will refresh in place once pypi#31 (pending) merges and the regenerator runs. --- .../source-repo-templates/api-docs.python.yml | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/automation/source-repo-templates/api-docs.python.yml b/automation/source-repo-templates/api-docs.python.yml index 2d46d464..33395381 100644 --- a/automation/source-repo-templates/api-docs.python.yml +++ b/automation/source-repo-templates/api-docs.python.yml @@ -103,13 +103,22 @@ jobs: pkg_dir="${entry%%:*}" uv pip install -e "packages/$pkg_dir" done - uv pip install lazydocs + # pydoc-markdown rather than lazydocs: lazydocs uses + # pkgutil's deprecated find_module(), which Python 3.12 + # removed. Every submodule walk fails with + # AttributeError: 'FileFinder' object has no attribute 'find_module' + # on the runner's Python 3.13. pydoc-markdown is on the + # find_spec() API and produces Markdown directly. + uv pip install "pydoc-markdown>=4" - name: Generate per-package Markdown - # lazydocs walks the import graph of each module and emits - # one .md per public submodule + an overview.md index. - # --src-base-url points at the source on GitHub for the ref - # we are documenting, so "View source" links resolve. + # pydoc-markdown produces a single combined Markdown stream + # per invocation. We render one file per top-level package + # at /overview.md so the docs site has a stable + # entry point that does not change as submodules are added + # or removed. The MarkdownRenderer with `render_module_header` + # and `descriptive_class_title` produces a layout similar to + # what lazydocs offered before its Python 3.13 incompat. run: | set -euo pipefail # shellcheck disable=SC1091 @@ -123,26 +132,24 @@ jobs: mod_name="${entry##*:}" out="$OUTPUT_DIR/$pkg_dir" mkdir -p "$out" - # Confirm the module is importable before invoking - # lazydocs so the failure mode is clear. + # Confirm the module is importable before invoking the + # renderer so the failure mode is clear. if ! python -c "import ${mod_name}" 2>/dev/null; then echo "::error ::cannot import ${mod_name} (entry ${entry})" missing=1 continue fi - # --src-base-url must point at the package root, not the - # repo root: lazydocs (run against an editable install) - # records source paths relative to the package's pyproject - # location, so prepending packages// produces - # correct GitHub source links. Without this prefix the - # "View source" anchors land on the repo root + a relative - # path that does not exist (404). - lazydocs \ - --output-path "$out" \ - --src-base-url "https://github.com/${{ github.repository }}/blob/${DOCS_REF_NAME}/packages/${pkg_dir}/" \ - --no-watermark \ - --validate \ - "$mod_name" + # `pydoc-markdown -p ` walks the package's import + # graph and emits one combined Markdown document to + # stdout. We capture it as overview.md per package. + pydoc-markdown -p "$mod_name" --render-toc > "$out/overview.md" + # Sanity check: empty output means the package has no + # docstrings or pydoc-markdown choked silently. + if [ ! -s "$out/overview.md" ]; then + echo "::error ::pydoc-markdown produced empty output for ${mod_name}" + missing=1 + continue + fi generated=$((generated + 1)) done if [ "$missing" -ne 0 ] || [ "$generated" -eq 0 ]; then @@ -151,22 +158,22 @@ jobs: fi - name: Write top-level index - # lazydocs emits an overview.md per package. Stitch a small - # README.md at the top of the api/ folder linking to each - # so a single nav entry can drive into all packages. + # pydoc-markdown emits an overview.md per package. Stitch a + # small README.md at the top of the api/ folder linking to + # each so a single nav entry can drive into all packages. run: | { echo "# ResQ Python SDK" echo "" - echo "Auto-generated reference for the public packages in" - echo "[${{ github.repository }}](https://github.com/${{ github.repository }})." + echo "You can use this auto-generated reference for the public packages in" + echo "[\`${{ github.repository }}\`](https://github.com/${{ github.repository }})." echo "" echo "## Packages" echo "" for entry in $PUBLIC_PACKAGES; do pkg_dir="${entry%%:*}" if [ -d "$OUTPUT_DIR/$pkg_dir" ]; then - echo "- [$pkg_dir]($pkg_dir/overview)" + echo "- [\`$pkg_dir\`]($pkg_dir/overview)" fi done } > "$OUTPUT_DIR/README.md" From a5b2afb98bd93d6de131b6b7e0147c9eaf69d7f4 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Sat, 9 May 2026 09:35:16 -0400 Subject: [PATCH 4/4] fix(template): walk submodules + alphabetize README package list Two issues raised on docs/PR#20: 1. `pydoc-markdown -p ` only documents the top-level module. The package's submodules (resq_mcp.core, resq_mcp.drone, etc.) were not walked, so both overview.md files contained nothing beyond `# Table of Contents`. Discover submodules via pkgutil in a small inline Python script, then pass every name to pydoc-markdown via a YAML config's `loaders[].modules` list (the Python-import form, which works with editable installs). Bump the renderer to enable `render_module_header`, `descriptive_class_title`, and `add_method_class_prefix` so the layout is closer to what lazydocs produced before its 3.13 incompatibility. Also tighten the empty-output sanity check from `! -s` to a line-count threshold; a 3-line file containing only a TOC header is still technically non-empty. 2. README package list now sorts the package directories alphabetically before emitting the bullets. Keeps the README in sync with `_pages.json` (which `find ... | sort` already produces) regardless of PUBLIC_PACKAGES env order. --- .../source-repo-templates/api-docs.python.yml | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/automation/source-repo-templates/api-docs.python.yml b/automation/source-repo-templates/api-docs.python.yml index 33395381..04d79c08 100644 --- a/automation/source-repo-templates/api-docs.python.yml +++ b/automation/source-repo-templates/api-docs.python.yml @@ -112,13 +112,14 @@ jobs: uv pip install "pydoc-markdown>=4" - name: Generate per-package Markdown - # pydoc-markdown produces a single combined Markdown stream - # per invocation. We render one file per top-level package - # at /overview.md so the docs site has a stable - # entry point that does not change as submodules are added - # or removed. The MarkdownRenderer with `render_module_header` - # and `descriptive_class_title` produces a layout similar to - # what lazydocs offered before its Python 3.13 incompat. + # `pydoc-markdown -p ` only documents the top-level + # module — it does NOT recurse into submodules, which left + # both overview.md files containing nothing but + # `# Table of Contents`. Walk the import graph ourselves + # via pkgutil and pass every submodule explicitly via an + # inline YAML config; this also lets us bump the renderer + # to enable module headers / class signatures so the output + # matches what lazydocs used to produce. run: | set -euo pipefail # shellcheck disable=SC1091 @@ -132,21 +133,56 @@ jobs: mod_name="${entry##*:}" out="$OUTPUT_DIR/$pkg_dir" mkdir -p "$out" - # Confirm the module is importable before invoking the - # renderer so the failure mode is clear. if ! python -c "import ${mod_name}" 2>/dev/null; then echo "::error ::cannot import ${mod_name} (entry ${entry})" missing=1 continue fi - # `pydoc-markdown -p ` walks the package's import - # graph and emits one combined Markdown document to - # stdout. We capture it as overview.md per package. - pydoc-markdown -p "$mod_name" --render-toc > "$out/overview.md" - # Sanity check: empty output means the package has no - # docstrings or pydoc-markdown choked silently. - if [ ! -s "$out/overview.md" ]; then - echo "::error ::pydoc-markdown produced empty output for ${mod_name}" + # Discover submodules via Python's import system, then + # build a YAML config that names every one explicitly. + # The python loader's `modules:` form imports each name + # rather than scanning the filesystem, so editable + # installs work correctly. + modules=$(python - "$mod_name" <<'PY' + import importlib, pkgutil, sys + name = sys.argv[1] + mod = importlib.import_module(name) + out = [name] + if hasattr(mod, "__path__"): + for info in pkgutil.walk_packages(mod.__path__, name + "."): + if "._" in info.name: + continue + out.append(info.name) + print("\n".join(out)) + PY + ) + cfg=$(mktemp --suffix=.yml) + { + echo "loaders:" + echo " - type: python" + echo " modules:" + echo "$modules" | while IFS= read -r m; do + [ -n "$m" ] && echo " - ${m}" + done + echo "processors:" + echo " - type: filter" + echo " expression: not name.startswith('_') or name == '__init__'" + echo " - type: smart" + echo " - type: crossref" + echo "renderer:" + echo " type: markdown" + echo " render_toc: true" + echo " render_module_header: true" + echo " descriptive_class_title: true" + echo " add_method_class_prefix: true" + } > "$cfg" + pydoc-markdown "$cfg" > "$out/overview.md" + rm -f "$cfg" + # Empty / TOC-only output means the renderer found no + # documentable members. Fail fast rather than syncing. + lines=$(wc -l < "$out/overview.md") + if [ "$lines" -lt 5 ]; then + echo "::error ::pydoc-markdown produced near-empty output for ${mod_name} (${lines} lines)" missing=1 continue fi @@ -161,6 +197,9 @@ jobs: # pydoc-markdown emits an overview.md per package. Stitch a # small README.md at the top of the api/ folder linking to # each so a single nav entry can drive into all packages. + # Package list is sorted alphabetically by directory name so + # it stays in sync with `_pages.json` (which `find ... | sort` + # produces below) regardless of PUBLIC_PACKAGES env order. run: | { echo "# ResQ Python SDK" @@ -171,7 +210,8 @@ jobs: echo "## Packages" echo "" for entry in $PUBLIC_PACKAGES; do - pkg_dir="${entry%%:*}" + echo "${entry%%:*}" + done | sort | while read -r pkg_dir; do if [ -d "$OUTPUT_DIR/$pkg_dir" ]; then echo "- [\`$pkg_dir\`]($pkg_dir/overview)" fi