Skip to content

Commit

Permalink
fix: correct version --prerelease use & enable --as-prerelease (#647
Browse files Browse the repository at this point in the history
)

* test(version): add validation of `--as-prerelease` and `--prerelease opts`

* fix(version-cmd): correct `--prerelease` use

  Prior to this change, `--prerelease` performed the role of converting whichever forced
version into a prerelease version declaration, which was an unintentional breaking
change to the CLI compared to v7.

  `--prerelease` now forces the next version to increment the prerelease revision,
which makes it consistent with `--patch`, `--minor` and `--major`. Temporarily disabled
the ability to force a prerelease.

  Resolves: #639

* feat(version-cmd): add `--as-prerelease` option to force the next version to be a prerelease

  Prior to this change, `--prerelease` performed the role that `--as-prerelease` now does,
which was an unintentional breaking change to the CLI compared to v7.

  `--prerelease` is used to force the next version to increment the prerelease revision,
which makes it consistent with `--patch`, `--minor` and `--major`, while `--as-prerelease`
forces for the next version to be converted to a prerelease version type before it is
applied to the project regardless of the bump level.

  Resolves: #639

* docs(commands): update version command options definition about prereleases

---------

Co-authored-by: codejedi365 <codejedi365@gmail.com>
  • Loading branch information
bernardcooke53 and codejedi365 committed Apr 27, 2024
1 parent 34226d2 commit 2acb5ac
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 95 deletions.
99 changes: 85 additions & 14 deletions docs/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,39 +173,110 @@ number (``1.0.0``).

.. _cmd-version-option-force-level:

``--major/--minor/--patch``
***************************
``--major/--minor/--patch/--prerelease``
****************************************

Force the next version to increment the major, minor or patch digit, respectively.
These flags are optional but mutually exclusive, so only one may be supplied, or none at all.
Using these flags overrides the usual calculation for the next version; this can be useful, say,
when a project wants to release its initial 1.0.0 version.
Force the next version to increment the major, minor or patch digits, or the prerelease revision,
respectively. These flags are optional but mutually exclusive, so only one may be supplied, or
none at all. Using these flags overrides the usual calculation for the next version; this can
be useful, say, when a project wants to release its initial 1.0.0 version.

.. warning::
Using these flags will override the value of prerelease, **regardless of your configuration or the
current version**. To produce a prerelease with the appropriate digit incremented you should also
supply the :ref:`cmd-version-option-prerelease` flag. If you do not, using these flags will force

Using these flags will override the configured value of ``prerelease`` (configured
in your :ref:`Release Group<multibranch-releases-configuring>`),
**regardless of your configuration or the current version**.

To produce a prerelease with the appropriate digit incremented you should also
supply the :ref:`cmd-version-option-as-prerelease` flag. If you do not, using these flags will force
a full (non-prerelease) version to be created.

For example, suppose your project's current version is ``0.2.1-rc.1``. The following
shows how these options can be combined with ``--as-prerelease`` to force different
versions:

.. code-block:: bash
semantic-release version --prerelease --print
# 0.2.1-rc.2
semantic-release version --patch --print
# 0.2.2
semantic-release version --minor --print
# 0.3.0
semantic-release version --major --print
# 1.0.0
semantic-release version --minor --as-prerelease --print
# 0.3.0-rc.1
semantic-release version --prerelease --as-prerelease --print
# 0.2.1-rc.2
These options are forceful overrides, but there is no action required for subsequent releases
performed using the usual calculation algorithm.

Supplying ``--prerelease`` will cause Python Semantic Release to scan your project history
for any previous prereleases with the same major, minor and patch versions as the latest
version and the same :ref:`prerelease token<cmd-version-option-prerelease-token>` as the
one passed by command-line or configuration. If one is not found, ``--prerelease`` will
produce the next version according to the following format:

.. code-block:: python
f"{latest_version.major}.{latest_version.minor}.{latest_version.patch}-{prerelease_token}.1"
However, if Python Semantic Release identifies a previous *prerelease* version with the same
major, minor and patch digits as the latest version, *and* the same prerelease token as the
one supplied by command-line or configuration, then Python Semantic Release will increment
the revision found on that previous prerelease version in its new version.

