ruststream 0.7.5

Async messaging framework for Rust: broker-agnostic traits, router, codecs, and a conformance harness for broker authors.
Documentation
#!/usr/bin/env python3
"""Require an edit to a documentation page to reach that page's other languages.

The rule is symmetric: it takes its input from changed pages in any language, not from
English alone.

Three properties bound what it enforces:

- A page with no counterpart yet is skipped, not failed. Demanding a translation that has
  not been written would block every edit until the whole site is translated. The skip is
  temporary: once every page exists in every language, a missing counterpart is a failure
  like any other.
- Adding a page does not demand an edit elsewhere: only pages that were modified, deleted or
  renamed put the rule in force.
- It checks that a file changed, not that it changed correctly, and one character satisfies
  it. It catches "forgot the translation entirely", nothing more, and is paired with a
  review of the translation itself.

The declared way out is a label on the pull request, handled in the workflow rather than
here. Without one the way out becomes a token edit to the translated file, which marks a
page as revisited when nobody read it.
"""

import argparse
import subprocess
import sys
from pathlib import Path

# Keep in sync with `i18n.locales` in `website/docusaurus.config.ts`.
LOCALES = ("ru", "zh")

# English pages live under `docs/`; each translation mirrors their paths in its locale's tree.
DOCS_PREFIX = "docs/"
TRANSLATION_PREFIX = {
    loc: f"website/i18n/{loc}/docusaurus-plugin-content-docs/current/" for loc in LOCALES
}
# Statuses that put the rule in force: the page existed and its content moved. A rename that
# changed nothing (`R100`) is excluded, because relocating a file says nothing about what it
# says: moving the whole tree from one layout to another would otherwise demand an edit to
# every page in every language to prove that none of them changed.
TRIGGERING = ("M", "D", "R")

# Pages written in English only: the localized sites serve them in English under a
# banner that says so, so an edit to one has no counterpart to reach.
ENGLISH_ONLY = ("components/",)

REPO = Path(__file__).resolve().parent.parent


def git(*args: str) -> str:
    return subprocess.run(
        ["git", *args], cwd=REPO, check=True, capture_output=True, text=True
    ).stdout


def relative_page(path: str) -> str | None:
    """A page's path relative to its language's tree, or None for a file that is not a
    translated page."""
    if not path.endswith(".md"):
        return None
    for prefix in (DOCS_PREFIX, *TRANSLATION_PREFIX.values()):
        if path.startswith(prefix):
            relative = path[len(prefix) :]
            return None if relative.startswith(ENGLISH_ONLY) else relative
    return None


def family(path: str) -> tuple[str, dict[str, str]]:
    """The English original of a page and every locale counterpart it could have."""
    relative = relative_page(path)
    english = f"{DOCS_PREFIX}{relative}"
    return english, {loc: f"{prefix}{relative}" for loc, prefix in TRANSLATION_PREFIX.items()}


def changed_pages(base: str) -> dict[str, str]:
    """Documentation pages in the diff against `base`, mapped to their change status."""
    merge_base = git("merge-base", base, "HEAD").strip()
    raw = git("diff", "--name-status", "-M", merge_base, "HEAD")
    pages: dict[str, str] = {}
    for line in raw.splitlines():
        fields = line.split("\t")
        code = fields[0]
        status = code[0]
        if status == "R" and code[1:] == "100":
            continue
        # A rename reports the old path and the new one; both sides are part of the diff.
        for path in fields[1:]:
            if relative_page(path) is not None:
                pages[path] = status
    return pages


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--base",
        default="origin/main",
        help="branch, tag or commit the pull request targets (default: origin/main)",
    )
    args = parser.parse_args()

    pages = changed_pages(args.base)
    if not pages:
        print("no documentation pages changed")
        return 0

    errors: list[str] = []
    for path, status in sorted(pages.items()):
        if status not in TRIGGERING:
            continue
        english, translations = family(path)
        for member in (english, *translations.values()):
            if member == path or member in pages:
                continue
            if not (REPO / member).exists():
                continue
            errors.append(f"{path} changed, but its counterpart {member} did not")

    for error in errors:
        print(error, file=sys.stderr)
    if errors:
        print(
            "\nA change to what a page says belongs in every language that page exists in."
            "\nIf this edit is confined to one language (spelling, grammar, or a word choice"
            "\nthat was already right elsewhere), label the pull request and this job is"
            "\nskipped; see .github/workflows/docs.yml for the label name.",
            file=sys.stderr,
        )
        return 1
    print(f"checked {len(pages)} changed documentation page(s)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())