|  | #!/bin/bash -eu | 
|  |  | 
|  | # Copyright 2015 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # 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. | 
|  |  | 
|  | # Generate the release branches and handle the release tags. | 
|  |  | 
|  | set -eu | 
|  |  | 
|  | # Repositories to push the release branch and the release tag. | 
|  | : ${RELEASE_REPOSITORIES:="git@github.com:bazelbuild/bazel"} | 
|  |  | 
|  | # Repositories to push the master branch | 
|  | : ${MASTER_REPOSITORIES:="https://bazel.googlesource.com/bazel"} | 
|  |  | 
|  | # Name of the default editor | 
|  | : ${EDITOR=vi} | 
|  |  | 
|  | # Author of the release commits | 
|  | : ${RELEASE_AUTHOR="Bazel Release System <noreply@google.com>"} | 
|  |  | 
|  | # Load relnotes.sh | 
|  | SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) | 
|  | source ${SCRIPT_DIR}/relnotes.sh | 
|  |  | 
|  | # Load common.sh | 
|  | source ${SCRIPT_DIR}/common.sh | 
|  |  | 
|  | # Editing release notes info for the user | 
|  | RELEASE_NOTE_MESSAGE='# Editing release notes | 
|  | # Modify the release notes to make them suitable for the release. | 
|  | # Every line starting with a # will be removed as well as every | 
|  | # empty line at the start and at the end. | 
|  | ' | 
|  |  | 
|  | # Set the release name $1 (and eventually the candidate number $2). | 
|  | function set_release_name() { | 
|  | git notes --ref=release remove 2>/dev/null || true | 
|  | git notes --ref=release-candidate remove 2>/dev/null || true | 
|  | git notes --ref=release append -m "$1" | 
|  | [ -z "${2-}" ] || git notes --ref=release-candidate append -m "$2" | 
|  | } | 
|  |  | 
|  | # Trim empty lines at the beginning and the end of the buffer. | 
|  | function trim_empty_lines() { | 
|  | local f="$(echo $'\f')"  # linefeed because OSX sed does not support \f | 
|  | # Replace all new line by a linefeed, then using sed, remove the leading | 
|  | # and trailing linefeeds and convert them back to newline | 
|  | tr '\n' '\f' | sed -e "s/^$f*//" -e "s/$f*$//" | tr '\f' '\n' | 
|  | } | 
|  |  | 
|  | # Launch the editor and return the edited release notes | 
|  | function release_note_editor() { | 
|  | local tmpfile="$1" | 
|  | local origin_branch="$2" | 
|  | local branch_name="${3-}" | 
|  | $EDITOR ${tmpfile} || { | 
|  | echo "Editor failed, cancelling release creation..." >&2 | 
|  | git checkout -q ${origin_branch} >/dev/null | 
|  | [ -z "${branch_name}" ] || git branch -D ${branch_name} | 
|  | exit 1 | 
|  | } | 
|  | # Stripping the release notes | 
|  | local relnotes="$(cat ${tmpfile} | grep -v '^#' | trim_empty_lines)" | 
|  | if [ -z "${relnotes}" ]; then | 
|  | echo "Release notes are empty, cancelling release creation..." >&2 | 
|  | git checkout -q ${origin_branch} >/dev/null | 
|  | [ -z "${branch_name}" ] || git branch -D ${branch_name} | 
|  | exit 1 | 
|  | fi | 
|  | echo "${relnotes}" >${tmpfile} | 
|  | } | 
|  |  | 
|  | # Create the release commit by changing the CHANGELOG file | 
|  | function create_release_commit() { | 
|  | local release_title="$1" | 
|  | local release_name="$2" | 
|  | local relnotes="$3" | 
|  | local tmpfile="$4" | 
|  | local baseline="$5" | 
|  | shift 5 | 
|  | local cherrypicks=$@ | 
|  | local changelog_path="$PWD/CHANGELOG.md" | 
|  |  | 
|  | version_info=$(create_revision_information $baseline $cherrypicks) | 
|  | # CHANGELOG.md | 
|  | cat >${tmpfile} <<EOF | 
|  | ## ${release_title} | 
|  |  | 
|  | EOF | 
|  | if [ -n "${version_info}" ]; then | 
|  | cat >>${tmpfile} <<EOF | 
|  | \`\`\` | 
|  | ${version_info} | 
|  | \`\`\` | 
|  | EOF | 
|  | fi | 
|  | cat >>${tmpfile} <<EOF | 
|  |  | 
|  | ${relnotes} | 
|  | EOF | 
|  |  | 
|  | if [ -f "${changelog_path}" ]; then | 
|  | echo >>${tmpfile} | 
|  | cat "${changelog_path}" >>${tmpfile} | 
|  | fi | 
|  | cat ${tmpfile} > ${changelog_path} | 
|  | git add ${changelog_path} | 
|  | # Commit message | 
|  | cat >${tmpfile} <<EOF | 
|  | ${release_title} | 
|  |  | 
|  | ${version_info} | 
|  |  | 
|  | ${relnotes} | 
|  | EOF | 
|  | git commit --no-verify -F ${tmpfile} --no-edit --author "${RELEASE_AUTHOR}" | 
|  | } | 
|  |  | 
|  | function apply_cherry_picks() { | 
|  | echo "Applying cherry-picks" | 
|  | # Apply cherry-picks | 
|  | for i in $@; do | 
|  | local previous_head="$(git rev-parse HEAD)" | 
|  | echo "  Cherry-picking $i" | 
|  | git cherry-pick $i >/dev/null || { | 
|  | echo "Failed to cherry-pick $i. please resolve the conflict and exit." >&2 | 
|  | echo "  Use 'git cherry-pick --abort; exit' to abort the cherry-picks." >&2 | 
|  | echo "  Use 'git cherry-pick --continue; exit' to resolve the conflict." >&2 | 
|  | bash | 
|  | if [ "$(git rev-parse HEAD)" == "${previous_head}" ]; then | 
|  | echo "Cherry-pick aborted, aborting the whole command..." >&2 | 
|  | return 1 | 
|  | fi | 
|  | } | 
|  | done | 
|  | return 0 | 
|  | } | 
|  |  | 
|  | # Execute the create command: | 
|  | #   Create a new release named "$1" with "$2" as the baseline commit. | 
|  | function create_release() { | 
|  | local release_name="$1" | 
|  | local baseline="$2" | 
|  | shift 2 | 
|  | local origin_branch=$(git_get_branch) | 
|  | local branch_name="release-${release_name}" | 
|  | local release_title="Release ${release_name} ($(date +%Y-%m-%d))" | 
|  | local tmpfile=$(mktemp ${TMPDIR:-/tmp}/relnotes-XXXXXXXX) | 
|  | local tmpfile2=$(mktemp ${TMPDIR:-/tmp}/relnotes-XXXXXXXX) | 
|  | trap 'rm -f ${tmpfile} ${tmpfile2}' EXIT | 
|  |  | 
|  | # Get the rc number (1 by default) | 
|  | local rc=1 | 
|  | if [ -n "$(git branch --list --column ${branch_name})" ]; then | 
|  | rc=$(($(get_release_candidate "${branch_name}")+1)) | 
|  | fi | 
|  |  | 
|  | # Save the changelog so we compute the relnotes against HEAD. | 
|  | git show master:CHANGELOG.md >${tmpfile2} 2>/dev/null || echo >${tmpfile2} | 
|  |  | 
|  | echo "Creating new release branch ${branch_name} for release ${release_name}" | 
|  | git checkout -B ${branch_name} ${baseline} | 
|  |  | 
|  | apply_cherry_picks $@ || { | 
|  | git checkout ${origin_branch} | 
|  | git branch -D ${branch_name} | 
|  | exit 1 | 
|  | } | 
|  |  | 
|  | echo "Creating release notes" | 
|  | echo "${RELEASE_NOTE_MESSAGE}" > ${tmpfile} | 
|  | echo "# ${release_title}" >> ${tmpfile} | 
|  | echo >> ${tmpfile} | 
|  | create_release_notes "${tmpfile2}" >> ${tmpfile} | 
|  | release_note_editor ${tmpfile} "${origin_branch}" "${branch_name}" | 
|  | local relnotes="$(cat ${tmpfile})" | 
|  |  | 
|  | echo "Creating the release commit" | 
|  | create_release_commit "${release_title}" "${release_name}" \ | 
|  | "${relnotes}" "${tmpfile}" "${baseline}" $@ | 
|  | set_release_name "${release_name}" "${rc}" | 
|  |  | 
|  | rm -f ${tmpfile} ${tmpfile2} | 
|  | trap - EXIT | 
|  | } | 
|  |  | 
|  | # Push the release branch to the release repositories so a release | 
|  | # candidate can be created. | 
|  | function push_release_candidate() { | 
|  | local branch="$(get_release_branch)" | 
|  | for repo in ${RELEASE_REPOSITORIES}; do | 
|  | git push -f ${repo} +${branch} | 
|  | git push -f ${repo} +refs/notes/release | 
|  | git push -f ${repo} +refs/notes/release-candidate | 
|  | done | 
|  | } | 
|  |  | 
|  | # Deletes the release branch after a release or abandoning the release | 
|  | function cleanup_branches() { | 
|  | local tag_name=$1 | 
|  | local i | 
|  | echo "Destroying the release branches for release ${tag_name}" | 
|  | # Destroy branch, ignoring if it doesn't exists. | 
|  | git branch -D release-${tag_name} &>/dev/null || true | 
|  | for i in $RELEASE_REPOSITORIES; do | 
|  | git push -f $i :release-${tag_name} &>/dev/null || true | 
|  | done | 
|  | } | 
|  |  | 
|  | # Releases the current release branch, creating the necessary tag, | 
|  | # destroying the release branch, updating the master's CHANGELOG.md | 
|  | # and pushing everything to GitHub. | 
|  | function do_release() { | 
|  | local branch=$(get_release_branch) | 
|  | local tag_name=$(get_release_name) | 
|  |  | 
|  | echo -n "You are about to release branch ${branch} in tag ${tag_name}, confirm? [y/N] " | 
|  | read answer | 
|  | if [ "$answer" = "y" -o "$answer" = "Y" ]; then | 
|  | # Remove release "candidate" | 
|  | set_release_name "${tag_name}" | 
|  | echo "Creating the tag" | 
|  | git tag ${tag_name} | 
|  |  | 
|  | echo "Cherry-picking CHANGELOG.md modification into master" | 
|  | git checkout master | 
|  | # Ensuring we are up to date for master | 
|  | git pull --rebase $(echo "$MASTER_REPOSITORIES" | cut -d " " -f 1) master | 
|  | # We do not cherry-pick because we might have conflict if the baseline | 
|  | # does not contains the latest CHANGELOG.md file, so trick it. | 
|  | local changelog_path="$PWD/CHANGELOG.md" | 
|  | git show ${branch}:CHANGELOG.md >${changelog_path} | 
|  | local tmpfile=$(mktemp ${TMPDIR:-/tmp}/relnotes-XXXXXXXX) | 
|  | trap 'rm -f ${tmpfile}' EXIT | 
|  | git_commit_msg ${branch} >${tmpfile} | 
|  | git add ${changelog_path} | 
|  | git commit --no-verify -F ${tmpfile} --no-edit --author "${RELEASE_AUTHOR}" | 
|  | rm -f ${tmpfile} | 
|  | trap - EXIT | 
|  |  | 
|  | echo "Pushing the change to remote repositories" | 
|  | for i in $MASTER_REPOSITORIES; do | 
|  | git push $i +master | 
|  | done | 
|  | for i in $RELEASE_REPOSITORIES; do | 
|  | git push $i +refs/tags/${tag_name} | 
|  | git push $i +refs/notes/release-candidate | 
|  | git push $i +refs/notes/release | 
|  | done | 
|  | cleanup_branches ${tag_name} | 
|  | fi | 
|  | } | 
|  |  | 
|  | # Abandon the current release, deleting the branch on the local | 
|  | # repository and on GitHub, discarding all changes | 
|  | function abandon_release() { | 
|  | local branch_info=$(get_release_branch) | 
|  | local tag_name=$(get_release_name) | 
|  | echo -n "You are about to abandon release ${tag_name}, confirm? [y/N] " | 
|  | read answer | 
|  | if [ "$answer" = "y" -o "$answer" = "Y" ]; then | 
|  | git checkout -q master >/dev/null | 
|  | cleanup_branches ${tag_name} | 
|  | fi | 
|  | } | 
|  |  | 
|  | function usage() { | 
|  | cat >&2 <<EOF | 
|  | Usage: $1 command [arguments] | 
|  | Available commands are: | 
|  | - create RELEASE_NAME BASELINE [COMMIT1 ... COMMITN]: creates a new | 
|  | release branch for release named RELEASE_NAME, cutting it at | 
|  | the commit BASELINE and cherry-picking COMMIT1 ... COMMITN. | 
|  | - push: push the current release branch to release repositories. | 
|  | - release: do the actual release of the current release branch. | 
|  | - abandon: abandon the current release branch. | 
|  |  | 
|  | The typical workflow for the release manager is: | 
|  | 1. Create the release branch using the decided name for the release | 
|  | (usually a version number). The BASELINE is generally a baseline | 
|  | that has been tested extensively including inside Google. | 
|  | 2. Push to the repository and wait for the continuous integration | 
|  | to rebuild and deploy the various artifacts and send the annoucement | 
|  | mails about a new release candidate being available. | 
|  | 3. If necessary, creates a new release branch with the same name to | 
|  | address return from the users and go back to 2. | 
|  | 4. Either abandon or release the branch depending on if too much | 
|  | problems are reported or if the release is considered viable. The | 
|  | CI system should then rebuild from the tag and deploy the artifact | 
|  | to GitHub and sends the announcement mails about the new release. | 
|  | EOF | 
|  | exit 1 | 
|  | } | 
|  |  | 
|  | git diff-index --quiet HEAD -- || { | 
|  | echo "There are pending changes in this git repository." >&2 | 
|  | echo "Please commit or stash them before using that script." >&2 | 
|  | exit 1 | 
|  | } | 
|  |  | 
|  | [ "$(git rev-parse --show-toplevel)" == "$PWD" ] || { | 
|  | echo "You should run this script from the root of the git repository." >&2 | 
|  | exit 1 | 
|  | } | 
|  |  | 
|  | progname=$0 | 
|  | cmd=${1-} | 
|  | shift || usage $progname | 
|  |  | 
|  | case $cmd in | 
|  | create) | 
|  | (( $# >= 2 )) || usage $progname | 
|  | create_release "$@" | 
|  | ;; | 
|  | push) | 
|  | push_release_candidate | 
|  | ;; | 
|  | release) | 
|  | do_release | 
|  | ;; | 
|  | abandon) | 
|  | abandon_release | 
|  | ;; | 
|  | *) | 
|  | usage $progname | 
|  | ;; | 
|  | esac |