Skip to content

Commit

Permalink
Make release notes tool not dependent on local git
Browse files Browse the repository at this point in the history
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
g-gaston committed Oct 25, 2023
1 parent 45d39d6 commit d7b55d0
Show file tree
Hide file tree
Showing 7 changed files with 787 additions and 482 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,7 @@ release-alias-tag: ## Add the release alias tag to the last build tag

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

.PHONY: promote-images
promote-images: $(KPROMO)
Expand Down
82 changes: 82 additions & 0 deletions hack/tools/release/generator.go
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 &notesGenerator{
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
}
118 changes: 118 additions & 0 deletions hack/tools/release/github.go
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)
}
71 changes: 71 additions & 0 deletions hack/tools/release/list.go
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
}
Loading

0 comments on commit d7b55d0

Please sign in to comment.