-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make release notes tool not dependent on local git
Now all the date is retrieved through GitHub APIs, making the tool more portable and easier to use. It should not increase the rate limiting chances since it now performs less API requests (by getting the label from all PRs at once instead of with one request per PR).
- Loading branch information
Showing
7 changed files
with
787 additions
and
482 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
//go:build tools | ||
// +build tools | ||
|
||
/* | ||
Copyright 2023 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package main | ||
|
||
// notesGenerator orchestrates the release notes generation. | ||
// Lists the selected PRs for this collection of notes, | ||
// process them to generate one entry per PR and then | ||
// formats and prints the results. | ||
type notesGenerator struct { | ||
lister prLister | ||
processor prProcessor | ||
printer entriesPrinter | ||
} | ||
|
||
func newNotesGenerator(lister prLister, processor prProcessor, printer entriesPrinter) *notesGenerator { | ||
return ¬esGenerator{ | ||
lister: lister, | ||
processor: processor, | ||
printer: printer, | ||
} | ||
} | ||
|
||
// PR is a GitHub PR. | ||
type pr struct { | ||
number uint64 | ||
title string | ||
labels []string | ||
} | ||
|
||
// prLister returns a list of PRs. | ||
type prLister interface { | ||
listPRs() ([]pr, error) | ||
} | ||
|
||
// notesEntry represents a line item for the release notes. | ||
type notesEntry struct { | ||
title string | ||
section string | ||
prNumber string | ||
} | ||
|
||
// prProcessor generates notes entries for a list of PRs. | ||
type prProcessor interface { | ||
process([]pr) []notesEntry | ||
} | ||
|
||
// entriesPrinter formats and outputs to stdout the notes | ||
// based on a list of entries. | ||
type entriesPrinter interface { | ||
print([]notesEntry) | ||
} | ||
|
||
// run generates and prints the notes. | ||
func (g *notesGenerator) run() error { | ||
prs, err := g.lister.listPRs() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
entries := g.processor.process(prs) | ||
|
||
g.printer.print(entries) | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
//go:build tools | ||
// +build tools | ||
|
||
/* | ||
Copyright 2023 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"math" | ||
"os/exec" | ||
"time" | ||
) | ||
|
||
// githubDiff is the API response for the "compare" endpoint. | ||
type githubDiff struct { | ||
// MergeBaseCommit points to most recent common ancestor between two references. | ||
MergeBaseCommit githubCommitNode `json:"merge_base_commit"` | ||
} | ||
|
||
type githubCommitNode struct { | ||
Commit githubCommit `json:"commit"` | ||
} | ||
|
||
// githubCommit is the API response for a "git/commits" request. | ||
type githubCommit struct { | ||
Committer githubCommitter `json:"committer"` | ||
} | ||
|
||
type githubCommitter struct { | ||
Date time.Time `json:"date"` | ||
} | ||
|
||
// githubPRList is the API response for the "search" endpoint. | ||
type githubPRList struct { | ||
Total int `json:"total_count"` | ||
Items []githubPR `json:"items"` | ||
} | ||
|
||
// githubPR is the API object included in a "search" query response when the | ||
// return item is a PR. | ||
type githubPR struct { | ||
Number uint64 `json:"number"` | ||
Title string `json:"title"` | ||
Labels []githubLabel `json:"labels"` | ||
} | ||
|
||
type githubLabel struct { | ||
Name string `json:"name"` | ||
} | ||
|
||
// githubClient uses the gh CLI to make API request to GitHub. | ||
type githubClient struct { | ||
// repo is full [org]/[repo_name] | ||
repo string | ||
} | ||
|
||
// getDiff calls the `compare` endpoint. | ||
func (c githubClient) getDiff(base, head string) (githubDiff, error) { | ||
diff := githubDiff{} | ||
if err := c.runGHAPICommand(fmt.Sprintf("repos/%s/compare/%s...%s?per_page=1'", c.repo, base, head), &diff); err != nil { | ||
return githubDiff{}, err | ||
} | ||
return diff, nil | ||
} | ||
|
||
// listMergedPRs calls the `search` endpoint and queries for PRs. | ||
func (c githubClient) listMergedPRs(baseBranch string, after time.Time) ([]githubPR, error) { | ||
pageSize := 100 | ||
page := 0 | ||
totalPRs := math.MaxInt | ||
|
||
searchQuery := fmt.Sprintf("repo:%s+base:%s+is:pr+is:merged+merged:>%s", c.repo, baseBranch, after.Format(time.RFC3339)) | ||
|
||
var prs []githubPR | ||
|
||
for len(prs) < totalPRs { | ||
page++ | ||
url := fmt.Sprintf("search/issues?per_page=%d&page=%d&q=%s", pageSize, page, searchQuery) | ||
log.Println("Calling search endpoint: " + url) | ||
prList := githubPRList{} | ||
if err := c.runGHAPICommand(url, &prList); err != nil { | ||
return nil, err | ||
} | ||
|
||
prs = append(prs, prList.Items...) | ||
totalPRs = prList.Total | ||
} | ||
|
||
return prs, nil | ||
} | ||
|
||
func (c githubClient) runGHAPICommand(url string, response any) error { | ||
cmd := exec.Command("gh", "api", url) | ||
|
||
out, err := cmd.CombinedOutput() | ||
if err != nil { | ||
return fmt.Errorf("%s: %v", string(out), err) | ||
} | ||
|
||
return json.Unmarshal(out, response) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
//go:build tools | ||
// +build tools | ||
|
||
/* | ||
Copyright 2023 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"log" | ||
) | ||
|
||
// githubPRLister lists PRs from GitHub. | ||
type githubPRLister struct { | ||
client *githubClient | ||
from, branch string | ||
} | ||
|
||
func newPRLister(repo, fromTag, branch string) *githubPRLister { | ||
return &githubPRLister{ | ||
client: &githubClient{repo: repo}, | ||
from: fromTag, | ||
branch: branch, | ||
} | ||
} | ||
|
||
// listPRs queries the PRs merged in a branch after | ||
// the `from` reference. | ||
func (l *githubPRLister) listPRs() ([]pr, error) { | ||
log.Printf("Computing diff between %s and %s", l.branch, l.from) | ||
diff, err := l.client.getDiff(l.branch, l.from) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
log.Printf("Listing PRs from branch %s starting after %s", l.branch, diff.MergeBaseCommit.Commit.Committer.Date) | ||
gPRs, err := l.client.listMergedPRs(l.branch, diff.MergeBaseCommit.Commit.Committer.Date) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
log.Printf("Found %d PRs in github", len(gPRs)) | ||
|
||
prs := make([]pr, 0, len(gPRs)) | ||
for _, p := range gPRs { | ||
labels := make([]string, 0, len(p.Labels)) | ||
for _, l := range p.Labels { | ||
labels = append(labels, l.Name) | ||
} | ||
prs = append(prs, pr{ | ||
number: p.Number, | ||
title: p.Title, | ||
labels: labels, | ||
}) | ||
} | ||
|
||
return prs, nil | ||
} |
Oops, something went wrong.