Skip to content

Commit 0198ae9

Browse files
🌱 Add comprehensive Go module validation with retract check
Replaces basic shell check with comprehensive validator that: - Validates file paths using golang.org/x/mod/module.CheckFilePath - Ensures broken v4.10.0 remains retracted in go.mod - Creates consumer module to verify `go install` compatibility - Runs `go mod tidy` and `go build ./...` to test installability Prevents releasing broken tags and ensures v4.10.0 stays retracted.
1 parent 2342d00 commit 0198ae9

File tree

3 files changed

+244
-54
lines changed

3 files changed

+244
-54
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ test-license: ## Run the license check
190190

191191
.PHONY: test-gomod
192192
test-gomod: ## Run the Go module compatibility check
193-
./test/check-gomod.sh
193+
go run ./hack/test/check_go_module.go
194194

195195
.PHONY: test-external-plugin
196196
test-external-plugin: install ## Run tests for external plugin

hack/test/check_go_module.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Go module sanity checker validates module compatibility before release:
18+
// 1. Validates file paths with x/mod/module.CheckFilePath
19+
// 2. Ensures required retracted versions are present in go.mod
20+
// 3. Reads module path and Go version from go.mod
21+
// 4. Creates a consumer module to test installability
22+
// 5. Runs `go mod tidy` and `go build ./...` to verify module works
23+
//
24+
// This prevents releasing tags that break `go install`.
25+
//
26+
// Run with:
27+
// go run ./hack/test/check_go_module.go
28+
29+
package main
30+
31+
import (
32+
"bufio"
33+
"bytes"
34+
"fmt"
35+
log "log/slog"
36+
"os"
37+
"os/exec"
38+
"path/filepath"
39+
"strings"
40+
41+
"golang.org/x/mod/module"
42+
)
43+
44+
func main() {
45+
if err := checkFilePaths(); err != nil {
46+
log.Error("file path validation failed", "error", err)
47+
os.Exit(1)
48+
}
49+
50+
if err := checkRetractedVersions(); err != nil {
51+
log.Error("retracted version check failed", "error", err)
52+
os.Exit(1)
53+
}
54+
55+
modulePath, goVersion, err := readGoModInfo()
56+
if err != nil {
57+
log.Error("failed to read go.mod", "error", err)
58+
os.Exit(1)
59+
}
60+
61+
if err := setupAndCheckConsumer(modulePath, goVersion); err != nil {
62+
log.Error("consumer module validation failed", "error", err)
63+
os.Exit(1)
64+
}
65+
66+
log.Info("Go module compatibility check passed")
67+
}
68+
69+
func checkFilePaths() error {
70+
log.Info("Checking Go module file paths")
71+
72+
out, err := exec.Command("git", "ls-files").Output()
73+
if err != nil {
74+
return fmt.Errorf("failed to list git tracked files: %w", err)
75+
}
76+
77+
var invalidPaths []string
78+
for _, line := range strings.Split(string(out), "\n") {
79+
path := strings.TrimSpace(line)
80+
if path == "" {
81+
continue
82+
}
83+
84+
if err := module.CheckFilePath(path); err != nil {
85+
invalidPaths = append(invalidPaths, fmt.Sprintf(" %q: %v", path, err))
86+
}
87+
}
88+
89+
if len(invalidPaths) > 0 {
90+
var buf bytes.Buffer
91+
buf.WriteString("invalid file paths found:\n")
92+
for _, p := range invalidPaths {
93+
buf.WriteString(p)
94+
buf.WriteByte('\n')
95+
}
96+
return fmt.Errorf("%s", buf.String())
97+
}
98+
99+
log.Info("File path validation passed")
100+
return nil
101+
}
102+
103+
func checkRetractedVersions() error {
104+
log.Info("Checking for required retracted versions in go.mod")
105+
106+
content, err := os.ReadFile("go.mod")
107+
if err != nil {
108+
return fmt.Errorf("failed to read go.mod: %w", err)
109+
}
110+
111+
requiredRetractions := []string{
112+
"retract v4.10.0", // invalid filename causes go get/install failure (#5211)
113+
}
114+
115+
for _, retract := range requiredRetractions {
116+
if !strings.Contains(string(content), retract) {
117+
return fmt.Errorf("missing required retraction: %s", retract)
118+
}
119+
}
120+
121+
log.Info("Retracted versions check passed")
122+
return nil
123+
}
124+
125+
func readGoModInfo() (modulePath, goVersion string, err error) {
126+
log.Info("Reading module info from go.mod")
127+
128+
f, openErr := os.Open("go.mod")
129+
if openErr != nil {
130+
return "", "", fmt.Errorf("failed to open go.mod: %w", openErr)
131+
}
132+
defer func() {
133+
if closeErr := f.Close(); closeErr != nil {
134+
log.Warn("failed to close go.mod", "error", closeErr)
135+
}
136+
}()
137+
138+
sc := bufio.NewScanner(f)
139+
for sc.Scan() {
140+
line := strings.TrimSpace(sc.Text())
141+
142+
// Read module path from first line
143+
if strings.HasPrefix(line, "module ") {
144+
modulePath = strings.TrimSpace(strings.TrimPrefix(line, "module "))
145+
log.Info("Found module path", "module", modulePath)
146+
}
147+
148+
// Read Go version
149+
if strings.HasPrefix(line, "go ") {
150+
goVersion = strings.TrimSpace(strings.TrimPrefix(line, "go "))
151+
log.Info("Found Go version", "version", goVersion)
152+
}
153+
154+
// Stop once we have both
155+
if modulePath != "" && goVersion != "" {
156+
break
157+
}
158+
}
159+
160+
if modulePath == "" {
161+
return "", "", fmt.Errorf("no 'module' directive found in go.mod")
162+
}
163+
if goVersion == "" {
164+
return "", "", fmt.Errorf("no 'go' directive found in go.mod")
165+
}
166+
167+
return modulePath, goVersion, nil
168+
}
169+
170+
func setupAndCheckConsumer(modulePath, goVersion string) error {
171+
log.Info("Creating consumer module", "module", modulePath, "go_version", goVersion)
172+
173+
// Create temporary directory under hack/test/ (covered by **/e2e-*/** in .gitignore)
174+
consumerDir := filepath.Join("hack", "test", "e2e-module-check")
175+
if err := os.MkdirAll(consumerDir, 0o755); err != nil {
176+
return fmt.Errorf("failed to create temp dir: %w", err)
177+
}
178+
defer func() {
179+
if err := os.RemoveAll(consumerDir); err != nil {
180+
log.Warn("failed to cleanup temp dir", "dir", consumerDir, "error", err)
181+
}
182+
}()
183+
184+
if err := writeConsumerFiles(consumerDir, modulePath, goVersion); err != nil {
185+
return err
186+
}
187+
188+
log.Info("Running go mod tidy in consumer module")
189+
if err := runCommand(consumerDir, "go", "mod", "tidy"); err != nil {
190+
return fmt.Errorf("go mod tidy failed: %w", err)
191+
}
192+
193+
log.Info("Building consumer module")
194+
if err := runCommand(consumerDir, "go", "build", "./..."); err != nil {
195+
return fmt.Errorf("go build failed: %w", err)
196+
}
197+
198+
log.Info("Consumer module build succeeded")
199+
return nil
200+
}
201+
202+
func writeConsumerFiles(consumerDir, modulePath, goVersion string) error {
203+
goMod := fmt.Sprintf(`module module-consumer
204+
205+
go %s
206+
207+
require %s v4.0.0-00010101000000-000000000000
208+
209+
replace %s => ../../..
210+
`, goVersion, modulePath, modulePath)
211+
212+
// Use a basic import from the module to verify it can be consumed
213+
mainGo := fmt.Sprintf(`package main
214+
215+
import (
216+
_ "%s/pkg/plugins/golang/v4"
217+
)
218+
219+
func main() {}
220+
`, modulePath)
221+
222+
if err := os.WriteFile(filepath.Join(consumerDir, "go.mod"), []byte(goMod), 0o644); err != nil {
223+
return fmt.Errorf("failed to write consumer go.mod: %w", err)
224+
}
225+
226+
if err := os.WriteFile(filepath.Join(consumerDir, "main.go"), []byte(mainGo), 0o644); err != nil {
227+
return fmt.Errorf("failed to write consumer main.go: %w", err)
228+
}
229+
230+
return nil
231+
}
232+
233+
// runCommand executes a command in the specified directory with stdout/stderr connected
234+
func runCommand(dir, name string, args ...string) error {
235+
cmd := exec.Command(name, args...)
236+
cmd.Dir = dir
237+
cmd.Stdout = os.Stdout
238+
cmd.Stderr = os.Stderr
239+
if err := cmd.Run(); err != nil {
240+
return fmt.Errorf("command %s failed in %s: %w", name, dir, err)
241+
}
242+
return nil
243+
}

test/check-gomod.sh

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)