Skip to content

ci: add module compatibility check - #4723

Merged
vvoland merged 2 commits into
docker:masterfrom
thaJeztah:govalidator
Apr 1, 2026
Merged

ci: add module compatibility check#4723
vvoland merged 2 commits into
docker:masterfrom
thaJeztah:govalidator

Conversation

@thaJeztah

Copy link
Copy Markdown
Member

This package imports all "importable" packages, i.e., packages that:

  • are not applications ("main")
  • are not internal
  • and that have non-test go-files

We do this to verify that our code can be consumed as a dependency in "module mode". When using a dependency that does not have a go.mod (i.e.; is not a "module"), go implicitly generates a go.mod. Lacking information from the dependency itself, it assumes "go1.16" language (see DefaultGoModVersion). Starting with Go1.21, go downgrades the language version used for such dependencies, which means that any language feature used that is not supported by go1.16 results in a compile error;

# github.com/docker/cli/cli/context/store
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)

These errors do NOT occur when using GOPATH mode, nor do they occur when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod" approach used in this repository).

As a workaround for this situation, we must include "//go:build" comments in any file that uses newer go-language features (such as the "any" type or the "min()", "max()" builtins).

From the go toolchain docs (https://go.dev/doc/toolchain):

The go line for each module sets the language version the compiler enforces
when compiling packages in that module. The language version can be changed
on a per-file basis by using a build constraint.

For example, a module containing code that uses the Go 1.21 language version
should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
If a specific source file should be compiled only when using a newer Go
toolchain, adding //go:build go1.22 to that source file both ensures that
only Go 1.22 and newer toolchains will compile the file and also changes
the language version in that file to Go 1.22.

This file is a generated module that imports all packages provided in the repository, which replicates an external consumer using our code as a dependency in go-module mode, and verifies all files in those packages have the correct "//go:build " set.

To test this package:

make shell
make -C ./internal/gocompat/
make: Entering directory '/go/src/github.com/docker/cli/internal/gocompat'
GO111MODULE=off go generate .
GO111MODULE=on go mod tidy
GO111MODULE=on go test -v
# github.com/docker/cli/templates
../../templates/templates.go:13:17: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/compose/template
../../cli/compose/template/template.go:98:45: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/template/template.go:105:27: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/template/template.go:141:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/compose/types
../../cli/compose/types/types.go:53:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:86:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:105:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:137:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:211:20: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:343:35: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:442:40: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:469:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:490:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:587:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:442:40: too many errors
# github.com/docker/cli/cli/context/store
../../cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/store.go:75:23: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/metadatastore.go:43:58: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/metadatastore.go:48:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/metadatastore.go:80:30: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/command/idresolver
../../cli/command/idresolver/idresolver.go:6:2: "github.com/docker/docker/api/types" imported and not used
../../cli/command/idresolver/idresolver.go:7:2: "github.com/docker/docker/api/types/swarm" imported and not used
../../cli/command/idresolver/idresolver.go:9:2: "github.com/pkg/errors" imported and not used
../../cli/command/idresolver/idresolver.go:28:49: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/command/idresolver/idresolver.go:58:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/compose/schema
../../cli/compose/schema/schema.go:20:46: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/schema/schema.go:27:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/schema/schema.go:45:32: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/schema/schema.go:66:33: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
FAIL	gocompat [build failed]
make: *** [Makefile:3: verify] Error 1
make: Leaving directory '/go/src/github.com/docker/cli/internal/gocompat'

- What I did

- How I did it

- How to verify it

- Description for the changelog

- A picture of a cute animal (not mandatory but encouraged)

@codecov-commenter

codecov-commenter commented Dec 15, 2023

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@laurazard

Copy link
Copy Markdown
Member

This is going to get rid of so many headaches in the future! Thanks @thaJeztah :')

@thaJeztah

Copy link
Copy Markdown
MemberAuthor

Was still deciding whether we should merge this one as-is (but we don't have a flow yet to run it cleanly, which could be some stage in the Dockerfile), or if we could make this work without that; per moby/moby#46941 (comment)

Good news! go vetwill be able to perform the module compatibility check itself starting in Go 1.23.

@thaJeztahthaJeztah modified the milestones: v-future, 29.3.1Mar 16, 2026
@thaJeztah
thaJeztah marked this pull request as ready for review March 16, 2026 19:40
@thaJeztah
thaJeztah requested a review from CopilotMarch 16, 2026 19:41

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an automated “module consumer compatibility” verification by generating a temporary module that imports all importable packages, ensuring files that use newer Go language features are properly guarded with //go:build constraints when the repo is consumed as a dependency in module mode.

Changes:

  • Add internal/gocompat generator + Makefile to build a synthetic module and compile-import all packages.
  • Vendor golang.org/x/mod and update vendor.mod/vendor.sum to support parsing/formatting go.mod content.
  • Add a new GitHub Actions job to run the gocompat check in CI.

Reviewed changes

Copilot reviewed 7 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
.github/workflows/validate.ymlAdds a CI job to run the new gocompat verification.
internal/gocompat/.gitignoreIgnores generated module/test artifacts in internal/gocompat.
internal/gocompat/MakefileAdds generate and verify targets for the gocompat check.
internal/gocompat/generate.goAdds go:generate entrypoint for the generator.
internal/gocompat/modulegenerator.goImplements the generator that writes a synthetic go.mod and imports all packages.
vendor.modAdds golang.org/x/mod (and a tool line) to support the generator.
vendor.sumAdds checksums for the new golang.org/x/mod version.
vendor/modules.txtRegisters vendored golang.org/x/mod packages.
vendor/golang.org/x/mod/LICENSEAdds vendored license for golang.org/x/mod.
vendor/golang.org/x/mod/PATENTSAdds vendored patents notice for golang.org/x/mod.
vendor/golang.org/x/mod/internal/lazyregexp/lazyre.goVendored x/mod internal helper used by the new dependency.
vendor/golang.org/x/mod/modfile/print.goVendored x/mod/modfile formatting support.
vendor/golang.org/x/mod/modfile/read.goVendored x/mod/modfile parsing support.
vendor/golang.org/x/mod/modfile/rule.goVendored x/mod/modfile editing/AST logic.
vendor/golang.org/x/mod/modfile/work.goVendored x/mod/modfile go.work support.
vendor/golang.org/x/mod/module/module.goVendored x/mod/module utilities.
vendor/golang.org/x/mod/module/pseudo.goVendored x/mod/module pseudo-version helpers.
vendor/golang.org/x/mod/semver/semver.goVendored x/mod/semver utilities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment thread.github/workflows/validate.yml Outdated
Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment threadinternal/gocompat/modulegenerator.go Outdated

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an internal “module mode compatibility” check that simulates consuming this repository as a dependency (with an implicit go1.16 language default) by generating a small module that imports all importable packages and running it in CI. This helps ensure files using newer Go language features are guarded with appropriate //go:build go1.xx constraints so external consumers don’t hit unexpected compile failures.

Changes:

  • Add internal/gocompat generator + Makefile to build a generated module that imports all non-main, non-internal packages.
  • Add a new CI job to run the gocompat verification.
  • Vendor golang.org/x/mod (and update vendor.mod / vendor.sum) to generate/parse go.mod content for the check.

Reviewed changes

Copilot reviewed 7 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
.github/workflows/validate.ymlAdds a CI job to run the gocompat verification.
internal/gocompat/.gitignoreIgnores generated module artifacts (go.mod/go.sum/main_test.go).
internal/gocompat/MakefileAdds generate and verify targets to produce and test the generated module.
internal/gocompat/generate.goAdds go:generate entrypoint to run the module generator.
internal/gocompat/modulegenerator.goImplements generation of the test file importing all packages and the generated go.mod.
vendor.modAdds golang.org/x/mod (and a tool entry) to support go.mod generation/parsing for gocompat.
vendor.sumAdds checksums for golang.org/x/mod v0.32.0.
vendor/modules.txtRecords vendored golang.org/x/mod packages.
vendor/golang.org/x/mod/LICENSEVendors upstream license text for golang.org/x/mod.
vendor/golang.org/x/mod/PATENTSVendors upstream patents grant for golang.org/x/mod.
vendor/golang.org/x/mod/internal/lazyregexp/lazyre.goVendors lazy regexp helper used by x/mod.
vendor/golang.org/x/mod/modfile/print.goVendors go.mod printer used by generator/parsing.
vendor/golang.org/x/mod/modfile/read.goVendors go.mod parser/AST used by generator/parsing.
vendor/golang.org/x/mod/modfile/rule.goVendors go.mod rule handling needed by modfile.Parse and edits.
vendor/golang.org/x/mod/modfile/work.goVendors go.work support used by x/mod/modfile.
vendor/golang.org/x/mod/module/module.goVendors module path/version logic used by x/mod/modfile.
vendor/golang.org/x/mod/module/pseudo.goVendors pseudo-version helpers used by module logic.
vendor/golang.org/x/mod/semver/semver.goVendors semver helpers used by module logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment threadinternal/gocompat/modulegenerator.go Outdated
Comment threadinternal/gocompat/Makefile Outdated

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an internal/gocompat verification module intended to catch Go “language version downgrades” that happen when this repository is consumed as a dependency in module mode without an upstream go.mod, and updates vendoring to support generating/parsing the required module files.

Changes:

  • Introduces internal/gocompat generator + Makefile to build a synthetic module that imports all non-internal, non-main packages and runs go test to surface language-version/build-tag issues.
  • Vendors golang.org/x/mod (and updates vendor.mod, vendor.sum, vendor/modules.txt) so the generator can manipulate vendor.mod via modfile.
  • Adds a GitHub Actions job to run the gocompat verification in CI.

Reviewed changes

Copilot reviewed 7 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
vendor/modules.txtAdds vendored module listing entries for golang.org/x/mod.
vendor/golang.org/x/mod/semver/semver.goNew vendored x/mod semver implementation.
vendor/golang.org/x/mod/module/pseudo.goNew vendored x/mod pseudo-version helpers.
vendor/golang.org/x/mod/module/module.goNew vendored x/mod module path/version validation utilities.
vendor/golang.org/x/mod/modfile/work.goNew vendored x/mod go.work parsing/editing.
vendor/golang.org/x/mod/modfile/rule.goNew vendored x/mod go.mod parsing/editing logic (used by generator).
vendor/golang.org/x/mod/modfile/read.goNew vendored x/mod parser infrastructure.
vendor/golang.org/x/mod/modfile/print.goNew vendored x/mod formatter/printer.
vendor/golang.org/x/mod/internal/lazyregexp/lazyre.goNew vendored lazy regexp wrapper.
vendor/golang.org/x/mod/PATENTSNew vendored licensing file for x/mod.
vendor/golang.org/x/mod/LICENSENew vendored licensing file for x/mod.
vendor.sumAdds sums for golang.org/x/mod v0.32.0.
vendor.modAdds golang.org/x/mod dependency and a tool entry for the generator’s modfile usage.
internal/gocompat/modulegenerator.goGenerator that builds the import-all test and synthesizes a module based on vendor.mod.
internal/gocompat/generate.goAdds go:generate entrypoint to run the generator.
internal/gocompat/MakefileAdds generate/verify/clean targets for running the compatibility check.
internal/gocompat/.gitignoreIgnores generated module/test artifacts.
.github/workflows/validate.ymlAdds CI job to run the gocompat verification.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadinternal/gocompat/modulegenerator.go
Comment on lines +59 to +75
modFile := filepath.Join(rootDir, "go.mod")
_, err := os.Stat(modFile)
if err == nil {
return errors.New("go.mod exists in the repository root")
}

// create an empty go.mod without go version.
//
// this go.mod must exist when running the test.
err = os.WriteFile(modFile, []byte("module github.com/docker/cli\n"), 0o644)
if err != nil {
return fmt.Errorf("failed to write go.mod: %w", err)
}
defer func() {
if retErr != nil {
_ = os.Remove(modFile)
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving that for a future exercise.

Comment threadinternal/gocompat/Makefile Outdated
Comment thread.github/workflows/validate.yml Outdated
@thaJeztah
thaJeztahforce-pushed the govalidator branch 2 times, most recently from d6e7d9d to ebb067dCompareMarch 25, 2026 14:50
@thaJeztahthaJeztah changed the title implement module compatibility checkci: add module compatibility checkMar 25, 2026
@thaJeztah

Copy link
Copy Markdown
MemberAuthor

Good news; it works; bad news; we missed one (again); only on master though;

Error: ../../cli-plugins/hooks/template.go:25:19: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
Error: ../../cli-plugins/hooks/template.go:26:19: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)

This package imports all "importable" packages, i.e., packages that:
- are not applications ("main")
- are not internal
- and that have non-test go-files
We do this to verify that our code can be consumed as a dependency
in "module mode". When using a dependency that does not have a go.mod
(i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
information from the dependency itself, it assumes "go1.16" language
(see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
language version used for such dependencies, which means that any
language feature used that is not supported by go1.16 results in a
compile error;
# github.com/docker/cli/cli/context/store
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
/go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
These errors do NOT occur when using GOPATH mode, nor do they occur
when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
approach used in this repository).
As a workaround for this situation, we must include "//go:build" comments
in any file that uses newer go-language features (such as the "any" type
or the "min()", "max()" builtins).
From the go toolchain docs (https://go.dev/doc/toolchain):
> The go line for each module sets the language version the compiler enforces
> when compiling packages in that module. The language version can be changed
> on a per-file basis by using a build constraint.
>
> For example, a module containing code that uses the Go 1.21 language version
> should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
> If a specific source file should be compiled only when using a newer Go
> toolchain, adding //go:build go1.22 to that source file both ensures that
> only Go 1.22 and newer toolchains will compile the file and also changes
> the language version in that file to Go 1.22.
This file is a generated module that imports all packages provided in
the repository, which replicates an external consumer using our code
as a dependency in go-module mode, and verifies all files in those
packages have the correct "//go:build <go language version>" set.
To test this package:
make shell
make -C ./internal/gocompat/
make: Entering directory '/go/src/github.com/docker/cli/internal/gocompat'
GO111MODULE=off go generate .
GO111MODULE=on go mod tidy
GO111MODULE=on go test -v
# github.com/docker/cli/templates
../../templates/templates.go:13:17: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/compose/template
../../cli/compose/template/template.go:98:45: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/template/template.go:105:27: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/template/template.go:141:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/compose/types
../../cli/compose/types/types.go:53:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:86:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:105:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:137:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:211:20: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:343:35: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:442:40: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:469:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:490:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:587:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/types/types.go:442:40: too many errors
# github.com/docker/cli/cli/context/store
../../cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/store.go:75:23: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/metadatastore.go:43:58: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/metadatastore.go:48:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/context/store/metadatastore.go:80:30: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/command/idresolver
../../cli/command/idresolver/idresolver.go:6:2: "github.com/docker/docker/api/types" imported and not used
../../cli/command/idresolver/idresolver.go:7:2: "github.com/docker/docker/api/types/swarm" imported and not used
../../cli/command/idresolver/idresolver.go:9:2: "github.com/pkg/errors" imported and not used
../../cli/command/idresolver/idresolver.go:28:49: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/command/idresolver/idresolver.go:58:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
# github.com/docker/cli/cli/compose/schema
../../cli/compose/schema/schema.go:20:46: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/schema/schema.go:27:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/schema/schema.go:45:32: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
../../cli/compose/schema/schema.go:66:33: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
FAIL	gocompat [build failed]
make: *** [Makefile:3: verify] Error 1
make: Leaving directory '/go/src/github.com/docker/cli/internal/gocompat'
[DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
[2]: https://go.dev/doc/toolchain
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@vvoland
vvoland merged commit fea2465 into docker:masterApr 1, 2026
91 of 92 checks passed
@thaJeztah
thaJeztah deleted the govalidator branch April 1, 2026 14:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@thaJeztah@codecov-commenter@laurazard@vvoland