Skip to content
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

[BUG] inc premajor, preminor, and prepatch do the wrong thing if the version is already a pre-release. #751

Open
1 task done
wraithgar opened this issue Dec 11, 2024 · 36 comments
Assignees
Labels
Bug thing that needs fixing Needs Discussion is pending a discussion Needs Triage needs an initial review

Comments

@wraithgar
Copy link
Member

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

From the docs:

premajor in one call will bump the version up to the next major version and down to a prerelease of that major version. preminor, and prepatch work the same way.

If we do these steps in isolation (using the internal, "pre" that does the down to a prerelease) we get:

> semver.inc('2.0.0-pre', 'major')
'2.0.0'
> semver.inc('2.0.0', 'pre')
'2.0.0-0'

But if we do premajor

> semver.inc('2.0.0-pre', 'premajor')
'3.0.0-0'

Expected Behavior

premajor et al should "do what it says on the tin", and not jump an extra major/minor/patch.

Steps To Reproduce

No response

Environment

semver@7.6.3

@wraithgar wraithgar added Bug thing that needs fixing Needs Discussion is pending a discussion Needs Triage needs an initial review labels Dec 11, 2024
@reggi
Copy link

reggi commented Dec 11, 2024

real world use case: npm/template-oss#495

@wraithgar wraithgar self-assigned this Dec 11, 2024
@ljharb
Copy link
Contributor

ljharb commented Dec 11, 2024

presumably 2.0.0 + premajor would still jump to 3.0.0-0?

@wraithgar
Copy link
Member Author

Yes, they all currently do the right thing if the version being incremented is not already a prerelease.

> semver.inc('2.0.0', 'major')
'3.0.0'
> semver.inc('3.0.0', 'pre')
'3.0.0-0'
> semver.inc('2.0.0', 'premajor')
'3.0.0-0'

@wraithgar
Copy link
Member Author

> semver.inc('3.2.4-pre.1', 'premajor')
'4.0.0-0'

It seems we also drop the existing preid, unlike

> semver.inc('3.2.4-pre.1', 'prerelease')
'3.2.4-pre.2'

@reggi
Copy link

reggi commented Dec 11, 2024

I also want to note that semver inc currently isn't always strict and does "just drop prerelease"

Incremented "premajor version" will always drop prerelease.

> semver.inc('7.0.0-pre.0', 'major')
'7.0.0'
> semver.inc('7.0.0-pre.0', 'minor')
'7.0.0'
> semver.inc('7.0.0-pre.0', 'patch')
'7.0.0'

Incremented "preminor version" will drop prerelease if minor change.

> semver.inc('6.1.0-pre.0', 'major')
'7.0.0'
> semver.inc('6.1.0-pre.0', 'minor')
'6.1.0'
> semver.inc('6.1.0-pre.0', 'patch')
'6.1.0'

Incremented "prepatch version" will drop prerelease if patch change.

> semver.inc('6.0.5-pre.0', 'major')
'7.0.0'
> semver.inc('6.0.5-pre.0', 'minor')
'6.1.0'
> semver.inc('6.0.5-pre.0', 'patch')
'6.0.5'

So there is prerelease context aware functionality already incorporated into the existing inc.

@wraithgar
Copy link
Member Author

Draft PR at #752

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

With the draft, premajor, preminor, prepatch do two things to the pre-release:

  1. add a new pre-release 0, if it does not exist
  2. increases the pre-release id, if it exists

That's confusing.

I see these use cases:
a) Starting a new release with pre-release 0 (premajor, preminor, prepatch)
b) Increasing pre-releases, usually several times (prerelease)
c) Removing the pre-release (release, new)

Using premajor, preminor, prepatch with an existing pre-release should be an error. They work correctly otherwise.

@reggi
Copy link

reggi commented Dec 12, 2024

@mbtools I don't think it's feasible to throw, you need the conditional logic, you missed the case where your version is a preminor and you want to go to premajor, it shouldn't throw, it should bump if it's premior or prepatch and if it's premajor it will increment the pre.0 number.

@wraithgar
Copy link
Member Author

wraithgar commented Dec 12, 2024

That's confusing.

I would submit that this is a product of design, and unavoidable. Prerelease is not very intuitive and a lot of folks stumble here.

The use case here is one of automation. Consider a situation where you know you have a major/minor/patch change to be made, and you also are either in pre-release mode or you aren't. You should be able to tell semver to bump your version with just those two pieces of information. Otherwise it's a LOT more tooling and work to infer what you should do.

