See More

#!/bin/sh # ============================================================================ # release-version.sh # ============================================================================ # Releases a new version of a component extending the pom-scijava parent. # # Authors: Johannes Schindelin & Curtis Rueden # ============================================================================ # -- Avoid localized output that might confuse the script -- export LC_ALL=C # -- Functions -- debug() { test "$DEBUG" || return echo "[DEBUG] $@" } die() { echo "$*" >&2 exit 1 } no_changes_pending() { git update-index -q --refresh && git diff-files --quiet --ignore-submodules && git diff-index --cached --quiet --ignore-submodules HEAD -- } # Get list of modules from aggregator POM get_modules() { grep -oP '\K[^<]+' pom.xml 2>/dev/null || true } # Check if current directory is a multi-module aggregator is_aggregator() { test -f pom.xml && grep -q '' pom.xml } # Resolve snapshot dependencies to latest releases (TODO for later) resolve_snapshot_deps() { module_pom=$1 debug "Resolving snapshot dependencies in $module_pom" echo "TODO: Implement automatic snapshot dependency resolution" echo "For now, please manually update any *.version properties to releases" echo "Press enter when ready to continue..." read } # Update all modules that depend on the released module to use the next snapshot version update_inter_module_deps() { released_module=$1 next_snapshot_version=$2 debug "Updating inter-module dependencies on $released_module to $next_snapshot_version" # Property name pattern: scijava-meta -> scijava-meta.version property_name="${released_module}.version" # Find all module POMs that have this property for module_dir in $(get_modules) do module_pom="$module_dir/pom.xml" if test -f "$module_pom" && grep -q "<${property_name}>" "$module_pom" then debug "Updating $property_name in $module_pom to $next_snapshot_version" sed -i.bak "s|<${property_name}>.*${property_name}>|<${property_name}>${next_snapshot_version}${property_name}>|" "$module_pom" rm -f "$module_pom.bak" git add "$module_pom" fi done } # -- Constants and settings -- SCIJAVA_BASE_REPOSITORY=-DaltDeploymentRepository=scijava.releases::default::dav:https://maven.scijava.org/content/repositories SCIJAVA_RELEASES_REPOSITORY=$SCIJAVA_BASE_REPOSITORY/releases SCIJAVA_THIRDPARTY_REPOSITORY=$SCIJAVA_BASE_REPOSITORY/thirdparty # Parse command line options. BATCH_MODE=--batch-mode SKIP_VERSION_CHECK= SKIP_BRANCH_CHECK= SKIP_LICENSE_UPDATE= SKIP_PUSH= SKIP_GPG= TAG= DEV_VERSION= EXTRA_ARGS= ALT_REPOSITORY= PROFILE=-Pdeploy-to-scijava DRY_RUN= USAGE= VERSION= while test $# -gt 0 do case "$1" in --dry-run) DRY_RUN=echo;; --no-batch-mode) BATCH_MODE=;; --skip-version-check) SKIP_VERSION_CHECK=t;; --skip-branch-check) SKIP_BRANCH_CHECK=t;; --skip-license-update) SKIP_LICENSE_UPDATE=t;; --skip-push) SKIP_PUSH=t;; --tag=*) ! git rev-parse --quiet --verify refs/tags/"${1#--*=}" || die "Tag ${1#--*=} exists already!" TAG="-Dtag=${1#--*=}";; --dev-version=*|--development-version=*) DEV_VERSION="-DdevelopmentVersion=${1#--*=}";; --extra-arg=*|--extra-args=*) EXTRA_ARGS="$EXTRA_ARGS ${1#--*=}";; --alt-repository=scijava-releases) ALT_REPOSITORY=$SCIJAVA_RELEASES_REPOSITORY;; --alt-repository=scijava-thirdparty) ALT_REPOSITORY=$SCIJAVA_THIRDPARTY_REPOSITORY;; --alt-repository=*|--alt-deployment-repository=*) ALT_REPOSITORY="${1#--*=}";; --skip-gpg) SKIP_GPG=t EXTRA_ARGS="$EXTRA_ARGS -Dgpg.skip=true";; --help) USAGE=t break;; -*) echo "Unknown option: $1" >&2 USAGE=t break;; *) test -z "$VERSION" || { echo "Extraneous argument: $1" >&2 USAGE=t break } VERSION=$1;; esac shift done test "$USAGE" && die "Usage: $0 [options] [] Where is the version to release. If omitted, it will prompt you. Options include: --dry-run - Simulate the release without actually doing it. --skip-version-check - Skips the SemVer and parent pom version checks. --skip-branch-check - Skips the default branch check. --skip-license-update - Skips update of the copyright blurbs. --skip-push - Do not push to the remote git repository. --dev-version= - Specify next development version explicitly; e.g.: if you release 2.0.0-beta-1, by default Maven will set the next development version at 2.0.0-beta-2-SNAPSHOT, but maybe you want to set it to 2.0.0-SNAPSHOT instead. --alt-repository= - Deploy release to a different remote repository. --skip-gpg - Do not perform GPG signing of artifacts. " # -- Extract project details -- # Note: For multi-module projects, this will be re-extracted after module selection debug "Extracting project details" echoArg='${project.version}:${license.licenseName}:${project.parent.groupId}:${project.parent.artifactId}:${project.parent.version}' projectDetails=$(mvn -B -N -Dexec.executable=echo -Dexec.args="$echoArg" exec:exec -q) test $? -eq 0 || projectDetails=$(mvn -B -U -N -Dexec.executable=echo -Dexec.args="$echoArg" exec:exec -q) test $? -eq 0 || die "Could not extract version from pom.xml. Error follows:\n$projectDetails" printf '%s' "$projectDetails\n" | grep -Fqv '[ERROR]' || die "Error extracting version from pom.xml. Error follows:\n$projectDetails" # HACK: Even with -B, some versions of mvn taint the output with the [0m # color reset sequence. So we forcibly remove such sequences, just to be safe. projectDetails=$(printf '%s' "$projectDetails" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g") # And also remove extraneous newlines, particularly any trailing ones. projectDetails=$(printf '%s' "$projectDetails" | tr -d '\n') currentVersion=${projectDetails%%:*} projectDetails=${projectDetails#*:} licenseName=${projectDetails%%:*} parentGAV=${projectDetails#*:} # -- Sanity checks -- debug "Performing sanity checks" # Check that we have push rights to the repository. if [ ! "$SKIP_PUSH" ] then debug "Checking repository push rights" push=$(git remote -v | grep origin | grep '(push)') test "$push" || die 'No push URL found for remote origin. Please use "git remote -v" to double check your remote settings.' echo "$push" | grep -q 'git:/' && die 'Remote origin is read-only. Please use "git remote set-url origin ..." to change it.' fi # Discern the version to release. debug "Gleaning release version" pomVersion=${currentVersion%-SNAPSHOT} test "$VERSION" -o ! -t 0 || { printf 'Version? [%s]: ' "$pomVersion" read VERSION test "$VERSION" || VERSION=$pomVersion } # Check that the release version number starts with a digit. test "$VERSION" || die 'Please specify the version to release!' test "$SKIP_VERSION_CHECK" || { case "$VERSION" in [0-9]*) ;; *) die "Version '$VERSION' does not start with a digit! If you are sure, try again with --skip-version-check flag." esac } # Check that the release version number conforms to SemVer. VALID_SEMVER_BUMP="$(cd "$(dirname "$0")" && pwd)/valid-semver-bump.sh" test -f "$VALID_SEMVER_BUMP" || die "Missing helper script at '$VALID_SEMVER_BUMP' Do you have a full clone of https://github.com/scijava/scijava-scripts?" test "$SKIP_VERSION_CHECK" || { debug "Checking conformance to SemVer" sh -$- "$VALID_SEMVER_BUMP" "$pomVersion" "$VERSION" || die "If you are sure, try again with --skip-version-check flag." } # Check that the project extends the latest version of pom-scijava. test "$SKIP_VERSION_CHECK" -o "$parentGAV" != "${parentGAV#$}" || { debug "Checking pom-scijava parent version" psjMavenMetadata=https://repo1.maven.org/maven2/org/scijava/pom-scijava/maven-metadata.xml latestParentVersion=$(curl -fsL "$psjMavenMetadata" | grep '' | sed 's;.*>\([^<]*\)<.*;\1;') currentParentVersion=${parentGAV##*:} test "$currentParentVersion" = "$latestParentVersion" || die "Newer version of parent '$parentGAV' is available: $latestParentVersion. I recommend you update it before releasing. Or if you know better, try again with --skip-version-check flag." } # Check that the working copy is clean. debug "Checking if working copy is clean" no_changes_pending || die 'There are uncommitted changes!' test -z "$(git ls-files -o --exclude-standard)" || die 'There are untracked files! Please stash them before releasing.' # Discern default branch. debug "Discerning default branch" currentBranch=$(git rev-parse --abbrev-ref --symbolic-full-name HEAD) upstreamBranch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u}) remote=${upstreamBranch%/*} defaultBranch=$(git remote show "$remote" | grep "HEAD branch" | sed 's/.*: //') # Check that we are on the main branch. test "$SKIP_BRANCH_CHECK" || { debug "Checking current branch" test "$currentBranch" = "$defaultBranch" || die "Non-default branch: $currentBranch. If you are certain you want to release from this branch, try again with --skip-branch-check flag." } # -- Detect multi-module structure -- debug "Detecting project structure" IS_AGGREGATOR= MODULE_NAME= if is_aggregator then IS_AGGREGATOR=t debug "Multi-module aggregator detected" # Prompt for which module to release modules=$(get_modules) echo "Available modules:" echo "$modules" | sed 's/^/ /' printf 'Which module to release?: ' read MODULE_NAME test "$MODULE_NAME" || die 'Module name is required for aggregator releases' # Validate module exists test -d "$MODULE_NAME" || die "Module directory '$MODULE_NAME' not found" echo "$modules" | grep -qx "$MODULE_NAME" || die "Module '$MODULE_NAME' not found in aggregator POM" # Re-extract project details from the selected module debug "Extracting module project details" projectDetails=$(mvn -B -N -f "$MODULE_NAME/pom.xml" -Dexec.executable=echo -Dexec.args="$echoArg" exec:exec -q) test $? -eq 0 || projectDetails=$(mvn -B -U -N -f "$MODULE_NAME/pom.xml" -Dexec.executable=echo -Dexec.args="$echoArg" exec:exec -q) test $? -eq 0 || die "Could not extract version from $MODULE_NAME/pom.xml. Error follows:\n$projectDetails" projectDetails=$(printf '%s' "$projectDetails" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g") projectDetails=$(printf '%s' "$projectDetails" | tr -d '\n') currentVersion=${projectDetails%%:*} projectDetails=${projectDetails#*:} licenseName=${projectDetails%%:*} parentGAV=${projectDetails#*:} fi # If REMOTE is unset, use branch's upstream remote by default. REMOTE="${REMOTE:-$remote}" # Check that the main branch isn't behind the upstream branch. debug "Ensuring local branch is up-to-date" HEAD="$(git rev-parse HEAD)" && git fetch "$REMOTE" "$defaultBranch" && FETCH_HEAD="$(git rev-parse FETCH_HEAD)" && test "$FETCH_HEAD" = HEAD || test "$FETCH_HEAD" = "$(git merge-base $FETCH_HEAD $HEAD)" || die "'$defaultBranch' is not up-to-date" # Check for release-only files committed to the main branch. debug "Checking for spurious release-only files" for release_file in release.properties pom.xml.releaseBackup do if [ -e "$release_file" ] then echo "==========================================================================" echo "NOTE: $release_file was committed to source control. Removing now." echo "==========================================================================" git rm -rf "$release_file" && git commit "$release_file" \ -m 'Remove $release_file' \ -m 'It should only exist on release tags.' fi done # Ensure that schema location URL uses HTTPS, not HTTP. debug "Checking that schema location URL uses HTTPS" if grep -q http://maven.apache.org/xsd/maven-4.0.0.xsd pom.xml >/dev/null 2>/dev/null then echo "=====================================================================" echo "NOTE: Your POM's schema location uses HTTP, not HTTPS. Fixing it now." echo "=====================================================================" sed 's;http://maven.apache.org/xsd/maven-4.0.0.xsd;https://maven.apache.org/xsd/maven-4.0.0.xsd;' pom.xml > pom.new && mv -f pom.new pom.xml && git commit pom.xml -m 'POM: use HTTPS for schema location URL' \ -m 'Maven no longer supports plain HTTP for the schema location.' \ -m 'And using HTTP now generates errors in Eclipse (and probably other IDEs).' fi # Check project xmlns, xmlns:xsi, and xsi:schemaLocation attributes. debug "Checking correctness of POM project XML attributes" grep -qF 'xmlns="http://maven.apache.org/POM/4.0.0"' pom.xml >/dev/null 2>/dev/null && grep -qF 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' pom.xml >/dev/null 2>/dev/null && grep -qF 'xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 ' pom.xml >/dev/null 2>/dev/null || { echo "=====================================================================" echo "NOTE: Your POM's project attributes are incorrect. Fixing it now." echo "=====================================================================" sed 's;xmlns="[^"]*";xmlns="http://maven.apache.org/POM/4.0.0";' pom.xml > pom.new && mv -f pom.new pom.xml && sed 's;xmlns:xsi="[^"]*";xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";' pom.xml > pom.new && mv -f pom.new pom.xml && sed 's;xsi:schemaLocation="[^"]*";xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd";' pom.xml > pom.new && mv -f pom.new pom.xml && git commit pom.xml -m 'POM: fix project attributes' \ -m 'The XML schema for Maven POMs is located at:' \ -m ' https://maven.apache.org/xsd/maven-4.0.0.xsd' \ -m 'Its XML namespace is the string:' \ -m ' http://maven.apache.org/POM/4.0.0' \ -m 'So that exact string must be the value of xmlns. It must also match the first half of xsi:schemaLocation, which maps that namespace to an actual URL online where the schema resides. Otherwise, the document is not a Maven POM.' \ -m 'Similarly, the xmlns:xsi attribute of an XML document declaring a particular schema should always use the string identifier:' \ -m ' http://www.w3.org/2001/XMLSchema-instance' \ -m "because that's the namespace identifier for instances of an XML schema." \ -m "For details, see the specification at: https://www.w3.org/TR/xmlschema-1/" } # Change forum references from forum.image.net to forum.image.sc. debug "Checking correctness of forum URL references" if grep -q 'https*://forum.imagej.net' pom.xml >/dev/null 2>/dev/null then echo "================================================================" echo "NOTE: Your POM still references forum.imagej.net. Fixing it now." echo "================================================================" sed 's;https*://forum.imagej.net;https://forum.image.sc;g' pom.xml > pom.new && mv -f pom.new pom.xml && git commit pom.xml \ -m 'POM: fix forum.image.sc tag link' \ -m 'The Discourse software updated the tags path from /tags/ to /tag/.' fi # Ensure that references to forum.image.sc use /tag/, not /tags/. debug "Checking correctness of forum tag references" if grep -q forum.image.sc/tags/ pom.xml >/dev/null 2>/dev/null then echo "==================================================================" echo "NOTE: Your POM has an old-style forum.image.sc tag. Fixing it now." echo "==================================================================" sed 's;forum.image.sc/tags/;forum.image.sc/tag/;g' pom.xml > pom.new && mv -f pom.new pom.xml && git commit pom.xml \ -m 'POM: fix forum.image.sc tag link' \ -m 'The Discourse software updated the tags path from /tags/ to /tag/.' fi # For multi-module aggregators, prepare the release environment. if test "$IS_AGGREGATOR" then debug "Preparing multi-module release for $MODULE_NAME" # Filter aggregator POM to only include this module $DRY_RUN awk -v module="$MODULE_NAME" ' //,/<\/modules>/ { if ($0 ~ //) { if ($0 ~ module) { print } else { gsub(//, "") print } next } } { print } ' pom.xml > pom.xml.filtered $DRY_RUN mv pom.xml.filtered pom.xml # Resolve snapshot dependencies resolve_snapshot_deps "$MODULE_NAME/pom.xml" # Commit these preparation changes (Commit O) $DRY_RUN git add pom.xml "$MODULE_NAME/pom.xml" $DRY_RUN git commit -m "Prepare $MODULE_NAME for release: filter aggregator and resolve snapshot dependencies" fi # Ensure license headers are up-to-date. test "$SKIP_LICENSE_UPDATE" -o -z "$licenseName" -o "$licenseName" = "N/A" || { debug "Ensuring that license headers are up-to-date" mvn license:update-project-license license:update-file-header && git add LICENSE.txt || die 'Failed to update copyright blurbs. You can skip the license update using the --skip-license-update flag.' no_changes_pending || die 'Copyright blurbs needed an update -- commit changes and try again. Or if the license headers are being added erroneously to certain files, exclude them by setting license.excludes in your POM; e.g.: **/script_templates/** Alternately, try again with the --skip-license-update flag.' } # -- Set up multi-module arguments if needed -- MAVEN_PL_ARGS= if test "$IS_AGGREGATOR" then debug "Configuring release:prepare for multi-module" MAVEN_PL_ARGS="-pl $MODULE_NAME -DautoVersionSubmodules=false" # Override tag name to include module name if test -z "$TAG" then TAG="-Dtag=${MODULE_NAME}-${VERSION}" fi fi # Prepare new release without pushing (requires the release plugin >= 2.1). debug "Preparing new release" $DRY_RUN mvn $BATCH_MODE release:prepare -DpushChanges=false -Dresume=false $TAG \ $PROFILE $DEV_VERSION -DreleaseVersion="$VERSION" $MAVEN_PL_ARGS \ "-Darguments=-Dgpg.skip=true ${EXTRA_ARGS# }" || die 'The release preparation step failed -- look above for errors and fix them. Use "mvn javadoc:javadoc | grep error" to check for javadoc syntax errors.' # Squash the maven-release-plugin's two commits into one. if test -z "$DRY_RUN" then debug "Squashing release commits" test "[maven-release-plugin] prepare for next development iteration" = \ "$(git show -s --format=%s HEAD)" || die "maven-release-plugin's commits are unexpectedly missing!" fi if test "$IS_AGGREGATOR" then # For multi-module: revert the prep commit (O), then squash O+A+B+R debug "Reverting preparation commit and squashing" # Current state: ...prev → O → A → B # Revert O to produce R $DRY_RUN git revert --no-edit HEAD~2 && # Update inter-module dependencies to use the next snapshot version # (The revert brought back old snapshot deps, but other modules should use the next snapshot) if test -z "$DRY_RUN" then next_version=$(grep '' "$MODULE_NAME/pom.xml" | head -1 | sed 's/.*\(.*\)<\/version>.*/\1/') update_inter_module_deps "$MODULE_NAME" "$next_version" fi && # Now: ...prev → O → A → B → R (+ inter-module dep updates) # Squash the last 4 commits (plus any inter-module dep updates) $DRY_RUN git reset --soft HEAD~4 && # Net changes staged: module version bump + inter-module deps updated to next snapshot if ! git diff-index --cached --quiet --ignore-submodules HEAD -- then if test -z "$DRY_RUN" then next_version=$(grep '' "$MODULE_NAME/pom.xml" | head -1 | sed 's/.*\(.*\)<\/version>.*/\1/') else next_version="" fi $DRY_RUN git commit -s -m "Bump to next development cycle $MODULE_NAME: $VERSION → $next_version" fi else # Original single-module logic: squash A+B $DRY_RUN git reset --soft HEAD^^ && if ! git diff-index --cached --quiet --ignore-submodules HEAD -- then $DRY_RUN git commit -s -m "Bump to next development cycle" fi fi && # Extract the name of the new tag. debug "Extracting new tag name" if test -z "$DRY_RUN" then tag=$(sed -n 's/^scm.tag=//p' < release.properties) else tag="" fi && # Rewrite the tag to include release.properties (and filtered aggregator for multi-module). debug "Rewriting tag to include release.properties" test -n "$tag" && # HACK: SciJava projects use SSH ([email protected]:...) for developerConnection. # The release:perform command wants to use the developerConnection URL when # checking out the release tag. But reading from this URL requires credentials # which the CI system typically does not have. So we replace the scm.url in # the release.properties file to use the public (https://github.com/...) URL. # This is OK, since release:perform does not need write access to the repo. $DRY_RUN sed -i.bak -e 's|^scm.url=scm\\:git\\:[email protected]\\:|scm.url=scm\\:git\\:https\\://github.com/|' release.properties && # For multi-module: add -pl to arguments so release:perform only builds the released module if test "$IS_AGGREGATOR" then debug "Adding -pl $MODULE_NAME to release.properties arguments" # Extract existing arguments, append -pl, write back existing_args=$(sed -n 's/^exec.additionalArguments=//p' release.properties) if test -n "$existing_args" then # Append to existing args $DRY_RUN sed -i.bak2 "s|^exec.additionalArguments=.*|exec.additionalArguments=$existing_args -pl $MODULE_NAME|" release.properties else # Add new line echo "exec.additionalArguments=-pl $MODULE_NAME" >> release.properties fi test -f release.properties.bak2 && $DRY_RUN rm release.properties.bak2 fi && $DRY_RUN rm release.properties.bak && $DRY_RUN git checkout "$tag" && if test "$IS_AGGREGATOR" then # For multi-module: get the filtered aggregator from the parent commit (O) debug "Incorporating filtered aggregator into tag" $DRY_RUN git checkout HEAD~1 -- pom.xml && $DRY_RUN git add -f release.properties pom.xml else # Original: just add release.properties $DRY_RUN git add -f release.properties fi && $DRY_RUN git commit --amend --no-edit && $DRY_RUN git tag -d "$tag" && $DRY_RUN git tag "$tag" HEAD && $DRY_RUN git checkout @{-1} && # Push the current branch and the tag. if test -z "$SKIP_PUSH" then debug "Pushing changes" $DRY_RUN git push "$REMOTE" HEAD $tag fi # Remove files generated by the release process. They can end up # committed to the mainline branch and hosing up later releases. debug "Cleaning up" $DRY_RUN rm -f release.properties pom.xml.releaseBackup debug "Release complete!"