-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
feat/package/ranges #139
feat/package/ranges #139
Conversation
lib/helpers/package-json.js
Outdated
@@ -30,10 +30,10 @@ function setNewValue(currentFileContent, depType, depName, newVersion) { | |||
return currentFileContent; | |||
} | |||
// Update the file = this is what we want | |||
parsedContents[depType][depName] = newVersion; | |||
parsedContents[depType][depName] = (upgradeType === 'major' ? '^' : upgradeType === 'minor' ? '~' : '') + newVersion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a major flaw in my approach, as I can't reasonable decide which range character to use based on the upgradeType
.
Even a pinned version may require a major upgrade.
Therefore I'll need to extract the original range character and then prepend the new version with that range.
@destroyerofbuilds can you give me some examples of how you intend to use this? e.g. if package.json says And I assume you want to also use it in conjunction with |
@rarkins using your examples, and assuming we're only interested in upgrading dependencies (this does not take into consideration how your
A few others:
|
On further thought, I don't know if it's possible to support any of those range patterns (or even all the ranges allowed per the Range Grammer). Just as an arbitrary example, what would I do in the case of I think at the point that the range spec is more complicated than a single |
Perhaps the easiest would be an option for setting the range spec with the default being an empty string (which is essentially the existing behavior of Then I could call |
I expect that this concept is only useful to library authors, not webapp users. For a webapp users, I still can't see any reason why you wouldn't want to pin versions in package.json (+ use yarn), if using renovate. This provides more granular control than using ranges with yarn, as you might experience multiple dependency updates at once in yarn.lock, one of which is faulty. For library authors - why use ranges instead of pinning, if also using renovate? This would be for the "commonly accepted" reason - the wider your dependency semver ranges, the lower the chance you force users to use multiple versions of the same (sub-)dependency. So this implies that "the wider the range the better - so long as they actually work". The majority of library authors would use the default Patch ranges ( Minor ranges ( Then finally we have the advanced semver ranges such as mentioned above. We wouldn't need to do anything until a version greater than the existing range(s) is released, e.g.
|
What are our guiding principles then, if users are to enable some type of "pinVersions = false" option in renovate?
I'm still wondering if for complex ranges our goal may not be to automatically "guess" what new range they'll want, but instead simply to alert them to the existing of new version(s) and let them decide the new range, if our PR with new version passes their tests. |
From my perspective, I don't see any value in pinning dependencies in applications. To remove the risk of transitivie dependencies introducing breaking changes you must use a lockfile in your application. Once you've introduced a lockfile, you've negated any benefit of pinning dependencies in your application's
I'm not sure I follow. Are you saying that with pinned dependencies in your application's How would transitive dependencies not change in
I think you covered it in the rest of that paragraph. (A full example is here) Your third paragraph here also captures my opinion on the matter.
Actually, I think it's a concern with compatibly with newer minor versions. Your comments elsewhere about Angular 1.x being a good example of a library that does not follow semver.
|
Codecov Report
@@ Coverage Diff @@
## master #139 +/- ##
=========================================
+ Coverage 68.94% 70.55% +1.6%
=========================================
Files 19 19
Lines 805 849 +44
Branches 130 147 +17
=========================================
+ Hits 555 599 +44
Misses 250 250
Continue to review full report at Codecov.
|
I'm slowly working my way through updating the tests to reflect the new support for range semantics. |
Let's say that your direct dependencies are packages a,b,c. Now compare this to the scenario where you pin versions in
|
It's also quite possible in webapps that a "broken" version 1.2.0 of You could work out a way to reproduce it - or better yet add a test to detect it, but then what? In the semver range scenario, you're left to hunt back through your In the pinned @destroyerofbuilds this is why I'm thoroughly convinced that all webapp users should be pinning versions in package.json + using yarn & renovate. |
I like the scenario you've setup, and I think it's valid. What I feel you're missing are dependencies d,e,f,g,h,i,j,k,l,m,n,o,p that your dependencies a,b,c depend on in some combination, but are listed in a Therefore, when So pinning dependency Instead you're still going to have to work through the changes in
Both For direct dependencies, just run I really wish they had a way to interactively upgrade transient dependencies, one at a time, for the purpose of testing compatibility. Furthermore, I then wish they had an interactive flattening command, because once everything is upgraded, there may be opportunities to flatten the directory structure. |
lib/helpers/versions.js
Outdated
|
||
// If no operator exists then return an empty string. | ||
// This will cause the dependency version to get pinned | ||
// to a specific version, which is the existing behavior of `renovate` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rarkins in the case where the version is either pinned, or using advance semantic range semantics, then we keep renovate
's existing behavior.
Only when the semantic version range is simple do we keep the range.
Therefore applications can, if they choose, pin dependencies in their package.json
file and this change will respect that.
@rarkins you may be interested in yarnpkg/yarn#2543. They are actively working on an RFC for the feature - yarnpkg/rfcs#54 |
@destroyerofbuilds by the way, I haven't really looked at any of your code here yet, as I'm still trying to imagine what the requirements are. Let me know if/when you want me to look into implementation details here. |
I agree that it's not addressing this transitive dependency problem. That doesn't take away from the fact it addresses the direct dependency one though.
On this topic, it's a pity that yarn doesn't let you opt in to honouring "pinned" yarn.lock versions of your dependencies. For node.js applications, that seems to be a no-brainer to me because duplicate dependencies in server-side
I don't think that really helps in the way that you want in this case. Even their example case of |
@rarkins do you have a suggestion for how I could allow for ranged versions to remain in a |
@hbetts here is one approach:
The question then is what to do practically with point 2 above. I think for tilde and caret ranges, it's pretty easy to know what the next range should be. For more complex ranges (e.g. anything containing an
e.g. let's say the current Also, over time this allows us to also improve the "intelligence" of the range upgrade logic, e.g. if we see an existing range like |
@rarkins I've added a I've also added tests for the Also, I added the This pull request, and it's code, is ready for your review, and hopefully, acceptance. |
@destroyerofbuilds I'm trying to work out what's the logical best way to achieve this, because getting the range groupings right needs more than the simple linear approach when pinning versions. For example if the current version range is Alternatively, do you think anyone would ever want us to raise a separate PR for each range, like we do for each major version? e.g. they'd want separate PRs for Because this logic diverges from the linear logic of pinned versions, and because this functionality would be per-package.json and not per-dependency, my instinct is that we might want to create a new |
Agreed. You're following example was spot on in that regard.
Agreed.
That seems reasonable. This pull request handles at least the
I don't know. Seems like doing that would be consistent with the way
I don't really have anything to add for that proposal. I agree that the logic may seriously complicate what is an already working upgrade function, but then, there's a lot of overlap as well. |
test/helpers/versions.spec.js
Outdated
}); | ||
it('complex range should append pinned version to existing range', () => { | ||
versionsHelper.computeRangedVersion('~1.0.0 || ^2.0.0', '2.2.3') | ||
.should.eql('~1.0.0 || ^2.0.0 || 2.2.3'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one doesn't make sense to me because 2.2.3
already satisfies the existing range of ~1.0.0 || ^2.0.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. This particular scenario would never happen. I've adjusted the test to use version ranges that make sense.
@@ -161,4 +240,19 @@ describe('helpers/versions', () => { | |||
versionsHelper.isPastLatest(qJson, '2.0.3').should.eql(true); | |||
}); | |||
}); | |||
describe('.computeRangedVersion(currentVersion, newVersion)', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to consider the case when there are multiple available versions and not just the next one. e.g. if current version is ^1.0.0
and versions 2.0.0
and 2.1.0
are available then I think we'd want the result to be ^2.0.0
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
Though computeRangeVersion
is not responsible for deciding which versions to upgrade to. It's only responsible for deciding what range character to place in front of a version.
versionsHelper.computeRangedVersion('^1.0.0', '2.0.0').should.eql('^2.0.0'); | ||
}); | ||
it('should return `~` when single operand and `~` operator is used', () => { | ||
versionsHelper.computeRangedVersion('~1.0.0', '2.0.0').should.eql('~2.0.0'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another test: If current version was ~1.0.0
and versions 1.1.0
, 1.2.0
, 2.0.0
and 2.0.1
are available then I think the user would want to see two PRs - one for ~1.2.0
and another for ~2.0.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
Though computeRangeVersion
is not responsible for deciding which versions to upgrade to. It's only responsible for deciding what range character to place in front of a version.
@@ -95,23 +95,23 @@ const options = [ | |||
name: 'commitMessage', | |||
description: 'Commit message template', | |||
type: 'string', | |||
default: 'Update dependency {{depName}} to version {{newVersion}}', | |||
default: 'Update dependency {{depName}} to version {{newVersionRange}}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we should deprecate newVersion
in favour of newVersionRange
, i.e. make it overloaded such that it can be a specific version, or it can be any type of complex semver too. I checked the package.json documentation and they describe dependencies
as:
Dependencies are specified in a simple object that maps a package name to a version range.
Therefore, newVersionRange
seems applicable. Although for consistency it seems like we should rename currentVersion
to currentVersionRange
too. Should we be super-consistent and also rename nextVersionMajor
to nextVersionRangeMajor
etc too?
Finally, I propose we do this the following way:
- New
xVersionRange
params replace existingxVersion
params - Keep
xVersion
params in current major version ofrenovate
but remove them next major upgrade- Just for any existing users who are currently overriding any default strings using them, hopefully they check the next major version "release notes" (commit messages)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we could be less semantically correct and keep using xVersion
instead of xVersionRange
for now (i.e. nextVersion
could be 1.1.0
or ^1.1.0
) and maybe rename it in next major version to xVersionRange
to be more semantically correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, implied by my above discussion: I don't think we ever need nextVersion
and nextVersionRange
at the same time in an "upgrade" because you can't be pinning and ranging at the same time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see this comment about the use of newVersionRange
- #139 (review)
It was simply to work around how semver
works. I have no preference on naming scheme.
lib/config/definitions.js
Outdated
@@ -131,6 +131,11 @@ const options = [ | |||
description: 'Requested reviewers for Pull Requests (GitHub only)', | |||
type: 'list', | |||
}, | |||
{ | |||
name: 'pinVersions', | |||
description: 'Convert ranged versions to a pinned version', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'Convert ranged versions in package.json to pinned versions'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've switched it to your wording.
lib/helpers/versions.js
Outdated
// Group updates for the current dependency by the major version. | ||
if (!allUpgrades[parsedVersion.major] || | ||
semver.gt(newVersion, allUpgrades[parsedVersion.major].newVersion)) { | ||
let upgradeType = 'patch'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that it's time to start properly differentiating between major/minor/patch. Potentially could be pushed into a separate PR though if we wish to simplify this PR's changes even further.
lib/helpers/versions.js
Outdated
// Check whether the current version is pinned, and if so, just return the | ||
// new version, which, itself, should be a pinned version number. | ||
if (parsedRange.length === 1 && parsedRange[0].operator === undefined) { | ||
rangedVersion = newVersion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simpler logic if we return newVersion
here and the subsequent else if
becomes an if
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm simplified the logic.
lib/helpers/versions.js
Outdated
rangedVersion = newVersion; | ||
|
||
// Check whether the existing version range is _simple_, meaning | ||
// the range is a single operator, like `^`, `~`, `>=`, etc on a single operand, `1.0.0`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a definitive list of operators that it could be? ^
and ~
are the simplest, but for example I can't see why we would ever upgrade a simple>=
dependency range to anything else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm tempted to be more specific about operators (e.g. whitelist ones we understand, such as ~
and ^
) and let everything else fall back to existing || new
..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe primitive operators, tilde and carrot.
However, I think X-range may also be a single operator, but I'm not sure how to handle that one, so, yeah, I think locking down to ~
and ^
is probably the safe course of action for now.
@destroyerofbuilds I've added some comments as a review. Sorry if this is starting to get "messy" to follow. I'll try to summarise again the key issues we need to make decisions or changes on. If we are in agreement on these points, either of us can make the changes necessary to have this PR merged. Parsing existing rangesFor this initial approach, I think we may need to enhance the "simple" operator check and instead explicitly check for ones we understand. We understand From https://github.com/npm/node-semver, these are "primitive" operators:
Then we have: I'm thinking that we maybe initially support only newVersion or newVersionRange
Tilde range logicShould we have one PR per major release (1.x, 2.x, etc) or one PR per upgrade "range" (e.g. for 1.1.x, 1.2.x, and 2.0.x)? I think let's just have one per major release, and if anyone wants more granular ranges then they can raise an issue to show their need. Should we support the "lowest satisfying version of the maximum range"? |
Thank you @rarkins for the feedback. I'll do my best to get back to you tomorrow on the discussion points. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
|
||
// Group updates for the current dependency by the major version. | ||
if (!upgrades[parsedVersion.major] || | ||
semver.gt(newVersion, upgrades[parsedVersion.major].newVersion)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I keep a separate newVersion
and newVersionRange
is because of this check. semver.gt
only works on a pinned version.
Please feel free to push a commit to replace the use of newVersion
and newVersionRange
with any naming scheme you feel is appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sember.gtr
actually handles ranges. So that could be used here if someone would like to consolidate the use of newVersion
and newVersionRange
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think let's consolidate them and utilise the semver.gtr
function
I agree with you. This has been changed in the code. I just read this comment after making my other comment about supporting only
Please not my comment about
I think one per range (depending on whether the range is using However, I would not get any current value from that work, and it would mean a greater refactoring of this code, which is not something I want to pursue at this time. At the moment, this change adds value, and enhancing support for more ranges can be evaluated once more people on-board with range support.
Yes. Let me see what I can do there. |
I'm not making headway on coming up with something that supports that feature. I'm sorry. |
isPastLatest(dep, version) && !isPastLatest(dep, workingVersion)) | ||
|
||
// Process all remaining versions | ||
.reduce((upgrades, newVersion) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, even though I'm really out of time to work on the minimum satisfying version feature, one idea I thought of is that you can, right before reduce
, use Lodash's groupBy
method like .groupBy(semver.major)
, to group versions by major, and then, if the range is ~
, further group by semver.minor
.
Then you could use semver
's minSatisfying
method to find the lowest version in each group after constructing a pseudo version, like, for minor group 5
within major group 4
, ~4.5.0
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@destroyerofbuilds I think I know how to solve this using a semver range "hack". We could do it like this:
- Find the maximum version for each major version (e.g. existing might be
~1.5.0
and we find1.7.6
). This could be the same logic already used before this PR - Convert this maximum version to a minor semver range by dropping the last digit, e.g.
1.7
. In semver speak,1.7
is equivalent to1.7.x
which for our purposes is equivalent to a tilde range - Use
semver
'sminSatisfying
by passing it the full array of versions and1.7
. This usually would then return1.7.0
but could also return1.7.1
for example, if1.7.0
is missing.
In theory we would be iterating over the versions array twice to do this, but I think the performance impact would be so minimal that there's no point worrying about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! Sounds good to me!
@destroyerofbuilds I think we agree on what needs to be done now to get this PR completed - let me know if you would like me to make edits directly to your fork/feature. |
@rarkins I didn't get a chance to start/complete the So please feel free to apply commits on top of the existing work. |
Replaced by #155 |
No description provided.