aboutsummaryrefslogtreecommitdiff
path: root/maintainers/build-release-notes.py
blob: 7be7bc2b99af8c4d429729a9962cd3092037a662 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from collections import defaultdict
import frontmatter
import sys
import pathlib
import textwrap
from typing import Any, Tuple

GH_BASE = "https://github.com/NixOS/nix"
FORGEJO_BASE = "https://git.lix.systems/lix-project/lix"
GERRIT_BASE = "https://gerrit.lix.systems/c/lix/+"
KNOWN_KEYS = ('synopsis', 'cls', 'issues', 'prs', 'significance', 'category', 'credits')

SIGNIFICANCECES = {
    None: 0,
    'significant': 10,
}

# This is just hardcoded for better validation. If you think there should be
# more of them, feel free to add more.
CATEGORIES = [
    'Breaking Changes',
    'Features',
    'Improvements',
    'Fixes',
    'Packaging',
    'Miscellany',
]

def format_link(ident: str, gh_part: str, fj_part: str) -> str:
    # FIXME: deprecate github as default
    if ident.isdigit():
        num, link, base = int(ident), f"#{ident}", f"{GH_BASE}/{gh_part}"
    elif ident.startswith("gh#"):
        num, link, base = int(ident[3:]), ident, f"{GH_BASE}/{gh_part}"
    elif ident.startswith("fj#"):
        num, link, base = int(ident[3:]), ident, f"{FORGEJO_BASE}/{fj_part}"
    else:
        raise Exception("unrecognized reference format", ident)
    return f"[{link}]({base}/{num})"

def format_issue(issue: str) -> str:
    return format_link(issue, "issues", "issues")
def format_pr(pr: str) -> str:
    return format_link(pr, "pull", "pulls")
def format_cl(clid: int) -> str:
    return f"[cl/{clid}]({GERRIT_BASE}/{clid})"

def plural_list(strs: list[str]) -> str:
    if len(strs) <= 1:
        return ''.join(strs)
    else:
        comma = ',' if len(strs) >= 3 else ''
        return '{}{} and {}'.format(', '.join(strs[:-1]), comma, strs[-1])

def listify(l: list | int) -> list:
    if not isinstance(l, list):
        return [l]
    else:
        return l

def do_category(entries: list[Tuple[pathlib.Path, Any]]):
    for p, entry in sorted(entries, key=lambda e: (-SIGNIFICANCECES[e[1].metadata.get('significance')], e[0])):
        try:
            header = entry.metadata['synopsis']
            links = []
            links += [format_issue(str(s)) for s in listify(entry.metadata.get('issues', []))]
            links += [format_pr(str(s)) for s in listify(entry.metadata.get('prs', []))]
            links += [format_cl(cl) for cl in listify(entry.metadata.get('cls', []))]
            if links != []:
                header += " " + " ".join(links)
            if header:
                print(f"- {header}")
                print()
            print(textwrap.indent(entry.content, '  '))
            if credits := listify(entry.metadata.get('credits', [])):
                print()
                print(textwrap.indent('Many thanks to {} for this.'.format(plural_list(credits)), '  '))
        except Exception as e:
            e.add_note(f"in {p}")
            raise


def run_on_dir(d):
    d = pathlib.Path(d)
    if not d.is_dir():
        raise ValueError(f'provided path {d} is not a directory')
    paths = pathlib.Path(d).glob('*.md')
    entries = defaultdict(list)
    for p in paths:
        try:
            e = frontmatter.load(p)
            if 'synopsis' not in e.metadata:
                raise Exception('missing synopsis')
            unknownKeys = set(e.metadata.keys()) - set(KNOWN_KEYS)
            if unknownKeys:
                raise Exception('unknown keys', unknownKeys)
            category = e.metadata.get('category', 'Miscellany')
            if category not in CATEGORIES:
                raise Exception('unknown category', category)
            entries[category].append((p, e))
        except Exception as e:
            e.add_note(f"in {p}")
            raise

    for category in CATEGORIES:
        if entries[category]:
            print('\n#', category)
            do_category(entries[category])


if __name__ == '__main__':
    for d in sys.argv[1:]:
        run_on_dir(d)