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  }