|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 |
|
3 | 3 | import re |
4 | | -from typing import Optional, List, Set, Union, Type |
5 | | -from github.PullRequest import PullRequest |
6 | | -from github.GitRelease import GitRelease |
7 | | -from github.Repository import Repository |
| 4 | +from github import Repository, GitRelease |
8 | 5 |
|
9 | 6 |
|
10 | 7 | def is_valid_tag(tag: str) -> bool: |
11 | 8 | tag_regex = r'^v\d+\.\d+\.\d+$' |
12 | 9 | return bool(re.match(tag_regex, tag)) |
13 | 10 |
|
14 | 11 |
|
15 | | -def create_release(repository: Repository, tag: str, notes: str) -> Union[None, Type[Exception]]: |
| 12 | +def create_release(repository: Repository, tag: str) -> GitRelease: |
16 | 13 | if not is_valid_tag(tag): |
17 | 14 | raise Exception(f'Invalid tag: {tag}') |
18 | 15 |
|
19 | 16 | try: |
20 | | - repository.create_git_release(tag=tag, name=tag, message=notes, draft=False, prerelease=False) |
21 | | - |
| 17 | + return repository.create_git_release( |
| 18 | + tag=tag, name=tag, draft=False, prerelease=False, generate_release_notes=True |
| 19 | + ) |
22 | 20 | except Exception as exp: |
23 | 21 | raise Exception(f'create_release: Error creating release/tag {tag}: {exp!s}') from exp |
24 | | - |
25 | | - |
26 | | -def get_sorted_merged_pulls(pulls: List[PullRequest], last_release: Optional[GitRelease]) -> List[PullRequest]: |
27 | | - # Get merged pulls after last release |
28 | | - if not last_release: |
29 | | - return sorted( |
30 | | - ( |
31 | | - pull |
32 | | - for pull in pulls |
33 | | - if pull.merged |
34 | | - and pull.base.ref == 'main' |
35 | | - and not pull.title.startswith('chore: release') |
36 | | - and not pull.user.login.startswith('github-actions') |
37 | | - ), |
38 | | - key=lambda pull: pull.merged_at, |
39 | | - ) |
40 | | - |
41 | | - return sorted( |
42 | | - ( |
43 | | - pull |
44 | | - for pull in pulls |
45 | | - if pull.merged |
46 | | - and pull.base.ref == 'main' |
47 | | - and (pull.merged_at > last_release.created_at) |
48 | | - and not pull.title.startswith('chore: release') |
49 | | - and not pull.user.login.startswith('github-actions') |
50 | | - ), |
51 | | - key=lambda pull: pull.merged_at, |
52 | | - ) |
53 | | - |
54 | | - |
55 | | -def get_pr_contributors(pull_request: PullRequest) -> List[str]: |
56 | | - contributors = set() |
57 | | - for commit in pull_request.get_commits(): |
58 | | - commit_message = commit.commit.message |
59 | | - if commit_message.startswith('Co-authored-by:'): |
60 | | - coauthor = commit_message.split('<')[0].split(':')[-1].strip() |
61 | | - contributors.add(coauthor) |
62 | | - else: |
63 | | - author = commit.author |
64 | | - if author: |
65 | | - contributors.add(author.login) |
66 | | - return sorted(list(contributors), key=str.lower) |
67 | | - |
68 | | - |
69 | | -def get_old_contributors(pulls: List[PullRequest], last_release: Optional[GitRelease]) -> Set[str]: |
70 | | - contributors = set() |
71 | | - if last_release: |
72 | | - merged_pulls = [pull for pull in pulls if pull.merged and pull.merged_at <= last_release.created_at] |
73 | | - |
74 | | - for pull in merged_pulls: |
75 | | - pr_contributors = get_pr_contributors(pull) |
76 | | - for contributor in pr_contributors: |
77 | | - contributors.add(contributor) |
78 | | - |
79 | | - return contributors |
80 | | - |
81 | | - |
82 | | -def get_new_contributors(old_contributors: List[str], merged_pulls: List[PullRequest]) -> List[str]: |
83 | | - new_contributors = set() |
84 | | - for pull in merged_pulls: |
85 | | - pr_contributors = get_pr_contributors(pull) |
86 | | - for contributor in pr_contributors: |
87 | | - if contributor not in old_contributors: |
88 | | - new_contributors.add(contributor) |
89 | | - |
90 | | - return sorted(list(new_contributors), key=str.lower) |
91 | | - |
92 | | - |
93 | | -def get_last_release(releases: List[GitRelease]) -> Optional[GitRelease]: |
94 | | - sorted_releases = sorted(releases, key=lambda r: r.created_at, reverse=True) |
95 | | - |
96 | | - if sorted_releases: |
97 | | - return sorted_releases[0] |
98 | | - |
99 | | - return None |
100 | | - |
101 | | - |
102 | | -def multiple_contributors_mention_md(contributors: List[str]) -> str: |
103 | | - contrib_by = '' |
104 | | - if len(contributors) <= 1: |
105 | | - for contrib in contributors: |
106 | | - contrib_by += f'@{contrib}' |
107 | | - else: |
108 | | - for contrib in contributors: |
109 | | - contrib_by += f'@{contrib}, ' |
110 | | - |
111 | | - contrib_by = contrib_by[:-2] |
112 | | - last_comma = contrib_by.rfind(', ') |
113 | | - contrib_by = contrib_by[:last_comma].strip() + ' and ' + contrib_by[last_comma + 1 :].strip() |
114 | | - return contrib_by |
115 | | - |
116 | | - |
117 | | -def whats_changed_md(repo_full_name: str, merged_pulls: List[PullRequest]) -> List[str]: |
118 | | - whats_changed = [] |
119 | | - for pull in merged_pulls: |
120 | | - contributors = get_pr_contributors(pull) |
121 | | - contrib_by = multiple_contributors_mention_md(contributors) |
122 | | - |
123 | | - whats_changed.append( |
124 | | - f'* {pull.title} by {contrib_by} in https://github.com/{repo_full_name}/pull/{pull.number}' |
125 | | - ) |
126 | | - |
127 | | - return whats_changed |
128 | | - |
129 | | - |
130 | | -def get_first_contribution(merged_pulls: List[str], contributor: str) -> Optional[PullRequest]: |
131 | | - for pull in merged_pulls: |
132 | | - contrubutors = get_pr_contributors(pull) |
133 | | - if contributor in contrubutors: |
134 | | - return pull |
135 | | - |
136 | | - # ? unreachable |
137 | | - return None |
138 | | - |
139 | | - |
140 | | -def new_contributors_md(repo_full_name: str, merged_pulls: List[PullRequest], new_contributors: List[str]) -> List[str]: |
141 | | - contributors_by_pr = {} |
142 | | - contributors_md = [] |
143 | | - for contributor in new_contributors: |
144 | | - first_contrib = get_first_contribution(merged_pulls, contributor) |
145 | | - |
146 | | - if not first_contrib: |
147 | | - continue |
148 | | - |
149 | | - if first_contrib.number not in contributors_by_pr.keys(): |
150 | | - contributors_by_pr[first_contrib.number] = [contributor] |
151 | | - else: |
152 | | - contributors_by_pr[first_contrib.number] += [contributor] |
153 | | - |
154 | | - contributors_by_pr = dict(sorted(contributors_by_pr.items())) |
155 | | - for pr_number, contributors in contributors_by_pr.items(): |
156 | | - contributors.sort(key=str.lower) |
157 | | - contrib_by = multiple_contributors_mention_md(contributors) |
158 | | - |
159 | | - contributors_md.append( |
160 | | - f'* {contrib_by} made their first contribution in https://github.com/{repo_full_name}/pull/{pr_number}' |
161 | | - ) |
162 | | - |
163 | | - return contributors_md |
164 | | - |
165 | | - |
166 | | -def full_changelog_md(repository_name: str, last_tag_name: str, next_tag_name: str) -> Optional[str]: |
167 | | - if not last_tag_name: |
168 | | - return None |
169 | | - return f'**Full Changelog**: https://github.com/{repository_name}/compare/{last_tag_name}...{next_tag_name}' |
170 | | - |
171 | | - |
172 | | -def contruct_release_notes(repository: Repository, next_tag_name: str) -> str: |
173 | | - repo_name = repository.full_name |
174 | | - last_release = get_last_release(repository.get_releases()) |
175 | | - all_pulls = repository.get_pulls(state='closed') |
176 | | - |
177 | | - sorted_merged_pulls = get_sorted_merged_pulls(all_pulls, last_release) |
178 | | - old_contributors = get_old_contributors(all_pulls, last_release) |
179 | | - new_contributors = get_new_contributors(old_contributors, sorted_merged_pulls) |
180 | | - |
181 | | - whats_changed = whats_changed_md(repo_name, sorted_merged_pulls) |
182 | | - |
183 | | - new_contrib_md = new_contributors_md(repo_name, sorted_merged_pulls, new_contributors) |
184 | | - |
185 | | - notes = "## What's changed\n" |
186 | | - for changes in whats_changed: |
187 | | - notes += changes + '\n' |
188 | | - |
189 | | - notes += '\n' |
190 | | - |
191 | | - if new_contributors: |
192 | | - notes += '## New Contributors\n' |
193 | | - for new_contributor in new_contrib_md: |
194 | | - notes += new_contributor + '\n' |
195 | | - |
196 | | - notes += '\n' |
197 | | - |
198 | | - if last_release: |
199 | | - notes += full_changelog_md(repository.full_name, last_release.title, next_tag_name) |
200 | | - |
201 | | - return notes |
0 commit comments