Skip to content

Commit d6f1010

Browse files
authored
Merge pull request #746 from json-schema-org/annotations
Annotate Specification Links in GH Actions
2 parents c2badb1 + 4aec22c commit d6f1010

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Show Specification Annotations
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'tests/**'
7+
8+
jobs:
9+
annotate:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.x'
19+
20+
- name: Generate Annotations
21+
run: pip install uritemplate && bin/annotate-specification-links

bin/annotate-specification-links

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Annotate pull requests to the GitHub repository with links to specifications.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from pathlib import Path
9+
from typing import Any
10+
import json
11+
import re
12+
import sys
13+
14+
from uritemplate import URITemplate
15+
16+
17+
BIN_DIR = Path(__file__).parent
18+
TESTS = BIN_DIR.parent / "tests"
19+
URLS = json.loads(BIN_DIR.joinpath("specification_urls.json").read_text())
20+
21+
22+
def urls(version: str) -> dict[str, URITemplate]:
23+
"""
24+
Retrieve the version-specific URLs for specifications.
25+
"""
26+
for_version = {**URLS["json-schema"][version], **URLS["external"]}
27+
return {k: URITemplate(v) for k, v in for_version.items()}
28+
29+
30+
def annotation(
31+
path: Path,
32+
message: str,
33+
line: int = 1,
34+
level: str = "notice",
35+
**kwargs: Any,
36+
) -> str:
37+
"""
38+
Format a GitHub annotation.
39+
40+
See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
41+
for full syntax.
42+
"""
43+
44+
if kwargs:
45+
additional = "," + ",".join(f"{k}={v}" for k, v in kwargs.items())
46+
else:
47+
additional = ""
48+
49+
relative = path.relative_to(TESTS.parent)
50+
return f"::{level} file={relative},line={line}{additional}::{message}\n"
51+
52+
53+
def line_number_of(path: Path, case: dict[str, Any]) -> int:
54+
"""
55+
Crudely find the line number of a test case.
56+
"""
57+
with path.open() as file:
58+
description = case["description"]
59+
return next(
60+
(i + 1 for i, line in enumerate(file, 1) if description in line),
61+
1,
62+
)
63+
64+
65+
def main():
66+
# Clear annotations which may have been emitted by a previous run.
67+
sys.stdout.write("::remove-matcher owner=me::\n")
68+
69+
for version in TESTS.iterdir():
70+
if version.name in {"draft-next", "latest"}:
71+
continue
72+
73+
version_urls = urls(version.name)
74+
75+
for path in version.rglob("*.json"):
76+
try:
77+
contents = json.loads(path.read_text())
78+
except json.JSONDecodeError as error:
79+
error = annotation(
80+
level="error",
81+
path=path,
82+
line=error.lineno,
83+
col=error.pos + 1,
84+
title=str(error),
85+
)
86+
sys.stdout.write(error)
87+
88+
for test_case in contents:
89+
specifications = test_case.get("specification")
90+
if specifications is not None:
91+
for each in specifications:
92+
quote = each.pop("quote", "")
93+
(kind, section), = each.items()
94+
95+
number = re.search(r"\d+", kind)
96+
spec = "" if number is None else number.group(0)
97+
98+
url = version_urls[kind].expand(
99+
spec=spec,
100+
section=section,
101+
)
102+
103+
message = f"{url}\n\n{quote}" if quote else url
104+
sys.stdout.write(
105+
annotation(
106+
path=path,
107+
line=line_number_of(path, test_case),
108+
title="Specification Link",
109+
message=message,
110+
),
111+
)
112+
113+
114+
if __name__ == "__main__":
115+
main()

bin/specification_urls.json

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"json-schema": {
3+
"draft2020-12": {
4+
"core": "https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-{section}",
5+
"validation": "https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-{section}"
6+
},
7+
"draft2019-09": {
8+
"core": "https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.{section}",
9+
"validation": "https://json-schema.org/draft/2019-09/draft-handrews-json-schema-validation-02#rfc.section.{section}"
10+
},
11+
"draft7": {
12+
"core": "https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.{section}",
13+
"validation": "https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.{section}"
14+
},
15+
"draft6": {
16+
"core": "https://json-schema.org/draft-06/draft-wright-json-schema-01#rfc.section.{section}",
17+
"validation": "https://json-schema.org/draft-06/draft-wright-json-schema-validation-01#rfc.section.{section}"
18+
},
19+
"draft4": {
20+
"core": "https://json-schema.org/draft-04/draft-zyp-json-schema-04#rfc.section.{section}",
21+
"validation": "https://json-schema.org/draft-04/draft-fge-json-schema-validation-00#rfc.section.{section}"
22+
},
23+
"draft3": {
24+
"core": "https://json-schema.org/draft-03/draft-zyp-json-schema-03.pdf"
25+
}
26+
},
27+
28+
"external": {
29+
"ecma262": "https://262.ecma-international.org/{section}",
30+
"perl5": "https://perldoc.perl.org/perlre#{section}",
31+
"rfc": "https://www.rfc-editor.org/rfc/{spec}.txt#{section}",
32+
"iso": "https://www.iso.org/obp/ui"
33+
}
34+
}

0 commit comments

Comments
 (0)