Skip to content

Commit fea2465

Browse files
authored
Merge pull request #4723 from thaJeztah/govalidator
ci: add module compatibility check
2 parents 538ee85 + 58c1585 commit fea2465

File tree

18 files changed

+5263
-0
lines changed

18 files changed

+5263
-0
lines changed

.github/workflows/validate.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,26 @@ jobs:
8080
shell: 'script --return --quiet --command "bash {0}"'
8181
run: |
8282
make -f docker.Makefile ${{ matrix.target }}
83+
84+
validate-gocompat:
85+
runs-on: ubuntu-24.04
86+
env:
87+
GOPATH: ${{ github.workspace }}
88+
GO111MODULE: off
89+
steps:
90+
-
91+
name: Checkout
92+
uses: actions/checkout@v6
93+
with:
94+
path: src/github.com/docker/cli
95+
-
96+
name: Set up Go
97+
uses: actions/setup-go@v6
98+
with:
99+
go-version: "1.25.8"
100+
-
101+
name: Run gocompat check
102+
shell: 'script --return --quiet --command "bash {0}"'
103+
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
104+
run: |
105+
make -C ./internal/gocompat verify

internal/gocompat/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
go.mod
2+
go.sum
3+
main.go
4+
main_test.go

internal/gocompat/Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.PHONY: verify
2+
verify: generate
3+
@GO111MODULE=on go test -v; status=$$?; \
4+
@$(MAKE) clean || true; \
5+
exit $$status
6+
7+
.PHONY: generate
8+
generate: clean
9+
GO111MODULE=off go generate .
10+
GO111MODULE=on go mod tidy
11+
12+
.PHONY: clean
13+
clean:
14+
@rm -f go.mod go.sum main.go main_test.go ../../go.mod

