-
Notifications
You must be signed in to change notification settings - Fork 1.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
The way Keystone does semver is not meaningful to package consumers #2826
Comments
Hey @thekevinbrown thanks for the comprehensive write-up! I flagged the conversation in Slack yesterday to come back to and haven't read through all the details yet but I think I've got the jist, and this issue is really helpful 👍 Firstly, an acknowledgement that I think this is a real problem for Keystone's users; and unfortunately I think this is the sort of edge-case issue where the current state of dependency management in the node ecosystem fails us. In theory, Note: in my experience peer dependencies are... basically broken for our use case. If anyone thinks they would help, we can have that conversation. Also a side note, we can maybe mitigate this a bit by reviewing how the packages in Keystone are actually split up - bearing in mind that a common complaint about versions 1-4 was the "single package" approach and the sheer number of dependencies that were installed but given use case {x} didn't need to be. Add in multiple native modules for database adapters, etc. and the problem gets worse so I expect while we can maybe minimise the number of core keystone packages and therefore reduce the chance for incompatibilities in upgrades to crop up, we can't not deal with it. Onto how I think we can improve things: Starting with the example of:
I do not think either package should have a dependency on the other one (this is currently not technically correct, see my note at the bottom). Looks like this:
Now,
Really, they're all part of the same "Public API" of the A side note about how I think about semver Semver is, more than anything, the way that package authors have to communicate to users how to interpret new versions. Use major to say "the API or some important internal implementation has changed; you probably need to update, or at least carefully verify, your usage of this package when you upgrade". Use minor to say "we added a feature but it's safe to upgrade". Use patch to say "we fixed something, you should upgrade and expect no changes" So versions are a bit of an art, because what if "correct behaviour" in a project was depending on a bug in a dependency that gets fixed in a patch release? Often, it's up to package authors to make calls like that, and guess at the probable impact of changes. Which encourages more major versions. But more major (and worse, really frequent major) versions are a liability for users because of the amount of work required to upgrade safely. Version churn can really hurt a project's community. Ultimately we have to strike a balance and imo it's better to be cautious with changes, but not at the cost of progress. There's no perfect solution, which comes back to why I think of it as a communication mechanism. Back to the point Let's say we (breaking) change the "user-facing" API of Outcome:
Now, what if we (breaking) change the "keystone-facing" API and not the "user-facing" API of We also need (in theory) a patch update to ... except they can't. They can only safely upgrade if they also upgrade the new major version of the So what do we want to communicate here? I think it's this:
But how do users know that these two package upgrades should be linked? One option is to "lock" major version numbers across multiple packages. But I'm disinclined to do that because I think it'll get out of control and is very likely to introduce unnecessary upgrade churn for users. (unless we moved to monthly releases for major changes, but I don't think we're there yet in terms of maturity as a project. Maybe in the future) To explain how this can go badly: We do a major mongoose upgrade, without otherwise changing any of the public API of
Pushing a major out to both of them because we upgraded mongoose (which this project is not even using) communicates a lie, and escalates upgrade fatigue by wasting people's time. So I believe that lock-stepping versions is bad. What else we can do Because I don't believe node / npm / The Platform has a solution for this, I think we should solve it ourselves. I thought about adding this really early on, and now kind of regret not doing it. Let's imagine that packages, alongside their package version, declare an API Compatibility version. So in the two packages:
If we change the "user-facing" API of
But if we change the "keystone-facing" API (and corresponding usage) we'd increment the api compatibility version as well:
(note that since we're changing the adapter API, we need a new major of the mongoose adapter as well) The API compatibility range would be validated by keystone when you start it, and an incompatible package would throw with a link to the docs and changelog. We'd have a handful of API compatibility keys, others I can think of include This is a bit more work for Keystone, but I like a few things about it:
I haven't thought through exactly how we'd code the validation yet but it's probably a reasonably straight-forward thing to implement cleanly and I think will prevent future pain related to this issue. Regarding technical correctness of the "these packages should not be dependent on each other at all" statement: today, // @keystonejs/adapter-knex/lib/adapter-knex.js
const { BaseKeystoneAdapter, BaseListAdapter, BaseFieldAdapter } = require('@keystonejs/keystone'); I think this needs some more thought, maybe the base adapters belong in another package and not keystone core, to clear up the relationship between packages. But I don't think any outcome here changes the rest of my points above so I'm going to call that discussion out of scope for now. Keen to hear thoughts on this, and if there's any enthusiasm to implement an api compatibility interface. |
To sum up my thoughts: Yup, you're right about the deps here, I didn't mean to imply this exact scenario happened, or this was the order of the deps in real Keystone, just trying to illustrate how it's breaking for people. Might have been better to call it I think we can fix this entirely by doing what Gatsby does, which has not gotten out of hand even though they have a lot of packages. Their semver is meaningful to package consumers, and they're a monorepo with a lot of contributors. On peer deps, why not just remove the dependencies that are unused in this case? They're definitely peer deps (so personally I'd put them in peer deps) but if you don't want to do that, why have them in I get where you're going with |
Ah, a compromise occurred to me:
That gets most of what we want here, no? |
Optional peerDependencies very much exist now and I think they would make sense here. (https://yarnpkg.com/configuration/manifest#peerDependenciesMeta.optional, npm/cli@a123410) I think that a lot of things would probably have non-optional peer dependencies on And because of the way Changesets works with peerDependencies, packages would get major bumps when their peer dependencies get major or minor bumps(we need to do a major bump on a peer dependent when the peer dependency gets even a minor because it could introduce a new feature which the peer dependent depends on, this might cause over-eager major bumps but I think that's better than the opposite and we have some ideas around how Changesets could solve that). |
That solution sounds great @mitchellhamilton! |
I'm at risk of being off base here and it's getting late in the day to go into details but... from what I understand, Gatsby is sort of a "hub and spokes" many-package architecture, whereas Keystone is more of a constellation architecture where users choose the shape of the constellation. Long story short (and hard to explain but) I think the reasons it hasn't gotten out of hand for them won't stay true for Keystone. What I really want is for Keystone to say "I expect a thing that is compatible with the way I expect to use it" -- without knowing what that package is. Keep in mind, someone could make a closed source flat-file database adapter if they wanted to, implement the adapter API, and it works fine. When keystone starts it asks the adapter "hey do you conform to A few major versions and some changes later, keystone says "hey do you conform to Sure database adapters (which we're talking about throughout these examples) don't sell the value well because, how many of them are there ever going to be? But the same pattern holds up for stuff like file adapters (which some fields are provided) and even the fields API -- and then we start to see why this is important, because supporting custom fields is very much a first class concern (and value prop) for Keystone. So when a List gets configured with fields, it should say "hey I'm expecting to work with This is the crux of why, even given ecosystem tooling at the npm/yarn level, I don't think we can dodge implementing this for long in Keystone - and especially as the community/ecosystem grows. Basically we've gotten away with it so far because of the rate of adoption vs. rate of breaking changes for inter-package APIs. Optional peer dependencies looks great but breaking changes to inter-package APIs !== major package versions, knowing my field package is compatible with However, I'm talking about introducing a whole new thing to Keystone to solve a problem and I like what @mitchellhamilton has suggested as (at least) an interim solution, and it'll work well for packages in the monorepo which is a great step.
💯 agree, we should never patch -> boom Maybe I didn't call out clearly enough that changing a compat version will always mean a major package version. Another thing I like about explicitly considering and maintaining an internal API compat version is that, while it's a bit more to consider, as developers of the framework we really should be spending time thinking about that. From my understanding of the release that originally caused this discussion to be happening now, it was missed that "hey if we change package A so it works with the new API of package B", the version of an instance of a class exposed by package B being passed to package A is actually part of package A's public API and that API has been broken, we should release a new major version. |
I am no expert here and have not been running a big production to share the pain. from solution side I believe we can get some help from If idea is that changeset for each release creates something like this:
in this case I am sure not many would like this solution on top of existing tooling of yarn/npm. |
For the record, many of Gatsby's packages are interlinked via internal APIs, just like Keystone. There are methods on the core package that other packages call to configure the system. There are sources that feed transformers, which feed plugins, etc. There's also an implicit API in the graphql schema they share, and a breaking change will affect that whole pipeline. So no, I still don't think it's inherently different. They handle this by having more rigour around when they release majors, and they batch up the breaking changes so users don't have to deal with lots of majors. I feel like we're immediately talking about tactics with the
I think you can only actually have any two of those at once, not all three. For example:
I feel like I'm asking, "Which of these two are more important than the other?" and I'm hearing, "All three!" But that's exactly like a client that says, "I want it to be good, fast, AND cheap!" I think we should decide which one of these we want to compromise on, because I just don't see a way to get all three at once. My ideal as a package consumer would be to have a release schedule so our sites don't break and change their DB schema on the regular. @jesstelford told me on Slack:
But I genuinely don't see how that's the case. You either break semver, minimise how many breaking releases you do, or you have a big version number. Those are very clearly the options to me, and I'd love a bit more insight from the team if you feel I'm seeing this wrong. On the tactics, I don't care what Keystone uses internally to version and maintain semver. As long as the project either says it complies with semver (and actually does), or says it doesn't comply with semver, I'm a happy camper. It sounds to me from what you're saying @JedWatson that we're not ready to follow a release schedule, and we do want to maintain semver, ergo we're expecting to get a pretty high version number. Did I understand correctly? |
The issue here is not that, it's that there was a change in a peer dependency that is currently marked as a dependency. Because of that, there's no signal to the project that something external to that package needs to change as part of the update, it just updates its internal dependency, (which is irrelevant as it's not used) then boom. Peer dependencies need to be declared as peer dependencies, or even if we bump a major, there's still going to be no indication from tools like dependabot or greenkeeper that this change needs to happen. Bumping a major is still better than patch or minor here (this particular example was a minor, that in my view should definitely have been a major), but also, it's a peer dependency that isn't indicated as one. |
I think there should be a If we plan to make some major changes aligned with packages it will be good time to start doing |
@JedWatson Just a heads up that NPM v7 is going to automatically install peer deps: https://github.com/npm/rfcs/blob/latest/accepted/0025-install-peer-deps.md So it looks like optional peer deps are the way forward here to me. |
It looks like there hasn't been any activity here in over 6 months. Sorry about that! We've flagged this issue for special attention. It wil be manually reviewed by maintainers, not automatically closed. If you have any additional information please leave us a comment. It really helps! Thank you for you contribution. :) |
Given that most things are now in the |
Bug report
We had a heated debate about this yesterday on Slack. Changes to the way we version packages are going to effect most contributors, so I wanted to ensure we could have this conversation openly and involve everyone that wants to be involved, hence the GitHub issue instead of just leaving it at the Slack conversation.
Describe the bug
Currently this is happening to users:
5.0.1
->5.0.2
Expected behaviour
From semver.org:
This patch change is not backwards compatible, so should be a major bump.
What?
But Kevin, we do extensive amounts of work to comply with semver!
Explanation
I'm going to create an oversimplified example of why this happens as I think that would be easier to follow, and I'm going to go painfully slow so every step is clear. I have not researched exactly what combination of packages caused this for Chris on Slack yesterday, but please be assured this is biting people regularly, who are reaching out on Slack and in other GitHub issues. This has also happened on the Demo project, and install, run, broken out of the box is not the best introduction to any development tool.
So, for my made up example, let's simplify and say we only have two packages involved in Keystone,
@keystonejs/keystone
, and@keystonejs/adapter-knex
. We publish as follows:@keystonejs/[email protected]
Dependencies: None
@keystonejs/[email protected]
Dependencies:
@keystonejs/keystone: ^5.0.0
So I follow the (again, simplified, not real) docs and do the following in my project:
My Project
Dependencies:
@keystonejs/keystone: ^5.0.0
,@keystonejs/adapter-knex: ^5.0.0
Success
Everything works at this point.
Output
Next Version
We decide that we hate the way
keystone.add
works, and refactor it to destructure the arguments as follows:@keystonejs/[email protected]
Dependencies: None
Now we've made a breaking change, so we dutifully bumped the
keystone
package to the next major version. However,adapter-knex
's API hasn't changed, so we just bump its dependency and callkeystone.add
the new way, publishing as a patch:@keystonejs/[email protected]
Dependencies:
@keystonejs/keystone: ^6.0.0
What now?
I've been hard at work on my Keystone site, and it's almost ready to release. Since I'm all about consuming the latest versions, I use Greenkeeper, Dependabot, Snyk or even just npm-check-updates to see if there are any patches I should pull in before I go live. Using npm-check-updates I see this:
Since the project has repeatedly assured me its version numbers comply with semver, I know I can pull in just the patch version and not the major. I'm close to live so I don't want to break anything. I update just
adapter-knex
.Or even easier
I don't change
package.json
in My Project at all, and I justnpm i
oryarn
without a lockfile. Because I have^5.0.0
foradapter-knex
, npm dutifully installs the patch update and leaves v6 of keystone out of it.In either of these cases
Now we have the following situation when running
npm ls
:Which, if
adapter-knex
did its own requires would be fine, but remember My Project is the one that requires both and hooks them up together.When I start Keystone, I get:
The
[email protected]
dependency is not actually used, asadapter-knex
does notrequire('@keystonejs/keystone')
itself. I do the require in My Project. When I do that, I get[email protected]
, and[email protected]
, then string them together, as per the docs.I now have an incompatible combo of packages, all from bumping a single package with a patch version. Even worse is that unless I have integration tests, I may not even notice this is broken if it's deep within Keystone and results in data loss on the way to the DB instead of preventing startup. I may deploy this with CI assuming we're all good and only notice that the patch broke Keystone in production.
Required resolution
Either:
or
npm i
without a lock file would dutifully bring in the patch and not the major. Then there'd be a console warning that there's an unsatisfied peer dependency of@keystonejs/[email protected]
. This is far from perfect, but at least it's something. The current setup just silently breaks because I pulled in a patch.or
or
Desired resolution
It'd be nice if it was more clear what was compatible with what, e.g. #2606. I still feel we can use the version number to do that.
Existing opinions
Here's a summation of each person's viewpoint from the Slack chat yesterday. If you feel this is not a fair representation of what was said, please let me know, I'm doing this to save you having to reiterate, not to put words into your mouth:
@Noviny: This is a bug, users should be able to pull in patch and minor changes being confident that their Keystone won't break. Being able to consume a minor or patch update without having to upgrade anything else should be the outcome of any solution, definitely.
@jesstelford: This is working as expected. What you're experiencing is a combination of a configuration issue and the fact that NPM will install two versions side by side. The fact that one installation ended up with multiple versions is, unfortunately, entirely out of our hands. We are following semver. If you feel you can't trust it, then you can't trust it anywhere in the npm/yarn ecosystem, not just in Keystone.
If you want to be sure you can pull in that patch version, either read through the dependencies in
package.json
of that package and make sure none have changed majors, and if they have, read through all of thosepackage.json
files too, then make sure everything is compatible yourself, or install it, thennpm ls
and look for duplicates. The semver being a patch does not imply that it's safe to pull in the update by itself, it means that that particular package's API has not changed, and it has not.@MadeByMike: I see the points on both sides, this is a nuanced discussion, let's talk about it more.
Let's keep the conversation going. I'm particularly interested in what @molomby, @timleslie, @Vultraz, @gautamsi and @JedWatson think about this.
The text was updated successfully, but these errors were encountered: