blob: 2deefe0262e3438f663177bc1a21f7c9b18dc07c [file] [log] [blame]
#!/bin/bash +e
#
# Copyright 2017-present The Material Motion and Material Components for
# iOS 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.
parentcmd=$(basename "${BASH_SOURCE[1]}")
cmd=$(basename "${BASH_SOURCE[0]}")
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
usage() {
"$dir/readme_to_console" "$dir/README-release.md"
}
# HELPER METHODS
current_branch() {
git rev-parse --abbrev-ref HEAD
}
enforce_clean_state() {
if [[ $(git status --porcelain) ]]; then
echo "${B}Your git repo is not in a clean state.${N}"
echo "Please revert or commit all changes before cutting a release."
git status
exit 1
fi
}
enforce_release_cut() {
if [ ! $(git rev-parse --verify release-candidate 2> /dev/null) ]; then
echo "No release in progress."
exit 1
fi
}
enforce_changelog_version() {
changelog_version=$(awk '/# (.+)/ { print $2; exit}' "$rootdir/CHANGELOG.md")
changelog_version=$(version_for_platform $changelog_version)
if [ "$changelog_version" == "#develop#" ]; then
echo "${B}You haven't updated CHANGELOG.md with the current version yet.${N}"
echo "Please run '$parentcmd $cmd bump [version]' before running this command again."
exit 1
elif [ "$version" != "$changelog_version" ]; then
echo "Mismatch in CHANGELOG.md's latest version."
echo
echo " CHANGELOG.md latest version: $changelog_version"
echo " Desired version: $version"
echo
echo "Please edit CHANGELOG.md or change your version number."
echo
exit 1
fi
}
get_latest_branch() {
if [ ! $(git rev-parse --verify release-candidate 2> /dev/null) ]; then
echo "HEAD"
else
echo "release-candidate"
fi
}
version_for_platform() {
user_version="$1"
if [ -f "$rootdir/Podfile" ]; then
echo "v${user_version#v}"
elif [ -f "$rootdir/build.gradle" ]; then
echo "${user_version#v}"
else
echo "v${user_version#v}"
fi
}
# COMMAND METHODS
# Creates a release-candidate branch based off the latest origin/develop
cut_release() {
isHotFix=false;
while test $# -gt 0; do
case "$1" in
--hotfix)
isHotFix=true;
shift
;;
esac
done
if [ $(git rev-parse --verify release-candidate 2> /dev/null) ]; then
echo "${B}Release already cut.${N}"
echo "Consider deleting your existing release-candidate branch."
exit 1
fi
git fetch
git show develop >> /dev/null 2>&1 || { git checkout -b develop origin/develop; }
deviance=$(git log develop..origin/develop --oneline | wc -l)
if [ $deviance -ne 0 ]; then
echo
echo " Your local develop branch is behind origin/develop."
echo " Refusing to continue until you've rebased off of origin/develop."
echo
echo " git checkout develop"
echo " git rebase origin/develop"
echo
exit 1
fi
deviance=$(git log origin/develop..develop --oneline | wc -l)
if [ $deviance -ne "0" ]; then
echo
echo " Your local develop branch is ahead of origin/develop."
echo " Refusing to continue until you've landed your local changes into origin/develop."
echo
exit 1 # TODO: Revert this line so we bail out.
fi
if $isHotFix; then
branch=origin/stable
branch_message="This is a hotfix... branching off $branch"
else
branch=origin/develop
branch_message="This is a normal release... branching off $branch"
fi
echo $branch_message
git checkout -b release-candidate $branch
touch "$rootdir/CHANGELOG.md"
if ! grep "# #develop#" "$rootdir/CHANGELOG.md" >> /dev/null; then
echo "Generating API diff..."
CHANGELOG_TMP_PATH=$(mktemp -d)
generate_release_apidiff | tee "$CHANGELOG_TMP_PATH/api_diff"
CHANGELOG_PATH=$(cat "$CHANGELOG_TMP_PATH/api_diff" | grep "Changelog=" | cut -d'=' -f2)
# Add the new changelog contents in reverse order:
echo -e "\n---\n" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
generate_release_notes | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
echo -e "\n## Component changes" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
cat "$CHANGELOG_PATH" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
echo -e "## API changes" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
echo -e "## New features\n" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
echo -e "## New deprecations\n" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
echo -e "## Breaking changes\n" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
echo -e "# #develop#\n" | cat - "$rootdir/CHANGELOG.md" > /tmp/out && mv /tmp/out "$rootdir/CHANGELOG.md"
fi
git add "$rootdir/CHANGELOG.md"
git commit -m "Automatic changelog preparation for release."
git push origin release-candidate -u
RELEASE_SHA=$(git merge-base --fork-point release-candidate $branch)
today=$(date +'%B%%20%d,%%20%Y')
echo
echo
echo "${B}If you have not already, please send the following email:$N"
echo
echo "> Consider setting up Gmail or Inbox as your default mailto handler:"
echo "> https://productforums.google.com/forum/#!topic/chrome/oxPLcXhbt9w"
echo
echo "Copy and paste the following into a browser to compose the email:"
echo
echo " mailto:material-components-ios-discuss%40googlegroups.com?subject=State%20of%20material-components-ios'%20$today%20release&body=I%20am%20about%20to%20cut%20the%20release%20for%20$today.%0A%0AThe%20release%20is%20being%20cut%20at%20$RELEASE_SHA.%0AView%20this%20SHA%20on%20GitHub%20at%20https%3A%2F%2Fgithub.com%2Fmaterial-components%2Fmaterial-components-ios%2Fcommit%2F$RELEASE_SHA%0A%0AWe%20encourage%20clients%20to%20test%20this%20release.%20To%20do%20so%2C%20check%20out%20the%0Arelease-candidate%20branch%20like%20so%3A%0A%0A%20%20%20%20git%20fetch%0A%20%20%20%20git%20checkout%20origin%2Frelease-candidate%0A"
echo
echo "Or compose the email by hand using the following template:"
echo
echo "To: material-components-ios-discuss@googlegroups.com"
echo "Subject: State of material-components-ios's $today release"
echo "Body:"
echo "I am about to cut the release for $today."
echo
echo "The release is being cut at $RELEASE_SHA."
echo "View this SHA on GitHub at https://github.com/material-components/material-components-ios/commit/$RELEASE_SHA"
echo
echo "We encourage clients to test this release. To do so, check out the"
echo "release-candidate branch like so:"
echo
echo " git fetch"
echo " git checkout origin/release-candidate"
echo
echo "<end of email body>"
echo
echo
PULL_REQUEST_URL="https://github.com/material-components/material-components-ios/compare/stable...release-candidate"
echo "${B}You can now start the release-candidate pull request:${N}"
echo
echo " $PULL_REQUEST_URL"
echo
echo "This will initiate public testing of the release candidate."
echo
echo "${B}You can now kick off internal testing.${N}"
}
abort_release() {
enforce_release_cut
echo "${B}About to abort the release candidate.${N}"
echo "${B}${U}This action is not easily reversible.${N}"
echo
echo -n "Press enter to continue..."
read
git checkout origin/develop
git branch -D release-candidate
git push origin :release-candidate
}
test_release() {
"$dir/prep_all"
"$dir/build_all" --verbose
"$dir/test_all"
}
bump_release() {
if [ -z "$1" ]; then
echo "Missing desired version."
echo
echo "Usage: $parentcmd $cmd bump <desired version> [<old version>]"
echo
exit 1
fi
enforce_release_cut
new_version="$1"
new_version=${new_version#v}
if [ -z "$2" ]; then
last_version=$(git describe --tags $(git rev-list --tags --max-count=1))
else
last_version="$2"
fi
last_version=${last_version#v}
grep -FIlr "$last_version" . 2>/dev/null | grep -v -f "$dir/versionignore" | while read line; do
sed -i.bak "s:$last_version:$new_version:g" "$line"
rm "$line.bak"
done
grep -FIlr "#develop#" . 2>/dev/null | grep -v -f "$dir/versionignore" | while read line; do
sed -i.bak "s:#develop#:$new_version:g" "$line"
rm "$line.bak"
done
}
merge_release() {
enforce_release_cut
version=$(version_for_platform $1)
if [ -z "$version" ]; then
echo "Must provide a ${U}version${N} argument."
exit 1
fi
current_branch=$(current_branch)
if [ "$current_branch" != "release-candidate" ]; then
echo "Checking out the release-candidate branch..."
git checkout release-candidate
fi
enforce_changelog_version
if [ $(git rev-list --tags --max-count=1 2> /dev/null) ]; then
last_version=$(git describe --tags $(git rev-list --tags --max-count=1))
last_version=${last_version#v}
last_version=$(echo "$last_version" | sed "s:\.:\\\\.:g")
if grep -Ilr "$last_version" . | grep -v -f "$dir/versionignore"; then
echo "Old version $last_version found in the files above."
read -r -p "Continue? [y/N] " response
case $response in
[yY][eE][sS]|[yY]) ;;
*)
echo "Aborting release merge. Please run $parentcmd $cmd bump"
exit 1
;;
esac
fi
fi
echo "Merging release-candidate into stable and develop..."
# Merge in to stable.
git fetch
if [ ! $(git rev-parse --verify stable 2> /dev/null) ]; then
git checkout -b stable origin/stable
else
git checkout stable
fi
git rebase origin/stable
git merge --no-ff release-candidate --no-edit
if [ ! $(git rev-parse --verify develop 2> /dev/null) ]; then
git checkout -b develop origin/develop
else
git checkout develop
fi
git rebase origin/develop
git merge --no-ff release-candidate --no-edit
git branch -D release-candidate
git checkout stable
}
publish_release() {
version=$(version_for_platform $1)
if [ -z "$version" ]; then
echo "Must provide a ${U}version${N} argument."
exit 1
fi
current_branch=$(current_branch)
if [ "$current_branch" != "stable" ]; then
echo "This command must be run from the ${B}stable${N} branch."
echo
echo "Your current branch: $current_branch"
exit 1
fi
enforce_changelog_version
ghtoken=$(cat ~/.gh.json | grep "github_token" | cut -d'"' -f4)
if [ -z "$ghtoken" ]; then
echo "Must be authenticated with gh."
echo
echo "Install gh by running:"
echo
echo " npm install -g gh"
echo
echo "And then get an auth token by running:"
echo
echo " gh user --whoami"
echo
exit 1
fi
curl -sH "Authorization: token $ghtoken" \
"https://api.github.com/repos/material-components/material-components-ios/releases/tags/$version" \
| grep -q 'message": "Not Found'
if [ $? -ne 0 ]; then # Found the release
echo "Release already cut."
echo
echo " Release URL: https://github.com/material-components/material-components-ios/releases/tag/$version"
open "https://github.com/material-components/material-components-ios/releases/tag/$version"
exit 0
fi
git push origin stable develop
tmp_path=$(mktemp -d)
curl -s \
-H "Authorization: token $ghtoken" \
-H "Content-Type: application/json" \
-X POST \
-d '{"tag_name":"'$version'","target_commitish":"stable","name":"'$version'","draft":true}' \
"https://api.github.com/repos/material-components/material-components-ios/releases" > "$tmp_path/release"
if [ $? -ne 0 ]; then # Found the release
echo "Failed to draft the release. Check that it doesn't already exist before continuing."
echo "https://api.github.com/repos/material-components/material-components-ios/releases"
exit 1
fi
htmlurl=$(cat "$tmp_path/release" | grep '^ "html_url' | cut -d'"' -f4)
htmlurl=$(echo $htmlurl | sed "s:/tag/:/edit/:")
echo "A draft release has been made."
echo
echo " Edit the draft: $htmlurl"
echo
echo "Update the release's description with the following:${B}"
awk '/# / { print $0; while(getline > 0) {if (/^# /) exit; print $0 }}' "$rootdir/CHANGELOG.md" | tail -n +2
echo ${N}
echo "Deleting remote release-candidate..."
git push origin :release-candidate
git checkout develop
echo "Press enter to open the release draft url in your browser:"
read
if [ "$(uname)" == "Darwin" ]; then
open $htmlurl
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
xdg-open $htmlurl
fi
}
publish_podspec() {
cd "$rootdir"
echo "Verifying Cocoapod trunk permissions"
pod trunk me > cocoapods_permissions.log
if grep -Fq "MaterialComponents" cocoapods_permissions.log; then
echo "Running a pod trunk push from stable..."
git checkout stable
pod trunk push MaterialComponents.podspec
rm cocoapods_permissions.log
else
echo "You do not have the right permissions to push the pod via the CocoaPods API. See cocoapods_permissions.log for more info."
fi
}
# Generators
generate_release_apidiff() {
CHANGELOG_TMP_PATH=$(mktemp -d)
validate_commit() {
git cat-file -t $1 >> /dev/null 2> /dev/null || { echo "$1 is not a valid commit."; exit 1; }
}
# Verify commits
if [ -n "$1" ]; then
old_commit=$(git rev-list -n 1 $1)
else
old_commit=$(git rev-list -n 1 origin/stable)
fi
new_commit=$(git rev-list -n 1 $(get_latest_branch))
if [[ -z "$old_commit" || -z "$new_commit" ]]; then
echo "Unable to get commit shas."
exit 1
fi
validate_commit $old_commit
validate_commit $new_commit
TMP_PATH=$(mktemp -d)
OLD_ROOT_PATH="$TMP_PATH/old"
NEW_ROOT_PATH="$TMP_PATH/new"
clean_clones() {
if [ ! -z "$OLD_ROOT_PATH" ]; then
rm -rf "$OLD_ROOT_PATH"
fi
if [ ! -z "$NEW_ROOT_PATH" ]; then
rm -rf "$NEW_ROOT_PATH"
fi
}
trap clean_clones EXIT
"$dir/temporary_clone_at_ref" "$OLD_ROOT_PATH" $old_commit
"$dir/temporary_clone_at_ref" "$NEW_ROOT_PATH" $new_commit
# Find command in all component src directories and grab search path for "Material$component.h"
old_header_search_paths=""
new_header_search_paths=""
for d in $NEW_ROOT_PATH/components/*/src; do
folder=$(dirname $d)
component=$(basename $folder)
old_header_search_paths="$old_header_search_paths --oldargs -I$OLD_ROOT_PATH/components/$component/src/ "
new_header_search_paths="$new_header_search_paths --newargs -I$NEW_ROOT_PATH/components/$component/src/ "
done
if [ ! -f "$dir/external/material-motion-apidiff/src/pathapidiff" ]; then
git submodule update --init --recursive
fi
ALL_CHANGELOG_PATH="$TMP_PATH/changelog"
ALL_ERROR_LOG_PATH="$TMP_PATH/errlog"
echo "Changelog=$ALL_CHANGELOG_PATH"
echo "Errors=$ALL_ERROR_LOG_PATH"
# Run new pathdiff script on each umbrella header in array
for path_to_component in $(generate_release_components "$old_commit" | grep -v "private"); do
component=$(basename $path_to_component)
echo -n "Diffing $component..."
if [ ! -d "$OLD_ROOT_PATH/components/$path_to_component/src" ]; then
echo >> $ALL_CHANGELOG_PATH
echo "### $component" >> $ALL_CHANGELOG_PATH
echo >> $ALL_CHANGELOG_PATH
echo "**New component.**" >> $ALL_CHANGELOG_PATH
echo "New!"
continue
fi
CHANGES_PATH="$TMP_PATH/${component}changes"
ERROR_PATH="$TMP_PATH/${component}errlog"
"$dir/external/material-motion-apidiff/src/pathapidiff" \
"$OLD_ROOT_PATH" "$NEW_ROOT_PATH" objc "/components/$component/src/Material$component.h" \
>> "$CHANGES_PATH" \
2>> "$ERROR_PATH"
if [ -s "$CHANGES_PATH" ]; then
echo >> $ALL_CHANGELOG_PATH
echo "### $component" >> $ALL_CHANGELOG_PATH
cat "$CHANGES_PATH" >> $ALL_CHANGELOG_PATH
echo -n " Changes detected."
fi
if [ -s "$ERROR_PATH" ]; then
echo "### $component" >> "$ALL_ERROR_LOG_PATH"
cat "$ERROR_PATH" >> "$ALL_ERROR_LOG_PATH"
fi
echo
done
}
generate_release_authors() {
git log origin/stable...$(get_latest_branch) --format="%ae" | sort | uniq
}
generate_release_components() {
if [ -n "$1" ]; then
old_commit="$1"
else
old_commit=origin/stable
fi
git diff --name-only "$old_commit"..$(get_latest_branch) components/ \
| grep "src/" | cut -d'/' -f2- | rev | cut -d'/' -f3- | rev | sed 's|/src||' | sort | uniq
}
generate_release_files() {
git diff --name-only origin/stable..$(get_latest_branch) components/
}
generate_release_headers() {
git diff --name-only origin/stable..$(get_latest_branch) components/ \
| grep -i -e "components\/.*\/src\/.*\.h" \
| grep -v -i -e "\/private\/"
}
generate_release_log() {
git --no-pager log origin/stable..$(get_latest_branch) "$@"
}
generate_release_diff() {
git_diff=diff
if [ "$1" == "--use_diff_tool" ]; then
git_diff=difftool
shift 1
fi
git $git_diff origin/stable..$(get_latest_branch) "$@"
}
generate_release_notes() {
find components -type d -name 'src' | while read path; do
folder=$(dirname $path)
component=$(echo $folder | cut -d'/' -f2-)
if [[ $component == private* ]]; then
continue;
fi
if [ $(git log --pretty=oneline --no-merges origin/stable..$(get_latest_branch) $folder \
| wc -l) == "0" ]; then
continue
fi
componentdiff() {
git log \
--pretty="* [%s](https://github.com/material-components/material-components-ios/commit/%H) (%an)" \
--no-merges \
origin/stable..$(get_latest_branch) \
$folder
}
if [[ $(componentdiff) ]]; then
echo
echo "### $component"
if [[ $(componentdiff | grep "\[$component\]\!") ]]; then
echo
echo "#### Breaking changes"
echo
componentdiff \
| grep "\[$component\]\!" \
| sed "s|\[$component\]!|**Breaking**: |" \
| sort
fi
if [[ $(componentdiff | grep -v "\[$component\]\!") ]]; then
echo
echo "#### Changes"
echo
componentdiff \
| grep -v "\[$component\]!" \
| sed "s|\[$component\] ||" \
| sort
fi
fi
done
}
generate_release_source() {
git diff --name-only origin/stable..$(get_latest_branch) components/ \
| grep "src/"
}
generate_release_stories() {
generate_release_log | grep -i pivotal
}
if [ $# -eq 0 ]; then
usage
exit 1
fi
if [ ! $(git rev-parse --is-inside-work-tree -q 2> /dev/null) ]; then
echo "Must be run from a git directory."
exit 1
fi
if [ -t 1 ] ; then # We're writing directly to terminal
readonly B=$(tput bold)
readonly U=$(tput smul)
readonly N=$(tput sgr0)
else # We're in a pipe
readonly B=""
readonly U=""
readonly N=""
fi
rootdir=$( cd "$(dirname $(git rev-parse --git-dir))" && pwd )
#enforce_clean_state
case "$1" in
cut) cut_release ${@:2} ;;
test) test_release ${@:2} ;;
bump) bump_release ${@:2} ;;
merge) merge_release ${@:2} ;;
publish) publish_release ${@:2} ;;
podspec) publish_podspec ${@:2} ;;
apidiff) generate_release_apidiff ${@:2} ;; # args: [base sha]
authors) generate_release_authors ${@:2} ;;
components) generate_release_components ${@:2} ;; # args: [base sha]
diff) generate_release_diff ${@:2} ;;
files) generate_release_files ${@:2} ;;
headers) generate_release_headers ${@:2} ;;
log) generate_release_log ${@:2} ;;
notes) generate_release_notes ${@:2} ;;
source) generate_release_source ${@:2} ;;
stories) generate_release_stories ${@:2} ;;
abort) abort_release ${@:2} ;;
*) usage ;;
esac