If I have a piece of software that's on 10.1.0-pre.0, and I have two new changes:

  • patch and breaking, I should expect to use premajor to now bump to 11.0.0-pre.0.
  • feat and patch, I should expect to use preminor to bump to 10.1.0-pre.1 since I'm already in preminor mode, but I need a new version.

Consider the original purpose "bump to next release type and drop into pre-release mode". If you're already in the prerelease mode for that type that's effectively just a new pre-release for that same version.

ETA:

10.0.0-pre.0 bumped to the next major is 10.0.0 and then dropped to pre-release would be 10.0.0-pre.0 again, but that's not what you actually want. You are still asking for the "next" premajor". You really do want 10.0.0-pre.1

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

I would use the new release to exit pre-release mode, and then use the existing premajor/minor/patch to get into a new pre-release. That's a clear separation of functionality.

@wraithgar
Copy link
Member Author

How do you make sure the new pre-release is a new version and not the same one as before?

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

After release, we have a semver (no pre-release). So any premajor/minor/patch will increase the version.

@wraithgar
Copy link
Member Author

wraithgar commented Dec 12, 2024

I think you'll need to show an example cause that's either the current incorrect behavior or the other incorrect behavior (re-using versions) we're trying to avoid.

Let's say we're in 10.0.0-pre.0 (for the sake of examples I'm using pre.0 instead of the default pre).

Either

  • release gets us to 10.0.0
  • pre gets us back to 10.0.0-pre.0 (incorrect)

Or

  • release gets us to 10.0.0
  • premmajor gets us to 11.0.0-pre.0 (incorrect)

We are already in a premajor when our version is 10.0.0-pre.0. A premajor bump is effectively just an increase in prerelease.

@wraithgar
Copy link
Member Author

To put it another way: we need semver itself to be able to do the correct thing here. We've found that without fail if folks try to hand-roll this logic they get something wrong. We even get it wrong but at least we can centralize the logic and fix it once.

If I know:

  • I am in prerelease mode for a major change.
  • I have a major breaking change

How do I tell semver, given no other context, how to effect the correct version bump of incrementing my prerelease version only?

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

There are a couple small mistakes in your comments 😉

Example:

9.1.0
semver -i premajor 9.1.0  // we need a breaking fix
10.0.0-0
semver -i prerelease 10.0.0-0 // wip
10.0.0-1
semver -i prerelease 10.0.0-1 // wip
10.0.0-2
semver -i release 10.0.0-2 // release the fix
10.0.0

or

9.1.0
semver -i preminor 9.1.0  // we need a small fix
9.2.0-0
semver -i prerelease 9.2.0-0 // wip
9.2.0-1
semver -i prerelease 9.2.0-1 // wip
9.2.0-2
semver -i release 9.2.0-2 // release the fix
9.2.0

@wraithgar
Copy link
Member Author

from 10.1.0-pre.0 to 10.1.0-pre.1 is prerelease (not preminor)

It's also preminor! That's the argument we're making here. The 10.1.0-pre.0 is already in preminor. so asking for preminor again should just bump the prerelease.

The other comment was updated.

@wraithgar
Copy link
Member Author

in this example

9.1.0
semver -i preminor 9.1.0  // we need a small fix
9.2.0-0
semver -i prerelease 9.2.0-0 // wip
9.2.0-1
semver -i prerelease 9.2.0-1 // wip
9.2.0-2
semver -i release 9.2.0-2 // release the fix
9.2.0

How do you know it's only a prerelease bump every time? Where is that logic living? Without this bugfix you have to figure out if you're already in a preminor version, and the change you want to make is no bigger than a minor.

It's easy to use the tools as-is if you are a human making these decisions. This is impossible to automate currently without having to write a lot of potentially fragile extra logic that should be in semver itself.

@wraithgar
Copy link
Member Author

I see these use cases:

You are forgetting

d) our release process is automated and we need to be able to tell semver what to do from two pieces of information: are we in pre-release mode, and what level change are we making.

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

The 10.1.0-pre.0 is already in preminor. so asking for preminor again should just bump the prerelease.

That's contrary to the docs. There's always a version increase:

preminor ... will bump the version up to the next minor version ...

We can argue whether that's 10.1.0 or 10.2.0 but increasing just the pre-release i.e. staying on the same version is something else.

--

I understand the need for automation but I don't see the problem. Let's make it concrete:

