go: proposal: cmd/go: ignore +incompatible versions as of Go 1.14

Abstract

This is a proposal to ignore +incompatible versions found in the module graph starting with Go version 1.14.

Background

The go command requires that the import path of a module (or package within a module) match its semantically-versioned API. In particular, starting with major version 2 — the first breaking change from the API stabilized in major version 1 — the module path must end with a /vN suffix indicating the major version of its API.

However, prior to the introduction of modules, many Go package maintainers had already reused existing import paths (such as github.com/google/go-github) across multiple major versions. To accommodate the migration to modules for users of those packages, the vgo prototype — and the initial module support released in the go command in Go 1.11 — allowed those existing major versions to be used directly, and even preferred them over compatible versions (#26238) under two conditions:

  1. The module with the incompatible version must not contain an explicit go.mod file.

  2. The version must be annotated in the user’s go.mod and go.sum files with the suffix +incompatible, indicating that the selected version is not compatible with the original API for that path.

Unfortunately, those exceptions introduce a number of problems:

  • #34189: If a user adopts modules (by adding an explicit go.mod file) and then accidentally tags a pre-module commit with an inappropriate major version, the erroneous pre-module tag will always be semantically higher than the highest valid module-enabled release tag (1.99[…].99[…]).

  • #27009: If a user has already tagged a major version above 1 and adopts modules (by adding an explicit go.mod file), then they must either adopt the major-subdirectory layout for their project (disrupting developers on the project), or break upgrades for non-module users (who will be expecting the unversioned import path).

  • #31543: Since the damage of introducing breaking changes is already done, users often expect that their v2-or-higher repository can be migrated to modules without changing its import path, but for various reasons that does not work in the general case.

  • #33795, #32695: Since +metadata tags in general are not semantically meaningful, the fact that +incompatible is semantically meaningful requires numerous special cases that are difficult to test and maintain, and creates confusion about when incompatible versions are or are not allowed.

  • #29731: The possibility of using a +incompatible version led us to support major-version wildcard queries, which are only useful for legacy repos and interfere with more useful branch queries.

  • #34254: The constraints on +incompatible versions derive from the module path, but that introduces even more complexity (and makes things more difficult to debug) with replace directives, which involve two module paths that may or may not impose the same constraints on the corresponding versions.


In contrast, for another interesting case of legacy tagging — semantic versions with metadata (#31713) — we came up with what I believe is a simpler solution: instead of accepting the non-canonical version tags as-is, we instead rewrite them to canonical pseudo-versions with an appropriate major version.

In light of the problems we have encountered with incompatible major versions, I believe that we should have applied a similar strategy for incompatible versions: perhaps using them for “latest” version resolution, but rewriting them to canonical pseudo-versions.

Unfortunately, the decision was made, and cannot be unmade in light of our subsequent experience without breaking compatibility.


…or can it?

Observation

Since a +incompatible version cannot have an explicit go.mod file, it cannot impose any transitive requirements on module selection. Therefore, a +incompatible version selected as the minimal version of a module cannot impact the version selected for any other module.

This implies that if we ignore the +incompatible versions in the module graph entirely, we will not accidentally drop requirements that pertain to other modules.

This leads to the following proposal (see the comment below; updates will be linked from here).

CC @jayconrod @thepudds @hyangah @katiehockman @heschik

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 4
  • Comments: 40 (37 by maintainers)

Commits related to this issue

Most upvoted comments

I’d like to reiterate the previous commenters’ support for keeping resolved versions the same. For large projects, maintaining the exact versions of dependencies can be tricky. If dependencies change when moving to a new version of Go, that version of Go will be “tainted” by the associated breakage.

For any plan moving forward I think that, as a minimum requirement, modules should continue to build with exactly the same resolved dependencies on at least the last two versions of Go. People need to be able try out the latest version while still having the option to drop back to the previous version.

So anything landing in 1.14 should change the go.mod file in such a way that 1.13 arrives at the same resolved dependencies. I suspect that a phased plan, with the final phase landing in 1.15, might be the best way forward here.

perhaps this could get rolled out across two releases

A go 1.13 binary working in a go 1.14 module would already untidy it in many cases (replacing v0 or v1 pseudo-versions with +incompatible versions from transitive dependencies), and at that point there doesn’t seem to be a significant advantage to delaying the error-check for go 1.14 modules.

If we’re gating on go 1.14, we could treat +incompatible versions the same way we treat branch names (like master): in the main module, they’re allowed but resolved to a valid version or pseudo-version; in other modules, they’re rejected with an error. I think an error would be preferable to ignoring them.

One problem with this: adding go 1.14 to go.mod does not prevent old versions of the Go command from adding +incompatible versions. So a module could be broken unintentionally in a mixed environment.