#!/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 " # 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 } # Set the release name to $1 (and eventually the candidate number to $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" if [[ ! -z "${2-}" ]]; then git notes --ref=release-candidate append -m "$2" fi } # Trim empty lines at the beginning and the end of the buffer. function __trim_empty_lines() { # 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 branch_name="${2-}" $EDITOR ${tmpfile} || { echo "Editor failed, cancelling release creation..." >&2 return 1 } # Strip 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 return 1 fi echo "${relnotes}" > "${tmpfile}" } # 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}" echo } >> ${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 } # Add the origin of the cherry-pick in case the patch-id diverge and we cannot # find the original commit. git notes --ref=cherrypick add -f -m "${commit}" done return 0 } # Find out the last release since the fork between HEAD from master. function __find_last_release() { local baseline="${1:-$(get_release_baseline "HEAD")}" local changes="$(git log --pretty=format:%H "${baseline}~".."${branch}")" for change in ${changes}; do if git notes --ref=release show ${change} &>/dev/null; then echo ${change} return 0 fi done } # Execute the create command: # Create a new release named "$1" with "$2" as the baseline commit. function __create_release() { local force_rc= 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}" # Fetch everything from remote repositories to avoid conflicts git fetch -f "${RELEASE_REPOSITORY}" git fetch -f "${RELEASE_REPOSITORY}" 'refs/notes/*:refs/notes/*' local last_release="$(git rev-parse --verify "${branch_name}" 2>/dev/null || true)" 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 } __setup_git_notes "${force_rc}" "${release_name}" "${last_release}" || { git checkout master git branch -D ${branch_name} exit 1 } echo "Created $(get_full_release_name) on branch ${branch_name}." } # Setup the git notes for a release. # It looks in the history since the merge base with master to find # the latest release candidate and create the git notes for: # the release name (ref=release) # the release candidate number (ref=release-candidate) # the release notes (ref=release-notes) # Args: # $1: set to a number to force the release candidate number (default # is taken by increasing the previous release candidate number). # $2: (optional) Specify the release name (default is taken by the # last release candidate or the branch name, e.g. if the branch # name is release-v1, then the name will be 'v1'). # $3: (optional) Specify the commit for last release. function __setup_git_notes() { local force_rc="$1" local branch_name="$(git_get_branch)" # Figure out where we are in release: find the rc, the baseline, cherrypicks # and release name. local rc=${force_rc:-1} local baseline="$(get_release_baseline "HEAD")" local cherrypicks="$(get_cherrypicks "HEAD" "${baseline}")" local last_release="${3-$(__find_last_release "${baseline}")}" local release_name="${2-}" if [ -n "${last_release}" ]; then if [ -z "${force_rc}" ]; then rc=$(($(get_release_candidate "${last_release}")+1)) fi if [ -z "${release_name}" ]; then release_name="$(get_release_name "${last_release}")" fi elif [ -z "${release_name}" ]; then if [[ "${branch_name}" =~ ^release-(.*)$ ]]; then release_name="${BASH_REMATCH[1]}" else echo "Cannot determine the release name." >&2 echo "Please create a release branch with the name release-." >&2 return 1 fi fi # Edit the release notes local tmpfile=$(mktemp --tmpdir relnotes-XXXXXXXX) trap "rm -f ${tmpfile}" EXIT echo "Creating release notes" # Save the changelog so we compute the relnotes against HEAD. git show master:CHANGELOG.md > "${tmpfile}" # Compute the new release notes local relnotes="$(create_release_notes "${tmpfile}" "${baseline}" ${cherrypicks})" # Try to merge the release notes if there was a previous release if [ -n "${last_release}" ]; then # Compute the previous release notes local last_baseline="$(get_release_baseline "${last_release}")" git checkout -q "${last_release}" local last_relnotes="$(create_release_notes "${tmpfile}")" git checkout -q "${branch_name}" local last_savedrelnotes="$(get_release_notes "${last_release}")" relnotes="$(__merge_release_notes "${branch_name}" "${relnotes}" \ "${last_relnotes}" "${last_savedrelnotes}")" fi echo "${RELEASE_NOTE_MESSAGE}" > "${tmpfile}" echo "# $(get_release_title "${release_name}rc${rc}")" >> "${tmpfile}" echo >> "${tmpfile}" echo "${relnotes}" >> "${tmpfile}" __release_note_editor "${tmpfile}" "${branch_name}" || return 1 relnotes="$(cat ${tmpfile})" # Add the git notes. git notes --ref=release add -f -m "${release_name}" git notes --ref=release-candidate add -f -m "${rc}" git notes --ref=release-notes add -f -m "${relnotes}" # Clean-up. rm -f "${tmpfile}" trap - EXIT } # 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 release notes refs but also a given ref function __push_notes_and_ref() { local ref="$1" __push_if_exists "${RELEASE_REPOSITORY}" "${ref}" __push_if_exists "${RELEASE_REPOSITORY}" "refs/notes/release" __push_if_exists "${RELEASE_REPOSITORY}" "refs/notes/release-candidate" __push_if_exists "${RELEASE_REPOSITORY}" "refs/notes/release-notes" __push_if_exists "${RELEASE_REPOSITORY}" "refs/notes/cherrypick" } # Push the release branch to the release repositories so a release # candidate can be created. function __push_release_candidate() { __push_notes_and_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}" git branch -D "release-${tag_name}" &>/dev/null || true git push -f "${RELEASE_REPOSITORY}" ":release-${tag_name}" &>/dev/null || true } # 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" ] || [ "$answer" = "Y" ]; then echo "Creating the release commit" __create_release_commit "${tag_name}" __set_release_name "${tag_name}" git notes --ref=release add -f -m "${tag_name}" git notes --ref=release-candidate remove || true 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_notes_and_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) echo -n "You are about to abandon release ${tag_name}, confirm? [y/N] " read answer if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then git notes --ref=release remove || true git notes --ref=release-candidate remove || true git checkout -q master >/dev/null __cleanup_branches ${tag_name} fi } function __usage() { cat >&2 <&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 ;; generate-rc) force_rc= if [[ "${1-}" =~ ^--force_rc=([0-9]*)$ ]]; then force_rc=${BASH_REMATCH[1]} shift 1 fi __setup_git_notes "${force_rc}" ;; abandon) __abandon_release ;; *) __usage $progname ;; esac