Skip to content

Commit f0db98b

Browse files
authored
Prefer precise version numbers in action pin algorithm (#7260)
1 parent 7be5744 commit f0db98b

4 files changed

Lines changed: 146 additions & 14 deletions

File tree

.github/workflows/daily-team-status.lock.yml

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

pkg/cli/semver.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ func parseVersion(v string) *semanticVersion {
5656
return ver
5757
}
5858

59+
// isPreciseVersion returns true if this version has explicit minor and patch components
60+
// For example, "v6.0.0" is precise, but "v6" is not
61+
func (v *semanticVersion) isPreciseVersion() bool {
62+
// Check if raw version has at least two dots (major.minor.patch format)
63+
// or at least one dot for major.minor format
64+
// "v6" -> not precise
65+
// "v6.0" -> somewhat precise (has minor)
66+
// "v6.0.0" -> precise (has minor and patch)
67+
versionPart := strings.TrimPrefix(v.raw, "v")
68+
dotCount := strings.Count(versionPart, ".")
69+
return dotCount >= 2 // Require at least major.minor.patch
70+
}
71+
5972
// isNewer returns true if this version is newer than the other
6073
func (v *semanticVersion) isNewer(other *semanticVersion) bool {
6174
if v.major != other.major {

pkg/cli/semver_precise_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestIsPreciseVersion(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
version string
11+
expected bool
12+
}{
13+
{
14+
name: "major only - not precise",
15+
version: "v6",
16+
expected: false,
17+
},
18+
{
19+
name: "major.minor - not precise",
20+
version: "v6.0",
21+
expected: false,
22+
},
23+
{
24+
name: "major.minor.patch - precise",
25+
version: "v6.0.0",
26+
expected: true,
27+
},
28+
{
29+
name: "major.minor.patch non-zero - precise",
30+
version: "v6.0.1",
31+
expected: true,
32+
},
33+
{
34+
name: "full version - precise",
35+
version: "v6.1.2",
36+
expected: true,
37+
},
38+
{
39+
name: "without v prefix - precise",
40+
version: "6.0.0",
41+
expected: true,
42+
},
43+
{
44+
name: "single digit major - not precise",
45+
version: "v1",
46+
expected: false,
47+
},
48+
{
49+
name: "three component version - precise",
50+
version: "v1.2.3",
51+
expected: true,
52+
},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
v := parseVersion(tt.version)
58+
if v == nil {
59+
t.Fatalf("Failed to parse version: %s", tt.version)
60+
}
61+
62+
result := v.isPreciseVersion()
63+
if result != tt.expected {
64+
t.Errorf("isPreciseVersion() for %q = %v, want %v", tt.version, result, tt.expected)
65+
}
66+
})
67+
}
68+
}
69+
70+
func TestPreciseVersionPreference(t *testing.T) {
71+
// Test that when comparing equal versions, precise versions are preferred
72+
v6 := parseVersion("v6")
73+
v600 := parseVersion("v6.0.0")
74+
75+
if v6 == nil || v600 == nil {
76+
t.Fatal("Failed to parse versions")
77+
}
78+
79+
// They should parse to the same major.minor.patch
80+
if v6.major != v600.major || v6.minor != v600.minor || v6.patch != v600.patch {
81+
t.Errorf("v6 and v6.0.0 should parse to same major.minor.patch, got v6=%+v, v600=%+v", v6, v600)
82+
}
83+
84+
// v6.0.0 should be precise, v6 should not
85+
if !v600.isPreciseVersion() {
86+
t.Error("v6.0.0 should be precise")
87+
}
88+
89+
if v6.isPreciseVersion() {
90+
t.Error("v6 should not be precise")
91+
}
92+
93+
// Neither should be considered "newer" than the other
94+
if v6.isNewer(v600) {
95+
t.Error("v6 should not be newer than v6.0.0")
96+
}
97+
98+
if v600.isNewer(v6) {
99+
t.Error("v6.0.0 should not be newer than v6")
100+
}
101+
}

pkg/cli/update_actions.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ func getLatestActionRelease(repo, currentVersion string, allowMajor, verbose boo
211211
if latestCompatibleVersion == nil || releaseVer.isNewer(latestCompatibleVersion) {
212212
latestCompatible = release
213213
latestCompatibleVersion = releaseVer
214+
} else if !releaseVer.isNewer(latestCompatibleVersion) &&
215+
releaseVer.major == latestCompatibleVersion.major &&
216+
releaseVer.minor == latestCompatibleVersion.minor &&
217+
releaseVer.patch == latestCompatibleVersion.patch {
218+
// If versions are equal, prefer the more precise one (e.g., "v6.0.0" over "v6")
219+
if releaseVer.isPreciseVersion() && !latestCompatibleVersion.isPreciseVersion() {
220+
latestCompatible = release
221+
latestCompatibleVersion = releaseVer
222+
}
214223
}
215224
}
216225

@@ -301,6 +310,15 @@ func getLatestActionReleaseViaGit(repo, currentVersion string, allowMajor, verbo
301310
if latestCompatibleVersion == nil || releaseVer.isNewer(latestCompatibleVersion) {
302311
latestCompatible = release
303312
latestCompatibleVersion = releaseVer
313+
} else if !releaseVer.isNewer(latestCompatibleVersion) &&
314+
releaseVer.major == latestCompatibleVersion.major &&
315+
releaseVer.minor == latestCompatibleVersion.minor &&
316+
releaseVer.patch == latestCompatibleVersion.patch {
317+
// If versions are equal, prefer the more precise one (e.g., "v6.0.0" over "v6")
318+
if releaseVer.isPreciseVersion() && !latestCompatibleVersion.isPreciseVersion() {
319+
latestCompatible = release
320+
latestCompatibleVersion = releaseVer
321+
}
304322
}
305323
}
306324

0 commit comments

Comments
 (0)