From 4ee02b7b28d2493d92c67f711e351576f67eca11 Mon Sep 17 00:00:00 2001 From: Jake Nielsen Date: Tue, 4 Aug 2020 11:40:14 -0700 Subject: [PATCH] Adds an automatic version-rolling script --- .bumpversion.cfg | 6 + Pipfile | 3 +- Pipfile.lock | 10 +- tools/create_release.sh | 256 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 .bumpversion.cfg create mode 100755 tools/create_release.sh diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..f6159b8 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,6 @@ +[bumpversion] +current_version = 1.0.2 +commit = False +tag = False + +[bumpversion:file:setup.py] diff --git a/Pipfile b/Pipfile index 7617010..54ed293 100644 --- a/Pipfile +++ b/Pipfile @@ -4,8 +4,9 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -swagger-parser = {editable = true,git = "ssh://git@github.com/jakedialpad/swagger-parser.git",ref = "v1.0.1b"} +swagger-parser = {ref = "v1.0.1b",git = "ssh://git@github.com/jakedialpad/swagger-parser.git",editable = true} swagger-stub = "*" +bump2version = "*" [packages] requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index dbbff48..af068f5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c384c5f9c370eda0bc4df918026331c7088ad2b6692056af1210954b8359890d" + "sha256": "b7f79fb3d3f8503987fcfe4e908f37b4d2d4f5c78d58f157e010af615d5fe75a" }, "pipfile-spec": 6, "requires": { @@ -84,6 +84,14 @@ "markers": "python_version < '3.2'", "version": "==1.6.1" }, + "bump2version": { + "hashes": [ + "sha256:524bde030318fe2543038defe0f77739605636fef96924883813cb290cf79c1e", + "sha256:bfcc051498dda9fd9ac8634689f4516e1c20fdeeace3278932cc6e1248418b36" + ], + "index": "pypi", + "version": "==0.5.11" + }, "certifi": { "hashes": [ "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", diff --git a/tools/create_release.sh b/tools/create_release.sh new file mode 100755 index 0000000..76f04c2 --- /dev/null +++ b/tools/create_release.sh @@ -0,0 +1,256 @@ +#!/bin/bash + +DOC="Build and release a new version of the python-dialpad package. + +Usage: + create_version.sh [-h] [--patch|--minor|--major] [--bump-only|--no-upload] + +By default, this script will: +1 - Run the tests +2 - Check that we're on master +3 - Bump the patch-number in setup.py +4 - Check whether there are any remote changes that haven't been pulled +5 - Build the distribution package +6 - Verify the package integrity +7 - Commit the patch-number bump +8 - Tag the release +9 - Upload the package to PyPI +10 - Push the commit and tag to github + +If anything fails along the way, the script will bail out. + +Options: + -h --help Show this message + --patch Bump the patch version number (default) + --minor Bump the minor version number + --major Bump the major version number + --bump-only Make the appropriate version bump, but don't do anything else + (i.e. stop after performing step 3) + --no-upload Do everything other than uploading the package to PyPI + (i.e. skip step 9) +" +# docopt parser below, refresh this parser with `docopt.sh create_release.sh` +# shellcheck disable=2016,1075 +docopt() { parse() { if ${DOCOPT_DOC_CHECK:-true}; then local doc_hash +doc_hash=$(printf "%s" "$DOC" | shasum -a 256) +if [[ ${doc_hash:0:5} != "$digest" ]]; then +stderr "The current usage doc (${doc_hash:0:5}) does not match \ +what the parser was generated with (${digest}) +Run \`docopt.sh\` to refresh the parser."; _return 70; fi; fi; local root_idx=$1 +shift; argv=("$@"); parsed_params=(); parsed_values=(); left=(); testdepth=0 +local arg; while [[ ${#argv[@]} -gt 0 ]]; do if [[ ${argv[0]} = "--" ]]; then +for arg in "${argv[@]}"; do parsed_params+=('a'); parsed_values+=("$arg"); done +break; elif [[ ${argv[0]} = --* ]]; then parse_long +elif [[ ${argv[0]} = -* && ${argv[0]} != "-" ]]; then parse_shorts +elif ${DOCOPT_OPTIONS_FIRST:-false}; then for arg in "${argv[@]}"; do +parsed_params+=('a'); parsed_values+=("$arg"); done; break; else +parsed_params+=('a'); parsed_values+=("${argv[0]}"); argv=("${argv[@]:1}"); fi +done; local idx; if ${DOCOPT_ADD_HELP:-true}; then +for idx in "${parsed_params[@]}"; do [[ $idx = 'a' ]] && continue +if [[ ${shorts[$idx]} = "-h" || ${longs[$idx]} = "--help" ]]; then +stdout "$trimmed_doc"; _return 0; fi; done; fi +if [[ ${DOCOPT_PROGRAM_VERSION:-false} != 'false' ]]; then +for idx in "${parsed_params[@]}"; do [[ $idx = 'a' ]] && continue +if [[ ${longs[$idx]} = "--version" ]]; then stdout "$DOCOPT_PROGRAM_VERSION" +_return 0; fi; done; fi; local i=0; while [[ $i -lt ${#parsed_params[@]} ]]; do +left+=("$i"); ((i++)) || true; done +if ! required "$root_idx" || [ ${#left[@]} -gt 0 ]; then error; fi; return 0; } +parse_shorts() { local token=${argv[0]}; local value; argv=("${argv[@]:1}") +[[ $token = -* && $token != --* ]] || _return 88; local remaining=${token#-} +while [[ -n $remaining ]]; do local short="-${remaining:0:1}" +remaining="${remaining:1}"; local i=0; local similar=(); local match=false +for o in "${shorts[@]}"; do if [[ $o = "$short" ]]; then similar+=("$short") +[[ $match = false ]] && match=$i; fi; ((i++)) || true; done +if [[ ${#similar[@]} -gt 1 ]]; then +error "${short} is specified ambiguously ${#similar[@]} times" +elif [[ ${#similar[@]} -lt 1 ]]; then match=${#shorts[@]}; value=true +shorts+=("$short"); longs+=(''); argcounts+=(0); else value=false +if [[ ${argcounts[$match]} -ne 0 ]]; then if [[ $remaining = '' ]]; then +if [[ ${#argv[@]} -eq 0 || ${argv[0]} = '--' ]]; then +error "${short} requires argument"; fi; value=${argv[0]}; argv=("${argv[@]:1}") +else value=$remaining; remaining=''; fi; fi; if [[ $value = false ]]; then +value=true; fi; fi; parsed_params+=("$match"); parsed_values+=("$value"); done +}; parse_long() { local token=${argv[0]}; local long=${token%%=*} +local value=${token#*=}; local argcount; argv=("${argv[@]:1}") +[[ $token = --* ]] || _return 88; if [[ $token = *=* ]]; then eq='='; else eq='' +value=false; fi; local i=0; local similar=(); local match=false +for o in "${longs[@]}"; do if [[ $o = "$long" ]]; then similar+=("$long") +[[ $match = false ]] && match=$i; fi; ((i++)) || true; done +if [[ $match = false ]]; then i=0; for o in "${longs[@]}"; do +if [[ $o = $long* ]]; then similar+=("$long"); [[ $match = false ]] && match=$i +fi; ((i++)) || true; done; fi; if [[ ${#similar[@]} -gt 1 ]]; then +error "${long} is not a unique prefix: ${similar[*]}?" +elif [[ ${#similar[@]} -lt 1 ]]; then +[[ $eq = '=' ]] && argcount=1 || argcount=0; match=${#shorts[@]} +[[ $argcount -eq 0 ]] && value=true; shorts+=(''); longs+=("$long") +argcounts+=("$argcount"); else if [[ ${argcounts[$match]} -eq 0 ]]; then +if [[ $value != false ]]; then +error "${longs[$match]} must not have an argument"; fi +elif [[ $value = false ]]; then +if [[ ${#argv[@]} -eq 0 || ${argv[0]} = '--' ]]; then +error "${long} requires argument"; fi; value=${argv[0]}; argv=("${argv[@]:1}") +fi; if [[ $value = false ]]; then value=true; fi; fi; parsed_params+=("$match") +parsed_values+=("$value"); }; required() { local initial_left=("${left[@]}") +local node_idx; ((testdepth++)) || true; for node_idx in "$@"; do +if ! "node_$node_idx"; then left=("${initial_left[@]}"); ((testdepth--)) || true +return 1; fi; done; if [[ $((--testdepth)) -eq 0 ]]; then +left=("${initial_left[@]}"); for node_idx in "$@"; do "node_$node_idx"; done; fi +return 0; }; either() { local initial_left=("${left[@]}"); local best_match_idx +local match_count; local node_idx; ((testdepth++)) || true +for node_idx in "$@"; do if "node_$node_idx"; then +if [[ -z $match_count || ${#left[@]} -lt $match_count ]]; then +best_match_idx=$node_idx; match_count=${#left[@]}; fi; fi +left=("${initial_left[@]}"); done; ((testdepth--)) || true +if [[ -n $best_match_idx ]]; then "node_$best_match_idx"; return 0; fi +left=("${initial_left[@]}"); return 1; }; optional() { local node_idx +for node_idx in "$@"; do "node_$node_idx"; done; return 0; }; switch() { local i +for i in "${!left[@]}"; do local l=${left[$i]} +if [[ ${parsed_params[$l]} = "$2" ]]; then +left=("${left[@]:0:$i}" "${left[@]:((i+1))}") +[[ $testdepth -gt 0 ]] && return 0; if [[ $3 = true ]]; then +eval "((var_$1++))" || true; else eval "var_$1=true"; fi; return 0; fi; done +return 1; }; stdout() { printf -- "cat <<'EOM'\n%s\nEOM\n" "$1"; }; stderr() { +printf -- "cat <<'EOM' >&2\n%s\nEOM\n" "$1"; }; error() { +[[ -n $1 ]] && stderr "$1"; stderr "$usage"; _return 1; }; _return() { +printf -- "exit %d\n" "$1"; exit "$1"; }; set -e; trimmed_doc=${DOC:0:1027} +usage=${DOC:64:83}; digest=b986d; shorts=(-h '' '' '' '' '') +longs=(--help --patch --minor --major --bump-only --no-upload) +argcounts=(0 0 0 0 0 0); node_0(){ switch __help 0; }; node_1(){ +switch __patch 1; }; node_2(){ switch __minor 2; }; node_3(){ switch __major 3 +}; node_4(){ switch __bump_only 4; }; node_5(){ switch __no_upload 5; } +node_6(){ optional 0; }; node_7(){ either 1 2 3; }; node_8(){ optional 7; } +node_9(){ either 4 5; }; node_10(){ optional 9; }; node_11(){ required 6 8 10; } +node_12(){ required 11; }; cat <<<' docopt_exit() { +[[ -n $1 ]] && printf "%s\n" "$1" >&2; printf "%s\n" "${DOC:64:83}" >&2; exit 1 +}'; unset var___help var___patch var___minor var___major var___bump_only \ +var___no_upload; parse 12 "$@"; local prefix=${DOCOPT_PREFIX:-''} +local docopt_decl=1; [[ $BASH_VERSION =~ ^4.3 ]] && docopt_decl=2 +unset "${prefix}__help" "${prefix}__patch" "${prefix}__minor" \ +"${prefix}__major" "${prefix}__bump_only" "${prefix}__no_upload" +eval "${prefix}"'__help=${var___help:-false}' +eval "${prefix}"'__patch=${var___patch:-false}' +eval "${prefix}"'__minor=${var___minor:-false}' +eval "${prefix}"'__major=${var___major:-false}' +eval "${prefix}"'__bump_only=${var___bump_only:-false}' +eval "${prefix}"'__no_upload=${var___no_upload:-false}'; local docopt_i=0 +for ((docopt_i=0;docopt_i /dev/null + exit 1 +} + +confirm() { + if [ -z "$*" ]; then + read -p "Shall we proceed? (y/N)" -r confirmation + else + read -p "$*" -r confirmation + fi + if [[ ! $confirmation =~ ^[Yy]$ ]]; then + bail_out + fi + echo +} + +REPO_DIR=`dirname "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"` + +pushd $REPO_DIR &> /dev/null + +# Do a safety-confirmation if the user is about to do something that isn't trivial to undo. +if [ $__bump_only == "false" ]; then + if [ $__no_upload == "true" ]; then + echo "You're about to build and push a new ($VERSION_PART) release to Github" + echo "(Although we won't upload the package to PyPI)" + else + echo "You're about to build and push a new ($VERSION_PART) release to Github AND PyPI" + fi + confirm "Are you that's what you want to do? (y/N)" +fi + +# Do some sanity checks to make sure we're in a sufficient state to actually do what the user wants. + +# If we're planning to do more than just bump the version, then make sure "twine" is installed. +if [[ $__bump_only == "false" ]]; then + if ! command -v twine &> /dev/null; then + echo "You must install twine (pip install twine) if you want to upload to PyPI" + bail_out + fi +fi + +# Make sure we're on master (but let the user proceed if they reeeeally want to). +branch_name=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') +if [ "$branch_name" != "master" ]; then + echo "We probably shouldn't be bumping the version number if we're not on the master branch." + confirm "Are you this is want you want? (y/N)" +fi + +# Run the unit tests and make sure they're passing. +pipenv run pytest || bail_out + +# If we're *only* bumping the version, then we're safe to proceed at this point. +if [ $__bump_only == "true" ]; then + pipenv run bump2version --allow-dirty $VERSION_PART + exit +fi + +# In any other scenario, we should make sure the working directory is clean, and that we're +# up-to-date with origin/master +if ! git pull origin master --dry-run -v 2>&1 | grep "origin/master" | grep "up to date" &> /dev/null; then + echo "There are changes that you need to pull on master." + bail_out +fi + +# We'll let bump2version handle the dirty-working-directory scenario. +pipenv run bump2version $VERSION_PART || bail_out + +# Now we need to build the package, so let's clear away any junk that might be lying around. +rm -rf ./dist &> /dev/null +rm -rf ./build &> /dev/null + +# The build stdout is a bit noisy, but stderr will be helpful if there's an error. +pipenv run python ./setup.py sdist bdist_wheel > /dev/null || bail_out + +# Make sure there aren't any issues with the package. +twine check dist/* || bail_out + +# Upload the package if that's desirable. +if [ $__no_upload == "false" ]; then + twine upload dist/* || bail_out +fi + +# Finally, commit the changes, tag the commit, and push. +git add . +new_version=`cat .bumpversion.cfg | grep "current_version = " | sed "s/current_version = //g"` +git commit -m "Release version $new_version" +git tag -a "v$new_version" -m "Release version $new_version" + +git push origin master +git push origin "v$new_version" + +echo "Congrats!" +if [ $__no_upload == "true" ]; then + echo "The $new_version release commit has been pushed to GitHub, and tagged as \"v$new_version\"" +else + echo "The $new_version release is now live on PyPI, and tagged as \"v$new_version\" on GitHub" +fi + +popd &> /dev/null