#!/bin/bash # # rtm -- Release to Master # Implements the release process of Vincent Driessen's branching model: # http://nvie.com/posts/a-successful-git-branching-model/ # # Since we use the model at Bol.com for puppet modules, this script # will also check for metadata.json and Modulefile but it only does # something with it if it actually finds it. # # If xclip is installed then the version number will be sent to the # clipboard. This is handy for if you want to add it to the annotated # tag during the gitflow release process. # # Author: Steven Meunier # Version: 0.2.0 set -e script_abort() { echo echo "Oops, it looks like something went wrong that I can't recover from." echo "You now have two options:" echo " 1) Fix the problem and continue the release process manually." echo " 2) Rollback any part of the release already performed, fix what" echo " caused the issue and try running the script again." echo "Whatever you decide, please don't blindly re-run this script as the results" echo "could be unexpected." exit 1 } trap script_abort ERR commit_counts () { local SOURCE=${1:-"@{upstream}"} local TARGET=${2:-"HEAD"} echo $(git rev-list --no-merges --count --left-right $SOURCE...$TARGET 2> /dev/null) } commits_behind () { local SOURCE=${1:-"@{upstream}"} local TARGET=${2:-"HEAD"} local counts=( $(commit_counts $SOURCE $TARGET) ) echo ${counts[0]} } commits_ahead () { local SOURCE=${1:-"@{upstream}"} local TARGET=${2:-"HEAD"} local counts=( $(commit_counts $SOURCE $TARGET) ) echo ${counts[1]} } get_current_branch() { echo -n $(git describe --contains --all HEAD) } get_git_release () { local BRANCH=${1:-"master"} echo -n $(git describe $BRANCH 2> /dev/null | cut -d'-' -f 1) } get_metadata_release () { if [[ -f "metadata.json" ]] then echo -n $(grep -E "\"version\"" metadata.json | sed -E "s/\"version\":\s*[\"\'](.*)[\"\']\,$/\1/") elif [[ -f "Modulefile" ]] then echo -n $(grep -E "^version" Modulefile | sed -E "s/^version '(.*)'$/\1/") fi } get_git_dir () { echo $(git rev-parse --git-dir 2> /dev/null) } get_remote_name () { local BRANCH=${1:-"master"} echo $(git config --local --get "branch.${BRANCH}.remote" 2> /dev/null) } git_push () { local BRANCH=$1 local THROUGH_GERRIT=${2:-"true"} git checkout -q $BRANCH if [[ "${THROUGH_GERRIT}" =~ ^[tT]{1}([rR]{1}[uU]{1}[eE]{1})*$ ]] then local GERRIT_PREFIX="HEAD:refs/for" else local GERRIT_PREFIX="HEAD:refs/heads" fi git push $(get_remote_name ${BRANCH}) ${GERRIT_PREFIX}/${BRANCH} } git_push_tag () { local TAG=${1:-${VERSION}} git push $(get_remote_name ${BRANCH}) ${TAG} } increment_version () { # Taken from http://stackoverflow.com/questions/8653126/how-to-increment-version-number-in-a-shell-script local VERSION=${1:-$(get_git_release)} echo $VERSION | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}' } is_inside_work_tree () { [ $(git rev-parse --is-inside-work-tree 2> /dev/null) == "true" ] } is_gitflow_installed () { local GITFLOW=$(git flow version > /dev/null 2>&1) if [[ "$?" -ne "0" ]]; then false else true fi } show_diff () { local SOURCE_BRANCH=${1:-"master"} local TARGET_BRANCH=${2:-$(get_remote_name ${SOURCE_BRANCH})/${SOURCE_BRANCH}} git diff ${TARGET_BRANCH} ${SOURCE_BRANCH} } show_commits_to_be_pushed () { local SOURCE_BRANCH=${1:-"master"} local TARGET_BRANCH=${2:-$(get_remote_name ${SOURCE_BRANCH})/${SOURCE_BRANCH}} local FORMAT=${3:-"format:%Cgreen%h%Creset %<(70)%s %<(25)%Cblue%an%Creset (%ai)"} echo "The following commits will be pushed to ${TARGET_BRANCH}:" echo -e "$(git log --pretty="${FORMAT}" ${TARGET_BRANCH}..${SOURCE_BRANCH})" } show_current_release () { local METADATA_RELEASE=$(get_metadata_release) local GIT_RELEASE=$(get_git_release) if [[ $GIT_RELEASE != $METADATA_RELEASE && "x${METADATA_RELEASE}" != "x" ]] then local VERSION_SUFFIX=" (metadata.json/Modulefile has ${METADATA_RELEASE})" else local VERSION_SUFFIX="" fi echo $GIT_RELEASE $VERSION_SUFFIX } yes_no_prompt () { local PROMPT=${1:-"Are you sure? [y/N] "} local CMD_WHEN_YES=${2:-"true"} local CMD_WHEN_NO=${3:-"false"} read -p "$PROMPT" RESPONSE if [[ $RESPONSE =~ ^[yY]([eE]{1}[sS]{1})*$ ]] then $CMD_WHEN_YES elif [[ $RESPONSE =~ ^[nN][oO]*$ ]] then $CMD_WHEN_NO else echo "Please answer 'yes' or 'no'" yes_no_prompt "$PROMPT" "$CMD_WHEN_YES" "$CMD_WHEN_NO" fi } convert_modulefile () { PUPPETBIN=$(which puppet 2> /dev/null) : ${EDITOR:=$(which vi 2> /dev/null)} if [[ -z "${PUPPETBIN}" ]] then echo "Puppet is required to convert the Modulefile. Skipping conversion." else "${PUPPETBIN}" module build if [[ $? -eq 0 ]] then cp pkg/*/metadata.json ./ yes_no_prompt "Edit metadata.json before commit? [y/N] " "${EDITOR} metadata.json" true if $(grep -q metadata.json .gitignore) then sed -i '/metadata.json/d' .gitignore git add .gitignore fi git add metadata.json git rm -f Modulefile rm -rf pkg git commit -m "Convert Modulefile to metadata.json" else echo "Converting Modulefile to metadata.json failed." fi fi } update_metadata () { local VERSION=$1 local UPDATE_FLAG=0 if [[ -f "metadata.json" && "x${VERSION}" != "x" && $(get_metadata_release) != "${VERSION}" ]] then sed -i -e "s/\"version\":.*/\"version\": \"${VERSION}\"\,/" metadata.json git add metadata.json UPDATE_FLAG=1 fi if [[ $UPDATE_FLAG != 0 ]] then git commit -m "Bumped version" fi } send_version_to_clipboard () { local VERSION=${1} local XCLIP_BIN=$(which xclip 2> /dev/null) if [[ "x$XCLIP_BIN" != "x" ]] then echo -n $VERSION | ${XCLIP_BIN} -in fi } prompt_release_version () { local INPUT_VERSION local INCREMENT_VERSION=$(increment_version) echo "Current release is:" $(show_current_release) echo "Default version to release: $INCREMENT_VERSION" read -e -p "Version to Release: " INPUT_VERSION if [ "x$INPUT_VERSION" != "x" ] then VERSION=$INPUT_VERSION else VERSION=$INCREMENT_VERSION fi } ensure_change_id () { # Gerrit is likely to be configured to require a change-id but # merge commits with gitflow don't add it so we need to do that # ourselves. We do that by amending the merge commit. If the # change-id hook is installed then a change-id will be added. If # not, then nothing will happen and presumably Gerrit is then not # configured to require it either. local LOG=$(git log -1) local VERSION=${1:-${VERSION}} if $(echo ${LOG} | grep -q "Merge:") && [[ $(echo ${LOG} | grep -q "Change-Id:") != 0 ]] then COMMIT=$(echo ${LOG} | head -1 | awk '{print $2}') echo "Change-Id is missing. Adding it..." git commit --amend -C ${COMMIT} # This will mess up our annotated tag so we need to move it to # our amended commit if [[ $(get_current_branch) == "master" ]] then TAG_MSG=$(git show --pretty="format:%b" ${VERSION} | tail -n +3) git tag -f -a ${VERSION} -m "${TAG_MSG}" fi fi } ensure_git_flow_enabled () { if ! [[ $(grep gitflow $(get_git_dir)/config) ]] then echo "Enabling gitflow" git flow init -d > /dev/null fi } gitflow_release () { local VERSION=${1} # gitflow creates an annotated tag so the user will be prompted to # enter one. There is no way around that unless we perform the # steps ourselves rather than use gitflow. git flow release start $VERSION # While we could have converted the Modulefile while checking for # the version, we wait until starting the release for so that we # can include the commit for it as part of the release. if [[ -f "Modulefile" ]] && [[ ! -f "metadata.json" ]] then echo "Modulefile is deprecated. Will attempt to convert it to metadata.json" convert_modulefile fi update_metadata $VERSION git flow release finish $VERSION } push_changes () { local BRANCH=${1:-"master"} if [[ $(commits_ahead $(get_remote_name ${BRANCH})/${BRANCH} ${BRANCH}) -ne 0 ]] then yes_no_prompt "Push branch '${BRANCH}' to '$(get_remote_name ${BRANCH})' [y/N] " "git_push ${BRANCH}" true else echo "No changes to push for branch '${BRANCH}'" fi } verify_changes () { local BRANCH=${1:-"master"} if [[ $(commits_ahead $(get_remote_name ${BRANCH})/${BRANCH} ${BRANCH}) -ne 0 ]] then yes_no_prompt "Show commits to be pushed for '${BRANCH}'? [y/N] " "show_commits_to_be_pushed ${BRANCH}" true yes_no_prompt "Show diff for '${BRANCH}'? [y/N] " "show_diff ${BRANCH}" true else echo "Local and remote branch '${BRANCH}' are the same" fi } ### Preparation GIT_DIR=${1:-$(pwd)} cd $GIT_DIR if ! $(is_inside_work_tree) then echo "Error: ${GIT_DIR} is not a git repository" exit 1 fi for BRANCH in "develop" "master" do if [[ $(commits_ahead $(get_remote_name ${BRANCH})/${BRANCH} ${BRANCH}) -ne 0 ]] then echo "Error: Your local branch '${BRANCH}' is ahead of the remote branch" echo "It's not possible to perform a release." echo "I recommend creating a new branch, resetting to the remote branch and trying again:" echo " git checkout -b ${BRANCH}_ahead" echo " git checkout ${BRANCH}" echo " git reset --hard $(get_remote_name ${BRANCH})/${BRANCH}" exit 1 fi done if ! $(is_gitflow_installed) then echo "Error: Gitflow is required. It can be obtained from:" echo "https://github.com/nvie/gitflow" exit 1 fi ensure_git_flow_enabled git fetch > /dev/null for BRANCH in "develop" "master" do echo "Preparing ${BRANCH} for release:" git checkout -q ${BRANCH} # It's more efficient to only pull the branch if there are changes # but it doesn't hurt to explicitly pull either...just to be sure # we don't have any issues when pushing to Gerrit git pull --rebase echo done if [[ $(commits_ahead master develop) -eq 0 ]] then echo "Nothing to merge to master for ${GIT_DIR}" echo "Current version is $(show_current_release)" send_version_to_clipboard $(get_git_release) exit 2 fi ### End Preparation ### Before release to master # Exploit BASH's global variables so that the VERSION that is obtained # in the function is available outside of the function. Hence, no # explicit assignment to a variable here. If we did assign to a # variable, we wouldn't be able to echo the current version. prompt_release_version send_version_to_clipboard ${VERSION} gitflow_release ${VERSION} ### Give user a chance to see what will change before making any ### changes on the remote server for BRANCH in "develop" "master" do git checkout -q ${BRANCH} ensure_change_id verify_changes ${BRANCH} done ### Now we're ready to push the changes for BRANCH in "develop" "master" do push_changes ${BRANCH} done yes_no_prompt "Push tag '${VERSION}' to '$(get_remote_name master)' [y/N] " "git_push_tag ${VERSION}" true exit 0