sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/external-plugins/cherrypicker/server.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "os" 26 "regexp" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 33 utilerrors "k8s.io/apimachinery/pkg/util/errors" 34 cherrypicker "sigs.k8s.io/prow/cmd/external-plugins/cherrypicker/lib" 35 "sigs.k8s.io/prow/pkg/config" 36 "sigs.k8s.io/prow/pkg/git/v2" 37 "sigs.k8s.io/prow/pkg/github" 38 "sigs.k8s.io/prow/pkg/pluginhelp" 39 "sigs.k8s.io/prow/pkg/plugins" 40 ) 41 42 const pluginName = "cherrypick" 43 const defaultLabelPrefix = "cherrypick/" 44 45 var cherryPickRe = regexp.MustCompile(`(?m)^(?:/cherrypick|/cherry-pick)\s+(.+)$`) 46 var releaseNoteRe = regexp.MustCompile(`(?s)(?:Release note\*\*:\s*(?:<!--[^<>]*-->\s*)?` + "```(?:release-note)?|```release-note)(.+?)```") 47 var titleTargetBranchIndicatorTemplate = `[%s] ` 48 49 var notOrgMemberMessageTemplate = "only [%s](https://github.com/orgs/%s/people) org members may request cherry picks. If you are already part of the org, make sure to [change](https://github.com/orgs/%s/people?query=%s) your membership to public. Otherwise you can still do the cherry-pick manually. " 50 51 type githubClient interface { 52 AddLabel(org, repo string, number int, label string) error 53 AssignIssue(org, repo string, number int, logins []string) error 54 CreateComment(org, repo string, number int, comment string) error 55 CreateFork(org, repo string) (string, error) 56 CreatePullRequest(org, repo, title, body, head, base string, canModify bool) (int, error) 57 CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error) 58 EnsureFork(forkingUser, org, repo string) (string, error) 59 GetPullRequest(org, repo string, number int) (*github.PullRequest, error) 60 GetPullRequestPatch(org, repo string, number int) ([]byte, error) 61 GetPullRequests(org, repo string) ([]github.PullRequest, error) 62 GetRepo(owner, name string) (github.FullRepo, error) 63 IsMember(org, user string) (bool, error) 64 ListIssueComments(org, repo string, number int) ([]github.IssueComment, error) 65 GetIssueLabels(org, repo string, number int) ([]github.Label, error) 66 ListOrgMembers(org, role string) ([]github.TeamMember, error) 67 } 68 69 // HelpProvider construct the pluginhelp.PluginHelp for this plugin. 70 func HelpProvider(_ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 71 pluginHelp := &pluginhelp.PluginHelp{ 72 Description: `The cherrypick plugin is used for cherrypicking PRs across branches. For every successful cherrypick invocation a new PR is opened against the target branch and assigned to the requestor. If the parent PR contains a release note, it is copied to the cherrypick PR.`, 73 } 74 pluginHelp.AddCommand(pluginhelp.Command{ 75 Usage: "/cherrypick [branch]", 76 Description: "Cherrypick a PR to a different branch. This command works both in merged PRs (the cherrypick PR is opened immediately) and open PRs (the cherrypick PR opens as soon as the original PR merges). If multiple branches are specified, separated by a space, a cherrypick for the first branch will be created with a comment to cherrypick the remaining branches after the first merges.", 77 Featured: true, 78 // depends on how the cherrypick server runs; needs auth by default (--allow-all=false) 79 WhoCanUse: "Members of the trusted organization for the repo.", 80 Examples: []string{"/cherrypick release-3.9", "/cherry-pick release-1.15", "/cherrypick release-1.6 release-1.5 release-1.4"}, 81 }) 82 return pluginHelp, nil 83 } 84 85 // Server implements http.Handler. It validates incoming GitHub webhooks and 86 // then dispatches them to the appropriate plugins. 87 type Server struct { 88 tokenGenerator func() []byte 89 botUser *github.UserData 90 email string 91 92 gc git.ClientFactory 93 // Used for unit testing 94 push func(forkName, newBranch string, force bool) error 95 ghc githubClient 96 log *logrus.Entry 97 98 // Labels to apply to the cherrypicked PR. 99 labels []string 100 // Use prow to assign users to cherrypicked PRs. 101 prowAssignments bool 102 // Allow anybody to do cherrypicks. 103 allowAll bool 104 // Create an issue on cherrypick conflict. 105 issueOnConflict bool 106 // Set a custom label prefix. 107 labelPrefix string 108 109 bare *http.Client 110 patchURL string 111 112 repoLock sync.Mutex 113 repos []github.Repo 114 mapLock sync.Mutex 115 lockMap map[cherryPickRequest]*sync.Mutex 116 } 117 118 type cherryPickRequest struct { 119 org string 120 repo string 121 pr int 122 targetBranch string 123 } 124 125 // ServeHTTP validates an incoming webhook and puts it into the event channel. 126 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 127 eventType, eventGUID, payload, ok, _ := github.ValidateWebhook(w, r, s.tokenGenerator) 128 if !ok { 129 return 130 } 131 fmt.Fprint(w, "Event received. Have a nice day.") 132 133 if err := s.handleEvent(eventType, eventGUID, payload); err != nil { 134 logrus.WithError(err).Error("Error parsing event.") 135 } 136 } 137 138 func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error { 139 l := logrus.WithFields(logrus.Fields{ 140 "event-type": eventType, 141 github.EventGUID: eventGUID, 142 }) 143 switch eventType { 144 case "issue_comment": 145 var ic github.IssueCommentEvent 146 if err := json.Unmarshal(payload, &ic); err != nil { 147 return err 148 } 149 go func() { 150 if err := s.handleIssueComment(l, ic); err != nil { 151 s.log.WithError(err).WithFields(l.Data).Info("Cherry-pick failed.") 152 } 153 }() 154 case "pull_request": 155 var pr github.PullRequestEvent 156 if err := json.Unmarshal(payload, &pr); err != nil { 157 return err 158 } 159 go func() { 160 if err := s.handlePullRequest(l, pr); err != nil { 161 s.log.WithError(err).WithFields(l.Data).Info("Cherry-pick failed.") 162 } 163 }() 164 default: 165 logrus.Debugf("skipping event of type %q", eventType) 166 } 167 return nil 168 } 169 170 func (s *Server) handleIssueComment(l *logrus.Entry, ic github.IssueCommentEvent) error { 171 // Only consider new comments in PRs. 172 if !ic.Issue.IsPullRequest() || ic.Action != github.IssueCommentActionCreated { 173 return nil 174 } 175 176 org := ic.Repo.Owner.Login 177 repo := ic.Repo.Name 178 num := ic.Issue.Number 179 commentAuthor := ic.Comment.User.Login 180 181 // Do not create a new logger, its fields are re-used by the caller in case of errors 182 *l = *l.WithFields(logrus.Fields{ 183 github.OrgLogField: org, 184 github.RepoLogField: repo, 185 github.PrLogField: num, 186 }) 187 188 cherryPickMatches := cherryPickRe.FindAllStringSubmatch(ic.Comment.Body, -1) 189 if len(cherryPickMatches) == 0 || len(cherryPickMatches[0]) < 2 { 190 return nil 191 } 192 branches := strings.Fields(cherryPickMatches[0][1]) 193 targetBranch := branches[0] 194 var chainBranches []string 195 if len(branches) > 1 { 196 chainBranches = branches[1:] 197 } 198 199 if ic.Issue.State != "closed" { 200 if !s.allowAll { 201 // Only members should be able to do cherry-picks. 202 ok, err := s.ghc.IsMember(org, commentAuthor) 203 if err != nil { 204 return err 205 } 206 if !ok { 207 resp := fmt.Sprintf(notOrgMemberMessageTemplate, org, org, org, commentAuthor) 208 l.Info(resp) 209 return s.ghc.CreateComment(org, repo, num, plugins.FormatICResponse(ic.Comment, resp)) 210 } 211 } 212 resp := fmt.Sprintf("once the present PR merges, I will cherry-pick it on top of %s in a new PR and assign it to you.", targetBranch) 213 l.Info(resp) 214 return s.ghc.CreateComment(org, repo, num, plugins.FormatICResponse(ic.Comment, resp)) 215 } 216 217 pr, err := s.ghc.GetPullRequest(org, repo, num) 218 if err != nil { 219 return fmt.Errorf("failed to get pull request %s/%s#%d: %w", org, repo, num, err) 220 } 221 baseBranch := pr.Base.Ref 222 title := pr.Title 223 body := pr.Body 224 225 // Cherry-pick only merged PRs. 226 if !pr.Merged { 227 resp := "cannot cherry-pick an unmerged PR" 228 l.Info(resp) 229 return s.ghc.CreateComment(org, repo, num, plugins.FormatICResponse(ic.Comment, resp)) 230 } 231 232 // TODO: Use an allowlist for allowed base and target branches. 233 if baseBranch == targetBranch { 234 resp := fmt.Sprintf("base branch (%s) needs to differ from target branch (%s)", baseBranch, targetBranch) 235 l.Info(resp) 236 return s.ghc.CreateComment(org, repo, num, plugins.FormatICResponse(ic.Comment, resp)) 237 } 238 239 if !s.allowAll { 240 // Only org members should be able to do cherry-picks. 241 ok, err := s.ghc.IsMember(org, commentAuthor) 242 if err != nil { 243 return err 244 } 245 if !ok { 246 resp := fmt.Sprintf(notOrgMemberMessageTemplate, org, org, org, commentAuthor) 247 l.Info(resp) 248 return s.ghc.CreateComment(org, repo, num, plugins.FormatICResponse(ic.Comment, resp)) 249 } 250 } 251 252 *l = *l.WithFields(logrus.Fields{ 253 "requestor": ic.Comment.User.Login, 254 "target_branch": targetBranch, 255 }) 256 l.Debug("Cherrypick request.") 257 return s.handle(l, ic.Comment.User.Login, &ic.Comment, org, repo, targetBranch, baseBranch, chainBranches, title, body, num) 258 } 259 260 func (s *Server) handlePullRequest(l *logrus.Entry, pre github.PullRequestEvent) error { 261 // Only consider newly merged PRs 262 if pre.Action != github.PullRequestActionClosed && pre.Action != github.PullRequestActionLabeled { 263 return nil 264 } 265 266 pr := pre.PullRequest 267 if !pr.Merged || pr.MergeSHA == nil { 268 return nil 269 } 270 271 org := pr.Base.Repo.Owner.Login 272 repo := pr.Base.Repo.Name 273 baseBranch := pr.Base.Ref 274 num := pr.Number 275 title := pr.Title 276 body := pr.Body 277 278 // Do not create a new logger, its fields are re-used by the caller in case of errors 279 *l = *l.WithFields(logrus.Fields{ 280 github.OrgLogField: org, 281 github.RepoLogField: repo, 282 github.PrLogField: num, 283 }) 284 285 comments, err := s.ghc.ListIssueComments(org, repo, num) 286 if err != nil { 287 return fmt.Errorf("failed to list comments: %w", err) 288 } 289 290 // requestor -> target branch -> issue comment 291 requestorToComments := make(map[string]map[string]*github.IssueComment) 292 // target branch -> chain branches (eg. "release-1.6" -> []string{"release-1.5", "release-1.4"}) 293 targetBranchToChainBranches := make(map[string][]string) 294 295 // first look for our special comments 296 for i := range comments { 297 c := comments[i] 298 cherryPickMatches := cherryPickRe.FindAllStringSubmatch(c.Body, -1) 299 for _, match := range cherryPickMatches { 300 targetBranch := strings.Fields(match[1]) 301 if requestorToComments[c.User.Login] == nil { 302 requestorToComments[c.User.Login] = make(map[string]*github.IssueComment) 303 } 304 requestorToComments[c.User.Login][targetBranch[0]] = &c 305 if len(targetBranch) > 1 { 306 targetBranchToChainBranches[targetBranch[0]] = targetBranch[1:] 307 } 308 } 309 } 310 311 foundCherryPickComments := len(requestorToComments) != 0 312 313 // now look for our special labels 314 labels, err := s.ghc.GetIssueLabels(org, repo, num) 315 if err != nil { 316 return fmt.Errorf("failed to get issue labels: %w", err) 317 } 318 319 if requestorToComments[pr.User.Login] == nil { 320 requestorToComments[pr.User.Login] = make(map[string]*github.IssueComment) 321 } 322 323 foundCherryPickLabels := false 324 for _, label := range labels { 325 if strings.HasPrefix(label.Name, s.labelPrefix) { 326 requestorToComments[pr.User.Login][label.Name[len(s.labelPrefix):]] = nil // leave this nil which indicates a label-initiated cherry-pick 327 foundCherryPickLabels = true 328 } 329 } 330 331 if !foundCherryPickComments && !foundCherryPickLabels { 332 return nil 333 } 334 335 if !foundCherryPickLabels && pre.Action == github.PullRequestActionLabeled { 336 return nil 337 } 338 339 // Figure out membership. 340 if !s.allowAll { 341 // TODO: Possibly cache this. 342 members, err := s.ghc.ListOrgMembers(org, "all") 343 if err != nil { 344 return err 345 } 346 for requestor := range requestorToComments { 347 isMember := false 348 for _, m := range members { 349 if requestor == m.Login { 350 isMember = true 351 break 352 } 353 } 354 if !isMember { 355 delete(requestorToComments, requestor) 356 } 357 } 358 } 359 360 // Handle multiple comments serially. Make sure to filter out 361 // comments targeting the same branch. 362 handledBranches := make(map[string]bool) 363 var errs []error 364 for requestor, branches := range requestorToComments { 365 for targetBranch, ic := range branches { 366 if handledBranches[targetBranch] { 367 // Branch already handled. Skip. 368 continue 369 } 370 if targetBranch == baseBranch { 371 resp := fmt.Sprintf("base branch (%s) needs to differ from target branch (%s)", baseBranch, targetBranch) 372 l.Info(resp) 373 if err := s.createComment(l, org, repo, num, ic, resp); err != nil { 374 l.WithError(err).WithField("response", resp).Error("Failed to create comment.") 375 } 376 continue 377 } 378 handledBranches[targetBranch] = true 379 l := l.WithFields(logrus.Fields{ 380 "requestor": requestor, 381 "target_branch": targetBranch, 382 }) 383 l.Debug("Cherrypick request.") 384 var chainedBranches []string 385 if branches, ok := targetBranchToChainBranches[targetBranch]; ok { 386 chainedBranches = branches 387 } 388 err := s.handle(l, requestor, ic, org, repo, targetBranch, baseBranch, chainedBranches, title, body, num) 389 if err != nil { 390 errs = append(errs, fmt.Errorf("failed to create cherrypick: %w", err)) 391 } 392 } 393 } 394 return utilerrors.NewAggregate(errs) 395 } 396 397 var cherryPickBranchFmt = "cherry-pick-%d-to-%s" 398 399 func (s *Server) handle(logger *logrus.Entry, requestor string, comment *github.IssueComment, org, repo, targetBranch, baseBranch string, chainBranches []string, title, body string, num int) error { 400 var lock *sync.Mutex 401 func() { 402 s.mapLock.Lock() 403 defer s.mapLock.Unlock() 404 if _, ok := s.lockMap[cherryPickRequest{org, repo, num, targetBranch}]; !ok { 405 if s.lockMap == nil { 406 s.lockMap = map[cherryPickRequest]*sync.Mutex{} 407 } 408 s.lockMap[cherryPickRequest{org, repo, num, targetBranch}] = &sync.Mutex{} 409 } 410 lock = s.lockMap[cherryPickRequest{org, repo, num, targetBranch}] 411 }() 412 lock.Lock() 413 defer lock.Unlock() 414 415 forkName, err := s.ensureForkExists(org, repo) 416 if err != nil { 417 logger.WithError(err).Warn("failed to ensure fork exists") 418 resp := fmt.Sprintf("cannot fork %s/%s: %v", org, repo, err) 419 return s.createComment(logger, org, repo, num, comment, resp) 420 } 421 422 // Clone the repo, checkout the target branch. 423 startClone := time.Now() 424 r, err := s.gc.ClientFor(org, repo) 425 if err != nil { 426 return fmt.Errorf("failed to get git client for %s/%s: %w", org, forkName, err) 427 } 428 defer func() { 429 if err := r.Clean(); err != nil { 430 logger.WithError(err).Error("Error cleaning up repo.") 431 } 432 }() 433 if err := r.Checkout(targetBranch); err != nil { 434 logger.WithError(err).Warn("failed to checkout target branch") 435 resp := fmt.Sprintf("cannot checkout `%s`: %v", targetBranch, err) 436 return s.createComment(logger, org, repo, num, comment, resp) 437 } 438 logger.WithField("duration", time.Since(startClone)).Info("Cloned and checked out target branch.") 439 440 // Fetch the patch from GitHub 441 localPath, err := s.getPatch(org, repo, targetBranch, num) 442 if err != nil { 443 return fmt.Errorf("failed to get patch: %w", err) 444 } 445 446 if err := r.Config("user.name", s.botUser.Login); err != nil { 447 return fmt.Errorf("failed to configure git user: %w", err) 448 } 449 email := s.email 450 if email == "" { 451 email = s.botUser.Email 452 } 453 if err := r.Config("user.email", email); err != nil { 454 return fmt.Errorf("failed to configure git email: %w", err) 455 } 456 457 // New branch for the cherry-pick. 458 newBranch := fmt.Sprintf(cherryPickBranchFmt, num, targetBranch) 459 460 // Check if that branch already exists, which means there is already a PR for that cherry-pick. 461 if r.BranchExists(newBranch) { 462 // Find the PR and link to it. 463 prs, err := s.ghc.GetPullRequests(org, repo) 464 if err != nil { 465 return fmt.Errorf("failed to get pullrequests for %s/%s: %w", org, repo, err) 466 } 467 for _, pr := range prs { 468 if pr.Head.Ref == fmt.Sprintf("%s:%s", s.botUser.Login, newBranch) { 469 logger.WithField("preexisting_cherrypick", pr.HTMLURL).Info("PR already has cherrypick") 470 resp := fmt.Sprintf("Looks like #%d has already been cherry picked in %s", num, pr.HTMLURL) 471 return s.createComment(logger, org, repo, num, comment, resp) 472 } 473 } 474 } 475 476 // Create the branch for the cherry-pick. 477 if err := r.CheckoutNewBranch(newBranch); err != nil { 478 return fmt.Errorf("failed to checkout %s: %w", newBranch, err) 479 } 480 481 // Title for GitHub issue/PR. 482 titleTargetBranchIndicator := fmt.Sprintf(titleTargetBranchIndicatorTemplate, targetBranch) 483 title = fmt.Sprintf("%s%s", titleTargetBranchIndicator, omitBaseBranchFromTitle(title, baseBranch)) 484 485 // Apply the patch. 486 if err := r.Am(localPath); err != nil { 487 errs := []error{fmt.Errorf("failed to `git am`: %w", err)} 488 logger.WithError(err).Warn("failed to apply PR on top of target branch") 489 resp := fmt.Sprintf("#%d failed to apply on top of branch %q:\n```\n%v\n```", num, targetBranch, err) 490 if err := s.createComment(logger, org, repo, num, comment, resp); err != nil { 491 errs = append(errs, fmt.Errorf("failed to create comment: %w", err)) 492 } 493 494 if s.issueOnConflict { 495 resp = fmt.Sprintf("Manual cherrypick required.\n\n%v", resp) 496 if err := s.createIssue(logger, org, repo, title, resp, num, comment, nil, []string{requestor}); err != nil { 497 errs = append(errs, fmt.Errorf("failed to create issue: %w", err)) 498 } 499 } 500 501 return utilerrors.NewAggregate(errs) 502 } 503 504 push := r.PushToNamedFork 505 if s.push != nil { 506 push = s.push 507 } 508 // Push the new branch in the bot's fork. 509 if err := push(forkName, newBranch, true); err != nil { 510 logger.WithError(err).Warn("failed to push chery-picked changes to GitHub") 511 resp := fmt.Sprintf("failed to push cherry-picked changes in GitHub: %v", err) 512 return utilerrors.NewAggregate([]error{err, s.createComment(logger, org, repo, num, comment, resp)}) 513 } 514 515 // Open a PR in GitHub. 516 var cherryPickBody string 517 if s.prowAssignments { 518 cherryPickBody = cherrypicker.CreateCherrypickBody(num, requestor, releaseNoteFromParentPR(body), chainBranches) 519 } else { 520 cherryPickBody = cherrypicker.CreateCherrypickBody(num, "", releaseNoteFromParentPR(body), chainBranches) 521 } 522 head := fmt.Sprintf("%s:%s", s.botUser.Login, newBranch) 523 createdNum, err := s.ghc.CreatePullRequest(org, repo, title, cherryPickBody, head, targetBranch, true) 524 if err != nil { 525 logger.WithError(err).Warn("failed to create new pull request") 526 resp := fmt.Sprintf("new pull request could not be created: %v", err) 527 return utilerrors.NewAggregate([]error{err, s.createComment(logger, org, repo, num, comment, resp)}) 528 } 529 *logger = *logger.WithField("new_pull_request_number", createdNum) 530 resp := fmt.Sprintf("new pull request created: #%d", createdNum) 531 logger.Info("new pull request created") 532 if err := s.createComment(logger, org, repo, num, comment, resp); err != nil { 533 return fmt.Errorf("failed to create comment: %w", err) 534 } 535 for _, label := range s.labels { 536 if err := s.ghc.AddLabel(org, repo, createdNum, label); err != nil { 537 return fmt.Errorf("failed to add label %s: %w", label, err) 538 } 539 } 540 if s.prowAssignments { 541 if err := s.ghc.AssignIssue(org, repo, createdNum, []string{requestor}); err != nil { 542 logger.WithError(err).Warn("failed to assign to new PR") 543 // Ignore returning errors on failure to assign as this is most likely 544 // due to users not being members of the org so that they can't be assigned 545 // in PRs. 546 return nil 547 } 548 } 549 return nil 550 } 551 552 // omitBaseBranchFromTitle returns the title without the base branch's 553 // indicator, if there is one. We do this to avoid long cherry-pick titles when 554 // doing a backport of a backport. 555 // 556 // Example of long cherry-pick titles: 557 // Original PR title: "Hello world" 558 // Backport to release-9.9 title: "[release-9.9] Hello world" 559 // Backport to release-9.8 title: "[release-9.8] [release-9.9] Hello world" 560 // 561 // This function helps by making the second backport title 562 // be "[release-9.8] Hello world" instead, by deleting the first occurrence 563 // of "[release-9.9]" from the first backport's title. 564 // 565 // When baseBranch is empty, this function simply returns the title as-is for convenience. 566 func omitBaseBranchFromTitle(title, baseBranch string) string { 567 if baseBranch == "" { 568 return title 569 } 570 571 return strings.Replace(title, fmt.Sprintf(titleTargetBranchIndicatorTemplate, baseBranch), "", 1) 572 } 573 574 func (s *Server) createComment(l *logrus.Entry, org, repo string, num int, comment *github.IssueComment, resp string) error { 575 if err := func() error { 576 if comment != nil { 577 return s.ghc.CreateComment(org, repo, num, plugins.FormatICResponse(*comment, resp)) 578 } 579 return s.ghc.CreateComment(org, repo, num, fmt.Sprintf("In response to a cherrypick label: %s", resp)) 580 }(); err != nil { 581 l.WithError(err).Warn("failed to create comment") 582 return err 583 } 584 logrus.Debug("Created comment") 585 return nil 586 } 587 588 // createIssue creates an issue on GitHub. 589 func (s *Server) createIssue(l *logrus.Entry, org, repo, title, body string, num int, comment *github.IssueComment, labels, assignees []string) error { 590 issueNum, err := s.ghc.CreateIssue(org, repo, title, body, 0, labels, assignees) 591 if err != nil { 592 return s.createComment(l, org, repo, num, comment, fmt.Sprintf("new issue could not be created for failed cherrypick: %v", err)) 593 } 594 595 return s.createComment(l, org, repo, num, comment, fmt.Sprintf("new issue created for failed cherrypick: #%d", issueNum)) 596 } 597 598 // ensureForkExists ensures a fork of org/repo exists for the bot. 599 func (s *Server) ensureForkExists(org, repo string) (string, error) { 600 fork := s.botUser.Login + "/" + repo 601 602 // fork repo if it doesn't exist 603 repo, err := s.ghc.EnsureFork(s.botUser.Login, org, repo) 604 if err != nil { 605 return repo, err 606 } 607 608 s.repoLock.Lock() 609 defer s.repoLock.Unlock() 610 s.repos = append(s.repos, github.Repo{FullName: fork, Fork: true}) 611 return repo, nil 612 } 613 614 // getPatch gets the patch for the provided PR and creates a local 615 // copy of it. It returns its location in the filesystem and any 616 // encountered error. 617 func (s *Server) getPatch(org, repo, targetBranch string, num int) (string, error) { 618 patch, err := s.ghc.GetPullRequestPatch(org, repo, num) 619 if err != nil { 620 return "", err 621 } 622 localPath := fmt.Sprintf("/tmp/%s_%s_%d_%s.patch", org, repo, num, normalize(targetBranch)) 623 out, err := os.Create(localPath) 624 if err != nil { 625 return "", err 626 } 627 defer out.Close() 628 if _, err := io.Copy(out, bytes.NewBuffer(patch)); err != nil { 629 return "", err 630 } 631 return localPath, nil 632 } 633 634 func normalize(input string) string { 635 return strings.Replace(input, "/", "-", -1) 636 } 637 638 // releaseNoteNoteFromParentPR gets the release note from the 639 // parent PR and formats it as per the PR template so that 640 // it can be copied to the cherry-pick PR. 641 func releaseNoteFromParentPR(body string) string { 642 potentialMatch := releaseNoteRe.FindStringSubmatch(body) 643 if potentialMatch == nil { 644 return "" 645 } 646 return fmt.Sprintf("```release-note\n%s\n```", strings.TrimSpace(potentialMatch[1])) 647 }