Nerdbank.gitversioning: Feature suggestion: nbgv command to create a release branch

Created on 10 Jan 2019  路  13Comments  路  Source: dotnet/Nerdbank.GitVersioning

For some of my projects, I use the following branching model in conjunction with Nerdbank.GitVersioning:

  • master always contains the latest development version, releases are built from a release/vX.Y branch
  • master is always a prerelease version, so in version.json the version is set to e.g. 1.2-pre
  • When a release branch is created, it is branched off from master and the prerelease tag is removed, while the version of master is increments. So in the above example, master would switch to 1.3-pre and a release branch release/v1.2would be created where the version is just 1.2

I wrote a small script to automate this process. However, I think this would be useful to be included as a command in the nbgv tool.

This way, creating a new release would be as simple as running

nbgv create-release

which would

  • read the version from version.json
  • create a release branch
  • edit and commit new version.json on release branch
  • edit and commit new version.json on master branch

Would you be open to include such a feature?
I would be happy to implement it and provide a PR.

Most helpful comment

Should we prohibit some scenarios, e.g. adding a prerelease tag to a version that has no tag (effectively decrementing the version)?

At least printing a warning makes sense. We could block it and potentially allow it if -force was specified, but we don't need to go to the trouble of supporting that before folks ask for it.

When including the git height in the tag, is the format always X.Y-tag.{height} or can the height be prefixed and suffixed with more tags? This could make setting the tag as part of the prepare-release command difficult

Yes, it can appear in multiple formats. My suggestion would be to simply add/replace the first prerelease identifier using the one specified at the command line. So -beta becomes -rc, -beta.{height} becomes -rc.{height}, -beta.{height}.foo becomes -rc.{height}.foo.

Regarding your scenarios, I'll assume that all your vX.Y branch specs are based on a version.json file with:

{
  "release": {
    "branchName": "v{0}",
  }
}

Since if the branchName was release/v{0} then I presume all your branch name checks would be matching for the new pattern.

