diff --git a/.github/mkdocs.yml b/.github/mkdocs.yml index 5798728..8745066 100644 --- a/.github/mkdocs.yml +++ b/.github/mkdocs.yml @@ -50,58 +50,66 @@ plugins: background: light-dark(white, black) shadow: false +# Auto-generated by scripts/generate-docs.py — do not edit manually. +# Kept in version control so `mkdocs serve` works without running the generator. nav: - Home: index.md - Workflows: - Overview: workflows/index.md - iOS: - workflows/ios/index.md - - Test: workflows/ios/selfhosted-test.md + - Build (Deprecated): workflows/ios/selfhosted-build.md - Nightly Build: workflows/ios/selfhosted-nightly-build.md - - On-demand Build: workflows/ios/selfhosted-on-demand-build.md + - On-Demand Build: workflows/ios/selfhosted-on-demand-build.md - Release: workflows/ios/selfhosted-release.md - - Build (Deprecated): workflows/ios/selfhosted-build.md + - Test: workflows/ios/selfhosted-test.md - iOS + KMP: - workflows/ios-kmp/index.md - - Test: workflows/ios-kmp/selfhosted-test.md - Build: workflows/ios-kmp/selfhosted-build.md - Release: workflows/ios-kmp/selfhosted-release.md + - Test: workflows/ios-kmp/selfhosted-test.md - Android: - workflows/android/index.md - - PR Check: workflows/android/cloud-check.md - - Nightly Build: workflows/android/cloud-nightly-build.md - - Release (Firebase): workflows/android/cloud-release-firebase.md - - Release (Google Play): workflows/android/cloud-release-googleplay.md - Generate Baseline Profiles: workflows/android/cloud-generate-baseline-profiles.md + - Nightly Build: workflows/android/cloud-nightly-build.md + - PR Check: workflows/android/cloud-check.md + - Release (Firebase): workflows/android/cloud-release-firebaseAppDistribution.md + - Release (Google Play): workflows/android/cloud-release-googlePlay.md - KMP: - workflows/kmp/index.md - - Detect Changes: workflows/kmp/cloud-detect-changes.md - Combined Nightly Build: workflows/kmp/combined-nightly-build.md + - Detect Changes: workflows/kmp/cloud-detect-changes.md - Universal: - workflows/universal/index.md - - Workflows Lint: workflows/universal/workflows-lint.md - Cloud Backup: workflows/universal/cloud-backup.md - Self-hosted Backup: workflows/universal/selfhosted-backup.md + - Workflows Lint: workflows/universal/workflows-lint.md - Actions: - Overview: actions/index.md - - Android: - - actions/android/index.md - - Setup Environment: actions/android/setup-environment.md - - Check: actions/android/check.md - - Build Firebase: actions/android/build-firebase.md - - Build Google Play: actions/android/build-googleplay.md - - Generate Baseline Profiles: actions/android/generate-baseline-profiles.md - iOS: - actions/ios/index.md - Export Secrets: actions/ios/export-secrets.md - - Fastlane Test: actions/ios/fastlane-test.md - Fastlane Beta: actions/ios/fastlane-beta.md - Fastlane Release: actions/ios/fastlane-release.md - - KMP Build: actions/ios/kmp-build.md + - Fastlane Test: actions/ios/fastlane-test.md + - iOS + KMP: + - actions/ios-kmp/index.md + - Build: actions/ios-kmp/build.md + - Android: + - actions/android/index.md + - Build Firebase: actions/android/build-firebase.md + - Build Google Play: actions/android/build-googlePlay.md + - Check: actions/android/check.md + - Generate Baseline Profiles: actions/android/generate-baseline-profiles.md + - Setup Environment: actions/android/setup-environment.md + - KMP: + - actions/kmp/index.md + - Detect Changes: actions/kmp/detect-changes.md + - Universal: + - actions/universal/index.md + - Detect Changes & Changelog: actions/universal/detect-changes-and-generate-changelog.md - Utility: - actions/utility/index.md - - KMP Detect Changes: actions/utility/kmp-detect-changes.md - - Detect Changes & Changelog: actions/utility/detect-changes-changelog.md - JIRA Transition Tickets: actions/utility/jira-transition-tickets.md copyright: Made with ❤️‍🔥 at Futured diff --git a/.github/scripts/config.py b/.github/scripts/config.py index fb0e2b6..5eda977 100644 --- a/.github/scripts/config.py +++ b/.github/scripts/config.py @@ -30,6 +30,7 @@ # source – relative path to the YAML file # category – category id (must exist in CATEGORY_LABELS) # title – display title (default: YAML `name:` field) +# nav_title – short title for the mkdocs nav sidebar # output – output markdown path # runner – runner label shown in docs # not_reusable – bool; True hides the "Usage" snippet (auto-detected @@ -43,6 +44,7 @@ # source – relative path to action.yml # category – category id (must exist in CATEGORY_LABELS) # title – display title (default: YAML `name:` field) +# nav_title – short title for the mkdocs nav sidebar # output – output markdown path # readme – relative path to a README.md to embed (auto-detected) # @@ -54,6 +56,34 @@ }, "workflows-lint": { "not_reusable": True, + "nav_title": "Workflows Lint", + }, + "android-cloud-check": { + "nav_title": "PR Check", + }, + "android-cloud-release-firebaseAppDistribution": { + "nav_title": "Release (Firebase)", + }, + "android-cloud-release-googlePlay": { + "nav_title": "Release (Google Play)", + }, + "kmp-combined-nightly-build": { + "nav_title": "Combined Nightly Build", + }, + "android-build-firebase": { + "nav_title": "Build Firebase", + }, + "android-build-googlePlay": { + "nav_title": "Build Google Play", + }, + "android-setup-environment": { + "nav_title": "Setup Environment", + }, + "ios-export-secrets": { + "nav_title": "Export Secrets", + }, + "universal-detect-changes-and-generate-changelog": { + "nav_title": "Detect Changes & Changelog", }, } diff --git a/.github/scripts/generate-docs.py b/.github/scripts/generate-docs.py index aec2e92..6995668 100644 --- a/.github/scripts/generate-docs.py +++ b/.github/scripts/generate-docs.py @@ -27,6 +27,7 @@ sys.path.insert(0, str(ROOT_DIR)) from scripts.config import ACTIONS, CATEGORY_LABELS, WORKFLOWS +from scripts.nav_generator import build_nav, inject_nav, render_nav_yaml from scripts.enrichers.ai_enricher import AIEnricher from scripts.enrichers.base import BaseEnricher, EnrichmentResult from scripts.enrichers.readme_enricher import ReadmeEnricher @@ -260,6 +261,16 @@ def main() -> None: ) print(f" Written: docs/actions/index.md") + # ------------------------------------------------------------------- + # Generate nav in mkdocs.yml + # ------------------------------------------------------------------- + print("\nGenerating nav...") + nav = build_nav(WORKFLOWS, ACTIONS, CATEGORY_LABELS) + nav_yaml = render_nav_yaml(nav) + mkdocs_path = ROOT_DIR / "mkdocs.yml" + inject_nav(mkdocs_path, nav_yaml) + print(f" Updated: {mkdocs_path.relative_to(ROOT_DIR)}") + # ------------------------------------------------------------------- # Summary # ------------------------------------------------------------------- diff --git a/.github/scripts/nav_generator.py b/.github/scripts/nav_generator.py new file mode 100644 index 0000000..4290c82 --- /dev/null +++ b/.github/scripts/nav_generator.py @@ -0,0 +1,236 @@ +"""Auto-generate mkdocs.yml nav section from the config registry.""" + +from __future__ import annotations + +from collections import Counter +from pathlib import Path + + +# Lookup table for category labels that appear differently in YAML titles. +# "iOS + KMP" is the display label, but YAML `name:` fields use "iOS KMP". +_LABEL_VARIANTS: dict[str, list[str]] = { + "iOS + KMP": ["iOS + KMP", "iOS KMP"], +} + +_RUNNER_PREFIXES = ["Self-hosted", "Cloud", "Combined"] + +_ACRONYM_MAP: dict[str, str] = { + "ios": "iOS", + "kmp": "KMP", + "jira": "JIRA", + "pr": "PR", +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _title_case(text: str) -> str: + """Title-case *text* while preserving known acronyms (iOS, KMP, JIRA, PR).""" + words = text.split() + result: list[str] = [] + for word in words: + # Separate leading/trailing punctuation (e.g. "(Deprecated)", "&") + prefix = "" + suffix = "" + core = word + while core and not core[0].isalnum(): + prefix += core[0] + core = core[1:] + while core and not core[-1].isalnum(): + suffix = core[-1] + suffix + core = core[:-1] + + lower = core.lower() + if lower in _ACRONYM_MAP: + result.append(prefix + _ACRONYM_MAP[lower] + suffix) + elif core: + result.append(prefix + core[0].upper() + core[1:] + suffix) + else: + result.append(word) + return " ".join(result) + + +def derive_nav_title( + title: str, + category: str, + category_labels: dict[str, str], +) -> str: + """Derive a short nav title by stripping category and runner prefixes. + + Strips the category label (e.g. "iOS", "Android") and runner prefix + ("Self-hosted", "Cloud") from the full YAML title, then title-cases + the remainder. Falls back to the original title if stripping produces + an empty string. + """ + label = category_labels.get(category, "") + variants = _LABEL_VARIANTS.get(label, [label]) if label else [] + + stripped = title + for variant in variants: + if stripped.startswith(variant + " "): + stripped = stripped[len(variant) :].strip() + break + + for runner in _RUNNER_PREFIXES: + if stripped.startswith(runner + " "): + stripped = stripped[len(runner) :].strip() + break + + if not stripped: + return _title_case(title) + + return _title_case(stripped) + + +def _disambiguate_duplicates(entries: list[dict]) -> None: + """Prepend runner type when two entries share the same nav title. + + Mutates *entries* in place. Runner type is derived from the output + path stem (e.g. ``cloud-backup`` -> "Cloud Backup"). + """ + title_counts = Counter(e["nav_title"] for e in entries) + duplicates = {t for t, c in title_counts.items() if c > 1} + if not duplicates: + return + + for entry in entries: + if entry["nav_title"] not in duplicates: + continue + stem = Path(entry["path"]).stem + if stem.startswith("cloud-"): + entry["nav_title"] = f"Cloud {entry['nav_title']}" + elif stem.startswith("selfhosted-"): + entry["nav_title"] = f"Self-hosted {entry['nav_title']}" + elif stem.startswith("combined-"): + entry["nav_title"] = f"Combined {entry['nav_title']}" + + +# --------------------------------------------------------------------------- +# Nav builder +# --------------------------------------------------------------------------- + + +def _build_type_section( + registry: dict[str, dict], + category_labels: dict[str, str], + doc_type: str, +) -> list: + """Build the nav sub-tree for either workflows or actions.""" + section: list = [{"Overview": f"{doc_type}/index.md"}] + + # Group entries by category. + by_category: dict[str, list[tuple[str, dict]]] = {} + for key, cfg in registry.items(): + by_category.setdefault(cfg["category"], []).append((key, cfg)) + + # Walk categories in CATEGORY_LABELS order (preserves intended ordering). + for cat_id, cat_label in category_labels.items(): + if cat_id not in by_category: + continue + + cat_items: list = [f"{doc_type}/{cat_id}/index.md"] + + nav_entries: list[dict] = [] + for key, cfg in by_category[cat_id]: + if "nav_title" in cfg: + nav_title = cfg["nav_title"] + else: + nav_title = derive_nav_title(cfg["title"], cat_id, category_labels) + rel_path = cfg["output"].removeprefix("docs/") + nav_entries.append({"key": key, "nav_title": nav_title, "path": rel_path}) + + _disambiguate_duplicates(nav_entries) + nav_entries.sort(key=lambda e: e["nav_title"]) + + for entry in nav_entries: + cat_items.append({entry["nav_title"]: entry["path"]}) + + section.append({cat_label: cat_items}) + + return section + + +def build_nav( + workflows: dict[str, dict], + actions: dict[str, dict], + category_labels: dict[str, str], +) -> list: + """Build the full ``nav`` structure as a nested Python list. + + The returned list mirrors mkdocs' nav format and can be rendered to + YAML with :func:`render_nav_yaml`. + """ + return [ + {"Home": "index.md"}, + {"Workflows": _build_type_section(workflows, category_labels, "workflows")}, + {"Actions": _build_type_section(actions, category_labels, "actions")}, + ] + + +# --------------------------------------------------------------------------- +# YAML renderer +# --------------------------------------------------------------------------- + + +def _render_list(lines: list[str], items: list, indent: int) -> None: + prefix = " " * indent + for item in items: + if isinstance(item, str): + lines.append(f"{prefix}- {item}") + elif isinstance(item, dict): + key, value = next(iter(item.items())) + if isinstance(value, str): + lines.append(f"{prefix}- {key}: {value}") + elif isinstance(value, list): + lines.append(f"{prefix}- {key}:") + _render_list(lines, value, indent + 4) + + +def render_nav_yaml(nav: list) -> str: + """Render a nav structure to a YAML string. + + Uses a simple custom renderer instead of PyYAML to avoid + ``!!python/name:`` tag issues in other parts of mkdocs.yml. + """ + lines = ["nav:"] + _render_list(lines, nav, indent=2) + return "\n".join(lines) + "\n" + + +# --------------------------------------------------------------------------- +# Injection +# --------------------------------------------------------------------------- + + +def inject_nav(mkdocs_path: str | Path, nav_yaml: str) -> None: + """Replace the ``nav:`` section in *mkdocs_path* with *nav_yaml*. + + Finds the ``nav:`` line at column 0, then the next top-level key, + and replaces everything in between. + """ + path = Path(mkdocs_path) + lines = path.read_text().splitlines(keepends=True) + + nav_start: int | None = None + nav_end: int | None = None + + for i, line in enumerate(lines): + if nav_start is None: + if line.startswith("nav:"): + nav_start = i + elif line.strip() and not line[0].isspace(): + nav_end = i + break + + if nav_start is None: + raise ValueError("Could not find 'nav:' at column 0 in mkdocs.yml") + if nav_end is None: + nav_end = len(lines) + + before = "".join(lines[:nav_start]) + after = "".join(lines[nav_end:]) + + path.write_text(before + nav_yaml + "\n" + after) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 4db3288..d25238a 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -13,16 +13,18 @@ on: - '.github/scripts/**' - '.github/requirements-docs.txt' workflow_dispatch: + delete: permissions: contents: write concurrency: - group: deploy-docs + group: deploy-docs-${{ github.ref }} cancel-in-progress: true jobs: deploy: + if: github.event_name != 'delete' runs-on: ubuntu-latest defaults: run: @@ -46,16 +48,15 @@ jobs: { echo "version=$VERSION" echo "alias=latest" - echo "ref=$VERSION" } >> "$GITHUB_OUTPUT" else + BRANCH="${GITHUB_REF#refs/heads/}" { - echo "version=main" + echo "version=${BRANCH//\//-}" echo "alias=" - echo "ref=main" } >> "$GITHUB_OUTPUT" fi - - run: python scripts/generate-docs.py --ref ${{ steps.version.outputs.ref }} + - run: python scripts/generate-docs.py --ref ${{ steps.version.outputs.version }} - run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com @@ -65,5 +66,28 @@ jobs: mike deploy --push --update-aliases ${{ steps.version.outputs.version }} ${{ steps.version.outputs.alias }} mike set-default --push latest else - mike deploy --push main + mike deploy --push ${{ steps.version.outputs.version }} fi + + cleanup-preview: + if: github.event_name == 'delete' && github.event.ref_type == 'branch' && github.event.ref != 'main' + runs-on: ubuntu-latest + defaults: + run: + working-directory: .github + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: .github/requirements-docs.txt + - run: pip install -r requirements-docs.txt + - run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - run: | + VERSION="${{ github.event.ref }}" + mike delete --push "${VERSION//\//-}"