diff --git a/Makefile b/Makefile index 6424b78f2..35021fd3b 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ TEST_REQUIRES_ROOT_PACKAGES=$(filter \ ) # Project binaries. -COMMANDS=ctr containerd containerd-stress containerd-release +COMMANDS=ctr containerd containerd-stress MANPAGES=ctr.1 containerd.1 containerd-config.1 containerd-config.toml.5 # Build tags seccomp and apparmor are needed by CRI plugin. diff --git a/cmd/containerd-release/main.go b/cmd/containerd-release/main.go deleted file mode 100644 index c190d0e27..000000000 --- a/cmd/containerd-release/main.go +++ /dev/null @@ -1,290 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package main - -import ( - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "regexp" - "sort" - "strings" - "text/tabwriter" - "text/template" - "unicode" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" -) - -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 - CloneURL string -} - -type download struct { - Filename string - Hash string -} - -type projectChange struct { - Name string - Changes []change -} - -type projectRename struct { - Old string `toml:"old"` - New string `toml:"new"` -} - -type release struct { - ProjectName string `toml:"project_name"` - GithubRepo string `toml:"github_repo"` - 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"` - - // dependency options - MatchDeps string `toml:"match_deps"` - RenameDeps map[string]projectRename `toml:"rename_deps"` - - // generated fields - Changes []projectChange - Contributors []string - Dependencies []dependency - Tag string - Version string - Downloads []download -} - -func main() { - app := cli.NewApp() - app.Name = "release" - app.Description = `release tooling. - -This tool should be ran from the root of the project 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", - }, - cli.BoolFlag{ - Name: "debug,d", - Usage: "show debug output", - }, - cli.StringFlag{ - Name: "tag,t", - Usage: "tag name for the release, defaults to release file name", - }, - cli.StringFlag{ - Name: "template", - Usage: "template filepath to use in place of the default", - Value: defaultTemplateFile, - }, - cli.BoolFlag{ - Name: "linkify,l", - Usage: "add links to changelog", - }, - } - app.Action = func(context *cli.Context) error { - var ( - releasePath = context.Args().First() - tag = context.String("tag") - linkify = context.Bool("linkify") - ) - if tag == "" { - tag = parseTag(releasePath) - } - version := strings.TrimLeft(tag, "v") - if context.Bool("debug") { - logrus.SetLevel(logrus.DebugLevel) - } - r, err := loadRelease(releasePath) - if err != nil { - return err - } - logrus.Infof("Welcome to the %s release tool...", r.ProjectName) - - mailmapPath, err := filepath.Abs(".mailmap") - if err != nil { - return errors.Wrap(err, "failed to resolve mailmap") - } - gitConfigs["mailmap.file"] = mailmapPath - - var ( - contributors = map[contributor]int{} - projectChanges = []projectChange{} - ) - - changes, err := changelog(r.Previous, r.Commit) - if err != nil { - return err - } - if linkify { - if err := linkifyChanges(changes, githubCommitLink(r.GithubRepo), githubPRLink(r.GithubRepo)); err != nil { - return err - } - } - if err := addContributors(r.Previous, r.Commit, contributors); err != nil { - return err - } - projectChanges = append(projectChanges, projectChange{ - Name: "", - Changes: changes, - }) - - logrus.Infof("creating new release %s with %d new changes...", tag, len(changes)) - rd, err := fileFromRev(r.Commit, vendorConf) - if err != nil { - return err - } - previous, err := getPreviousDeps(r.Previous) - if err != nil { - return err - } - deps, err := parseDependencies(rd) - if err != nil { - return err - } - renameDependencies(previous, r.RenameDeps) - updatedDeps := updatedDeps(previous, deps) - - sort.Slice(updatedDeps, func(i, j int) bool { - return updatedDeps[i].Name < updatedDeps[j].Name - }) - - if r.MatchDeps != "" && len(updatedDeps) > 0 { - re, err := regexp.Compile(r.MatchDeps) - if err != nil { - return errors.Wrap(err, "unable to compile 'match_deps' regexp") - } - td, err := ioutil.TempDir("", "tmp-clone-") - if err != nil { - return errors.Wrap(err, "unable to create temp clone directory") - } - defer os.RemoveAll(td) - - cwd, err := os.Getwd() - if err != nil { - return errors.Wrap(err, "unable to get cwd") - } - for _, dep := range updatedDeps { - matches := re.FindStringSubmatch(dep.Name) - if matches == nil { - continue - } - logrus.Debugf("Matched dependency %s with %s", dep.Name, r.MatchDeps) - var name string - if len(matches) < 2 { - name = path.Base(dep.Name) - } else { - name = matches[1] - } - if err := os.Chdir(td); err != nil { - return errors.Wrap(err, "unable to chdir to temp clone directory") - } - git("clone", dep.CloneURL, name) - - if err := os.Chdir(name); err != nil { - return errors.Wrapf(err, "unable to chdir to cloned %s directory", name) - } - - changes, err := changelog(dep.Previous, dep.Commit) - if err != nil { - return errors.Wrapf(err, "failed to get changelog for %s", name) - } - if err := addContributors(dep.Previous, dep.Commit, contributors); err != nil { - return errors.Wrapf(err, "failed to get authors for %s", name) - } - if linkify { - if !strings.HasPrefix(dep.Name, "github.com/") { - logrus.Debugf("linkify only supported for Github, skipping %s", dep.Name) - } else { - ghname := dep.Name[11:] - if err := linkifyChanges(changes, githubCommitLink(ghname), githubPRLink(ghname)); err != nil { - return err - } - } - } - - projectChanges = append(projectChanges, projectChange{ - Name: name, - Changes: changes, - }) - - } - if err := os.Chdir(cwd); err != nil { - return errors.Wrap(err, "unable to chdir to previous cwd") - } - } - - // update the release fields with generated data - r.Contributors = orderContributors(contributors) - r.Dependencies = updatedDeps - r.Changes = projectChanges - r.Tag = tag - r.Version = version - - // Remove trailing new lines - r.Preface = strings.TrimRightFunc(r.Preface, unicode.IsSpace) - - tmpl, err := getTemplate(context) - if err != nil { - return err - } - - if context.Bool("dry") { - t, err := template.New("release-notes").Parse(tmpl) - if err != nil { - return err - } - - w := tabwriter.NewWriter(os.Stdout, 8, 8, 2, ' ', 0) - if err := t.Execute(w, r); err != nil { - return err - } - return w.Flush() - } - logrus.Info("release complete!") - return nil - } - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/cmd/containerd-release/template.go b/cmd/containerd-release/template.go deleted file mode 100644 index a1da2d2e3..000000000 --- a/cmd/containerd-release/template.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package main - -const ( - defaultTemplateFile = "TEMPLATE" - releaseNotes = `{{.ProjectName}} {{.Version}} - -Welcome to the {{.Tag}} release of {{.ProjectName}}! -{{- if .PreRelease }} -*This is a pre-release of {{.ProjectName}}* -{{- end}} - -{{.Preface}} - -Please try out the release binaries and report any issues at -https://github.com/{{.GithubRepo}}/issues. - -{{- range $note := .Notes}} - -### {{$note.Title}} - -{{$note.Description}} -{{- end}} - -### Contributors -{{range $contributor := .Contributors}} -* {{$contributor}} -{{- end -}} - -{{range $project := .Changes}} - -### Changes{{if $project.Name}} from {{$project.Name}}{{end}} -{{range $change := $project.Changes }} -* {{$change.Commit}} {{$change.Description}} -{{- end}} -{{- end}} - -### Dependency Changes - -Previous release can be found at [{{.Previous}}](https://github.com/{{.GithubRepo}}/releases/tag/{{.Previous}}) -{{range $dep := .Dependencies}} -* **{{$dep.Name}}** {{if $dep.Previous}}{{$dep.Previous}} -> {{$dep.Commit}}{{else}}{{$dep.Commit}} **_new_**{{end}} -{{- end}} -` -) diff --git a/cmd/containerd-release/util.go b/cmd/containerd-release/util.go deleted file mode 100644 index 552338490..000000000 --- a/cmd/containerd-release/util.go +++ /dev/null @@ -1,338 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package main - -import ( - "bufio" - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "regexp" - "sort" - "strings" - - "github.com/BurntSushi/toml" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" -) - -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(r io.Reader) ([]dependency, error) { - var deps []dependency - s := bufio.NewScanner(r) - 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) - } - - var cloneURL string - if len(parts) == 3 { - cloneURL = parts[2] - } else { - cloneURL = "git://" + parts[0] - } - - deps = append(deps, dependency{ - Name: parts[0], - Commit: parts[1], - CloneURL: cloneURL, - }) - } - if err := s.Err(); err != nil { - return nil, err - } - return deps, nil -} - -func getPreviousDeps(previous string) ([]dependency, error) { - r, err := fileFromRev(previous, vendorConf) - if err != nil { - return nil, err - } - return parseDependencies(r) -} - -func changelog(previous, commit string) ([]change, error) { - raw, err := getChangelog(previous, commit) - if err != nil { - return nil, err - } - return parseChangelog(raw) -} - -func gitChangeDiff(previous, commit string) string { - if previous != "" { - return fmt.Sprintf("%s..%s", previous, commit) - } - return commit -} - -func getChangelog(previous, commit string) ([]byte, error) { - return git("log", "--oneline", gitChangeDiff(previous, commit)) -} - -func linkifyChanges(c []change, commit, msg func(change) (string, error)) error { - for i := range c { - commitLink, err := commit(c[i]) - if err != nil { - return err - } - - description, err := msg(c[i]) - if err != nil { - return err - } - - c[i].Commit = fmt.Sprintf("[`%s`](%s)", c[i].Commit, commitLink) - c[i].Description = description - - } - - return nil -} - -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 fileFromRev(rev, file string) (io.Reader, error) { - p, err := git("show", fmt.Sprintf("%s:%s", rev, file)) - if err != nil { - return nil, err - } - - return bytes.NewReader(p), nil -} - -var gitConfigs = map[string]string{} - -func git(args ...string) ([]byte, error) { - var gitArgs []string - for k, v := range gitConfigs { - gitArgs = append(gitArgs, "-c", fmt.Sprintf("%s=%s", k, v)) - } - gitArgs = append(gitArgs, args...) - o, err := exec.Command("git", gitArgs...).CombinedOutput() - if err != nil { - return nil, fmt.Errorf("%s: %s", err, o) - } - return o, nil -} - -func renameDependencies(deps []dependency, renames map[string]projectRename) { - if len(renames) == 0 { - return - } - type dep struct { - shortname string - name string - } - renameMap := map[string]dep{} - for shortname, rename := range renames { - renameMap[rename.Old] = dep{ - shortname: shortname, - name: rename.New, - } - } - for i := range deps { - if updated, ok := renameMap[deps[i].Name]; ok { - logrus.Debugf("Renamed %s from %s to %s", updated.shortname, deps[i].Name, updated.name) - deps[i].Name = updated.name - } - } -} - -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 -} - -type contributor struct { - name string - email string -} - -func addContributors(previous, commit string, contributors map[contributor]int) error { - raw, err := git("log", `--format=%aE %aN`, gitChangeDiff(previous, commit)) - if err != nil { - return err - } - s := bufio.NewScanner(bytes.NewReader(raw)) - for s.Scan() { - p := strings.SplitN(s.Text(), " ", 2) - if len(p) != 2 { - return errors.Errorf("invalid author line: %q", s.Text()) - } - c := contributor{ - name: p[1], - email: p[0], - } - contributors[c] = contributors[c] + 1 - } - if err := s.Err(); err != nil { - return err - } - return nil -} - -func orderContributors(contributors map[contributor]int) []string { - type contribstat struct { - name string - email string - count int - } - all := make([]contribstat, 0, len(contributors)) - for c, count := range contributors { - all = append(all, contribstat{ - name: c.name, - email: c.email, - count: count, - }) - } - sort.Slice(all, func(i, j int) bool { - if all[i].count == all[j].count { - return all[i].name < all[j].name - } - return all[i].count > all[j].count - }) - names := make([]string, len(all)) - for i := range names { - logrus.Debugf("Contributor: %s <%s> with %d commits", all[i].name, all[i].email, all[i].count) - names[i] = all[i].name - } - - return names -} - -// getTemplate will use a builtin template if the template is not specified on the cli -func getTemplate(context *cli.Context) (string, error) { - path := context.GlobalString("template") - f, err := os.Open(path) - if err != nil { - // if the template file does not exist and the path is for the default template then - // return the compiled in template - if os.IsNotExist(err) && path == defaultTemplateFile { - return releaseNotes, nil - } - return "", err - } - defer f.Close() - data, err := ioutil.ReadAll(f) - if err != nil { - return "", err - } - return string(data), nil -} - -func githubCommitLink(repo string) func(change) (string, error) { - return func(c change) (string, error) { - full, err := git("rev-parse", c.Commit) - if err != nil { - return "", err - } - commit := strings.TrimSpace(string(full)) - - return fmt.Sprintf("https://github.com/%s/commit/%s", repo, commit), nil - } -} - -func githubPRLink(repo string) func(change) (string, error) { - r := regexp.MustCompile("^Merge pull request #[0-9]+") - return func(c change) (string, error) { - var err error - message := r.ReplaceAllStringFunc(c.Description, func(m string) string { - idx := strings.Index(m, "#") - pr := m[idx+1:] - - // TODO: Validate links using github API - // TODO: Validate PR merged as commit hash - link := fmt.Sprintf("https://github.com/%s/pull/%s", repo, pr) - - return fmt.Sprintf("%s [#%s](%s)", m[:idx], pr, link) - }) - if err != nil { - return "", err - } - return message, nil - } -}