Skip to content

Instantly share code, notes, and snippets.

Last active Apr 2, 2021
What would you like to do?

release-note aggregator

  • identifies PRs merged since the last release with kind and release notes, separates by kind, prints Markdown to stdout
  • written in Go instead of Bash
  • makes as few GitHub API requests as possible, to avoid rate limits
    • when it does hit rate limiting, it'll wait for quota and retry

Usage and example output

$ go run ./ >
# Release Notes

## Features

- PR #641: Introduce new Failed Reasons for BuildRun `Status.Conditions` and enhance the scenarios when BuildRuns are marked as Failed.
- PR #623: Exposing MaxConcurrentReconciles configuration of controllers to allow increasing them from the default (1) to be able to handle a larger amount of objects in the system
- PR #617: Update Kaniko to v1.5.1 including better retry support for the image push to the container registry
- PR #603: - Adding a sample build strategy for [ko](

## Fixes

- PR #690: Tag nightly release images to avoid registry garbage collection
- PR #639: Github actions: ensure nightly and release jobs are not run on contributor forks.
- PR #592: Fixed an issue where the build-controller fails to Reconcile with a panic due to a nil pointer dereference. The issue can arise in case there is a failed TaskRun with a pod that has still running (unfinished) containers.

## API Changes

- PR #683: Remove unused fields from GitSource
- PR #674: BuildRun .status.succeeded and .status.reason are marked as deprecated in favor of .status.conditions.
- PR #609: BREAKING: Rename API group from to

## Documentation

- PR #669: Ensure the README is easier to digest and that it highlights the main value for Shipwright users.
- PR #648: Your release note here
- PR #610: BREAKING: Change installation namespace, SA, ClusterRole{Binding}, and Deployment names
go 1.16
require ( v17.0.0+incompatible v1.1.0 // indirect
) v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
package main
import (
var (
ghOwner = flag.String("gh_owner", "shipwright-io", "GitHub repo owner")
ghRepo = flag.String("gh_repo", "build", "GitHub repo name")
prevRelease = flag.String("prev", "v0.3.0", "Previous release to search since")
func main() {
ctx := context.Background()
client := github.NewClient(nil)
var rel *github.RepositoryRelease
var err error
for {
rel, _, err = client.Repositories.GetReleaseByTag(ctx, *ghOwner, *ghRepo, *prevRelease)
if err != nil {
if rlerr, ok := err.(*github.RateLimitError); ok {
wait := time.Until(rlerr.Rate.Reset.Time)
log.Printf("hit rate limit (%q); waiting %s for reset... ", rlerr.Message, wait)
log.Fatalf("Getting release %q: %v", *prevRelease, err)
// We'll stop when we reach a PR merged before the previous release was published.
// This isn't necesarily a reliable heuristic because of cherrypicks, but it's probably good enough in most cases.
highwater := rel.PublishedAt.Time
log.Printf("Previous release %q was published at %s, looking for PRs merged since then...", *prevRelease, highwater)
pageNum := 0
prs := map[string][]*github.PullRequest{}
for {
log.Printf("requesting page %d...", pageNum)
page, _, err := client.PullRequests.List(ctx, *ghOwner, *ghRepo, &github.PullRequestListOptions{
State: "closed",
ListOptions: github.ListOptions{
PerPage: 100,
Page: pageNum,
if err != nil {
if rlerr, ok := err.(*github.RateLimitError); ok {
wait := time.Until(rlerr.Rate.Reset.Time)
log.Printf("hit rate limit (%q); waiting %s for reset... ", rlerr.Message, wait)
log.Fatalf("Listing pull requests: %v", err)
for _, pr := range page {
if pr.MergedAt == nil {
// Ignore PRs that are closed without being merged.
if pr.MergedAt.Before(highwater) {
log.Printf("PR #%d was merged before previous release, done looking!", *pr.Number)
break L
if !strings.Contains(*pr.Body, "```release-note") {
lbls := map[string]bool{}
for _, lbl := range pr.Labels {
lbls[*lbl.Name] = true
if lbls["release-note-none"] {
continue L2
for lbl := range lbls {
if strings.HasPrefix(lbl, "kind/") {
k := strings.TrimPrefix(lbl, "kind/")
prs[k] = append(prs[k], pr)
log.Println("generating release notes...")
fmt.Println("# Release Notes")
for _, section := range []struct {
header, kind string
{"Features", "feature"},
{"Fixes", "bug"},
{"API Changes", "api-change"},
{"Documentation", "documentation"},
{"Miscelaneous", "misc"},
} {
if len(prs[section.kind]) == 0 {
// Don't print header if there's no PRs of that kind.
fmt.Println("##", section.header)
for _, pr := range prs[section.kind] {
relnote := ""
print := false
for _, line := range strings.Split(*pr.Body, "\n") {
if strings.HasPrefix(line, "```release-note") {
print = true
if strings.HasPrefix(line, "```") {
if print {
relnote += line + "\n"
relnote = strings.TrimSpace(relnote)
if relnote == "" {
log.Printf("Didn't find release note for PR #%d!", *pr.Number)
if relnote == "NONE" {
fmt.Printf("- PR #%d: %s\n", *pr.Number, relnote)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment