| #!/bin/bash | 
 |  | 
 | # 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. | 
 |  | 
 | set -eu | 
 |  | 
 | # Generate the release branches and handle the release tags. | 
 |  | 
 | # Name of the default editor. | 
 | : ${EDITOR=vi} | 
 |  | 
 | # Repositories to push the release branch and the release tag. | 
 | RELEASE_REPOSITORY="git@github.com:bazelbuild/bazel" | 
 |  | 
 | # Repositories to push the master branch. | 
 | MASTER_REPOSITORY="https://bazel.googlesource.com/bazel" | 
 |  | 
 | # 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. | 
 | ' | 
 |  | 
 | # Merge three release notes using branch $1 as a base. | 
 | # Args: | 
 | #   $1 the branch name to use to play on | 
 | #   $2 the new generated release notes | 
 | #   $3 the last generated release notes | 
 | #   $4 the last edited release notes | 
 | function __merge_release_notes() { | 
 |   local branch_name="$1" | 
 |   local relnotes="$2" | 
 |   local last_relnotes="$3" | 
 |   local last_savedrelnotes="$4" | 
 |   if [ "${last_relnotes}" == "${last_savedrelnotes}" ]; then | 
 |     echo "${relnotes}" | 
 |   else | 
 |     # Merge the three release notes using "git merge". | 
 |     git checkout -q -b "${branch_name}-merge-notes-1" | 
 |     echo "${last_relnotes}" >.relnotes | 
 |     git add .relnotes | 
 |     git commit -q -m "last_relnotes" --allow-empty | 
 |  | 
 |     echo "${last_savedrelnotes}" >.relnotes | 
 |     git add .relnotes | 
 |     git commit -q -m "last_savedrelnotes" --allow-empty | 
 |     git checkout -q -b "${branch_name}-merge-notes-2" HEAD~ | 
 |  | 
 |     echo "${relnotes}" >.relnotes | 
 |     git add .relnotes | 
 |     git commit -q -m "relnotes" --allow-empty | 
 |  | 
 |     git merge -q --no-commit "${branch_name}-merge-notes-1" &>/dev/null || true | 
 |     cat .relnotes | 
 |  | 
 |     # Clean-up | 
 |     git merge --abort || true &>/dev/null | 
 |     git checkout -q "${branch_name}" | 
 |     git branch -D ${branch_name}-merge-notes-{1,2} >/dev/null | 
 |   fi | 
 | } | 
 |  | 
 | # Create the release commit by changing the CHANGELOG file | 
 | function __create_release_commit() { | 
 |   local infos=$(generate_release_message "${1}" HEAD '```') | 
 |   local changelog_path="$PWD/CHANGELOG.md" | 
 |  | 
 |   # Get the CHANGELOG.md from master to avoid missing release notes from release | 
 |   # that were in-between. | 
 |   git checkout master CHANGELOG.md | 
 |  | 
 |   # CHANGELOG.md | 
 |   local tmpfile="$(mktemp --tmpdir relnotes-XXXXXXXX)" | 
 |   { | 
 |     echo -n "## ${infos}" | 
 |     echo | 
 |     echo | 
 |     cat "${changelog_path}" | 
 |   } >> ${tmpfile} | 
 |   mv "${tmpfile}" "${changelog_path}" | 
 |   git add "${changelog_path}" | 
 |  | 
 |   # Commit | 
 |   infos="$(echo "${infos}" | grep -Ev '^```$')" | 
 |   git commit --no-verify -m "${infos}" --no-edit --author "${RELEASE_AUTHOR}" | 
 | } | 
 |  | 
 | function __apply_cherry_picks() { | 
 |   echo "Applying cherry-picks" | 
 |   # Apply cherry-picks | 
 |   for commit in "$@"; do | 
 |     local previous_head="$(git rev-parse HEAD)" | 
 |     echo "  Cherry-picking ${commit}" | 
 |     git cherry-pick ${commit} >/dev/null || { | 
 |       echo "Failed to cherry-pick ${commit}. 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 force_rc=1 | 
 |   if [[ "$1" =~ ^--force_rc=([0-9]*)$ ]]; then | 
 |     force_rc=${BASH_REMATCH[1]} | 
 |     shift 1 | 
 |   fi | 
 |   local release_name="$1" | 
 |   local baseline="$2" | 
 |   shift 2 | 
 |   local branch_name="release-${release_name}rc${force_rc}" | 
 |  | 
 |   # Fetch everything from remote repositories to avoid conflicts | 
 |   git fetch -f "${RELEASE_REPOSITORY}" | 
 |  | 
 |   echo "Creating new release branch ${branch_name} for release ${release_name}" | 
 |   git checkout -B ${branch_name} ${baseline} | 
 |  | 
 |   __apply_cherry_picks $@ || { | 
 |     git checkout master | 
 |     git branch -D ${branch_name} | 
 |     exit 1 | 
 |   } | 
 |  | 
 |   echo "Created $(get_full_release_name) on branch ${branch_name}." | 
 | } | 
 |  | 
 | # Force push a ref $2 to repo $1 if exists | 
 | function __push_if_exists() { | 
 |   if git show-ref -q "${2}"; then | 
 |     git push -f "${1}" "+${2}" | 
 |   fi | 
 | } | 
 |  | 
 | # Push a given ref | 
 | function __push_ref() { | 
 |   local ref="$1" | 
 |   __push_if_exists "${RELEASE_REPOSITORY}" "${ref}" | 
 | } | 
 |  | 
 | # Push the release branch to the release repositories so a release | 
 | # candidate can be created. | 
 | function __push_release_candidate() { | 
 |   __push_ref "$(get_release_branch)" | 
 | } | 
 |  | 
 | # Deletes the release branch after a release or abandoning the release | 
 | function __cleanup_branches() { | 
 |   local tag_name=$1 | 
 |   echo "Destroying the release branches for release ${tag_name}" | 
 |   for branch in $(git branch | grep -Po "release-${tag_name}rc([0-9])*") | 
 |   do | 
 |     echo "Deleting ${branch}" | 
 |     git branch -D "${branch}" &>/dev/null || true | 
 |     git push -f "${RELEASE_REPOSITORY}" ":${branch}" &>/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) | 
 |   local candidate=$(get_release_candidate) | 
 |  | 
 |   echo -n "You are about to release branch ${branch} in tag ${tag_name}, confirm? [y/N] " | 
 |   read answer | 
 |   if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then | 
 |     echo "Creating the release commit" | 
 |     __create_release_commit "${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 "$MASTER_REPOSITORY" 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 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" | 
 |     git push "${MASTER_REPOSITORY}" +master | 
 |     __push_ref "refs/tags/${tag_name}" | 
 |     __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) | 
 |   local candidate=$(get_release_candidate) | 
 |   echo -n "You are about to abandon release ${tag_name}, confirm? [y/N] " | 
 |   read answer | 
 |   if [ "$answer" = "y" ] || [ "$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 [--force_rc=RC] 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. The release candidate number will be | 
 |       computed from existing release branch unless --force_rc is | 
 |       specified. | 
 |   - 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 |