| #!/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 |