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  }