Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs-issue-generation: Script can now handle multiple release notes #85490

Merged
merged 1 commit into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 115 additions & 47 deletions pkg/cmd/docs-issue-generation/docs_issue_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,29 @@ import (
"time"
)

// commit contains details about each formatted commit
type commit struct {
sha string
title string
releaseNote string
// docsIssue contains details about each formatted commit to be committed to the docs repo
type docsIssue struct {
sourceCommitSha string
title string
body string
}

// pr contains details about the pull request
// pr contains details about the cockroach pull request
type pr struct {
number int
mergeBranch string
commits []commit
docsIssues []docsIssue
}

// ghSearch contains a list of search items (in this case, PRs) from the GitHub API
type ghSearch struct { // this struct holds the search results of all PRs based on a given commit
// ghSearch contains a list of search items (in this case, cockroach PRs) based on a given commit from the GitHub API.
type ghSearch struct {
Items []ghSearchItem `json:"items"`
}

// ghSearchItem contains details about a specific PR, including its number, from the GitHub API
// ghSearchItem contains details about a specific cockroach PR,
// including its number, from the GitHub API.
type ghSearchItem struct {
Number int `json:"number"` // PR number
PRNumber int `json:"number"`
PullRequest ghSearchItemPr `json:"pull_request"`
}

Expand All @@ -53,7 +54,9 @@ type ghSearchItemPr struct {
MergedAt *time.Time `json:"merged_at"`
}

// this struct holds details about the PR from the GitHub API
// ghPull holds the base branch for a cockroach PR from the GitHub API.
// It directly mirrors the structure of the PR where the branch name
// field (Ref) is nested within the Base field.
type ghPull struct {
Base struct {
Ref string `json:"ref"` // this is the destination branch of the PR
Expand All @@ -66,7 +69,7 @@ type ghPullCommit struct {
Commit ghPullCommitMsg `json:"commit"`
}

// ghPullCommitMsg holds the commit message from the GitHub API
// ghPullCommitMsg holds the commit message in the cockroach PR from the GitHub API
type ghPullCommitMsg struct {
Message string `json:"message"` // the commit message
}
Expand All @@ -81,21 +84,36 @@ type parameters struct {
func docsIssueGeneration(params parameters) {
var search ghSearch // a search for all PRs based on a given commit SHA
var prs []pr
if err := httpGet("https://api.github.com/search/issues?q=sha:"+params.Sha+"+repo:cockroachdb/cockroach+is:merged", params.Token, &search); err != nil {
err := httpGet(
"https://api.github.com/search/issues?q=sha:"+params.Sha+"+repo:cockroachdb/cockroach+is:merged",
params.Token,
&search,
)
if err != nil {
log.Fatal(err)
}
prs = prNums(search) // populate slice of PRs of type pr
for _, pr := range prs { // for each PR in the list, get the merge branch and the list of eligible commits
var pull ghPull
if err := httpGet("https://api.github.com/repos/cockroachdb/cockroach/pulls/"+strconv.Itoa(pr.number), params.Token, &pull); err != nil {
err := httpGet(
"https://api.github.com/repos/cockroachdb/cockroach/pulls/"+strconv.Itoa(pr.number),
params.Token,
&pull,
)
if err != nil {
log.Fatal(err)
}
pr.mergeBranch = pull.Base.Ref
var commits []ghPullCommit
if err := httpGet("https://api.github.com/repos/cockroachdb/cockroach/pulls/"+strconv.Itoa(pr.number)+"/commits?per_page:250", params.Token, &commits); err != nil {
err = httpGet(
"https://api.github.com/repos/cockroachdb/cockroach/pulls/"+strconv.Itoa(pr.number)+"/commits?per_page:250",
params.Token,
&commits,
)
if err != nil {
log.Fatal(err)
}
pr.commits = getCommits(commits, pr.number)
pr.docsIssues = getIssues(commits, pr.number)
pr.createDocsIssues(params.Token)
}
}
Expand All @@ -115,7 +133,8 @@ func httpGet(url string, token string, out interface{}) error {
if err != nil {
return err
}
if err := json.Unmarshal(bs, out); err != nil { // unmarshal (convert) the byte slice into an interface
// unmarshal (convert) the byte slice into an interface
if err := json.Unmarshal(bs, out); err != nil {
return err
}
return nil
Expand All @@ -124,61 +143,110 @@ func httpGet(url string, token string, out interface{}) error {
// prNums returns an array of PRS for the given GH search of PRs against a given commit.
func prNums(search ghSearch) []pr {
var result []pr
for _, x := range search.Items { // each PR returned from the search is iterated through, and a new pr object is created for each
// each PR returned from the search is iterated through, and a new pr object is created for each
for _, x := range search.Items {
if x.PullRequest.MergedAt != nil {
result = append(result, pr{number: x.Number}) // a new object of type pr containing the PR number is appended to the result slice
// a new object of type pr containing the PR number is appended to the result slice
result = append(result, pr{number: x.PRNumber})
}
}
return result // return a slice of prs
}

// getCommits takes a list of commits from GitHub as well as the PR associated with those commits and outputs a formatted list of commits with valid release notes on that PR
func getCommits(pullCommit []ghPullCommit, prNumber int) []commit {
var result []commit
// getIssues takes a list of commits from GitHub as well as the PR number associated with those commits and outputs a
// formatted list of docs issues with valid release notes
func getIssues(pullCommit []ghPullCommit, prNumber int) []docsIssue {
var result []docsIssue
for _, c := range pullCommit {
message := c.Commit.Message
rn := formatReleaseNote(message, prNumber, c.Sha) // return a slice of data that locates every instance of the phrase "release note (", case insensitive
if rn != "" { // checks to make sure a match was returned
x := commit{ // declare a new commit object
sha: c.Sha,
title: formatTitle(message),
releaseNote: rn,
// return a slice of data that locates every instance of the phrase "release note (", case-insensitive
rns := formatReleaseNotes(message, prNumber, c.Sha)
for i, rn := range rns {
{ // checks to make sure a match was returned
x := docsIssue{ // declare a new commit object
sourceCommitSha: c.Sha,
title: formatTitle(message, prNumber, i+1, len(rns)),
body: rn,
}
result = append(result, x)
}
result = append(result, x)
}
}
return result
}

func formatReleaseNote(message string, prNumber int, sha string) string {
re := regexp.MustCompile(`(?s)[rR]elease [nN]ote \(.*`) // this regex checks to make sure there's a release note within the commit
reNeg := regexp.MustCompile(`([rR]elease [nN]ote \(bug fix.*)|([rR]elease [nN]ote: [nN]one)`) // this regex is used to exclude bug fixes or releases without release notes
rn := re.FindString(message) // return the first instance of the phrase "release note (", case insensitive
if len(rn) > 0 && !reNeg.MatchString(message) { // checks to make sure the desired release note is not null and doesn't match the negating string.
return fmt.Sprintf(
"Related PR: https://github.com/cockroachdb/cockroach/pull/%s\nCommit: https://github.com/cockroachdb/cockroach/commit/%s\n\n---\n\n%s",
var (
noRNRE = regexp.MustCompile(`[rR]elease [nN]ote: [nN]one`)
allRNRE = regexp.MustCompile(`[rR]elease [nN]ote \(.*`)
bugFixRNRE = regexp.MustCompile(`([rR]elease [nN]ote \(bug fix\):.*)`)
releaseJustificationRE = regexp.MustCompile(`[rR]elease [jJ]ustification:.*`)
)

// formatReleaseNotes generates a list of docsIssue bodies for the docs repo based on a given CRDB sha
func formatReleaseNotes(message string, prNumber int, crdbSha string) []string {
rnBodySlice := []string{}
if noRNRE.MatchString(message) {
return rnBodySlice
}
splitString := strings.Split(message, "\n")
releaseNoteLines := []string{}
var rnBody string
for _, x := range splitString {
validRn := allRNRE.MatchString(x)
bugFixRn := bugFixRNRE.MatchString(x)
releaseJustification := releaseJustificationRE.MatchString(x)
if len(releaseNoteLines) > 0 && (validRn || releaseJustification) {
rnBody = fmt.Sprintf(
"Related PR: https://github.com/cockroachdb/cockroach/pull/%s\n"+
"Commit: https://github.com/cockroachdb/cockroach/commit/%s\n"+
"\n---\n\n%s",
strconv.Itoa(prNumber),
crdbSha,
strings.Join(releaseNoteLines, "\n"),
)
rnBodySlice = append(rnBodySlice, strings.TrimSuffix(rnBody, "\n"))
rnBody = ""
releaseNoteLines = []string{}
}
if (validRn && !bugFixRn) || (len(releaseNoteLines) > 0 && !bugFixRn && !releaseJustification) {
releaseNoteLines = append(releaseNoteLines, x)
}
}
if len(releaseNoteLines) > 0 { // commit whatever is left in the buffer to the rnBodySlice set
rnBody = fmt.Sprintf(
"Related PR: https://github.com/cockroachdb/cockroach/pull/%s\n"+
"Commit: https://github.com/cockroachdb/cockroach/commit/%s\n"+
"\n---\n\n%s",
strconv.Itoa(prNumber),
sha,
rn,
crdbSha,
strings.Join(releaseNoteLines, "\n"),
)
rnBodySlice = append(rnBodySlice, strings.TrimSuffix(rnBody, "\n"))
}
return ""
return rnBodySlice
}

func formatTitle(message string) string {
if i := strings.IndexRune(message, '\n'); i > 0 {
return message[:i]
func formatTitle(message string, prNumber int, index int, totalLength int) string {
var commitTitle string
if i := strings.IndexRune(message, '\n'); i > 0 { //
commitTitle = message[:i]
} else {
commitTitle = message
}
result := fmt.Sprintf("PR #%d - %s", prNumber, commitTitle)
if totalLength > 1 {
result += fmt.Sprintf(" (%d of %d)", index, totalLength)
}
return message
return result
}

func (p pr) createDocsIssues(token string) {
postURL := "https://api.github.com/repos/cockroachdb/docs/issues"
for _, commit := range p.commits {
for _, issue := range p.docsIssues {
reqBody, err := json.Marshal(map[string]interface{}{
"title": commit.title,
"title": issue.title,
"labels": []string{"C-product-change", p.mergeBranch},
"body": commit.releaseNote,
"body": issue.body,
})
if err != nil {
log.Fatal(err)
Expand Down
Loading