Skip to content

Commit b4e39fb

Browse files
authored
chore: add rc support for dependency updater (#909)
* chore: add rc support for dependency updater * fix: skip unparseable versions in dependency updater When the current tag couldn't be parsed (e.g., prefixed tag without tagPrefix configured), the updater would allow any tag through, potentially selecting tags from different products in the same repo. Now unparseable tags are properly skipped in both validation and max-version selection. * remove tag
1 parent 29fcaba commit b4e39fb

5 files changed

Lines changed: 349 additions & 43 deletions

File tree

dependency_updater/dependency_updater.go

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -177,75 +177,97 @@ func getAndUpdateDependency(ctx context.Context, client *github.Client, dependen
177177
}
178178

179179
func getVersionAndCommit(ctx context.Context, client *github.Client, dependencies Dependencies, dependencyType string) (string, string, VersionUpdateInfo, error) {
180-
var version *github.RepositoryRelease
180+
var selectedTag *github.RepositoryTag
181181
var commit string
182182
var diffUrl string
183183
var updatedDependency VersionUpdateInfo
184-
foundPrefixVersion := false
185184
options := &github.ListOptions{Page: 1}
185+
currentTag := dependencies[dependencyType].Tag
186+
tagPrefix := dependencies[dependencyType].TagPrefix
187+
186188
if dependencies[dependencyType].Tracking == "tag" {
189+
// Collect all valid tags across all pages, then find the max version
190+
var validTags []*github.RepositoryTag
191+
187192
for {
188-
releases, resp, err := client.Repositories.ListReleases(
193+
tags, resp, err := client.Repositories.ListTags(
189194
ctx,
190195
dependencies[dependencyType].Owner,
191196
dependencies[dependencyType].Repo,
192197
options)
193198

194199
if err != nil {
195-
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting releases: %s", err)
200+
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting tags: %s", err)
196201
}
197202

198-
if dependencies[dependencyType].TagPrefix == "" {
199-
version = releases[0]
200-
if *version.TagName != dependencies[dependencyType].Tag {
201-
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
202-
dependencies[dependencyType].Tag + "..." + *version.TagName
203-
}
204-
break
205-
} else if dependencies[dependencyType].TagPrefix != "" {
206-
for release := range releases {
207-
if strings.HasPrefix(*releases[release].TagName, dependencies[dependencyType].TagPrefix) {
208-
version = releases[release]
209-
foundPrefixVersion = true
210-
if *version.TagName != dependencies[dependencyType].Tag {
211-
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
212-
dependencies[dependencyType].Tag + "..." + *version.TagName
213-
}
214-
break
215-
}
203+
for _, tag := range tags {
204+
// Skip if tagPrefix is set and doesn't match
205+
if tagPrefix != "" && !strings.HasPrefix(*tag.Name, tagPrefix) {
206+
continue
216207
}
217-
if foundPrefixVersion {
218-
break
208+
209+
// Check if this is a valid upgrade (not a downgrade)
210+
if err := ValidateVersionUpgrade(currentTag, *tag.Name, tagPrefix); err != nil {
211+
continue
219212
}
220-
options.Page = resp.NextPage
221-
} else if resp.NextPage == 0 {
213+
214+
validTags = append(validTags, tag)
215+
}
216+
217+
if resp.NextPage == 0 {
222218
break
223219
}
220+
options.Page = resp.NextPage
224221
}
222+
223+
// Find the maximum version among valid tags
224+
for _, tag := range validTags {
225+
// Skip if this tag can't be parsed
226+
if _, err := ParseVersion(*tag.Name, tagPrefix); err != nil {
227+
log.Printf("Skipping unparseable tag %s: %v", *tag.Name, err)
228+
continue
229+
}
230+
231+
if selectedTag == nil {
232+
selectedTag = tag
233+
continue
234+
}
235+
236+
cmp, err := CompareVersions(*tag.Name, *selectedTag.Name, tagPrefix)
237+
if err != nil {
238+
log.Printf("Error comparing versions %s and %s: %v", *tag.Name, *selectedTag.Name, err)
239+
continue
240+
}
241+
if cmp > 0 {
242+
selectedTag = tag
243+
}
244+
}
245+
246+
// If no valid version found, keep current version
247+
if selectedTag == nil {
248+
log.Printf("No valid upgrade found for %s, keeping %s", dependencyType, currentTag)
249+
return currentTag, dependencies[dependencyType].Commit, VersionUpdateInfo{}, nil
250+
}
251+
252+
if *selectedTag.Name != currentTag {
253+
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
254+
currentTag + "..." + *selectedTag.Name
255+
}
256+
257+
// Get commit SHA from the tag
258+
commit = *selectedTag.Commit.SHA
225259
}
226260

227261
if diffUrl != "" {
228262
updatedDependency = VersionUpdateInfo{
229263
dependencies[dependencyType].Repo,
230264
dependencies[dependencyType].Tag,
231-
*version.TagName,
265+
*selectedTag.Name,
232266
diffUrl,
233267
}
234268
}
235269

236-
if dependencies[dependencyType].Tracking == "tag" {
237-
versionCommit, _, err := client.Repositories.GetCommit(
238-
ctx,
239-
dependencies[dependencyType].Owner,
240-
dependencies[dependencyType].Repo,
241-
"refs/tags/"+*version.TagName,
242-
&github.ListOptions{})
243-
if err != nil {
244-
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting commit for "+dependencyType+": %s", err)
245-
}
246-
commit = *versionCommit.SHA
247-
248-
} else if dependencies[dependencyType].Tracking == "branch" {
270+
if dependencies[dependencyType].Tracking == "branch" {
249271
branchCommit, _, err := client.Repositories.ListCommits(
250272
ctx,
251273
dependencies[dependencyType].Owner,
@@ -270,8 +292,8 @@ func getVersionAndCommit(ctx context.Context, client *github.Client, dependencie
270292
}
271293
}
272294

273-
if version != nil {
274-
return *version.TagName, commit, updatedDependency, nil
295+
if selectedTag != nil {
296+
return *selectedTag.Name, commit, updatedDependency, nil
275297
}
276298

277299
return "", commit, updatedDependency, nil

dependency_updater/go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ require (
88
github.com/urfave/cli/v3 v3.3.8
99
)
1010

11-
require github.com/google/go-querystring v1.1.0 // indirect
11+
require (
12+
github.com/Masterminds/semver/v3 v3.4.0 // indirect
13+
github.com/google/go-querystring v1.1.0 // indirect
14+
)

dependency_updater/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
2+
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
13
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
24
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/ethereum-optimism/optimism v1.13.3 h1:rfPx7OembMnoEASU1ozA/Foa7Am7UA+h0SB+OUrxn7s=

dependency_updater/version.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/Masterminds/semver/v3"
9+
)
10+
11+
// rcPattern matches various RC formats: -rc1, -rc.1, -rc-1, -RC1, etc.
12+
var rcPattern = regexp.MustCompile(`(?i)-rc[.-]?(\d+)`)
13+
14+
// ParseVersion extracts and normalizes a semantic version from a tag string.
15+
// It handles tagPrefix stripping, v-prefix normalization, and RC format normalization.
16+
func ParseVersion(tag string, tagPrefix string) (*semver.Version, error) {
17+
versionStr := tag
18+
19+
// Step 1: Strip tagPrefix if present (e.g., "op-node/v1.16.2" -> "v1.16.2")
20+
if tagPrefix != "" && strings.HasPrefix(tag, tagPrefix) {
21+
versionStr = strings.TrimPrefix(tag, tagPrefix)
22+
versionStr = strings.TrimPrefix(versionStr, "/")
23+
}
24+
25+
// Step 2: Normalize RC formats to semver-compatible format
26+
// "-rc1" -> "-rc.1", "-rc-1" -> "-rc.1"
27+
versionStr = normalizeRCFormat(versionStr)
28+
29+
// Step 3: Parse using Masterminds/semver (handles v prefix automatically)
30+
v, err := semver.NewVersion(versionStr)
31+
if err != nil {
32+
return nil, fmt.Errorf("invalid version format %q: %w", tag, err)
33+
}
34+
35+
return v, nil
36+
}
37+
38+
// normalizeRCFormat converts various RC formats to semver-compatible format.
39+
// Examples: "-rc1" -> "-rc.1", "-rc-2" -> "-rc.2"
40+
func normalizeRCFormat(version string) string {
41+
return rcPattern.ReplaceAllString(version, "-rc.$1")
42+
}
43+
44+
// ValidateVersionUpgrade checks if transitioning from currentTag to newTag
45+
// is a valid upgrade (not a downgrade).
46+
// Returns nil if valid, error explaining why if invalid.
47+
func ValidateVersionUpgrade(currentTag, newTag, tagPrefix string) error {
48+
// First-time setup: no current version, any valid version is acceptable
49+
if currentTag == "" {
50+
_, err := ParseVersion(newTag, tagPrefix)
51+
return err
52+
}
53+
54+
// Parse current version
55+
currentVersion, err := ParseVersion(currentTag, tagPrefix)
56+
if err != nil {
57+
// Current version unparseable - still validate new version is parseable
58+
_, newErr := ParseVersion(newTag, tagPrefix)
59+
return newErr
60+
}
61+
62+
// Parse new version
63+
newVersion, err := ParseVersion(newTag, tagPrefix)
64+
if err != nil {
65+
return fmt.Errorf("new version %q is not a valid semver: %w", newTag, err)
66+
}
67+
68+
// Check for downgrade
69+
if newVersion.LessThan(currentVersion) {
70+
return fmt.Errorf(
71+
"version downgrade detected: %s -> %s",
72+
currentTag, newTag,
73+
)
74+
}
75+
76+
return nil
77+
}
78+
79+
// CompareVersions compares two version tags and returns:
80+
// -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
81+
// Returns 0 and error if either version cannot be parsed.
82+
func CompareVersions(v1Tag, v2Tag, tagPrefix string) (int, error) {
83+
v1, err := ParseVersion(v1Tag, tagPrefix)
84+
if err != nil {
85+
return 0, err
86+
}
87+
v2, err := ParseVersion(v2Tag, tagPrefix)
88+
if err != nil {
89+
return 0, err
90+
}
91+
return v1.Compare(v2), nil
92+
}

0 commit comments

Comments
 (0)