diff --git a/cmd/release/BUILD.bazel b/cmd/release/BUILD.bazel new file mode 100644 index 0000000000..569be9fbea --- /dev/null +++ b/cmd/release/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "sigs.k8s.io/cluster-api-provider-aws/cmd/release", + visibility = ["//visibility:private"], +) + +go_binary( + name = "release", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/cmd/release/main.go b/cmd/release/main.go new file mode 100644 index 0000000000..f6a80c65e7 --- /dev/null +++ b/cmd/release/main.go @@ -0,0 +1,274 @@ +/* +Copyright 2019 The Kubernetes 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 ( + "flag" + "fmt" + "os" + "os/exec" + "path" +) + +/* +required parameters + +https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line +github token for modifying release + +# dependencies +* requires a build environment (make artifact-release must work) +* github.com/itchio/gothub +*/ + +const ( + // TODO(chuckha): figure this out based on directory name + repository = "cluster-api-provider-aws" +) + +func main() { + var remote, user, version string + + fs := flag.NewFlagSet("main", flag.ExitOnError) + fs.Usage = documentation + fs.StringVar(&remote, "remote", "origin", "name of the remote in local git") + fs.StringVar(&user, "user", "kubernetes-sigs", "the github user/organization of the repo to release a version to") + + // TODO(chuckha): it would be ideal if we could release major/minor/patch and have it + // automatically bump the latest tag git finds + // until then, hard code it + fs.StringVar(&version, "version", "", "the version number, for example v0.2.0") + if err := fs.Parse(os.Args[1:]); err != nil { + fmt.Println(err) + os.Exit(1) + } + required("version", version) + + checkDependencies() + + cfg := config{ + version: version, + artifactDir: "out", + artifacts: []string{ + "cluster-api-provider-aws-examples.tar", + "clusterawsadm-darwin-amd64", + "clusterawsadm-linux-amd64", + "clusterctl-darwin-amd64", + "clusterctl-linux-amd64", + }, + } + + run := &runner{ + releaser: gothubReleaser{ + artifactsDir: cfg.artifactDir, + user: user, + repository: repository, + }, + tagger: git{ + repository: repository, + remote: remote, + }, + config: cfg, + } + + if err := run.run(); err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Println("The next steps are:") + fmt.Println() + // TODO(chuckha): automate writing the release notes + fmt.Println("- Write the release notes. It is a manual process") + // TODO(chuckha): something something docker container + fmt.Println("- push the container images") + // TODO(chuckha): send an email or at least print out the contents of an + // email to + fmt.Println("- Email kubernetes-dev@googlegroups.com to announce that a release happened") +} + +func required(arg, val string) { + if val == "" { + fmt.Printf("%v is a required parameter\n", arg) + os.Exit(1) + } +} + +func checkDependencies() { + _, err := exec.LookPath("gothub") + if err != nil { + fmt.Println("Please install gothub:") + fmt.Println() + fmt.Println(" go get github.com/itchio/gothub") + os.Exit(1) + } + + if ght := os.Getenv("GITHUB_TOKEN"); len(ght) == 0 { + fmt.Println("Please set the GITHUB_TOKEN environment variable.") + fmt.Println("Read this guide for more information on creating Personal Access Tokens") + fmt.Println() + fmt.Println("https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line") + os.Exit(1) + } +} + +func documentation() { + fmt.Println(` +This tool is designed to help automate the release process. + +Usage: + + release [-remote ] [-user ] -version + +Options: + + -remote + The local name of the remote repository. + The default is origin. + + -user + The github username or organization of the repository to publish a release to. + The default is kubernetes-sigs. + + -version + The name of the version to release. + +Examples: + +1. + origin is github.com/kubernetes-sigs/cluster-api-provider-aws + local master points to origin/master branch + + To release: + + ./release -remote origin -version v1.1.1 + +2. + origin is github.com/myuser/cluster-api-provider-aws + upstream is github.com/kubernetes-sigs/cluster-api-provider-aws + local master points to upstream/master branch + + To release: + + ./release -remote upstream -version v1.1.1 + +3. + To test release + + ./release -remote YOUR_FORK -user YOUR_GITHUB_USER_NAME -version v1.1.1`) +} + +type config struct { + version string + artifacts []string + artifactDir string +} + +type runner struct { + releaser releaser + tagger tagger + config config +} + +func (r runner) run() error { + fmt.Print("tagging repository ") + if err := r.tagger.tag(r.config.version); err != nil { + return err + } + fmt.Println("🐲") + fmt.Print("pushing tag ") + if err := r.tagger.pushTag(r.config.version); err != nil { + return err + } + fmt.Println("🐲") + fmt.Print("drafting a release ") + if err := r.releaser.draft(r.config.version); err != nil { + return err + } + fmt.Println("🐲") + for _, artifact := range r.config.artifacts { + fmt.Printf("uploading %q ", artifact) + if err := r.releaser.upload(r.config.version, artifact); err != nil { + return err + } + fmt.Println("🐲") + } + return nil +} + +type releaser interface { + draft(string) error + upload(string, string) error +} + +type gothubReleaser struct { + // github user or organization (kubernetes-sigs, heptiolabs, chuckha, etc) + user string + // repository is the name of the repository + repository string + + artifactsDir string +} + +func (g gothubReleaser) draft(version string) error { + cmd := exec.Command("gothub", "release", "--tag", version, "--user", g.user, "--repo", g.repository, "--draft") + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + } + return err +} +func (g gothubReleaser) upload(version, file string) error { + cmd := exec.Command("gothub", "upload", "--tag", version, "--user", g.user, "--repo", g.repository, "--file", path.Join(g.artifactsDir, file), "--name", file) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + } + return err +} + +type tagger interface { + tag(string) error + pushTag(string) error +} + +type git struct { + // repository is the name of the repository + repository string + + // remote is the local name of the remote + remote string +} + +func (g git) tag(version string) error { + cmd := exec.Command("git", "tag", "-s", "-m", fmt.Sprintf("A release of %q for version %q", g.repository, version), version) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + } + return err +} + +func (g git) pushTag(version string) error { + // TODO(chuckha): this shouldn't exit if it fails because the tag already + cmd := exec.Command("git", "push", g.remote, version) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + } + return err +} diff --git a/docs/releasing.md b/docs/releasing.md index 9fcf319c2a..95d99400ea 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,17 +1,25 @@ # Release process +## Semi-automatic + +1. make the release artifacts `make release-artifacts` +2. Run the release tool found in `cmd/release` +3. Pick up the manual steps starting with step 6 + +## Manual + 1. Create a draft release in github -3. Tag the repository and push the tag `git tag -s $VERSION ` -4. Run `make release-artifacts` -5. Attach the tarball to the drafted release -6. Attach `clusterawsadm` and `clusterctl` to the drafted release (for darwin +2. Tag the repository and push the tag `git tag -s $VERSION ` +3. Run `make release-artifacts` +4. Attach the tarball to the drafted release +5. Attach `clusterawsadm` and `clusterctl` to the drafted release (for darwin and linux architectures) -7. Write the release notes (see note below on release notes) -8. Get someone with permission to copy the container image from your own +6. Write the release notes (see note below on release notes) +7. Get someone with permission to copy the container image from your own personal gcr registry to the production one, or have them build and push the container image themselves. -9. Publish release -10. Email `kubernetes-dev@googlegroups.com` to announce the release +8. Publish release +9. Email `kubernetes-dev@googlegroups.com` to announce the release ## Expected artifacts