diff --git a/tools/release/create_release.py b/tools/release/create_release.py index de162081f..a34ebcca9 100755 --- a/tools/release/create_release.py +++ b/tools/release/create_release.py @@ -1,25 +1,21 @@ #!/usr/bin/env python3 -import os +from os import environ, path from github import Github -import lib.release as release +from lib.release import create_release -WORK_DIR = os.path.dirname(os.path.abspath(__file__)).replace('/tools/release', '') +WORK_DIR = path.dirname(path.abspath(__file__)).replace('/tools/release', '') -NEXT_TAG = os.environ['NEXT_RELEASE_TAG'] -REPO_NAME = os.environ['GITHUB_REPOSITORY'] -TOKEN = os.environ['GITHUB_TOKEN'] +NEXT_TAG = environ.get('NEXT_RELEASE_TAG', None) +REPO_NAME = environ.get('GITHUB_REPOSITORY', None) +TOKEN = environ.get('GITHUB_TOKEN', None) if not NEXT_TAG or not REPO_NAME or not TOKEN: raise Exception('Bad environment variables. Invalid GITHUB_REPOSITORY, GITHUB_TOKEN or NEXT_RELEASE_TAG') g = Github(TOKEN) -repo = g.get_repo(REPO_NAME) +repository = g.get_repo(REPO_NAME) -release_notes = release.contruct_release_notes(repo, NEXT_TAG) - -release.create_release(repo, NEXT_TAG, release_notes) - -release = repo.get_release(NEXT_TAG) +release = create_release(repository, NEXT_TAG) release.upload_asset('singleheader/ada.cpp') release.upload_asset('singleheader/ada.h') release.upload_asset('singleheader/ada_c.h') diff --git a/tools/release/lib/release.py b/tools/release/lib/release.py index f535693e2..60795dc53 100644 --- a/tools/release/lib/release.py +++ b/tools/release/lib/release.py @@ -1,10 +1,7 @@ #!/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: @@ -12,190 +9,13 @@ def is_valid_tag(tag: str) -> bool: 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 diff --git a/tools/release/lib/tests/test_release.py b/tools/release/lib/tests/test_release.py index cb96bdf4e..42abfc2d0 100644 --- a/tools/release/lib/tests/test_release.py +++ b/tools/release/lib/tests/test_release.py @@ -368,141 +368,6 @@ def get_pulls(state: str = 'closed') -> list[PullRequest]: ) -def test_get_sorted_merged_pulls() -> None: - pulls = RepoStub.get_pulls(state='closed') - last_release = None - - sorted_merged_pulls = release.get_sorted_merged_pulls(pulls, last_release) - - # Should return all the merged pull requests since there is no previous release - assert sorted_merged_pulls == 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, - ) - - -def test_get_last_release() -> None: - releases = RepoStub.get_releases() - - # Should return the latest release - last_release = release.get_last_release(releases) - assert last_release.created_at == datetime(2023, 4, 1) - - # Should return None (in case there are no releases yet) - last_release = release.get_last_release([]) - assert last_release is None - - -def test_get_old_contributors() -> None: - last_release = release.get_last_release(RepoStub.get_releases()) - - old_contributors = release.get_old_contributors(RepoStub.get_pulls(), last_release) - - # Should return contributors until last release, including co-authors - assert old_contributors == { - 'contributor_2', - 'contributor_3', - 'contributor_4', - 'old_contrib_coauthor', - 'old_contrib_coauthor2', - } - - -def test_get_new_contributors() -> None: - last_release = release.get_last_release(RepoStub.get_releases()) - all_pulls = RepoStub.get_pulls() - - # merged pulls after last release - merged_pulls = release.get_sorted_merged_pulls(all_pulls, last_release) - old_contributors = release.get_old_contributors(all_pulls, last_release) - - # Should return a List sorted in alphabetic order with only the new contributors since - # last release - new_contributors = release.get_new_contributors(old_contributors, merged_pulls) - - assert new_contributors == [ - 'new_contributor_1', - 'new_contributor_2', - 'new_contributor_coauthor1', - 'new_contributor_coauthor2', - 'new_contributor_coauthor3', - 'new_contributor_coauthor4', - ] - - -def test_whats_changed_md() -> None: - repo_stub = RepoStub() - last_release = release.get_last_release(RepoStub.get_releases()) - all_pulls = RepoStub.get_pulls() - # merged pulls after last release - merged_pulls = release.get_sorted_merged_pulls(all_pulls, last_release) - - whats_changed = release.whats_changed_md(repo_stub.full_name, merged_pulls) - - assert whats_changed == [ - '* Feature 8 by @new_contributor_1, @new_contributor_coauthor3 and @new_contributor_coauthor4 in https://github.com/ada-url/ada/pull/15', - '* Feature 9 by @new_contributor_2 and @new_contributor_coauthor1 in https://github.com/ada-url/ada/pull/13', - '* Feature 7 by @contributor_3 and @new_contributor_coauthor2 in https://github.com/ada-url/ada/pull/14', - ] - - -def test_new_contributors_md() -> None: - repo_stub = RepoStub() - last_release = release.get_last_release(RepoStub.get_releases()) - all_pulls = RepoStub.get_pulls() - - merged_pulls = release.get_sorted_merged_pulls(all_pulls, last_release) - old_contributors = release.get_old_contributors(all_pulls, last_release) - new_contributors = release.get_new_contributors(old_contributors, merged_pulls) - - # Should return a markdown containing the new contributors and their first contribution - new_contributors_md = release.new_contributors_md(repo_stub.full_name, merged_pulls, new_contributors) - - assert new_contributors_md == [ - '* @new_contributor_2 and @new_contributor_coauthor1 made their first contribution in https://github.com/ada-url/ada/pull/13', - '* @new_contributor_coauthor2 made their first contribution in https://github.com/ada-url/ada/pull/14', - '* @new_contributor_1, @new_contributor_coauthor3 and @new_contributor_coauthor4 made their first contribution in https://github.com/ada-url/ada/pull/15', # noqa: E501 - ] - - -def test_full_changelog_md() -> None: - repo_stub = RepoStub() - last_tag = release.get_last_release(repo_stub.get_releases()) - - full_changelog = release.full_changelog_md(repo_stub.full_name, last_tag.title, 'v3.0.0') - assert full_changelog == '**Full Changelog**: https://github.com/ada-url/ada/compare/v1.0.3...v3.0.0' - - full_changelog = release.full_changelog_md(repo_stub.full_name, None, 'v3.0.0') - assert full_changelog is None - - -def test_contruct_release_notes() -> None: - repo_stub = RepoStub() - - notes = release.contruct_release_notes(repo_stub, 'v3.0.0') - assert ( - notes - == "## What's changed\n" - + '* Feature 8 by @new_contributor_1, @new_contributor_coauthor3 and @new_contributor_coauthor4 in https://github.com/ada-url/ada/pull/15\n' - + '* Feature 9 by @new_contributor_2 and @new_contributor_coauthor1 in https://github.com/ada-url/ada/pull/13\n' - + '* Feature 7 by @contributor_3 and @new_contributor_coauthor2 in https://github.com/ada-url/ada/pull/14\n' - + '\n' - + '## New Contributors\n' - + '* @new_contributor_2 and @new_contributor_coauthor1 made their first contribution in https://github.com/ada-url/ada/pull/13\n' - + '* @new_contributor_coauthor2 made their first contribution in https://github.com/ada-url/ada/pull/14\n' - + '* @new_contributor_1, @new_contributor_coauthor3 and @new_contributor_coauthor4 made their first contribution in https://github.com/ada-url/ada/pull/15\n' # noqa: E501 - + '\n' - + '**Full Changelog**: https://github.com/ada-url/ada/compare/v1.0.3...v3.0.0' - ) - - def test_is_valid_tag() -> None: assert release.is_valid_tag('v1.0.0') is True assert release.is_valid_tag('v1.1.1') is True @@ -511,14 +376,3 @@ def test_is_valid_tag() -> None: assert release.is_valid_tag('v1.0.0.0') is False assert release.is_valid_tag('1.0.0') is False assert release.is_valid_tag('1.0.1') is False - - -def test_multiple_contributors_mention_md() -> None: - contributors = ['contrib1', 'contrib2', 'contrib3', 'contrib4'] - - md_contributors_mention = release.multiple_contributors_mention_md(contributors) - assert md_contributors_mention == '@contrib1, @contrib2, @contrib3 and @contrib4' - - contributors = ['contrib1'] - md_contributors_mention = release.multiple_contributors_mention_md(contributors) - assert md_contributors_mention == '@contrib1'