github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/scripts/internal/workflow-helpers/jira.go (about) 1 package workflow_helpers 2 3 import ( 4 "io" 5 "os" 6 "regexp" 7 "sort" 8 "strings" 9 10 "github.com/ActiveState/cli/internal/errs" 11 "github.com/ActiveState/cli/internal/logging" 12 "github.com/ActiveState/cli/internal/testhelpers/secrethelper" 13 "github.com/andygrunwald/go-jira" 14 "github.com/blang/semver" 15 "github.com/google/go-github/v45/github" 16 ) 17 18 var jiraIssueRx = regexp.MustCompile(`(?i)(DX-\d+)`) 19 20 const JiraStatusTodo = "To Do" 21 const JiraStatusInProgress = "In Progress" 22 const JiraStatusPending = "Pending" 23 24 func InitJiraClient() (*jira.Client, error) { 25 username := secrethelper.GetSecretIfEmpty(os.Getenv("JIRA_USERNAME"), "user.JIRA_USERNAME") 26 password := secrethelper.GetSecretIfEmpty(os.Getenv("JIRA_TOKEN"), "user.JIRA_TOKEN") 27 28 tp := &jira.BasicAuthTransport{ 29 Username: username, 30 Password: password, 31 } 32 jiraClient, err := jira.NewClient(tp.Client(), "https://activestatef.atlassian.net/") 33 if err != nil { 34 return nil, errs.Wrap(err, "Failed to create JIRA client") 35 } 36 return jiraClient, nil 37 } 38 39 func ParseJiraKey(v string) (string, error) { 40 matches := jiraIssueRx.FindStringSubmatch(v) 41 if len(matches) < 1 { 42 return "", errs.New("Could not extract jira key from %s, please ensure it matches the regex: %s", v, jiraIssueRx.String()) 43 } 44 return strings.ToUpper(matches[1]), nil 45 } 46 47 func JqlUnpaged(client *jira.Client, jql string) ([]jira.Issue, error) { 48 result := []jira.Issue{} 49 perPage := 100 50 51 for x := 0; x < 100; x++ { // hard limit of 100,000 commits 52 issues, _, err := client.Issue.Search(jql, &jira.SearchOptions{ 53 StartAt: x * perPage, 54 MaxResults: perPage, 55 }) 56 if err != nil { 57 return nil, errs.Wrap(err, "Failed to search JIRA") 58 } 59 result = append(result, issues...) 60 if len(issues) < perPage { 61 break 62 } 63 } 64 65 return result, nil 66 } 67 68 func ParseJiraVersion(version string) (semver.Version, error) { 69 return semver.Parse(ParseJiraVersionRaw(version)) 70 } 71 72 func ParseJiraVersionRaw(version string) string { 73 return strings.TrimPrefix(version, "v") 74 } 75 76 func FetchJiraIssue(jiraClient *jira.Client, jiraIssueID string) (*jira.Issue, error) { 77 jiraIssue, _, err := jiraClient.Issue.Get(jiraIssueID, nil) 78 if err != nil { 79 return nil, errs.Wrap(err, "failed to get Jira issue") 80 } 81 82 return jiraIssue, nil 83 } 84 85 type Version struct { 86 semver.Version 87 JiraID string 88 } 89 90 func FetchAvailableVersions(jiraClient *jira.Client) ([]Version, error) { 91 pj, _, err := jiraClient.Project.Get("DX") 92 if err != nil { 93 return nil, errs.Wrap(err, "Failed to get JIRA project") 94 } 95 96 emptySemver := semver.Version{} 97 result := []Version{} 98 for _, version := range pj.Versions { 99 if version.Archived != nil && *version.Archived { 100 continue 101 } 102 if version.Released != nil && *version.Released { 103 continue 104 } 105 semversion, err := ParseJiraVersion(version.Name) 106 if err != nil || semversion.EQ(emptySemver) { 107 logging.Debug("Not a semver version %s; skipping", version.Name) 108 continue 109 } 110 result = append(result, Version{semversion, version.ID}) 111 } 112 113 sort.Slice(result, func(i, j int) bool { 114 return result[i].LT(result[j].Version) 115 }) 116 117 return result, nil 118 } 119 120 var VersionMaster = semver.MustParse("0.0.0") 121 122 func ParseTargetFixVersion(issue *jira.Issue, availableVersions []Version) (target Version, original *jira.FixVersion, err error) { 123 if len(issue.Fields.FixVersions) < 1 { 124 return Version{}, nil, errs.New("Jira issue does not have a fixVersion assigned: %s\n", issue.Key) 125 } 126 127 if len(issue.Fields.FixVersions) > 1 { 128 return Version{}, nil, errs.New("Jira issue has multiple fixVersions assigned: %s. This is incompatible with our workflow.", issue.Key) 129 } 130 131 fixVersion := issue.Fields.FixVersions[0] 132 if fixVersion.Archived != nil && *fixVersion.Archived || fixVersion.Released != nil && *fixVersion.Released { 133 return Version{}, nil, errs.New("fixVersion '%s' has either been archived or released\n", fixVersion.Name) 134 } 135 136 switch fixVersion.Name { 137 case VersionNextFeasible: 138 target, err := ParseJiraVersion(strings.Split(fixVersion.Description, " ")[0]) 139 if err != nil { 140 return Version{}, nil, errs.Wrap(err, "Failed to parse Jira version from description: %s", fixVersion.Description) 141 } 142 if len(availableVersions) < 1 { 143 return Version{}, nil, errs.New("No feasible versions available") 144 } 145 for _, version := range availableVersions { 146 if version.EQ(target) { 147 return version, fixVersion, nil 148 } 149 } 150 return Version{}, nil, errs.New("Next feasible version does not exist: %s", target.String()) 151 case VersionNextUnscheduled: 152 return Version{VersionMaster, ""}, fixVersion, nil 153 } 154 155 v, err := ParseJiraVersion(fixVersion.Name) 156 return Version{v, fixVersion.ID}, fixVersion, err 157 } 158 159 func IsMergedStatus(status string) bool { 160 if strings.HasPrefix(status, "Ready for") || status == "Done" || strings.Contains(status, "Testing") { 161 return true 162 } 163 return false 164 } 165 166 func FetchJiraIDsInCommits(commits []*github.RepositoryCommit) []string { 167 found := []string{} 168 for _, commit := range commits { 169 key, err := ParseJiraKey(commit.GetCommit().GetMessage()) 170 if err != nil { 171 continue 172 } 173 found = append(found, strings.ToUpper(key)) 174 } 175 return found 176 } 177 178 func UpdateJiraFixVersion(client *jira.Client, issue *jira.Issue, versionID string) error { 179 issueUpdate := &jira.Issue{ 180 ID: issue.ID, 181 Key: issue.Key, 182 Fields: &jira.IssueFields{ 183 FixVersions: []*jira.FixVersion{{ID: versionID}}, 184 }, 185 } 186 187 if len(issue.Fields.FixVersions) > 0 { 188 fixVersion := issue.Fields.FixVersions[0] 189 switch fixVersion.Name { 190 case VersionNextFeasible: 191 issueUpdate.Fields.Labels = append(issueUpdate.Fields.Labels, "WasNextFeasible") 192 case VersionNextUnscheduled: 193 issueUpdate.Fields.Labels = append(issueUpdate.Fields.Labels, "WasNextUnscheduled") 194 } 195 } 196 197 issueUpdate.Fields.FixVersions = []*jira.FixVersion{{ID: versionID}} 198 _, response, err := client.Issue.Update(issueUpdate) 199 res, err2 := io.ReadAll(response.Body) 200 if err2 != nil { 201 res = []byte(err2.Error()) 202 } 203 if err != nil { 204 return errs.Wrap(err, string(res)) 205 } 206 return nil 207 } 208 209 func UpdateJiraStatus(client *jira.Client, issue *jira.Issue, statusName string) error { 210 transitions, _, err := client.Issue.GetTransitions(issue.ID) 211 if err != nil { 212 return errs.Wrap(err, "failed to get Jira transitions") 213 } 214 215 var transition *jira.Transition 216 for _, t := range transitions { 217 if t.To.Name == statusName { 218 transition = &t 219 break 220 } 221 } 222 if transition == nil { 223 return errs.New("failed to find a Jira transition that changes the status to %s for issue %s", statusName, issue.Key) 224 } 225 226 response, err := client.Issue.DoTransition(issue.ID, transition.ID) 227 if err != nil && response == nil { 228 return errs.Wrap(err, "failed to perform Jira transition") 229 } 230 231 // Include response body in error 232 res, err2 := io.ReadAll(response.Body) 233 if err2 != nil { 234 res = []byte(err2.Error()) 235 } 236 if err != nil { 237 return errs.Wrap(err, string(res)) 238 } 239 return nil 240 }