400 lines
11 KiB
Bash
Executable File
400 lines
11 KiB
Bash
Executable File
#!/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 <smeunier@bol.com>
|
|
# 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
|