Skip to content

Commit 58685fc

Browse files
author
Dylan Le
committed
tools/relnotes: Update gopls and vscode-go release note script
Features: - Sort by issue number - Use CL author's username in contribution section - Specify semantic version of the release in cmd - Identify CLs that relate to gopls: - Commit message says `internal/lsp/*:` or `gopls/*:` - Referenced issue is in golang/vscode-go - Referenced issue is in golang/go, and is labeled “gopls” Change-Id: I8934216da5c8ab573403e00b027d5f6ae44e6d75 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/406294 Reviewed-by: Suzy Mueller <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 0522373 commit 58685fc

File tree

1 file changed

+176
-37
lines changed

1 file changed

+176
-37
lines changed

tools/relnotes/relnotes.go

Lines changed: 176 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ package main
99
import (
1010
"bytes"
1111
"context"
12+
"encoding/json"
1213
"flag"
1314
"fmt"
1415
"io/ioutil"
1516
"log"
17+
"net/http"
18+
1619
"path/filepath"
1720
"regexp"
1821
"sort"
@@ -27,17 +30,26 @@ import (
2730
)
2831

2932
var (
30-
milestone = flag.String("milestone", "", "milestone associated with the release")
31-
filterDirs = flag.String("dirs", "", "comma-separated list of directories that should be touched for a CL to be considered relevant")
32-
sinceCL = flag.Int("cl", -1, "the gerrit change number of the first CL to include in the output. Only changes submitted more recently than 'cl' will be included.")
33-
project = flag.String("project", "vscode-go", "name of the golang project")
34-
mdMode = flag.Bool("md", false, "write MD output")
35-
exclFile = flag.String("exclude-from", "", "optional path to changelog MD file. If specified, any 'CL NNNN' occurence in the content will cause that CL to be excluded from this tool's output.")
33+
milestone = flag.String("milestone", "", "milestone associated with the release")
34+
filterDirs = flag.String("dirs", "", "comma-separated list of directories that should be touched for a CL to be considered relevant")
35+
sinceCL = flag.Int("cl", -1, "the gerrit change number of the first CL to include in the output. Only changes submitted more recently than 'cl' will be included.")
36+
project = flag.String("project", "vscode-go", "name of the golang project")
37+
exclFile = flag.String("exclude-from", "", "optional path to changelog MD file. If specified, any 'CL NNNN' occurence in the content will cause that CL to be excluded from this tool's output.")
38+
semanticVersion = flag.String("semver", "", "the semantic version of the new release")
39+
githubTokenFilePath = flag.String("token", "", "the absolute path to the github token file")
3640
)
3741

3842
func main() {
3943
flag.Parse()
4044

45+
if *semanticVersion == "" {
46+
log.Fatal("Must provide -semver.")
47+
}
48+
49+
if *githubTokenFilePath == "" {
50+
log.Fatal("Must provide -token.")
51+
}
52+
4153
var existingMD []byte
4254
if *exclFile != "" {
4355
var err error
@@ -86,7 +98,7 @@ func main() {
8698
})
8799

88100
var changes []*generic.Changelist
89-
authors := map[*maintner.GitPerson]bool{}
101+
cls := map[*maintner.GerritCL]bool{}
90102
ger.ForeachProjectUnsorted(func(gp *maintner.GerritProject) error {
91103
if gp.Server() != "go.googlesource.com" || gp.Project() != *project {
92104
return nil
@@ -124,34 +136,50 @@ func main() {
124136
return nil
125137
}
126138
}
127-
changes = append(changes, golang.GerritToGenericCL(cl))
128-
authors[cl.Owner()] = true
139+
if isGoplsChangeList(golang.GerritToGenericCL(cl)) {
140+
changes = append(changes, golang.GerritToGenericCL(cl))
141+
cls[cl] = true
142+
}
129143
return nil
130144
})
131145
return nil
132146
})
133147

134-
sort.Slice(changes, func(i, j int) bool {
135-
return changes[i].Number < changes[j].Number
136-
})
148+
fmt.Printf("# Version: %s\n\n", *semanticVersion)
149+
fmt.Printf("## TODO: version - ")
150+
now := time.Now()
151+
fmt.Printf("%s\n\n", now.Format("2 Jan, 2006"))
152+
fmt.Printf("### Changes\n\n")
153+
mdPrintChanges(changes, false)
154+
fmt.Printf("\n\n")
137155

