def build_entry(branch, repo, auth, changelog_path, resolve_backports): """Build a python version entry""" repo = repo or util.get_repo() branch = branch or util.get_branch() # Get the new version version = util.get_version() # Get the existing changelog and run some validation changelog = Path(changelog_path).read_text(encoding="utf-8") if START_MARKER not in changelog or END_MARKER not in changelog: raise ValueError("Missing insert marker for changelog") if changelog.find(START_MARKER) != changelog.rfind(START_MARKER): raise ValueError("Insert marker appears more than once in changelog") # Get changelog entry entry = get_version_entry( f"origin/{branch}", repo, version, auth=auth, resolve_backports=resolve_backports, ) changelog = insert_entry(changelog, entry, version=version) Path(changelog_path).write_text(changelog, encoding="utf-8") # Stage changelog util.run(f"git add {util.normalize_path(changelog_path)}")
def test_create_release_commit(py_package, build_mock): util.bump_version("0.0.2a0") version = util.get_version() util.run("python -m build .") shas = util.create_release_commit(version) assert util.normalize_path("dist/foo-0.0.2a0.tar.gz") in shas assert util.normalize_path("dist/foo-0.0.2a0-py3-none-any.whl") in shas
def test_get_changelog_version_entry(py_package, mocker): version = util.get_version() mocked_gen = mocker.patch( "jupyter_releaser.changelog.generate_activity_md") mocked_gen.return_value = testutil.CHANGELOG_ENTRY branch = "foo" resp = changelog.get_version_entry(branch, "bar/baz", version) mocked_gen.assert_called_with("bar/baz", since=None, kind="pr", branch=branch, heading_level=2, auth=None) assert f"## {version}" in resp assert testutil.PR_ENTRY in resp mocked_gen.return_value = testutil.CHANGELOG_ENTRY resp = changelog.get_version_entry(branch, "bar/baz", version, resolve_backports=True, auth="bizz") mocked_gen.assert_called_with( "bar/baz", since=None, kind="pr", branch=branch, heading_level=2, auth="bizz", ) assert f"## {version}" in resp assert testutil.PR_ENTRY in resp
def bump(force, spec): status = run("git status --porcelain").strip() if len(status) > 0: raise Exception("Must be in a clean git state with no untracked files") curr = parse_version(get_version()) if spec == 'next': spec = f"{curr.major}.{curr.minor}." if curr.pre: p, x = curr.pre spec += f"{curr.micro}{p}{x + 1}" else: spec += f"{curr.micro + 1}" version = parse_version(spec) # bump the Python package python_cmd = f"{TBUMP_CMD} {version}" run(python_cmd) # convert the Python version js_version = f"{version.major}.{version.minor}.{version.micro}" if version.pre: p, x = version.pre p = p.replace("a", "alpha").replace("b", "beta") js_version += f"-{p}.{x}" # bump the JS packages lerna_cmd = f"{LERNA_CMD} {js_version}" if force: lerna_cmd += " --yes" run(lerna_cmd)
def draft_changelog(version_spec, branch, repo, auth, dry_run): """Create a changelog entry PR""" repo = repo or util.get_repo() branch = branch or util.get_branch() version = util.get_version() tags = util.run("git --no-pager tag") if f"v{version}" in tags.splitlines(): raise ValueError(f"Tag v{version} already exists") # Check out any unstaged files from version bump util.run("git checkout -- .") title = f"{changelog.PR_PREFIX} for {version} on {branch}" commit_message = f'git commit -a -m "{title}"' body = title # Check for multiple versions if util.PACKAGE_JSON.exists(): body += npm.get_package_versions(version) body += '\n\nAfter merging this PR run the "Draft Release" Workflow with the following inputs' body += f""" | Input | Value | | ------------- | ------------- | | Target | {repo} | | Branch | {branch} | | Version Spec | {version_spec} | """ util.log(body) make_changelog_pr(auth, branch, repo, title, commit_message, body, dry_run=dry_run)
def check_entry(branch, repo, auth, changelog_path, since, resolve_backports, output): """Check changelog entry""" branch = branch or util.get_branch() # Get the new version version = util.get_version() # Finalize changelog changelog = Path(changelog_path).read_text(encoding="utf-8") start = changelog.find(START_MARKER) end = changelog.find(END_MARKER) if start == -1 or end == -1: # pragma: no cover raise ValueError("Missing new changelog entry delimiter(s)") if start != changelog.rfind(START_MARKER): # pragma: no cover raise ValueError("Insert marker appears more than once in changelog") final_entry = changelog[start + len(START_MARKER):end] repo = repo or util.get_repo() raw_entry = get_version_entry( f"origin/{branch}", repo, version, since=since, auth=auth, resolve_backports=resolve_backports, ) if f"# {version}" not in final_entry: # pragma: no cover util.log(final_entry) raise ValueError(f"Did not find entry for {version}") final_prs = re.findall(r"\[#(\d+)\]", final_entry) raw_prs = re.findall(r"\[#(\d+)\]", raw_entry) for pr in raw_prs: # Allow for changelog PR to not be in changelog itself skip = False for line in raw_entry.splitlines(): if f"[#{pr}]" in line and "changelog" in line.lower(): skip = True break if skip: continue if not f"[#{pr}]" in final_entry: # pragma: no cover raise ValueError(f"Missing PR #{pr} in changelog") for pr in final_prs: if not f"[#{pr}]" in raw_entry: # pragma: no cover raise ValueError( f"PR #{pr} does not belong in changelog for {version}") if output: Path(output).write_text(final_entry, encoding="utf-8")
def tag_release(dist_dir, no_git_tag_workspace): """Create release commit and tag""" # Get the new version version = util.get_version() # Create the release commit util.create_release_commit(version, dist_dir) # Create the annotated release tag tag_name = f"v{version}" util.run(f'git tag {tag_name} -a -m "Release {tag_name}"') # Create annotated release tags for workspace packages if given if not no_git_tag_workspace: npm.tag_workspace_packages()
def update(spec, force=False): prev = get_version() is_final = not is_prerelease(prev) if is_final and spec == "release": raise Exception( 'Use "major" or "minor" to switch back to alpha release') if is_final and spec == "build": raise Exception("Cannot increment a build on a final release") # If this is a major release during the alpha cycle, bump # just the Python version. if "a" in prev and spec == "major": run(f"bumpversion {spec}") return # Determine the version spec to use for lerna. lerna_version = "preminor" if spec == "build": lerna_version = "prerelease" # a -> b elif spec == "release" and "a" in prev: lerna_version = "prerelease --preid=beta" # b -> rc elif spec == "release" and "b" in prev: lerna_version = "prerelease --preid=rc" # rc -> final elif spec == "release" and "c" in prev: lerna_version = "patch" if lerna_version == "preminor": lerna_version += " --preid=alpha" cmd = f"jlpm run lerna version --force-publish --no-push --no-git-tag-version {lerna_version}" if force: cmd += " --yes" # For a preminor release, we bump 10 minor versions so that we do # not conflict with versions during minor releases of the top level package. if lerna_version == "preminor": for i in range(10): run(cmd) else: run(cmd) # Bump the version. run(f"bumpversion {spec} --allow-dirty")
def test_create_release_commit_hybrid(py_package, build_mock): # Add an npm package and test with that util.bump_version("0.0.2a0") version = util.get_version() testutil.create_npm_package(py_package) pkg_json = py_package / "package.json" data = json.loads(pkg_json.read_text(encoding="utf-8")) data["version"] = version pkg_json.write_text(json.dumps(data, indent=4), encoding="utf-8") txt = (py_package / "tbump.toml").read_text(encoding="utf-8") txt += testutil.TBUMP_NPM_TEMPLATE (py_package / "tbump.toml").write_text(txt, encoding="utf-8") util.run("python -m build .") shas = util.create_release_commit(version) assert len(shas) == 2 assert util.normalize_path("dist/foo-0.0.2a0.tar.gz") in shas
def bump(force, spec): status = run("git status --porcelain").strip() if len(status) > 0: raise Exception("Must be in a clean git state with no untracked files") # Make sure we have a valid version spec. if spec not in OPTIONS: raise ValueError(f"Version spec must be one of: {OPTIONS}") prev = get_version() is_final = not is_prerelease(prev) if spec == "next": spec = "patch" if is_final else "build" if spec == "patch": patch(force) return update(spec, force)
def patch(force=False): version = get_version() if is_prerelease(version): raise Exception("Can only make a patch release from a final version") run("bumpversion patch", quiet=True) # switches to alpha run("bumpversion release --allow-dirty", quiet=True) # switches to beta run("bumpversion release --allow-dirty", quiet=True) # switches to rc. run("bumpversion release --allow-dirty", quiet=True) # switches to final. # Version the changed cmd = "jlpm run lerna version patch --no-push --force-publish --no-git-tag-version" if force: cmd += " --yes" run(cmd)
def test_get_changelog_version_entry(py_package, mocker): version = util.get_version() mocked_gen = mocker.patch( "jupyter_releaser.changelog.generate_activity_md") mocked_gen.return_value = testutil.CHANGELOG_ENTRY branch = "foo" resp = changelog.get_version_entry(branch, "bar/baz", version) until = util.run( f'git --no-pager log -n 1 origin/{branch} --pretty=format:"%H"') until = until.replace("%", "") mocked_gen.assert_called_with( "bar/baz", since="v0.0.1", kind="pr", branch=branch, heading_level=2, auth=None, until=until, ) assert f"## {version}" in resp assert testutil.PR_ENTRY in resp mocked_gen.return_value = testutil.CHANGELOG_ENTRY resp = changelog.get_version_entry(branch, "bar/baz", version, resolve_backports=True, auth="bizz") mocked_gen.assert_called_with( "bar/baz", since="v0.0.1", until=until, kind="pr", branch=branch, heading_level=2, auth="bizz", ) assert f"## {version}" in resp assert testutil.PR_ENTRY in resp
def bump_version(version_spec, version_cmd): """Bump the version and verify new version""" util.bump_version(version_spec, version_cmd=version_cmd) version = util.get_version() # A properly parsed version will have a "major" attribute parsed = parse_version(version) if util.SETUP_PY.exists() and not hasattr(parsed, "major"): raise ValueError(f"Invalid version {version}") # Bail if tag already exists tag_name = f"v{version}" if tag_name in util.run("git --no-pager tag").splitlines(): msg = f"Tag {tag_name} already exists!" msg += " To delete run: `git push --delete origin {tag_name}`" raise ValueError(msg) return version
def test_get_version_python(py_package): assert util.get_version() == "0.0.1" util.bump_version("0.0.2a0") assert util.get_version() == "0.0.2a0"
def draft_release( ref, branch, repo, auth, changelog_path, version_cmd, dist_dir, dry_run, post_version_spec, assets, ): """Publish Draft GitHub release and handle post version bump""" branch = branch or util.get_branch() repo = repo or util.get_repo() assets = assets or glob(f"{dist_dir}/*") version = util.get_version() body = changelog.extract_current(changelog_path) prerelease = util.is_prerelease(version) # Bump to post version if given if post_version_spec: post_version = bump_version(post_version_spec, version_cmd) util.log(f"Bumped version to {post_version}") util.run(f'git commit -a -m "Bump to {post_version}"') if dry_run: return owner, repo_name = repo.split("/") gh = GhApi(owner=owner, repo=repo_name, token=auth) # Remove draft releases over a day old if bool(os.environ.get("GITHUB_ACTIONS")): for release in gh.repos.list_releases(): if str(release.draft).lower() == "false": continue created = release.created_at d_created = datetime.strptime(created, r"%Y-%m-%dT%H:%M:%SZ") delta = datetime.utcnow() - d_created if delta.days > 0: gh.repos.delete_release(release.id) remote_url = util.run("git config --get remote.origin.url") if not os.path.exists(remote_url): util.run(f"git push origin HEAD:{branch} --follow-tags --tags") util.log(f"Creating release for {version}") util.log(f"With assets: {assets}") release = gh.create_release( f"v{version}", branch, f"Release v{version}", body, True, prerelease, files=assets, ) # Set the GitHub action output util.actions_output("release_url", release.html_url)
def test_bump_version(npm_package, runner): runner(["prep-git", "--git-url", npm_package]) runner(["bump-version", "--version-spec", "1.0.1-rc0"]) os.chdir(util.CHECKOUT_NAME) version = util.get_version() assert version == "1.0.1-rc0"
def test_bump_version(py_package): for spec in ["1.0.1", "1.0.1.dev1", "1.0.3a4"]: util.bump_version(spec) assert util.get_version() == spec
def test_get_version_npm(npm_package): assert util.get_version() == "1.0.0" npm = util.normalize_path(shutil.which("npm")) run(f"{npm} version patch") assert util.get_version() == "1.0.1"