blob: 963768b432f83a829b8951010205bf87a6b1d991 [file] [log] [blame] [edit]
#!/usr/bin/env python3
"""
Annotate pull requests to the GitHub repository with links to specifications.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import json
import re
import sys
from uritemplate import URITemplate
BIN_DIR = Path(__file__).parent
TESTS = BIN_DIR.parent / "tests"
URLS = json.loads(BIN_DIR.joinpath("specification_urls.json").read_text())
def urls(version: str) -> dict[str, URITemplate]:
"""
Retrieve the version-specific URLs for specifications.
"""
for_version = {**URLS["json-schema"][version], **URLS["external"]}
return {k: URITemplate(v) for k, v in for_version.items()}
def annotation(
path: Path,
message: str,
line: int = 1,
level: str = "notice",
**kwargs: Any,
) -> str:
"""
Format a GitHub annotation.
See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
for full syntax.
"""
if kwargs:
additional = "," + ",".join(f"{k}={v}" for k, v in kwargs.items())
else:
additional = ""
relative = path.relative_to(TESTS.parent)
return f"::{level} file={relative},line={line}{additional}::{message}\n"
def line_number_of(path: Path, case: dict[str, Any]) -> int:
"""
Crudely find the line number of a test case.
"""
with path.open() as file:
description = case["description"]
return next(
(i + 1 for i, line in enumerate(file, 1) if description in line),
1,
)
def extract_kind_and_spec(key: str) -> (str, str):
"""
Extracts specification number and kind from the defined key
"""
can_have_spec = ["rfc", "iso"]
if not any(key.startswith(el) for el in can_have_spec):
return key, ""
number = re.search(r"\d+", key)
spec = "" if number is None else number.group(0)
kind = key.removesuffix(spec)
return kind, spec
def main():
# Clear annotations which may have been emitted by a previous run.
sys.stdout.write("::remove-matcher owner=me::\n")
for version in TESTS.iterdir():
if version.name in {"draft-next", "latest"}:
continue
version_urls = urls(version.name)
for path in version.rglob("*.json"):
try:
contents = json.loads(path.read_text())
except json.JSONDecodeError as error:
error = annotation(
level="error",
path=path,
line=error.lineno,
col=error.pos + 1,
title=str(error),
message=f"cannot load {path}"
)
sys.stdout.write(error)
continue
for test_case in contents:
specifications = test_case.get("specification")
if specifications is not None:
for each in specifications:
quote = each.pop("quote", "")
(key, section), = each.items()
(kind, spec) = extract_kind_and_spec(key)
url_template = version_urls[kind]
if url_template is None:
error = annotation(
level="error",
path=path,
line=line_number_of(path, test_case),
title=f"unsupported template '{kind}'",
message=f"cannot find a URL template for '{kind}'"
)
sys.stdout.write(error)
continue
url = url_template.expand(
spec=spec,
section=section,
)
message = f"{url}\n\n{quote}" if quote else url
sys.stdout.write(
annotation(
path=path,
line=line_number_of(path, test_case),
title="Specification Link",
message=message,
),
)
if __name__ == "__main__":
main()