github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/scripts/ci/propagate-pr/main.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "os" 6 "strconv" 7 "strings" 8 9 "github.com/ActiveState/cli/internal/environment" 10 "github.com/ActiveState/cli/internal/errs" 11 "github.com/ActiveState/cli/internal/osutils" 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 type Meta struct { 21 Repo *github.Repository 22 ActivePR *github.PullRequest 23 ActiveStory *jira.Issue 24 ActiveVersion semver.Version 25 } 26 27 type MergeIntends []MergeIntend 28 29 type MergeIntend struct { 30 SourceBranch string 31 TargetBranch string 32 } 33 34 func (m MergeIntend) String() string { 35 return fmt.Sprintf("Merge %s into %s", m.SourceBranch, m.TargetBranch) 36 } 37 38 func (m MergeIntends) String() string { 39 v := "" 40 for _, vv := range m { 41 v += fmt.Sprintf("\n%s", vv.String()) 42 } 43 return v 44 } 45 46 func main() { 47 if err := run(); err != nil { 48 wc.Print("Error: %s\n", errs.JoinMessage(err)) 49 os.Exit(1) 50 } 51 } 52 53 func run() error { 54 finish := wc.PrintStart("Initializing clients") 55 // Initialize Clients 56 ghClient := wh.InitGHClient() 57 jiraClient, err := wh.InitJiraClient() 58 if err != nil { 59 return errs.Wrap(err, "failed to initialize Jira client") 60 } 61 finish() 62 63 // Grab input 64 if len(os.Args) != 2 { 65 return errs.New("Usage: propagate-pr <pr-number>") 66 } 67 prNumber, err := strconv.Atoi(os.Args[1]) 68 if err != nil { 69 return errs.Wrap(err, "pr number should be numeric") 70 } 71 72 finish = wc.PrintStart("Fetching meta for PR %d", prNumber) 73 // Collect meta information about the PR and all it's related resources 74 meta, err := fetchMeta(ghClient, jiraClient, prNumber) 75 if err != nil { 76 return errs.Wrap(err, "failed to fetch meta") 77 } 78 finish() 79 80 if meta.ActiveVersion.EQ(wh.VersionMaster) { 81 wc.Print("Target version is master, no propagation required") 82 return nil 83 } 84 85 // Find open version PRs 86 finish = wc.PrintStart("Finding open version PRs that need to adopt this PR") 87 versionPRs, err := wh.FetchVersionPRs(ghClient, wh.AssertGT, meta.ActiveVersion, -1) 88 if err != nil { 89 return errs.Wrap(err, "failed to fetch version PRs") 90 } 91 finish() 92 93 // Parse merge intends 94 intend := MergeIntends{} 95 currentBranch := meta.ActivePR.GetBase().GetRef() 96 targetBranches := []string{} 97 for _, pr := range versionPRs { 98 if pr.GetState() != "open" { 99 return errs.Wrap(err, "Version PR %d is not open, does the source PR have the right fixVersion associated?", pr.GetNumber()) 100 } 101 intend = append(intend, MergeIntend{ 102 SourceBranch: currentBranch, 103 TargetBranch: pr.GetHead().GetRef(), 104 }) 105 targetBranches = append(targetBranches, pr.GetHead().GetRef()) 106 currentBranch = pr.GetHead().GetRef() 107 } 108 109 // Always end with master 110 intend = append(intend, MergeIntend{ 111 SourceBranch: currentBranch, 112 TargetBranch: wh.MasterBranch, 113 }) 114 targetBranches = append(targetBranches, wh.MasterBranch) 115 116 wc.Print("Found %d branches that need to adopt this PR: %s\n", len(intend), strings.Join(targetBranches, ", ")) 117 118 // Iterate over the merge intends and merge them 119 for i, v := range intend { 120 finish = wc.PrintStart("Merging %s into %s", v.SourceBranch, v.TargetBranch) 121 122 if os.Getenv("DRYRUN") == "true" { 123 wc.Print("DRY RUN: Skipping merge") 124 finish() 125 continue 126 } 127 128 root := environment.GetRootPathUnsafe() 129 stdout, stderr, err := osutils.ExecSimpleFromDir(root, "git", []string{"checkout", v.TargetBranch}, nil) 130 if err != nil { 131 return errs.Wrap(err, "failed to checkout %s, stdout:\n%s\nstderr:\n%s", v.TargetBranch, stdout, stderr) 132 } 133 134 commitMessage := fmt.Sprintf("Merge branch %s to adopt changes from PR #%d", v.SourceBranch, prNumber) 135 stdout, stderr, err = merge(root, v.SourceBranch, commitMessage) 136 if err != nil { 137 conflictOnlyInVersionTxt := strings.Count(stdout, "CONFLICT (content): Merge conflict in") == 1 && 138 strings.Contains(stdout, "Merge conflict in version.txt") 139 140 if conflictOnlyInVersionTxt { 141 // Abort the conflicted merge and then redo it, preferring the target branch's version.txt. 142 stdout, stderr, err = osutils.ExecSimpleFromDir(root, "git", []string{"merge", "--abort"}, nil) 143 if err != nil { 144 return errs.Wrap(err, "failed to abort conflicted merge\nstdout: %s\nstderr: %s\n", stdout, stderr) 145 } 146 stdout, stderr, err = merge(root, v.SourceBranch, commitMessage, "-s", "ort", "-X", "ours") 147 } 148 149 if err != nil { 150 return errs.Wrap(err, 151 "failed to merge %s into %s. please manually merge the following branches: %s"+ 152 "\nstdout:\n%s\nstderr:\n%s", 153 v.SourceBranch, v.TargetBranch, intend[i:].String(), stdout, stderr) 154 } 155 } 156 157 stdout, stderr, err = osutils.ExecSimpleFromDir(root, "git", []string{"push"}, nil) 158 if err != nil { 159 return errs.Wrap(err, 160 "failed to merge %s into %s. please manually merge the following branches: %s"+ 161 "\nstdout:\n%s\nstderr:\n%s", 162 v.SourceBranch, v.TargetBranch, intend[i:].String(), stdout, stderr) 163 } 164 165 finish() 166 } 167 168 return nil 169 } 170 171 func fetchMeta(ghClient *github.Client, jiraClient *jira.Client, prNumber int) (Meta, error) { 172 // Grab PR information about the PR that this automation is being ran on 173 finish := wc.PrintStart("Fetching Active PR %d", prNumber) 174 prBeingHandled, _, err := ghClient.PullRequests.Get(context.Background(), "ActiveState", "cli", prNumber) 175 if err != nil { 176 return Meta{}, errs.Wrap(err, "failed to get PR") 177 } 178 wc.Print("PR retrieved: %s", prBeingHandled.GetTitle()) 179 finish() 180 181 if prBeingHandled.GetState() != "closed" && !prBeingHandled.GetMerged() { 182 if os.Getenv("DRYRUN") != "true" { 183 return Meta{}, errs.New("Active PR should be merged before it can be propagated.") 184 } 185 } 186 187 finish = wc.PrintStart("Extracting Jira Issue ID from Active PR: %s", prBeingHandled.GetTitle()) 188 jiraIssueID, err := wh.ExtractJiraIssueID(prBeingHandled) 189 if err != nil { 190 return Meta{}, errs.Wrap(err, "PR does not have Jira issue ID associated with it: %s", prBeingHandled.Links.GetHTML().GetHRef()) 191 } 192 wc.Print("Extracted Jira Issue ID: %s", jiraIssueID) 193 finish() 194 195 // Retrieve Relevant Jira Issue for PR being handled 196 finish = wc.PrintStart("Fetching Jira issue") 197 jiraIssue, err := wh.FetchJiraIssue(jiraClient, jiraIssueID) 198 if err != nil { 199 return Meta{}, errs.Wrap(err, "failed to get Jira issue") 200 } 201 finish() 202 203 finish = wc.PrintStart("Fetching Jira Versions") 204 availableVersions, err := wh.FetchAvailableVersions(jiraClient) 205 if err != nil { 206 return Meta{}, errs.Wrap(err, "Failed to fetch JIRA issue") 207 } 208 finish() 209 210 // Retrieve Relevant Fixversion 211 finish = wc.PrintStart("Extracting target fixVersion from Jira issue") 212 fixVersion, _, err := wh.ParseTargetFixVersion(jiraIssue, availableVersions) 213 if err != nil { 214 return Meta{}, errs.Wrap(err, "failed to get fixVersion") 215 } 216 wc.Print("Extracted fixVersion: %s", fixVersion) 217 finish() 218 219 if err := wh.ValidVersionBranch(prBeingHandled.GetBase().GetRef(), fixVersion.Version); err != nil { 220 return Meta{}, errs.Wrap(err, "Failed to validate that the target branch for the active PR is correct.") 221 } 222 223 result := Meta{ 224 Repo: &github.Repository{}, 225 ActivePR: prBeingHandled, 226 ActiveStory: jiraIssue, 227 ActiveVersion: fixVersion.Version, 228 } 229 230 return result, nil 231 } 232 233 func merge(rootDir, sourceBranch, commitMessage string, extraGitArgs ...string) (string, string, error) { 234 args := append([]string{ 235 "merge", sourceBranch, 236 "-m", commitMessage, 237 "--no-edit", 238 }, extraGitArgs...) 239 return osutils.ExecSimpleFromDir(rootDir, "git", args, nil) 240 }