-
Notifications
You must be signed in to change notification settings - Fork 0
release-train: develop -> staging #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+304
−25
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| name: SDK install extras | ||
|
|
||
| # Asserts that every documented `pip install "tracebloc[...]"` line names an | ||
| # extra that actually exists in the release its version floor resolves to. | ||
| # | ||
| # Catches the failure mode from backend#1858: tools-help/tracebloc.mdx | ||
| # documented `tracebloc[boosting]` and `tracebloc[survival]` for eight | ||
| # published releases after both were removed in 0.10.0. | ||
| # | ||
| # This is the one class of docs error no other check here can see: | ||
| # - Mintlify validates links and MDX; a fenced code block is opaque to it. | ||
| # - `pip` does not fail on an unknown extra. It warns, installs the core | ||
| # package, and exits 0 — so the reader's install "succeeds" and then dies | ||
| # later as an ImportError, far from the command that caused it. | ||
| # | ||
| # Runs on a schedule as well as on push, because the docs can rot without the | ||
| # docs changing: a floating `>=` floor silently re-points at each new release, | ||
| # so an extra removed upstream breaks a page nobody edited. | ||
| # | ||
| # No step interpolates event data into a shell command; the only expression | ||
| # used is github.ref, in the concurrency key. | ||
|
|
||
| on: | ||
| push: | ||
| branches: [develop, main] | ||
| pull_request: | ||
| paths: | ||
| - '**.mdx' | ||
| - '**.md' | ||
| - 'scripts/check-sdk-extras.py' | ||
| - '.github/workflows/sdk-extras-check.yml' | ||
| schedule: | ||
| - cron: '30 6 * * 1' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: sdk-extras-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| check: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
|
|
||
| - name: Validate documented extras against PyPI | ||
| run: python3 scripts/check-sdk-extras.py |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| #!/usr/bin/env python3 | ||
| """Validate every documented `pip install "tracebloc[...]"` line against PyPI. | ||
|
|
||
| Why this exists (backend#1858) | ||
| ------------------------------ | ||
| `tools-help/tracebloc.mdx` documented `tracebloc[boosting]` and | ||
| `tracebloc[survival]` for eight published releases after both extras were | ||
| removed in 0.10.0. Nothing caught it, because this is the one class of docs | ||
| error that no docs tool can see: | ||
|
|
||
| * Mintlify validates links and MDX. A fenced code block is opaque to it. | ||
| * `pip` does NOT fail on an unknown extra. It prints | ||
| "WARNING: tracebloc X does not provide the extra 'boosting'", installs | ||
| the core package, and exits 0. The user's build appears to succeed and | ||
| then dies later as an ImportError, far from the command that caused it. | ||
|
|
||
| So a wrong extra in the docs silently mis-installs software. This script | ||
| closes that gap by resolving each documented extra against the published | ||
| `Provides-Extra` metadata for the version the documented floor selects. | ||
|
|
||
| It also checks the version floor itself, because the floor is what made the | ||
| original bug silent: `>=0.8.1` floats forward to a release that no longer has | ||
| the extras, while still being satisfiable by an ancient release on an old | ||
| Python. | ||
|
|
||
| Usage: | ||
| python3 scripts/check-sdk-extras.py # scan the repo | ||
| python3 scripts/check-sdk-extras.py FILE... # scan specific files | ||
|
|
||
| Exits 0 if every documented extra exists, 1 otherwise. Requires network | ||
| access to pypi.org. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import re | ||
| import sys | ||
| import urllib.error | ||
| import urllib.request | ||
| from pathlib import Path | ||
|
|
||
| PACKAGE = "tracebloc" | ||
| PYPI_URL = f"https://pypi.org/pypi/{PACKAGE}/json" | ||
|
|
||
| # Matches: tracebloc[a,b]>=1.2.3 / tracebloc[a] / tracebloc[a]==1.2.3 | ||
| # Captures the extras list and, when present, the version specifier. | ||
| SPEC_RE = re.compile( | ||
| r"\b" + PACKAGE + r"\[([^\]]+)\]\s*(?:(==|>=|~=|>)\s*([0-9][0-9A-Za-z.*+!-]*))?" | ||
| ) | ||
|
|
||
| DOC_SUFFIXES = {".mdx", ".md"} | ||
| SKIP_DIRS = {".git", "node_modules", ".venv", "images"} | ||
|
|
||
|
|
||
| def fetch_metadata() -> dict: | ||
| try: | ||
| with urllib.request.urlopen(PYPI_URL, timeout=30) as resp: | ||
| return json.load(resp) | ||
| except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc: | ||
| sys.exit(f"error: could not read {PYPI_URL}: {exc}") | ||
|
|
||
|
|
||
| def parse_version(value: str) -> tuple: | ||
| """Coarse numeric version key. Good enough to order this package's tags.""" | ||
| parts = [] | ||
| for chunk in value.split("."): | ||
| digits = re.match(r"\d+", chunk) | ||
| parts.append(int(digits.group()) if digits else 0) | ||
| return tuple(parts) | ||
|
|
||
|
|
||
| # A PEP 440 pre-release / dev-release marker on the end of the release segment: | ||
| # 0.19.0a1, 1.2b3, 2.0rc1, 1.0.dev1. `.postN` is deliberately NOT here — post | ||
| # releases install by default, so they are ordinary candidates. | ||
| # | ||
| # Hand-rolled because this script is stdlib-only by construction: its workflow | ||
| # runs `python3 scripts/check-sdk-extras.py` with no setup-python and no pip | ||
| # install, so `packaging.version` is not importable and adding it would mean | ||
| # adding a dependency step to a docs check. | ||
| _PRERELEASE_RE = re.compile( | ||
| r"^\d+(?:\.\d+)*[._-]?(?:a|b|c|rc|alpha|beta|pre|preview|dev)\d*", | ||
| re.IGNORECASE, | ||
| ) | ||
|
|
||
|
|
||
| def is_prerelease(value: str) -> bool: | ||
| return bool(_PRERELEASE_RE.match(value)) | ||
|
|
||
|
|
||
| def resolve_version(floor: str | None, operator: str | None, releases: list[str]) -> str: | ||
| """Which published version does this documented specifier actually select? | ||
|
|
||
| `pip` picks the NEWEST version satisfying the specifier, so that — not the | ||
| floor itself — is the version whose extras the reader ends up with. | ||
|
|
||
| PRE-RELEASES ARE EXCLUDED, because pip does not install them without `--pre` | ||
| and the documented commands do not pass it. `parse_version` reduces a tag to | ||
| a digit tuple, so `0.19.0a1` became (0, 19, 0) and outranked `0.18.1` — the | ||
| gate would then check the extras of a version no reader can get, and pass or | ||
| fail on a package they will never install (Bugbot, docs#114). | ||
|
|
||
| If a package has ONLY pre-releases the filter would leave nothing to pick, so | ||
| it falls back to the unfiltered list rather than crashing on an empty max(). | ||
| WHICH pre-release that picks is arbitrary: `parse_version` reduces 1.0.0a1 | ||
| and 1.0.0b2 to the same (1, 0, 0), and ordering them properly needs real | ||
| PEP 440 parsing, which this stdlib-only script cannot do. Stated rather than | ||
| silently relied on — tracebloc has 11 stable releases, so the branch is a | ||
| guard against an empty max(), not a code path anyone rides. | ||
| """ | ||
| stable = [r for r in releases if not is_prerelease(r)] or releases | ||
| if floor is None: | ||
| return max(stable, key=parse_version) | ||
| if operator == "==": | ||
| return floor | ||
| candidates = [r for r in stable if parse_version(r) >= parse_version(floor)] | ||
| if not candidates: | ||
| return floor | ||
| return max(candidates, key=parse_version) | ||
|
|
||
|
|
||
| def iter_doc_files(roots: list[str]) -> list[Path]: | ||
| if roots: | ||
| return [Path(r) for r in roots] | ||
| found: list[Path] = [] | ||
| for path in Path(".").rglob("*"): | ||
| if any(part in SKIP_DIRS for part in path.parts): | ||
| continue | ||
| if path.is_file() and path.suffix in DOC_SUFFIXES: | ||
| found.append(path) | ||
| return sorted(found) | ||
|
|
||
|
|
||
| def main(argv: list[str]) -> int: | ||
| meta = fetch_metadata() | ||
| releases = sorted(meta["releases"].keys(), key=parse_version) | ||
| latest = meta["info"]["version"] | ||
|
|
||
| # Provides-Extra per version needs a per-version fetch; cache it. | ||
| extras_cache: dict[str, set[str]] = { | ||
| latest: set(meta["info"].get("provides_extra") or []) | ||
| } | ||
|
|
||
| def extras_for(version: str) -> set[str] | None: | ||
| if version in extras_cache: | ||
| return extras_cache[version] | ||
| url = f"https://pypi.org/pypi/{PACKAGE}/{version}/json" | ||
| try: | ||
| with urllib.request.urlopen(url, timeout=30) as resp: | ||
| data = json.load(resp) | ||
| except (urllib.error.URLError, TimeoutError, json.JSONDecodeError): | ||
| extras_cache[version] = None | ||
| return None | ||
| extras_cache[version] = set(data["info"].get("provides_extra") or []) | ||
| return extras_cache[version] | ||
|
|
||
| failures: list[str] = [] | ||
| checked = 0 | ||
|
|
||
| for path in iter_doc_files(argv): | ||
| try: | ||
| text = path.read_text(encoding="utf-8") | ||
| except (OSError, UnicodeDecodeError): | ||
| continue | ||
|
|
||
| for lineno, line in enumerate(text.splitlines(), start=1): | ||
| for match in SPEC_RE.finditer(line): | ||
| extras_raw, operator, floor = match.groups() | ||
| extras = [e.strip() for e in extras_raw.split(",") if e.strip()] | ||
| checked += 1 | ||
|
|
||
| if floor is not None and floor not in releases: | ||
| failures.append( | ||
| f"{path}:{lineno}: version {floor} is not published on PyPI" | ||
| ) | ||
| continue | ||
|
|
||
| version = resolve_version(floor, operator, releases) | ||
| available = extras_for(version) | ||
| if available is None: | ||
| failures.append( | ||
| f"{path}:{lineno}: could not read metadata for {PACKAGE} {version}" | ||
| ) | ||
| continue | ||
|
|
||
| for extra in extras: | ||
| if extra not in available: | ||
| failures.append( | ||
| f"{path}:{lineno}: {PACKAGE}[{extra}] does not exist in " | ||
| f"{version} (the version '{operator or ''}{floor or 'latest'}' " | ||
| f"resolves to). Available: {', '.join(sorted(available))}" | ||
| ) | ||
|
|
||
| print(f"Checked {checked} documented '{PACKAGE}[...]' spec(s); latest release is {latest}.") | ||
|
|
||
| if failures: | ||
| print(f"\n{len(failures)} problem(s) found:\n", file=sys.stderr) | ||
| for failure in failures: | ||
| print(f" {failure}", file=sys.stderr) | ||
| print( | ||
| "\nNote: pip warns and exits 0 on an unknown extra, so a wrong extra here " | ||
| "silently installs the core SDK only.", | ||
| file=sys.stderr, | ||
| ) | ||
| return 1 | ||
|
|
||
| print("All documented extras exist in the versions they resolve to.") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main(sys.argv[1:])) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.