138-
if *mdMode {
139-
fmt.Printf("## TODO: version - ")
140-
now := time.Now()
141-
fmt.Printf("%s\n\n", now.Format("2 Jan, 2006"))
142-
fmt.Printf("### Changes\n\n")
143-
mdPrintChanges(changes, true)
156+
fmt.Printf("### Issues\n\n")
157+
mdPrintIssues(changes, *milestone)
158+
fmt.Printf("\n\n")
144159

145-
fmt.Printf("### Issues\n\n")
146-
mdPrintIssues(changes, *milestone)
160+
fmt.Printf("### Release comments\n\n")
161+
mdPrintReleaseComments(changes)
162+
fmt.Printf("\n\n")
147163

148-
fmt.Printf("\n### Thanks\n\n")
149-
mdPrintContributors(authors)
150-
} else {
151-
for _, change := range changes {
152-
fmt.Printf(" %s\n", change.Subject)
164+
fmt.Printf("\n### Thanks\n\n")
165+
mdPrintContributors(cls)
166+
}
167+
168+
func isGoplsChangeList(cl *generic.Changelist) bool {
169+
if strings.Contains(cl.Subject, "internal/lsp") || strings.Contains(cl.Subject, "gopls") {
170+
return true
171+
}
172+
for _, issue := range cl.AssociatedIssues {
173+
if issue.Repo == "golang/vscode-go" {
174+
return true
175+
}
176+
for _, label := range issue.Labels {
177+
if label == "gopls" {
178+
return true
179+
}
153180
}
154181
}
182+
return false
155183
}
156184

157185
func mdPrintChanges(changes []*generic.Changelist, byCategory bool) {
@@ -178,7 +206,7 @@ func mdPrintChanges(changes []*generic.Changelist, byCategory bool) {
178206
}
179207
fmt.Printf(" <!-- CL %d -->\n", change.Number)
180208
}
181-
// Group CLs by category or by number order.
209+
// Group CLs by category or by first associated issue number.
182210
if byCategory {
183211
pkgMap := map[string][]*generic.Changelist{}
184212
for _, change := range changes {
@@ -190,7 +218,29 @@ func mdPrintChanges(changes []*generic.Changelist, byCategory bool) {
190218
}
191219
}
192220
} else {
193-
for _, change := range changes {
221+
sort.Slice(changes, func(i, j int) bool {
222+
// Sort first by associated issue, then by CL number.
223+
var iIssue, jIssue int // first associated issues
224+
if len(changes[i].AssociatedIssues) > 0 {
225+
iIssue = changes[i].AssociatedIssues[0].Number
226+
}
227+
if len(changes[j].AssociatedIssues) > 0 {
228+
jIssue = changes[j].AssociatedIssues[0].Number
229+
}
230+
if iIssue != 0 && jIssue != 0 {
231+
return iIssue < jIssue // sort CLs with issues first
232+
}
233+
return iIssue != 0 || changes[i].Number < changes[j].Number
234+
})
235+
236+
currentChange := -1
237+
for i, change := range changes {
238+
if len(change.AssociatedIssues) > 0 && change.AssociatedIssues[0].Number != currentChange {
239+
currentChange = change.AssociatedIssues[0].Number
240+
fmt.Printf("CL(s) for issue %d:\n", currentChange)
241+
} else if len(change.AssociatedIssues) == 0 && (i == 0 || len(changes[i-1].AssociatedIssues) > 0) {
242+
fmt.Printf("CL(s) not associated with any issue:\n")
243+
}
194244
printChange(change)
195245
}
196246
}
@@ -212,6 +262,22 @@ func mdPrintIssues(changes []*generic.Changelist, milestone string) {
212262
}
213263
}
214264

265+
func mdPrintReleaseComments(changes []*generic.Changelist) {
266+
type Issue struct {
267+
repo string
268+
number int
269+
}
270+
printedIssues := make(map[Issue]bool)
271+
for _, change := range changes {
272+
for _, issue := range change.AssociatedIssues {
273+
if _, ok := printedIssues[Issue{issue.Repo, issue.Number}]; !ok {
274+
printedIssues[Issue{issue.Repo, issue.Number}] = true
275+
printIssueReleaseComment(issue.Repo, issue.Number)
276+
}
277+
}
278+
}
279+
}
280+
215281
// clPackage returns the package name from the CL's commit message,
216282
// or "??" if it's formatted unconventionally.
217283
func clPackage(cl *maintner.GerritCL) string {
@@ -243,17 +309,90 @@ func releaseNote(cl *generic.Changelist) string {
243309
return ""
244310
}
245311

246-
func mdPrintContributors(authors map[*maintner.GitPerson]bool) {
247-
var names []string
248-
for author := range authors {
249-
// It would be great to look up the GitHub username by using:
250-
// https://pkg.go.dev/golang.org/x/build/internal/gophers#GetPerson.
251-
names = append(names, author.Name())
312+
func mdPrintContributors(cls map[*maintner.GerritCL]bool) {
313+
var usernames []string
314+
for changelist := range cls {
315+
author, err := fetchCLAuthorName(changelist, *project)
316+
if err != nil {
317+
log.Fatal("Error fetching Github information for %s: %v\n", changelist.Owner(), err)
318+
}
319+
usernames = append(usernames, author)
320+
}
321+
usernames = unique(usernames)
322+
if len(usernames) > 1 {
323+
usernames[len(usernames)-1] = "and " + usernames[len(usernames)-1]
324+
}
325+
326+
fmt.Printf("Thank you for your contribution, %s!\n", strings.Join(usernames, ", "))
327+
}
328+
329+
func getURL(url string) ([]byte, error) {
330+
req, _ := http.NewRequest("GET", url, nil)
331+
if token, err := ioutil.ReadFile(*githubTokenFilePath); err == nil {
332+
req.Header.Set("Authorization", "token "+strings.TrimSpace(string(token)))
333+
}
334+
res, err := http.DefaultClient.Do(req)
335+
if err != nil {
336+
return nil, err
337+
}
338+
defer res.Body.Close()
339+
body, err := ioutil.ReadAll(res.Body)
340+
if err != nil {
341+
log.Fatalf("Error fetching Github information at %s: %v\n", url, err)
342+
}
343+
return body, nil
344+
}
345+
346+
func fetchCLAuthorName(changelist *maintner.GerritCL, repo string) (string, error) {
347+
githubRepoMapping := map[string]string{
348+
"tools": "golang/tools",
349+
"vscode-go": "golang/vscode-go",
350+
}
351+
body, err := getURL(fmt.Sprintf("https://api.github.com/repos/%s/commits/%s", githubRepoMapping[repo], changelist.Commit.Hash))
352+
if err != nil {
353+
return "", err
354+
}
355+
var resp map[string]interface{}
356+
if err := json.Unmarshal(body, &resp); err != nil {
357+
return "", err
358+
}
359+
if authorInfo, _ := resp["author"].(map[string]interface{}); authorInfo != nil {
360+
if username, ok := authorInfo["login"].(string); ok {
361+
return "@" + username, nil
362+
}
363+
}
364+
return changelist.Owner().Name(), nil
365+
}
366+
367+
// printIssueReleaseComment collects the release comments, which marked by the annotation *Release*, from the issues included in this release.
368+
func printIssueReleaseComment(repo string, issueNumber int) {
369+
body, err := getURL(fmt.Sprintf("https://api.github.com/repos/%s/issues/%d/comments", repo, issueNumber))
370+
if err != nil {
371+
log.Fatal(err)
252372
}
253-
sort.Strings(names)
254-
if len(names) > 1 {
255-
names[len(names)-1] = "and " + names[len(names)-1]
373+
var issueComments []interface{}
374+
if err := json.Unmarshal(body, &issueComments); err != nil {
375+
log.Fatalf("Error fetching Github information for issue %d:\n", issueNumber)
376+
}
377+
for _, comment := range issueComments {
378+
c, _ := comment.(map[string]interface{})
379+
if str, ok := c["body"].(string); ok && strings.Contains(str, "*Release*") {
380+
fmt.Println(str)
381+
return
382+
}
256383
}
384+
}
257385

258-
fmt.Printf("Thank you for your contribution, %s!\n", strings.Join(names, ", "))
386+
// unique returns a ascendingly sorted set of unique strings among its input
387+
func unique(input []string) []string {
388+
m := make(map[string]bool)
389+
for _, entry := range input {
390+
m[entry] = true
391+
}
392+
var list []string
393+
for key, _ := range m {
394+
list = append(list, key)
395+
}
396+
sort.Strings(list)
397+
return list
259398
}

0 commit comments

Comments
 (0)