internal/gocompat/generate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package main
2+
3+
//go:generate go run modulegenerator.go
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//go:build ignore
2+
// +build ignore
3+
4+
package main
5+
6+
import (
7+
"bytes"
8+
"errors"
9+
"fmt"
10+
"log"
11+
"os"
12+
"os/exec"
13+
"path/filepath"
14+
"strings"
15+
"text/template"
16+
17+
"golang.org/x/mod/modfile"
18+
)
19+
20+
func main() {
21+
if err := generateApp(); err != nil {
22+
log.Fatal(err)
23+
}
24+
rootDir := "../../"
25+
if err := generateModule(rootDir); err != nil {
26+
log.Fatal(err)
27+
}
28+
}
29+
30+
func generateApp() error {
31+
cmd := exec.Command("go", "list", "-find", "-f", `{{- if ne .Name "main"}}{{if .GoFiles}}{{.ImportPath}}{{end}}{{end -}}`, "../../...")
32+
out, err := cmd.CombinedOutput()
33+
if err != nil {
34+
return fmt.Errorf("go list failed: %w\nOutput:\n%s", err, string(out))
35+
}
36+
37+
var pkgs []string
38+
for _, p := range strings.Split(string(out), "\n") {
39+
if strings.TrimSpace(p) == "" || strings.Contains(p, "/internal") {
40+
continue
41+
}
42+
pkgs = append(pkgs, p)
43+
}
44+
tmpl, err := template.New("main").Parse(appTemplate)
45+
if err != nil {
46+
return err
47+
}
48+
49+
var buf bytes.Buffer
50+
err = tmpl.Execute(&buf, appContext{Generator: cmd.String(), Packages: pkgs})
51+
if err != nil {
52+
return err
53+
}
54+
55+
return os.WriteFile("main_test.go", buf.Bytes(), 0o644)
56+
}
57+
58+
func generateModule(rootDir string) (retErr error) {
59+
modFile := filepath.Join(rootDir, "go.mod")
60+
_, err := os.Stat(modFile)
61+
if err == nil {
62+
return errors.New("go.mod exists in the repository root")
63+
}
64+
if !errors.Is(err, os.ErrNotExist) {
65+
return fmt.Errorf("failed to stat go.mod: %w", err)
66+
}
67+
68+
// create an empty go.mod without go version.
69+
//
70+
// this go.mod must exist when running the test.
71+
err = os.WriteFile(modFile, []byte("module github.com/docker/cli\n"), 0o644)
72+
if err != nil {
73+
return fmt.Errorf("failed to write go.mod: %w", err)
74+
}
75+
defer func() {
76+
if retErr != nil {
77+
_ = os.Remove(modFile)
78+
}
79+
}()
80+
81+
content, err := os.ReadFile(filepath.Join(rootDir, "vendor.mod"))
82+
if err != nil {
83+
return err
84+
}
85+
mod, err := modfile.Parse(filepath.Join(rootDir, "vendor.mod"), content, nil)
86+
if err != nil {
87+
return err
88+
}
89+
if err := mod.AddModuleStmt("example.com/gocompat"); err != nil {
90+
return err
91+
}
92+
if err := mod.AddReplace("github.com/docker/cli", "", rootDir, ""); err != nil {
93+
return err
94+
}
95+
if err := mod.AddGoStmt("1.24"); err != nil {
96+
return err
97+
}
98+
out, err := mod.Format()
99+
if err != nil {
100+
return err
101+
}
102+
tmpl, err := template.New("mod").Parse(modTemplate)
103+
if err != nil {
104+
return err
105+
}
106+
107+
gen, err := os.Executable()
108+
if err != nil {
109+
return err
110+
}
111+
112+
var buf bytes.Buffer
113+
err = tmpl.Execute(&buf, appContext{Generator: gen, Dependencies: string(out)})
114+
if err != nil {
115+
return err
116+
}
117+
118+
return os.WriteFile("go.mod", buf.Bytes(), 0o644)
119+
}
120+
121+
type appContext struct {
122+
Generator string
123+
Packages []string
124+
Dependencies string
125+
}
126+
127+
const appTemplate = `// Code generated by "{{ .Generator }}". DO NOT EDIT.
128+
129+
package main_test
130+
131+
import (
132+
"testing"
133+
134+
// Import all importable packages, i.e., packages that:
135+
//
136+
// - are not applications ("main")
137+
// - are not internal
138+
// - and that have non-test go-files
139+
{{- range .Packages }}
140+
_ "{{ . }}"
141+
{{- end}}
142+
)
143+
144+
// This file imports all "importable" packages, i.e., packages that:
145+
//
146+
// - are not applications ("main")
147+
// - are not internal
148+
// - and that have non-test go-files
149+
//
150+
// We do this to verify that our code can be consumed as a dependency
151+
// in "module mode". When using a dependency that does not have a go.mod
152+
// (i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
153+
// information from the dependency itself, it assumes "go1.16" language
154+
// (see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
155+
// language version used for such dependencies, which means that any
156+
// language feature used that is not supported by go1.16 results in a
157+
// compile error;
158+
//
159+
// # github.com/docker/cli/cli/context/store
160+
// /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)
161+
// /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)
162+
//
163+
// These errors do NOT occur when using GOPATH mode, nor do they occur
164+
// when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
165+
// approach used in this repository).
166+
//
167+
// As a workaround for this situation, we must include "//go:build" comments
168+
// in any file that uses newer go-language features (such as the "any" type
169+
// or the "min()", "max()" builtins).
170+
//
171+
// From the go toolchain docs (https://go.dev/doc/toolchain):
172+
//
173+
// > The go line for each module sets the language version the compiler enforces
174+
// > when compiling packages in that module. The language version can be changed
175+
// > on a per-file basis by using a build constraint.
176+
// >
177+
// > For example, a module containing code that uses the Go 1.21 language version
178+
// > should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
179+
// > If a specific source file should be compiled only when using a newer Go
180+
// > toolchain, adding //go:build go1.22 to that source file both ensures that
181+
// > only Go 1.22 and newer toolchains will compile the file and also changes
182+
// > the language version in that file to Go 1.22.
183+
//
184+
// This file is a generated module that imports all packages provided in
185+
// the repository, which replicates an external consumer using our code
186+
// as a dependency in go-module mode, and verifies all files in those
187+
// packages have the correct "//go:build <go language version>" set.
188+
//
189+
// [DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
190+
// [2]: https://go.dev/doc/toolchain
191+
func TestModuleCompatibility(t *testing.T) {
192+
t.Log("all packages have the correct go version specified through //go:build")
193+
}
194+
`
195+
196+
const modTemplate = `// Code generated by "{{ .Generator }}". DO NOT EDIT.
197+
198+
{{.Dependencies}}
199+
`

vendor.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ module github.com/docker/cli
66

77
go 1.24.0
88

9+
tool golang.org/x/mod/modfile // for module compatibility check
10+
911
require (
1012
dario.cat/mergo v1.0.2
1113
github.com/containerd/errdefs v1.0.0
@@ -98,6 +100,7 @@ require (
98100
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
99101
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
100102
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
103+
golang.org/x/mod v0.32.0 // indirect
101104
golang.org/x/net v0.50.0 // indirect
102105
golang.org/x/time v0.14.0 // indirect
103106
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect

vendor.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
236236
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
237237
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
238238
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
239+
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
240+
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
239241
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
240242
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
241243
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

vendor/golang.org/x/mod/LICENSE

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/golang.org/x/mod/PATENTS

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)