549 lines
14 KiB
Bash
Executable File
549 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
#
|
|
# Cleanroom Project release generator script
|
|
#
|
|
# by emaloney, 7 June 2015
|
|
#
|
|
|
|
set -o pipefail # to ensure xcodebuild pipeline errors are propagated correctly
|
|
|
|
SCRIPT_NAME=$(basename "$0")
|
|
SCRIPT_DIR=$(cd "$PWD" ; cd `dirname "$0"` ; echo "$PWD")
|
|
|
|
source "${SCRIPT_DIR}/include-common.sh"
|
|
|
|
showHelp()
|
|
{
|
|
define HELP <<HELP
|
|
$SCRIPT_NAME
|
|
|
|
Issues a new release of the project contained in this git repository.
|
|
|
|
Usage:
|
|
|
|
$SCRIPT_NAME <release-type> [...]
|
|
|
|
Where:
|
|
|
|
<release-type> is 'major', 'minor' or 'patch', depending on which portion
|
|
of the version number should be incremented for this release.
|
|
|
|
Optional arguments:
|
|
|
|
--set-version <version>
|
|
Make <version> the version number being released
|
|
|
|
--auto
|
|
Run automatically without awaiting user confirmation
|
|
|
|
--tag
|
|
Tags the repo with the version number upon success
|
|
|
|
--push
|
|
Push all changes upon success
|
|
|
|
--amend
|
|
Causes any commits to be amends to the previous commit
|
|
|
|
--branch <branch>
|
|
Specifies <branch> be used as the git branch for operations
|
|
|
|
--commit-message-file <file>
|
|
Specifies the contents <file> should be used as the commit message
|
|
|
|
--no-commit
|
|
Skips committing any changes; implies --no-tag
|
|
|
|
--no-tag
|
|
Overrides --tag if specified; --no-tag is the default
|
|
|
|
--stash-dirty-files
|
|
Stashes dirty files before attempting to release
|
|
|
|
--commit-dirty-files
|
|
Commits dirty files before attempting to release
|
|
|
|
--ignore-dirty-files
|
|
Ignores dirty files; implies --no-commit --no-tag
|
|
|
|
--skip-docs
|
|
Skips generating the documentation
|
|
|
|
--skip-tests
|
|
Skips running the unit tests :-(
|
|
|
|
--quiet
|
|
Silences output
|
|
|
|
--summarize
|
|
Minimizes output (ideal for invoking from other scripts)
|
|
|
|
Further detail can be found below.
|
|
|
|
How it works
|
|
|
|
By default, the script inspects the appropriate property list file(s)
|
|
to determine the current version of the project. The script then
|
|
increments the version number according to the release type
|
|
specified:
|
|
|
|
major — When the major release type is specified, the major version
|
|
component is incremented, and both the minor and patch
|
|
components are reset to zero. 2.1.3 becomes 3.0.0.
|
|
|
|
minor — When the minor release type is specified, the major version
|
|
component is not changed, while the minor component is
|
|
incremented and patch component is reset to zero.
|
|
2.1.3 becomes 2.2.0.
|
|
|
|
patch — When the patch release type is specified, the major and minor
|
|
version components remain unchanged, while the patch component
|
|
is incremented. 2.1.3 becomes 2.1.4.
|
|
|
|
The script then updates all necessary references to the version
|
|
elsewhere in the project.
|
|
|
|
Then, the API documentation is rebuilt, and the repository is tagged
|
|
with the appropriate version number for the release.
|
|
|
|
Finally, if the --push argument was supplied, the entire release is
|
|
pushed to the repo's origin remote.
|
|
|
|
Specifying the version explicitly
|
|
|
|
The --set-version argument can be supplied along with a version number
|
|
if you wish to specify the exact version number to use.
|
|
|
|
The version number is expected to contain exactly three integer
|
|
components separated by periods; trailing zeros are used if
|
|
necessary.
|
|
|
|
If you wanted to set a release version of 4.2.1, for example, you
|
|
could call the script as follows:
|
|
|
|
$SCRIPT_NAME --set-version 4.2.1
|
|
|
|
NOTE: When the --set-version argument is supplied, the release-type
|
|
argument does not need to be specified (and it will be ignored
|
|
if it is).
|
|
|
|
User Confirmation
|
|
|
|
By default, this script requires user confirmation before making
|
|
any changes.
|
|
|
|
To allow this script to be invoked by other scripts, an automated
|
|
mode is also supported.
|
|
|
|
When this script is run in automated mode, the user will not be
|
|
asked to confirm any actions; all actions are performed immediately.
|
|
|
|
To enable automated mode, supply the --auto argument.
|
|
|
|
Releasing with uncommitted changes
|
|
|
|
Normally, this script will refuse to continue if the repository
|
|
is dirty; that is, if there are any modified files that haven't
|
|
yet been committed.
|
|
|
|
However, you can force a release to be issued from a dirty repo
|
|
using either the --stash-dirty-files or the --commit-dirty-files
|
|
argument.
|
|
|
|
The --stash-dirty-files option causes a git stash operation to
|
|
occur at the start of the release process, and a stash pop at the
|
|
end. This safely moves the dirty files out of the way when the
|
|
script it doing its thing, and restores them when it is done.
|
|
|
|
The --commit-dirty-files option causes the dirty files to be
|
|
committed along with the other changes that occur during the
|
|
release process.
|
|
|
|
In addition, an --ignore-dirty-files option is available, which
|
|
lets you go through the entire release process, but stops short
|
|
of committing and tagging. This allows you to run through the
|
|
entire release process without committing you to committing.
|
|
|
|
Note that these options are mutually exclusive and may not be
|
|
used with each other.
|
|
|
|
Help
|
|
|
|
This documentation is displayed when supplying the --help (or -help, -h,
|
|
or -?) argument.
|
|
|
|
Note that when this script displays help documentation, all other
|
|
command line arguments are ignored and no other actions are performed.
|
|
|
|
HELP
|
|
printf "$HELP" | less
|
|
}
|
|
|
|
validateVersion()
|
|
{
|
|
if [[ ! ($1 =~ ^[0-9]+\.[0-9]+\.[0-9]+$) ]]; then
|
|
exitWithErrorSuggestHelp "Expected $2 to contain three period-separated numeric components (eg., 3.6.1, 4.0.0, etc.); got $1 instead"
|
|
fi
|
|
}
|
|
|
|
cleanupDirtyStash()
|
|
{
|
|
updateStatus "Restoring previously-stashed modified files"
|
|
executeCommand "git stash pop"
|
|
}
|
|
|
|
#
|
|
# make sure we're in a git repo
|
|
#
|
|
cd "$SCRIPT_DIR/../../."
|
|
git status 2&> /dev/null
|
|
if [[ $? != 0 ]]; then
|
|
exitWithErrorSuggestHelp "You must invoke this script from within a git repo"
|
|
fi
|
|
|
|
#
|
|
# parse the command-line arguments
|
|
#
|
|
AMEND_ARGS=""
|
|
BRANCH=master
|
|
STASH_DIRTY_FILES=0
|
|
COMMIT_DIRTY_FILES=0
|
|
IGNORE_DIRTY_FILES=0
|
|
while [[ $1 ]]; do
|
|
case $1 in
|
|
--set-version)
|
|
shift
|
|
if [[ -z $1 ]]; then
|
|
exitWithErrorSuggestHelp "The $1 argument expects a value"
|
|
else
|
|
validateVersion $1 "the version passed with the --set-version argument"
|
|
SET_VERSION=$1
|
|
fi
|
|
;;
|
|
|
|
--auto|-a)
|
|
AUTOMATED_MODE=1
|
|
;;
|
|
|
|
--amend)
|
|
AMEND_ARGS="--amend --no-edit"
|
|
;;
|
|
|
|
--stash-dirty-files)
|
|
STASH_DIRTY_FILES=1
|
|
;;
|
|
|
|
--commit-dirty-files)
|
|
COMMIT_DIRTY_FILES=1
|
|
;;
|
|
|
|
--ignore-dirty-files)
|
|
IGNORE_DIRTY_FILES=1
|
|
NO_COMMIT=1
|
|
NO_TAG=1
|
|
;;
|
|
|
|
--no-commit)
|
|
NO_COMMIT=1
|
|
NO_TAG=1
|
|
;;
|
|
|
|
--no-tag)
|
|
NO_TAG=1
|
|
;;
|
|
|
|
--tag)
|
|
TAG_WHEN_DONE=1
|
|
;;
|
|
|
|
--push)
|
|
PUSH_WHEN_DONE=1
|
|
;;
|
|
|
|
--branch|-b)
|
|
if [[ $2 ]]; then
|
|
BRANCH="$2"
|
|
shift
|
|
fi
|
|
;;
|
|
|
|
--commit-message-file|-m)
|
|
if [[ $2 ]]; then
|
|
COMMIT_MESSAGE=`cat "$2"`
|
|
shift
|
|
fi
|
|
;;
|
|
|
|
--skip-docs)
|
|
SKIP_DOCUMENTATION=1
|
|
;;
|
|
|
|
--skip-tests)
|
|
SKIP_TESTS=1
|
|
;;
|
|
|
|
--quiet|-q)
|
|
QUIET=1
|
|
QUIET_ARG="-q"
|
|
;;
|
|
|
|
--summarize|-z)
|
|
SUMMARIZE=1
|
|
QUIET=1
|
|
QUIET_ARG="-q"
|
|
;;
|
|
|
|
--rebase)
|
|
REBASE=1
|
|
;;
|
|
|
|
--help|-help|-h|-\?)
|
|
SHOW_HELP=1
|
|
;;
|
|
|
|
-*)
|
|
exitWithErrorSuggestHelp "Unrecognized argument: $1"
|
|
;;
|
|
|
|
*)
|
|
if [[ -z $ARGS ]]; then
|
|
ARGS=$1
|
|
else
|
|
ARGS="$ARGS $1"
|
|
fi
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ $SHOW_HELP ]]; then
|
|
showHelp
|
|
exit 1
|
|
fi
|
|
|
|
for ARG in $ARGS; do
|
|
if [[ -z $RELEASE_TYPE ]]; then
|
|
RELEASE_TYPE="$ARG"
|
|
else
|
|
exitWithErrorSuggestHelp "Unrecognized argument: $ARG"
|
|
fi
|
|
done
|
|
|
|
#
|
|
# validate the input
|
|
#
|
|
if [[ $(( $STASH_DIRTY_FILES + $COMMIT_DIRTY_FILES + $IGNORE_DIRTY_FILES )) > 1 ]]; then
|
|
exitWithErrorSuggestHelp "The --stash-dirty-files, --commit-dirty-files and --ignore-dirty-files arguments are mutually exclusive and can't be used with each other"
|
|
fi
|
|
if [[ ! -z $RELEASE_TYPE ]]; then
|
|
if [[ ! -z $SET_VERSION ]]; then
|
|
exitWithErrorSuggestHelp "The release type can't be specified when --set-version is used"
|
|
elif [[ $RELEASE_TYPE != "major" && $RELEASE_TYPE != "minor" && $RELEASE_TYPE != "patch" ]]; then
|
|
exitWithErrorSuggestHelp "The release type argument must be one of: 'major', 'minor' or 'patch'"
|
|
fi
|
|
elif [[ -z $SET_VERSION ]]; then
|
|
if [[ -z $RELEASE_TYPE ]]; then
|
|
exitWithErrorSuggestHelp "The release type ('major', 'minor' or 'patch') must be specified as an argument."
|
|
fi
|
|
fi
|
|
|
|
#
|
|
# figure out what the current version is
|
|
#
|
|
FRAMEWORK_PLIST_FILE="Info-Target.plist"
|
|
FRAMEWORK_PLIST_PATH="$SCRIPT_DIR/../$FRAMEWORK_PLIST_FILE"
|
|
PLIST_BUDDY=/usr/libexec/PlistBuddy
|
|
CURRENT_VERSION=`$PLIST_BUDDY "$FRAMEWORK_PLIST_PATH" -c "Print :CFBundleShortVersionString"`
|
|
validateVersion "$CURRENT_VERSION" "the CFBundleShortVersionString value in the $FRAMEWORK_PLIST_FILE file"
|
|
|
|
#
|
|
# now, do the right thing depending on the command-line arguments
|
|
#
|
|
if [[ ! -z $SET_VERSION ]]; then
|
|
VERSION=$SET_VERSION
|
|
elif [[ ! -z $RELEASE_TYPE ]]; then
|
|
MAJOR_VERSION=`echo $CURRENT_VERSION | awk -F . '{print int($1)}'`
|
|
MINOR_VERSION=`echo $CURRENT_VERSION | awk -F . '{print int($2)}'`
|
|
PATCH_VERSION=`echo $CURRENT_VERSION | awk -F . '{print int($3)}'`
|
|
|
|
case $RELEASE_TYPE in
|
|
major)
|
|
MAJOR_VERSION=$(( $MAJOR_VERSION + 1 ))
|
|
MINOR_VERSION=0
|
|
PATCH_VERSION=0
|
|
;;
|
|
|
|
minor)
|
|
MINOR_VERSION=$(( $MINOR_VERSION + 1 ))
|
|
PATCH_VERSION=0
|
|
;;
|
|
|
|
patch)
|
|
PATCH_VERSION=$(( $PATCH_VERSION + 1 ))
|
|
;;
|
|
esac
|
|
|
|
VERSION="${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}"
|
|
fi
|
|
|
|
#
|
|
# try to figure out the origin repo name
|
|
#
|
|
REPO_NAME=$(git remote -v | grep "^origin" | grep "(fetch)" | awk '{print $2}' | xargs basename | sed 'sq.git$qq')
|
|
if [[ -z "$REPO_NAME" ]]; then
|
|
exitWithErrorSuggestHelp "Couldn't determine repo name"
|
|
fi
|
|
|
|
#
|
|
# output a warning if there are conflicting tag flags
|
|
#
|
|
if [[ $TAG_WHEN_DONE && $NO_TAG ]]; then
|
|
exitWithErrorSuggestHelp "--tag can't be specified with --no-tag, --no-commit or --ignore-dirty-files"
|
|
fi
|
|
|
|
#
|
|
# see if we've got uncommitted changes
|
|
#
|
|
git rev-parse --quiet --verify HEAD > /dev/null
|
|
if [[ $? == 0 ]]; then
|
|
git diff-index --quiet HEAD -- ; REPO_IS_DIRTY=$?
|
|
else
|
|
# if HEAD doesn't exist (which is how we get here), then treat the
|
|
# repo as if it were dirty
|
|
REPO_IS_DIRTY=1
|
|
fi
|
|
if [[ $REPO_IS_DIRTY != 0 && $(( $STASH_DIRTY_FILES + $COMMIT_DIRTY_FILES + $IGNORE_DIRTY_FILES )) == 0 ]]; then
|
|
exitWithErrorSuggestHelp "You have uncommitted changes in this repo; won't do anything" "(use --stash-dirty-files, --commit-dirty-files or\n\t--ignore-dirty-files to bypass this error)"
|
|
fi
|
|
REPO_URL=`git remote get-url --push origin 2> /dev/null`
|
|
if [[ $? == 0 ]]; then
|
|
git ls-remote --heads $REPO_URL $BRANCH | grep "refs/heads/$BRANCH" > /dev/null
|
|
if [[ $? == 0 ]]; then
|
|
REMOTE_BRANCH_EXISTS=1
|
|
fi
|
|
fi
|
|
if [[ ! $REMOTE_BRANCH_EXISTS ]]; then
|
|
GIT_PUSH_ARGS="--set-upstream origin $BRANCH"
|
|
fi
|
|
|
|
confirmationPrompt "Releasing $REPO_NAME $VERSION (current is $CURRENT_VERSION)"
|
|
|
|
if [[ $REPO_IS_DIRTY && $STASH_DIRTY_FILES > 0 ]]; then
|
|
updateStatus "Stashing modified files"
|
|
executeCommand "git stash"
|
|
trap cleanupDirtyStash EXIT
|
|
fi
|
|
|
|
#
|
|
# make sure it builds
|
|
#
|
|
XCODEBUILD=/usr/bin/xcodebuild
|
|
XCODEBUILD_CMD="$XCODEBUILD"
|
|
updateStatus "Verifying that $REPO_NAME builds"
|
|
if [[ $QUIET ]]; then
|
|
XCODEBUILD_CMD="$XCODEBUILD -quiet"
|
|
fi
|
|
if [[ ! -x "$XCODEBUILD" ]]; then
|
|
exitWithErrorSuggestHelp "Expected to find xcodebuild at path $XCODEBUILD"
|
|
fi
|
|
|
|
#
|
|
# use xcpretty if it is available
|
|
#
|
|
XCODEBUILD_PIPETO=""
|
|
XCPRETTY=`which xcpretty`
|
|
if [[ $? == 0 ]]; then
|
|
XCODEBUILD_PIPETO="| $XCPRETTY"
|
|
fi
|
|
|
|
#
|
|
# determine build settings
|
|
#
|
|
PROJECT_SPECIFIER="-project CleanroomLogger.xcodeproj"
|
|
COMPILE_PLATFORMS="iOS macOS tvOS watchOS"
|
|
PROJECT_NAME="CleanroomLogger"
|
|
|
|
#
|
|
# build for each platform
|
|
#
|
|
for PLATFORM in $COMPILE_PLATFORMS; do
|
|
updateStatus "Building: $PROJECT_NAME for $PLATFORM..."
|
|
summarize "building $PROJECT_NAME for $PLATFORM"
|
|
if [[ $SKIP_TESTS ]]; then
|
|
BUILD_ACTION="clean build"
|
|
else
|
|
BUILD_ACTION="clean $(testActionForPlatform $PLATFORM)"
|
|
fi
|
|
RUN_DESTINATION="$(runDestinationForPlatform $PLATFORM)"
|
|
if [[ $QUIET ]]; then
|
|
executeCommand "$XCODEBUILD_CMD $PROJECT_SPECIFIER -scheme \"${REPO_NAME}\" -configuration Debug -destination \"$RUN_DESTINATION\" $BUILD_ACTION $XCODEBUILD_PIPETO" 2&> /dev/null
|
|
else
|
|
executeCommand "$XCODEBUILD_CMD $PROJECT_SPECIFIER -scheme \"${REPO_NAME}\" -configuration Debug -destination \"$RUN_DESTINATION\" $BUILD_ACTION $XCODEBUILD_PIPETO"
|
|
fi
|
|
done
|
|
|
|
#
|
|
# bump version numbers
|
|
#
|
|
updateStatus "Adjusting version numbers"
|
|
executeCommand "$PLIST_BUDDY \"$FRAMEWORK_PLIST_PATH\" -c \"Set :CFBundleShortVersionString $VERSION\""
|
|
agvtool bump > /dev/null
|
|
summarize "bumped version to $VERSION from $CURRENT_VERSION for $RELEASE_TYPE release"
|
|
|
|
#
|
|
# commit changes
|
|
#
|
|
BUILD_NUMBER=`agvtool vers -terse`
|
|
if [[ -z $COMMIT_MESSAGE ]]; then
|
|
COMMIT_MESSAGE="Release $VERSION (build $BUILD_NUMBER)"
|
|
if [[ $REPO_IS_DIRTY && $COMMIT_DIRTY_FILES > 0 ]]; then
|
|
COMMIT_MESSAGE="$COMMIT_MESSAGE -- committed with other changes"
|
|
fi
|
|
else
|
|
COMMIT_MESSAGE="[$VERSION] $COMMIT_MESSAGE"
|
|
fi
|
|
if [[ -z $NO_COMMIT ]]; then
|
|
updateStatus "Committing changes"
|
|
printf "%s" "$COMMIT_MESSAGE" | git commit -a $QUIET_ARG $AMEND_ARGS -F -
|
|
summarize "committed changes to \"$BRANCH\" branch"
|
|
else
|
|
updateStatus "! Not committing changes; --no-commit or --ignore-dirty-files was specified"
|
|
printf "> To commit manually, use:\n\n git commit -a -m '$COMMIT_MESSAGE'\n"
|
|
fi
|
|
|
|
#
|
|
# rebase with existing changes if needed
|
|
#
|
|
if [[ $REBASE && $REMOTE_BRANCH_EXISTS ]]; then
|
|
updateStatus "Rebasing with existing $BRANCH branch"
|
|
executeCommand "git pull origin $BRANCH $QUIET_ARG --rebase --allow-unrelated-histories --strategy=recursive -Xtheirs"
|
|
summarize "rebased \"$BRANCH\" branch"
|
|
fi
|
|
|
|
#
|
|
# tag repo with new version number
|
|
#
|
|
if [[ $TAG_WHEN_DONE && -z $NO_COMMIT && -z $NO_TAG ]]; then
|
|
updateStatus "Tagging repo for $VERSION release"
|
|
executeCommand "git tag -a $VERSION -m 'Release $VERSION issued by $SCRIPT_NAME'"
|
|
summarize "tagged \"$BRANCH\" branch with $VERSION"
|
|
else
|
|
updateStatus "! Not tagging repo; --tag was not specified"
|
|
printf "> To tag manually, use:\n\n git tag -a $VERSION -m 'Release $VERSION issued by $SCRIPT_NAME'\n"
|
|
fi
|
|
|
|
#
|
|
# push if we should
|
|
#
|
|
if [[ $PUSH_WHEN_DONE && -z $NO_COMMIT ]]; then
|
|
ORIGIN_URL=`git remote get-url --push origin`
|
|
updateStatus "Pushing changes to \"$BRANCH\" branch of $ORIGIN_URL"
|
|
executeCommand "git push $QUIET_ARG $GIT_PUSH_ARGS"
|
|
if [[ $TAG_WHEN_DONE && !$NO_TAG ]]; then
|
|
executeCommand "git push --tags $QUIET_ARG"
|
|
fi
|
|
summarize "pushed changes to \"$BRANCH\" branch of $ORIGIN_URL"
|
|
else
|
|
printf "\n> REMEMBER: The release isn't done until you push the changes! Don't forget to:\n\n git push && git push --tags\n"
|
|
fi
|