Skip to content

Commit 6a6a295

Browse files
authored
fix(tools): Change gen-release-notes to use git instead of scraping web (google#3958)
1 parent d1dd1a5 commit 6a6a295

File tree

5 files changed

+63
-5024
lines changed

5 files changed

+63
-5024
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ Finally, to use the top-of-trunk version of this repo, use the following command
4949
go get github.com/google/go-github/v82@master
5050
```
5151

52+
To discover all the changes that have occured since a prior release, you can
53+
first clone the repo, then run (for example):
54+
55+
```bash
56+
go run tools/gen-release-notes/main.go --tag v82.0.0
57+
```
58+
5259
## Usage ##
5360

5461
```go
@@ -297,7 +304,7 @@ client := github.NewClient(
297304

298305
Alternatively, the [bored-engineer/github-conditional-http-transport](https://github.com/bored-engineer/github-conditional-http-transport)
299306
package relies on (undocumented) GitHub specific cache logic and is
300-
recommended when making requests using short-lived credentials such as a
307+
recommended when making requests using short-lived credentials such as a
301308
[GitHub App installation token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation).
302309

303310
### Creating and Updating Resources ###
@@ -350,7 +357,7 @@ for {
350357

351358
#### Iterators (**experimental**) ####
352359

353-
Go v1.23 introduces the new `iter` package.
360+
Go v1.23 introduces the new `iter` package.
354361

355362
With the `enrichman/gh-iter` package, it is possible to create iterators for `go-github`. The iterator will handle pagination for you, looping through all the available results.
356363

@@ -370,8 +377,8 @@ For complete usage of `enrichman/gh-iter`, see the full [package docs](https://g
370377
#### Middleware ####
371378

372379
You can use [gofri/go-github-pagination](https://github.com/gofri/go-github-pagination) to handle
373-
pagination for you. It supports both sync and async modes, as well as customizations.
374-
By default, the middleware automatically paginates through all pages, aggregates results, and returns them as an array.
380+
pagination for you. It supports both sync and async modes, as well as customizations.
381+
By default, the middleware automatically paginates through all pages, aggregates results, and returns them as an array.
375382
See `example/ratelimit/main.go` for usage.
376383

377384
### Webhooks ###

tools/gen-release-notes/main.go

Lines changed: 52 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,44 @@
33
// Use of this source code is governed by a BSD-style
44
// license that can be found in the LICENSE file.
55

6-
// gen-release-notes first reads the web page https://github.com/google/go-github
7-
// to determine what the prior release was, (e.g. "v76.0.0")
8-
// then reads https://github.com/google/go-github/compare/commit-list?range=${PRIOR_RELEASE}...master
9-
// to find out what changes were made since then.
6+
// gen-release-notes calls `git` to determine what the prior release was, (e.g. "v76.0.0")
7+
// then calls `git` again to find out what changes were made since then.
108
//
119
// Finally, it writes the release notes to stdout, summarizing the
1210
// breaking and non-breaking changes since that release.
11+
//
12+
// Usage:
13+
//
14+
// go run tools/gen-release-notes/main.go [--tag v76.0.0]
1315
package main
1416

1517
import (
18+
"bytes"
1619
"flag"
1720
"fmt"
18-
"io"
1921
"log"
20-
"net/http"
22+
"os"
23+
"os/exec"
2124
"regexp"
2225
"strings"
2326
)
2427

25-
const (
26-
baseWebURL = "https://github.com/google/go-github"
27-
28-
// fragile, but works for now.
29-
detailsDiv = `<div class="flex-auto min-width-0 js-details-container Details">`
30-
)
31-
3228
var (
33-
releaseRE = regexp.MustCompile(`<span [^>]+>([^<]+)</span>\s*<span title="Label: Latest"[^>]*>`)
34-
linkPrimaryRE = regexp.MustCompile(`(?ms)(.*?<a class="Link--primary[^>]+>)`)
35-
startAvatarRE = regexp.MustCompile(`(?ms)(<div class="AvatarStack)`)
36-
bracketsRE = regexp.MustCompile(`(?ms)(<[^>]+>)`)
37-
newlinesRE = regexp.MustCompile(`(?m)(\n+)`)
29+
sinceTag = flag.String("tag", "", "List all changes since this tag (e.g. 'v76.0.0')")
30+
3831
descriptionRE = regexp.MustCompile(`^\* (.*?\((#[^\)]+)\))`)
32+
releaseTagRE = regexp.MustCompile(`[^a-zA-Z0-9.\-_]+`)
3933
)
4034

4135
func main() {
4236
log.SetFlags(0)
4337
flag.Parse()
4438

45-
priorRelease := getPriorRelease()
46-
log.Printf("Prior release: %v", priorRelease)
39+
priorRelease := *sinceTag
40+
if priorRelease == "" {
41+
priorRelease = getPriorRelease()
42+
log.Printf("Prior release: %v", priorRelease)
43+
}
4744

4845
newChanges := newChangesSinceRelease(priorRelease)
4946

@@ -54,13 +51,6 @@ func main() {
5451
}
5552

5653
func genReleaseNotes(text string) string {
57-
// strip everything before first detailsDiv:
58-
idx := strings.Index(text, detailsDiv)
59-
if idx < 0 {
60-
log.Fatal("Could not find detailsDiv")
61-
}
62-
text = text[idx:]
63-
6454
allLines := splitIntoPRs(text)
6555
fullBreakingLines, fullNonBreakingLines := splitBreakingLines(allLines)
6656
refBreakingLines, refNonBreakingLines := genRefLines(fullBreakingLines, fullNonBreakingLines)
@@ -73,7 +63,7 @@ func genReleaseNotes(text string) string {
7363
}
7464

7565
func splitIntoPRs(text string) []string {
76-
parts := strings.Split(text, detailsDiv)
66+
parts := strings.Split("\n"+text, "\ncommit ")
7767
if len(parts) < 2 {
7868
log.Fatal("unable to find PRs")
7969
}
@@ -82,101 +72,25 @@ func splitIntoPRs(text string) []string {
8272
if part == "" {
8373
continue
8474
}
85-
newDiv := matchDivs(part)
86-
for {
87-
oldDiv := newDiv
88-
newDiv = stripPRHTML(oldDiv)
89-
if newDiv == oldDiv {
90-
break
91-
}
92-
}
93-
prs = append(prs, newDiv)
94-
}
95-
return prs
96-
}
97-
98-
func stripPRHTML(text string) string {
99-
_, innerText := getTagSequence(text)
100-
if before, _, ok := strings.Cut(text, "</a>"); ok {
101-
newText := before + strings.Join(innerText, "")
102-
newText = strings.ReplaceAll(newText, "…", "")
103-
newText = newlinesRE.ReplaceAllString(newText, "\n ")
104-
return newText
105-
}
106-
return text
107-
}
108-
109-
func getTagSequence(text string) (tagSeq, innerText []string) {
110-
m := bracketsRE.FindAllStringIndex(text, -1)
111-
var lastEnd int
112-
for _, pair := range m {
113-
start := pair[0]
114-
end := pair[1] - 1
115-
if lastEnd > 0 && start > lastEnd+1 {
116-
rawText := text[lastEnd+1 : start]
117-
s := strings.TrimSpace(rawText)
118-
switch s {
119-
case "", "&hellip;": // skip
120-
default:
121-
// Special case:
122-
if strings.HasPrefix(rawText, "BREAKING") {
123-
rawText = "\n\n" + rawText
124-
}
125-
innerText = append(innerText, rawText)
126-
}
127-
}
128-
lastEnd = end
129-
s := text[start+1 : end]
130-
if s == "code" {
131-
innerText = append(innerText, " `")
132-
continue
133-
}
134-
if s == "/code" {
135-
innerText = append(innerText, "` ")
75+
lines := strings.Split(part, "\n")
76+
if len(lines) < 5 { // commit, Author:, Date:, blank, msg
13677
continue
13778
}
138-
if s[0] == '/' {
139-
tagSeq = append(tagSeq, s)
140-
continue
141-
}
142-
if before, _, ok := strings.Cut(s, " "); ok {
143-
tagSeq = append(tagSeq, before)
144-
} else {
145-
tagSeq = append(tagSeq, s)
146-
}
147-
}
148-
return tagSeq, innerText
149-
}
150-
151-
func matchDivs(text string) string {
152-
chunks := strings.Split(text, `</div>`)
153-
var divCount int
154-
var lastChunk int
155-
for i, chunk := range chunks {
156-
chunks[i] = strings.TrimSpace(chunks[i])
157-
divsInChunk := strings.Count(chunk, `<div `)
158-
divCount += divsInChunk
159-
lastChunk++
160-
if lastChunk == divCount {
161-
newDivs := strings.Join(chunks[:lastChunk], "")
162-
return stripLinkPrimary(newDivs)
79+
var newPR []string
80+
for _, line := range lines[1:] {
81+
line = strings.TrimSpace(line)
82+
if line == "" || strings.HasPrefix(line, "Author: ") || strings.HasPrefix(line, "Date: ") {
83+
continue
84+
}
85+
if len(newPR) == 0 {
86+
newPR = append(newPR, "* "+line)
87+
} else {
88+
newPR = append(newPR, " "+line)
89+
}
16390
}
91+
prs = append(prs, strings.Join(newPR, "\n"))
16492
}
165-
return ""
166-
}
167-
168-
func stripLinkPrimary(text string) string {
169-
m := linkPrimaryRE.FindStringSubmatch(text)
170-
if len(m) != 2 {
171-
log.Fatalf("unable to find link primary in: '%v'", text)
172-
}
173-
newText := strings.TrimSpace(text[len(m[0]):])
174-
// As a special case, trim off all the Avatar stuff
175-
m2 := startAvatarRE.FindStringIndex(newText)
176-
if len(m2) > 0 {
177-
newText = newText[:m2[0]]
178-
}
179-
return "* " + newText
93+
return prs
18094
}
18195

18296
func splitBreakingLines(allLines []string) (breaking, nonBreaking []string) {
@@ -206,46 +120,33 @@ func genRefLines(breaking, nonBreaking []string) (ref, refNon []string) {
206120
return ref, refNon
207121
}
208122

209-
func newChangesSinceRelease(priorRelease string) string {
210-
url := fmt.Sprintf("%v/compare/commit-list?range=%v...master", baseWebURL, priorRelease)
211-
resp, err := http.Get(url) //nolint:gosec
212-
must(err)
213-
defer resp.Body.Close()
214-
215-
body, err := io.ReadAll(resp.Body)
216-
must(err)
123+
func runCommand(cmdArgs []string) string {
124+
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:gosec
125+
out := &bytes.Buffer{}
126+
cmd.Stdout = out
127+
cmd.Stderr = os.Stderr
217128

218-
return string(body)
219-
}
220-
221-
func getPriorRelease() string {
222-
resp, err := http.Get(baseWebURL)
223-
must(err)
224-
defer resp.Body.Close()
225-
226-
body, err := io.ReadAll(resp.Body)
227-
must(err)
228-
229-
matches := releaseRE.FindStringSubmatch(string(body))
230-
if len(matches) != 2 {
231-
log.Fatal("could not find release info")
129+
log.Printf("Running command: %v", strings.Join(cmdArgs, " "))
130+
if err := cmd.Run(); err != nil {
131+
log.Fatalf("command failed: %v", err)
232132
}
233133

234-
priorRelease := strings.TrimSpace(matches[1])
235-
if priorRelease == "" {
236-
log.Fatal("found empty prior release version")
237-
}
134+
return strings.TrimSpace(out.String())
135+
}
238136

239-
return priorRelease
137+
func newChangesSinceRelease(priorRelease string) string {
138+
priorRelease = releaseTagRE.ReplaceAllString(priorRelease, "")
139+
cmdArgs := []string{"git", "log", priorRelease + "..", "--no-color"}
140+
return runCommand(cmdArgs)
240141
}
241142

242-
func must(err error) {
243-
if err != nil {
244-
log.Fatal(err)
245-
}
143+
func getPriorRelease() string {
144+
cmdArgs := []string{"git", "describe", "--tags", "--abbrev=0"}
145+
return runCommand(cmdArgs)
246146
}
247147

248-
const releaseNotesFmt = `This release contains the following breaking API changes:
148+
const releaseNotesFmt = `
149+
This release contains the following breaking API changes:
249150
250151
%v
251152

0 commit comments

Comments
 (0)