Repository: kubernetes/release (go)

Issue:
[script] Retrieve proper author and PR type when generating release notes
<!-- Please only use this template for submitting feature requests -->

#### What would you like to be added:

The release-notes script would retrieve the author of the fix from the parent PR. 
Currently, the script retrieves the `https://github.com/k8s-infra-cherrypick-robot` as the author.

Similarly (less important, because it does not need to be repeated when re-generating notes) it would be great to retrieve the PR type (like kind or feature).

#### Why is this needed:

It takes time to manually adjust the release notes. Sometimes the notes are regenerated, and in total it can take something like 30min of manual chore. Similarly, we need to manually annotate type for the cherry-picks (`/kind bug`).

Example release in Kueue: https://github.com/kubernetes-sigs/kueue/issues/2985
For this we have to manually adjust the notes. Example cherry-pick PR: https://github.com/kubernetes-sigs/kueue/pull/2879.


Pull request:
Retrieve proper author for the automated cherry-pick PRs
<!--  Thanks for sending a pull request!  Here are some tips for you:

- If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
- Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
- If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
- If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind feature

#### What this PR does / why we need it:

When automated cherry-picks are created by the [k8s-infra-cherrypick-robot](https://github.com/k8s-infra-cherrypick-robot), the script uses information from the cherry-pick PR for release notes generation.

#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes

or

None
-->

Fixes #3746 

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
Use origin PR info for release notes instead of cherry-pick PRs generated by k8s-infra-cherrypick-robot.
```


Patch:
diff --git a/pkg/notes/notes.go b/pkg/notes/notes.go
index 1eeac675..2502d821 100644
--- a/pkg/notes/notes.go
+++ b/pkg/notes/notes.go
@@ -51,9 +51,12 @@ import (
 var (
 	errNoPRIDFoundInCommitMessage       = errors.New("no PR IDs found in the commit message")
 	errNoPRFoundForCommitSHA            = errors.New("no PR found for this commit")
+	errNoOriginPRIDFoundInPR            = errors.New("no origin PR IDs found in the PR")
 	apiSleepTime                  int64 = 60
 )
 
+var regexK8sCherryPickBotBranch = regexp.MustCompile(`cherry-pick-(?P<number>\d+)-to`)
+
 const (
 	DefaultOrg  = "kubernetes"
 	DefaultRepo = "kubernetes"
@@ -63,6 +66,8 @@ const (
 	maxParallelRequests = 10
 )
 
+const k8sCherryPickBotUsername = "k8s-infra-cherrypick-robot"
+
 type (
 	Notes []string
 	Kind  string
@@ -855,6 +860,22 @@ func (g *Gatherer) notesForCommit(commit *gogithub.RepositoryCommit) (*Result, e
 
 		// If we found a valid release note, return the PR, otherwise, take the next one
 		if s != "" {
+			if isAutomatedCherryPickPR(pr) {
+				logrus.Infof("PR #%d seems to be an automated cherry-pick, retrieving origin info", pr.GetNumber())
+
+				originPRNum, err := originPrNumFromPr(pr)
+				if err != nil {
+					return nil, err
+				}
+
+				originPR, err := g.getPr(originPRNum)
+				if err != nil {
+					return nil, err
+				}
+
+				pr.User = originPR.GetUser()
+			}
+
 			res := &Result{commit: commit, pullRequest: pr}
 			logrus.Infof("PR #%d seems to contain a release note", pr.GetNumber())
 			// Do not test further PRs for this commit as soon as one PR matched
@@ -867,6 +888,27 @@ func (g *Gatherer) notesForCommit(commit *gogithub.RepositoryCommit) (*Result, e
 	return nil, nil
 }
 
+func isAutomatedCherryPickPR(pr *gogithub.PullRequest) bool {
+	if pr == nil || pr.GetUser() == nil {
+		return false
+	}
+
+	return pr.GetUser().GetLogin() == k8sCherryPickBotUsername
+}
+
+func originPrNumFromPr(pr *gogithub.PullRequest) (int, error) {
+	if pr == nil || pr.GetHead() == nil {
+		return 0, errNoOriginPRIDFoundInPR
+	}
+
+	originPR := prForRegex(regexK8sCherryPickBotBranch, pr.GetHead().GetLabel())
+	if originPR == 0 {
+		return 0, errNoOriginPRIDFoundInPR
+	}
+
+	return originPR, nil
+}
+
 type resultList struct {
 	sync.RWMutex
 	list []*Result
@@ -1095,30 +1137,33 @@ func (g *Gatherer) prsForCommitFromMessage(commitMessage string) (prs []*gogithu
 		return nil, err
 	}
 
-	var res *gogithub.PullRequest
-
-	var resp *gogithub.Response
-
-	for _, pr := range prsNum {
+	for _, prNum := range prsNum {
 		// Given the PR number that we've now converted to an integer, get the PR from
 		// the API
-		for {
-			res, resp, err = g.client.GetPullRequest(g.context, g.options.GithubOrg, g.options.GithubRepo, pr)
-			if err != nil {
-				if !canWaitAndRetry(resp, err) {
-					return nil, err
-				}
-			} else {
-				break
-			}
+		pr, err := g.getPr(prNum)
+		if err != nil {
+			return prs, err
 		}
 
-		prs = append(prs, res)
+		prs = append(prs, pr)
 	}
 
 	return prs, err
 }
 
+func (g *Gatherer) getPr(prNum int) (*gogithub.PullRequest, error) {
+	for {
+		res, resp, err := g.client.GetPullRequest(g.context, g.options.GithubOrg, g.options.GithubRepo, prNum)
+		if err != nil {
+			if !canWaitAndRetry(resp, err) {
+				return nil, err
+			}
+		} else {
+			return res, nil
+		}
+	}
+}
+
 func prsNumForCommitFromMessage(commitMessage string) (prs []int, err error) {
 	// Thankfully k8s-merge-robot commits the PR number consistently. If this ever
 	// stops being true, this definitely won't work anymore.


Test patch:
diff --git a/pkg/notes/notes_gatherer_test.go b/pkg/notes/notes_gatherer_test.go
index 935fa4ae..7d172c6b 100644
--- a/pkg/notes/notes_gatherer_test.go
+++ b/pkg/notes/notes_gatherer_test.go
@@ -321,6 +321,80 @@ func TestGatherNotes(t *testing.T) {
 			},
 			expectedGetPullRequestCallCount: 6,
 		},
+		"when the PR is a cherry pick": {
+			commitList: []*github.RepositoryCommit{
+				repoCommit("123", "Merge a regular commit (#123)"),
+				repoCommit("124", "Merge an automated cherry-pick (#124)"),
+			},
+			getPullRequestStubber: func(t *testing.T) getPullRequestStub {
+				seenPRs := newIntsRecorder(122, 123, 124)
+				prsMap := map[int]*github.PullRequest{
+					122: {
+						Number: intPtr(122),
+						Body:   strPtr("122\n```release-note\nfrom 122\n```\n"),
+						User: &github.User{
+							Login: strPtr("test-user-a"),
+						},
+					},
+					123: {
+						Number: intPtr(123),
+						Body:   strPtr("123\n```release-note\nfrom 123\n```\n"),
+						User: &github.User{
+							Login: strPtr("test-user-b"),
+						},
+					},
+					124: {
+						Number: intPtr(124),
+						Body:   strPtr("124\n```release-note\nfrom 124\n```\n"),
+						User: &github.User{
+							Login: strPtr("k8s-infra-cherrypick-robot"),
+						},
+						Head: &github.PullRequestBranch{
+							Label: strPtr("k8s-infra-cherrypick-robot:cherry-pick-122-to-release-0.x"),
+						},
+					},
+				}
+
+				return func(_ context.Context, org, repo string, prID int) (*github.PullRequest, *github.Response, error) {
+					checkOrgRepo(t, org, repo)
+					if err := seenPRs.Mark(prID); err != nil {
+						t.Errorf("In GetPullRequest: %v", err)
+					}
+
+					return prsMap[prID], nil, nil
+				}
+			},
+			resultsChecker: func(t *testing.T, results []*Result) {
+				type pr struct {
+					num    int
+					author string
+				}
+				expectMap := map[string]pr{
+					"123": {
+						num:    123,
+						author: "test-user-b",
+					},
+					"124": {
+						num:    124,
+						author: "test-user-a",
+					},
+				}
+
+				for _, result := range results {
+					expected, found := expectMap[*result.commit.SHA]
+					if !found {
+						t.Errorf("Unexpected SHA %s", *result.commit.SHA)
+					}
+					if expected.num != *result.pullRequest.Number {
+						t.Errorf("Expecting %d got %d for SHA %s", expected.num, *result.pullRequest.Number, *result.commit.SHA)
+					}
+					if expected.author != *result.pullRequest.User.Login {
+						t.Errorf("Expecting %s got %s for SHA %s", expected.author, *result.pullRequest.User.Login, *result.commit.SHA)
+					}
+				}
+			},
+			expectedGetPullRequestCallCount: 3,
+		},
 		"when GetPullRequest(...) returns an error": {
 			commitList: []*github.RepositoryCommit{repoCommit("some-sha", "some #123 thing")},
 			listPullRequestsWithCommitStubber: func(t *testing.T) listPullRequestsWithCommitStub {