@@ -18,16 +18,30 @@ import (
1818)
1919
2020const (
21- k8sRepo = "k8s.io/kubernetes"
22- expectedMajorMinorParts = 2
23- goModFilePerms = fs .FileMode (0600 )
24- minGoListVersionFields = 2
25- minValidPatchNumber = 1
21+ k8sRepo = "k8s.io/kubernetes"
22+ expectedMajorMinorParts = 2
23+ goModFilename = "go.mod"
24+ goModFilePerms = fs .FileMode (0600 )
25+ minGoListVersionFields = 2
26+ minPatchNumberToDecrementFrom = 1 // We can only decrement patch if it's 1 or greater (to get 0 or greater)
2627)
2728
2829//nolint:gochecknoglobals
2930var goExe = "go"
3031
32+ // readAndParseGoMod reads and parses the go.mod file.
33+ func readAndParseGoMod (filename string ) ([]byte , * modfile.File , error ) {
34+ modBytes , err := os .ReadFile (filename )
35+ if err != nil {
36+ return nil , nil , fmt .Errorf ("error reading %s: %w" , filename , err )
37+ }
38+ modF , err := modfile .Parse (filename , modBytes , nil )
39+ if err != nil {
40+ return nil , nil , fmt .Errorf ("error parsing %s: %w" , filename , err )
41+ }
42+ return modBytes , modF , nil
43+ }
44+
3145func main () {
3246 log .SetFlags (0 )
3347 if os .Getenv ("GOEXE" ) != "" {
@@ -40,21 +54,16 @@ func main() {
4054 }
4155 modRoot := findModRoot (wd )
4256 if modRoot == "" {
43- log .Fatalf ("Failed to find go.mod in %s or parent directories" , wd )
57+ log .Fatalf ("Failed to find %s in %s or parent directories" , goModFilename , wd )
4458 }
4559 if err := os .Chdir (modRoot ); err != nil {
4660 log .Fatalf ("Error changing directory to %s: %v" , modRoot , err )
4761 }
4862 log .Printf ("Running in module root: %s" , modRoot )
4963
50- modBytes , err := os . ReadFile ( "go.mod" )
64+ _ , modF , err := readAndParseGoMod ( goModFilename )
5165 if err != nil {
52- log .Fatalf ("Error reading go.mod: %v" , err )
53- }
54-
55- modF , err := modfile .Parse ("go.mod" , modBytes , nil )
56- if err != nil {
57- log .Fatalf ("Error parsing go.mod: %v" , err )
66+ log .Fatal (err ) // Error already formatted by helper function
5867 }
5968
6069 // Find k8s.io/kubernetes version
@@ -66,20 +75,22 @@ func main() {
6675 }
6776 }
6877 if k8sVer == "" {
69- log .Fatalf ("Could not find %s in go.mod require block" , k8sRepo )
78+ log .Fatalf ("Could not find %s in %s require block" , k8sRepo , goModFilename )
7079 }
7180 log .Printf ("Found %s version: %s" , k8sRepo , k8sVer )
7281
7382 // Calculate target staging version
7483 if ! semver .IsValid (k8sVer ) {
7584 log .Fatalf ("Invalid semver for %s: %s" , k8sRepo , k8sVer )
7685 }
86+ // Example: k8sVer = v1.32.3
7787 majorMinor := semver .MajorMinor (k8sVer ) // e.g., "v1.32"
7888 patch := strings .TrimPrefix (k8sVer , majorMinor + "." ) // e.g., "3"
7989 if len (strings .Split (majorMinor , "." )) != expectedMajorMinorParts {
8090 log .Fatalf ("Unexpected format for MajorMinor: %s" , majorMinor )
8191 }
82- targetStagingVer := "v0" + strings .TrimPrefix (majorMinor , "v1" ) + "." + patch // e.g., "v0.32.3"
92+ // targetStagingVer becomes "v0" + ".32" + "." + "3" => "v0.32.3"
93+ targetStagingVer := "v0" + strings .TrimPrefix (majorMinor , "v1" ) + "." + patch
8394 if ! semver .IsValid (targetStagingVer ) {
8495 log .Fatalf ("Calculated invalid staging semver: %s" , targetStagingVer )
8596 }
@@ -96,7 +107,7 @@ func main() {
96107 output , err := runGoCommand ("list" , "-m" , "-json" , "all" )
97108 if err != nil {
98109 // Try downloading first if list fails
99- log .Println ("go list failed, trying go mod download..." )
110+ log .Println ("' go list' failed, trying ' go mod download' ..." )
100111 if _ , downloadErr := runGoCommand ("mod" , "download" ); downloadErr != nil {
101112 log .Fatalf ("Error running 'go mod download' after list failed: %v" , downloadErr )
102113 }
@@ -121,15 +132,17 @@ func main() {
121132 continue
122133 }
123134
124- // Use replacement path if it exists
135+ // Use replacement path if it exists, but skip local file replacements
125136 effectivePath := mod .Path
126137 if mod .Replace != nil {
127- effectivePath = mod .Replace .Path
128- // Skip local file replacements
129- if ! strings .Contains (effectivePath , "." ) { // Basic check if it looks like a module path vs local path
130- log .Printf ("Skipping local replace: %s => %s" , mod .Path , effectivePath )
138+ // Heuristic: Assume module paths have a domain-like structure (e.g., 'xxx.yyy/zzz') in the first segment.
139+ // Local paths usually don't (e.g., '../othermod', './local').
140+ parts := strings .SplitN (mod .Replace .Path , "/" , 2 )
141+ if len (parts ) > 0 && ! strings .Contains (parts [0 ], "." ) {
142+ log .Printf ("Skipping local replace: %s => %s" , mod .Path , mod .Replace .Path )
131143 continue
132144 }
145+ effectivePath = mod .Replace .Path
133146 }
134147
135148 // Check existence of target version, fallback to previous patch if needed
@@ -152,33 +165,39 @@ func main() {
152165 pins [mod .Path ] = determinedVer
153166 }
154167
155- // Add k8s.io/kubernetes itself to the pins map
168+ // Add k8s.io/kubernetes itself to the pins map (ensures it's covered by the replace logic)
156169 pins [k8sRepo ] = k8sVer
157170 log .Printf ("Identified %d k8s.io/* modules to manage." , len (pins ))
158171
159- // 7. Parse go.mod again (to have a fresh modfile object)
160- modBytes , err = os .ReadFile ("go.mod" )
161- if err != nil {
162- log .Fatalf ("Error reading go.mod again: %v" , err )
163- }
164- modF , err = modfile .Parse ("go.mod" , modBytes , nil )
172+ // Parse go.mod again (needed in case `go list` modified it)
173+ _ , modF , err = readAndParseGoMod (goModFilename )
165174 if err != nil {
166- log .Fatalf ( " Error parsing go.mod again: %v" , err )
175+ log .Fatal ( err ) // Error already formatted by helper function
167176 }
168177
169178 // Remove all existing k8s.io/* replaces
170179 log .Println ("Removing existing k8s.io/* replace directives..." )
171180 var replacesToRemove []string
172181 for _ , rep := range modF .Replace {
173- if strings .HasPrefix (rep .Old .Path , "k8s.io/" ) {
182+ // Only remove replaces targeting k8s.io/* modules (not local replacements like ../staging)
183+ // assumes standard module paths contain '.'
184+ if strings .HasPrefix (rep .Old .Path , "k8s.io/" ) && strings .Contains (rep .New .Path , "." ) {
174185 replacesToRemove = append (replacesToRemove , rep .Old .Path )
186+ } else if strings .HasPrefix (rep .Old .Path , "k8s.io/" ) {
187+ log .Printf ("Note: Found existing non-module replace for %s, leaving untouched: %s => %s %s" , rep .Old .Path , rep .Old .Path , rep .New .Path , rep .New .Version )
175188 }
176189 }
177- for _ , path := range replacesToRemove {
178- if err := modF .DropReplace (path , "" ); err != nil {
179- // Tolerate errors if the replace was already somehow removed
180- log .Printf ("Note: Error dropping replace for %s (might be benign): %v" , path , err )
190+ if len (replacesToRemove ) > 0 {
191+ for _ , path := range replacesToRemove {
192+ log .Printf ("Removing replace for: %s" , path )
193+ // Drop replace expects oldPath and oldVersion. Version is empty for path-only replaces.
194+ if err := modF .DropReplace (path , "" ); err != nil {
195+ // Tolerate errors if the replace was already somehow removed or structure changed
196+ log .Printf ("Note: Error dropping replace for %s (might be benign): %v" , path , err )
197+ }
181198 }
199+ } else {
200+ log .Println ("No existing k8s.io/* module replaces found to remove." )
182201 }
183202
184203 // Add new replace directives
@@ -193,7 +212,6 @@ func main() {
193212 for _ , path := range sortedPaths {
194213 version := pins [path ]
195214 // Add replace for the module path itself (e.g., k8s.io/api => k8s.io/api v0.32.3)
196- // This handles cases where the effective path from `go list` might differ due to other replaces
197215 if err := modF .AddReplace (path , "" , path , version ); err != nil {
198216 log .Fatalf ("Error adding replace for %s => %s %s: %v" , path , path , version , err )
199217 }
@@ -202,30 +220,31 @@ func main() {
202220
203221 // Write go.mod
204222 log .Println ("Writing updated go.mod..." )
205- modF .Cleanup () // Sort blocks, etc.
223+ modF .Cleanup () // Sort blocks, remove redundant directives etc.
206224 newModBytes , err := modF .Format ()
207225 if err != nil {
208226 log .Fatalf ("Error formatting go.mod: %v" , err )
209227 }
210- if err := os .WriteFile ("go.mod" , newModBytes , goModFilePerms ); err != nil {
211- log .Fatalf ("Error writing go.mod : %v" , err )
228+ if err := os .WriteFile (goModFilename , newModBytes , goModFilePerms ); err != nil {
229+ log .Fatalf ("Error writing %s : %v" , goModFilename , err )
212230 }
213231
214232 // Run `go mod tidy`
215- goVer := modF .Go .Version
233+ goVer := ""
234+ if modF .Go != nil { // Ensure Go directive exists before accessing Version
235+ goVer = modF .Go .Version
236+ }
216237 tidyArgs := []string {"mod" , "tidy" }
217238 if goVer != "" {
218239 tidyArgs = append (tidyArgs , fmt .Sprintf ("-go=%s" , goVer ))
219- log .Printf ("Running 'go mod tidy -go=%s'..." , goVer )
220- } else {
221- log .Println ("Running 'go mod tidy'..." )
222240 }
241+ log .Printf ("Running '%s %s'..." , goExe , strings .Join (tidyArgs , " " ))
223242 if _ , err := runGoCommand (tidyArgs ... ); err != nil {
224243 log .Fatalf ("Error running 'go mod tidy': %v" , err )
225244 }
226245
227246 // Run `go mod download k8s.io/kubernetes`
228- log .Printf ("Running 'go mod download %s'..." , k8sRepo )
247+ log .Printf ("Running '%s mod download %s'..." , goExe , k8sRepo )
229248 if _ , err := runGoCommand ("mod" , "download" , k8sRepo ); err != nil {
230249 // This might not be fatal, could be network issues, but log it prominently
231250 log .Printf ("WARNING: Error running 'go mod download %s': %v" , k8sRepo , err )
@@ -237,7 +256,7 @@ func main() {
237256// findModRoot searches for go.mod in dir and parent directories
238257func findModRoot (dir string ) string {
239258 for {
240- if _ , err := os .Stat (filepath .Join (dir , "go.mod" )); err == nil {
259+ if _ , err := os .Stat (filepath .Join (dir , goModFilename )); err == nil {
241260 return dir
242261 }
243262 parent := filepath .Dir (dir )
@@ -264,16 +283,24 @@ func runGoCommand(args ...string) ([]byte, error) {
264283// getModuleVersions retrieves the list of available versions for a module
265284func getModuleVersions (modulePath string ) ([]string , error ) {
266285 output , err := runGoCommand ("list" , "-m" , "-versions" , modulePath )
286+ // Combine output and error message for checking because 'go list' sometimes writes errors to stdout
287+ combinedOutput := string (output )
288+ if err != nil {
289+ combinedOutput += err .Error ()
290+ }
291+
292+ // Check if the error/output indicates "no matching versions" - this is not a fatal error for our logic
293+ if strings .Contains (combinedOutput , "no matching versions" ) {
294+ return []string {}, nil // Return empty list, not an error
295+ }
296+ // If there was an actual error beyond "no matching versions"
267297 if err != nil {
268- // Check if the error is "no matching versions" - this is not a fatal error for our logic
269- if strings .Contains (string (output )+ err .Error (), "no matching versions" ) {
270- return []string {}, nil // Return empty list, not an error
271- }
272298 return nil , fmt .Errorf ("error listing versions for %s: %w" , modulePath , err )
273299 }
300+
274301 fields := strings .Fields (string (output ))
275302 if len (fields ) < minGoListVersionFields {
276- return []string {}, nil // No versions listed
303+ return []string {}, nil // No versions listed (e.g., just the module path)
277304 }
278305 return fields [1 :], nil // First field is the module path
279306}
@@ -301,10 +328,18 @@ func getLatestExistingVersion(modulePath, targetVer string) (string, error) {
301328 majorMinor := semver .MajorMinor (targetVer ) // e.g., v0.32
302329 patchStr := strings .TrimPrefix (targetVer , majorMinor + "." ) // e.g., 3
303330 var patch int
304- if _ , err := fmt .Sscan (patchStr , & patch ); err != nil || patch < minValidPatchNumber {
305- log .Printf ("Could not parse patch version or patch < %d for %s, cannot determine predecessor." , minValidPatchNumber , targetVer )
331+ // Use Sscan to parse the integer patch number
332+ if _ , err := fmt .Sscan (patchStr , & patch ); err != nil {
333+ log .Printf ("Could not parse patch version from %s for module %s: %v. Cannot determine predecessor." , targetVer , modulePath , err )
306334 return "" , nil // Cannot determine predecessor
307335 }
336+
337+ // Only try to decrement if the patch number is >= the minimum required to do so
338+ if patch < minPatchNumberToDecrementFrom {
339+ log .Printf ("Patch version %d is less than %d for %s, cannot determine predecessor." , patch , minPatchNumberToDecrementFrom , targetVer )
340+ return "" , nil // Cannot determine predecessor (e.g., target was v0.32.0)
341+ }
342+
308343 prevPatchVer := fmt .Sprintf ("%s.%d" , majorMinor , patch - 1 ) // e.g., v0.32.2
309344
310345 foundPrev := false
0 commit comments