This repo is on 7.6.3. We get a new PR, a typical "fix". That should trigger prepatch to 7.6.4-0 (that's the action that created the chore: release 7.6.4 PR). Subsequent patch PRs lead to prerelease i.e. 7.6.4-1 etc. (that's the action that adds the PR to chore: release 7.6.4). The action that merges chore: release 7.6.4 should include semver -i release to remove any pre-release and make it ready to publish to npm.

If a PR includes "feat" (or whatever criteria is used here), it should trigger preminor which will get us to 7.7.0-0 (the chore: release 7.6.4 should be renamed to chore: release 7.7.0 or the 7.6.4 is closed and chore: release 7.7.0 becomes a new PR).

If a PR includes "BREAKING", it triggers premajor which would get us to 8.0.0-0.

@ljharb
Copy link
Contributor

ljharb commented Dec 12, 2024

Just clarifying, does "preminor" mean "before minor" or "prerelease, minor"?

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

I'm always referring to the option of the inc command/method.

@ljharb
Copy link
Contributor

ljharb commented Dec 12, 2024

Right, i just mean conceptually - like which definition are yall using in your head when you see that?

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

Conceptually, 10.1.0-pre.0 is a pre-release of a coming minor version (or minor release).

Using the short form "preminor" to describe something like "10.1.0-pre.0" is not very clear and mixing the inc option with the meaning of the version.

@wraithgar
Copy link
Member Author

premajor in one call will bump the version up to the next major version and down to a prerelease of that major version. preminor, and prepatch work the same way.

We are asking the question "what does it mean to do this operation on a version that is already in a prerelease for that release type already"?

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

inc prepatch means going to a new patch
inc preminor means going to a new minor release
inc premajor means going to a new major release
...and initializing the pre-release identifier

My interpretation of the doc is that this will always lead to a higher x.y.z version ("bump the version") and is independent of any existing pre-release identifier. My preference is to error if a pre-release id exists, but it works correctly now.

@mbtools
Copy link
Contributor

mbtools commented Dec 12, 2024

I'm just one semver user and suggest to get some other opinions :-)

It would be good to separate feat: add "release" inc type from #752. It's needed by the community 🙏

@reggi
Copy link

reggi commented Dec 12, 2024

@mbtools maybe it would be helpful to you to take a look at the literal "automated process" function we'd use with release-please https://github.com/npm/template-oss/blob/main/lib/release/semver-versioning-strategy.js#L56-L68

  bump(currentVersion, commits) {
    const prerelease = this.options.prerelease
    const tag = this.options.prereleaseType
    const releaseType = parseCommits(commits)
    const addPreIfNeeded = prerelease ? `pre${releaseType}` : releaseType
    const version = inc(currentVersion.toString(), addPreIfNeeded, tag)
    /* istanbul ignore next */
    if (!version) {
      throw new Error('Could not bump version')
    }
    return Version.parse(version)
  }

@wraithgar
Copy link
Member Author

If you want to independently add the release type with tests go for it. I was just adding it in a discrete commit while I was in here.

@mbtools
Copy link
Contributor

mbtools commented Dec 13, 2024

Looking at the template code and related test cases was helpful. I agree with the mapping very much 👍