For example, if ``"0.2.1-rc.1"`` and already exists as a previous version, and the latest version
is ``"0.2.1"``, invoking the following command will produce ``"0.2.1-rc.2"``:

.. code-block:: bash
semantic-release version --prerelease --prerelease-token "rc" --print
.. warning::

This is true irrespective of the branch from which ``"0.2.1-rc.1"`` was released from.
The check for previous prereleases "leading up to" this normal version is intended to
help prevent collisions in git tags to an extent, but isn't foolproof. As the example
shows it is possible to release a prerelease for a normal version that's already been
released when using this flag, which would in turn be ignored by tools selecting
versions by `SemVer precedence rules`_.


.. _SemVer precedence rules: https://semver.org/#spec-item-11


.. seealso::
- :ref:`configuration`
- :ref:`config-branches`

.. _cmd-version-option-prerelease:
.. _cmd-version-option-as-prerelease:

``--prerelease``
****************
``--as-prerelease``
*******************

Force the next version to be a prerelease. As with :ref:`cmd-version-option-force-level`, this option
After performing the normal calculation of the next version, convert the resulting next version
to a prerelease before applying it. As with :ref:`cmd-version-option-force-level`, this option
is a forceful override, but no action is required to resume calculating versions as normal on the
subsequent releases.
subsequent releases. The main distinction between ``--prerelease`` and ``--as-prerelease`` is that
the latter will not *force* a new version if one would not have been released without supplying
the flag.

This can be useful when making a single prerelease on a branch that would typically release
normal versions.

If not specified in :ref:`cmd-version-option-prerelease-token`, the prerelease token is idenitified using the
:ref:`Multibranch Release Configuration <multibranch-releases-configuring>`

See the examples alongside :ref:`cmd-version-option-force-level` for how to use this flag.

.. _cmd-version-option-prerelease-token:

``--prerelease-token [VALUE]``
Expand Down
2 changes: 1 addition & 1 deletion docs/multibranch_releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,5 @@ This would lead to versions such as ``1.1.1+main.20221127`` or ``2.0.0-rc.4+2.x.

.. note::
Remember that is always possible to override the release rules configured by using
the :ref:`cmd-version-option-force-level` and :ref:`cmd-version-option-prerelease`
the :ref:`cmd-version-option-force-level` and :ref:`cmd-version-option-as-prerelease`
flags.
101 changes: 74 additions & 27 deletions semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@


def is_forced_prerelease(
force_prerelease: bool, force_level: str | None, prerelease: bool
as_prerelease: bool, forced_level_bump: LevelBump | None, prerelease: bool
) -> bool:
"""
Determine if this release is forced to have prerelease on/off.
Expand All @@ -51,8 +51,17 @@ def is_forced_prerelease(
it's False.
Otherwise (``force_level is None``) use the value of ``prerelease``
"""
log.debug(", ".join(f"{k} = {v}" for k, v in locals().items()))
return force_prerelease or ((force_level is None) and prerelease)
local_vars = list(locals().items())
log.debug(
"%s: %s",
is_forced_prerelease.__name__,
", ".join(f"{k} = {v}" for k, v in local_vars),
)
return (
as_prerelease
or forced_level_bump is LevelBump.PRERELEASE_REVISION
or ((forced_level_bump is None) and prerelease)
)


def last_released(
Expand All @@ -63,16 +72,41 @@ def last_released(


def version_from_forced_level(
repo: Repo, level_bump: LevelBump, translator: VersionTranslator
repo: Repo, forced_level_bump: LevelBump, translator: VersionTranslator
) -> Version:
ts_and_vs = tags_and_versions(repo.tags, translator)

# If we have no tags, return the default version
if not ts_and_vs:
return Version.parse(DEFAULT_VERSION).bump(level_bump)
return Version.parse(DEFAULT_VERSION).bump(forced_level_bump)

_, latest_version = ts_and_vs[0]
return latest_version.bump(level_bump)
if forced_level_bump is not LevelBump.PRERELEASE_REVISION:
return latest_version.bump(forced_level_bump)

# We need to find the latest version with the prerelease token
# we're looking for, and return that version + an increment to
# the prerelease revision.

# NOTE this can probably be cleaned up.
# ts_and_vs are in order, so check if we're looking at prereleases
# for the same (major, minor, patch) as the latest version.
# If we are, we can increment the revision and we're done. If
# we don't find a prerelease targeting this version with the same
# token as the one we're looking to prerelease, we can use revision 1.
for _, version in ts_and_vs:
if not (
version.major == latest_version.major
and version.minor == latest_version.minor
and version.patch == latest_version.patch
):
break
if (
version.is_prerelease
and version.prerelease_token == translator.prerelease_token
):
return version.bump(LevelBump.PRERELEASE_REVISION)
return latest_version.to_prerelease(token=translator.prerelease_token, revision=1)


def apply_version_to_source_files(
Expand Down Expand Up @@ -146,10 +180,10 @@ def shell(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
help="Print the last released version tag and exit",
)
@click.option(
"--prerelease",
"force_prerelease",
"--as-prerelease",
"as_prerelease",
is_flag=True,
help="Force the next version to be a prerelease",
help="Ensure the next version to be released is a prerelease version",
)
@click.option(
"--prerelease-token",
Expand All @@ -161,19 +195,25 @@ def shell(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
"--major",
"force_level",
flag_value="major",
help="force the next version to be a major release",
help="Force the next version to be a major release",
)
@click.option(
"--minor",
"force_level",
flag_value="minor",
help="force the next version to be a minor release",
help="Force the next version to be a minor release",
)
@click.option(
"--patch",
"force_level",
flag_value="patch",
help="force the next version to be a patch release",
help="Force the next version to be a patch release",
)
@click.option(
"--prerelease",
"force_level",
flag_value="prerelease_revision",
help="Force the next version to be a prerelease",
)
@click.option(
"--commit/--no-commit",
Expand Down Expand Up @@ -225,7 +265,7 @@ def version( # noqa: C901
print_only_tag: bool = False,
print_last_released: bool = False,
print_last_released_tag: bool = False,
force_prerelease: bool = False,
as_prerelease: bool = False,
prerelease_token: str | None = None,
force_level: str | None = None,
commit_changes: bool = True,
Expand Down Expand Up @@ -272,9 +312,10 @@ def version( # noqa: C901
ctx.exit(0)

parser = runtime.commit_parser
forced_level_bump = None if not force_level else LevelBump.from_string(force_level)
prerelease = is_forced_prerelease(
force_prerelease=force_prerelease,
force_level=force_level,
as_prerelease=as_prerelease,
forced_level_bump=forced_level_bump,
prerelease=runtime.prerelease,
)
hvcs_client = runtime.hvcs_client
Expand Down Expand Up @@ -307,23 +348,19 @@ def version( # noqa: C901
log.info("No vcs release will be created because pushing changes is disabled")
make_vcs_release &= push_changes

if force_prerelease:
log.warning("Forcing prerelease due to '--prerelease' command-line flag")
elif force_level:
if forced_level_bump:
log.warning(
"Forcing prerelease=False due to '--%s' command-line flag and no "
"'--prerelease' flag",
"Forcing a '%s' release due to '--%s' command-line flag",
force_level,
)

if force_level:
level_bump = LevelBump.from_string(force_level)
log.warning(
"Forcing a %s level bump due to '--force' command-line option", force_level
(
force_level
if forced_level_bump is not LevelBump.PRERELEASE_REVISION
else "prerelease"
),
)

new_version = version_from_forced_level(
repo=repo, level_bump=level_bump, translator=translator
repo=repo, forced_level_bump=forced_level_bump, translator=translator
)

# We only turn the forced version into a prerelease if the user has specified
Expand All @@ -348,6 +385,16 @@ def version( # noqa: C901
if build_metadata:
new_version.build_metadata = build_metadata

if as_prerelease:
before_conversion, new_version = new_version, new_version.to_prerelease(
token=translator.prerelease_token
)
log.info(
"Converting %s to %s due to '--as-prerelease' command-line option",
before_conversion,
new_version,
)

gha_output.released = False
gha_output.version = new_version
ctx.call_on_close(gha_output.write_if_possible)
Expand Down

0 comments on commit 2acb5ac

Please sign in to comment.