#!/bin/bash set -eu # we don't want "pipefail" to implement idempotency ### # # Publishes all maven modules in the specified directory to maven. # # Usage: ./publib-maven [DIR] # # DIR is the root directory of a local maven repository (default `dist/java`). # # MAVEN_STAGING_PROFILE_ID - Maven Central (sonatype) staging profile ID (e.g. 68a05363083174) # MAVEN_USERNAME - User name for Sonatype # MAVEN_PASSWORD - Password for Sonatype # MAVEN_GPG_PRIVATE_KEY - GPG private key where newlines are encoded as "\n" # MAVEN_GPG_PRIVATE_KEY_FILE - GPG private key file (mutually exclusive with MAVEN_GPG_PRIVATE_KEY) # MAVEN_GPG_PRIVATE_KEY_PASSPHRASE - The passphrase of the provided key. # MAVEN_DRYRUN - Set to "true" for a dry run # MAVEN_SERVER_ID - Used in maven settings for credential lookup # MAVEN_REPOSITORY_URL - Deployment repository when not deploying to maven central # ### ### # # HOW TO CREATE A GPG KEY? # # gpg --full-generate-key (use RSA, 4096, passphrase) # # Export and publish the public key: # 1. gpg -a --export > public.pem # 2. Go to https://keyserver.ubuntu.com/ and submit the public key # # Export and use the private key: # 1. gpg -a --export-secret-keys > private.pem # 2. Set MAVEN_GPG_PRIVATE_KEY_FILE =t point to `private.pem` # or # 3. You can also export the private key to a single line where newlines are encoded # as "\n" and then assign it to MAVEN_GPG_PRIVATE_KEY. # MAVEN_GPG_PRIVATE_KEY="$(echo $(cat -e private.pem) | sed 's/\$ /\\n/g' | sed 's/\$$//')" # ### readonly CENTRAL_SERVER="ossrh" # ------------------------------------------------------------------------------------------------- # we need this monstrosity because this script is installed as a symlink to node_modules/.bin # and we need a way to find it's real location so we can reference stuff that's not in .bin # https://stackoverflow.com/a/246128 SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" SOURCE="$(readlink "$SOURCE")" [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located done scriptdir="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" # ------------------------------------------------------------------------------------------------- error() { echo "❌ $@"; exit 1; } validate_central_parameters() { [[ -z "${MAVEN_STAGING_PROFILE_ID:-}" ]] && error "MAVEN_STAGING_PROFILE_ID is required" return 0 } validate_non_central_parameters() { [[ -z "${MAVEN_SERVER_ID:-}" ]] && error "MAVEN_SERVER_ID is required" [[ -z "${MAVEN_REPOSITORY_URL:-}" ]] && error "MAVEN_REPOSITORY_URL is required" return 0 } validate_signing_parameters() { [[ -z "${MAVEN_GPG_PRIVATE_KEY_PASSPHRASE:-}" ]] && error "MAVEN_GPG_PRIVATE_KEY_PASSPHRASE is required" [[ -z "${MAVEN_GPG_PRIVATE_KEY_FILE:-}" ]] && [[ -z "${MAVEN_GPG_PRIVATE_KEY:-}" ]] && error "MAVEN_GPG_PRIVATE_KEY_FILE or MAVEN_GPG_PRIVATE_KEY is required" [[ -n "${MAVEN_GPG_PRIVATE_KEY:-}" ]] && [[ -n "${MAVEN_GPG_PRIVATE_KEY_FILE:-}" ]] && error "Cannot specify both MAVEN_GPG_PRIVATE_KEY and MAVEN_GPG_PRIVATE_KEY_FILE" return 0 } validate_parameters() { [[ -z "${MAVEN_USERNAME:-}" ]] && error "MAVEN_USERNAME is required" [[ -z "${MAVEN_PASSWORD:-}" ]] && error "MAVEN_PASSWORD is required" if ${is_central}; then validate_central_parameters else validate_non_central_parameters fi if (${is_signed} && ! ${is_central}); then error "Package signing is currently only supported for Maven Central" fi if (${is_signed} || ${is_central}); then validate_signing_parameters fi } import_gpg_key() { #--------------------------------------------------------------------------------------------------------------------------------------- # Import private GPG key #--------------------------------------------------------------------------------------------------------------------------------------- echo "Importing GPG key..." >&2 # GnuPG will occasionally bail out with "gpg: failed: Inappropriate ioctl for device", the following attempts to fix export GNUPGHOME=$(mktemp -d) export GPG_TTY=$(tty) if [ -n "${MAVEN_GPG_PRIVATE_KEY:-}" ]; then MAVEN_GPG_PRIVATE_KEY_FILE="${GNUPGHOME}/private.pem" echo -e "${MAVEN_GPG_PRIVATE_KEY}" > ${MAVEN_GPG_PRIVATE_KEY_FILE} fi gpg --allow-secret-key-import --batch --yes --no-tty --import "${MAVEN_GPG_PRIVATE_KEY_FILE}" || { error "GPG key import failed" } gpg_key_id=$(gpg --list-keys --with-colons | grep pub | cut -d: -f5) echo "gpg_key_id=${gpg_key_id}" } create_maven_settings() { # Create a settings.xml file with the user+password for maven mvn_settings="${workdir}/mvn-settings.xml" if ${is_signed}; then cat > ${mvn_settings} <<-EOF ${server_id} \${env.MAVEN_USERNAME} \${env.MAVEN_PASSWORD} gpg.passphrase \${env.MAVEN_GPG_PRIVATE_KEY_PASSPHRASE} EOF else cat > ${mvn_settings} <<-EOF ${server_id} \${env.MAVEN_USERNAME} \${env.MAVEN_PASSWORD} EOF fi } sign_artifacts() { echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo " Preparing repository" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" # on a mac, --pinentry-mode to "loopback" are required and I couldn't find a # way to do so via -Dgpg.gpgArguments or the settings file, so here we are. local gpg_exec="gpg" if [[ "$(uname)" == "Darwin" ]]; then gpg_exec="/tmp/publib-gpg.sh" echo "#!/bin/bash" > $gpg_exec echo "exec gpg --pinentry-mode loopback \"\$@\"" >> $gpg_exec chmod +x $gpg_exec fi # Sign and stage our artifacts into a local directory for pom in ${poms}; do $mvn --settings=${mvn_settings} gpg:sign-and-deploy-file \ -X \ -Durl=file://${staging} \ -DrepositoryId=${server_id} \ -Dgpg.homedir=${GNUPGHOME} \ -Dgpg.keyname=0x${gpg_key_id} \ -Dgpg.executable=${gpg_exec} \ -DpomFile=${pom} \ -Dfile=${pom/.pom/.jar} \ -Dsources=${pom/.pom/-sources.jar} \ -Djavadoc=${pom/.pom/-javadoc.jar} done } deploy_central() { echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo " Deploying and closing repository..." echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" staging_output="${workdir}/deploy-output.txt" $mvn --settings=${mvn_settings} \ org.sonatype.plugins:nexus-staging-maven-plugin:1.6.13:deploy-staged-repository \ -DrepositoryDirectory=${staging} \ -DnexusUrl=${MAVEN_ENDPOINT:-https://oss.sonatype.org} \ -DserverId=${server_id} \ -DautoReleaseAfterClose=true \ -DstagingProgressTimeoutMinutes=30 \ -DstagingProfileId=${MAVEN_STAGING_PROFILE_ID} | tee ${staging_output} # we need to consule PIPESTATUS sinec "tee" is the last command if [ ${PIPESTATUS[0]} -ne 0 ]; then error "Repository deployment failed" fi echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo " Releasing repository" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" # Extract the ID of the closed repository from the log output of "deploy-staged-repository" # This is because "deploy-staged-repository" doesn't seem to support autoReleaseAfterClose # See https://issues.sonatype.org/browse/OSSRH-42487 if $dry_run; then echo 'Closing staging repository with ID "dummyrepo"' > ${staging_output} fi repository_id="$(cat ${staging_output} | grep "Closing staging repository with ID" | cut -d'"' -f2)" if [ -z "${repository_id}" ]; then echo "❌ Unable to extract repository ID from deploy-staged-repository output." echo "This means it failed to close or there was an unexpected problem." echo "At any rate, we can't release it. Sorry" exit 1 fi echo "Repository ID: ${repository_id}" # Create a dummy pom.xml because the "release" goal needs one, but it doesn't care about it at all release_pom="${workdir}/release-pom.xml" cat > ${release_pom} < 4.0.0 dummy dummy 0.0.0 HERE # Release! release_output="${workdir}/release-output.txt" $mvn --settings ${mvn_settings} -f ${release_pom} \ org.sonatype.plugins:nexus-staging-maven-plugin:1.6.5:release \ -DserverId=${server_id} \ -DnexusUrl=${MAVEN_ENDPOINT:-https://oss.sonatype.org} \ -DstagingProfileId=${MAVEN_STAGING_PROFILE_ID} \ -DstagingProgressTimeoutMinutes=30 \ -DstagingRepositoryId=${repository_id} | tee ${release_output} # If release failed, check if this was caused because we are trying to publish # the same version again, which is not an error. The magic string "does not # allow updating artifact" for a ".pom" file indicates that we are trying to # override an existing version. Otherwise, fail! if [ ${PIPESTATUS[0]} -ne 0 ]; then if cat ${release_output} | grep "does not allow updating artifact" | grep -q ".pom"; then echo "⚠️ Artifact already published. Skipping" else error "Release failed" fi fi } deploy_non_central() { release_output="${workdir}/release-output.txt" for pom in ${poms}; do $mvn --settings=${mvn_settings} deploy:deploy-file \ -Durl=${MAVEN_REPOSITORY_URL} \ -DrepositoryId=${server_id} \ -Dfile=${pom/.pom/.jar} \ -DpomFile=${pom} \ -Dsources=${pom/.pom/-sources.jar} \ -Djavadoc=${pom/.pom/-javadoc.jar} | tee ${release_output} # If release failed, check if this was caused because we are trying to publish # the same version again, which is not an error. The magic string "409 Conflict" # indicates that we are trying to # override an existing version. Otherwise, fail! if [ ${PIPESTATUS[0]} -ne 0 ]; then if cat ${release_output} | grep -q "409 Conflict"; then echo "⚠️ Artifact already published. Skipping" else error "Release failed" fi fi done } main() { cd "${1:-"dist/java"}" server_id="${MAVEN_SERVER_ID:-"${CENTRAL_SERVER}"}" if [[ "${server_id}" == "${CENTRAL_SERVER}" ]]; then is_central=true else is_central=false fi if [[ -n "${MAVEN_GPG_PRIVATE_KEY_FILE:-}" ]] || [[ -n "${MAVEN_GPG_PRIVATE_KEY:-}" ]]; then is_signed=true else is_signed=false fi validate_parameters if [[ -n "${MAVEN_DRYRUN:-}" ]]; then echo "===========================================" echo " 🏜️ DRY-RUN MODE 🏜️" echo "===========================================" mvn="echo mvn" dry_run=true else mvn=mvn dry_run=false fi if ${is_signed}; then import_gpg_key fi poms="$(find . -name '*.pom')" if [ -z "${poms}" ]; then error "No JARS to publish: no .pom files found under $PWD" fi if ${is_central}; then echo "📦 Publishing to Maven Central" else echo "📦 Publishing to ${server_id}" fi staging=$(mktemp -d) workdir=$(mktemp -d) echo ${workdir} create_maven_settings if ${is_signed}; then # This currently only works with Maven Central sign_artifacts fi if ${is_central}; then deploy_central else deploy_non_central fi echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "✅ All Done!" } main "$@"; exit