-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
Proposal: improve UX for major module versions #38762
Comments
I think this feedback is good, and it has been given before: #37765 Assuming that pkg.go.dev will become more prominent, and eventually replace I get that it would not be as comprehensive as the other two changes proposed here, but it might be best to attempt fixing this UX problem in incremental steps, only considering the more aggressive/intrusive changes when the smaller steps prove to not be enough. |
@mvdan thanks for the pointer to that proposal; missed that in our review of related issues. Added to the appendix. |
Regarding the second subproposal: What I'm wondering is whether the hard error that disallows installing of deprecated trees is future proof.
Both of these cases could use a "give me the last non-deprecated version within this major version" - because erring out does not help (in installing/in fixing a security issue) and suggestion to use a new major version doesn't either, because of the potential API changes. Also this is sort of breaking the semver contract of "patch versions don't mess things up" - since the newest patch version cannot be installed at all - this would break tools that automate dependency security updates (think dependabot, snyk) - when a new patch version gets released (with just a deprecation tag), the tool that tries to suggest an update fails in a weird way. Maybe I'm misunderstanding what happens when I hope I understood the flows correctly. In any case, thanks for this proposal, I love UX improvements. |
@kokes one thing we discussed, but didn't include in this proposal, was including in the deprecation error message a mention of the previous minor/patch version in addition to the pointer to the next available major version. Something like:
(This is just off the top of my head - such a message should be finessed to emphasise the major version, while still offering a path to the previous working version.) I think that would address the cases you describe. With regard to external tools, they'd need to understand deprecation as much as it affects their operation, the same way they need to understand various other aspects of Go modules. |
To suggest an alternative (haven't thought it fully through, though - you might have considered this and rejected it for reasons I can't think of right now):
I think this would solve the same issues, in that users doing the obvious thing without checking which versions exist would still default to the latest. But in this more common case no extra step of mentally parsing the output and re-entering/changing is needed. The less common case (for some reason I specifically want to use an older version) is still allowed.
I'm not super against this, but I would prefer if a satisfying solution can be found that doesn't include this. I would just really like to avoid having to manually touch |
SIV dictates that
I don't think any use case described in these proposals requires manually editing go.mod?
Deprecation would apply only to specific, fully-qualified versions. The behavior described by the proposal as "deprecating an entire major version tree" is emergent from the Concretely, if you have (say) v1.7.1 which is not deprecated, you would need to tag (say) v1.7.2 as deprecated, you couldn't retroactively apply the deprecated bit to v1.7.1. Consumers currently using v1.7.1 would continue to function without impact; new consumers could explicitly request v1.7.1 if they really needed to use the v1 major version tree. Which is all to say: it's still perfectly possible to use a previous specific version of a module, even if the most recent version in the major version tree has been marked as deprecated. (It's also possible to un-deprecate a major version tree, by tagging a new specific version with the deprecated directive removed from the go.mod.) |
I often work by just typing an import path into a file and then later running |
Not to nitpick, but by name, it dictates that for imports. But I'm not talking about imports, I'm talking about arguments passed to I agree that the nomenclature isn't entirely precise and might very well contradict the possibility of this, if taken literally. Personally, I don't really care about nomenclature though, but mostly whether the semantics make sense :) And I don't see where what I imagine breaks existing users. To clarify: Say, I'd write a tool called "add-go-module", which, when given the name of a module (with or without version-specifier respectively) behaves as I described to insert a
I don't understand this. The proposal seems to clearly state that the go tool should be prohibited from mechanically adding a deprecated version. So ISTM that if I want to add a deprecated version, I would have to add that manually.
But not v1.7.2, correct? Otherwise I don't understand this sentence:
This seems to strongly imply that (part of?) the build fails if I want to use a deprecated version of a module? At least the first time I add it? The use-case I was talking about is specifically to maintain a |
Consumers should never be allowed to add explicitly deprecated versions. The go tool should refuse to add them to the go.mod, and if they are manually placed in the go.mod, the go tool should refuse to compile them.
Ah. This use case is not a match for the semantics of |
You want to maintain a deprecated branch ? that seems paradoxical but if you really want to then:
each time you patch your deprecated branch you release a tag which is not deprecated then you release a deprecated one just after. |
Is this |
No, deprecation is defined as the |
That's why I mentioned that I don't like those semantics.
I disagree. The definition of "deprecated" I am using is consistent with what Google gives:
I don't think "regarded as obsolete and best avoided" implies "can't be used". In the end, what "best avoided" means is a tradeoff. If it where an actual absolute, you already have that semantic: Remove the public API - builds will break and no one will use it. Obviously that is too strict an interpretation. I don't see why "[edit]don't[/edit] use it if you can avoid it, but I will still offer some support if you can't" is so weak an interpretation as to be "paradoxical".
To me, the patch release is still deprecated though. But I understand that it's technologically possible to work around the restriction imposed by the definition of deprecation in the proposal. |
Only tags having the deprecated directive wouldn't be usable. That way it ensures that, if the latest tag of a branch is deprecated, people are aware the whole branch is obsolete (because of the error go would throw upon getting by default the latest tag of the branch). If they still want to use that branch then they should |
I don't yet have a set of specific comments on this particular proposal (though I am very happy this is being discussed). These proposals could be implemented separately and it seems like maybe they should be two separate issues (rolling this much into one proposal makes it tough for people to focus on their comments)? Proposal 1 encompasses what I've suggested here: #38502 (I'd be happy to expand it to incorporate Maybe we slim this issue down to just Proposal 2? |
FYI I've gone from go 1.14 to 1.11 and I managed to build a project that imports a $ mkdir -p tmp/mybin
$ cd tmp/mybin
$ go mod init mybin
$ go get github.com/sylr/go-mod-deprecated@v1.0.6
$ cat <<EOF > main.go
package main
import (
deprecated "github.com/sylr/go-mod-deprecated"
)
func main() {
if deprecated.IsDeprecated() {
println("Deprecated")
}
}
EOF
$ for go in go1.{11..14}; do $go run .; done
Deprecated
Deprecated
Deprecated
Deprecated This proves that all modules aware go releases would be compatible with this new directive. |
I love the proposal to add error warnings when importing a module that isn't the latest major version. However, I'm skeptical that that should be the default behavior, I would prefer to get the latest major version by default. If someone knows of a link as to why the latest isn't the default I would love to read it. The second proposal grinds my gears a bit. If I understand correctly - the proposal would make the tool error out when trying to import a package that has been marked as deprecated? Seems problematic. Sometimes users have good reason to ignore deprecations (This isn't the JS community, we don't turn off bitwise operators because usually the dev meant to use the logical operator). I would be in support of the deprecated directive if it resulted in a simple warning as well, rather than an error. |
I had to read proposal 2 a few times. It makes sense if the only change is to go.mod when you add the deprecation directive, but that wasn't immediately clear to me. Deprecation isn't a good name for the proposed semantics. This is for the step after where the earlier versions are no longer maintained at all. I'd expect a deprecation to be a warning that what's described here is on the horizon. |
@lane-c-wagner
Semantic Import Versioning as implemented in Go modules creates ambiguities when a user types an unqualified import path e.g.
It is important that the second proposal does not merely warn users that a module version is deprecated, but actively prevent them from using it. The goal of the second proposal is to place extra power into the hands of package producers, to allow them to express constraints on their consumers that are not currently possible. Any lesser effect on consumers reduces the value of the proposal to near zero. But, this doesn't mean that package consumers can't effectively ignore the deprecation warning. They simply have to declare their dependency on a previous, un-deprecated version in the same major version tree of the module. The immediately previous version will have equivalent functionality, minus the deprecated bit. The thesis of these proposals: if a consumer adds
I think we're open to using a different name than |
Discontinued is a much better name for what's described here. I think my major point of confusion was why a deprecated version would be an error—but a discontinued version being an error makes perfect sense. |
I think this is a good proposal, and I may stretching it a little when I say that the "UX major module version" thing goes beyond the Currently, going from The above is a major concern -- and you could probably back it by stats telling that Go is a language where modules rarely get to version 2 and above. I would suggest something in the line of: A major version number must always be present in the import path, else you get the latest major version -- which will leave |
@bep that's kind of a non-starter because v0/v1 do not have the major version in the path, and the go tool should do what you ask it to, not what it thinks you mean. |
FWIW, my first comment comes exactly from my disagreement with this. The title of this issue is "improve UX for major version modules". And IMO, the setup as suggested in the proposal is just not a good UX at all. I would argue that in >>90% of cases, a user wants the newest major version. And downloading the wrong version and showing me a message "please retype with this changed version number" is going to frustrate me. What's more, I think that most of the time releasing a You say you'd find it confusing if the newest major version was used and that's subjective and thus fair enough. I just don't understand the confusion, because to me it seems the most natural and obvious thing to happen. And to me, the default should reflect the overwhelming majority of use-cases. I would even be happier if instead of printing a warning, the
Here's a question: Why not commit an empty
I think that's correct. To be explicit: The current best alternative (apart from above "tagging an empty package") is to mark all exported APIs as deprecated. The value a warning for deprecated packages would still provide is mainly, that the user doesn't have to explicitly call Personally, the corollary to me is, that Proposal 2 just shouldn't happen. I find the current form not acceptable and if a weaker form isn't worth pursuing, then I wouldn't like either. Of course that's just my opinion.
Just to state this clearly: As a module publisher, this is fixable by setting the current branch in github (or whatever hosting you use) accordingly. Yes, it's something module publishers need to be aware of, but it's not something that any user or even developer needs to know. It's a one-time cost to pay. |
That is a statement with a whole lot of assumptions. The |
A solution for this also needs to be proxy friendly. There should be a way to get deprecated modules using regular Go tooling so Go Modules proxies can fetch and serve those versions as well. It is not clear to me in the proposal if commands like |
There is an existing mechanism for marking a package deprecated: Add a "// Deprecated: " package doc comment. For a module containing multiple packages, do this for each package. Deprecation comments are advisory. Causing the module fetch to fail is substantially more severe; it's equivalent to checking in a new version of the package with a just an empty What happens if a deprecated module needs a security fix? Presumably, you need to release a new, non-deprecated version followed by a re-deprecation. Will users discover that fix, especially after they have been trained to avoid "go get -u" for this module? Philosophically, I am dubious about the release policy that module deprecations seem to encourage. The Go standard library has gone for ten years without breaking changes. When we released a new version of the protobuf module, we went to great lengths to preserve the old API as a wrapper of the new one to avoid imposing unnecessary toil on existing users. This is the standard we should strive for; new major versions come at a tremendous cost to users, should be considered only as a last resort, and should provide a path for users of the previous version to upgrade at their own pace or not at all. Providing tools to make it easier to write off old users strikes me as a step in the wrong direction. |
Proposal 2 provides module authors a stronger way of asserting that an old version is no longer maintained than purely advisory. It is opt-in, and does not break consumers. |
It does break consumers. It is (still) functionally equivalent to committing an empty I do not understand why you continue to ignore what is being said - by multiple people in the thread - but it is really frustrating. It is impossible to have a conversation this way. |
A small counter-proposal: A module may be deprecated by attaching a
The $ go get -u
go: example.com/ancient/foo => v1.2.3
go: example.com/ancient/foo is deprecated:
go: Use example.com/ancient/foo/v2. It's ever so much nicer. This is more flexible than reporting on a new major version, since it permits also providing a notice suggesting an entirely different module path or none at all. ("Deprecated: This is full of security holes and will never be fixed.") For example, this would provide an simple way for This easily permits the case of "proposal 2" here: Tag a version containing nothing but a |
Module proxies do not know which version of Go is being used by clients. Enforcing these rules on the proxy side will provide a different behavior compared to what users get when they resolve from source and so can break the reproducibility of builds. The proxy should serve the content and let the client decide if it is good or not. Users should be free to select which version of Go to use and when to upgrade so they should only be affected by the deprecation rules when they decide to move to a Go version that has it, regardless of where they are getting their modules from. |
I see, this makes sense 👍 |
Just so I understand fully: does that version break because the |
Because it contains no buildable You can, of course, do this today. The only difference from "Proposal 2: A new deprecated directive" above is that the user won't be notified about why they're broken. Surfacing a deprecation notice from the |
@neild Got it. I need to think about it a bit more deeply, but on the surface, I like it. |
@neild Might I suggest:
|
@sylr The Deprecated comment has precedent. Whatever you think of it, I feel it's more confusing to have two different conventions. |
The nice thing about a directive with an optional import path in a sentinel release is that the go tool can follow the discontinuations/redirects until it finds the least version that's still maintained at the appropriate path. If foo/v2 and foo/v3 are discontinued and v4 is at a new path entirely, bar/v4, the message for trying to upgrade v2 could see that its discontinued, look at foo/v3 and see that it's moved to bar/v4. If discontinuation is surfaced in the module proxy, this will be very fast. If it doesn't follow this automatically, either
I don't really want to do either. Having
that's surfaced in some way by the go tool, like just printing it when upgrading to that version, would be a nice way to signal that discontinuation may be coming but even if it's not you should move on at some point. |
I would question whether this case is one worth optimizing for. How many modules have a lengthy sequence of major version increments, including jumps across entirely different module paths? A deprecation notice is simple, and can be written in a forward-looking fashion that doesn't mention a specific updated version:
I also wonder if the case of a module which goes through many incompatible API changes and offers no support for older versions would not be better addressed by remaining at v0. |
Probably not that many now but that number can only increase. Even if the rate of increase or at least relative proportion stays low, arguably it being uncommon would be a good argument for its UX being optimized: so that the uncommon situation is as pleasant and uniform as the common situation. If github shuts down 10 years from now, I wouldn't want to have to read hundreds of bespoke deprecation notices to figure out where all my dependencies landed. But that could be a case against conflating deprecation, discontinuation, and redirection, because in that scenario the new import paths are likely compatible for all versions and simply written differently. |
As @neild notes, if you want to break Note that #24031 (already approved and under review, probably going to land in 1.16) would give you another such mechanism: you could publish a As far as I can tell, the non-redundant part of Proposal 2 (which really ought to have been filed as a separate proposal!) is the opportunity for the module author to suggest some specific replacement when they break the older major version. And I agree with @neild that that would be better-served — and more idiomatic — as a distinguished comment than as a new, redundant directive. |
So, let's turn to Proposal 1. The specific text proposed is:
Even ignoring the likelihood of incompatible API changes, the suggested command would not result in a working build: the user would still need to update their import paths to refer to the packages in the new module in order to use it, and if the packages include shared state and (as @peterbourgon suggests) the module author cannot be bothered to rewrite them in terms of the new API, then all other dependencies using that module may also need to be updated to use the new major version. We have been trying to eliminate diagnostics from the If there were some tool to automate the import-path rewriting (perhaps #32014 or #32816), then perhaps we could suggest that tool, but as it stands I think the suggested message needs to be rethought. |
Finally, note that we already have a It would probably not be terribly difficult to write a tool that adds such a comment to every exported identifier of every package within a given module. That would allow existing tools that already understand deprecation (such as some IDEs?) to surface that information in a way that is friendlier to the user than breaking |
It should be sufficient to add a Still, having the |
+1 to what @bcmills and @neild have said about not breaking I'll add that the main way module retractions (#24031) will be surfaced to the user is through a warning in |
DEPRECATED
Please see the new proposals #40357 and #40323 that supersede this one.
Proposal: improve UX for major module versions
Peter Bourgon (@peterbourgon), Andrew Gerrand (@adg)
Problem statement
When a user wants to use a module, it is the v0/v1 version of that module which is most prominent, as it is selected by the base repository path:
github.com/user/repo
is in effect a constraint to v0.x.y/v1.x.y.To use v2 or above, Semantic Import Versioning requires that the major version number is a suffix of the module path:
github.com/user/repo/v2
(constrained to v2.x.y),github.com/user/repo/v3
(constrained to v3.x.y), and so on.It’s easy for module consumers to default to v0/v1, even if that version is obsoleted by a more recent major version. Module consumers may even be totally unaware of later major versions.
Discoverability is a key issue. The mechanisms for module authors to advertise recent major versions are inconsistent, and can be low-visibility (documentation? README.md?) or highly disruptive (printing deprecation warnings in init, broken builds to force an investigation).
Abstract
We propose two improvements: one targeted at module consumers, and the other at producers.
For consumers, we propose a mechanism that notifies users of the latest major version of a module dependency when that dependency is first added to a project.
For producers, we propose adding a
deprecated
directive togo.mod
files to signify the end-of-life of a major version.These are just preliminary ideas which we hope to refine and improve in response to feedback gathered here.
Proposal 1: Notification of latest major version
We propose notifying users of new major versions when:
There are a few ways users add requirements to their modules:
go get github.com/user/repo
require github.com/user/repo
line to theirgo.mod
file manuallyFor the latter two, the module isn't fetched until the go command is invoked within the module.
There are a few ways users update requirements:
go get [-u] github.com/user/repo
go get -u ./...
from the module rootgo list -m -u all
In each of these cases, the go tool plays a key role, and so we propose to make the go tool print a note if a requirement is being added when there is a more recent major version of the module available.
Examples
Consider a user fetching peterbourgon/ff with
go get
. We propose adding a notification to the output, alerting the user to a new major version:Consider a user listing all of the most recent versions of their dependencies. We propose adding the latest major version alongside any new minor or patch versions:
If a new requirement is added to a
go.mod
file manually, the go tool would print the notification when it first fetches the new module, as part of ago build
,go test
, etc. run.Proposal 2: A new
deprecated
directiveProducer-side deprecation feature: add a
deprecated
line, which will cause the module to fail to fetch at that version, printing an error with the most recent non-deprecated version of the same major version.If the go tool were asked to fetch this deprecated module, it would fail:
If the Proposal 1 were adopted, the error message could be more useful:
The deprecated directive may optionally include a successor module identifier. If specified, the error printed when fetching this module version would also include a reference to the successor module. This can be used to point to the next major version:
Or to a new import path altogether:
If the go tool were asked to fetch this deprecated module, it would fail with a more explicit suggestion to the user:
If the successor module is also marked as deprecated and includes a successor module, the go tool might follow those links, and print the final non-deprecated module in the error message.
When package producers decide that a major version is deprecated, the intent is for them to create a new minor or patch release of that major version with the deprecated directive in the
go.mod
. This version will only be selected by the go tool when the corresponding major version is first added to a project, or if someone tries to update from an earlier version of that major version. In both cases, the go tool fails to fetch the module and provides a useful and actionable message: the user is instructed to pick a non-deprecated version.Users can always use earlier versions of that major version, and MVS should ensure that the deprecated version will not be selected. If module A has a requirement for B at v1.7.0, and B v1.7.1 is later tagged as deprecated, A can continue to use B at v1.7.0, and the maintainer of A will only become aware of the deprecation when they try to update B.
It should not be possible for the go tool to mechanically add a deprecated version to a
go.mod
file.A major version may be "un-deprecated" by publishing a subsequent minor or patch version without a
deprecated
directive in itsgo.mod
. The specific deprecated version remains unusable, but the earlier and later versions still work normally.Examples
A producer deprecating v1 of their module:
github.com/user/repo
v1.0.0, v1.1.0, ... up to v1.7.0github.com/user/repo/v2
v2.0.0, and soon afterward v2.0.1deprecated
ordeprecated github.com/user/repo/v2
line to thego.mod
of the v1 branch and tags it v1.7.1A consumer using a module that is then deprecated:
require github.com/user/repo v1.7.0
A consumer tries to fetch a module at a deprecated major version:
go get github.com/user/repo
as a new dependencygo get
selects the highest version in the v0 or v1 trees, which is v1.7.1go get
prints an error message:Composition
One of Go’s strengths is the orthogonality of its features. We believe the proposed features compose nicely and strengthen each other.
Taken separately, the proposals can stand on their own: P1 provides package consumers with useful information without direct action from package producers; P2 allows package producers to give their consumers more specific guidance on an opt-in basis.
Taken together, the proposals enrich each other: P1 improves the error messages that accompany P2; P2 funnels users into the upgrade paths created by P1.
Integrations
pkg.go.dev
The Go package discovery website at pkg.go.dev shows modules and their versions. However, it obscures successive major versions when they exist, apparently treating major module versions as completely distinct. For example, the landing page for peterbourgon/ff shows v1.7.0 with a "Latest" bubble beside it. The versions tab does list other major versions, but under the heading "Other modules containing this package", which is confusing.
Instead, pkg.go.dev could feature a prominent indicator on the landing page for a v0/v1 module that there are two successive major versions (v2 and v3), to funnel the user toward the latter.
Editor integration (gopls, goimports)
Go text editor integrations typically include a feature that automatically adds import statements to source files based on the mentioned identifiers. Because of Semantic Import Versioning, this also gives those tools the responsibility of choosing the major version of the imported module. In the case where there is no suitable existing requirement in the project’s
go.mod
file, these editor integrations could alert the user to the availability of newer major module versions. How this works is outside the scope of this proposal.Appendix
retract
directiveThe text was updated successfully, but these errors were encountered: