github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/scripts/ci/verify-pr/main.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/ActiveState/cli/internal/errs"
    11  	"github.com/ActiveState/cli/internal/httputil"
    12  	wc "github.com/ActiveState/cli/scripts/internal/workflow-controllers"
    13  	wh "github.com/ActiveState/cli/scripts/internal/workflow-helpers"
    14  	"github.com/andygrunwald/go-jira"
    15  	"github.com/blang/semver"
    16  	"github.com/google/go-github/v45/github"
    17  	"golang.org/x/net/context"
    18  )
    19  
    20  /*
    21  - Pushes to fixVersion PR should verify that it has all the intended PRs for that version
    22  - PRs should always have a fixVersion value
    23  */
    24  
    25  func main() {
    26  	if err := run(); err != nil {
    27  		wc.Print("Error: %s\n", errs.JoinMessage(err))
    28  		os.Exit(1)
    29  	}
    30  }
    31  
    32  func run() error {
    33  	// Validate Input
    34  	{
    35  		// Verify input args
    36  		if len(os.Args) != 2 {
    37  			return errs.New("Usage: verify-pr <pr-number>")
    38  		}
    39  	}
    40  
    41  	prID, err := strconv.Atoi(os.Args[1])
    42  	if err != nil {
    43  		return errs.Wrap(err, "PR number should be numeric")
    44  	}
    45  
    46  	finish := wc.PrintStart("Initializing clients")
    47  	ghClient := wh.InitGHClient()
    48  	jiraClient, err := wh.InitJiraClient()
    49  	if err != nil {
    50  		return errs.Wrap(err, "Failed to initialize JIRA client")
    51  	}
    52  	finish()
    53  
    54  	finish = wc.PrintStart("Fetching PR")
    55  	pr, _, err := ghClient.PullRequests.Get(context.Background(), "ActiveState", "cli", prID)
    56  	if err != nil {
    57  		return errs.Wrap(err, "Failed to fetch PR")
    58  	}
    59  	finish()
    60  
    61  	if wh.IsVersionBranch(pr.GetHead().GetRef()) {
    62  		finish = wc.PrintStart("Verifying Version PR")
    63  		if err := verifyVersionRC(ghClient, jiraClient, pr); err != nil {
    64  			return errs.Wrap(err, "Failed to Version PR")
    65  		}
    66  		finish()
    67  	}
    68  	finish = wc.PrintStart("Verifying PR")
    69  	if err := verifyPR(jiraClient, pr); err != nil {
    70  		return errs.Wrap(err, "Failed to verify PR")
    71  	}
    72  	finish()
    73  
    74  	return nil
    75  }
    76  
    77  func verifyVersionRC(ghClient *github.Client, jiraClient *jira.Client, pr *github.PullRequest) error {
    78  	if pr.GetBase().GetRef() != wh.StagingBranch {
    79  		return errs.New("PR should be targeting the staging branch: '%s'", wh.StagingBranch)
    80  	}
    81  
    82  	finish := wc.PrintStart("Parsing version from PR title")
    83  	version, err := wh.VersionFromPRTitle(pr.GetTitle())
    84  	if err != nil {
    85  		return errs.Wrap(err, "Failed to parse version from PR title")
    86  	}
    87  	wc.Print("Version: %s", version)
    88  	finish()
    89  
    90  	finish = wc.PrintStart("Fetching Jira issues targeting %s", version)
    91  	issues, _, err := jiraClient.Issue.Search(fmt.Sprintf(
    92  		`project = "DX" AND fixVersion=v%s ORDER BY statusCategoryChangedDate ASC`,
    93  		version), nil)
    94  	if err != nil {
    95  		return errs.Wrap(err, "Failed to fetch JIRA issues, does the version 'v%s' exist on Jira?", version)
    96  	}
    97  
    98  	found := map[string]bool{}
    99  	jiraIDs := map[string]jira.Issue{}
   100  	for _, issue := range issues {
   101  		if issue.Fields == nil || issue.Fields.Status == nil {
   102  			return errs.New("Jira fields and/or status properties are nil, this should never happen..")
   103  		}
   104  		jiraIDs[strings.ToUpper(issue.Key)] = issue
   105  		found[strings.ToUpper(issue.Key)] = false
   106  	}
   107  	finish()
   108  
   109  	finish = wc.PrintStart("Fetching previous version PR")
   110  	prevVersionPR, err := wh.FetchVersionPR(ghClient, wh.AssertLT, version)
   111  	if err != nil {
   112  		return errs.Wrap(err,
   113  			"Failed to find previous version PR for %s.", version.String())
   114  	}
   115  	wc.Print("Got: %s\n", prevVersionPR.GetTitle())
   116  	finish()
   117  
   118  	finish = wc.PrintStart("Verifying we have all the commits from the previous version PR, comparing %s to %s", pr.Head.GetRef(), prevVersionPR.Head.GetRef())
   119  	behind, err := wh.GetCommitsBehind(ghClient, prevVersionPR.Head.GetRef(), pr.Head.GetRef())
   120  	if err != nil {
   121  		return errs.Wrap(err, "Failed to compare to previous version PR")
   122  	}
   123  	if len(behind) > 0 {
   124  		commits := []string{}
   125  		for _, c := range behind {
   126  			commits = append(commits, c.GetSHA()+": "+c.GetCommit().GetMessage())
   127  		}
   128  		return errs.New("PR is behind the previous version PR (%s) by %d commits, missing commits:\n%s",
   129  			prevVersionPR.GetTitle(), len(behind), strings.Join(commits, "\n"))
   130  	}
   131  	finish()
   132  
   133  	finish = wc.PrintStart("Fetching commits for PR %d", pr.GetNumber())
   134  	commits, err := wh.FetchCommitsByRef(ghClient, pr.GetHead().GetSHA(), func(commit *github.RepositoryCommit) bool {
   135  		return commit.GetSHA() == prevVersionPR.GetHead().GetSHA()
   136  	})
   137  	if err != nil {
   138  		return errs.Wrap(err, "Failed to fetch commits")
   139  	}
   140  	finish()
   141  
   142  	finish = wc.PrintStart("Matching commits against jira issues")
   143  	for _, commit := range commits {
   144  		key, err := wh.ParseJiraKey(commit.GetCommit().GetMessage())
   145  		if err != nil {
   146  			continue
   147  		}
   148  		key = strings.ToUpper(key) // ParseJiraKey already does this, but it's implicit
   149  
   150  		if _, ok := jiraIDs[key]; ok {
   151  			found[key] = true
   152  		}
   153  	}
   154  
   155  	notFound := []string{}
   156  	notFoundCritical := []string{}
   157  	for jiraID, isFound := range found {
   158  		if !isFound {
   159  			issue := jiraIDs[jiraID]
   160  			if wh.IsMergedStatus(issue.Fields.Status.Name) {
   161  				notFoundCritical = append(notFoundCritical, issue.Key+": "+jiraIDs[jiraID].Fields.Summary)
   162  			} else {
   163  				notFound = append(notFound, issue.Key+": "+jiraIDs[jiraID].Fields.Summary)
   164  			}
   165  		}
   166  	}
   167  
   168  	sort.Strings(notFound)
   169  	sort.Strings(notFoundCritical)
   170  
   171  	if len(notFound) > 0 {
   172  		return errs.New("PR not ready as it's still missing commits for the following JIRA issues:\n"+
   173  			"Pending story completion:\n%s\n\n"+
   174  			"Missing stories:\n%s", strings.Join(notFound, "\n"), strings.Join(notFoundCritical, "\n"))
   175  	}
   176  	finish()
   177  
   178  	return nil
   179  }
   180  
   181  func verifyPR(jiraClient *jira.Client, pr *github.PullRequest) error {
   182  	finish := wc.PrintStart("Parsing Jira issue from PR title")
   183  	jiraIssueID, err := wh.ExtractJiraIssueID(pr)
   184  	if err != nil {
   185  		return errs.Wrap(err, "Failed to extract JIRA issue ID from PR")
   186  	}
   187  	wc.Print("JIRA Issue: %s\n", jiraIssueID)
   188  	finish()
   189  
   190  	finish = wc.PrintStart("Fetching Jira issue %s", jiraIssueID)
   191  	jiraIssue, err := wh.FetchJiraIssue(jiraClient, jiraIssueID)
   192  	if err != nil {
   193  		return errs.Wrap(err, "Failed to fetch JIRA issue")
   194  	}
   195  	finish()
   196  
   197  	finish = wc.PrintStart("Fetching Jira Versions")
   198  	availableVersions, err := wh.FetchAvailableVersions(jiraClient)
   199  	if err != nil {
   200  		return errs.Wrap(err, "Failed to fetch JIRA issue")
   201  	}
   202  	finish()
   203  
   204  	// Grab latest version on release channel to use as cutoff
   205  	finish = wc.PrintStart("Fetching latest version on release channel")
   206  	latestReleaseversionBytes, err := httputil.Get("https://raw.githubusercontent.com/ActiveState/cli/release/version.txt")
   207  	if err != nil {
   208  		return errs.Wrap(err, "failed to fetch latest release version")
   209  	}
   210  	latestReleaseversion, err := semver.Parse(strings.TrimSpace(string(latestReleaseversionBytes)))
   211  	if err != nil {
   212  		return errs.Wrap(err, "failed to parse version blob")
   213  	}
   214  	wc.Print("Latest version on release channel: %s", latestReleaseversion)
   215  	finish()
   216  
   217  	finish = wc.PrintStart("Verifying fixVersion")
   218  	version, _, err := wh.ParseTargetFixVersion(jiraIssue, availableVersions)
   219  	if err != nil {
   220  		return errs.Wrap(err, "Failed to parse fixVersion")
   221  	}
   222  	finish()
   223  
   224  	if !version.EQ(wh.VersionMaster) {
   225  		// Ensure we have a valid version
   226  		if version.LTE(latestReleaseversion) {
   227  			return errs.New("Target fixVersion is either is less than the latest release version")
   228  		}
   229  	}
   230  
   231  	finish = wc.PrintStart("Validating target branch")
   232  	if err := wh.ValidVersionBranch(pr.GetBase().GetRef(), version.Version); err != nil {
   233  		return errs.Wrap(err, "Invalid target branch, ensure your PR is targeting a versioned branch")
   234  	}
   235  	finish()
   236  
   237  	return nil
   238  }