Skip to content

Commit d7b55d0

Browse files
committed
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).
1 parent 45d39d6 commit d7b55d0

File tree

7 files changed

+787
-482
lines changed

7 files changed

+787
-482
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ release-alias-tag: ## Add the release alias tag to the last build tag
10991099

11001100
.PHONY: release-notes-tool
11011101
release-notes-tool:
1102-
go build -o bin/notes hack/tools/release/notes.go
1102+
go build -o bin/notes -tags tools sigs.k8s.io/cluster-api/hack/tools/release
11031103

11041104
.PHONY: promote-images
11051105
promote-images: $(KPROMO)

hack/tools/release/generator.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//go:build tools
2+
// +build tools
3+
4+
/*
5+
Copyright 2023 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package main
21+
22+
// notesGenerator orchestrates the release notes generation.
23+
// Lists the selected PRs for this collection of notes,
24+
// process them to generate one entry per PR and then
25+
// formats and prints the results.
26+
type notesGenerator struct {
27+
lister prLister
28+
processor prProcessor
29+
printer entriesPrinter
30+
}
31+
32+
func newNotesGenerator(lister prLister, processor prProcessor, printer entriesPrinter) *notesGenerator {
33+
return &notesGenerator{
34+
lister: lister,
35+
processor: processor,
36+
printer: printer,
37+
}
38+
}
39+
40+
// PR is a GitHub PR.
41+
type pr struct {
42+
number uint64
43+
title string
44+
labels []string
45+
}
46+
47+
// prLister returns a list of PRs.
48+
type prLister interface {
49+
listPRs() ([]pr, error)
50+
}
51+
52+
// notesEntry represents a line item for the release notes.
53+
type notesEntry struct {
54+
title string
55+
section string
56+
prNumber string
57+
}
58+
59+
// prProcessor generates notes entries for a list of PRs.
60+
type prProcessor interface {
61+
process([]pr) []notesEntry
62+
}
63+
64+
// entriesPrinter formats and outputs to stdout the notes
65+
// based on a list of entries.
66+
type entriesPrinter interface {
67+
print([]notesEntry)
68+
}
69+
70+
// run generates and prints the notes.
71+
func (g *notesGenerator) run() error {
72+
prs, err := g.lister.listPRs()
73+
if err != nil {
74+
return err
75+
}
76+
77+
entries := g.processor.process(prs)
78+
79+
g.printer.print(entries)
80+
81+
return nil
82+
}

hack/tools/release/github.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//go:build tools
2+
// +build tools
3+
4+
/*
5+
Copyright 2023 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package main
21+
22+
import (
23+
"encoding/json"
24+
"fmt"
25+
"log"
26+
"math"
27+
"os/exec"
28+
"time"
29+
)
30+
31+
// githubDiff is the API response for the "compare" endpoint.
32+
type githubDiff struct {
33+
// MergeBaseCommit points to most recent common ancestor between two references.
34+
MergeBaseCommit githubCommitNode `json:"merge_base_commit"`
35+
}
36+
37+
type githubCommitNode struct {
38+
Commit githubCommit `json:"commit"`
39+
}
40+
41+
// githubCommit is the API response for a "git/commits" request.
42+
type githubCommit struct {
43+
Committer githubCommitter `json:"committer"`
44+
}
45+
46+
type githubCommitter struct {
47+
Date time.Time `json:"date"`
48+
}
49+
50+
// githubPRList is the API response for the "search" endpoint.
51+
type githubPRList struct {
52+
Total int `json:"total_count"`
53+
Items []githubPR `json:"items"`
54+
}
55+
56+
// githubPR is the API object included in a "search" query response when the
57+
// return item is a PR.
58+
type githubPR struct {
59+
Number uint64 `json:"number"`
60+
Title string `json:"title"`
61+
Labels []githubLabel `json:"labels"`
62+
}
63+
64+
type githubLabel struct {
65+
Name string `json:"name"`
66+
}
67+
68+
// githubClient uses the gh CLI to make API request to GitHub.
69+
type githubClient struct {
70+
// repo is full [org]/[repo_name]
71+
repo string
72+
}
73+
74+
// getDiff calls the `compare` endpoint.
75+
func (c githubClient) getDiff(base, head string) (githubDiff, error) {
76+
diff := githubDiff{}
77+
if err := c.runGHAPICommand(fmt.Sprintf("repos/%s/compare/%s...%s?per_page=1'", c.repo, base, head), &diff); err != nil {
78+
return githubDiff{}, err
79+
}
80+
return diff, nil
81+
}
82+
83+
// listMergedPRs calls the `search` endpoint and queries for PRs.
84+
func (c githubClient) listMergedPRs(baseBranch string, after time.Time) ([]githubPR, error) {
85+
pageSize := 100
86+
page := 0
87+
totalPRs := math.MaxInt
88+
89+
searchQuery := fmt.Sprintf("repo:%s+base:%s+is:pr+is:merged+merged:>%s", c.repo, baseBranch, after.Format(time.RFC3339))
90+
91+
var prs []githubPR
92+
93+
for len(prs) < totalPRs {
94+
page++
95+
url := fmt.Sprintf("search/issues?per_page=%d&page=%d&q=%s", pageSize, page, searchQuery)
96+
log.Println("Calling search endpoint: " + url)
97+
prList := githubPRList{}
98+
if err := c.runGHAPICommand(url, &prList); err != nil {
99+
return nil, err
100+
}
101+
102+
prs = append(prs, prList.Items...)
103+
totalPRs = prList.Total
104+
}
105+
106+
return prs, nil
107+
}
108+
109+
func (c githubClient) runGHAPICommand(url string, response any) error {
110+
cmd := exec.Command("gh", "api", url)
111+
112+
out, err := cmd.CombinedOutput()
113+
if err != nil {
114+
return fmt.Errorf("%s: %v", string(out), err)
115+
}
116+
117+
return json.Unmarshal(out, response)
118+
}

hack/tools/release/list.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//go:build tools
2+
// +build tools
3+
4+
/*
5+
Copyright 2023 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package main
21+
22+
import (
23+
"log"
24+
)
25+
26+
// githubPRLister lists PRs from GitHub.
27+
type githubPRLister struct {
28+
client *githubClient
29+
from, branch string
30+
}
31+
32+
func newPRLister(repo, fromTag, branch string) *githubPRLister {
33+
return &githubPRLister{
34+
client: &githubClient{repo: repo},
35+
from: fromTag,
36+
branch: branch,
37+
}
38+
}
39+
40+
// listPRs queries the PRs merged in a branch after
41+
// the `from` reference.
42+
func (l *githubPRLister) listPRs() ([]pr, error) {
43+
log.Printf("Computing diff between %s and %s", l.branch, l.from)
44+
diff, err := l.client.getDiff(l.branch, l.from)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
log.Printf("Listing PRs from branch %s starting after %s", l.branch, diff.MergeBaseCommit.Commit.Committer.Date)
50+
gPRs, err := l.client.listMergedPRs(l.branch, diff.MergeBaseCommit.Commit.Committer.Date)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
log.Printf("Found %d PRs in github", len(gPRs))
56+
57+
prs := make([]pr, 0, len(gPRs))
58+
for _, p := range gPRs {
59+
labels := make([]string, 0, len(p.Labels))
60+
for _, l := range p.Labels {
61+
labels = append(labels, l.Name)
62+
}
63+
prs = append(prs, pr{
64+
number: p.Number,
65+
title: p.Title,
66+
labels: labels,
67+
})
68+
}
69+
70+
return prs, nil
71+
}

0 commit comments

Comments
 (0)