diff --git a/README.md b/README.md index 5cf3f21..85121d2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # release-script Scripts to automate the release process. + +These scripts automate the release process which to-date has been +an intricate manual process. These are the steps: + +1. Check-out a current master branch +2. Create a release candidate branch +3. Generate release notes +3. Update version numbers and RELEASE.rst +4. Commit updates and push branch +6. Open PR against ``release candidate`` branch +7. Merge PR once ``travis-ci`` build succeeds +8. Generate release notes with checkboxes +8. Open PR against ``release`` branch +10. Open PR against ``master`` branch +11. Merge PRs once developers verify their commits +12. Send email notifications + +## Notes +1. diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..3d1e5b0 --- /dev/null +++ b/release.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# A script to automate the release process + +# Usage +# release working_dir version_num + +set -euf -o pipefail + +[[ "${TRACE:-}" ]] && set -x + +error () { # error that writes to stderr, not stdout. + >&2 echo $@ +} + +# Quote nesting works as described here: http://stackoverflow.com/a/6612417/4972 +# SCRIPT_DIR via http://www.ostricher.com/2014/10/the-right-way-to-get-the-directory-of-a-bash-script/ +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Default variables to empty if not present. Necessary due to the -u option specified above. +# For more information on this, look here: +# http://redsymbol.net/articles/unofficial-bash-strict-mode/#solution-positional-parameters +WORKING_DIR="${1:-}" # default $1 to empty if it's not supplied +VERSION="${2:-}" +OLD_VERSION= # set later. + +if [[ -z "$WORKING_DIR" ]]; then + error "You must specify a working directory as the first argument." + exit 1 +fi + +if [[ -z "$VERSION" ]]; then + error "You must specify a version as the second argument." + exit 1 +fi + +# Ensures the current working directory doesn't have tracked but uncommitted files in git. +clean_working_dir () { + if [[ "$(git status -s | grep -m1 "^ ")" ]]; then + error "Not checking out release. You have uncommitted files in your working directory." + exit 1 + fi +} + +# Check that requisite programs are available +validate_dependencies () { + local -i missing=0 + if ! hash hub 2>/dev/null; then + missing=$missing+1 + error 'Please install hub https://hub.github.com/' + fi + if ! hash git 2>/dev/null; then + missing=$missing+1 + error 'Please install git https://git-scm.com/downloads' + fi + if ! hash perl 2>/dev/null; then + missing=$missing+1 + error 'Please install perl https://www.perl.org/get.html' + fi + + if ! hash git-release-notes 2>/dev/null; then + missing=$missing+1 + error 'Please install git-release-notes https://www.npmjs.com/package/git-release-notes' + fi + + if [[ 0 -ne $missing ]]; then + exit $missing + fi +} + +# Updates the local repo +update_copy () { + cd $WORKING_DIR # change to repository working directory + clean_working_dir + git checkout master -q + git pull -q +} + +set_old_version () { + cd $WORKING_DIR + OLD_VERSION="$(find . -maxdepth 2 -name 'settings.py' | xargs grep VERSION | tr "\"" ' ' | awk '{print $3}')" + if [[ -z "$OLD_VERSION" ]]; then + error "Could not determine the old version." + exit 1 + fi +} + +# Checks out the release-candidate branch +checkout_release () { + cd $WORKING_DIR + clean_working_dir + # Create the branch if it doesn't exist. If it does, just check it out + git checkout -qb release-candidate 2>/dev/null || (git checkout -q release-candidate && git merge -q -m "Release $VERSION" master) +} + + +update_versions () { + # maxdepth, so we don't pull things from .tox, etc + find $WORKING_DIR -maxdepth 2 -name 'settings.py' | xargs perl -pi -e "s/VERSION = .*/VERSION = \"$VERSION\"/g" + find $WORKING_DIR -maxdepth 2 -name 'setup.py' | xargs perl -pi -e "s/version=.*/version='$VERSION',/g" +} + +update_release_notes () { + cd $WORKING_DIR + # Create/Update RELEASE.rst + # +4 is to offset the header of the template we don't want yet. + IFS=$'\n' # sets separator to only newlines. see http://askubuntu.com/a/344418 + NEW_RELEASE_NOTES=$(git-release-notes v$OLD_VERSION..master $SCRIPT_DIR/util/release_notes_rst.ejs) + + echo 'Release Notes' > releases_rst.new + echo '=============' >> releases_rst.new + echo '' >> releases_rst.new + echo "Version $VERSION" >> releases_rst.new + echo '-------------' >> releases_rst.new + echo '' >> releases_rst.new + + # we do this because, without it, bash ignores newlines in between the bullets. + for line in $NEW_RELEASE_NOTES; do + echo $line >> releases_rst.new + done; + echo '' >> releases_rst.new + cat RELEASE.rst >> releases_rst.new + mv releases_rst.new RELEASE.rst + # explicit add, because we know the location & we will need it for the first release + git add RELEASE.rst + git commit -q --all --message "Release $VERSION" +} + + +build_release () { + git push -q origin release-candidate:release-candidate + echo "Building release..." +} + +generate_prs () { + echo "Release $VERSION" > release-notes-checklist + echo "" >> release-notes-checklist + git-release-notes v$OLD_VERSION..master $SCRIPT_DIR/util/release_notes.ejs >> release-notes-checklist + hub pull-request -b release -h "release-candidate" -F release-notes-checklist +} + +main () { + validate_dependencies + update_copy + checkout_release + set_old_version + update_versions + update_release_notes + build_release + generate_prs + echo "version $OLD_VERSION has been updated to $VERSION" + echo "Go tell engineers to check their work. PR is on the repo." + echo "After they are done, run the next script." +} + + +# Next script: +# - tag build +# - push tags +# - merge release-candidate to release +# - merge release to master + +main diff --git a/util/release_notes.ejs b/util/release_notes.ejs new file mode 100644 index 0000000..ebbcacc --- /dev/null +++ b/util/release_notes.ejs @@ -0,0 +1,20 @@ +<% +by_author = {}; + +commits.forEach(function (commit) { + var author = commit.authorName; + by_author[author] = by_author[author] || []; + by_author[author].push(commit); +}) + +Object.keys(by_author).forEach(function (author) { + %>## <%= author %> +<% + by_author[author].forEach(function (commit) { + %> - [ ] <%= commit.title %> ([<%= commit.sha1.substring(0,8) %>](../commit/<%= commit.sha1 %>)) +<% + }); + %> +<% +}); +%> diff --git a/util/release_notes_rst.ejs b/util/release_notes_rst.ejs new file mode 100644 index 0000000..08ce531 --- /dev/null +++ b/util/release_notes_rst.ejs @@ -0,0 +1,2 @@ +<% commits.forEach(function (commit) { %>- <%= commit.title %> +<% }) %>