I consolidated it into one statement (see npm/template-oss#497).

  const nextReleaseType =
    !prereleaseMode ? releaseType :                          // normal release
      !isPrerelease ? 'pre' + releaseType :                  // first prerelease
        !isMajor && releaseType === 'major' ? 'premajor' :   // minor or patch prerelease and major coming
          !isMinor && releaseType === 'minor' ? 'preminor' : // patch prerelease and minor coming
            'prerelease'                                     // next prerelease

If I understand @wraithgar correctly, you would like to reduce this to

  const nextReleaseType =
    !prereleaseMode ? releaseType : 'pre' + releaseType 

and move the other logic into semver.inc.

This is tempting but I don't think it matches the documentation for inc. It means, for example, that semver.inc('7.0.0-pre.1', 'premajor' ) = '7.0.0-pre.2'. This is not "bumping the version" and "down to a prerelease." It's the opposite: keeping the version and upping the prerelease.

I'm fine with such change but it's breaking and should be documented accordingly.

@reggi
Copy link

reggi commented Dec 13, 2024

@mbtools i'm glad that helped and thanks for the rewrite (a lot of ternaries though), the idea is that we'd remove that function and just embed that logic into semver, it's hard to advocate / explain to third party libraries such as release-please to implement "semver" but then we have this custom function thrown in there.

If I understand @wraithgar correctly, you would like to reduce this to

  const nextReleaseType =
    !prereleaseMode ? releaseType : 'pre' + releaseType 

yes!

This is tempting but I don't think it matches the documentation for inc. It means, for example, that semver.inc('7.0.0-pre.1', 'premajor' ) = '7.0.0-pre.2'. This is not "bumping the version" and "down to a prerelease." It's the opposite: keeping the version and upping the prerelease.

this is why I commented above with #751 (comment)

inc already has conditional logic that doesn't do the literal "bump the version", I see this as precedent that alternative paths happen when you are in prerelease mode:

For instance this doesn't do what it says: (and would be made more literal by a release releaseType for sure)

> semver.inc('7.0.0-pre.0', 'major')
'7.0.0'

@wraithgar
Copy link
Member Author

> semver.inc('7.0.0-pre.0', 'major')
'7.0.0'

That is a major increase. prerelease is not intuitive.

@mbtools
Copy link
Contributor

mbtools commented Dec 13, 2024

Alright, we agree on what semver should do for npm 😃

The remaining question: Will it break other projects?

I think the existing premajor/minor/patch behavior is correct and I doubt I'm alone.

If a project is on 6.x.y and we get a breaking change, we do premajor to get to 7.0.0-pre-0. We work on it i.e. prerelease to 7.0.0-pre.4. If we get another breaking change, before releasing the major, that's premajor again.

Today, this results in 8.0.0-pre-0. So this second breaking change goes to a different release.

With the suggested change, the second breaking change would be folded into the existing 7.0.0... line. That is quite different. Projects working on multiple release lines in parallel might have an issue with that.

@reggi
Copy link

reggi commented Dec 13, 2024

I think the existing premajor/minor/patch behavior is correct and I doubt I'm alone.

If a project is on 6.x.y and we get a breaking change, we do premajor to get to 7.0.0-pre-0. We work on it i.e. prerelease to 7.0.0-pre.4. If we get another breaking change, before releasing the major, that's premajor again.

Today, this results in 8.0.0-pre-0. So this second breaking change goes to a different release.

I also wonder if this will break for anyone. In your example I personally don't see the point never releasing a version 7 because you made you wanted to make two successive premajors releases ie 7.0.0-pre.0 then 8.0.0-pre.0 would result in having something like 6.9.2 to 8.0.0.

I think the difference in thinking is "are we breaking 6.9.2" vs "are we breaking 7.0.0-pre.0" and I philosophically don't think you can make a breaking change to a prerelease. Any change should reference the latest non-prerelease. Also whats stopping jumping n number of majors during the prerelease cycle? 6.9.2 to 18.3.24 is not useful.

@ljharb
Copy link
Contributor

ljharb commented Dec 13, 2024

I'd say you can't avoid making breaking changes in a prerelease :-p iow that every prerelease is potentially breaking version to version.

but i agree that if this is only changing semantics for people who are bumping from one prerelease to another, it's going to affect a very small number of people.

@reggi
Copy link

reggi commented Dec 13, 2024

Is there a scenario where someone may be branching out different future versions and keeping multiple "futureports" (opposite of a "backport") at the same time and would like to maintain 7.0.0-0 and 8.0.0-0 non-linearly, maybe?

Here's the argument against the proposal, the "pain path"

  • Person branches 6.9.2
  • Person branches from 6.9.2 and to a v7 branch runs npm version premajor gets 7.0.0-0
  • Person branches from 6.9.2 and to a v8 branch runs npm version premajor x2 and gets 7.0.0-1 and they want 8.0.0-0

This person should just change the version manually, they're non-linearly trying to "increment".

Within this scenario the v7 branch still should never jump to v8, though...

@mbtools
Copy link
Contributor

mbtools commented Dec 14, 2024

We use changesets in the Verdaccio project. I found this:

image

For a breaking change, they use semver.inc(... 'major') and prerelease++. The effect of their "increment" implementation is 7.0.0-pre.1 + breaking change = 8.0.0-pre.2. So they do expect a major increase while prerelease mode 👀

The good news is that they don't use pre... so a change here won't break them.

--

release-please interpretation is similar to what is suggested i.e. keep the version, increase the counter. However, their interpretation of the prerelease number is different. 1.0.0-beta01 + breaking change = 1.0.0-beta02. 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug thing that needs fixing Needs Discussion is pending a discussion Needs Triage needs an initial review
Projects
None yet
Development

No branches or pull requests

4 participants