github.com/kubernetes-incubator/kube-aws@v0.16.4/hack/relnote.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "log" 8 "os" 9 "os/exec" 10 "sort" 11 "strings" 12 13 "github.com/google/go-github/github" 14 "golang.org/x/net/context" 15 "golang.org/x/oauth2" 16 ) 17 18 type Item struct { 19 number int 20 title string 21 summary string 22 actionsRequired string 23 isDocUpdate bool 24 isMetaUpdate bool 25 isImprovement bool 26 isFeature bool 27 isBugFix bool 28 isProposal bool 29 isRefactoring bool 30 } 31 32 func Info(msg string) { 33 println(msg) 34 } 35 36 func Title(title string) { 37 fmt.Printf("\n# %s\n\n", title) 38 } 39 40 func Header(title string) { 41 fmt.Printf("\n## %s\n\n", title) 42 } 43 44 func PanicIfError(err error) { 45 if err != nil { 46 panic(err) 47 } 48 } 49 50 func capture(cmdName string, cmdArgs []string) (string, error) { 51 fmt.Printf("running %s %v\n", cmdName, cmdArgs) 52 cmd := exec.Command(cmdName, cmdArgs...) 53 54 stdoutBuffer := bytes.Buffer{} 55 56 { 57 stdoutReader, err := cmd.StdoutPipe() 58 if err != nil { 59 return "", fmt.Errorf("failed to pipe stdout: %v", err) 60 } 61 62 stdoutScanner := bufio.NewScanner(stdoutReader) 63 go func() { 64 for stdoutScanner.Scan() { 65 stdoutBuffer.WriteString(stdoutScanner.Text()) 66 } 67 }() 68 } 69 70 stderrBuffer := bytes.Buffer{} 71 { 72 stderrReader, err := cmd.StderrPipe() 73 if err != nil { 74 return "", fmt.Errorf("failed to pipe stderr: %v", err) 75 } 76 77 stderrScanner := bufio.NewScanner(stderrReader) 78 go func() { 79 for stderrScanner.Scan() { 80 stderrBuffer.WriteString(stderrScanner.Text()) 81 } 82 }() 83 } 84 85 err := cmd.Start() 86 if err != nil { 87 return "", fmt.Errorf("failed to start command: %v: %s", err, stderrBuffer.String()) 88 } 89 90 err = cmd.Wait() 91 if err != nil { 92 return "", fmt.Errorf("failed to wait command: %v: %s", err, stderrBuffer.String()) 93 } 94 95 return stdoutBuffer.String(), nil 96 } 97 98 func filesChangedInCommit(refName string) []string { 99 output, err := capture("bash", []string{"-c", fmt.Sprintf("git log -m -1 --name-only --pretty=format: %s | awk -v RS= '{ print; exit }'", refName)}) 100 if err != nil { 101 panic(err) 102 } 103 files := strings.Split(output, "\n") 104 return files 105 } 106 107 func onlyDocsAreChanged(files []string) bool { 108 all := true 109 for _, file := range files { 110 all = all && (strings.HasPrefix(file, "Documentation/") || strings.HasPrefix(file, "docs/")) 111 } 112 return all 113 } 114 115 func onlyMiscFilesAreChanged(files []string) bool { 116 all := true 117 for _, file := range files { 118 all = all && (len(strings.Split(file, "/")) == 1 || strings.HasPrefix(file, "hack/") || strings.HasPrefix(file, "ci/") || strings.HasPrefix(file, "e2e/")) 119 } 120 return all 121 } 122 123 func containsAny(str string, substrs []string) bool { 124 for _, sub := range substrs { 125 if strings.Contains(str, sub) { 126 return true 127 } 128 } 129 return false 130 } 131 132 type Labels []github.Label 133 134 func (labels Labels) Contains(name string) bool { 135 found := false 136 for _, label := range labels { 137 if label.GetName() == name { 138 found = true 139 } 140 } 141 return found 142 } 143 144 var errorlog *log.Logger 145 146 func init() { 147 errorlog = log.New(os.Stderr, "", 0) 148 } 149 150 func exitWithErrorMessage(msg string) { 151 errorlog.Println(msg) 152 os.Exit(1) 153 } 154 155 func indent(orig string, num int) string { 156 lines := strings.Split(orig, "\n") 157 space := "" 158 buf := bytes.Buffer{} 159 for i := 0; i < num; i++ { 160 space = space + " " 161 } 162 for _, line := range lines { 163 buf.WriteString(fmt.Sprintf("%s%s\n", space, line)) 164 } 165 return buf.String() 166 } 167 168 type Config struct { 169 ctx context.Context 170 client *github.Client 171 org string 172 repository string 173 primaryMaintainer string 174 } 175 176 func collectIssuesForMilestoneNamed(releaseVersion string, config Config, allMilestones []*github.Milestone) []Item { 177 ctx := config.ctx 178 client := config.client 179 org := config.org 180 repository := config.repository 181 182 milestoneNumber := -1 183 for _, m := range allMilestones { 184 if m.GetTitle() == releaseVersion { 185 milestoneNumber = m.GetNumber() 186 } 187 } 188 if milestoneNumber == -1 { 189 exitWithErrorMessage(fmt.Sprintf("Milestone titled \"%s\" not found", releaseVersion)) 190 } 191 192 opt := &github.IssueListByRepoOptions{ 193 ListOptions: github.ListOptions{PerPage: 10}, 194 State: "closed", 195 Sort: "created", 196 Direction: "asc", 197 Milestone: fmt.Sprintf("%d", milestoneNumber), 198 } 199 200 items := []Item{} 201 202 // list all organizations for user "mumoshu" 203 var allIssues []*github.Issue 204 for { 205 issues, resp, err := client.Issues.ListByRepo(ctx, org, repository, opt) 206 PanicIfError(err) 207 for _, issue := range issues { 208 if issue.PullRequestLinks == nil { 209 fmt.Printf("skipping issue #%d %s\n", issue.GetNumber(), issue.GetTitle()) 210 continue 211 } 212 pr, _, err := client.PullRequests.Get(ctx, org, repository, issue.GetNumber()) 213 PanicIfError(err) 214 if !pr.GetMerged() { 215 continue 216 } 217 hash := pr.GetMergeCommitSHA() 218 219 login := issue.User.GetLogin() 220 num := issue.GetNumber() 221 title := issue.GetTitle() 222 summary := "" 223 if login != config.primaryMaintainer { 224 summary = fmt.Sprintf("#%d: %s(Thanks to @%s)", num, title, login) 225 } else { 226 summary = fmt.Sprintf("#%d: %s", num, title) 227 } 228 229 labels := Labels(issue.Labels) 230 231 isRefactoring := labels.Contains("refactoring") 232 233 fmt.Printf("analyzing #%d %s...\n", num, title) 234 fmt.Printf("labels=%v\n", labels) 235 changedFiles := filesChangedInCommit(hash) 236 237 isFeature := labels.Contains("feature") 238 239 isDocUpdate := labels.Contains("documentation") || 240 (!isFeature && onlyDocsAreChanged(changedFiles)) 241 if isDocUpdate { 242 fmt.Printf("%s is doc update\n", title) 243 } 244 245 isMiscUpdate := labels.Contains("release-infra") || 246 onlyMiscFilesAreChanged(changedFiles) 247 if isMiscUpdate { 248 fmt.Printf("%s is misc update\n", title) 249 } 250 251 isBugFix := labels.Contains("bug") || 252 (!isRefactoring && !isDocUpdate && !isMiscUpdate && (strings.Contains(title, "fix") || strings.Contains(title, "Fix"))) 253 254 isProposal := labels.Contains("proposal") || 255 (!isRefactoring && !isDocUpdate && !isMiscUpdate && !isBugFix && (strings.Contains(title, "proposal") || strings.Contains(title, "Proposal"))) 256 257 isImprovement := labels.Contains("improvement") || 258 (!isRefactoring && !isDocUpdate && !isMiscUpdate && !isBugFix && !isProposal && containsAny(title, []string{"improve", "Improve", "update", "Update", "bump", "Bump", "Rename", "rename"})) 259 260 if !isFeature { 261 isFeature = !isRefactoring && !isDocUpdate && !isMiscUpdate && !isBugFix && !isProposal && !isImprovement 262 } 263 264 actionsRequired := "" 265 noteShouldBeAdded := false 266 for _, label := range issue.Labels { 267 if label.GetName() == "release-note" { 268 noteShouldBeAdded = true 269 } 270 } 271 if noteShouldBeAdded { 272 body := issue.GetBody() 273 splits := strings.Split(body, "**Release note**:") 274 if len(splits) != 2 { 275 panic(fmt.Errorf("failed to extract release note from PR body: unexpected format of PR body: it should include \"**Release note**:\" followed by note: issue=%s body=%s", title, body)) 276 } 277 fmt.Printf("actions required(raw)=\"%s\"\n", splits[1]) 278 actionsRequired = strings.TrimSpace(splits[1]) 279 fmt.Printf("actions required(trimmed)=\"%s\"\n", actionsRequired) 280 281 if !strings.HasPrefix(actionsRequired, "* ") { 282 actionsRequired = fmt.Sprintf("* %s", actionsRequired) 283 } 284 } 285 286 item := Item{ 287 number: num, 288 title: title, 289 summary: summary, 290 actionsRequired: actionsRequired, 291 isMetaUpdate: isMiscUpdate, 292 isDocUpdate: isDocUpdate, 293 isImprovement: isImprovement, 294 isFeature: isFeature, 295 isBugFix: isBugFix, 296 isProposal: isProposal, 297 isRefactoring: isRefactoring, 298 } 299 items = append(items, item) 300 //Info(summary) 301 } 302 allIssues = append(allIssues, issues...) 303 if resp.NextPage == 0 { 304 break 305 } 306 opt.Page = resp.NextPage 307 } 308 309 return items 310 } 311 312 func generateNote(primaryMaintainer string, org string, repository string, releaseVersion string) { 313 rc := strings.Contains(releaseVersion, "rc") 314 315 accessToken, found := os.LookupEnv("GITHUB_ACCESS_TOKEN") 316 if !found { 317 exitWithErrorMessage("GITHUB_ACCESS_TOKEN must be set") 318 } 319 ctx := context.Background() 320 ts := oauth2.StaticTokenSource( 321 &oauth2.Token{AccessToken: accessToken}, 322 ) 323 tc := oauth2.NewClient(ctx, ts) 324 client := github.NewClient(tc) 325 326 config := Config{ 327 ctx: ctx, 328 client: client, 329 primaryMaintainer: primaryMaintainer, 330 org: org, 331 repository: repository, 332 } 333 334 milestoneOpt := &github.MilestoneListOptions{ 335 ListOptions: github.ListOptions{PerPage: 10}, 336 } 337 338 allMilestones := []*github.Milestone{} 339 for { 340 milestones, resp, err := client.Issues.ListMilestones(ctx, org, repository, milestoneOpt) 341 PanicIfError(err) 342 allMilestones = append(allMilestones, milestones...) 343 if resp.NextPage == 0 { 344 break 345 } 346 milestoneOpt.Page = resp.NextPage 347 } 348 349 milestoneNames := []string{} 350 351 if rc { 352 milestoneNames = append(milestoneNames, releaseVersion) 353 } else { 354 for _, m := range allMilestones { 355 if strings.HasPrefix(m.GetTitle(), releaseVersion) { 356 milestoneNames = append(milestoneNames, m.GetTitle()) 357 } 358 } 359 } 360 361 sort.Strings(milestoneNames) 362 363 fmt.Printf("Aggregating milestones: %s\n", strings.Join(milestoneNames, ", ")) 364 365 items := []Item{} 366 for _, n := range milestoneNames { 367 is := collectIssuesForMilestoneNamed(n, config, allMilestones) 368 items = append(items, is...) 369 } 370 371 Title("Changelog since v") 372 Info("Please see our [roadmap](https://github.com/kubernetes-incubator/kube-aws/blob/master/ROADMAP.md) for details on upcoming releases.") 373 374 Header("Component versions") 375 376 println("Kubernetes: v") 377 println("Etcd: v") 378 println("Calico: v") 379 println("Helm/Tiller: v") 380 381 Header("Actions required") 382 for _, item := range items { 383 if item.actionsRequired != "" { 384 fmt.Printf("* #%d: %s\n%s\n", item.number, item.title, indent(item.actionsRequired, 2)) 385 } 386 } 387 388 Header("Features") 389 for _, item := range items { 390 if item.isFeature { 391 Info("* " + item.summary) 392 } 393 } 394 395 Header("Improvements") 396 for _, item := range items { 397 if item.isImprovement { 398 Info("* " + item.summary) 399 } 400 } 401 402 Header("Bug fixes") 403 for _, item := range items { 404 if item.isBugFix { 405 Info("* " + item.summary) 406 } 407 } 408 409 Header("Documentation") 410 for _, item := range items { 411 if item.isDocUpdate { 412 Info("* " + item.summary) 413 } 414 } 415 416 Header("Refactorings") 417 for _, item := range items { 418 if item.isRefactoring { 419 Info("* " + item.summary) 420 } 421 } 422 423 Header("Other changes") 424 for _, item := range items { 425 if !item.isDocUpdate && !item.isFeature && !item.isImprovement && !item.isBugFix && !item.isRefactoring { 426 Info("* " + item.summary) 427 } 428 } 429 } 430 431 func main() { 432 releaseVersion, found := os.LookupEnv("VERSION") 433 if !found { 434 exitWithErrorMessage("VERSION must be set") 435 } 436 generateNote("mumoshu", "kubernetes-incubator", "kube-aws", releaseVersion) 437 }