Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 64 additions & 42 deletions dependency_updater/dependency_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,75 +177,97 @@ func getAndUpdateDependency(ctx context.Context, client *github.Client, dependen
}

func getVersionAndCommit(ctx context.Context, client *github.Client, dependencies Dependencies, dependencyType string) (string, string, VersionUpdateInfo, error) {
var version *github.RepositoryRelease
var selectedTag *github.RepositoryTag
var commit string
var diffUrl string
var updatedDependency VersionUpdateInfo
foundPrefixVersion := false
options := &github.ListOptions{Page: 1}
currentTag := dependencies[dependencyType].Tag
tagPrefix := dependencies[dependencyType].TagPrefix

if dependencies[dependencyType].Tracking == "tag" {
// Collect all valid tags across all pages, then find the max version
var validTags []*github.RepositoryTag

for {
releases, resp, err := client.Repositories.ListReleases(
tags, resp, err := client.Repositories.ListTags(
ctx,
dependencies[dependencyType].Owner,
dependencies[dependencyType].Repo,
options)

if err != nil {
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting releases: %s", err)
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting tags: %s", err)
}

if dependencies[dependencyType].TagPrefix == "" {
version = releases[0]
if *version.TagName != dependencies[dependencyType].Tag {
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
dependencies[dependencyType].Tag + "..." + *version.TagName
}
break
} else if dependencies[dependencyType].TagPrefix != "" {
for release := range releases {
if strings.HasPrefix(*releases[release].TagName, dependencies[dependencyType].TagPrefix) {
version = releases[release]
foundPrefixVersion = true
if *version.TagName != dependencies[dependencyType].Tag {
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
dependencies[dependencyType].Tag + "..." + *version.TagName
}
break
}
for _, tag := range tags {
// Skip if tagPrefix is set and doesn't match
if tagPrefix != "" && !strings.HasPrefix(*tag.Name, tagPrefix) {
continue
}
if foundPrefixVersion {
break

// Check if this is a valid upgrade (not a downgrade)
if err := ValidateVersionUpgrade(currentTag, *tag.Name, tagPrefix); err != nil {
continue
}
options.Page = resp.NextPage
} else if resp.NextPage == 0 {

validTags = append(validTags, tag)
}

if resp.NextPage == 0 {
break
}
options.Page = resp.NextPage
}

// Find the maximum version among valid tags
for _, tag := range validTags {
// Skip if this tag can't be parsed
if _, err := ParseVersion(*tag.Name, tagPrefix); err != nil {
log.Printf("Skipping unparseable tag %s: %v", *tag.Name, err)
continue
}

if selectedTag == nil {
selectedTag = tag
continue
}

cmp, err := CompareVersions(*tag.Name, *selectedTag.Name, tagPrefix)
if err != nil {
log.Printf("Error comparing versions %s and %s: %v", *tag.Name, *selectedTag.Name, err)
continue
}
if cmp > 0 {
selectedTag = tag
}
}

// If no valid version found, keep current version
if selectedTag == nil {
log.Printf("No valid upgrade found for %s, keeping %s", dependencyType, currentTag)
return currentTag, dependencies[dependencyType].Commit, VersionUpdateInfo{}, nil
}

if *selectedTag.Name != currentTag {
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
currentTag + "..." + *selectedTag.Name
}

// Get commit SHA from the tag
commit = *selectedTag.Commit.SHA
}

if diffUrl != "" {
updatedDependency = VersionUpdateInfo{
dependencies[dependencyType].Repo,
dependencies[dependencyType].Tag,
*version.TagName,
*selectedTag.Name,
diffUrl,
}
}

if dependencies[dependencyType].Tracking == "tag" {
versionCommit, _, err := client.Repositories.GetCommit(
ctx,
dependencies[dependencyType].Owner,
dependencies[dependencyType].Repo,
"refs/tags/"+*version.TagName,
&github.ListOptions{})
if err != nil {
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting commit for "+dependencyType+": %s", err)
}
commit = *versionCommit.SHA

} else if dependencies[dependencyType].Tracking == "branch" {
if dependencies[dependencyType].Tracking == "branch" {
branchCommit, _, err := client.Repositories.ListCommits(
ctx,
dependencies[dependencyType].Owner,
Expand All @@ -270,8 +292,8 @@ func getVersionAndCommit(ctx context.Context, client *github.Client, dependencie
}
}

if version != nil {
return *version.TagName, commit, updatedDependency, nil
if selectedTag != nil {
return *selectedTag.Name, commit, updatedDependency, nil
}

return "", commit, updatedDependency, nil
Expand Down
5 changes: 4 additions & 1 deletion dependency_updater/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ require (
github.com/urfave/cli/v3 v3.3.8
)

require github.com/google/go-querystring v1.1.0 // indirect
require (
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
)
2 changes: 2 additions & 0 deletions dependency_updater/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ethereum-optimism/optimism v1.13.3 h1:rfPx7OembMnoEASU1ozA/Foa7Am7UA+h0SB+OUrxn7s=
Expand Down
92 changes: 92 additions & 0 deletions dependency_updater/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"fmt"
"regexp"
"strings"

"github.com/Masterminds/semver/v3"
)

// rcPattern matches various RC formats: -rc1, -rc.1, -rc-1, -RC1, etc.
var rcPattern = regexp.MustCompile(`(?i)-rc[.-]?(\d+)`)

// ParseVersion extracts and normalizes a semantic version from a tag string.
// It handles tagPrefix stripping, v-prefix normalization, and RC format normalization.
func ParseVersion(tag string, tagPrefix string) (*semver.Version, error) {
versionStr := tag

// Step 1: Strip tagPrefix if present (e.g., "op-node/v1.16.2" -> "v1.16.2")
if tagPrefix != "" && strings.HasPrefix(tag, tagPrefix) {
versionStr = strings.TrimPrefix(tag, tagPrefix)
versionStr = strings.TrimPrefix(versionStr, "/")
}

// Step 2: Normalize RC formats to semver-compatible format
// "-rc1" -> "-rc.1", "-rc-1" -> "-rc.1"
versionStr = normalizeRCFormat(versionStr)

// Step 3: Parse using Masterminds/semver (handles v prefix automatically)
v, err := semver.NewVersion(versionStr)
if err != nil {
return nil, fmt.Errorf("invalid version format %q: %w", tag, err)
}

return v, nil
}

// normalizeRCFormat converts various RC formats to semver-compatible format.
// Examples: "-rc1" -> "-rc.1", "-rc-2" -> "-rc.2"
func normalizeRCFormat(version string) string {
return rcPattern.ReplaceAllString(version, "-rc.$1")
}

// ValidateVersionUpgrade checks if transitioning from currentTag to newTag
// is a valid upgrade (not a downgrade).
// Returns nil if valid, error explaining why if invalid.
func ValidateVersionUpgrade(currentTag, newTag, tagPrefix string) error {
// First-time setup: no current version, any valid version is acceptable
if currentTag == "" {
_, err := ParseVersion(newTag, tagPrefix)
return err
}

// Parse current version
currentVersion, err := ParseVersion(currentTag, tagPrefix)
if err != nil {
// Current version unparseable - still validate new version is parseable
_, newErr := ParseVersion(newTag, tagPrefix)
return newErr
}

// Parse new version
newVersion, err := ParseVersion(newTag, tagPrefix)
if err != nil {
return fmt.Errorf("new version %q is not a valid semver: %w", newTag, err)
}

// Check for downgrade
if newVersion.LessThan(currentVersion) {
return fmt.Errorf(
"version downgrade detected: %s -> %s",
currentTag, newTag,
)
}

return nil
}

// CompareVersions compares two version tags and returns:
// -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
// Returns 0 and error if either version cannot be parsed.
func CompareVersions(v1Tag, v2Tag, tagPrefix string) (int, error) {
v1, err := ParseVersion(v1Tag, tagPrefix)
if err != nil {
return 0, err
}
v2, err := ParseVersion(v2Tag, tagPrefix)
if err != nil {
return 0, err
}
return v1.Compare(v2), nil
}
Loading