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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
#!/usr/bin/env python3
import re
from typing import Optional, List, Set, Union, Type
from github.PullRequest import PullRequest
from github.GitRelease import GitRelease
from github.Repository import Repository
def is_valid_tag(tag: str) -> bool:
tag_regex = r'^v\d+\.\d+\.\d+$'
return bool(re.match(tag_regex, tag))
def create_release(repository: Repository, tag: str, notes: str) -> Union[None, Type[Exception]]:
if not is_valid_tag(tag):
raise Exception(f'Invalid tag: {tag}')
try:
repository.create_git_release(tag=tag, name=tag, message=notes, draft=False, prerelease=False)
except Exception as exp:
raise Exception(f'create_release: Error creating release/tag {tag}: {exp!s}') from exp
def get_sorted_merged_pulls(pulls: List[PullRequest], last_release: Optional[GitRelease]) -> List[PullRequest]:
# Get merged pulls after last release
if not last_release:
return sorted(
(
pull
for pull in pulls
if pull.merged
and pull.base.ref == 'main'
and not pull.title.startswith('chore: release')
and not pull.user.login.startswith('github-actions')
),
key=lambda pull: pull.merged_at,
)
return sorted(
(
pull
for pull in pulls
if pull.merged
and pull.base.ref == 'main'
and (pull.merged_at > last_release.created_at)
and not pull.title.startswith('chore: release')
and not pull.user.login.startswith('github-actions')
),
key=lambda pull: pull.merged_at,
)
def get_pr_contributors(pull_request: PullRequest) -> List[str]:
contributors = set()
for commit in pull_request.get_commits():
commit_message = commit.commit.message
if commit_message.startswith('Co-authored-by:'):
coauthor = commit_message.split('<')[0].split(':')[-1].strip()
contributors.add(coauthor)
else:
author = commit.author
if author:
contributors.add(author.login)
return sorted(list(contributors), key=str.lower)
def get_old_contributors(pulls: List[PullRequest], last_release: Optional[GitRelease]) -> Set[str]:
contributors = set()
if last_release:
merged_pulls = [pull for pull in pulls if pull.merged and pull.merged_at <= last_release.created_at]
for pull in merged_pulls:
pr_contributors = get_pr_contributors(pull)
for contributor in pr_contributors:
contributors.add(contributor)
return contributors
def get_new_contributors(old_contributors: List[str], merged_pulls: List[PullRequest]) -> List[str]:
new_contributors = set()
for pull in merged_pulls:
pr_contributors = get_pr_contributors(pull)
for contributor in pr_contributors:
if contributor not in old_contributors:
new_contributors.add(contributor)
return sorted(list(new_contributors), key=str.lower)
def get_last_release(releases: List[GitRelease]) -> Optional[GitRelease]:
sorted_releases = sorted(releases, key=lambda r: r.created_at, reverse=True)
if sorted_releases:
return sorted_releases[0]
return None
def multiple_contributors_mention_md(contributors: List[str]) -> str:
contrib_by = ''
if len(contributors) <= 1:
for contrib in contributors:
contrib_by += f'@{contrib}'
else:
for contrib in contributors:
contrib_by += f'@{contrib}, '
contrib_by = contrib_by[:-2]
last_comma = contrib_by.rfind(', ')
contrib_by = contrib_by[:last_comma].strip() + ' and ' + contrib_by[last_comma + 1 :].strip()
return contrib_by
def whats_changed_md(repo_full_name: str, merged_pulls: List[PullRequest]) -> List[str]:
whats_changed = []
for pull in merged_pulls:
contributors = get_pr_contributors(pull)
contrib_by = multiple_contributors_mention_md(contributors)
whats_changed.append(
f'* {pull.title} by {contrib_by} in https://github.com/{repo_full_name}/pull/{pull.number}'
)
return whats_changed
def get_first_contribution(merged_pulls: List[str], contributor: str) -> Optional[PullRequest]:
for pull in merged_pulls:
contrubutors = get_pr_contributors(pull)
if contributor in contrubutors:
return pull
# ? unreachable
return None
def new_contributors_md(repo_full_name: str, merged_pulls: List[PullRequest], new_contributors: List[str]) -> List[str]:
contributors_by_pr = {}
contributors_md = []
for contributor in new_contributors:
first_contrib = get_first_contribution(merged_pulls, contributor)
if not first_contrib:
continue
if first_contrib.number not in contributors_by_pr.keys():
contributors_by_pr[first_contrib.number] = [contributor]
else:
contributors_by_pr[first_contrib.number] += [contributor]
contributors_by_pr = dict(sorted(contributors_by_pr.items()))
for pr_number, contributors in contributors_by_pr.items():
contributors.sort(key=str.lower)
contrib_by = multiple_contributors_mention_md(contributors)
contributors_md.append(
f'* {contrib_by} made their first contribution in https://github.com/{repo_full_name}/pull/{pr_number}'
)
return contributors_md
def full_changelog_md(repository_name: str, last_tag_name: str, next_tag_name: str) -> Optional[str]:
if not last_tag_name:
return None
return f'**Full Changelog**: https://github.com/{repository_name}/compare/{last_tag_name}...{next_tag_name}'
def contruct_release_notes(repository: Repository, next_tag_name: str) -> str:
repo_name = repository.full_name
last_release = get_last_release(repository.get_releases())
all_pulls = repository.get_pulls(state='closed')
sorted_merged_pulls = get_sorted_merged_pulls(all_pulls, last_release)
old_contributors = get_old_contributors(all_pulls, last_release)
new_contributors = get_new_contributors(old_contributors, sorted_merged_pulls)
whats_changed = whats_changed_md(repo_name, sorted_merged_pulls)
new_contrib_md = new_contributors_md(repo_name, sorted_merged_pulls, new_contributors)
notes = "## What's changed\n"
for changes in whats_changed:
notes += changes + '\n'
notes += '\n'
if new_contributors:
notes += '## New Contributors\n'
for new_contributor in new_contrib_md:
notes += new_contributor + '\n'
notes += '\n'
if last_release:
notes += full_changelog_md(repository.full_name, last_release.title, next_tag_name)
return notes
|