def test_minor_bump(self): self.assertEqual(get_new_version('0.0.0', 'minor'), '0.1.0') self.assertEqual(get_new_version('1.2.0', 'minor'), '1.3.0') self.assertEqual(get_new_version('1.2.1', 'minor'), '1.3.0') self.assertEqual(get_new_version('10.1.0', 'minor'), '10.2.0')
async def main(draft, dry_run): token = os.environ["GH_TOKEN"] async with aiohttp.ClientSession(loop=asyncio.get_event_loop()) as session: gh = GitHubAPI(session, __name__, oauth_token=token) version_file = Path("version_number") current_version = version_file.read_text() tag_hash = str( git("rev-list", "-n", "1", f"v{current_version}").strip()) print("current_version:", current_version, "[" + tag_hash[:8] + "]") sha = git("rev-parse", "HEAD").strip() print("sha:", sha) repo = get_repo() print("repo:", repo) commits_iter = gh.getiter(f"/repos/{repo}/commits?sha={sha}") commits = [] try: async for item in commits_iter: commit_hash = item["sha"] commit_message = item["commit"]["message"] if commit_hash == tag_hash: break try: _default_parser(commit_message) # if this succeeds, do nothing except UnknownCommitMessageStyleError as err: print("Unkown commit message style:") print(commit_message) if sys.stdout.isatty() and click.confirm( "Edit effective message?"): commit_message = click.edit(commit_message) _default_parser(commit_message) commit = Commit(commit_hash, commit_message) commits.append(commit) print("-", commit) except gidgethub.BadRequest: print( "BadRequest for commit retrieval. That is most likely because you forgot to push the merge commit." ) return if len(commits) > 100: print(len(commits), "are a lot. Aborting!") sys.exit(1) bump = evaluate_version_bump(commits) print("bump:", bump) if bump is None: print("-> nothing to do") return next_version = get_new_version(current_version, bump) print("next version:", next_version) next_tag = f"v{next_version}" changes = generate_changelog(commits) md = markdown_changelog(next_version, changes, header=False) print(md) if not dry_run: version_file.write_text(next_version) git.add(version_file) git.commit(m=f"Bump to version {next_tag}") # git.tag(next_tag) target_hash = str(git("rev-parse", "HEAD")).strip() print("target_hash:", target_hash) git.push() commit_ok = False print("Waiting for commit", target_hash[:8], "to be received") for _ in range(10): try: url = f"/repos/{repo}/commits/{target_hash}" await gh.getitem(url) commit_ok = True break except InvalidField as e: print("Commit", target_hash[:8], "not received yet") pass # this is what we want await asyncio.sleep(0.5) if not commit_ok: print("Commit", target_hash[:8], "was not created on remote") sys.exit(1) print("Commit", target_hash[:8], "received") await gh.post( f"/repos/{repo}/releases", data={ "body": md, "tag_name": next_tag, "name": next_tag, "draft": draft, "target_commitish": target_hash, }, )
def test_major_bump(self): self.assertEqual(get_new_version('0.0.0', 'major'), '1.0.0') self.assertEqual(get_new_version('0.1.0', 'major'), '1.0.0') self.assertEqual(get_new_version('0.1.9', 'major'), '1.0.0') self.assertEqual(get_new_version('10.1.0', 'major'), '11.0.0')
async def pr_action( # token: str = typer.Argument(..., envvar="GH_TOKEN"), ): context = json.loads(os.environ["GITHUB_CONTEXT"]) repo = context["repository"] target_branch = context["event"]["pull_request"]["base"]["ref"] print("Target branch:", target_branch) sha = context["event"]["pull_request"]["head"]["sha"] print("Source hash:", sha) token = os.environ.get("GH_TOKEN", context["token"]) print(token) async with aiohttp.ClientSession(loop=asyncio.get_event_loop()) as session: gh = GitHubAPI(session, __name__, oauth_token=token) merge_commit_sha = await get_merge_commit_sha( context["event"]["pull_request"]["number"], repo, gh) print("Merge commit sha:", merge_commit_sha) # Get current version from target branch current_version = await get_release_branch_version( repo, target_branch, gh) tag_hash = await get_tag_hash(f"v{current_version}", repo, gh) print("current_version:", current_version, "[" + tag_hash[:8] + "]") commits, unparsed_commits = await get_parsed_commit_range( start=merge_commit_sha, end=tag_hash, repo=repo, gh=gh) bump = evaluate_version_bump(commits) print("bump:", bump) if bump is None: print("-> nothing to do") return next_version = get_new_version(current_version, bump) print("next version:", next_version) next_tag = f"v{next_version}" changes = generate_changelog(commits) md = markdown_changelog(next_version, changes, header=False) body = "" existing_release = await get_release(next_version, repo, gh) body += f"# `v{current_version}` -> `v{next_version}`" if existing_release is not None: if current_version == next_version: body += "## :no_entry_sign: Merging this will not result in a new version (no `fix`, `feat` or breaking changes). I recommend **delaying** this PR until more changes accumulate." else: body += f"## :warning: **WARNING: A release for {next_version} already exists [here]({existing_release['html_url']})** :warning:" body += "\n" body += ":no_entry_sign: I recommend to **NOT** merge this and double check the target branch!" if len(unparsed_commits) > 0: body += "\n" * 3 body += "## :warning: This PR contains commits which are not parseable:" for commit in unparsed_commits: body += f"\n - {commit.message} ({commit.sha})" body += "\n **Make sure these commits do not contain changes which affect the bump version!**" body += "\n\n" body += "\n" body += f"## Merging this PR will create a new release `v{next_version}`\n" body += "### Changelog" body += md print(body) title = f"Release: {current_version} -> {next_version}" await gh.post(context["event"]["pull_request"]["url"], data={ "body": body, "title": title })
def assert_releaseable_changes_detected(current_tagged_version): bump_string = evaluate_version_bump(current_tagged_version) assert bump_string, "semantic-release would not create a release!" return bump_string def commit_and_tag_pyproject_toml(bump_string, old_version, new_version): tag_message = f"Creating {bump_string} release. Bumping version number from {old_version} to {new_version}." commit_message = f"build(version): {tag_message}" tag_name = f"v{new_version}" repository = git.Repo(PROJECT_ROOT_PATH) repository.index.add([PYPROJECT_TOML_PATH]) repository.index.commit(commit_message) repository.create_tag(tag_name, tag_message) assert_clean_main() pyproject_toml = get_current_project_version() current_project_version = pyproject_toml["tool"]["poetry"]["version"] assert_current_project_version_semver(current_project_version) enable_semantic_release_logging() current_tagged_version = get_current_version_by_tag() bump_string = assert_releaseable_changes_detected(current_tagged_version) new_version = get_new_version(current_tagged_version, bump_string) print( f"Creating {bump_string} release! Bumping from version {current_tagged_version} to version {new_version}!" ) pyproject_toml["tool"]["poetry"]["version"] = new_version set_current_project_version(pyproject_toml) commit_and_tag_pyproject_toml(bump_string, current_tagged_version, new_version)
def test_major_bump(self): assert get_new_version("0.0.0", "major") == "1.0.0" assert get_new_version("0.1.0", "major") == "1.0.0" assert get_new_version("0.1.9", "major") == "1.0.0" assert get_new_version("10.1.0", "major") == "11.0.0"
def test_patch_bump(self): assert get_new_version("0.0.0", "patch") == "0.0.1" assert get_new_version("0.1.0", "patch") == "0.1.1" assert get_new_version("10.0.9", "patch") == "10.0.10"
def test_minor_bump(self): self.assertEqual(get_new_version("0.0.0", "minor"), "0.1.0") self.assertEqual(get_new_version("1.2.0", "minor"), "1.3.0") self.assertEqual(get_new_version("1.2.1", "minor"), "1.3.0") self.assertEqual(get_new_version("10.1.0", "minor"), "10.2.0")
def test_patch_bump(self): self.assertEqual(get_new_version("0.0.0", "patch"), "0.0.1") self.assertEqual(get_new_version("0.1.0", "patch"), "0.1.1") self.assertEqual(get_new_version("10.0.9", "patch"), "10.0.10")
def test_None_bump(self): self.assertEqual(get_new_version('1.0.0', None), '1.0.0')
def test_major_bump(self): self.assertEqual(get_new_version("0.0.0", "major"), "1.0.0") self.assertEqual(get_new_version("0.1.0", "major"), "1.0.0") self.assertEqual(get_new_version("0.1.9", "major"), "1.0.0") self.assertEqual(get_new_version("10.1.0", "major"), "11.0.0")
def test_patch_bump(self): self.assertEqual(get_new_version('0.0.0', 'patch'), '0.0.1') self.assertEqual(get_new_version('0.1.0', 'patch'), '0.1.1') self.assertEqual(get_new_version('10.0.9', 'patch'), '10.0.10')
def test_none_bump(self): self.assertEqual(get_new_version("1.0.0", None), "1.0.0")
def test_none_bump(self): self.assertEqual(get_new_version('1.0.0', None), '1.0.0')
async def make_release( token: str = typer.Argument(..., envvar="GH_TOKEN"), draft: bool = True, dry_run: bool = False, edit: bool = False, ): async with aiohttp.ClientSession(loop=asyncio.get_event_loop()) as session: gh = GitHubAPI(session, __name__, oauth_token=token) version_file = Path("version_number") current_version = version_file.read_text() tag_hash = str(git("rev-list", "-n", "1", f"v{current_version}").strip()) print("current_version:", current_version, "[" + tag_hash[:8] + "]") sha = git("rev-parse", "HEAD").strip() print("sha:", sha) repo = get_repo() print("repo:", repo) commits, _ = await get_parsed_commit_range( start=sha, end=tag_hash, repo=repo, gh=gh, edit=edit ) bump = evaluate_version_bump(commits) print("bump:", bump) if bump is None: print("-> nothing to do") return next_version = get_new_version(current_version, bump) print("next version:", next_version) next_tag = f"v{next_version}" changes = generate_changelog(commits) md = markdown_changelog(next_version, changes, header=False) print(md) if not dry_run: version_file.write_text(next_version) git.add(version_file) zenodo_file = Path(".zenodo.json") update_zenodo(zenodo_file, repo, next_version) git.add(zenodo_file) citation_file = Path("CITATION.cff") update_citation(citation_file, next_version) git.add(citation_file) git.commit(m=f"Bump to version {next_tag}") target_hash = str(git("rev-parse", "HEAD")).strip() print("target_hash:", target_hash) git.push() commit_ok = False print("Waiting for commit", target_hash[:8], "to be received") for _ in range(RETRY_COUNT): try: url = f"/repos/{repo}/commits/{target_hash}" await gh.getitem(url) commit_ok = True break except InvalidField as e: print("Commit", target_hash[:8], "not received yet") pass # this is what we want await asyncio.sleep(RETRY_INTERVAL) if not commit_ok: print("Commit", target_hash[:8], "was not created on remote") sys.exit(1) print("Commit", target_hash[:8], "received") await gh.post( f"/repos/{repo}/releases", data={ "body": md, "tag_name": next_tag, "name": next_tag, "draft": draft, "target_commitish": target_hash, }, )
def test_minor_bump(self): assert type(get_new_version("0.0.0", "minor")) is str assert get_new_version("0.0.0", "minor") == "0.1.0" assert get_new_version("1.2.0", "minor") == "1.3.0" assert get_new_version("1.2.1", "minor") == "1.3.0" assert get_new_version("10.1.0", "minor") == "10.2.0"
async def pr_action( fail: bool = False, pr: int = None, token: Optional[str] = typer.Option(None, envvar="GH_TOKEN"), repo: Optional[str] = typer.Option(None, envvar="GH_REPO"), ): print("::group::Information") context = os.environ.get("GITHUB_CONTEXT") if context is not None: context = json.loads(context) repo = context["repository"] token = context["token"] else: if token is None or repo is None: raise ValueError("No context, need token and repo") if pr is None: raise ValueError("No context, need explicit PR to run on") async with aiohttp.ClientSession(loop=asyncio.get_event_loop()) as session: gh = GitHubAPI(session, __name__, oauth_token=token) if pr is not None: pr = await gh.getitem(f"repos/{repo}/pulls/{pr}") else: pr = context["event"]["pull_request"] target_branch = pr["base"]["ref"] print("Target branch:", target_branch) sha = pr["head"]["sha"] print("Source hash:", sha) merge_commit_sha = await get_merge_commit_sha( pr["number"], repo, gh, ) print("Merge commit sha:", merge_commit_sha) # Get current version from target branch current_version = await get_release_branch_version(repo, target_branch, gh) tag_hash = await get_tag_hash(f"v{current_version}", repo, gh) print("current_version:", current_version, "[" + tag_hash[:8] + "]") commits, unparsed_commits = await get_parsed_commit_range( start=merge_commit_sha, end=tag_hash, repo=repo, gh=gh ) bump = evaluate_version_bump(commits) print("bump:", bump) next_version = get_new_version(current_version, bump) print("next version:", next_version) next_tag = f"v{next_version}" print("::endgroup::") changes = generate_changelog(commits) md = markdown_changelog(next_version, changes, header=False) body = "" title = f"Release: {current_version} -> {next_version}" existing_release = await get_release(next_tag, repo, gh) existing_tag = await get_tag(next_tag, repo, gh) body += f"# `v{current_version}` -> `v{next_version}`\n" exit_code = 0 if existing_release is not None or existing_tag is not None: if current_version == next_version: body += ( "## :no_entry_sign: Merging this will not result in a new version (no `fix`, " "`feat` or breaking changes). I recommend **delaying** this PR until more changes accumulate.\n" ) print("::warning::Merging this will not result in a new version") else: exit_code = 1 title = f":no_entry_sign: {title}" if existing_release is not None: body += f"## :warning: **WARNING**: A release for '{next_tag}' already exists" body += f"[here]({existing_release['html_url']})** :warning:" print(f"::error::A release for tag '{next_tag}' already exists") else: body += ( f"## :warning: **WARNING**: A tag '{next_tag}' already exists" ) print(f"::error::A tag '{next_tag}' already exists") body += "\n" body += ":no_entry_sign: I recommend to **NOT** merge this and double check the target branch!\n\n" else: body += f"## Merging this PR will create a new release `v{next_version}`\n" if len(unparsed_commits) > 0: body += "\n" * 3 body += "## :warning: This PR contains commits which are not parseable:" for commit in unparsed_commits: msg, _ = commit.message.split("\n", 1) body += f"\n - {msg} {commit.sha})" body += "\n **Make sure these commits do not contain changes which affect the bump version!**" body += "\n\n" body += "### Changelog" body += md print("::group::PR message") print(body) print("::endgroup::") await gh.post(pr["url"], data={"body": body, "title": title}) if fail: sys.exit(exit_code)
def test_none_bump(self): assert get_new_version("1.0.0", None) == "1.0.0"
def test_prerelease(self): assert get_new_version("1.0.1-beta.1", "1.0.0", None, True) == "1.0.1-beta.2" assert get_new_version("1.0.1-beta.1", "1.0.0", "major", True) == "2.0.0-beta.1" assert get_new_version("1.0.1-beta.1", "1.0.0", "minor", True) == "1.1.0-beta.1" assert get_new_version("1.0.1-beta.1", "1.0.0", "patch", True) == "1.0.1-beta.2" assert get_new_version("1.0.0", "1.0.0", None, True) == "1.0.1-beta.1" assert get_new_version("1.0.0", "1.0.0", "major", True) == "2.0.0-beta.1" assert get_new_version("1.0.0", "1.0.0", "minor", True) == "1.1.0-beta.1" assert get_new_version("1.0.0", "1.0.0", "patch", True) == "1.0.1-beta.1" assert get_new_version("0.9.0-beta.1", "1.0.0", None, True) == "1.0.1-beta.1" assert get_new_version("0.9.0-beta.1", "1.0.0", "major", True) == "2.0.0-beta.1" assert get_new_version("0.9.0-beta.1", "1.0.0", "minor", True) == "1.1.0-beta.1" assert get_new_version("0.9.0-beta.1", "1.0.0", "patch", True) == "1.0.1-beta.1" with pytest.raises(ValueError): get_new_version("0.9.0", "1.0.0", None, True) with pytest.raises(ValueError): get_new_version("1.0.0", "0.9.0", None, True)