Regarding the transformation of the version on the master branch (or any branch that does not conform to the release branch pattern) as part of the command, I think we should add/change the first identifier of the -prerelease tag to be some constant, as defined by version.json. So the user can specify that any new version should start out as -alpha or -beta. So for examples (hint: these should become test cases, encoded as arguments to a [Theory]. I will assume that release.branchName = v{0} and release.firstUnstableTag = -alpha, release.versionIncrement = minor.

When run from any branch whose name does not conform to a release branch:

| HEAD version | Command | New version | Created branch -> version
| -- | -- | -- | -- |
| 1.0-beta | prepare-release | 1.1-alpha | v1.0 -> 1.0
| 1.0-beta | prepare-release rc | 1.1-alpha | v1.0 -> 1.0-rc
| 1.0-beta.{height} | prepare-release | 1.1-alpha.{height} | v1.0 -> 1.0
| 1.0-beta.{height} | prepare-release rc | 1.1-alpha.{height} | v1.0 -> 1.0-rc.{height}

When run from a release branch, no branch is created, and the following truth table applies:

| HEAD version | Command | Original branch version
| -- | -- | -- |
| 1.0-beta | prepare-release | 1.0
| 1.0-beta.{height} | prepare-release | 1.0
| 1.0-beta | prepare-release rc | 1.0-rc
| 1.0-beta.{height} | prepare-release rc | 1.0-rc.{height}

From this we see that there are only 2-3 independent variables, that impact different things. So I hope this is a simpler model and simpler to code up than thinking about all the scenarios and their variants that you have listed.

All 13 comments

That describes my work flow as well. I love the idea. But the release/ prefix to the branch name should somehow not be hard coded. If we can just figure it out automatically that would be great, otherwise perhaps a parameter passed to the command.

I would add one more step at the end: merge the new release branch into master and resolve the merge conflict as "keep ours".

That describes my work flow as well. I love the idea. But the release/ prefix to the branch name should somehow not be hard coded. If we can just figure it out automatically that would be great, otherwise perhaps a parameter passed to the command.

Could you elaborate on " figure it out automatically"? I guess this prefix would stay mostly fixed (at least per repo) so perhaps the default value could be put into version.json.

I would add one more step at the end: merge the new release branch into master and resolve the merge conflict as "keep ours".

I've never thought of merging back the release branch. Could you explain the advantages of this approach?

Could you elaborate on " figure it out automatically"? I guess this prefix would stay mostly fixed (at least per repo) so perhaps the default value could be put into version.json.

Well, the tool might look at the publicReleaseRefSpec property from version.json and find the first(?) regex that starts with refs/heads/, but that feels a little sketchy since it may have a v prefix, or may not, and we'd need to basically convert the regex into a parameterizable string, which doesn't sound generally solvable. So a new, dedicated property in the version.json file to describe it might be good. Can you make a proposal for us to discuss before coding?

I've never thought of merging back the release branch. Could you explain the advantages of this approach?

Fixes that are made to your servicing/release branches should presumably be merged into newer service/release branches, and into your master branch so that those fixes propagate into your upcoming release. But as the flow of changing the same line in version.json in both the release branch and the master branch by definition introduces a merge conflict, and you know you can resolve it at the time you create it as "keep ours", you can keep the next _real_ merge with fixes from release to master free of unnecessary merge conflicts.

If you make your fixes in master and cherry-pick to release branches as your only means of servicing, as some teams do, then this extra merge wouldn't strictly be necessary. But it's harmless to do even in that case, so doing it in the tool allows for either flow.

...
So a new, dedicated property in the version.json file to describe it might be good. Can you make a proposal for us to discuss before coding?

The version.jsoncould also include a setting controlling how the version on master in incremented, so I' suggest to model it something like this:

{
  "version": "1.0",
  "release": {
    "branchName": "release/v{0}",
    "versionIncrement" : "minor" 
  }
}
  • branchName: Format string for the release branch name. I'd just use a C# format string to keep the implementation simple.
  • versionIncrement: Controls whether the major or minor version is incremented on master

... you can keep the next _real_ merge with fixes from release to master free of unnecessary merge conflicts.

Makes sense, I'll include the merge back to master

That looks good. I think the nbgv command should include an optional parameter to explicitly set the new version number though. That way minor can be default but one can force the next version to be a major version bump, for example.

I'm also thinking that release may not be the operative word here, since folks will most likely do this during stabilization of a release, which precedes the release itself. That's not a design change though, so you can proceed with coding. But maybe we'll call the command prepare-release or branch-release instead of create-release.

Okay, great, I'll get back to you once I have something to show

Keep in mind that the pre-release tags might look like this "1.0-preview.{height}" or "1.0.0-preview.{height}" if someone wants to put the auto-incremented number for a prerelease outside of the patch.

What would you expect in that case, @onovotny? Removing the prerelease tag in the release branch would have to remove the shift in the build number, so the patch would bump from then on.

Exactly

Perhaps what we can do then is have this new command accept an optional parameter that is the new unstable tag. For example, if master is building -beta then the prepare-release rc command will change -beta to -rc in the new branch. This makes the command generally more useful (since stabilization branches might not want to jump directly to building "stable" packages), and it also allows folks who move the git height to some prerelease tag to use the command as well.

When the command is invoked on a branch whose name already conforms to the release name convention, instead of branching, it simply changes the prerelease tag. So:

# version: 1.0-beta.{height}
[master]> nbgv prepare-release rc
# v1.0-rc branch created and version set to 1.0-rc.{height}
# v1.1-beta continues in master branch
[v1.0]> nbgv prepare-release 
# version.json in current branch is now 1.0 (-rc.{height} is removed)
[v1.0]>

Thoughts?

I think this feature is more complex than I initially thought.

I'll try to summarize the different scenarios.
The git height can appear in the version as either (I'll use the wording from the VersionPrecision enum):

  1. Build (default)
  2. Revision
  3. As part of the prerelease tag

Assuming the release branch format is v{version}:

Scenario 1: The version in version.json is either X.Y or X.Y-tag

  • nbgv prepare-release on any branch != vX.Y

    • version on current branch is set to e.g. X.(Y+1) respectively X.(Y+1)-tag

    • new branch vX.Y without version X.Y

  • nbgv prepare-release rc on any branch != vX.Y

    • version on current branch is set to e.g. X.(Y+1) respectively X.(Y+1)-tag

    • new branch vX.Y without version X.Y-rc

  • nbgv prepare-release on branch vX.Y and version has prerelease tag

    • prerelease tag on current branch is removed

    • no new branch is created

  • nbgv prepare-release on branch vX.Y and version has no prerelease tag

    • version is not changed

    • Perhaps emit a warning

  • nbgv prepare-release rc on branch vX.Y and version has prerelease tag

    • version is set to X.Y-rc

    • no new branch is created

  • nbgv prepare-release rc on branch vX.Y and version has no prerelease tag

    • version is set to X.Y-rc (adding the prerelease tag)

    • no new branch is created

Scenario 2: The version in version.json is either X.Y.Z or X.Y.Z-tag

This scenario could be handled the same as scenario 1 just using a 3-segment version, e.g. a release branch would be vX.Y.Z

Scenario 3: The version in version.json is something like X.Y.Z-tag.{height}

  • nbgv prepare-release on any branch != vX.Y.Z

    • version on current branch is set to e.g. X.(Y+1).Z-tag.{height}

    • new branch vX.Y.Z with version set to X.Y.Z.

  • nbgv prepare-release rc on any branch != vX.Y.Z

    • version on current branch is set to e.g. X.(Y+1).Z-tag.{height}

    • new branch vX.Y.Z with version set to X.Y.Z-rc.{height}.

  • nbgv prepare-release on branch vX.Y.Z and version has prerelease tag

    • prerelease tag on current branch is removed

    • no new branch is created

  • nbgv prepare-release on branch vX.Y.Z and version has no prerelease tag

    • version is not changed

    • Perhaps emit a warning

  • nbgv prepare-release rc on branch vX.Y.Z and version has prerelease tag

    • version is set to X.Y.Z-rc.{height}

    • no new branch is created

Some open questions I'm currently not sure about:

  • Does this cover all possible scenarios?
  • Should we prohibit some scenarios, e.g. adding a prerelease tag to a version that has no tag (effectively decrementing the version)?
  • When including the git height in the tag, is the format always X.Y-tag.{height} or can the height be prefixed and suffixed with more tags? This could make setting the tag as part of the prepare-release command difficult

Should we prohibit some scenarios, e.g. adding a prerelease tag to a version that has no tag (effectively decrementing the version)?

At least printing a warning makes sense. We could block it and potentially allow it if -force was specified, but we don't need to go to the trouble of supporting that before folks ask for it.

When including the git height in the tag, is the format always X.Y-tag.{height} or can the height be prefixed and suffixed with more tags? This could make setting the tag as part of the prepare-release command difficult

Yes, it can appear in multiple formats. My suggestion would be to simply add/replace the first prerelease identifier using the one specified at the command line. So -beta becomes -rc, -beta.{height} becomes -rc.{height}, -beta.{height}.foo becomes -rc.{height}.foo.

Regarding your scenarios, I'll assume that all your vX.Y branch specs are based on a version.json file with:

{
  "release": {
    "branchName": "v{0}",
  }
}

Since if the branchName was release/v{0} then I presume all your branch name checks would be matching for the new pattern.

Regarding the transformation of the version on the master branch (or any branch that does not conform to the release branch pattern) as part of the command, I think we should add/change the first identifier of the -prerelease tag to be some constant, as defined by version.json. So the user can specify that any new version should start out as -alpha or -beta. So for examples (hint: these should become test cases, encoded as arguments to a [Theory]. I will assume that release.branchName = v{0} and release.firstUnstableTag = -alpha, release.versionIncrement = minor.

When run from any branch whose name does not conform to a release branch:

| HEAD version | Command | New version | Created branch -> version
| -- | -- | -- | -- |
| 1.0-beta | prepare-release | 1.1-alpha | v1.0 -> 1.0
| 1.0-beta | prepare-release rc | 1.1-alpha | v1.0 -> 1.0-rc
| 1.0-beta.{height} | prepare-release | 1.1-alpha.{height} | v1.0 -> 1.0
| 1.0-beta.{height} | prepare-release rc | 1.1-alpha.{height} | v1.0 -> 1.0-rc.{height}

When run from a release branch, no branch is created, and the following truth table applies:

| HEAD version | Command | Original branch version
| -- | -- | -- |
| 1.0-beta | prepare-release | 1.0
| 1.0-beta.{height} | prepare-release | 1.0
| 1.0-beta | prepare-release rc | 1.0-rc
| 1.0-beta.{height} | prepare-release rc | 1.0-rc.{height}

From this we see that there are only 2-3 independent variables, that impact different things. So I hope this is a simpler model and simpler to code up than thinking about all the scenarios and their variants that you have listed.

Regarding your scenarios, I'll assume that all your vX.Y branch specs are based on a version.json file with:

{
  "release": {
    "branchName": "v{0}",
  }
}

Yes, that's what I meant, forgot to explicitly mention that.

I think the behavior you described makes the most sense. I'll implement it like this

Was this page helpful?
0 / 5 - 0 ratings