Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simplify release generator #730

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions tools/release/create_release.py
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
190 changes: 5 additions & 185 deletions tools/release/lib/release.py
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:
anonrig marked this conversation as resolved.
Show resolved Hide resolved
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
146 changes: 0 additions & 146 deletions tools/release/lib/tests/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Loading