mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 10:19:02 +00:00
Add dev command to bump_version and make its interface explicit
bump_version could previously only create releases. It now provides two
subcommands, and the mutually exclusive --major/--minor/--patch flags (as
well as the explicit --version option) are replaced by a positional
argument, which cannot be misused:
bump_version.py release <current|major|minor|patch>
bump_version.py dev <major|minor|patch>
`dev` bumps the version to a new .dev0 version and commits it as
"Set version to vX" without creating a tag or touching the changelog. This
allows work on main to target a new minor/major version independently of a
release.
Because a .dev0 version no longer necessarily reserves the next patch
version, `release` now takes the target "current", which releases the
version reserved by the current .dev version (the usual case), whereas
major/minor/patch bump beyond it. The former special case of not
incrementing the patch version is thereby removed; `release patch` now
increments the patch version as its name suggests.
`release current` fails if the current version is not a development
version, in which case there is no reserved version to release.
This commit is contained in:
1 parent
18fa47bfcc
commit
6da423c70a
2 files changed
+124
-89
No files matched your search
+27
-6
@@ -8,12 +8,18 @@ and tools for formatting and type checking.
|
||||
## Release Process
|
||||
|
||||
1. Ensure clean git status.
|
||||
2. Set the version for release, e.g.
|
||||
|
||||
python scripts/bump_version.py --patch
|
||||
python scripts/bump_version.py --minor
|
||||
2. Set the version for release. Normally, the version to be released is the one already reserved by the
|
||||
current `.dev0` version (e.g. `1.8.0` when the repository is at `1.8.0.dev0`):
|
||||
|
||||
This also creates the git tag.
|
||||
python scripts/bump_version.py release current
|
||||
|
||||
To release a version beyond the reserved one, name the part to bump instead, e.g.
|
||||
|
||||
python scripts/bump_version.py release patch
|
||||
python scripts/bump_version.py release minor
|
||||
|
||||
This updates `CHANGELOG.md`, commits the release version, creates the git tag, and then
|
||||
commits the subsequent `.dev0` version for the next iteration.
|
||||
3. Push to GitHub:
|
||||
|
||||
git push
|
||||
@@ -26,4 +32,19 @@ and tools for formatting and type checking.
|
||||
[GitHub Releases page](https://github.com/oraios/serena/releases).
|
||||
When ready, publish it (click *Publish release*).
|
||||
This triggers the `publish` workflow, which builds and publishes the
|
||||
package to PyPI.
|
||||
package to PyPI.
|
||||
|
||||
### Bumping the Development Version
|
||||
|
||||
Independently of a release, the development version can be bumped, e.g. when work on `main`
|
||||
begins to target a new minor or major version:
|
||||
|
||||
python scripts/bump_version.py dev minor
|
||||
python scripts/bump_version.py dev major
|
||||
|
||||
This sets the version to the respective new `.dev0` version (e.g. `1.8.0.dev0`) and commits it as
|
||||
"Set version to vX"; it creates no tag and does not modify `CHANGELOG.md`.
|
||||
|
||||
The subsequent release of that version is then performed with `release current`.
|
||||
|
||||
Both commands require a clean git status and support `--dry-run` to preview the changes.
|
||||
+97
-83
@@ -17,98 +17,110 @@ from serena.util.git import get_git_status
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
VersionPart = Literal["major", "minor", "patch"]
|
||||
#: a version part to bump or, in the case of "current", the version already reserved by the current .dev version
|
||||
VersionTarget = Literal["major", "minor", "patch", "current"]
|
||||
_VERSION_PATTERN = re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.\w+)?$")
|
||||
_INIT_VERSION_PATTERN = re.compile(r'^(?P<before>__version__\s*=\s*")(?P<version>\d+\.\d+\.\d+(?:\.\w+)?)(?P<after>"\s*)$', re.MULTILINE)
|
||||
_PYPROJECT_VERSION_PATTERN = re.compile(
|
||||
r'(?m)^(?P<before>\[project\]\n(?:.*\n)*?^version\s*=\s*")(?P<version>\d+\.\d+\.\d+(?:\.\w+)?)(?P<after>"\s*)$'
|
||||
)
|
||||
_VERSION_SUFFIX_PATTERN = re.compile(r"^\d+\.\d+\.\d+\.(?P<suffix>\w+)$")
|
||||
_UNRELEASED_HEADER = "# Unreleased (main)\n"
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--major", "major", is_flag=True, help="Bump the major version and reset minor and patch to 0.")
|
||||
@click.option("--minor", "minor", is_flag=True, help="Bump the minor version and reset patch to 0.")
|
||||
@click.option("--patch", "patch", is_flag=True, help="Bump the patch version.")
|
||||
@click.option("--version", "-v", "target_version", metavar="X.Y.Z", help="Set an explicit version instead of bumping.")
|
||||
@click.option("--dry-run", is_flag=True, help="Show what would change without writing any files.")
|
||||
def bump_version(major: bool, minor: bool, patch: bool, target_version: str | None, dry_run: bool) -> None:
|
||||
git_status = get_git_status()
|
||||
if not git_status.is_clean:
|
||||
raise click.ClickException("Working directory is not clean. Please commit or stash your changes first.")
|
||||
_version_target_argument = click.argument("version_target", type=click.Choice(["current", "major", "minor", "patch"]))
|
||||
_version_part_argument = click.argument("version_part", type=click.Choice(["major", "minor", "patch"]))
|
||||
_dry_run_option = click.option("--dry-run", is_flag=True, help="Show what would change without writing any files.")
|
||||
|
||||
log.info("bump_version called: major=%s, minor=%s, patch=%s, target_version=%s", major, minor, patch, target_version)
|
||||
|
||||
# determine part to bump
|
||||
version_part = resolve_version_selection(major=major, minor=minor, patch=patch, target_version=target_version)
|
||||
log.info("Resolved version_part=%s", version_part)
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""Manages the Serena version."""
|
||||
|
||||
|
||||
@cli.command()
|
||||
@_version_target_argument
|
||||
@_dry_run_option
|
||||
def release(version_target: VersionTarget, dry_run: bool) -> None:
|
||||
"""Bumps the version for a release and starts the next dev iteration.
|
||||
|
||||
Bumps the version, updates the changelog, commits and tags the release, and then commits
|
||||
the subsequent .dev0 version.
|
||||
|
||||
VERSION_TARGET is either "current", releasing the version already reserved by the current .dev version
|
||||
(the usual case), or the part of the version to bump beyond it (major, minor or patch).
|
||||
"""
|
||||
require_clean_working_directory()
|
||||
log.info("release called: version_target=%s", version_target)
|
||||
|
||||
# bump it (never incrementing patch because it was already updated with the last .dev version)
|
||||
repo_root = find_repo_root()
|
||||
log.info("Repo root: %s", repo_root)
|
||||
new_version = bump_repo_version(
|
||||
repo_root, version_part=version_part, target_version=target_version, dry_run=dry_run, increment_patch=False
|
||||
)
|
||||
log.info("New version: %s", new_version)
|
||||
|
||||
# commit and tag for new version
|
||||
# bump to the release version
|
||||
new_version = bump_repo_version(repo_root, version_target=version_target, dry_run=dry_run)
|
||||
log.info("New version: %s", new_version)
|
||||
if dry_run:
|
||||
click.echo(f"Dry run complete. Version would be bumped to {new_version}")
|
||||
return
|
||||
else:
|
||||
os.system("uv lock")
|
||||
click.echo(f"Bumped version to {new_version}")
|
||||
os.system("git add -u")
|
||||
os.system(f'git commit -m "Release v{new_version}"')
|
||||
os.system(f"git tag v{new_version}")
|
||||
|
||||
# bump patch and add suffix for next dev iteration
|
||||
new_snapshot_version = bump_repo_version(
|
||||
repo_root,
|
||||
version_part="patch",
|
||||
target_version=None,
|
||||
dry_run=dry_run,
|
||||
target_version_suffix=".dev0",
|
||||
increment_patch=True,
|
||||
)
|
||||
# commit and tag the release version
|
||||
commit_version_change(new_version, message=f"Release v{new_version}")
|
||||
os.system(f"git tag v{new_version}")
|
||||
|
||||
# bump patch and add the suffix for the next dev iteration
|
||||
new_snapshot_version = bump_repo_version(repo_root, version_target="patch", dry_run=dry_run, target_version_suffix=".dev0")
|
||||
log.info("New snapshot version: %s", new_snapshot_version)
|
||||
commit_version_change(new_snapshot_version, message=f"Set version to v{new_snapshot_version}")
|
||||
|
||||
# commit the new snapshot version
|
||||
|
||||
@cli.command()
|
||||
@_version_part_argument
|
||||
@_dry_run_option
|
||||
def dev(version_part: VersionPart, dry_run: bool) -> None:
|
||||
"""Bumps the development version without creating a release.
|
||||
|
||||
Sets the version to a new .dev0 version and commits it; no tag is created and the changelog
|
||||
is not modified.
|
||||
|
||||
VERSION_PART is the part of the version to bump (major, minor or patch).
|
||||
"""
|
||||
require_clean_working_directory()
|
||||
log.info("dev called: version_part=%s", version_part)
|
||||
|
||||
repo_root = find_repo_root()
|
||||
log.info("Repo root: %s", repo_root)
|
||||
|
||||
new_version = bump_repo_version(repo_root, version_target=version_part, dry_run=dry_run, target_version_suffix=".dev0")
|
||||
log.info("New version: %s", new_version)
|
||||
if dry_run:
|
||||
click.echo(f"Dry run complete. Version would be bumped to {new_version}")
|
||||
return
|
||||
|
||||
commit_version_change(new_version, message=f"Set version to v{new_version}")
|
||||
|
||||
|
||||
def require_clean_working_directory() -> None:
|
||||
if not get_git_status().is_clean:
|
||||
raise click.ClickException("Working directory is not clean. Please commit or stash your changes first.")
|
||||
|
||||
|
||||
def commit_version_change(new_version: str, *, message: str) -> None:
|
||||
os.system("uv lock")
|
||||
click.echo(f"Bumped version to {new_snapshot_version}")
|
||||
click.echo(f"Bumped version to {new_version}")
|
||||
os.system("git add -u")
|
||||
os.system(f'git commit -m "Set version to v{new_snapshot_version}"')
|
||||
os.system(f'git commit -m "{message}"')
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
return Path(REPO_ROOT)
|
||||
|
||||
|
||||
def resolve_version_selection(*, major: bool, minor: bool, patch: bool, target_version: str | None) -> VersionPart | None:
|
||||
bump_flags_selected = sum([major, minor, patch])
|
||||
if target_version is not None and bump_flags_selected > 0:
|
||||
raise click.ClickException("Use either --version or one of --major/--minor/--patch, not both.")
|
||||
if bump_flags_selected > 1:
|
||||
raise click.ClickException("Use only one of --major, --minor, or --patch.")
|
||||
if target_version is not None:
|
||||
validate_version_string(target_version)
|
||||
return None
|
||||
if major:
|
||||
return "major"
|
||||
if minor:
|
||||
return "minor"
|
||||
if patch:
|
||||
return "patch"
|
||||
raise click.ClickException("No version bump selected. Use --major, --minor, --patch or --version.")
|
||||
|
||||
|
||||
def bump_repo_version(
|
||||
repo_root: Path,
|
||||
*,
|
||||
version_part: VersionPart | None,
|
||||
target_version: str | None,
|
||||
version_target: VersionTarget,
|
||||
dry_run: bool = False,
|
||||
target_version_suffix: str | None = None,
|
||||
increment_patch: bool = True,
|
||||
) -> str:
|
||||
pyproject_path = repo_root / "pyproject.toml"
|
||||
init_path = repo_root / "src" / "serena" / "__init__.py"
|
||||
@@ -130,12 +142,12 @@ def bump_repo_version(
|
||||
f"Version mismatch between pyproject.toml and src/serena/__init__.py: {current_version} != {init_version}"
|
||||
)
|
||||
|
||||
if target_version is not None:
|
||||
new_version = validate_version_string(target_version)
|
||||
else:
|
||||
if version_part is None:
|
||||
raise click.ClickException("No version target specified.")
|
||||
new_version = increment_version(current_version, version_part, increment_patch=increment_patch)
|
||||
if version_target == "current" and _VERSION_SUFFIX_PATTERN.search(current_version) is None:
|
||||
raise click.ClickException(
|
||||
f"The current version {current_version} is not a development version, so there is no reserved version to release. "
|
||||
f"Use major, minor or patch to bump the version instead."
|
||||
)
|
||||
new_version = increment_version(current_version, version_target)
|
||||
if target_version_suffix is not None:
|
||||
new_version += target_version_suffix
|
||||
log.info("New version will be: %s", new_version)
|
||||
@@ -199,7 +211,14 @@ def replace_version(text: str, pattern: re.Pattern[str], new_version: str, file_
|
||||
return f"{text[: match.start('version')]}{new_version}{text[match.end('version') :]}"
|
||||
|
||||
|
||||
def increment_version(version: str, version_part: VersionPart, increment_patch: bool) -> str:
|
||||
def increment_version(version: str, version_target: VersionTarget) -> str:
|
||||
"""
|
||||
Computes the new version, dropping any development suffix of the given version.
|
||||
|
||||
:param version: the current version
|
||||
:param version_target: the part of the version to bump or "current" to keep the version as is
|
||||
:return: the new version
|
||||
"""
|
||||
match = _VERSION_PATTERN.fullmatch(version)
|
||||
if match is None:
|
||||
raise click.ClickException(f"Unsupported version format: {version}")
|
||||
@@ -208,22 +227,17 @@ def increment_version(version: str, version_part: VersionPart, increment_patch:
|
||||
minor = int(match.group("minor"))
|
||||
patch = int(match.group("patch"))
|
||||
|
||||
if version_part == "major":
|
||||
return f"{major + 1}.0.0"
|
||||
if version_part == "minor":
|
||||
return f"{major}.{minor + 1}.0"
|
||||
elif version_part == "patch":
|
||||
if increment_patch:
|
||||
patch += 1
|
||||
return f"{major}.{minor}.{patch}"
|
||||
else:
|
||||
raise ValueError(version_part)
|
||||
|
||||
|
||||
def validate_version_string(version: str) -> str:
|
||||
if _VERSION_PATTERN.fullmatch(version) is None:
|
||||
raise click.ClickException(f"Unsupported version format: {version}")
|
||||
return version
|
||||
match version_target:
|
||||
case "major":
|
||||
return f"{major + 1}.0.0"
|
||||
case "minor":
|
||||
return f"{major}.{minor + 1}.0"
|
||||
case "patch":
|
||||
return f"{major}.{minor}.{patch + 1}"
|
||||
case "current":
|
||||
return f"{major}.{minor}.{patch}"
|
||||
case _:
|
||||
raise ValueError(version_target)
|
||||
|
||||
|
||||
def update_changelog(changelog_text: str, new_version: str) -> str:
|
||||
@@ -278,4 +292,4 @@ def split_unreleased_body(unreleased_body: str) -> tuple[str, str]:
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(name)s: %(message)s")
|
||||
log.info("Script starting")
|
||||
bump_version()
|
||||
cli()
|
||||
Reference in new issue
Block a user