-
-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
13 additions
and
343 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,201 +1,21 @@ | ||
#!/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 | ||
from github import Repository, GitRelease | ||
|
||
|
||
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]]: | ||
def create_release(repository: Repository, tag: str) -> GitRelease: | ||
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) | ||
|
||
return repository.create_git_release( | ||
tag=tag, name=tag, draft=False, prerelease=False, generate_release_notes=True | ||
) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters