Skip to content

feat(cmd-version): add support for partial tags #1115

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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

MatthieuSarter
Copy link

@MatthieuSarter MatthieuSarter commented Dec 8, 2024

Purpose

Add a new option add_partial_tags to handle creation and update of partial tags for major and major.minor.

Rationale

It will simplify the CI process for project using partial tags, as it will no more require to add scripts to update those after performing a version bump with psr.

How did you test?

For now, I only did manual testing. I need more time to setup a fully working test environment and investigate the way the tests are written. That's why the PR is opened as draft for now, just so you know that I'm working on the feature and can have a first look at it. I should have some spare time at the end of the month or in january to look at the tests.

How to Verify

  • Set add_partial_tags to true in configuration.
  • Run psr --no-op version on a project with partial tags.
  • Verify that you got no warning about invalid version tags.
  • Verify that the output shows the commands for creating the two partial tags.

Sample output without partial tags enabled:

psr --noop version
🛡 You are running in no-operation mode, because the '--noop' flag was supplied                                                                     config.py:700
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v0 as as Version: '0' is not a valid Version                                                 algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v0.3 as as Version: '0.3' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1 as as Version: '1' is not a valid Version                                                 algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1.0 as as Version: '1.0' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1.1 as as Version: '1.1' is not a valid Version                                             algorithm.py:47
1.2.0
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v0 as as Version: '0' is not a valid Version                                                 algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v0.3 as as Version: '0.3' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1 as as Version: '1' is not a valid Version                                                 algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1.0 as as Version: '1.0' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1.1 as as Version: '1.1' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v0 as as Version: '0' is not a valid Version                                                 algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v0.3 as as Version: '0.3' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1 as as Version: '1' is not a valid Version                                                 algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1.0 as as Version: '1.0' is not a valid Version                                             algorithm.py:47
           WARNING  [algorithm.tags_and_versions] Couldn't parse tag v1.1 as as Version: '1.1' is not a valid Version                                             algorithm.py:47
The next version is: 1.2.0! 🚀
🛡 semantic-release 'noop' mode is enabled! would have written your changelog to CHANGELOG.md
🛡 semantic-release 'noop' mode is enabled! would have updated versions in the following paths:
    pyproject.toml
    src\clitools\__init__.py
🛡 semantic-release 'noop' mode is enabled! would have run the build_command poetry build
🛡 semantic-release 'noop' mode is enabled!     would have run:
        git add <redacted>

🛡 semantic-release 'noop' mode is enabled!         would have run:
                            GIT_AUTHOR_NAME=semantic-release \
    GIT_AUTHOR_EMAIL=semantic-release \
    GIT_COMMITTER_NAME=semantic-release \
    GIT_COMMITTER_EMAIL=semantic-release \
    git commit -m 'Version 1.2.0

            Automatically generated by python-semantic-release'

🛡 semantic-release 'noop' mode is enabled!         would have run:
                            GIT_AUTHOR_NAME=semantic-release \
    GIT_AUTHOR_EMAIL=semantic-release \
    GIT_COMMITTER_NAME=semantic-release \
    GIT_COMMITTER_EMAIL=semantic-release \
    git tag -a v1.2.0 -m 'v1.2.0'

🛡 semantic-release 'noop' mode is enabled!     would have run:
        git push <redacted> main

🛡 semantic-release 'noop' mode is enabled!     would have run:
        git push <redacted> tag v1.2.0

🛡 semantic-release 'noop' mode is enabled! would have created a release for tag v1.2.0 with the following notes:
 ## v1.2.0 (2024-12-08)

And the same with rolling tags enabled:

psr --noop version
🛡 You are running in no-operation mode, because the '--noop' flag was supplied
1.2.0
The next version is: 1.2.0! 🚀
🛡 semantic-release 'noop' mode is enabled! would have written your changelog to CHANGELOG.md
🛡 semantic-release 'noop' mode is enabled! would have updated versions in the following paths:
    pyproject.toml
    src\clitools\__init__.py
🛡 semantic-release 'noop' mode is enabled! would have run the build_command poetry build
🛡 semantic-release 'noop' mode is enabled!     would have run:
        git add <redacted>

🛡 semantic-release 'noop' mode is enabled!         would have run:
                            GIT_AUTHOR_NAME=semantic-release \
    GIT_AUTHOR_EMAIL=semantic-release \
    GIT_COMMITTER_NAME=semantic-release \
    GIT_COMMITTER_EMAIL=semantic-release \
    git commit -m 'Version 1.2.0

            Automatically generated by python-semantic-release'

🛡 semantic-release 'noop' mode is enabled!         would have run:
                            GIT_AUTHOR_NAME=semantic-release \
    GIT_AUTHOR_EMAIL=semantic-release \
    GIT_COMMITTER_NAME=semantic-release \
    GIT_COMMITTER_EMAIL=semantic-release \
    git tag -a v1.2.0 -m 'v1.2.0'

🛡 semantic-release 'noop' mode is enabled!     would have run:
        git push <redacted> main

🛡 semantic-release 'noop' mode is enabled!     would have run:
        git push <redacted> tag v1.2.0 

🛡 semantic-release 'noop' mode is enabled!         would have run:
                            GIT_AUTHOR_NAME=semantic-release \
    GIT_AUTHOR_EMAIL=semantic-release \
    GIT_COMMITTER_NAME=semantic-release \
    GIT_COMMITTER_EMAIL=semantic-release \
    git tag -af v1 -m 'v1 is v1.2.0'

🛡 semantic-release 'noop' mode is enabled!     would have run:
        git push <redacted> tag v1 --force

🛡 semantic-release 'noop' mode is enabled!         would have run:
                            GIT_AUTHOR_NAME=semantic-release \
    GIT_AUTHOR_EMAIL=semantic-release \
    GIT_COMMITTER_NAME=semantic-release \
    GIT_COMMITTER_EMAIL=semantic-release \
    git tag -af v1.2 -m 'v1.2 is v1.2.0'

🛡 semantic-release 'noop' mode is enabled!     would have run:
        git push <redacted> tag v1.2 --force

🛡 semantic-release 'noop' mode is enabled! would have created a release for tag v1.2.0 with the following notes:
 ## v1.2.0 (2024-12-08)

@codejedi365 codejedi365 marked this pull request as ready for review December 8, 2024 16:01
@codejedi365 codejedi365 marked this pull request as draft December 8, 2024 16:56
@codejedi365
Copy link
Contributor

codejedi365 commented Dec 8, 2024

@MatthieuSarter, thanks for all the hard work you have put in here. It looks good and I was not thinking about this kind of feature or how to implement. One suggestion would be to change the configure name to a longer more descriptive name rolling_partial_tags or add_partial_tags because I personally didn't understand what rolling_tags were at first glance. I think the crucial difference is that they are partial version numbers and its a bit less important to denote them as rolling as PSR's activity is only for the current version. But we will bake in the ability for --force which enables PSR to work over in a repeating fashion.

Thanks for adding the descriptions and doc updates and timeline for testing rather than just passing it to me to complete (surprising how many people do this). To help understand the test suite, note there is the tests/ directory that is split into unit & e2e folders and how to run the test suite is documented in Contributing.rst or you can check out the .github/workflows/validate.yml configuration. I am more critical about the e2e than unit tests since that takes a lot of the guess work out of will it work for our thousands of users (3.8K repos on GitHub, average of 440K downloads/month). I think yours does fit mostly in the e2e test bucket anyway because you are depending on a configuration setting and then proving that tags are created or updated. With this said, e2e tests generally use our built in repo fixtures, which build actual test repositories of various branching, merging, and release strategies, then the tests apply configuration settings via update_pyproject_toml fixture and then execute the main entrypoint with the correct subcommand's cli args passed to click's CliRunner. Since your changes are all around the version subcommand, they should be added into the tests/e2e/cmd_version/ folder, possibly put into its own file as test_version_bump is quite large. As you will see any tests in test_version_bump assert that all actions have taken place so we know that this variation doesn't have a weird side impact on something we didn't consider. You should be considering at a minimum, a test for when previous partial version tags don't exist and then when they do previously exist. I don't think your change will need to test all repo variants but probably a few as it would be worth considering if prereleases exist or a prerelease is requested when add_partial_tags is enabled. Likely this would be repo_w_trunk_only_*_commits, repo_w_trunk_only_n_prereleases_*_commits (note all repo fixtures start with repo_*). One trick to testing is to capture the expected results from the prebuilt repos and then as part of the setup, reverse the repo down to the previous commit or tag and then validate that PSR versioned appropriately. You can also extract commits and version lists from the build definition through the get_ fixtures.

Recommend add_partial_tags be set to False as the default since we would be adding this as a new user configured feature into v9. Your test would set this value true during setup. You shouldn't need a new repo fixture just apply previous partial version tags manually after using get_version_strings_from_repo_def(repo_result["definition"]) for updating existing partial tags test.

It is probably also worth noting that a repo_* fixture returns a BuiltRepoResult object which includes an opened Repo object and the resulting build steps (with commit hashes). Repo fixtures also change the current directory to a temporary pytest folder so you can assume that you are in the fake project repo when running repo.git.* commands. For speed of iterative testing, we have a smart/dynamic cache of each repo fixture's start state in the .pytest_cache/d/psr-cached-repos/ and their definitions in .pytest_cache/v/psr/repos/. These are copied (rather than rebuilt) into the pytest temporary test directory so that each test is isolated from each test's git actions.

I did swap your PR to ready for review to see how your changes currently evaluate in the CI and nothing failed so you effectively added functionality without breaking anything (that we know of) which is awesome. Now we would just be looking for tests that exercise your new functionality (tag creations & updates) and any new ways that it would impact other non-default settings or environments. Good job so far!

@MatthieuSarter
Copy link
Author

@codejedi365 , thanks for your explanation, it will be helpfull to help me add tests :)

Regarding the name of the option, I used this name as it is the name I commonly encountered on other projects for naming such tags, especially in the Docker world (see here for instance: https://docs.vmware.com/en/VMware-Tanzu-Application-Catalog/services/main/GUID-apps-tutorials-understand-rolling-tags-containers-index.html ), but if you prefer an other terminology, I'm totally fine with it. Among your two suggestions, I would prefer rolling_partial_tags, which would both indicate that it enable rolling tags and that those tags are based on partial version (as opposed to rolling tags like "latest", without any version infos).

@codejedi365
Copy link
Contributor

Regarding the name of the option, I used this name as it is the name I commonly encountered on other projects for naming such tags, especially in the Docker world (see here for instance: https://docs.vmware.com/en/VMware-Tanzu-Application-Catalog/services/main/GUID-apps-tutorials-understand-rolling-tags-containers-index.html ), but if you prefer an other terminology, I'm totally fine with it. Among your two suggestions, I would prefer rolling_partial_tags, which would both indicate that it enable rolling tags and that those tags are based on partial version (as opposed to rolling tags like "latest", without any version infos).

I appreciate the insight and thought considered for the name. As I continued writing my response I think I have settled on the add_partial_tags as it best describes what it is doing to include the verb and subject. Adding the adjective of rolling conflates with the result and impact of one possible activity or use case but is not ultimately what PSR is doing. The setting needs to be descriptive of the action PSR is doing only. I highly recommend though that your documentation of the setting value include the example in detail of the use case for rolling tags as for a primary reason of why to use the setting. This is where the resulting impact really can used to inform users of new possibilities.

@codejedi365
Copy link
Contributor

codejedi365 commented Dec 9, 2024

@MatthieuSarter, Thinking about the tests a little more, I think it will be important to verify the following situations:

  1. [Tag Creation] Very first release on a new project with add_partial_tags = true (ie. repo_w_no_tags_*_commits)
  2. [Tag Creation] Ongoing project that just decided to enable the feature add_partial_tags = true (ie. repo_w_trunk_only_*_commits)
  3. [Tag Update] Existing partial tags on previous release, next release is a patch release, must update both major & minor (ie. repo_w_trunk_only_*_commits)
  4. [Tag Update & Create] Existing partial tags on previous release, next release is a minor release, must update the major partial tag & create new minor tag, while validating the old minor tag exists (ie. repo_w_trunk_only_*_commits)
  5. [Tag Create & Maintain] Existing partial tags on previous release, next release is a major release, must create a new major partial tag & create new minor tag, while validating the old major & minor tags still exist (ie. repo_w_trunk_only_*_commits)
  6. [Prerelease Validation] When add_partial_tags = true, and a prerelease occurs, no partial tag is created? (ie. repo_w_trunk_only_n_prereleases_*_commits)
  7. [build metadata tag creation] see question below
  8. [build metadata change/tag update] no change in version but build metadata changed, partial major, minor, and patch updated.

Question: how would we consider handling a build metadata change release or just a release with build metadata? I feel like we should be creating a "partial" release from the full tag which would result in a vX.X.X (from vX.X.X+build.20241201). Either way, this situation creates the need for additional tests to characterize the expected behavior of PSR.

Note: I recommend using the fixture mocked_git_push: MagicMock to block and capture your git push activity and then evaluate it. You can also use post_mocker: Mocker to capture the --vcs-release api upload as well which prevents errors as the domain's don't exist.

@codejedi365
Copy link
Contributor

the force push change was to help resolve the conflict in gitproject.py for you as I just caused that one today.

@MatthieuSarter
Copy link
Author

@codejedi365 , just a little message to tell you I didn't forgot about my commitment to add tests to this PR, but I had a tough start of the year (flu, bike crash...) so I didn't have the opportunity to work on it yet.

@codejedi365
Copy link
Contributor

codejedi365 commented Feb 9, 2025

@MatthieuSarter, thanks for the note, I was starting to debate if you would follow through as most just leave it to me to pick up where they left off. I'm glad you haven't.

I hope you feel better soon! It sounds like a rough start to 2025 for you--must be frustrating.

If you have seen the rebases, I have just been keeping what was written up to date with the latest version as stuff has refactored which should make it easier to pick up from.

@MatthieuSarter
Copy link
Author

MatthieuSarter commented Mar 19, 2025

@codejedi365, I finally had some time to start adding tests 🙂 Not much for now, but it's a start. I used test_version_bump.py::test_version_force_level as reference, and kept only what is pertinent for tag related checks (ie. dropped the check on version update in files, release creation in VCS, etc...).

Regarding the way of handling partial tag for prerelease, I think the answer is no update of the partial tags in this cases, as the partial tags should always point to a release, not a prerelease.

For now I opted for the same behavior in case of version with build metadata, as for me those versions are not release. But I'm not used to this versioning scheme, so I'm open to discussion about this, creating/updating partial tags up to the patch level also seems to be a reasonable option in this case.

@MatthieuSarter MatthieuSarter changed the title feat(cmd-version): add support for rolling tags feat(cmd-version): add support for partial tags Mar 19, 2025
@codejedi365
Copy link
Contributor

I finally had some time to start adding tests 🙂 Not much for now, but it's a start.

@MatthieuSarter, Glad to hear!

I used test_version_bump.py::test_version_force_level as reference, and kept only what is pertinent for tag related checks (ie. dropped the check on version update in files, release creation in VCS, etc...).

I would rather you not drop the assertions in e2e tests. In a e2e test of version, they are all pertinent. As I stated above, it is on purpose to have all the checks so we know that this variation doesn't have a weird side impact on something we didn't consider.

Regarding the way of handling partial tag for prerelease, I think the answer is no update of the partial tags in this cases, as the partial tags should always point to a release, not a prerelease.

I concur that should be the outcome. We will want a test to ensure these do not move.

For now I opted for the same behavior in case of version with build metadata, as for me those versions are not release. But I'm not used to this versioning scheme, so I'm open to discussion about this, creating/updating partial tags up to the patch level also seems to be a reasonable option in this case.

Build metadata releases are official releases, they just include more specific information (generally the date). Note that the + is the separator as opposed to the -. From the SemVer standard, build metadata is an additional labeling extension separate from pre-release but similar. The build metadata is not however taken into consideration for version precedence. After this research, I am feeling more confident we should implement partials to include patch, minor, major when set as a build-metadata. Although it is not factored into version precedence, I do think this would move the partial tags if you released a version with a new build metadata label. Example:

* 26bb37c (tag: v9.21.0+build.20250112, tag: v9.21.0, tag: v9.21, tag: v9) chore: Release 9.21.0+build.20250112
* 98287c9 ci(build): change build steps (#1202)
* 5093f30 (tag: v9.21.0+build.20250101) chore: Release 9.21.0+build.20250101       # <--- partials were here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants