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 }