@@ -10,7 +10,11 @@ import (
1010 "os/exec"
1111 "path/filepath"
1212 "regexp"
13+ "sort"
1314 "strings"
15+
16+ "github.com/Masterminds/semver/v3"
17+ "golang.org/x/mod/modfile"
1418)
1519
1620const (
@@ -47,31 +51,101 @@ func findGoModDirs(rootFolder, subDir string) ([]string, error) {
4751 return goModDirs , nil
4852}
4953
50- func getLastTag (pathPrefix string ) (string , error ) {
54+ func getRetractedTags (goModPath string ) ([]* semver.Constraints , error ) {
55+ data , err := os .ReadFile (goModPath )
56+ if err != nil {
57+ return nil , fmt .Errorf ("error reading go.mod file: %w" , err )
58+ }
59+
60+ modFile , err := modfile .Parse ("go.mod" , data , nil )
61+ if err != nil {
62+ return nil , fmt .Errorf ("error parsing go.mod file: %w" , err )
63+ }
64+
65+ var retractedTags []* semver.Constraints
66+ for _ , retract := range modFile .Retract {
67+ lowVersion , err := semver .NewVersion (retract .Low )
68+ if err != nil {
69+ return nil , fmt .Errorf ("error parsing retracted version: %w" , err )
70+ }
71+ highVersion , err := semver .NewVersion (retract .High )
72+ if err != nil {
73+ return nil , fmt .Errorf ("error parsing retracted version: %w" , err )
74+ }
75+ constraint , err := semver .NewConstraint (fmt .Sprintf (">= %s, <= %s" , lowVersion .String (), highVersion .String ()))
76+ if err != nil {
77+ return nil , fmt .Errorf ("error parsing retracted version: %w" , err )
78+ }
79+ retractedTags = append (retractedTags , constraint )
80+ fmt .Printf ("Retracted version: %s\n " , constraint )
81+ }
82+
83+ return retractedTags , nil
84+ }
85+
86+ func getLatestTag (pathPrefix string , retractedTags []* semver.Constraints ) (string , error ) {
87+ // use regex to find exact matches, as otherwise might include pre-release versions
88+ // or versions that partially match the path prefix, e.g. when seraching for 'lib'
89+ // we won't make sure we won't include tags like `lib/grafana/v1.0.0`
90+ grepRegex := fmt .Sprintf ("^%s/v[0-9]+\\ .[0-9]+\\ .[0-9]+$" , pathPrefix )
91+
5192 //nolint
52- cmd := exec .Command ("sh" , "-c" , fmt .Sprintf ("git tag | grep '%s' | tail -1 " , pathPrefix ))
93+ cmd := exec .Command ("sh" , "-c" , fmt .Sprintf ("git tag | grep -E '%s'" , grepRegex ))
5394 var out bytes.Buffer
5495 cmd .Stdout = & out
5596 err := cmd .Run ()
5697 if err != nil {
5798 return "" , fmt .Errorf ("error fetching tags: %w" , err )
5899 }
59100
60- tag := strings .TrimSpace (out .String ())
61- if tag == "" {
62- return "" , nil
101+ tags := strings .Split ( strings . TrimSpace (out .String ()), " \n " )
102+ if len ( tags ) == 0 {
103+ return "" , fmt . Errorf ( "no tags found for regex: %s" , grepRegex )
63104 }
64105
65- // Use regex to find the version tag starting with 'v'
66- re := regexp .MustCompile (`v\d+\.\d+\.\d+` )
67- matches := re .FindStringSubmatch (tag )
68- if len (matches ) > 0 {
69- tag = matches [0 ]
70- } else {
71- return "" , fmt .Errorf ("no valid version tag found in '%s'" , tag )
106+ // Parse the tags into semver versions
107+ var allTags []* semver.Version
108+ for _ , tag := range tags {
109+ v , err := semver .NewVersion (strings .TrimPrefix (tag , pathPrefix + "/" ))
110+ if err != nil {
111+ return "" , fmt .Errorf ("error parsing version tag: %w" , err )
112+ }
113+ allTags = append (allTags , v )
114+ }
115+
116+ // Sort the tags in descending order
117+ sort .Sort (sort .Reverse (semver .Collection (allTags )))
118+
119+ if len (retractedTags ) == 0 {
120+ tag := fmt .Sprintf ("v%s" , allTags [0 ].String ())
121+ return tag , nil
122+ }
123+
124+ // Find the latest tag that doesn't match any of the retracted tags
125+ for _ , tag := range allTags {
126+ isRetracted := false
127+ for _ , constraint := range retractedTags {
128+ if constraint .Check (tag ) {
129+ isRetracted = true
130+ break
131+ }
132+ }
133+
134+ if ! isRetracted {
135+ tag := fmt .Sprintf ("v%s" , tag .String ())
136+ fmt .Printf ("Found non-retracted tag: %s\n " , tag )
137+ return tag , nil
138+ }
72139 }
73140
74- return tag , nil
141+ fmt .Println ("No non-retracted tags found" )
142+ fmt .Printf ("All tags: %s\n " , strings .Join (tags , ", " ))
143+ fmt .Println ("Retracted tags:" )
144+ for _ , constraint := range retractedTags {
145+ fmt .Printf ("%s\n " , constraint )
146+ }
147+
148+ return "" , fmt .Errorf ("failed to find a non-retracted tag got path prefix: %s" , pathPrefix )
75149}
76150
77151func checkBreakingChanges (tag string ) (string , string , error ) {
@@ -94,13 +168,15 @@ func getIgnoredDirs(flag *string) []string {
94168 return ignoredDirs
95169}
96170
97- func isIgnoredDirPrefix (pathPrefix string , ignoredDirs []string ) bool {
171+ func isIgnoredDirRegex (pathPrefix string , ignoredDirs []string ) bool {
98172 for _ , d := range ignoredDirs {
99173 if d == "" {
100174 continue
101175 }
102- fmt .Printf ("Checking prefix: %s, path: %s\n " , d , pathPrefix )
103- if strings .HasPrefix (pathPrefix , d ) {
176+
177+ fmt .Printf ("Checking regex: %s, path: %s\n " , d , pathPrefix )
178+ re := regexp .MustCompile (d )
179+ if re .MatchString (pathPrefix ) {
104180 fmt .Printf ("Path is ignored, skipping: %s\n " , pathPrefix )
105181 return true
106182 }
@@ -111,7 +187,7 @@ func isIgnoredDirPrefix(pathPrefix string, ignoredDirs []string) bool {
111187func main () {
112188 rootFolder := flag .String ("root" , "." , "The root folder to start scanning from" )
113189 subDir := flag .String ("subdir" , "" , "The subdirectory inside the root folder to scan for modules" )
114- ignoreDirs := flag .String ("ignore" , "" , "Ignore directory paths starting with prefix " )
190+ ignoreDirs := flag .String ("ignore" , "" , "Ignore directory paths matching regex (comma-separated) " )
115191 flag .Parse ()
116192
117193 absRootFolder , err := filepath .Abs (* rootFolder )
@@ -133,24 +209,30 @@ func main() {
133209 // Convert the stripped path back to absolute
134210 pathPrefix := strings .TrimPrefix (dirPath , absRootFolder + string (os .PathSeparator ))
135211
136- if isIgnoredDirPrefix (pathPrefix , ignoredDirs ) {
212+ if isIgnoredDirRegex (pathPrefix , ignoredDirs ) {
213+ continue
214+ }
215+
216+ retractedVersions , err := getRetractedTags (filepath .Join (dirPath , "go.mod" ))
217+ if err != nil {
218+ fmt .Printf ("Error getting retracted versions: %v\n " , err )
137219 continue
138220 }
139221
140- lastTag , err := getLastTag (pathPrefix )
222+ latestTag , err := getLatestTag (pathPrefix , retractedVersions )
141223 if err != nil {
142- fmt .Printf ("Error finding last tag: %v\n " , err )
224+ fmt .Printf ("Error finding latest tag: %v\n " , err )
143225 continue
144226 }
145227
146- if lastTag != "" {
228+ if latestTag != "" {
147229 fmt .Printf ("%sProcessing directory: %s%s\n " , Yellow , dirPath , Reset )
148230 if err := os .Chdir (dirPath ); err != nil {
149231 fmt .Printf ("Error changing directory: %v\n " , err )
150232 continue
151233 }
152234
153- stdout , stderr , err := checkBreakingChanges (lastTag )
235+ stdout , stderr , err := checkBreakingChanges (latestTag )
154236 if err != nil {
155237 fmt .Printf ("Error running gorelease: %v\n " , err )
156238 breakingChanges = true
0 commit comments