|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +is_number() { |
| 4 | + case $1 in |
| 5 | + ''|*[!0-9]*) return 0 ;; |
| 6 | + *) return 1 ;; |
| 7 | + esac |
| 8 | +} |
| 9 | + |
| 10 | +# Show credits & help |
| 11 | +usage() { |
| 12 | + local SCRIPT_VER SCRIPT_AUTH_EMAIL SCRIPT_AUTH_NAME SCRIPT_HOME |
| 13 | + # NPM environment variables are fetched with cross-platform tool cross-env |
| 14 | + SCRIPT_VER=`cd $MODULE_DIR && npm run get-pkg-ver -s` |
| 15 | + SCRIPT_AUTH_NAME=`cd $MODULE_DIR && npm run get-pkg-auth -s` |
| 16 | + SCRIPT_AUTH_EMAIL=`cd $MODULE_DIR && npm run get-pkg-email -s` |
| 17 | + SCRIPT_NAME=`cd $MODULE_DIR && npm run get-pkg-name -s` |
| 18 | + SCRIPT_HOME=`cd $MODULE_DIR && npm run get-pkg-page -s` |
| 19 | + |
| 20 | + echo -e "${GREEN}"\ |
| 21 | + "_ _ ___ ___ ___ _ _ __ __ ___ "\ |
| 22 | + "\n| | || __>| . \ ___ | . >| | || \ \| . \ "\ |
| 23 | + "\n| ' || _> | /|___|| . \| ' || || _/ "\ |
| 24 | + "\n|__/ |___>|_\_\ |___/\___/|_|_|_||_| "\ |
| 25 | + "\n\t\t\t${LIGHTGRAY} ${BOLD}Version: $S_WARN${SCRIPT_VER}" |
| 26 | + |
| 27 | + echo -e "${S_NORM}${BOLD}Description:${RESET}"\ |
| 28 | + "\nThis script automates bumping the git software project's version automatically."\ |
| 29 | + "\nIt does several things that are typically required for releasing a Git repository, like git tagging, automatic updating of CHANGELOG.md, and incrementing the version number in various JSON files." |
| 30 | + |
| 31 | + echo -e "\n${S_NORM}${BOLD}Usage:${RESET}"\ |
| 32 | + "\n${SCRIPT_NAME} [-v <version number>] [-m <release message>] [-j <file1>] [-j <file2>].. [-n] [-p] [-h]" 1>&2; |
| 33 | + |
| 34 | + echo -e "\n${S_NORM}${BOLD}Options:${RESET}" |
| 35 | + echo -e "$S_WARN-v$S_NORM <version number>\tSpecify a manual version number" |
| 36 | + echo -e "$S_WARN-m$S_NORM <release message>\tCustom release message." |
| 37 | + echo -e "$S_WARN-f$S_NORM <filename.json>\tUpdate version number inside JSON files."\ |
| 38 | + "\n\t\t\tFor multiple files, add a separate -f option for each one, for example:"\ |
| 39 | + "\n\t\t\t${S_NORM}ver-bump -f src/plugin/package.json -f composer.json" |
| 40 | + echo -e "$S_WARN-p$S_NORM \t\t\tPush release branch to ORIGIN. " |
| 41 | + echo -e "$S_WARN-h$S_NORM \t\t\tShow this help message. \n" |
| 42 | + |
| 43 | + echo -e "${S_NORM}${BOLD}Credits:${S_LIGHT}"\ |
| 44 | + "\n${SCRIPT_AUTH_NAME} <${SCRIPT_AUTH_EMAIL}> ${RESET}"\ |
| 45 | + "\n${SCRIPT_HOME}\n" |
| 46 | +} |
| 47 | + |
| 48 | +# If there are no commits in repo, quit, because you can't tag with zero commits. |
| 49 | +check-commits-exist() { |
| 50 | + git rev-parse HEAD &> /dev/null |
| 51 | + if [ ! "$?" -eq 0 ]; then |
| 52 | + echo -e "\n${I_STOP} ${S_ERROR}Your current branch doesn't have any commits yet. Can't tag without at least one commit." >&2 |
| 53 | + echo |
| 54 | + exit 1 |
| 55 | + fi |
| 56 | +} |
| 57 | + |
| 58 | +get-commit-msg() { |
| 59 | + echo Bumped $([ -n "${V_PREV}" ] && echo "${V_PREV} –>" || echo "to ") "$V_USR_INPUT" |
| 60 | +} |
| 61 | + |
| 62 | +exit_abnormal() { |
| 63 | + usage # Show help |
| 64 | + exit 1 |
| 65 | +} |
| 66 | + |
| 67 | +# Process script options |
| 68 | +process-arguments() { |
| 69 | + local OPTIONS OPTIND OPTARG |
| 70 | + |
| 71 | + # Get positional parameters |
| 72 | + while getopts ":v:p:m:f:hbn" OPTIONS; do # Note: Adding the first : before the flags takes control of flags and prevents default error msgs. |
| 73 | + case "$OPTIONS" in |
| 74 | + h ) |
| 75 | + # Show help |
| 76 | + exit_abnormal |
| 77 | + ;; |
| 78 | + v ) |
| 79 | + # User has supplied a version number |
| 80 | + V_USR_SUPPLIED=$OPTARG |
| 81 | + ;; |
| 82 | + m ) |
| 83 | + REL_NOTE=$OPTARG |
| 84 | + # Custom release note |
| 85 | + echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Release note:" ${S_NORM}"'"$REL_NOTE"'" |
| 86 | + ;; |
| 87 | + f ) |
| 88 | + FLAG_JSON=true |
| 89 | + echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}JSON file via [-f]: <${S_NORM}${OPTARG}${S_LIGHT}>" |
| 90 | + # Store JSON filenames(s) |
| 91 | + JSON_FILES+=($OPTARG) |
| 92 | + ;; |
| 93 | + p ) |
| 94 | + FLAG_PUSH=true |
| 95 | + PUSH_DEST=${OPTARG} # Replace default with user input |
| 96 | + echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Pushing to <${S_NORM}${PUSH_DEST}${S_LIGHT}>, as the last action in this script." |
| 97 | + ;; |
| 98 | + n ) |
| 99 | + FLAG_NOCOMMIT=true |
| 100 | + echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable commit after tagging." |
| 101 | + ;; |
| 102 | + b ) |
| 103 | + FLAG_NOBRANCH=true |
| 104 | + echo -e "\n${S_LIGHT}Option set: ${S_NOTICE}Disable committing to new branch." |
| 105 | + ;; |
| 106 | + \? ) |
| 107 | + echo -e "\n${I_ERROR}${S_ERROR} Invalid option: ${S_WARN}-$OPTARG" >&2 |
| 108 | + echo |
| 109 | + exit_abnormal |
| 110 | + ;; |
| 111 | + : ) |
| 112 | + echo -e "\n${I_ERROR}${S_ERROR} Option ${S_WARN}-$OPTARG ${S_ERROR}requires an argument." >&2 |
| 113 | + echo |
| 114 | + exit_abnormal |
| 115 | + ;; |
| 116 | + esac |
| 117 | + done |
| 118 | +} |
| 119 | + |
| 120 | +# Suggests version from VERSION file, or grabs from user supplied -v <version>. |
| 121 | +# If none is set, suggest default from options. |
| 122 | + |
| 123 | +# - If <package.json> doesn't exist, warn + exit |
| 124 | +# - If -v specified, set version from that |
| 125 | +# - Else, |
| 126 | +# - Grab from package.json |
| 127 | +# - Suggest incremented number |
| 128 | +# - Give prompt to user to modify |
| 129 | +# - Set global |
| 130 | + |
| 131 | +# According to SemVer 2.0.0, given a version number MAJOR.MINOR.PATCH, suggest incremented value: |
| 132 | +# — MAJOR version when you make incompatible API changes, |
| 133 | +# — MINOR version when you add functionality in a backwards compatible manner, and |
| 134 | +# — PATCH version when you make backwards compatible bug fixes. |
| 135 | +process-version() { |
| 136 | + |
| 137 | + # If a version number is supplied by the user with [-v <version number>], then use it |
| 138 | + if [ -n "$V_USR_SUPPLIED" ]; then |
| 139 | + echo -e "\n${S_NOTICE}You selected version using [-v]:" "${S_WARN}${V_USR_SUPPLIED}" |
| 140 | + V_USR_INPUT="${V_USR_SUPPLIED}" |
| 141 | + else |
| 142 | + if [ -f $VER_FILE ] && [ -s $VER_FILE ]; then |
| 143 | + local IS_NO=0 |
| 144 | + |
| 145 | + # Get the existing version number |
| 146 | + V_PREV=$( sed -n 's/.*"version":.*"\(.*\)"\(,\)\{0,1\}/\1/p' $VER_FILE ) |
| 147 | + |
| 148 | + if [ -n "$V_PREV" ]; then |
| 149 | + |
| 150 | + echo -e "\n${S_NOTICE}Current version read from <${S_QUESTION}${VER_FILE}${S_NOTICE}> file: ${S_QUESTION}$V_PREV" |
| 151 | + |
| 152 | + V_PREV_LIST=(`echo $V_PREV | tr '.' ' '`) |
| 153 | + V_MAJOR=${V_PREV_LIST[0]}; V_MINOR=${V_PREV_LIST[1]}; V_PATCH=${V_PREV_LIST[2]}; |
| 154 | + |
| 155 | + is_number $V_MAJOR; (( IS_NO = $? )) |
| 156 | + is_number $V_MINOR; (( IS_NO = $? && $IS_NO )) |
| 157 | + |
| 158 | + # If major & minor are numbers, then proceed to increment patch |
| 159 | + if [ $IS_NO = 1 ]; then |
| 160 | + is_number $V_PATCH; |
| 161 | + [ $? == 1 ] && V_PATCH=$((V_PATCH + 1)) # Increment |
| 162 | + V_SUGGEST="$V_MAJOR.$V_MINOR.$V_PATCH" |
| 163 | + else |
| 164 | + # If patch not a number, do nothing |
| 165 | + echo -e "\n${I_WARN} ${S_WARN}Warning: ${S_QUESTION}${V_PREV}${S_WARN} doesn't look like a SemVer compatible version number!\n" |
| 166 | + fi |
| 167 | + |
| 168 | + else |
| 169 | + echo -e "\n${I_WARN} ${S_ERROR}Warning: <${S_QUESTION}${VER_FILE}${S_WARN}> doesn't contain a 'version' field!\n" |
| 170 | + fi |
| 171 | + else |
| 172 | + echo -ne "\n${S_WARN}Warning: <${S_QUESTION}${VER_FILE}${S_WARN}> " |
| 173 | + |
| 174 | + if [ ! -f $VER_FILE ]; then |
| 175 | + echo "was not found!"; |
| 176 | + elif [ ! -s $VER_FILE ]; then |
| 177 | + echo "is empty!"; |
| 178 | + fi |
| 179 | + fi |
| 180 | + |
| 181 | + # Confirm it with user |
| 182 | + echo -ne "\n${S_QUESTION}Enter a new version number or press <enter> to use [${S_NORM}$V_SUGGEST${S_QUESTION}]: " |
| 183 | + echo -ne "$S_WARN" |
| 184 | + read V_USR_INPUT |
| 185 | + |
| 186 | + if [ "$V_USR_INPUT" = "" ]; then |
| 187 | + V_USR_INPUT="${V_SUGGEST}" |
| 188 | + fi |
| 189 | + fi |
| 190 | +} |
| 191 | + |
| 192 | +# Only tag if tag doesn't already exist |
| 193 | +check-tag-exists() { |
| 194 | + TAG_CHECK_EXISTS=`git tag -l v"$V_USR_INPUT"` |
| 195 | + if [ -n "$TAG_CHECK_EXISTS" ]; then |
| 196 | + echo -e "\n${I_STOP} ${S_ERROR}Error: A release with that tag version number already exists!\n" |
| 197 | + exit 0 |
| 198 | + fi |
| 199 | +} |
| 200 | + |
| 201 | +# $1 : version |
| 202 | +# $2 : release note |
| 203 | +tag() { |
| 204 | + if [ -z "$2" ]; then |
| 205 | + # Default release note |
| 206 | + git tag -a "v$1" -m "Tag version $1." |
| 207 | + else |
| 208 | + # Custom release note |
| 209 | + git tag -a "v$1" -m "$2" |
| 210 | + fi |
| 211 | + echo -e "\n${I_OK} ${S_NOTICE}Added GIT tag" |
| 212 | +} |
| 213 | + |
| 214 | +# Change `version:` value in JSON files, like packager.json, composer.json, etc |
| 215 | +bump-json-files() { |
| 216 | + # if [ "$FLAG_JSON" != true ]; then return; fi |
| 217 | + |
| 218 | + JSON_PROCESSED=( ) # holds filenames after they've been changed |
| 219 | + |
| 220 | + for FILE in "${JSON_FILES[@]}"; do |
| 221 | + if [ -f $FILE ]; then |
| 222 | + # Get the existing version number |
| 223 | + V_OLD=$( sed -n 's/.*"version":.*"\(.*\)"\(,\)\{0,1\}/\1/p' $FILE ) |
| 224 | + |
| 225 | + if [ "$V_OLD" = "$V_USR_INPUT" ]; then |
| 226 | + echo -e "\n${I_ERROR} ${S_WARN}File <${S_QUESTION}$FILE${S_WARN}> already contains version ${S_NORM}$V_OLD" |
| 227 | + else |
| 228 | + # Write to output file |
| 229 | + FILE_MSG=`sed -i .temp "s/\"version\":\(.*\)\"$V_OLD\"/\"version\":\1\"$V_USR_INPUT\"/g" $FILE 2>&1` |
| 230 | + |
| 231 | + if [ "$?" -eq 0 ]; then |
| 232 | + echo -e "\n${I_OK} ${S_NOTICE}Updated file <${S_NORM}$FILE${S_NOTICE}> from ${S_QUESTION}$V_OLD ${S_NOTICE}-> ${S_QUESTION}$V_USR_INPUT" |
| 233 | + rm -f ${FILE}.temp |
| 234 | + # Add file change to commit message: |
| 235 | + GIT_MSG+="Updated $FILE, " |
| 236 | + else |
| 237 | + echo -e "\n${I_STOP} ${S_ERROR}Error\n$PUSH_MSG\n" |
| 238 | + fi |
| 239 | + fi |
| 240 | + |
| 241 | + JSON_PROCESSED+=($FILE) |
| 242 | + else |
| 243 | + echo -e "\n${S_WARN}File <${S_NORM}$FILE${S_WARN}> not found." |
| 244 | + fi |
| 245 | + done |
| 246 | + # Stage files that were changed: |
| 247 | + [ -n "${JSON_PROCESSED}" ] && git add "${JSON_PROCESSED[@]}" |
| 248 | +} |
| 249 | + |
| 250 | +# Handle VERSION file - for backward compatibility |
| 251 | +do-versionfile() { |
| 252 | + if [ -f VERSION ]; then |
| 253 | + GIT_MSG+="Updated VERSION, " |
| 254 | + echo $V_USR_INPUT > VERSION # Overwrite file |
| 255 | + # Stage file for commit |
| 256 | + git add VERSION |
| 257 | + |
| 258 | + echo -e "\n${I_OK} ${S_NOTICE}Updated [${S_NORM}VERSION${S_NOTICE}] file."\ |
| 259 | + "\n${I_WARN} ${S_ERROR}Deprecation warning: using a <${S_NORM}VERSION${S_ERROR}> file is deprecated since v0.2.0 — support will be removed in future versions." |
| 260 | + fi |
| 261 | +} |
| 262 | + |
| 263 | +# Dump git log history to CHANGELOG.md |
| 264 | +do-changelog() { |
| 265 | + |
| 266 | + # Log latest commits to CHANGELOG.md: |
| 267 | + # Get latest commits since last versio |
| 268 | + LOG_MSG=`git log --pretty=format:"- %s" $([ -n "$V_PREV" ] && echo "v${V_PREV}...HEAD") 2>&1` |
| 269 | + if [ ! "$?" -eq 0 ]; then |
| 270 | + echo -e "\n${I_STOP} ${S_ERROR}Error getting commit history since last version bump for logging to CHANGELOG.\n\n$LOG_MSG\n" |
| 271 | + exit 1 |
| 272 | + fi |
| 273 | + |
| 274 | + [ -f CHANGELOG.md ] && ACTION_MSG="Updated" || ACTION_MSG="Created" |
| 275 | + # Add info to commit message for later: |
| 276 | + GIT_MSG+="${ACTION_MSG} CHANGELOG.md, " |
| 277 | + |
| 278 | + # Add heading |
| 279 | + echo "## $V_USR_INPUT ($NOW)" > tmpfile |
| 280 | + |
| 281 | + # Log the bumping commit: |
| 282 | + # - The final commit is done after do-changelog(), so we need to create the log entry for it manually: |
| 283 | + echo "- ${GIT_MSG}$(get-commit-msg)" >> tmpfile |
| 284 | + # Add previous commits |
| 285 | + [ -n "$LOG_MSG" ] && echo "$LOG_MSG" >> tmpfile |
| 286 | + |
| 287 | + echo -en "\n" >> tmpfile |
| 288 | + |
| 289 | + if [ -f CHANGELOG.md ]; then |
| 290 | + # Append existing log |
| 291 | + cat CHANGELOG.md >> tmpfile |
| 292 | + else |
| 293 | + echo -e "\n${S_WARN}A [${S_NORM}CHANGELOG.md${S_WARN}] file was not found." |
| 294 | + fi |
| 295 | + |
| 296 | + mv tmpfile CHANGELOG.md |
| 297 | + |
| 298 | + # User prompts |
| 299 | + echo -e "\n${I_OK} ${S_NOTICE}${ACTION_MSG} [${S_NORM}CHANGELOG.md${S_NOTICE}] file" |
| 300 | + # Pause & allow user to open and edit the file: |
| 301 | + echo -en "\n${S_QUESTION}Make adjustments to [${S_NORM}CHANGELOG.md${S_QUESTION}] if required now. Press <enter> to continue." |
| 302 | + read |
| 303 | + |
| 304 | + # Stage log file, to commit later |
| 305 | + git add CHANGELOG.md |
| 306 | +} |
| 307 | + |
| 308 | +# |
| 309 | +check-branch-exist() { |
| 310 | + [ "$FLAG_NOBRANCH" = true ] && return |
| 311 | + |
| 312 | + BRANCH_MSG=`git rev-parse --verify "${REL_PREFIX}${V_USR_INPUT}" 2>&1` |
| 313 | + if [ "$?" -eq 0 ]; then |
| 314 | + echo -e "\n${I_STOP} ${S_ERROR}Error: Branch <${S_NORM}${REL_PREFIX}${V_USR_INPUT}${S_ERROR}> already exists!\n" |
| 315 | + exit 1 |
| 316 | + fi |
| 317 | +} |
| 318 | + |
| 319 | +# |
| 320 | +do-branch() { |
| 321 | + [ "$FLAG_NOBRANCH" = true ] && return |
| 322 | + |
| 323 | + echo -e "\n${S_NOTICE}Creating new release branch..." |
| 324 | + |
| 325 | + BRANCH_MSG=`git branch "${REL_PREFIX}${V_USR_INPUT}" 2>&1` |
| 326 | + if [ ! "$?" -eq 0 ]; then |
| 327 | + echo -e "\n${I_STOP} ${S_ERROR}Error\n$BRANCH_MSG\n" |
| 328 | + exit 1 |
| 329 | + else |
| 330 | + BRANCH_MSG=`git checkout "${REL_PREFIX}${V_USR_INPUT}" 2>&1` |
| 331 | + echo -e "\n${I_OK} ${S_NOTICE}${BRANCH_MSG}" |
| 332 | + fi |
| 333 | + |
| 334 | + # REL_PREFIX |
| 335 | +} |
| 336 | + |
| 337 | +# Stage & commit all files modified by this script |
| 338 | +do-commit() { |
| 339 | + [ "$FLAG_NOCOMMIT" = true ] && return |
| 340 | + |
| 341 | + GIT_MSG+="$(get-commit-msg)" |
| 342 | + echo -e "\n${S_NOTICE}Committing..." |
| 343 | + COMMIT_MSG=`git commit -m "${GIT_MSG}" 2>&1` |
| 344 | + if [ ! "$?" -eq 0 ]; then |
| 345 | + echo -e "\n${I_STOP} ${S_ERROR}Error\n$COMMIT_MSG\n" |
| 346 | + exit 1 |
| 347 | + else |
| 348 | + echo -e "\n${I_OK} ${S_NOTICE}$COMMIT_MSG" |
| 349 | + fi |
| 350 | +} |
| 351 | + |
| 352 | +# Pushes files + tags to remote repo. Changes are staged by earlier functions |
| 353 | +do-push() { |
| 354 | + [ "$FLAG_NOCOMMIT" = true ] && return |
| 355 | + |
| 356 | + if [ "$FLAG_PUSH" = true ]; then |
| 357 | + CONFIRM="Y" |
| 358 | + else |
| 359 | + echo -ne "\n${S_QUESTION}Push tags to <${S_NORM}${PUSH_DEST}${S_QUESTION}>? [${S_NORM}N/y${S_QUESTION}]: " |
| 360 | + read CONFIRM |
| 361 | + fi |
| 362 | + |
| 363 | + case "$CONFIRM" in |
| 364 | + [yY][eE][sS]|[yY] ) |
| 365 | + echo -e "\n${S_NOTICE}Pushing files + tags to <${S_NORM}${PUSH_DEST}${S_NOTICE}>..." |
| 366 | + PUSH_MSG=`git push "${PUSH_DEST}" v"$V_USR_INPUT" 2>&1` # Push new tag |
| 367 | + if [ ! "$?" -eq 0 ]; then |
| 368 | + echo -e "\n${I_STOP} ${S_WARN}Warning\n$PUSH_MSG" |
| 369 | + # exit 1 |
| 370 | + else |
| 371 | + echo -e "\n${I_OK} ${S_NOTICE}$PUSH_MSG" |
| 372 | + fi |
| 373 | + ;; |
| 374 | + esac |
| 375 | +} |
0 commit comments