From 769d9e16bdfd9805cacc81eecad3468e91a27ca6 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 11 Oct 2017 15:40:59 -0400 Subject: [PATCH] add release tool to generate releases This tool makes our standard release template easy to generate. It also adds a few features like marking changed dependnencies for packages and others to know what updated from the last release. usage: `containerd-release -n releases/v1.0.0-beta.2.toml` Signed-off-by: Michael Crosby --- Makefile | 2 +- cmd/containerd-release/main.go | 297 +++++++++++++++++++++++++++++++++ releases/v1.0.0-beta.2.toml | 20 +++ 3 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 cmd/containerd-release/main.go create mode 100644 releases/v1.0.0-beta.2.toml diff --git a/Makefile b/Makefile index 19127cb5e..fac78f134 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ TEST_REQUIRES_ROOT_PACKAGES=$(filter \ ) # Project binaries. -COMMANDS=ctr containerd containerd-stress +COMMANDS=ctr containerd containerd-stress containerd-release BINARIES=$(addprefix bin/,$(COMMANDS)) GO_TAGS=$(if $(BUILDTAGS),-tags "$(BUILDTAGS)",) diff --git a/cmd/containerd-release/main.go b/cmd/containerd-release/main.go new file mode 100644 index 000000000..444c39c55 --- /dev/null +++ b/cmd/containerd-release/main.go @@ -0,0 +1,297 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "text/template" + + "github.com/BurntSushi/toml" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +const releaseNotes = `Welcome to the release of containerd {{.Version}}! +{{if .PreRelease}} +*This is a pre-release of containerd* +{{- end}} + +{{.Preface}} + +{{range $note := .Notes}} +### {{$note.Title}} + +{{$note.Description}} +{{- end}} + +### Contributors +{{range $contributor := .Contributors}} +* {{$contributor}} +{{- end}} + +### Changes +{{range $change := .Changes}} +* {{$change.Commit}} {{$change.Description}} +{{- end}} + +### Dependency Changes + +Previous release can be found at [{{.Previous}}](https://github.com/containerd/containerd/releases/tag/{{.Previous}}) +{{range $dep := .Dependencies}} +* {{$dep.Previous}} -> {{$dep.Commit}} **{{$dep.Name}}** +{{- end}} + +## Downloads +` + +const vendorConf = "vendor.conf" + +type note struct { + Title string `toml:"title"` + Description string `toml:"description"` +} + +type change struct { + Commit string `toml:"commit"` + Description string `toml:"description"` +} + +type dependency struct { + Name string + Commit string + Previous string +} + +type release struct { + Commit string `toml:"commit"` + Previous string `toml:"previous"` + PreRelease bool `toml:"pre_release"` + Preface string `toml:"preface"` + Notes map[string]note `toml:"notes"` + BreakingChanges map[string]change `toml:"breaking"` + // generated fields + Changes []change + Contributors []string + Dependencies []dependency + Version string +} + +func main() { + app := cli.NewApp() + app.Name = "containerd-release" + app.Description = `release tooling for containerd. + +This tool should be ran from the root of the containerd repository for a new release. +` + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "dry,n", + Usage: "run the release tooling as a dry run to print the release notes to stdout", + }, + } + app.Action = func(context *cli.Context) error { + logrus.Info("Welcome to the containerd release tool...") + var ( + path = context.Args().First() + tag = parseTag(path) + ) + r, err := loadRelease(path) + if err != nil { + return err + } + previous, err := getPreviousDeps(r.Previous) + if err != nil { + return err + } + changes, err := changelog(r.Previous, r.Commit) + if err != nil { + return err + } + logrus.Infof("creating new release %s with %d new changes...", tag, len(changes)) + if err := checkoutRelease(r.Commit); err != nil { + return err + } + deps, err := parseDependencies(vendorConf) + if err != nil { + return err + } + updatedDeps := updatedDeps(previous, deps) + contributors, err := getContributors(r.Previous, r.Commit) + if err != nil { + return err + } + // update the release fields with generated data + r.Contributors = contributors + r.Dependencies = updatedDeps + r.Changes = changes + r.Version = tag + + if context.Bool("dry") { + t, err := template.New("release-notes").Parse(releaseNotes) + if err != nil { + return err + } + return t.Execute(os.Stdout, r) + } + return nil + } + if err := app.Run(os.Args); err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } +} + +func loadRelease(path string) (*release, error) { + var r release + if _, err := toml.DecodeFile(path, &r); err != nil { + if os.IsNotExist(err) { + return nil, errors.New("please specify the release file as the first argument") + } + return nil, err + } + return &r, nil +} + +func parseTag(path string) string { + return strings.TrimSuffix(filepath.Base(path), ".toml") +} + +func parseDependencies(depfile string) ([]dependency, error) { + f, err := os.Open(depfile) + if err != nil { + return nil, err + } + defer f.Close() + var deps []dependency + s := bufio.NewScanner(f) + for s.Scan() { + ln := strings.TrimSpace(s.Text()) + if strings.HasPrefix(ln, "#") || ln == "" { + continue + } + cidx := strings.Index(ln, "#") + if cidx > 0 { + ln = ln[:cidx] + } + ln = strings.TrimSpace(ln) + parts := strings.Fields(ln) + if len(parts) != 2 && len(parts) != 3 { + return nil, fmt.Errorf("invalid config format: %s", ln) + } + deps = append(deps, dependency{ + Name: parts[0], + Commit: parts[1], + }) + } + if err := s.Err(); err != nil { + return nil, err + } + return deps, nil +} + +func getPreviousDeps(previous string) ([]dependency, error) { + if _, err := git("checkout", previous); err != nil { + return nil, err + } + return parseDependencies(vendorConf) +} + +func checkoutRelease(commit string) error { + _, err := git("checkout", commit) + return err +} + +func changelog(previous, commit string) ([]change, error) { + raw, err := getChangelog(previous, commit) + if err != nil { + return nil, err + } + return parseChangelog(raw) +} + +func getChangelog(previous, commit string) ([]byte, error) { + return git("log", "--oneline", fmt.Sprintf("%s..%s", previous, commit)) +} + +func parseChangelog(changelog []byte) ([]change, error) { + var ( + changes []change + s = bufio.NewScanner(bytes.NewReader(changelog)) + ) + for s.Scan() { + fields := strings.Fields(s.Text()) + changes = append(changes, change{ + Commit: fields[0], + Description: strings.Join(fields[1:], " "), + }) + } + if err := s.Err(); err != nil { + return nil, err + } + return changes, nil +} + +func git(args ...string) ([]byte, error) { + o, err := exec.Command("git", args...).CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%s: %s", err, o) + } + return o, nil +} + +func updatedDeps(previous, deps []dependency) []dependency { + var updated []dependency + pm, cm := toDepMap(previous), toDepMap(deps) + for name, c := range cm { + d, ok := pm[name] + if !ok { + // it is a new dep and should be noted + updated = append(updated, c) + continue + } + // it exists, see if its updated + if d.Commit != c.Commit { + // set the previous commit + c.Previous = d.Commit + updated = append(updated, c) + } + } + return updated +} + +func toDepMap(deps []dependency) map[string]dependency { + out := make(map[string]dependency) + for _, d := range deps { + out[d.Name] = d + } + return out +} + +func getContributors(previous, commit string) ([]string, error) { + raw, err := git("log", "--format=%aN", fmt.Sprintf("%s..%s", previous, commit)) + if err != nil { + return nil, err + } + var ( + set = make(map[string]struct{}) + s = bufio.NewScanner(bytes.NewReader(raw)) + out []string + ) + for s.Scan() { + set[s.Text()] = struct{}{} + } + if err := s.Err(); err != nil { + return nil, err + } + for name := range set { + out = append(out, name) + } + sort.Strings(out) + return out, nil +} diff --git a/releases/v1.0.0-beta.2.toml b/releases/v1.0.0-beta.2.toml new file mode 100644 index 000000000..1aac2c4c9 --- /dev/null +++ b/releases/v1.0.0-beta.2.toml @@ -0,0 +1,20 @@ +# commit to be tagged for new release +commit = "60960e1c177046de655773258db53300d0e7f005" + +# previous release +previous = "v1.0.0-beta.1" + +pre_release = true + +preface = """This release does cool stuff""" + +# notable prs to include in the release notes, 1234 is the pr number +[notes] + [notes.1234] + title = "Some Title" + description = """This did something cool.""" + +[breaking] + [breaking.metrics] + pr = 1235 + description = """ """