From 48e8bfbd944a22e6ee124d22415bca781e555ca1 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 3 Sep 2019 23:00:10 -0700 Subject: [PATCH 01/19] Update settings for sbt-sonatype 3.0 --- .scalafmt.conf | 3 +- build.sbt | 29 +- project/build.properties | 3 +- project/plugins.sbt | 3 +- sbt | 215 +++-- src/main/scala/xerial/sbt/NexusClient.scala | 607 +++++++++++++ src/main/scala/xerial/sbt/Sonatype.scala | 855 ++++-------------- src/sbt-test/sbt-sonatype/example/build.sbt | 2 + .../sbt-sonatype/operations/build.sbt | 2 + version.sbt | 2 +- 10 files changed, 921 insertions(+), 800 deletions(-) create mode 100644 src/main/scala/xerial/sbt/NexusClient.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index bda502a5..965c7e9c 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,4 @@ -maxColumn = 180 +version = 2.0.1 +maxColumn = 120 style = defaultWithAlign optIn.breaksInsideChains = true diff --git a/build.sbt b/build.sbt index 6739d7d5..72616e4c 100755 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ import ReleaseTransformations._ -lazy val buildSettings = Seq( +lazy val buildSettings: Seq[Setting[_]] = Seq( organization := "org.xerial.sbt", organizationName := "Xerial project", organizationHomepage := Some(new URL("http://xerial.org/")), @@ -29,7 +29,7 @@ lazy val buildSettings = Seq( scriptedLaunchOpts := { scriptedLaunchOpts.value ++ Seq("-Xmx1024M", "-XX:MaxPermSize=256M", "-Dplugin.version=" + version.value) }, - crossSbtVersions := Vector("1.2.7"), + crossSbtVersions := Vector("1.2.8"), releaseCrossBuild := true, releaseTagName := { (version in ThisBuild).value }, releasePublishArtifactsAction := PgpKeys.publishSigned.value, @@ -49,15 +49,20 @@ lazy val buildSettings = Seq( ) ) +val AIRFRAME_VERSION = "19.9.2" + // Project modules -lazy val sbtSonatype = Project( - id = "sbt-sonatype", - base = file(".") -).enablePlugins(ScriptedPlugin) - .settings(buildSettings) - .settings( - libraryDependencies ++= Seq( - "org.apache.httpcomponents" % "httpclient" % "4.2.6", - "org.scalatest" %% "scalatest" % "3.0.1" % "test" +lazy val sbtSonatype = + project + .withId("sbt-sonatype") + .in(file(".")) + .enablePlugins(ScriptedPlugin) + .settings( + buildSettings, + testFrameworks += new TestFramework("wvlet.airspec.Framework"), + libraryDependencies ++= Seq( + "org.apache.httpcomponents" % "httpclient" % "4.2.6", + "org.wvlet.airframe" %% "airframe-http-finagle" % AIRFRAME_VERSION, + "org.wvlet.airframe" %% "airspec" % AIRFRAME_VERSION % "test" + ) ) - ) diff --git a/project/build.properties b/project/build.properties index ee0a5b57..c8997c4a 100755 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1 @@ -sbt.version=1.2.7 - +sbt.version=1.3.0-RC5 diff --git a/project/plugins.sbt b/project/plugins.sbt index 8d613e8d..a3ac1d7e 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,7 @@ addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.1.0-M7") -addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value diff --git a/sbt b/sbt index f0b5bddd..c7966f81 100755 --- a/sbt +++ b/sbt @@ -2,32 +2,34 @@ # # A more capable sbt runner, coincidentally also called sbt. # Author: Paul Phillips +# https://github.com/paulp/sbt-extras set -o pipefail -declare -r sbt_release_version="0.13.15" -declare -r sbt_unreleased_version="0.13.15" +declare -r sbt_release_version="1.2.8" +declare -r sbt_unreleased_version="1.3.0-RC1" -declare -r latest_212="2.12.1" -declare -r latest_211="2.11.11" -declare -r latest_210="2.10.6" +declare -r latest_213="2.13.0" +declare -r latest_212="2.12.9" +declare -r latest_211="2.11.12" +declare -r latest_210="2.10.7" declare -r latest_29="2.9.3" declare -r latest_28="2.8.2" declare -r buildProps="project/build.properties" -declare -r sbt_launch_ivy_release_repo="http://repo.typesafe.com/typesafe/ivy-releases" +declare -r sbt_launch_ivy_release_repo="https://repo.typesafe.com/typesafe/ivy-releases" declare -r sbt_launch_ivy_snapshot_repo="https://repo.scala-sbt.org/scalasbt/ivy-snapshots" -declare -r sbt_launch_mvn_release_repo="http://repo.scala-sbt.org/scalasbt/maven-releases" -declare -r sbt_launch_mvn_snapshot_repo="http://repo.scala-sbt.org/scalasbt/maven-snapshots" +declare -r sbt_launch_mvn_release_repo="https://repo.scala-sbt.org/scalasbt/maven-releases" +declare -r sbt_launch_mvn_snapshot_repo="https://repo.scala-sbt.org/scalasbt/maven-snapshots" -declare -r default_jvm_opts_common="-Xms512m -Xmx1536m -Xss2m" +declare -r default_jvm_opts_common="-Xms512m -Xss2m" declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" declare sbt_jar sbt_dir sbt_create sbt_version sbt_script sbt_new declare sbt_explicit_version declare verbose noshare batch trace_level -declare sbt_saved_stty debugUs +declare debugUs declare java_cmd="java" declare sbt_launch_dir="$HOME/.sbt/launchers" @@ -41,21 +43,24 @@ declare -a extra_jvm_opts extra_sbt_opts echoerr () { echo >&2 "$@"; } vlog () { [[ -n "$verbose" ]] && echoerr "$@"; } -die () { echo "Aborting: $@" ; exit 1; } - -# restore stty settings (echo in particular) -onSbtRunnerExit() { - [[ -n "$sbt_saved_stty" ]] || return - vlog "" - vlog "restoring stty: $sbt_saved_stty" - stty "$sbt_saved_stty" - unset sbt_saved_stty -} +die () { echo "Aborting: $*" ; exit 1; } -# save stty and trap exit, to ensure echo is re-enabled if we are interrupted. -trap onSbtRunnerExit EXIT -sbt_saved_stty="$(stty -g 2>/dev/null)" -vlog "Saved stty: $sbt_saved_stty" +setTrapExit () { + # save stty and trap exit, to ensure echo is re-enabled if we are interrupted. + SBT_STTY="$(stty -g 2>/dev/null)" + export SBT_STTY + + # restore stty settings (echo in particular) + onSbtRunnerExit() { + [ -t 0 ] || return + vlog "" + vlog "restoring stty: $SBT_STTY" + stty "$SBT_STTY" + } + + vlog "saving stty: $SBT_STTY" + trap onSbtRunnerExit EXIT +} # this seems to cover the bases on OSX, and someone will # have to tell me about the others. @@ -63,7 +68,7 @@ get_script_path () { local path="$1" [[ -L "$path" ]] || { echo "$path" ; return; } - local target="$(readlink "$path")" + local -r target="$(readlink "$path")" if [[ "${target:0:1}" == "/" ]]; then echo "$target" else @@ -71,8 +76,10 @@ get_script_path () { fi } -declare -r script_path="$(get_script_path "$BASH_SOURCE")" -declare -r script_name="${script_path##*/}" +script_path="$(get_script_path "${BASH_SOURCE[0]}")" +declare -r script_path +script_name="${script_path##*/}" +declare -r script_name init_default_option_file () { local overriding_var="${!1}" @@ -86,29 +93,14 @@ init_default_option_file () { echo "$default_file" } -declare sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" -declare jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" +sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" +jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" build_props_sbt () { [[ -r "$buildProps" ]] && \ grep '^sbt\.version' "$buildProps" | tr '=\r' ' ' | awk '{ print $2; }' } -update_build_props_sbt () { - local ver="$1" - local old="$(build_props_sbt)" - - [[ -r "$buildProps" ]] && [[ "$ver" != "$old" ]] && { - perl -pi -e "s/^sbt\.version\b.*\$/sbt.version=${ver}/" "$buildProps" - grep -q '^sbt.version[ =]' "$buildProps" || printf "\nsbt.version=%s\n" "$ver" >> "$buildProps" - - vlog "!!!" - vlog "!!! Updated file $buildProps setting sbt.version to: $ver" - vlog "!!! Previous value was: $old" - vlog "!!!" - } -} - set_sbt_version () { sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version @@ -119,7 +111,7 @@ url_base () { local version="$1" case "$version" in - 0.7.*) echo "http://simple-build-tool.googlecode.com" ;; + 0.7.*) echo "https://simple-build-tool.googlecode.com" ;; 0.10.* ) echo "$sbt_launch_ivy_release_repo" ;; 0.11.[12]) echo "$sbt_launch_ivy_release_repo" ;; 0.*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" @@ -141,7 +133,7 @@ make_url () { 0.10.* ) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 0.11.[12]) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 0.*) echo "$base/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; - *) echo "$base/org/scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; + *) echo "$base/org/scala-sbt/sbt-launch/$version/sbt-launch-${version}.jar" ;; esac } @@ -153,9 +145,9 @@ addResidual () { vlog "[residual] arg = '$1'" ; residual_args+=("$1"); } addResolver () { addSbt "set resolvers += $1"; } addDebugger () { addJava "-Xdebug" ; addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"; } setThisBuild () { - vlog "[addBuild] args = '$@'" + vlog "[addBuild] args = '$*'" local key="$1" && shift - addSbt "set $key in ThisBuild := $@" + addSbt "set $key in ThisBuild := $*" } setScalaVersion () { [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' @@ -169,7 +161,19 @@ setJavaHome () { export PATH="$JAVA_HOME/bin:$PATH" } -getJavaVersion() { "$1" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d \"; } +getJavaVersion() { + local -r str=$("$1" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d '"') + + # java -version on java8 says 1.8.x + # but on 9 and 10 it's 9.x.y and 10.x.y. + if [[ "$str" =~ ^1\.([0-9]+)\..*$ ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ "$str" =~ ^([0-9]+)\..*$ ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ -n "$str" ]]; then + echoerr "Can't parse java version from: $str" + fi +} checkJava() { # Warn if there is a Java version mismatch between PATH and JAVA_HOME/JDK_HOME @@ -190,14 +194,14 @@ checkJava() { } java_version () { - local version=$(getJavaVersion "$java_cmd") + local -r version=$(getJavaVersion "$java_cmd") vlog "Detected Java version: $version" - echo "${version:2:1}" + echo "$version" } # MaxPermSize critical on pre-8 JVMs but incurs noisy warning on 8+ default_jvm_opts () { - local v="$(java_version)" + local -r v="$(java_version)" if [[ $v -ge 8 ]]; then echo "$default_jvm_opts_common" else @@ -228,17 +232,22 @@ execRunner () { vlog "" } - [[ -n "$batch" ]] && exec /dev/null; then + if command -v curl > /dev/null 2>&1; then curl --fail --silent --location "$url" --output "$jar" - elif which wget >/dev/null; then + elif command -v wget > /dev/null 2>&1; then wget -q -O "$jar" "$url" fi } && [[ -r "$jar" ]] @@ -268,10 +273,57 @@ acquire_sbt_jar () { [[ -r "$sbt_jar" ]] } || { sbt_jar="$(jar_file "$sbt_version")" - download_url "$(make_url "$sbt_version")" "$sbt_jar" + jar_url="$(make_url "$sbt_version")" + + echoerr "Downloading sbt launcher for ${sbt_version}:" + echoerr " From ${jar_url}" + echoerr " To ${sbt_jar}" + + download_url "${jar_url}" "${sbt_jar}" + + case "${sbt_version}" in + 0.*) vlog "SBT versions < 1.0 do not have published MD5 checksums, skipping check"; echo "" ;; + *) verify_sbt_jar "${sbt_jar}" ;; + esac } } +verify_sbt_jar() { + local jar="${1}" + local md5="${jar}.md5" + + download_url "$(make_url "${sbt_version}").md5" "${md5}" > /dev/null 2>&1 + + if command -v md5sum > /dev/null 2>&1; then + if echo "$(cat "${md5}") ${jar}" | md5sum -c -; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + elif command -v md5 > /dev/null 2>&1; then + if [ "$(md5 -q "${jar}")" == "$(cat "${md5}")" ]; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + elif command -v openssl > /dev/null 2>&1; then + if [ "$(openssl md5 -r "${jar}" | awk '{print $1}')" == "$(cat "${md5}")" ]; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + else + echoerr "Could not find an MD5 command" + return 1 + fi +} + usage () { set_sbt_version cat < use the scala build at the specified directory -scala-version use the specified version of scala -binary-version use the specified scala version when searching for dependencies @@ -399,6 +452,7 @@ process_args () { -210) setScalaVersion "$latest_210" && shift ;; -211) setScalaVersion "$latest_211" && shift ;; -212) setScalaVersion "$latest_212" && shift ;; + -213) setScalaVersion "$latest_213" && shift ;; new) sbt_new=true && : ${sbt_explicit_version:=$sbt_release_version} && addResidual "$1" && shift ;; *) addResidual "$1" && shift ;; esac @@ -412,7 +466,7 @@ process_args "$@" readConfigFile() { local end=false until $end; do - read || end=true + read -r || end=true [[ $REPLY =~ ^# ]] || [[ -z $REPLY ]] || echo "$REPLY" done < "$1" } @@ -421,10 +475,10 @@ readConfigFile() { # can supply args to this runner if [[ -r "$sbt_opts_file" ]]; then vlog "Using sbt options defined in file $sbt_opts_file" - while read opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") + while read -r opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then vlog "Using sbt options defined in variable \$SBT_OPTS" - extra_sbt_opts=( $SBT_OPTS ) + IFS=" " read -r -a extra_sbt_opts <<< "$SBT_OPTS" else vlog "No extra sbt options have been defined" fi @@ -444,19 +498,18 @@ checkJava setTraceLevel() { case "$sbt_version" in "0.7."* | "0.10."* | "0.11."* ) echoerr "Cannot set trace level in sbt version $sbt_version" ;; - *) setThisBuild traceLevel $trace_level ;; + *) setThisBuild traceLevel "$trace_level" ;; esac } # set scalacOptions if we were given any -S opts -[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\"" +[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[*]}\"" -# Update build.properties on disk to set explicit version - sbt gives us no choice -[[ -n "$sbt_explicit_version" && -z "$sbt_new" ]] && update_build_props_sbt "$sbt_explicit_version" +[[ -n "$sbt_explicit_version" && -z "$sbt_new" ]] && addJava "-Dsbt.version=$sbt_explicit_version" vlog "Detected sbt version $sbt_version" if [[ -n "$sbt_script" ]]; then - residual_args=( $sbt_script ${residual_args[@]} ) + residual_args=( "$sbt_script" "${residual_args[@]}" ) else # no args - alert them there's stuff in here (( argumentCount > 0 )) || { @@ -477,6 +530,7 @@ EOM } # pick up completion if present; todo +# shellcheck disable=SC1091 [[ -r .sbt_completion.sh ]] && source .sbt_completion.sh # directory to store sbt launchers @@ -486,7 +540,7 @@ EOM # no jar? download it. [[ -r "$sbt_jar" ]] || acquire_sbt_jar || { # still no jar? uh-oh. - echo "Download failed. Obtain the jar manually and place it at $sbt_jar" + echo "Could not download and verify the launcher. Obtain the jar manually and place it at $sbt_jar" exit 1 } @@ -511,13 +565,13 @@ fi if [[ -r "$jvm_opts_file" ]]; then vlog "Using jvm options defined in file $jvm_opts_file" - while read opt; do extra_jvm_opts+=("$opt"); done < <(readConfigFile "$jvm_opts_file") + while read -r opt; do extra_jvm_opts+=("$opt"); done < <(readConfigFile "$jvm_opts_file") elif [[ -n "$JVM_OPTS" && ! ("$JVM_OPTS" =~ ^@.*) ]]; then vlog "Using jvm options defined in \$JVM_OPTS variable" - extra_jvm_opts=( $JVM_OPTS ) + IFS=" " read -r -a extra_jvm_opts <<< "$JVM_OPTS" else vlog "Using default jvm options" - extra_jvm_opts=( $(default_jvm_opts) ) + IFS=" " read -r -a extra_jvm_opts <<< "$(default_jvm_opts)" fi # traceLevel is 0.12+ @@ -539,13 +593,12 @@ main () { # we're not going to print those lines anyway. We strip that bit of # line noise, but leave the other codes to preserve color. mainFiltered () { - local ansiOverwrite='\r\x1BM\x1B[2K' - local excludeRegex=$(egrep -v '^#|^$' ~/.sbtignore | paste -sd'|' -) + local -r excludeRegex=$(grep -E -v '^#|^$' ~/.sbtignore | paste -sd'|' -) echoLine () { - local line="$1" - local line1="$(echo "$line" | sed 's/\r\x1BM\x1B\[2K//g')" # This strips the OverwriteLine code. - local line2="$(echo "$line1" | sed 's/\x1B\[[0-9;]*[JKmsu]//g')" # This strips all codes - we test regexes against this. + local -r line="$1" + local -r line1="${line//\r\x1BM\x1B\[2K//g}" # This strips the OverwriteLine code. + local -r line2="${line1//\x1B\[[0-9;]*[JKmsu]//g}" # This strips all codes - we test regexes against this. if [[ $line2 =~ $excludeRegex ]]; then [[ -n $debugUs ]] && echo "[X] $line1" @@ -562,7 +615,7 @@ mainFiltered () { # Obviously this is super ad hoc but I don't know how to improve on it. Testing whether # stdin is a terminal is useless because most of my use cases for this filtering are # exactly when I'm at a terminal, running sbt non-interactively. -shouldFilter () { [[ -f ~/.sbtignore ]] && ! egrep -q '\b(shell|console|consoleProject)\b' <<<"${residual_args[@]}"; } +shouldFilter () { [[ -f ~/.sbtignore ]] && ! grep -E -q '\b(shell|console|consoleProject)\b' <<<"${residual_args[@]}"; } # run sbt if shouldFilter; then mainFiltered; else main; fi diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala new file mode 100644 index 00000000..715bc557 --- /dev/null +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -0,0 +1,607 @@ +package xerial.sbt + +import java.io.IOException + +import org.apache.http.{HttpResponse, HttpStatus} +import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials} +import org.apache.http.client.HttpClient +import org.apache.http.client.methods.{HttpGet, HttpPost} +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.DefaultHttpClient +import sbt.{Credentials, DirectCredentials, Logger} + +import scala.io.Source +import scala.xml.{Utility, XML} + +/** + * Interface to access the REST API of Nexus + * @param log + * @param repositoryUrl + * @param profileName + * @param cred + * @param credentialHost + */ +class NexusRESTService( + log: Logger, + repositoryUrl: String, + val profileName: String, + cred: Seq[Credentials], + credentialHost: String +) { + + import NexusRESTService._ + + val monitor = new ActivityMonitor(log) + + def findTargetRepository(command: CommandType, arg: Option[String]): StagingRepositoryProfile = { + val repos = command match { + case Close => openRepositories + case Promote => closedRepositories + case Drop => stagingRepositoryProfiles + case CloseAndPromote => stagingRepositoryProfiles + } + if (repos.isEmpty) { + if (stagingProfiles.isEmpty) { + log.error(s"No staging profile found for $profileName") + log.error("Have you requested a staging profile and successfully published your signed artifact there?") + throw new IllegalStateException(s"No staging profile found for $profileName") + } else { + throw new IllegalStateException(command.errNotFound) + } + } + + def findSpecifiedInArg(target: String) = { + repos.find(_.repositoryId == target).getOrElse { + log.error(s"Repository $target is not found") + log.error(s"Specify one of the repository ids in:\n${repos.mkString("\n")}") + throw new IllegalArgumentException(s"Repository $target is not found") + } + } + + arg.map(findSpecifiedInArg).getOrElse { + if (repos.size > 1) { + log.error(s"Multiple repositories are found:\n${repos.mkString("\n")}") + log.error(s"Specify one of the repository ids in the command line") + throw new IllegalStateException("Found multiple staging repositories") + } else { + repos.head + } + } + } + + def openRepositories = stagingRepositoryProfiles.filter(_.isOpen).sortBy(_.repositoryId) + def closedRepositories = stagingRepositoryProfiles.filter(_.isClosed).sortBy(_.repositoryId) + + private def repoBase(url: String) = if (url.endsWith("/")) url.dropRight(1) else url + private val repo = { + val url = repoBase(repositoryUrl) + log.info(s"Nexus repository URL: $url") + log.info(s"sonatypeProfileName = ${profileName}") + url + } + + def Get[U](path: String)(body: HttpResponse => U): U = { + val req = new HttpGet(s"${repo}$path") + req.addHeader("Content-Type", "application/xml") + + val retry = new ExponentialBackOffRetry(initialWaitSeq = 0) + var toContinue = true + var response: HttpResponse = null + var ret: Any = null + while (toContinue && retry.hasNext) { + withHttpClient { client => + response = client.execute(req) + log.debug(s"Status line: ${response.getStatusLine}") + response.getStatusLine.getStatusCode match { + case HttpStatus.SC_OK => + toContinue = false + ret = body(response) + case HttpStatus.SC_INTERNAL_SERVER_ERROR => + log.warn(s"Received 500 error: ${response.getStatusLine}. Retrying...") + retry.doWait + case _ => + throw new IOException(s"Failed to retrieve data from $path: ${response.getStatusLine}") + } + } + } + if (ret == null) { + throw new IOException(s"Failed to retrieve data from $path") + } + ret.asInstanceOf[U] + } + + def IgnoreEntityContent(in: java.io.InputStream): Unit = () + + def Post(path: String, bodyXML: String, contentHandler: java.io.InputStream => Unit = IgnoreEntityContent) = { + val req = new HttpPost(s"${repo}$path") + req.setEntity(new StringEntity(bodyXML)) + req.addHeader("Content-Type", "application/xml") + + val retry = new ExponentialBackOffRetry(initialWaitSeq = 0) + var response: HttpResponse = null + var toContinue = true + while (toContinue && retry.hasNext) { + withHttpClient { client => + response = client.execute(req) + response.getStatusLine.getStatusCode match { + case HttpStatus.SC_INTERNAL_SERVER_ERROR => + log.warn(s"Received 500 error: ${response.getStatusLine}. Retrying...") + retry.doWait + case _ => + log.debug(s"Status line: ${response.getStatusLine}") + toContinue = false + contentHandler(response.getEntity.getContent) + } + } + } + if (toContinue) { + throw new IOException(s"Failed to retrieve data from $path") + } + response + } + + private def withHttpClient[U](body: HttpClient => U): U = { + val credt: DirectCredentials = Credentials + .forHost(cred, credentialHost) + .getOrElse { + throw new IllegalStateException( + s"No credential is found for $credentialHost. Prepare ~/.sbt/(sbt_version)/sonatype.sbt file." + ) + } + + val client = new DefaultHttpClient() + try { + val user = credt.userName + val passwd = credt.passwd + client.getCredentialsProvider.setCredentials( + new AuthScope(credt.host, AuthScope.ANY_PORT), + new UsernamePasswordCredentials(user, passwd) + ) + body(client) + } finally client.getConnectionManager.shutdown() + } + + def stagingRepositoryProfiles = { + log.info("Reading staging repository profiles...") + Get("/staging/profile_repositories") { response => + val profileRepositoriesXML = XML.load(response.getEntity.getContent) + val repositoryProfiles = for (p <- profileRepositoriesXML \\ "stagingProfileRepository") yield { + StagingRepositoryProfile( + (p \ "profileId").text, + (p \ "profileName").text, + (p \ "type").text, + (p \ "repositoryId").text, + (p \ "description").text + ) + } + val myProfiles = repositoryProfiles.filter(_.profileName == profileName) + if (myProfiles.isEmpty) { + log.warn(s"No staging repository is found. Do publishSigned first.") + } + myProfiles + } + } + + def stagingProfiles = { + log.info("Reading staging profiles...") + Get("/staging/profiles") { response => + val profileXML = XML.load(response.getEntity.getContent) + val profiles = for (p <- profileXML \\ "stagingProfile" if (p \ "name").text == profileName) yield { + StagingProfile( + (p \ "id").text, + (p \ "name").text, + (p \ "repositoryTargetId").text + ) + } + profiles + } + } + + lazy val currentProfile = { + val profiles = stagingProfiles + if (profiles.isEmpty) { + throw new IllegalArgumentException(s"Profile ${profileName} is not found") + } + profiles.head + } + + private def createRequestXML(description: String) = + s"""| + | + | + | ${Utility.escape(description)} + | + | + """.stripMargin + + private def promoteRequestXML(repo: StagingRepositoryProfile) = + s"""| + | + | + | ${repo.repositoryId} + | ${currentProfile.repositoryTargetId} + | ${repo.description} + | + | + """.stripMargin + + class ExponentialBackOffRetry(initialWaitSeq: Int = 5, intervalSeq: Int = 3, maxRetries: Int = 10) { + private var numTrial = 0 + private var currentInterval = intervalSeq + + def hasNext = numTrial < maxRetries + + def nextWait = { + val interval = if (numTrial == 0) initialWaitSeq else currentInterval + currentInterval = (currentInterval * 1.5 + 0.5).toInt + numTrial += 1 + interval + } + + def doWait: Unit = { + val w = nextWait + Thread.sleep(w * 1000) + } + + } + + def createStage(description: String = "Requested by sbt-sonatype plugin"): StagingRepositoryProfile = { + val postURL = s"/staging/profiles/${currentProfile.profileId}/start" + log.info(s"Creating staging repository in profile: ${currentProfile.profileName}") + var repo: StagingRepositoryProfile = null + val ret = Post( + postURL, + createRequestXML(description), + (in: java.io.InputStream) => { + val xml = XML.load(in) + val ids = xml \\ "data" \ "stagedRepositoryId" + if (1 != ids.size) + throw new IOException(s"Failed to create repository in profile: ${currentProfile.profileName}") + repo = StagingRepositoryProfile( + currentProfile.profileId, + currentProfile.profileName, + "open", + ids.head.text, + description + ) + log.info(s"Created successfully: ${repo.repositoryId}") + } + ) + if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { + throw new IOException( + s"Failed to create repository in profile: ${currentProfile.profileName}: ${ret.getStatusLine}" + ) + } + if (null == repo) { + throw new IOException( + s"Failed to create repository in profile: ${currentProfile.profileName}: no stagedRepositoryId" + ) + } + repo + } + + def closeStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { + var toContinue = true + if (repo.isClosed || repo.isReleased) { + log.info(s"Repository ${repo.repositoryId} is already closed") + toContinue = false + } + + if (toContinue) { + // Post close request + val postURL = s"/staging/profiles/${currentProfile.profileId}/finish" + log.info(s"Closing staging repository $repo") + val ret = Post(postURL, promoteRequestXML(repo)) + if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { + throw new IOException(s"Failed to send close operation: ${ret.getStatusLine}") + } + } + + toContinue = true + val timer = new ExponentialBackOffRetry() + while (toContinue && timer.hasNext) { + val activities = activitiesOf(repo) + monitor.report(activities) + activities.filter(_.name == "close").lastOption match { + case Some(activity) => + if (activity.isCloseSucceeded(repo.repositoryId)) { + toContinue = false + log.info("Closed successfully") + } else if (activity.containsError) { + log.error("Failed to close the repository") + activity.reportFailure(log) + throw new Exception("Failed to close the repository") + } else { + // Activity log exists, but the close phase is not yet terminated + log.debug("Close process is in progress ...") + timer.doWait + } + case None => + timer.doWait + } + } + if (toContinue) + throw new IOException("Timed out") + + repo.toClosed + } + + def dropStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { + val postURL = s"/staging/profiles/${currentProfile.profileId}/drop" + log.info(s"Dropping staging repository $repo") + val ret = Post(postURL, promoteRequestXML(repo)) + if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { + throw new IOException(s"Failed to drop ${repo.repositoryId}: ${ret.getStatusLine}") + } + log.info(s"Dropped successfully: ${repo.repositoryId}") + repo.toDropped + } + + def promoteStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { + var toContinue = true + if (repo.isReleased) { + log.info(s"Repository ${repo.repositoryId} is already released") + toContinue = false + } + + if (toContinue) { + // Post promote(release) request + val postURL = s"/staging/profiles/${currentProfile.profileId}/promote" + log.info(s"Promoting staging repository $repo") + val ret = Post(postURL, promoteRequestXML(repo)) + if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { + log.error(s"${ret.getStatusLine}") + for (errorLine <- Source.fromInputStream(ret.getEntity.getContent).getLines()) { + log.error(errorLine) + } + throw new Exception("Failed to promote the repository") + } + } + + toContinue = true + var result: StagingRepositoryProfile = null + val timer = new ExponentialBackOffRetry() + while (toContinue && timer.hasNext) { + val activities = activitiesOf(repo) + monitor.report(activities) + activities.filter(_.name == "release").lastOption match { + case Some(activity) => + if (activity.isReleaseSucceeded(repo.repositoryId)) { + log.info("Promoted successfully") + + // Drop after release + result = dropStage(repo.toReleased) + toContinue = false + } else if (activity.containsError) { + log.error("Failed to promote the repository") + activity.reportFailure(log) + throw new Exception("Failed to promote the repository") + } else { + log.debug("Release process is in progress ...") + timer.doWait + } + case None => + timer.doWait + } + } + if (toContinue) + throw new IOException("Timed out") + require(null != result) + result + } + + def stagingRepositoryInfo(repositoryId: String) = { + log.info(s"Seaching for repository $repositoryId ...") + val ret = Get(s"/staging/repository/$repositoryId") { response => + XML.load(response.getEntity.getContent) + } + ret + } + + def closeAndPromote(repo: StagingRepositoryProfile): StagingRepositoryProfile = { + if (repo.isReleased) { + dropStage(repo) + } else { + val closed = closeStage(repo) + promoteStage(closed) + } + } + + def activities: Seq[(StagingRepositoryProfile, Seq[StagingActivity])] = { + for (r <- stagingRepositoryProfiles) yield r -> activitiesOf(r) + } + + def activitiesOf(r: StagingRepositoryProfile): Seq[StagingActivity] = { + log.debug(s"Checking activity logs of ${r.repositoryId} ...") + val a = Get(s"/staging/repository/${r.repositoryId}/activity") { response => + val xml = XML.load(response.getEntity.getContent) + for (sa <- xml \\ "stagingActivity") yield { + val ae = for (event <- sa \ "events" \ "stagingActivityEvent") yield { + val props = for (prop <- event \ "properties" \ "stagingProperty") yield { + (prop \ "name").text -> (prop \ "value").text + } + ActivityEvent((event \ "timestamp").text, (event \ "name").text, (event \ "severity").text, props.toMap) + } + StagingActivity((sa \ "name").text, (sa \ "started").text, (sa \ "stopped").text, ae.toSeq) + } + } + a + } +} + +object NexusRESTService { + + /** + * Switches of a Sonatype command to use + */ + sealed trait CommandType { + def errNotFound: String + } + case object Close extends CommandType { + def errNotFound = "No open repository is found. Run publishSigned first" + } + case object Promote extends CommandType { + def errNotFound = "No closed repository is found. Run publishSigned and close commands" + } + case object Drop extends CommandType { + def errNotFound = "No staging repository is found. Run publishSigned first" + } + case object CloseAndPromote extends CommandType { + def errNotFound = "No staging repository is found. Run publishSigned first" + } + + /** + * Staging repository profile has an id of deployed artifact and the current staging state. + * @param profileId + * @param profileName + * @param stagingType + * @param repositoryId + * @param description + */ + case class StagingRepositoryProfile( + profileId: String, + profileName: String, + stagingType: String, + repositoryId: String, + description: String + ) { + override def toString = + s"[$repositoryId] status:$stagingType, profile:$profileName($profileId) description: $description" + def isOpen = stagingType == "open" + def isClosed = stagingType == "closed" + def isReleased = stagingType == "released" + + def toClosed = copy(stagingType = "closed") + def toDropped = copy(stagingType = "dropped") + def toReleased = copy(stagingType = "released") + } + + /** + * Staging profile is the information associated to a Sonatype account. + * @param profileId + * @param profileName + * @param repositoryTargetId + */ + case class StagingProfile(profileId: String, profileName: String, repositoryTargetId: String) + + /** + * Staging activity is an action to the staged repository + * @param name activity name, e.g. open, close, promote, etc. + * @param started + * @param stopped + * @param events + */ + case class StagingActivity(name: String, started: String, stopped: String, events: Seq[ActivityEvent]) { + override def toString = { + val b = Seq.newBuilder[String] + b += s"-activity -- name:$name, started:$started, stopped:$stopped" + for (e <- events) + b += s" ${e.toString}" + b.result.mkString("\n") + } + + def activityLog = s"Activity $name started:$started, stopped:$stopped" + + def log(log: Logger): Unit = { + log.info(activityLog) + val hasError = containsError + for (e <- suppressEvaluateLog) { + e.log(log, hasError) + } + } + + def suppressEvaluateLog = { + val in = events.toIndexedSeq + var cursor = 0 + val b = Seq.newBuilder[ActivityEvent] + while (cursor < in.size) { + val current = in(cursor) + if (cursor < in.size - 1) { + val next = in(cursor + 1) + if (current.name == "ruleEvaluate" && current.ruleType == next.ruleType) { + // skip + } else { + b += current + } + } + cursor += 1 + } + b.result + } + + def containsError = events.exists(_.severity != "0") + + def reportFailure(log: Logger): Unit = { + log.error(activityLog) + val failureReport = suppressEvaluateLog.filter(_.isFailure) + for (e <- failureReport) { + e.log(log, useErrorLog = true) + } + } + + def isReleaseSucceeded(repositoryId: String): Boolean = { + events + .find(_.name == "repositoryReleased") + .exists(_.property.getOrElse("id", "") == repositoryId) + } + + def isCloseSucceeded(repositoryId: String): Boolean = { + events + .find(_.name == "repositoryClosed") + .exists(_.property.getOrElse("id", "") == repositoryId) + } + + } + + /** + * ActivityEvent is an evaluation result (e.g., checksum, signature check, etc.) of a rule defined in a StagingActivity ruleset + * @param timestamp + * @param name + * @param severity + * @param property + */ + case class ActivityEvent(timestamp: String, name: String, severity: String, property: Map[String, String]) { + def ruleType: String = property.getOrElse("typeId", "other") + def isFailure = name == "ruleFailed" + + override def toString = + s"-event -- timestamp:$timestamp, name:$name, severity:$severity, ${property.map(p => s"${p._1}:${p._2}").mkString(", ")}" + + def log(s: Logger, useErrorLog: Boolean = false): Unit = { + val props = { + val front = + if (property.contains("typeId")) + Seq(property("typeId")) + else + Seq.empty + front ++ property.filter(_._1 != "typeId").map(p => s"${p._1}:${p._2}") + } + val messageLine = props.mkString(", ") + val name_s = name.replaceAll("rule(s)?", "") + val message = f"$name_s%10s: $messageLine" + if (useErrorLog) + s.error(message) + else + s.info(message) + } + } + + class ActivityMonitor(s: Logger) { + var reportedActivities = Set.empty[String] + var reportedEvents = Set.empty[ActivityEvent] + + def report(stagingActivities: Seq[StagingActivity]) = { + for (sa <- stagingActivities) { + if (!reportedActivities.contains(sa.started)) { + s.info(sa.activityLog) + reportedActivities += sa.started + } + for (ae <- sa.events if !reportedEvents.contains(ae)) { + ae.log(s, useErrorLog = false) + reportedEvents += ae + } + } + } + } + +} diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index b026e11a..cb9df585 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -7,22 +7,12 @@ package xerial.sbt +import sbt.Keys._ import sbt._ -import Keys._ -import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials} -import org.apache.http.impl.client.{DefaultHttpClient, BasicCredentialsProvider} -import org.apache.http.client.methods.{HttpPost, HttpGet} -import sbt.plugins.JvmPlugin -import scala.xml.{Utility, XML} -import org.apache.http.client.HttpClient -import org.apache.http.{HttpStatus, HttpResponse} -import org.apache.http.entity.StringEntity -import scala.io.Source -import java.io.IOException +import xerial.sbt.NexusRESTService._ /** * Plugin for automating release processes at Sonatype Nexus - * @author Taro L. Saito */ object Sonatype extends AutoPlugin { @@ -33,21 +23,19 @@ object Sonatype extends AutoPlugin { val sonatypeDefaultResolver = settingKey[Resolver]("Default Sonatype Resolver") val sonatypePublishTo = settingKey[Option[Resolver]]("Default Sonatype publishTo target") val sonatypeStagingRepositoryProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") - val sonatypeProjectHosting = settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") + val sonatypeProjectHosting = + settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") } object SonatypeKeys extends SonatypeKeys {} object autoImport extends SonatypeKeys {} - override def trigger = allRequirements - - override def requires = JvmPlugin - + override def trigger = noTrigger override def projectSettings = sonatypeSettings - import autoImport._ import SonatypeCommand._ + import autoImport._ lazy val sonatypeSettings = Seq[Def.Setting[_]]( sonatypeProfileName := organization.value, @@ -59,19 +47,20 @@ object Sonatype extends AutoPlugin { false }, credentials ++= { - val alreadyContainsSonatypeCredentials = credentials.value.collect { case d: DirectCredentials => d.host == sonatypeCredentialHost.value }.nonEmpty + val alreadyContainsSonatypeCredentials = credentials.value.collect { + case d: DirectCredentials => d.host == sonatypeCredentialHost.value + }.nonEmpty if (!alreadyContainsSonatypeCredentials) { val env = sys.env.get(_) (for { username <- env("SONATYPE_USERNAME") password <- env("SONATYPE_PASSWORD") - } yield - Credentials( - "Sonatype Nexus Repository Manager", - sonatypeCredentialHost.value, - username, - password - )).toSeq + } yield Credentials( + "Sonatype Nexus Repository Manager", + sonatypeCredentialHost.value, + username, + password + )).toSeq } else Seq.empty }, homepage := homepage.value.orElse(sonatypeProjectHosting.value.map(h => url(h.homepage))), @@ -126,15 +115,17 @@ object Sonatype extends AutoPlugin { } object GitHubHosting { - private val domain = "github.com" - def apply(user: String, repository: String, email: String) = ProjectHosting(domain, user, None, email, repository) - def apply(user: String, repository: String, fullName: String, email: String) = ProjectHosting(domain, user, Some(fullName), email, repository) + private val domain = "github.com" + def apply(user: String, repository: String, email: String) = ProjectHosting(domain, user, None, email, repository) + def apply(user: String, repository: String, fullName: String, email: String) = + ProjectHosting(domain, user, Some(fullName), email, repository) } object GitLabHosting { - private val domain = "gitlab.com" - def apply(user: String, repository: String, email: String) = ProjectHosting(domain, user, None, email, repository) - def apply(user: String, repository: String, fullName: String, email: String) = ProjectHosting(domain, user, Some(fullName), email, repository) + private val domain = "gitlab.com" + def apply(user: String, repository: String, email: String) = ProjectHosting(domain, user, None, email, repository) + def apply(user: String, repository: String, fullName: String, email: String) = + ProjectHosting(domain, user, Some(fullName), email, repository) } // aliases @@ -165,36 +156,42 @@ object Sonatype extends AutoPlugin { ) } - val sonatypeList: Command = Command.command("sonatypeList", "List staging repositories", "List published repository IDs") { state => - val rest = getNexusRestService(state) - val profiles = rest.stagingProfiles - val log = state.log - if (profiles.isEmpty) { - log.warn(s"No staging profile is found for ${rest.profileName}") - state.fail - } else { - log.info(s"Staging profiles (profileName:${rest.profileName}):") - log.info(profiles.mkString("\n")) - state + val sonatypeList: Command = + Command.command("sonatypeList", "List staging repositories", "List published repository IDs") { state => + val rest = getNexusRestService(state) + val profiles = rest.stagingProfiles + val log = state.log + if (profiles.isEmpty) { + log.warn(s"No staging profile is found for ${rest.profileName}") + state.fail + } else { + log.info(s"Staging profiles (profileName:${rest.profileName}):") + log.info(profiles.mkString("\n")) + state + } } - } private val repositoryIdParser: complete.Parser[Option[String]] = (Space ~> token(StringBasic, "(repositoryId)")).?.!!!("invalid input. please input repository name") private val sonatypeProfileParser: complete.Parser[Option[String]] = - (Space ~> token(StringBasic, "(sonatypeProfile)")).?.!!!("invalid input. please input sonatypeProfile (e.g., org.xerial)") + (Space ~> token(StringBasic, "(sonatypeProfile)")).?.!!!( + "invalid input. please input sonatypeProfile (e.g., org.xerial)" + ) private val sonatypeProfileDescriptionParser: complete.Parser[Either[String, (String, String)]] = Space ~> (token(StringBasic, "description") || (token(StringBasic <~ Space, "sonatypeProfile") ~ token(StringBasic, "description"))) - private def commandWithRepositoryId(name: String, briefHelp: String) = Command(name, (name, briefHelp), briefHelp)(_ => repositoryIdParser)(_) + private def commandWithRepositoryId(name: String, briefHelp: String) = + Command(name, (name, briefHelp), briefHelp)(_ => repositoryIdParser)(_) - private def commandWithSonatypeProfile(name: String, briefHelp: String) = Command(name, (name, briefHelp), briefHelp)(_ => sonatypeProfileParser)(_) + private def commandWithSonatypeProfile(name: String, briefHelp: String) = + Command(name, (name, briefHelp), briefHelp)(_ => sonatypeProfileParser)(_) - private def commandWithSonatypeProfileDescription(name: String, briefHelp: String) = Command(name, (name, briefHelp), briefHelp)(_ => sonatypeProfileDescriptionParser)(_) + private def commandWithSonatypeProfileDescription(name: String, briefHelp: String) = + Command(name, (name, briefHelp), briefHelp)(_ => sonatypeProfileDescriptionParser)(_) def getUpdatedPublishTo(profileName: String, current: Option[Option[Resolver]]): Seq[Setting[_]] = { val result = for { @@ -206,93 +203,102 @@ object Sonatype extends AutoPlugin { result.toSeq } - val sonatypeOpen: Command = commandWithSonatypeProfileDescription("sonatypeOpen", "Create a staging repository and set publishTo") { (state, profileNameDescription) => - val (profileName: Option[String], profileDescription: String) = profileNameDescription match { - case Left(d) => - (None, d) - case Right((n, d)) => - (Some(n), d) - } - val rest = getNexusRestService(state, profileName) - val repo = rest.createStage(profileDescription) - val path = "/staging/deployByRepositoryId/" + repo.repositoryId - val extracted = Project.extract(state) - - // accumulate changes for settings for current project and all aggregates - val newSettings : Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => - Seq( - ref / sonatypeStagingRepositoryProfile := repo, - ref / publishTo := Some(sonatypeDefaultResolver.value) - ) - } ++ Seq( - sonatypeStagingRepositoryProfile := repo, - publishTo := Some(sonatypeDefaultResolver.value) - ) - - val next = extracted.appendWithSession(newSettings, state) - next - } - - val sonatypeClose: Command = commandWithRepositoryId("sonatypeClose", "Close a stage and clear publishTo if it was set by sonatypeOpen") { (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(Close, repoID) - val repo2 = rest.closeStage(repo1) - val next = extracted.append(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - - val sonatypePromote: Command = commandWithRepositoryId("sonatypePromote", "Promote a staged repository") { (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(Promote, repoID) - val repo2 = rest.promoteStage(repo1) - val next = extracted.append(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - - val sonatypeDrop: Command = commandWithRepositoryId("sonatypeDrop", "Drop a staging repository") { (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(Drop, repoID) - val repo2 = rest.dropStage(repo1) - val next = extracted.append(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - - val sonatypeRelease: Command = commandWithRepositoryId("sonatypeRelease", "Publish with sonatypeClose and sonatypePromote") { (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) - val repo2 = rest.closeAndPromote(repo1) - val next = extracted.append(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } + val sonatypeOpen: Command = + commandWithSonatypeProfileDescription("sonatypeOpen", "Create a staging repository and set publishTo") { + (state, profileNameDescription) => + val (profileName: Option[String], profileDescription: String) = profileNameDescription match { + case Left(d) => + (None, d) + case Right((n, d)) => + (Some(n), d) + } + val rest = getNexusRestService(state, profileName) + val repo = rest.createStage(profileDescription) + val path = "/staging/deployByRepositoryId/" + repo.repositoryId + val extracted = Project.extract(state) + + // accumulate changes for settings for current project and all aggregates + val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => + Seq( + ref / sonatypeStagingRepositoryProfile := repo, + ref / publishTo := Some(sonatypeDefaultResolver.value) + ) + } ++ Seq( + sonatypeStagingRepositoryProfile := repo, + publishTo := Some(sonatypeDefaultResolver.value) + ) - val sonatypeReleaseAll: Command = commandWithSonatypeProfile("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { (state, profileName) => - val rest = getNexusRestService(state, profileName) - for { - repo <- rest.stagingRepositoryProfiles - _ = rest.closeAndPromote(repo) - } () - state - } + val next = extracted.appendWithSession(newSettings, state) + next + } + + val sonatypeClose: Command = + commandWithRepositoryId("sonatypeClose", "Close a stage and clear publishTo if it was set by sonatypeOpen") { + (state, parsed) => + val rest = getNexusRestService(state) + val extracted = Project.extract(state) + val currentRepoID = for { + repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) + } yield repo.repositoryId + val repoID = parsed.orElse(currentRepoID) + val repo1 = rest.findTargetRepository(Close, repoID) + val repo2 = rest.closeStage(repo1) + val next = extracted.appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) + next + } + + val sonatypePromote: Command = commandWithRepositoryId("sonatypePromote", "Promote a staged repository") { + (state, parsed) => + val rest = getNexusRestService(state) + val extracted = Project.extract(state) + val currentRepoID = for { + repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) + } yield repo.repositoryId + val repoID = parsed.orElse(currentRepoID) + val repo1 = rest.findTargetRepository(Promote, repoID) + val repo2 = rest.promoteStage(repo1) + val next = extracted.appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) + next + } + + val sonatypeDrop: Command = commandWithRepositoryId("sonatypeDrop", "Drop a staging repository") { + (state, parsed) => + val rest = getNexusRestService(state) + val extracted = Project.extract(state) + val currentRepoID = for { + repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) + } yield repo.repositoryId + val repoID = parsed.orElse(currentRepoID) + val repo1 = rest.findTargetRepository(Drop, repoID) + val repo2 = rest.dropStage(repo1) + val next = extracted.appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) + next + } + + val sonatypeRelease: Command = + commandWithRepositoryId("sonatypeRelease", "Publish with sonatypeClose and sonatypePromote") { (state, parsed) => + val rest = getNexusRestService(state) + val extracted = Project.extract(state) + val currentRepoID = for { + repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) + } yield repo.repositoryId + val repoID = parsed.orElse(currentRepoID) + val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) + val repo2 = rest.closeAndPromote(repo1) + val next = extracted.appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) + next + } + + val sonatypeReleaseAll: Command = + commandWithSonatypeProfile("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { + (state, profileName) => + val rest = getNexusRestService(state, profileName) + for { + repo <- rest.stagingRepositoryProfiles + _ = rest.closeAndPromote(repo) + } () + state + } val sonatypeDropAll: Command = commandWithSonatypeProfile("sonatypeDropAll", "Drop all staging repositories") { (state, profileName) => @@ -300,24 +306,25 @@ object Sonatype extends AutoPlugin { for { repo <- rest.stagingRepositoryProfiles _ = rest.dropStage(repo) - }() + } () state } - val sonatypeLog: Command = Command.command("sonatypeLog", "Show repository activities", "Show staging activity logs at Sonatype") { state => - val rest = getNexusRestService(state) - val alist = rest.activities - val log = state.log - if (alist.isEmpty) - log.warn("No staging log is found") - for ((repo, activities) <- alist) { - log.info(s"Staging activities of $repo:") - for (a <- activities) { - a.log(log) + val sonatypeLog: Command = + Command.command("sonatypeLog", "Show repository activities", "Show staging activity logs at Sonatype") { state => + val rest = getNexusRestService(state) + val alist = rest.activities + val log = state.log + if (alist.isEmpty) + log.warn("No staging log is found") + for ((repo, activities) <- alist) { + log.info(s"Staging activities of $repo:") + for (a <- activities) { + a.log(log) + } } + state } - state - } val sonatypeStagingRepositoryProfiles = Command.command("sonatypeStagingRepositoryProfiles") { state => val rest = getNexusRestService(state) @@ -346,558 +353,4 @@ object Sonatype extends AutoPlugin { } } - /** - * Switches of a Sonatype command to use - */ - private sealed trait CommandType { - def errNotFound: String - } - private case object Close extends CommandType { - def errNotFound = "No open repository is found. Run publishSigned first" - } - private case object Promote extends CommandType { - def errNotFound = "No closed repository is found. Run publishSigned and close commands" - } - private case object Drop extends CommandType { - def errNotFound = "No staging repository is found. Run publishSigned first" - } - private case object CloseAndPromote extends CommandType { - def errNotFound = "No staging repository is found. Run publishSigned first" - } - - /** - * Staging repository profile has an id of deployed artifact and the current staging state. - * @param profileId - * @param profileName - * @param stagingType - * @param repositoryId - * @param description - */ - case class StagingRepositoryProfile(profileId: String, profileName: String, stagingType: String, repositoryId: String, description: String) { - override def toString = s"[$repositoryId] status:$stagingType, profile:$profileName($profileId) description: $description" - def isOpen = stagingType == "open" - def isClosed = stagingType == "closed" - def isReleased = stagingType == "released" - - def toClosed = copy(stagingType = "closed") - def toDropped = copy(stagingType = "dropped") - def toReleased = copy(stagingType = "released") - } - - /** - * Staging profile is the information associated to a Sonatype account. - * @param profileId - * @param profileName - * @param repositoryTargetId - */ - case class StagingProfile(profileId: String, profileName: String, repositoryTargetId: String) - - /** - * Staging activity is an action to the staged repository - * @param name activity name, e.g. open, close, promote, etc. - * @param started - * @param stopped - * @param events - */ - case class StagingActivity(name: String, started: String, stopped: String, events: Seq[ActivityEvent]) { - override def toString = { - val b = Seq.newBuilder[String] - b += s"-activity -- name:$name, started:$started, stopped:$stopped" - for (e <- events) - b += s" ${e.toString}" - b.result.mkString("\n") - } - - def activityLog = s"Activity $name started:$started, stopped:$stopped" - - def log(log: Logger): Unit = { - log.info(activityLog) - val hasError = containsError - for (e <- suppressEvaluateLog) { - e.log(log, hasError) - } - } - - def suppressEvaluateLog = { - val in = events.toIndexedSeq - var cursor = 0 - val b = Seq.newBuilder[ActivityEvent] - while (cursor < in.size) { - val current = in(cursor) - if (cursor < in.size - 1) { - val next = in(cursor + 1) - if (current.name == "ruleEvaluate" && current.ruleType == next.ruleType) { - // skip - } else { - b += current - } - } - cursor += 1 - } - b.result - } - - def containsError = events.exists(_.severity != "0") - - def reportFailure(log: Logger): Unit = { - log.error(activityLog) - val failureReport = suppressEvaluateLog.filter(_.isFailure) - for (e <- failureReport) { - e.log(log, useErrorLog = true) - } - } - - def isReleaseSucceeded(repositoryId: String): Boolean = { - events - .find(_.name == "repositoryReleased") - .exists(_.property.getOrElse("id", "") == repositoryId) - } - - def isCloseSucceeded(repositoryId: String): Boolean = { - events - .find(_.name == "repositoryClosed") - .exists(_.property.getOrElse("id", "") == repositoryId) - } - - } - - /** - * ActivityEvent is an evaluation result (e.g., checksum, signature check, etc.) of a rule defined in a StagingActivity ruleset - * @param timestamp - * @param name - * @param severity - * @param property - */ - case class ActivityEvent(timestamp: String, name: String, severity: String, property: Map[String, String]) { - def ruleType: String = property.getOrElse("typeId", "other") - def isFailure = name == "ruleFailed" - - override def toString = s"-event -- timestamp:$timestamp, name:$name, severity:$severity, ${property.map(p => s"${p._1}:${p._2}").mkString(", ")}" - - def log(s: Logger, useErrorLog: Boolean = false): Unit = { - val props = { - val front = - if (property.contains("typeId")) - Seq(property("typeId")) - else - Seq.empty - front ++ property.filter(_._1 != "typeId").map(p => s"${p._1}:${p._2}") - } - val messageLine = props.mkString(", ") - val name_s = name.replaceAll("rule(s)?", "") - val message = f"$name_s%10s: $messageLine" - if (useErrorLog) - s.error(message) - else - s.info(message) - } - } - - class ActivityMonitor(s: Logger) { - var reportedActivities = Set.empty[String] - var reportedEvents = Set.empty[ActivityEvent] - - def report(stagingActivities: Seq[StagingActivity]) = { - for (sa <- stagingActivities) { - if (!reportedActivities.contains(sa.started)) { - s.info(sa.activityLog) - reportedActivities += sa.started - } - for (ae <- sa.events if !reportedEvents.contains(ae)) { - ae.log(s, useErrorLog = false) - reportedEvents += ae - } - } - } - } - - /** - * Interface to access the REST API of Nexus - * @param log - * @param repositoryUrl - * @param profileName - * @param cred - * @param credentialHost - */ - class NexusRESTService(log: Logger, repositoryUrl: String, val profileName: String, cred: Seq[Credentials], credentialHost: String) { - - val monitor = new ActivityMonitor(log) - - def findTargetRepository(command: CommandType, arg: Option[String]): StagingRepositoryProfile = { - val repos = command match { - case Close => openRepositories - case Promote => closedRepositories - case Drop => stagingRepositoryProfiles - case CloseAndPromote => stagingRepositoryProfiles - } - if (repos.isEmpty) { - if (stagingProfiles.isEmpty) { - log.error(s"No staging profile found for $profileName") - log.error("Have you requested a staging profile and successfully published your signed artifact there?") - throw new IllegalStateException(s"No staging profile found for $profileName") - } else { - throw new IllegalStateException(command.errNotFound) - } - } - - def findSpecifiedInArg(target: String) = { - repos.find(_.repositoryId == target).getOrElse { - log.error(s"Repository $target is not found") - log.error(s"Specify one of the repository ids in:\n${repos.mkString("\n")}") - throw new IllegalArgumentException(s"Repository $target is not found") - } - } - - arg.map(findSpecifiedInArg).getOrElse { - if (repos.size > 1) { - log.error(s"Multiple repositories are found:\n${repos.mkString("\n")}") - log.error(s"Specify one of the repository ids in the command line") - throw new IllegalStateException("Found multiple staging repositories") - } else { - repos.head - } - } - } - - def openRepositories = stagingRepositoryProfiles.filter(_.isOpen).sortBy(_.repositoryId) - def closedRepositories = stagingRepositoryProfiles.filter(_.isClosed).sortBy(_.repositoryId) - - private def repoBase(url: String) = if (url.endsWith("/")) url.dropRight(1) else url - private val repo = { - val url = repoBase(repositoryUrl) - log.info(s"Nexus repository URL: $url") - log.info(s"sonatypeProfileName = ${profileName}") - url - } - - def Get[U](path: String)(body: HttpResponse => U): U = { - val req = new HttpGet(s"${repo}$path") - req.addHeader("Content-Type", "application/xml") - - val retry = new ExponentialBackOffRetry(initialWaitSeq = 0) - var toContinue = true - var response: HttpResponse = null - var ret: Any = null - while (toContinue && retry.hasNext) { - withHttpClient { client => - response = client.execute(req) - log.debug(s"Status line: ${response.getStatusLine}") - response.getStatusLine.getStatusCode match { - case HttpStatus.SC_OK => - toContinue = false - ret = body(response) - case HttpStatus.SC_INTERNAL_SERVER_ERROR => - log.warn(s"Received 500 error: ${response.getStatusLine}. Retrying...") - retry.doWait - case _ => - throw new IOException(s"Failed to retrieve data from $path: ${response.getStatusLine}") - } - } - } - if (ret == null) { - throw new IOException(s"Failed to retrieve data from $path") - } - ret.asInstanceOf[U] - } - - def IgnoreEntityContent(in: java.io.InputStream): Unit = () - - def Post(path: String, bodyXML: String, contentHandler: java.io.InputStream => Unit = IgnoreEntityContent) = { - val req = new HttpPost(s"${repo}$path") - req.setEntity(new StringEntity(bodyXML)) - req.addHeader("Content-Type", "application/xml") - - val retry = new ExponentialBackOffRetry(initialWaitSeq = 0) - var response: HttpResponse = null - var toContinue = true - while (toContinue && retry.hasNext) { - withHttpClient { client => - response = client.execute(req) - response.getStatusLine.getStatusCode match { - case HttpStatus.SC_INTERNAL_SERVER_ERROR => - log.warn(s"Received 500 error: ${response.getStatusLine}. Retrying...") - retry.doWait - case _ => - log.debug(s"Status line: ${response.getStatusLine}") - toContinue = false - contentHandler(response.getEntity.getContent) - } - } - } - if (toContinue) { - throw new IOException(s"Failed to retrieve data from $path") - } - response - } - - private def withHttpClient[U](body: HttpClient => U): U = { - val credt: DirectCredentials = Credentials - .forHost(cred, credentialHost) - .getOrElse { - throw new IllegalStateException(s"No credential is found for $credentialHost. Prepare ~/.sbt/(sbt_version)/sonatype.sbt file.") - } - - val client = new DefaultHttpClient() - try { - val user = credt.userName - val passwd = credt.passwd - client.getCredentialsProvider.setCredentials( - new AuthScope(credt.host, AuthScope.ANY_PORT), - new UsernamePasswordCredentials(user, passwd) - ) - body(client) - } finally client.getConnectionManager.shutdown() - } - - def stagingRepositoryProfiles = { - log.info("Reading staging repository profiles...") - Get("/staging/profile_repositories") { response => - val profileRepositoriesXML = XML.load(response.getEntity.getContent) - val repositoryProfiles = for (p <- profileRepositoriesXML \\ "stagingProfileRepository") yield { - StagingRepositoryProfile((p \ "profileId").text, (p \ "profileName").text, (p \ "type").text, (p \ "repositoryId").text, (p \ "description").text) - } - val myProfiles = repositoryProfiles.filter(_.profileName == profileName) - if (myProfiles.isEmpty) { - log.warn(s"No staging repository is found. Do publishSigned first.") - } - myProfiles - } - } - - def stagingProfiles = { - log.info("Reading staging profiles...") - Get("/staging/profiles") { response => - val profileXML = XML.load(response.getEntity.getContent) - val profiles = for (p <- profileXML \\ "stagingProfile" if (p \ "name").text == profileName) yield { - StagingProfile( - (p \ "id").text, - (p \ "name").text, - (p \ "repositoryTargetId").text - ) - } - profiles - } - } - - lazy val currentProfile = { - val profiles = stagingProfiles - if (profiles.isEmpty) { - throw new IllegalArgumentException(s"Profile ${profileName} is not found") - } - profiles.head - } - - private def createRequestXML(description: String) = - s"""| - | - | - | ${Utility.escape(description)} - | - | - """.stripMargin - - private def promoteRequestXML(repo: StagingRepositoryProfile) = - s"""| - | - | - | ${repo.repositoryId} - | ${currentProfile.repositoryTargetId} - | ${repo.description} - | - | - """.stripMargin - - class ExponentialBackOffRetry(initialWaitSeq: Int = 5, intervalSeq: Int = 3, maxRetries: Int = 10) { - private var numTrial = 0 - private var currentInterval = intervalSeq - - def hasNext = numTrial < maxRetries - - def nextWait = { - val interval = if (numTrial == 0) initialWaitSeq else currentInterval - currentInterval = (currentInterval * 1.5 + 0.5).toInt - numTrial += 1 - interval - } - - def doWait: Unit = { - val w = nextWait - Thread.sleep(w * 1000) - } - - } - - def createStage(description: String = "Requested by sbt-sonatype plugin"): StagingRepositoryProfile = { - val postURL = s"/staging/profiles/${currentProfile.profileId}/start" - log.info(s"Creating staging repository in profile: ${currentProfile.profileName}") - var repo: StagingRepositoryProfile = null - val ret = Post( - postURL, - createRequestXML(description), - (in: java.io.InputStream) => { - val xml = XML.load(in) - val ids = xml \\ "data" \ "stagedRepositoryId" - if (1 != ids.size) - throw new IOException(s"Failed to create repository in profile: ${currentProfile.profileName}") - repo = StagingRepositoryProfile(currentProfile.profileId, currentProfile.profileName, "open", ids.head.text, description) - log.info(s"Created successfully: ${repo.repositoryId}") - } - ) - if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { - throw new IOException(s"Failed to create repository in profile: ${currentProfile.profileName}: ${ret.getStatusLine}") - } - if (null == repo) { - throw new IOException(s"Failed to create repository in profile: ${currentProfile.profileName}: no stagedRepositoryId") - } - repo - } - - def closeStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { - var toContinue = true - if (repo.isClosed || repo.isReleased) { - log.info(s"Repository ${repo.repositoryId} is already closed") - toContinue = false - } - - if (toContinue) { - // Post close request - val postURL = s"/staging/profiles/${currentProfile.profileId}/finish" - log.info(s"Closing staging repository $repo") - val ret = Post(postURL, promoteRequestXML(repo)) - if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { - throw new IOException(s"Failed to send close operation: ${ret.getStatusLine}") - } - } - - toContinue = true - val timer = new ExponentialBackOffRetry() - while (toContinue && timer.hasNext) { - val activities = activitiesOf(repo) - monitor.report(activities) - activities.filter(_.name == "close").lastOption match { - case Some(activity) => - if (activity.isCloseSucceeded(repo.repositoryId)) { - toContinue = false - log.info("Closed successfully") - } else if (activity.containsError) { - log.error("Failed to close the repository") - activity.reportFailure(log) - throw new Exception("Failed to close the repository") - } else { - // Activity log exists, but the close phase is not yet terminated - log.debug("Close process is in progress ...") - timer.doWait - } - case None => - timer.doWait - } - } - if (toContinue) - throw new IOException("Timed out") - - repo.toClosed - } - - def dropStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { - val postURL = s"/staging/profiles/${currentProfile.profileId}/drop" - log.info(s"Dropping staging repository $repo") - val ret = Post(postURL, promoteRequestXML(repo)) - if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { - throw new IOException(s"Failed to drop ${repo.repositoryId}: ${ret.getStatusLine}") - } - log.info(s"Dropped successfully: ${repo.repositoryId}") - repo.toDropped - } - - def promoteStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { - var toContinue = true - if (repo.isReleased) { - log.info(s"Repository ${repo.repositoryId} is already released") - toContinue = false - } - - if (toContinue) { - // Post promote(release) request - val postURL = s"/staging/profiles/${currentProfile.profileId}/promote" - log.info(s"Promoting staging repository $repo") - val ret = Post(postURL, promoteRequestXML(repo)) - if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { - log.error(s"${ret.getStatusLine}") - for (errorLine <- Source.fromInputStream(ret.getEntity.getContent).getLines()) { - log.error(errorLine) - } - throw new Exception("Failed to promote the repository") - } - } - - toContinue = true - var result: StagingRepositoryProfile = null - val timer = new ExponentialBackOffRetry() - while (toContinue && timer.hasNext) { - val activities = activitiesOf(repo) - monitor.report(activities) - activities.filter(_.name == "release").lastOption match { - case Some(activity) => - if (activity.isReleaseSucceeded(repo.repositoryId)) { - log.info("Promoted successfully") - - // Drop after release - result = dropStage(repo.toReleased) - toContinue = false - } else if (activity.containsError) { - log.error("Failed to promote the repository") - activity.reportFailure(log) - throw new Exception("Failed to promote the repository") - } else { - log.debug("Release process is in progress ...") - timer.doWait - } - case None => - timer.doWait - } - } - if (toContinue) - throw new IOException("Timed out") - require(null != result) - result - } - - def stagingRepositoryInfo(repositoryId: String) = { - log.info(s"Seaching for repository $repositoryId ...") - val ret = Get(s"/staging/repository/$repositoryId") { response => - XML.load(response.getEntity.getContent) - } - ret - } - - def closeAndPromote(repo: StagingRepositoryProfile): StagingRepositoryProfile = { - if (repo.isReleased) { - dropStage(repo) - } else { - val closed = closeStage(repo) - promoteStage(closed) - } - } - - def activities: Seq[(StagingRepositoryProfile, Seq[StagingActivity])] = { - for (r <- stagingRepositoryProfiles) yield r -> activitiesOf(r) - } - - def activitiesOf(r: StagingRepositoryProfile): Seq[StagingActivity] = { - log.debug(s"Checking activity logs of ${r.repositoryId} ...") - val a = Get(s"/staging/repository/${r.repositoryId}/activity") { response => - val xml = XML.load(response.getEntity.getContent) - for (sa <- xml \\ "stagingActivity") yield { - val ae = for (event <- sa \ "events" \ "stagingActivityEvent") yield { - val props = for (prop <- event \ "properties" \ "stagingProperty") yield { - (prop \ "name").text -> (prop \ "value").text - } - ActivityEvent((event \ "timestamp").text, (event \ "name").text, (event \ "severity").text, props.toMap) - } - StagingActivity((sa \ "name").text, (sa \ "started").text, (sa \ "stopped").text, ae.toSeq) - } - } - a - } - } } diff --git a/src/sbt-test/sbt-sonatype/example/build.sbt b/src/sbt-test/sbt-sonatype/example/build.sbt index b12672eb..a526576c 100644 --- a/src/sbt-test/sbt-sonatype/example/build.sbt +++ b/src/sbt-test/sbt-sonatype/example/build.sbt @@ -1,5 +1,7 @@ organization := "org.xerial.example" +enablePlugins(Sonatype) + sonatypeProfileName := "org.xerial" publishMavenStyle := true diff --git a/src/sbt-test/sbt-sonatype/operations/build.sbt b/src/sbt-test/sbt-sonatype/operations/build.sbt index 11f2f9f5..11b61972 100644 --- a/src/sbt-test/sbt-sonatype/operations/build.sbt +++ b/src/sbt-test/sbt-sonatype/operations/build.sbt @@ -1,3 +1,5 @@ +enablePlugins(Sonatype) + organization := System.getProperty("organization", "org.xerial.operations") sonatypeProfileName := System.getProperty("profile.name", "org.xerial") version := System.getProperty("version", "0.1") diff --git a/version.sbt b/version.sbt index 67a5d231..99cfbd6c 100755 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "2.7-SNAPSHOT" +version in ThisBuild := "3.0-SNAPSHOT" From ba732e2c3b6c6691336ef94347e1f3498227e564 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 10:01:24 -0700 Subject: [PATCH 02/19] Parallelize drop/releaseAll with Future --- src/main/scala/xerial/sbt/NexusClient.scala | 2 +- src/main/scala/xerial/sbt/Sonatype.scala | 23 ++++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index 715bc557..d8292330 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -247,7 +247,7 @@ class NexusRESTService( def createStage(description: String = "Requested by sbt-sonatype plugin"): StagingRepositoryProfile = { val postURL = s"/staging/profiles/${currentProfile.profileId}/start" - log.info(s"Creating staging repository in profile: ${currentProfile.profileName}") + log.info(s"Creating a staging repository in profile ${currentProfile.profileName} with a description key: ${description}") var repo: StagingRepositoryProfile = null val ret = Post( postURL, diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index cb9df585..68fe4336 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -11,6 +11,9 @@ import sbt.Keys._ import sbt._ import xerial.sbt.NexusRESTService._ +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext, Future} + /** * Plugin for automating release processes at Sonatype Nexus */ @@ -137,6 +140,8 @@ object Sonatype extends AutoPlugin { object SonatypeCommand { import complete.DefaultParsers._ + private implicit val ec = ExecutionContext.global + /** * Parsing repository id argument */ @@ -293,20 +298,22 @@ object Sonatype extends AutoPlugin { commandWithSonatypeProfile("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { (state, profileName) => val rest = getNexusRestService(state, profileName) - for { - repo <- rest.stagingRepositoryProfiles - _ = rest.closeAndPromote(repo) - } () + val tasks = rest.stagingRepositoryProfiles.map { repo => + Future.apply(rest.closeAndPromote(repo)) + } + val merged = Future.sequence(tasks) + Await.result(merged, Duration.Inf) state } val sonatypeDropAll: Command = commandWithSonatypeProfile("sonatypeDropAll", "Drop all staging repositories") { (state, profileName) => val rest = getNexusRestService(state, profileName) - for { - repo <- rest.stagingRepositoryProfiles - _ = rest.dropStage(repo) - } () + val dropTasks = rest.stagingRepositoryProfiles.map { repo => + Future.apply(rest.dropStage(repo)) + } + val merged = Future.sequence(dropTasks) + Await.result(merged, Duration.Inf) state } From e867f9bbbc7e0c79b222458fd05bdbb6752818e9 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 10:01:51 -0700 Subject: [PATCH 03/19] Allow reusing existing staging repositories in sonatypeOpen --- project/plugins.sbt | 2 +- sonatype.sbt | 2 ++ src/main/scala/xerial/sbt/Sonatype.scala | 12 ++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a3ac1d7e..1e536ac3 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.0-SNAPSHOT") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") diff --git a/sonatype.sbt b/sonatype.sbt index 8b2be178..c4d5d5e1 100644 --- a/sonatype.sbt +++ b/sonatype.sbt @@ -2,6 +2,8 @@ import xerial.sbt.Sonatype._ publishMavenStyle := true +enablePlugins(Sonatype) + sonatypeProfileName := "org.xerial" sonatypeProjectHosting := Some(GitHubHosting(user="xerial", repository="sbt-sonatype", email="leo@xerial.org")) developers := List( diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 68fe4336..504b5786 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -218,8 +218,16 @@ object Sonatype extends AutoPlugin { (Some(n), d) } val rest = getNexusRestService(state, profileName) - val repo = rest.createStage(profileDescription) - val path = "/staging/deployByRepositoryId/" + repo.repositoryId + val repo = { + val descriptionKey = s"[sbt-sonatype] ${profileDescription}" + // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later + def create = rest.createStage(descriptionKey) + + // Find the already opened profile or create a new one + rest.stagingRepositoryProfiles + .find(_.description == descriptionKey) + .getOrElse(create) + } val extracted = Project.extract(state) // accumulate changes for settings for current project and all aggregates From 5cc4b8b7ad51fd64c98626a4d65cbabf24014a40 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 10:32:56 -0700 Subject: [PATCH 04/19] Add sonatypeClean (key) --- src/main/scala/xerial/sbt/NexusClient.scala | 14 ++--- src/main/scala/xerial/sbt/Sonatype.scala | 59 ++++++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index d8292330..c49b2e5c 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -37,8 +37,8 @@ class NexusRESTService( val repos = command match { case Close => openRepositories case Promote => closedRepositories - case Drop => stagingRepositoryProfiles - case CloseAndPromote => stagingRepositoryProfiles + case Drop => stagingRepositoryProfiles() + case CloseAndPromote => stagingRepositoryProfiles() } if (repos.isEmpty) { if (stagingProfiles.isEmpty) { @@ -69,8 +69,8 @@ class NexusRESTService( } } - def openRepositories = stagingRepositoryProfiles.filter(_.isOpen).sortBy(_.repositoryId) - def closedRepositories = stagingRepositoryProfiles.filter(_.isClosed).sortBy(_.repositoryId) + def openRepositories = stagingRepositoryProfiles().filter(_.isOpen).sortBy(_.repositoryId) + def closedRepositories = stagingRepositoryProfiles().filter(_.isClosed).sortBy(_.repositoryId) private def repoBase(url: String) = if (url.endsWith("/")) url.dropRight(1) else url private val repo = { @@ -161,7 +161,7 @@ class NexusRESTService( } finally client.getConnectionManager.shutdown() } - def stagingRepositoryProfiles = { + def stagingRepositoryProfiles(warnIfMissing:Boolean = true) = { log.info("Reading staging repository profiles...") Get("/staging/profile_repositories") { response => val profileRepositoriesXML = XML.load(response.getEntity.getContent) @@ -175,7 +175,7 @@ class NexusRESTService( ) } val myProfiles = repositoryProfiles.filter(_.profileName == profileName) - if (myProfiles.isEmpty) { + if (myProfiles.isEmpty && warnIfMissing) { log.warn(s"No staging repository is found. Do publishSigned first.") } myProfiles @@ -408,7 +408,7 @@ class NexusRESTService( } def activities: Seq[(StagingRepositoryProfile, Seq[StagingActivity])] = { - for (r <- stagingRepositoryProfiles) yield r -> activitiesOf(r) + for (r <- stagingRepositoryProfiles()) yield r -> activitiesOf(r) } def activitiesOf(r: StagingRepositoryProfile): Seq[StagingActivity] = { diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 504b5786..311c44fd 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -58,12 +58,13 @@ object Sonatype extends AutoPlugin { (for { username <- env("SONATYPE_USERNAME") password <- env("SONATYPE_PASSWORD") - } yield Credentials( - "Sonatype Nexus Repository Manager", - sonatypeCredentialHost.value, - username, - password - )).toSeq + } yield + Credentials( + "Sonatype Nexus Repository Manager", + sonatypeCredentialHost.value, + username, + password + )).toSeq } else Seq.empty }, homepage := homepage.value.orElse(sonatypeProjectHosting.value.map(h => url(h.homepage))), @@ -91,6 +92,7 @@ object Sonatype extends AutoPlugin { }, commands ++= Seq( sonatypeList, + sonatypeClean, sonatypeOpen, sonatypeClose, sonatypePromote, @@ -208,6 +210,8 @@ object Sonatype extends AutoPlugin { result.toSeq } + private def descriptionKeyOf(profileDescription: String) = s"[sbt-sonatype] ${profileDescription}" + val sonatypeOpen: Command = commandWithSonatypeProfileDescription("sonatypeOpen", "Create a staging repository and set publishTo") { (state, profileNameDescription) => @@ -217,16 +221,17 @@ object Sonatype extends AutoPlugin { case Right((n, d)) => (Some(n), d) } - val rest = getNexusRestService(state, profileName) + val rest = getNexusRestService(state, profileName) val repo = { - val descriptionKey = s"[sbt-sonatype] ${profileDescription}" + val descriptionKey = descriptionKeyOf(profileDescription) // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later def create = rest.createStage(descriptionKey) // Find the already opened profile or create a new one - rest.stagingRepositoryProfiles - .find(_.description == descriptionKey) - .getOrElse(create) + rest + .stagingRepositoryProfiles(warnIfMissing = false) + .find(_.description == descriptionKey) + .getOrElse(create) } val extracted = Project.extract(state) @@ -245,6 +250,32 @@ object Sonatype extends AutoPlugin { next } + val sonatypeClean: Command = { + commandWithSonatypeProfileDescription("sonatypeClean", "Clean a staging repository using a given description") { + (state, profileNameDescription) => + val (profileName: Option[String], profileDescription: String) = profileNameDescription match { + case Left(d) => + (None, d) + case Right((n, d)) => + (Some(n), d) + } + val rest = getNexusRestService(state, profileName) + + val descriptionKey = descriptionKeyOf(profileDescription) + rest + .stagingRepositoryProfiles(warnIfMissing = false) + .find(_.description == descriptionKey) + .map { repo => + state.log.info(s"Found a staging repository for ${descriptionKey}") + rest.dropStage(repo) + } + .getOrElse { + state.log.info(s"No staging repository for ${descriptionKey} is found") + } + state + } + } + val sonatypeClose: Command = commandWithRepositoryId("sonatypeClose", "Close a stage and clear publishTo if it was set by sonatypeOpen") { (state, parsed) => @@ -306,7 +337,7 @@ object Sonatype extends AutoPlugin { commandWithSonatypeProfile("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { (state, profileName) => val rest = getNexusRestService(state, profileName) - val tasks = rest.stagingRepositoryProfiles.map { repo => + val tasks = rest.stagingRepositoryProfiles().map { repo => Future.apply(rest.closeAndPromote(repo)) } val merged = Future.sequence(tasks) @@ -317,7 +348,7 @@ object Sonatype extends AutoPlugin { val sonatypeDropAll: Command = commandWithSonatypeProfile("sonatypeDropAll", "Drop all staging repositories") { (state, profileName) => val rest = getNexusRestService(state, profileName) - val dropTasks = rest.stagingRepositoryProfiles.map { repo => + val dropTasks = rest.stagingRepositoryProfiles().map { repo => Future.apply(rest.dropStage(repo)) } val merged = Future.sequence(dropTasks) @@ -343,7 +374,7 @@ object Sonatype extends AutoPlugin { val sonatypeStagingRepositoryProfiles = Command.command("sonatypeStagingRepositoryProfiles") { state => val rest = getNexusRestService(state) - val repos = rest.stagingRepositoryProfiles + val repos = rest.stagingRepositoryProfiles() val log = state.log if (repos.isEmpty) log.warn(s"No staging repository is found for ${rest.profileName}") From 42570d4b50a2262350b999fb13c331c2cc154cae Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 11:55:19 -0700 Subject: [PATCH 05/19] Refactor sonatypeOpen/Clean/Prepare --- build.sbt | 9 +-- project/build.properties | 2 +- src/main/scala/xerial/sbt/NexusClient.scala | 35 +++++++-- src/main/scala/xerial/sbt/Sonatype.scala | 80 ++++++++++++--------- 4 files changed, 82 insertions(+), 44 deletions(-) diff --git a/build.sbt b/build.sbt index 72616e4c..fdb99f60 100755 --- a/build.sbt +++ b/build.sbt @@ -29,8 +29,8 @@ lazy val buildSettings: Seq[Setting[_]] = Seq( scriptedLaunchOpts := { scriptedLaunchOpts.value ++ Seq("-Xmx1024M", "-XX:MaxPermSize=256M", "-Dplugin.version=" + version.value) }, - crossSbtVersions := Vector("1.2.8"), - releaseCrossBuild := true, + crossSbtVersions := Vector("1.3.0"), + releaseCrossBuild := false, releaseTagName := { (version in ThisBuild).value }, releasePublishArtifactsAction := PgpKeys.publishSigned.value, releaseProcess := Seq[ReleaseStep]( @@ -41,10 +41,11 @@ lazy val buildSettings: Seq[Setting[_]] = Seq( setReleaseVersion, commitReleaseVersion, tagRelease, - releaseStepCommandAndRemaining("^ publishSigned"), + releaseStepCommandAndRemaining(s"sonatypePrepare 'sbt-sonatype ${version.value}'"), + releaseStepCommandAndRemaining("publishSigned"), setNextVersion, commitNextVersion, - releaseStepCommand("sonatypeReleaseAll"), + releaseStepCommand("sonatypeRelease"), pushChanges ) ) diff --git a/project/build.properties b/project/build.properties index c8997c4a..080a737e 100755 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.0-RC5 +sbt.version=1.3.0 diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index c49b2e5c..24af19e5 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -161,9 +161,35 @@ class NexusRESTService( } finally client.getConnectionManager.shutdown() } - def stagingRepositoryProfiles(warnIfMissing:Boolean = true) = { + def openOrCreate(descriptionKey: String): StagingRepositoryProfile = { + // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later + def create = createStage(descriptionKey) + + // Find the already opened profile or create a new one + findStagingRepositoryProfileWithKey(descriptionKey) + .getOrElse(createStage(descriptionKey)) + } + + def dropIfExistsByKey(descriptionKey: String): Unit = { + // Drop the staging repository if exists + findStagingRepositoryProfileWithKey(descriptionKey) + .map { repo => + log.info(s"Found a staging repository for ${descriptionKey}") + dropStage(repo) + } + .getOrElse { + log.info(s"No staging repository for ${descriptionKey} is found") + } + } + + def findStagingRepositoryProfileWithKey(descriptionKey: String): Option[StagingRepositoryProfile] = { + stagingRepositoryProfiles(warnIfMissing = false).find(_.description == descriptionKey) + } + + def stagingRepositoryProfiles(warnIfMissing: Boolean = true) = { + val profileId = currentProfile.profileId log.info("Reading staging repository profiles...") - Get("/staging/profile_repositories") { response => + Get(s"/staging/profile_repositories/${profileId}") { response => val profileRepositoriesXML = XML.load(response.getEntity.getContent) val repositoryProfiles = for (p <- profileRepositoriesXML \\ "stagingProfileRepository") yield { StagingRepositoryProfile( @@ -200,7 +226,7 @@ class NexusRESTService( lazy val currentProfile = { val profiles = stagingProfiles if (profiles.isEmpty) { - throw new IllegalArgumentException(s"Profile ${profileName} is not found") + throw new IllegalArgumentException(s"Profile ${profileName} is not found. Check your sonatypeProfileName setting in build.sbt") } profiles.head } @@ -247,7 +273,8 @@ class NexusRESTService( def createStage(description: String = "Requested by sbt-sonatype plugin"): StagingRepositoryProfile = { val postURL = s"/staging/profiles/${currentProfile.profileId}/start" - log.info(s"Creating a staging repository in profile ${currentProfile.profileName} with a description key: ${description}") + log.info( + s"Creating a staging repository in profile ${currentProfile.profileName} with a description key: ${description}") var repo: StagingRepositoryProfile = null val ret = Post( postURL, diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 311c44fd..8f65f415 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -92,6 +92,7 @@ object Sonatype extends AutoPlugin { }, commands ++= Seq( sonatypeList, + sonatypePrepare, sonatypeClean, sonatypeOpen, sonatypeClose, @@ -212,6 +213,23 @@ object Sonatype extends AutoPlugin { private def descriptionKeyOf(profileDescription: String) = s"[sbt-sonatype] ${profileDescription}" + private def updatePublishTo(state:State, repo:StagingRepositoryProfile): State = { + val extracted = Project.extract(state) + // accumulate changes for settings for current project and all aggregates + val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => + Seq( + ref / sonatypeStagingRepositoryProfile := repo, + ref / publishTo := Some(sonatypeDefaultResolver.value) + ) + } ++ Seq( + sonatypeStagingRepositoryProfile := repo, + publishTo := Some(sonatypeDefaultResolver.value) + ) + + val next = extracted.appendWithSession(newSettings, state) + next + } + val sonatypeOpen: Command = commandWithSonatypeProfileDescription("sonatypeOpen", "Create a staging repository and set publishTo") { (state, profileNameDescription) => @@ -222,33 +240,35 @@ object Sonatype extends AutoPlugin { (Some(n), d) } val rest = getNexusRestService(state, profileName) - val repo = { - val descriptionKey = descriptionKeyOf(profileDescription) - // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later - def create = rest.createStage(descriptionKey) - - // Find the already opened profile or create a new one - rest - .stagingRepositoryProfiles(warnIfMissing = false) - .find(_.description == descriptionKey) - .getOrElse(create) + + // Re-open or create a staging repository + val repo = rest.openOrCreate(descriptionKeyOf(profileDescription)) + + updatePublishTo(state, repo) + } + + val sonatypePrepare: Command = { + commandWithSonatypeProfileDescription( + "sonatypePrepare", + "Clean (if exists) and create a staging repository using a given description") { + (state, profileNameDescription) => + val (profileName: Option[String], profileDescription: String) = profileNameDescription match { + case Left(d) => + (None, d) + case Right((n, d)) => + (Some(n), d) } - val extracted = Project.extract(state) + val rest = getNexusRestService(state, profileName) + val descriptionKey = descriptionKeyOf(profileDescription) - // accumulate changes for settings for current project and all aggregates - val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => - Seq( - ref / sonatypeStagingRepositoryProfile := repo, - ref / publishTo := Some(sonatypeDefaultResolver.value) - ) - } ++ Seq( - sonatypeStagingRepositoryProfile := repo, - publishTo := Some(sonatypeDefaultResolver.value) - ) - - val next = extracted.appendWithSession(newSettings, state) - next + // Drop a previous one + rest.dropIfExistsByKey(descriptionKey) + // Create a new one + val repo = rest.createStage(descriptionKey) + + updatePublishTo(state, repo) } + } val sonatypeClean: Command = { commandWithSonatypeProfileDescription("sonatypeClean", "Clean a staging repository using a given description") { @@ -260,18 +280,8 @@ object Sonatype extends AutoPlugin { (Some(n), d) } val rest = getNexusRestService(state, profileName) - val descriptionKey = descriptionKeyOf(profileDescription) - rest - .stagingRepositoryProfiles(warnIfMissing = false) - .find(_.description == descriptionKey) - .map { repo => - state.log.info(s"Found a staging repository for ${descriptionKey}") - rest.dropStage(repo) - } - .getOrElse { - state.log.info(s"No staging repository for ${descriptionKey} is found") - } + rest.dropIfExistsByKey(descriptionKey) state } } From 90af85295d985de871815ab152f461e29de5fb59 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 12:01:40 -0700 Subject: [PATCH 06/19] Using a single request for getting staging repositories This is for the performance reason despite it's read unnecessary repositories --- src/main/scala/xerial/sbt/NexusClient.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index 24af19e5..3c751897 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -187,9 +187,10 @@ class NexusRESTService( } def stagingRepositoryProfiles(warnIfMissing: Boolean = true) = { - val profileId = currentProfile.profileId log.info("Reading staging repository profiles...") - Get(s"/staging/profile_repositories/${profileId}") { response => + // Note: using /stging/profile_repositories/(profile id) is preferred to reduce the response size, + // but Sonatype API is quite slow (as of Sep 2019) so using a single request was much better. + Get(s"/staging/profile_repositories") { response => val profileRepositoriesXML = XML.load(response.getEntity.getContent) val repositoryProfiles = for (p <- profileRepositoriesXML \\ "stagingProfileRepository") yield { StagingRepositoryProfile( From 8ff5acd447ac946d59a0a61d1e4a15cc9a69b6d1 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 12:41:52 -0700 Subject: [PATCH 07/19] Small optimization --- src/main/scala/xerial/sbt/NexusClient.scala | 48 +++++++++++---------- src/main/scala/xerial/sbt/Sonatype.scala | 42 +++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index 3c751897..56e1fa34 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -2,13 +2,14 @@ package xerial.sbt import java.io.IOException -import org.apache.http.{HttpResponse, HttpStatus} import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials} import org.apache.http.client.HttpClient import org.apache.http.client.methods.{HttpGet, HttpPost} import org.apache.http.entity.StringEntity import org.apache.http.impl.client.DefaultHttpClient +import org.apache.http.{HttpResponse, HttpStatus} import sbt.{Credentials, DirectCredentials, Logger} +import xerial.sbt.NexusRESTService.{ActivityEvent, ActivityMonitor, Close, CloseAndPromote, CommandType, Drop, Promote, StagingActivity, StagingProfile, StagingRepositoryProfile} import scala.io.Source import scala.xml.{Utility, XML} @@ -28,9 +29,6 @@ class NexusRESTService( cred: Seq[Credentials], credentialHost: String ) { - - import NexusRESTService._ - val monitor = new ActivityMonitor(log) def findTargetRepository(command: CommandType, arg: Option[String]): StagingRepositoryProfile = { @@ -162,23 +160,26 @@ class NexusRESTService( } def openOrCreate(descriptionKey: String): StagingRepositoryProfile = { - // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later - def create = createStage(descriptionKey) - // Find the already opened profile or create a new one findStagingRepositoryProfileWithKey(descriptionKey) + .map { repo => + log.info(s"Found a staging repository ${repo}") + repo + } + // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later .getOrElse(createStage(descriptionKey)) } - def dropIfExistsByKey(descriptionKey: String): Unit = { + def dropIfExistsByKey(descriptionKey: String): Option[StagingRepositoryProfile] = { // Drop the staging repository if exists findStagingRepositoryProfileWithKey(descriptionKey) .map { repo => - log.info(s"Found a staging repository for ${descriptionKey}") + log.info(s"Found a staging repository ${repo}") dropStage(repo) } - .getOrElse { + .orElse { log.info(s"No staging repository for ${descriptionKey} is found") + None } } @@ -209,7 +210,7 @@ class NexusRESTService( } } - def stagingProfiles = { + def stagingProfiles: Seq[StagingProfile] = { log.info("Reading staging profiles...") Get("/staging/profiles") { response => val profileXML = XML.load(response.getEntity.getContent) @@ -227,7 +228,8 @@ class NexusRESTService( lazy val currentProfile = { val profiles = stagingProfiles if (profiles.isEmpty) { - throw new IllegalArgumentException(s"Profile ${profileName} is not found. Check your sonatypeProfileName setting in build.sbt") + throw new IllegalArgumentException( + s"Profile ${profileName} is not found. Check your sonatypeProfileName setting in build.sbt") } profiles.head } @@ -273,9 +275,9 @@ class NexusRESTService( } def createStage(description: String = "Requested by sbt-sonatype plugin"): StagingRepositoryProfile = { - val postURL = s"/staging/profiles/${currentProfile.profileId}/start" - log.info( - s"Creating a staging repository in profile ${currentProfile.profileName} with a description key: ${description}") + val profile = currentProfile + val postURL = s"/staging/profiles/${profile.profileId}/start" + log.info(s"Creating a staging repository in profile ${profile.profileName} with a description key: ${description}") var repo: StagingRepositoryProfile = null val ret = Post( postURL, @@ -284,10 +286,10 @@ class NexusRESTService( val xml = XML.load(in) val ids = xml \\ "data" \ "stagedRepositoryId" if (1 != ids.size) - throw new IOException(s"Failed to create repository in profile: ${currentProfile.profileName}") + throw new IOException(s"Failed to create repository in profile: ${profile.profileName}") repo = StagingRepositoryProfile( - currentProfile.profileId, - currentProfile.profileName, + profile.profileId, + profile.profileName, "open", ids.head.text, description @@ -297,12 +299,12 @@ class NexusRESTService( ) if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { throw new IOException( - s"Failed to create repository in profile: ${currentProfile.profileName}: ${ret.getStatusLine}" + s"Failed to create repository in profile: ${profile.profileName}: ${ret.getStatusLine}" ) } if (null == repo) { throw new IOException( - s"Failed to create repository in profile: ${currentProfile.profileName}: no stagedRepositoryId" + s"Failed to create repository in profile: ${profile.profileName}: no stagedRepositoryId" ) } repo @@ -317,7 +319,7 @@ class NexusRESTService( if (toContinue) { // Post close request - val postURL = s"/staging/profiles/${currentProfile.profileId}/finish" + val postURL = s"/staging/profiles/${repo.profileId}/finish" log.info(s"Closing staging repository $repo") val ret = Post(postURL, promoteRequestXML(repo)) if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { @@ -355,7 +357,7 @@ class NexusRESTService( } def dropStage(repo: StagingRepositoryProfile): StagingRepositoryProfile = { - val postURL = s"/staging/profiles/${currentProfile.profileId}/drop" + val postURL = s"/staging/profiles/${repo.profileId}/drop" log.info(s"Dropping staging repository $repo") val ret = Post(postURL, promoteRequestXML(repo)) if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { @@ -374,7 +376,7 @@ class NexusRESTService( if (toContinue) { // Post promote(release) request - val postURL = s"/staging/profiles/${currentProfile.profileId}/promote" + val postURL = s"/staging/profiles/${repo.profileId}/promote" log.info(s"Promoting staging repository $repo") val ret = Post(postURL, promoteRequestXML(repo)) if (ret.getStatusLine.getStatusCode != HttpStatus.SC_CREATED) { diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 8f65f415..91ae7e71 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -213,7 +213,8 @@ object Sonatype extends AutoPlugin { private def descriptionKeyOf(profileDescription: String) = s"[sbt-sonatype] ${profileDescription}" - private def updatePublishTo(state:State, repo:StagingRepositoryProfile): State = { + private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { + state.log.info(s"Updating publishTo settings ...") val extracted = Project.extract(state) // accumulate changes for settings for current project and all aggregates val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => @@ -230,27 +231,10 @@ object Sonatype extends AutoPlugin { next } - val sonatypeOpen: Command = - commandWithSonatypeProfileDescription("sonatypeOpen", "Create a staging repository and set publishTo") { - (state, profileNameDescription) => - val (profileName: Option[String], profileDescription: String) = profileNameDescription match { - case Left(d) => - (None, d) - case Right((n, d)) => - (Some(n), d) - } - val rest = getNexusRestService(state, profileName) - - // Re-open or create a staging repository - val repo = rest.openOrCreate(descriptionKeyOf(profileDescription)) - - updatePublishTo(state, repo) - } - val sonatypePrepare: Command = { commandWithSonatypeProfileDescription( "sonatypePrepare", - "Clean (if exists) and create a staging repository using a given description") { + "Clean (if exists) and create a staging repository using a given description, then update publishTo") { (state, profileNameDescription) => val (profileName: Option[String], profileDescription: String) = profileNameDescription match { case Left(d) => @@ -265,11 +249,27 @@ object Sonatype extends AutoPlugin { rest.dropIfExistsByKey(descriptionKey) // Create a new one val repo = rest.createStage(descriptionKey) - updatePublishTo(state, repo) } } + val sonatypeOpen: Command = + commandWithSonatypeProfileDescription( + "sonatypeOpen", + "Open (or create if not exists) to a staging repository, then update publishTo") { (state, profileNameDescription) => + val (profileName: Option[String], profileDescription: String) = profileNameDescription match { + case Left(d) => + (None, d) + case Right((n, d)) => + (Some(n), d) + } + val rest = getNexusRestService(state, profileName) + + // Re-open or create a staging repository + val repo = rest.openOrCreate(descriptionKeyOf(profileDescription)) + updatePublishTo(state, repo) + } + val sonatypeClean: Command = { commandWithSonatypeProfileDescription("sonatypeClean", "Clean a staging repository using a given description") { (state, profileNameDescription) => @@ -279,7 +279,7 @@ object Sonatype extends AutoPlugin { case Right((n, d)) => (Some(n), d) } - val rest = getNexusRestService(state, profileName) + val rest = getNexusRestService(state, profileName) val descriptionKey = descriptionKeyOf(profileDescription) rest.dropIfExistsByKey(descriptionKey) state From 2ab623088dbedf81a83505f5e4726a0c13972aaf Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 14:13:46 -0700 Subject: [PATCH 08/19] Command to tasks --- src/main/scala/xerial/sbt/NexusClient.scala | 2 +- src/main/scala/xerial/sbt/Sonatype.scala | 274 +++++++++----------- 2 files changed, 118 insertions(+), 158 deletions(-) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index 56e1fa34..c38b481a 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -159,7 +159,7 @@ class NexusRESTService( } finally client.getConnectionManager.shutdown() } - def openOrCreate(descriptionKey: String): StagingRepositoryProfile = { + def openOrCreateByKey(descriptionKey: String): StagingRepositoryProfile = { // Find the already opened profile or create a new one findStagingRepositoryProfileWithKey(descriptionKey) .map { repo => diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 91ae7e71..d1bf3830 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -28,6 +28,23 @@ object Sonatype extends AutoPlugin { val sonatypeStagingRepositoryProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") val sonatypeProjectHosting = settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") + val sonatypeList = taskKey[Unit]("list staging repositories") + val sonatypePrepare = taskKey[StagingRepositoryProfile]( + "Clean (if exists) and create a staging repository using a given description, then update publishTo") + val sonatypeClean = + taskKey[Option[StagingRepositoryProfile]]("Clean a staging repository using a given description") + val sonatypeOpen = + taskKey[StagingRepositoryProfile]("Open (or create if not exists) to a staging repository, then update publishTo") + val sonatypeClose = + inputKey[StagingRepositoryProfile]("Close a stage and clear publishTo if it was set by sonatypeOpen") + val sonatypePromote = + inputKey[StagingRepositoryProfile]("Promote a staging repository") + val sonatypeDrop = + inputKey[StagingRepositoryProfile]("Drop a staging repository") + val sonatypeRelease = + inputKey[StagingRepositoryProfile]("Publish with sonatypeClose and sonatypePromote") + val sonatypeService = taskKey[NexusRESTService]("Sonatype REST API client") + val sonatypeSessionName = settingKey[String]("Used for identifying a sonatype staging repository") } object SonatypeKeys extends SonatypeKeys {} @@ -39,6 +56,7 @@ object Sonatype extends AutoPlugin { import SonatypeCommand._ import autoImport._ + import complete.DefaultParsers._ lazy val sonatypeSettings = Seq[Def.Setting[_]]( sonatypeProfileName := organization.value, @@ -90,16 +108,87 @@ object Sonatype extends AutoPlugin { Opts.resolver.sonatypeStaging }) }, + sonatypeSessionName := s"[sbt-sonatype] ${name.value} ${version.value}", + sonatypeService := { + getNexusRestService(state.value, Some(sonatypeProfileName.value)) + }, + sonatypeList := { + val rest = sonatypeService.value + val profiles = rest.stagingProfiles + val s = state.value + val log = s.log + if (profiles.isEmpty) { + log.warn(s"No staging profile is found for ${rest.profileName}") + s.fail + } else { + log.info(s"Staging profiles (profileName:${rest.profileName}):") + log.info(profiles.mkString("\n")) + } + }, + sonatypePrepare := { + val rest = sonatypeService.value + val descriptionKey = sonatypeSessionName.value + // Drop a previous staging repository if exists + rest.dropIfExistsByKey(descriptionKey) + // Create a new one + val repo = rest.createStage(descriptionKey) + updatePublishTo(state.value, repo) + repo + }, + sonatypeClean := { + val rest = sonatypeService.value + val descriptionKey = sonatypeSessionName.value + rest.dropIfExistsByKey(descriptionKey) + }, + sonatypeOpen := { + val rest = sonatypeService.value + // Re-open or create a staging repository + val repo = rest.openOrCreateByKey(sonatypeSessionName.value) + updatePublishTo(state.value, repo) + repo + }, + sonatypeClose := { + val args = spaceDelimited("").parsed.headOption + val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(Close, repoID) + val repo2 = rest.closeStage(repo1) + val s = state.value + Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + repo2 + }, + sonatypePromote := { + val args = spaceDelimited("").parsed.headOption + val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(Promote, repoID) + val repo2 = rest.promoteStage(repo1) + val s = state.value + Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + repo2 + }, + sonatypeDrop := { + val args = spaceDelimited("").parsed.headOption + val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(Drop, repoID) + val repo2 = rest.dropStage(repo1) + val s = state.value + Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + repo2 + }, + sonatypeRelease := { + val args = spaceDelimited("").parsed.headOption + val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) + val repo2 = rest.closeAndPromote(repo1) + val s = state.value + Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + repo2 + }, commands ++= Seq( - sonatypeList, - sonatypePrepare, - sonatypeClean, - sonatypeOpen, - sonatypeClose, - sonatypePromote, - sonatypeDrop, sonatypeDropAll, - sonatypeRelease, sonatypeReleaseAll, sonatypeLog, sonatypeStagingRepositoryProfiles, @@ -107,6 +196,24 @@ object Sonatype extends AutoPlugin { ) ) + private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { + state.log.info(s"Updating publishTo settings ...") + val extracted = Project.extract(state) + // accumulate changes for settings for current project and all aggregates + val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => + Seq( + ref / sonatypeStagingRepositoryProfile := repo, + ref / publishTo := Some(sonatypeDefaultResolver.value) + ) + } ++ Seq( + sonatypeStagingRepositoryProfile := repo, + publishTo := Some(sonatypeDefaultResolver.value) + ) + + val next = extracted.appendWithSession(newSettings, state) + next + } + case class ProjectHosting( domain: String, user: String, @@ -153,7 +260,7 @@ object Sonatype extends AutoPlugin { credential } - private def getNexusRestService(state: State, profileName: Option[String] = None) = { + def getNexusRestService(state: State, profileName: Option[String] = None) = { val extracted = Project.extract(state) new NexusRESTService( state.log, @@ -164,21 +271,6 @@ object Sonatype extends AutoPlugin { ) } - val sonatypeList: Command = - Command.command("sonatypeList", "List staging repositories", "List published repository IDs") { state => - val rest = getNexusRestService(state) - val profiles = rest.stagingProfiles - val log = state.log - if (profiles.isEmpty) { - log.warn(s"No staging profile is found for ${rest.profileName}") - state.fail - } else { - log.info(s"Staging profiles (profileName:${rest.profileName}):") - log.info(profiles.mkString("\n")) - state - } - } - private val repositoryIdParser: complete.Parser[Option[String]] = (Space ~> token(StringBasic, "(repositoryId)")).?.!!!("invalid input. please input repository name") @@ -187,7 +279,7 @@ object Sonatype extends AutoPlugin { "invalid input. please input sonatypeProfile (e.g., org.xerial)" ) - private val sonatypeProfileDescriptionParser: complete.Parser[Either[String, (String, String)]] = + val sonatypeProfileDescriptionParser: complete.Parser[Either[String, (String, String)]] = Space ~> (token(StringBasic, "description") || (token(StringBasic <~ Space, "sonatypeProfile") ~ token(StringBasic, "description"))) @@ -211,138 +303,6 @@ object Sonatype extends AutoPlugin { result.toSeq } - private def descriptionKeyOf(profileDescription: String) = s"[sbt-sonatype] ${profileDescription}" - - private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { - state.log.info(s"Updating publishTo settings ...") - val extracted = Project.extract(state) - // accumulate changes for settings for current project and all aggregates - val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => - Seq( - ref / sonatypeStagingRepositoryProfile := repo, - ref / publishTo := Some(sonatypeDefaultResolver.value) - ) - } ++ Seq( - sonatypeStagingRepositoryProfile := repo, - publishTo := Some(sonatypeDefaultResolver.value) - ) - - val next = extracted.appendWithSession(newSettings, state) - next - } - - val sonatypePrepare: Command = { - commandWithSonatypeProfileDescription( - "sonatypePrepare", - "Clean (if exists) and create a staging repository using a given description, then update publishTo") { - (state, profileNameDescription) => - val (profileName: Option[String], profileDescription: String) = profileNameDescription match { - case Left(d) => - (None, d) - case Right((n, d)) => - (Some(n), d) - } - val rest = getNexusRestService(state, profileName) - val descriptionKey = descriptionKeyOf(profileDescription) - - // Drop a previous one - rest.dropIfExistsByKey(descriptionKey) - // Create a new one - val repo = rest.createStage(descriptionKey) - updatePublishTo(state, repo) - } - } - - val sonatypeOpen: Command = - commandWithSonatypeProfileDescription( - "sonatypeOpen", - "Open (or create if not exists) to a staging repository, then update publishTo") { (state, profileNameDescription) => - val (profileName: Option[String], profileDescription: String) = profileNameDescription match { - case Left(d) => - (None, d) - case Right((n, d)) => - (Some(n), d) - } - val rest = getNexusRestService(state, profileName) - - // Re-open or create a staging repository - val repo = rest.openOrCreate(descriptionKeyOf(profileDescription)) - updatePublishTo(state, repo) - } - - val sonatypeClean: Command = { - commandWithSonatypeProfileDescription("sonatypeClean", "Clean a staging repository using a given description") { - (state, profileNameDescription) => - val (profileName: Option[String], profileDescription: String) = profileNameDescription match { - case Left(d) => - (None, d) - case Right((n, d)) => - (Some(n), d) - } - val rest = getNexusRestService(state, profileName) - val descriptionKey = descriptionKeyOf(profileDescription) - rest.dropIfExistsByKey(descriptionKey) - state - } - } - - val sonatypeClose: Command = - commandWithRepositoryId("sonatypeClose", "Close a stage and clear publishTo if it was set by sonatypeOpen") { - (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(Close, repoID) - val repo2 = rest.closeStage(repo1) - val next = extracted.appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - - val sonatypePromote: Command = commandWithRepositoryId("sonatypePromote", "Promote a staged repository") { - (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(Promote, repoID) - val repo2 = rest.promoteStage(repo1) - val next = extracted.appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - - val sonatypeDrop: Command = commandWithRepositoryId("sonatypeDrop", "Drop a staging repository") { - (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(Drop, repoID) - val repo2 = rest.dropStage(repo1) - val next = extracted.appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - - val sonatypeRelease: Command = - commandWithRepositoryId("sonatypeRelease", "Publish with sonatypeClose and sonatypePromote") { (state, parsed) => - val rest = getNexusRestService(state) - val extracted = Project.extract(state) - val currentRepoID = for { - repo <- extracted.getOpt(sonatypeStagingRepositoryProfile) - } yield repo.repositoryId - val repoID = parsed.orElse(currentRepoID) - val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) - val repo2 = rest.closeAndPromote(repo1) - val next = extracted.appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), state) - next - } - val sonatypeReleaseAll: Command = commandWithSonatypeProfile("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { (state, profileName) => From 9af789c79209fcc1897ca12b63c3b31da3b6f6a4 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 14:26:09 -0700 Subject: [PATCH 09/19] Parallelize sonatypePromote --- src/main/scala/xerial/sbt/Sonatype.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index d1bf3830..30d9ed05 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -58,6 +58,8 @@ object Sonatype extends AutoPlugin { import autoImport._ import complete.DefaultParsers._ + private implicit val ec = ExecutionContext.global + lazy val sonatypeSettings = Seq[Def.Setting[_]]( sonatypeProfileName := organization.value, sonatypeRepository := "https://oss.sonatype.org/service/local", @@ -126,14 +128,18 @@ object Sonatype extends AutoPlugin { } }, sonatypePrepare := { - val rest = sonatypeService.value val descriptionKey = sonatypeSessionName.value + state.value.log.info(s"Preparing a new staging repository for ${descriptionKey}") + val rest = sonatypeService.value // Drop a previous staging repository if exists - rest.dropIfExistsByKey(descriptionKey) + val dropTask = Future.apply(rest.dropIfExistsByKey(descriptionKey)) // Create a new one - val repo = rest.createStage(descriptionKey) - updatePublishTo(state.value, repo) - repo + val createTask = Future.apply(rest.createStage(descriptionKey)) + // Run two tasks in parallel + val merged = dropTask.zip(createTask) + val (droppedRepo, createdRepo) = Await.result(merged, Duration.Inf) + updatePublishTo(state.value, createdRepo) + createdRepo }, sonatypeClean := { val rest = sonatypeService.value @@ -250,7 +256,7 @@ object Sonatype extends AutoPlugin { object SonatypeCommand { import complete.DefaultParsers._ - private implicit val ec = ExecutionContext.global + /** * Parsing repository id argument From 64acec654c31fde84e3bcfd34d4d5441bee88679 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 15:02:15 -0700 Subject: [PATCH 10/19] Change remaining commands into task --- src/main/scala/xerial/sbt/NexusClient.scala | 26 +- src/main/scala/xerial/sbt/Sonatype.scala | 287 +++++++++----------- 2 files changed, 142 insertions(+), 171 deletions(-) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index c38b481a..39150e2d 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -9,7 +9,18 @@ import org.apache.http.entity.StringEntity import org.apache.http.impl.client.DefaultHttpClient import org.apache.http.{HttpResponse, HttpStatus} import sbt.{Credentials, DirectCredentials, Logger} -import xerial.sbt.NexusRESTService.{ActivityEvent, ActivityMonitor, Close, CloseAndPromote, CommandType, Drop, Promote, StagingActivity, StagingProfile, StagingRepositoryProfile} +import xerial.sbt.NexusRESTService.{ + ActivityEvent, + ActivityMonitor, + Close, + CloseAndPromote, + CommandType, + Drop, + Promote, + StagingActivity, + StagingProfile, + StagingRepositoryProfile +} import scala.io.Source import scala.xml.{Utility, XML} @@ -162,12 +173,15 @@ class NexusRESTService( def openOrCreateByKey(descriptionKey: String): StagingRepositoryProfile = { // Find the already opened profile or create a new one findStagingRepositoryProfileWithKey(descriptionKey) - .map { repo => - log.info(s"Found a staging repository ${repo}") - repo - } + .map { repo => + log.info(s"Found a staging repository ${repo}") + repo + } // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later - .getOrElse(createStage(descriptionKey)) + .getOrElse { + log.info(s"No staging repository for ${descriptionKey} is found. Create a new one.") + createStage(descriptionKey) + } } def dropIfExistsByKey(descriptionKey: String): Option[StagingRepositoryProfile] = { diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 30d9ed05..9c91288b 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -28,13 +28,13 @@ object Sonatype extends AutoPlugin { val sonatypeStagingRepositoryProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") val sonatypeProjectHosting = settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") - val sonatypeList = taskKey[Unit]("list staging repositories") val sonatypePrepare = taskKey[StagingRepositoryProfile]( - "Clean (if exists) and create a staging repository using a given description, then update publishTo") + "Clean (if exists) and create a staging repository for releaing the current version, then update publishTo") val sonatypeClean = - taskKey[Option[StagingRepositoryProfile]]("Clean a staging repository using a given description") + taskKey[Option[StagingRepositoryProfile]]("Clean a staging repository for the current version if it exists") val sonatypeOpen = - taskKey[StagingRepositoryProfile]("Open (or create if not exists) to a staging repository, then update publishTo") + taskKey[StagingRepositoryProfile]( + "Open (or create if not exists) to a staging repository for the current version, then update publishTo") val sonatypeClose = inputKey[StagingRepositoryProfile]("Close a stage and clear publishTo if it was set by sonatypeOpen") val sonatypePromote = @@ -45,6 +45,15 @@ object Sonatype extends AutoPlugin { inputKey[StagingRepositoryProfile]("Publish with sonatypeClose and sonatypePromote") val sonatypeService = taskKey[NexusRESTService]("Sonatype REST API client") val sonatypeSessionName = settingKey[String]("Used for identifying a sonatype staging repository") + + val sonatypeReleaseAll = + inputKey[Seq[StagingRepositoryProfile]]("Publish all staging repositories to Maven central") + val sonatypeDropAll = inputKey[Seq[StagingRepositoryProfile]]("Drop all staging repositories") + val sonatypeLog = taskKey[Unit]("Show staging activity logs at Sonatype") + + val sonatypeStagingRepositoryProfiles = + taskKey[Seq[StagingRepositoryProfile]]("show the list of staging repository profiles") + val sonatypeStagingProfiles = taskKey[Seq[StagingProfile]]("show the list of staging profiles") } object SonatypeKeys extends SonatypeKeys {} @@ -54,7 +63,6 @@ object Sonatype extends AutoPlugin { override def trigger = noTrigger override def projectSettings = sonatypeSettings - import SonatypeCommand._ import autoImport._ import complete.DefaultParsers._ @@ -114,29 +122,16 @@ object Sonatype extends AutoPlugin { sonatypeService := { getNexusRestService(state.value, Some(sonatypeProfileName.value)) }, - sonatypeList := { - val rest = sonatypeService.value - val profiles = rest.stagingProfiles - val s = state.value - val log = s.log - if (profiles.isEmpty) { - log.warn(s"No staging profile is found for ${rest.profileName}") - s.fail - } else { - log.info(s"Staging profiles (profileName:${rest.profileName}):") - log.info(profiles.mkString("\n")) - } - }, sonatypePrepare := { val descriptionKey = sonatypeSessionName.value state.value.log.info(s"Preparing a new staging repository for ${descriptionKey}") - val rest = sonatypeService.value + val rest = sonatypeService.value // Drop a previous staging repository if exists val dropTask = Future.apply(rest.dropIfExistsByKey(descriptionKey)) // Create a new one val createTask = Future.apply(rest.createStage(descriptionKey)) // Run two tasks in parallel - val merged = dropTask.zip(createTask) + val merged = dropTask.zip(createTask) val (droppedRepo, createdRepo) = Await.result(merged, Duration.Inf) updatePublishTo(state.value, createdRepo) createdRepo @@ -154,52 +149,109 @@ object Sonatype extends AutoPlugin { repo }, sonatypeClose := { - val args = spaceDelimited("").parsed.headOption + val args = repositoryIdParser.parsed val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(Close, repoID) - val repo2 = rest.closeStage(repo1) - val s = state.value + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(Close, repoID) + val repo2 = rest.closeStage(repo1) + val s = state.value Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, sonatypePromote := { - val args = spaceDelimited("").parsed.headOption + val args = repositoryIdParser.parsed val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(Promote, repoID) - val repo2 = rest.promoteStage(repo1) - val s = state.value + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(Promote, repoID) + val repo2 = rest.promoteStage(repo1) + val s = state.value Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, sonatypeDrop := { - val args = spaceDelimited("").parsed.headOption + val args = repositoryIdParser.parsed val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(Drop, repoID) - val repo2 = rest.dropStage(repo1) - val s = state.value + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(Drop, repoID) + val repo2 = rest.dropStage(repo1) + val s = state.value Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, sonatypeRelease := { - val args = spaceDelimited("").parsed.headOption + val args = repositoryIdParser.parsed val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) - val repo2 = rest.closeAndPromote(repo1) - val s = state.value + val rest = sonatypeService.value + val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) + val repo2 = rest.closeAndPromote(repo1) + val s = state.value Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, - commands ++= Seq( - sonatypeDropAll, - sonatypeReleaseAll, - sonatypeLog, - sonatypeStagingRepositoryProfiles, - sonatypeStagingProfiles - ) + sonatypeReleaseAll := { + val rest = sonatypeProfileParser.parsed match { + case Some(profileName) => + getNexusRestService(state.value, Some(profileName)) + case None => + sonatypeService.value + } + + val tasks = rest.stagingRepositoryProfiles().map { repo => + Future.apply(rest.closeAndPromote(repo)) + } + val merged = Future.sequence(tasks) + Await.result(merged, Duration.Inf) + }, + sonatypeDropAll := { + val rest = sonatypeProfileParser.parsed match { + case Some(profileName) => + getNexusRestService(state.value, Some(profileName)) + case None => + sonatypeService.value + } + val dropTasks = rest.stagingRepositoryProfiles().map { repo => + Future.apply(rest.dropStage(repo)) + } + val merged = Future.sequence(dropTasks) + Await.result(merged, Duration.Inf) + }, + sonatypeLog := { + val rest = sonatypeService.value + val alist = rest.activities + val log = state.value.log + if (alist.isEmpty) + log.warn("No staging log is found") + for ((repo, activities) <- alist) { + log.info(s"Staging activities of $repo:") + for (a <- activities) { + a.log(log) + } + } + }, + sonatypeStagingRepositoryProfiles := { + val rest = sonatypeService.value + val repos = rest.stagingRepositoryProfiles() + val log = state.value.log + if (repos.isEmpty) + log.warn(s"No staging repository is found for ${rest.profileName}") + else { + log.info(s"Staging repository profiles (sonatypeProfileName:${rest.profileName}):") + log.info(repos.mkString("\n")) + } + repos + }, + sonatypeStagingProfiles := { + val rest = sonatypeService.value + val profiles = rest.stagingProfiles + val log = state.value.log + if (profiles.isEmpty) + log.warn(s"No staging profile is found for ${rest.profileName}") + else { + log.info(s"Staging profiles (sonatypeProfileName:${rest.profileName}):") + log.info(profiles.mkString("\n")) + } + profiles + } ) private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { @@ -253,126 +305,31 @@ object Sonatype extends AutoPlugin { @deprecated("Use GitLabHosting (capital L) instead", "2.2") val GitlabHosting = GitLabHosting - object SonatypeCommand { - import complete.DefaultParsers._ + private val repositoryIdParser: complete.Parser[Option[String]] = + (Space ~> token(StringBasic, "(sonatype staging repository id)")).?.!!!( + "invalid input. please input a repository id") + private val sonatypeProfileParser: complete.Parser[Option[String]] = + (Space ~> token(StringBasic, "(sonatypeProfileName)")).?.!!!( + "invalid input. please input sonatypeProfileName (e.g., org.xerial)" + ) - - /** - * Parsing repository id argument - */ - private def getCredentials(extracted: Extracted, state: State) = { - val (nextState, credential) = extracted.runTask(credentials, state) - credential - } - - def getNexusRestService(state: State, profileName: Option[String] = None) = { - val extracted = Project.extract(state) - new NexusRESTService( - state.log, - extracted.get(sonatypeRepository), - profileName.getOrElse(extracted.get(sonatypeProfileName)), - getCredentials(extracted, state), - extracted.get(sonatypeCredentialHost) - ) - } - - private val repositoryIdParser: complete.Parser[Option[String]] = - (Space ~> token(StringBasic, "(repositoryId)")).?.!!!("invalid input. please input repository name") - - private val sonatypeProfileParser: complete.Parser[Option[String]] = - (Space ~> token(StringBasic, "(sonatypeProfile)")).?.!!!( - "invalid input. please input sonatypeProfile (e.g., org.xerial)" - ) - - val sonatypeProfileDescriptionParser: complete.Parser[Either[String, (String, String)]] = - Space ~> - (token(StringBasic, "description") || - (token(StringBasic <~ Space, "sonatypeProfile") ~ token(StringBasic, "description"))) - - private def commandWithRepositoryId(name: String, briefHelp: String) = - Command(name, (name, briefHelp), briefHelp)(_ => repositoryIdParser)(_) - - private def commandWithSonatypeProfile(name: String, briefHelp: String) = - Command(name, (name, briefHelp), briefHelp)(_ => sonatypeProfileParser)(_) - - private def commandWithSonatypeProfileDescription(name: String, briefHelp: String) = - Command(name, (name, briefHelp), briefHelp)(_ => sonatypeProfileDescriptionParser)(_) - - def getUpdatedPublishTo(profileName: String, current: Option[Option[Resolver]]): Seq[Setting[_]] = { - val result = for { - currentIfSet <- current - currentPublishTo <- currentIfSet - if profileName == currentPublishTo.name - setting = publishTo := None - } yield setting - result.toSeq - } - - val sonatypeReleaseAll: Command = - commandWithSonatypeProfile("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { - (state, profileName) => - val rest = getNexusRestService(state, profileName) - val tasks = rest.stagingRepositoryProfiles().map { repo => - Future.apply(rest.closeAndPromote(repo)) - } - val merged = Future.sequence(tasks) - Await.result(merged, Duration.Inf) - state - } - - val sonatypeDropAll: Command = commandWithSonatypeProfile("sonatypeDropAll", "Drop all staging repositories") { - (state, profileName) => - val rest = getNexusRestService(state, profileName) - val dropTasks = rest.stagingRepositoryProfiles().map { repo => - Future.apply(rest.dropStage(repo)) - } - val merged = Future.sequence(dropTasks) - Await.result(merged, Duration.Inf) - state - } - - val sonatypeLog: Command = - Command.command("sonatypeLog", "Show repository activities", "Show staging activity logs at Sonatype") { state => - val rest = getNexusRestService(state) - val alist = rest.activities - val log = state.log - if (alist.isEmpty) - log.warn("No staging log is found") - for ((repo, activities) <- alist) { - log.info(s"Staging activities of $repo:") - for (a <- activities) { - a.log(log) - } - } - state - } - - val sonatypeStagingRepositoryProfiles = Command.command("sonatypeStagingRepositoryProfiles") { state => - val rest = getNexusRestService(state) - val repos = rest.stagingRepositoryProfiles() - val log = state.log - if (repos.isEmpty) - log.warn(s"No staging repository is found for ${rest.profileName}") - else { - log.info(s"Staging repository profiles (sonatypeProfileName:${rest.profileName}):") - log.info(repos.mkString("\n")) - } - state - } - - val sonatypeStagingProfiles = Command.command("sonatypeStagingProfiles") { state => - val rest = getNexusRestService(state) - val profiles = rest.stagingProfiles - val log = state.log - if (profiles.isEmpty) - log.warn(s"No staging profile is found for ${rest.profileName}") - else { - log.info(s"Staging profiles (sonatypeProfileName:${rest.profileName}):") - log.info(profiles.mkString("\n")) - } - state - } + /** + * Parsing repository id argument + */ + private def getCredentials(extracted: Extracted, state: State) = { + val (nextState, credential) = extracted.runTask(credentials, state) + credential } + private def getNexusRestService(state: State, profileName: Option[String] = None) = { + val extracted = Project.extract(state) + new NexusRESTService( + state.log, + extracted.get(sonatypeRepository), + profileName.getOrElse(extracted.get(sonatypeProfileName)), + getCredentials(extracted, state), + extracted.get(sonatypeCredentialHost) + ) + } } From acba58132114f25f2b1175f14fd7095379282e33 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 15:19:42 -0700 Subject: [PATCH 11/19] sbt-sonatype version 3.0 prep --- build.sbt | 6 +++--- project/plugins.sbt | 4 ++-- src/main/scala/xerial/sbt/Sonatype.scala | 3 --- version.sbt | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index fdb99f60..4c25e66a 100755 --- a/build.sbt +++ b/build.sbt @@ -41,11 +41,11 @@ lazy val buildSettings: Seq[Setting[_]] = Seq( setReleaseVersion, commitReleaseVersion, tagRelease, - releaseStepCommandAndRemaining(s"sonatypePrepare 'sbt-sonatype ${version.value}'"), - releaseStepCommandAndRemaining("publishSigned"), + releaseStepTask(sonatypePrepare), + releaseStepCommandAndRemaining("^ publishSigned"), setNextVersion, commitNextVersion, - releaseStepCommand("sonatypeRelease"), + releaseStepInputTask(sonatypeRelease), pushChanges ) ) diff --git a/project/plugins.sbt b/project/plugins.sbt index 1e536ac3..79f9108c 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.0-SNAPSHOT") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0-M2") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.3") libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 9c91288b..993f9b43 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -314,9 +314,6 @@ object Sonatype extends AutoPlugin { "invalid input. please input sonatypeProfileName (e.g., org.xerial)" ) - /** - * Parsing repository id argument - */ private def getCredentials(extracted: Extracted, state: State) = { val (nextState, credential) = extracted.runTask(credentials, state) credential diff --git a/version.sbt b/version.sbt index 99cfbd6c..14ec4b9c 100755 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "3.0-SNAPSHOT" +version in ThisBuild := "3.0" From 528c03bce2b7d7e0cb2f8c6c09aeff881b2b0d5a Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 15:44:05 -0700 Subject: [PATCH 12/19] cleanup multiple pre-existing repos --- README.md | 10 ++-- src/main/scala/xerial/sbt/NexusClient.scala | 54 +++++++++------------ version.sbt | 2 +- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index fad060cc..375ee3e5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A sbt plugin for publishing your project to the Maven central repository through [Sonatype Nexus repository](http://oss.sonatype.org/). - [Release notes](ReleaseNotes.md) -- sbt-sonatype is available for sbt-0.13.5 or later. +- sbt-sonatype is available for sbt 1.x series. - You can also use sbt-sonatype for [publishing non-sbt projects](README.md#publishing-maven-projects) (e.g., Maven, Gradle, etc.) @@ -35,13 +35,13 @@ A sbt plugin for publishing your project to the Maven central repository through Import ***sbt-sonatype*** plugin and [sbt-pgp plugin](http://www.scala-sbt.org/sbt-pgp/) to use `sonatypeRelease` and `publishSigned` commands: ```scala -// For sbt 0.13.x (upto sbt-sonatype 2.3) +// For sbt 1.x (sbt-sonatype 2.3 or higher) addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "(version)") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0-M2") -// For sbt 1.2.x (sbt-sonatype 2.3 or higher) +// For sbt 0.13.x (upto sbt-sonatype 2.3) addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "(version)") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") ``` * If downloading the sbt-sonatype plugin fails, check the repository in the Maven central: . It will be usually synced within 10 minutes. diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index 39150e2d..d8374237 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -9,18 +9,6 @@ import org.apache.http.entity.StringEntity import org.apache.http.impl.client.DefaultHttpClient import org.apache.http.{HttpResponse, HttpStatus} import sbt.{Credentials, DirectCredentials, Logger} -import xerial.sbt.NexusRESTService.{ - ActivityEvent, - ActivityMonitor, - Close, - CloseAndPromote, - CommandType, - Drop, - Promote, - StagingActivity, - StagingProfile, - StagingRepositoryProfile -} import scala.io.Source import scala.xml.{Utility, XML} @@ -40,6 +28,8 @@ class NexusRESTService( cred: Seq[Credentials], credentialHost: String ) { + import xerial.sbt.NexusRESTService._ + val monitor = new ActivityMonitor(log) def findTargetRepository(command: CommandType, arg: Option[String]): StagingRepositoryProfile = { @@ -172,33 +162,37 @@ class NexusRESTService( def openOrCreateByKey(descriptionKey: String): StagingRepositoryProfile = { // Find the already opened profile or create a new one - findStagingRepositoryProfileWithKey(descriptionKey) - .map { repo => - log.info(s"Found a staging repository ${repo}") - repo - } + val repos = findStagingRepositoryProfilesWithKey(descriptionKey) + if (repos.size > 1) { + throw new IllegalStateException( + s"Multiple staging repositories for ${descriptionKey} exists. Run sonatypeDropAll first to clean up old repositories") + } else if (repos.size == 1) { + val repo = repos.head + log.info(s"Found a staging repository ${repo}") + repo + } else { // Create a new staging repository by appending [sbt-sonatype] prefix to its description so that we can find the repository id later - .getOrElse { - log.info(s"No staging repository for ${descriptionKey} is found. Create a new one.") - createStage(descriptionKey) - } + log.info(s"No staging repository for ${descriptionKey} is found. Create a new one.") + createStage(descriptionKey) + } } def dropIfExistsByKey(descriptionKey: String): Option[StagingRepositoryProfile] = { // Drop the staging repository if exists - findStagingRepositoryProfileWithKey(descriptionKey) - .map { repo => + val repos = findStagingRepositoryProfilesWithKey(descriptionKey) + if (repos.isEmpty) { + log.info(s"No staging repository for ${descriptionKey} is found") + None + } else { + repos.map { repo => log.info(s"Found a staging repository ${repo}") dropStage(repo) - } - .orElse { - log.info(s"No staging repository for ${descriptionKey} is found") - None - } + }.lastOption + } } - def findStagingRepositoryProfileWithKey(descriptionKey: String): Option[StagingRepositoryProfile] = { - stagingRepositoryProfiles(warnIfMissing = false).find(_.description == descriptionKey) + def findStagingRepositoryProfilesWithKey(descriptionKey: String): Seq[StagingRepositoryProfile] = { + stagingRepositoryProfiles(warnIfMissing = false).filter(_.description == descriptionKey) } def stagingRepositoryProfiles(warnIfMissing: Boolean = true) = { diff --git a/version.sbt b/version.sbt index 14ec4b9c..99cfbd6c 100755 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "3.0" +version in ThisBuild := "3.0-SNAPSHOT" From 57eb831e78fb19a56b35afe1a55653e704d589e7 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 15:47:48 -0700 Subject: [PATCH 13/19] Setting version to 3.0 --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 99cfbd6c..14ec4b9c 100755 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "3.0-SNAPSHOT" +version in ThisBuild := "3.0" From 5cc985984537c87e82fe201cad8310cf8ab3f64e Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 15:57:27 -0700 Subject: [PATCH 14/19] Setting version to 3.1-SNAPSHOT --- version.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.sbt b/version.sbt index 14ec4b9c..135f3ef5 100755 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "3.0" +version in ThisBuild := "3.1-SNAPSHOT" From 5c0bd75ae8a7de59c0932e9f82480e07d80ef2aa Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 16:18:46 -0700 Subject: [PATCH 15/19] Force overwrite previous settings --- build.sbt | 4 ++-- src/main/scala/xerial/sbt/NexusClient.scala | 3 ++- src/main/scala/xerial/sbt/Sonatype.scala | 8 ++++---- version.sbt | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index 4c25e66a..d30cb902 100755 --- a/build.sbt +++ b/build.sbt @@ -42,10 +42,10 @@ lazy val buildSettings: Seq[Setting[_]] = Seq( commitReleaseVersion, tagRelease, releaseStepTask(sonatypePrepare), - releaseStepCommandAndRemaining("^ publishSigned"), + releaseStepCommandAndRemaining("publishSigned"), + releaseStepInputTask(sonatypeRelease), setNextVersion, commitNextVersion, - releaseStepInputTask(sonatypeRelease), pushChanges ) ) diff --git a/src/main/scala/xerial/sbt/NexusClient.scala b/src/main/scala/xerial/sbt/NexusClient.scala index d8374237..2146b39e 100644 --- a/src/main/scala/xerial/sbt/NexusClient.scala +++ b/src/main/scala/xerial/sbt/NexusClient.scala @@ -60,7 +60,8 @@ class NexusRESTService( arg.map(findSpecifiedInArg).getOrElse { if (repos.size > 1) { log.error(s"Multiple repositories are found:\n${repos.mkString("\n")}") - log.error(s"Specify one of the repository ids in the command line") + log.error( + s"Specify one of the repository ids in the command line or run sonatypeDropAll to cleanup repositories") throw new IllegalStateException("Found multiple staging repositories") } else { repos.head diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 993f9b43..571336a6 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -155,7 +155,7 @@ object Sonatype extends AutoPlugin { val repo1 = rest.findTargetRepository(Close, repoID) val repo2 = rest.closeStage(repo1) val s = state.value - Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, sonatypePromote := { @@ -165,7 +165,7 @@ object Sonatype extends AutoPlugin { val repo1 = rest.findTargetRepository(Promote, repoID) val repo2 = rest.promoteStage(repo1) val s = state.value - Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, sonatypeDrop := { @@ -175,7 +175,7 @@ object Sonatype extends AutoPlugin { val repo1 = rest.findTargetRepository(Drop, repoID) val repo2 = rest.dropStage(repo1) val s = state.value - Project.extract(s).appendWithSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) repo2 }, sonatypeRelease := { @@ -268,7 +268,7 @@ object Sonatype extends AutoPlugin { publishTo := Some(sonatypeDefaultResolver.value) ) - val next = extracted.appendWithSession(newSettings, state) + val next = extracted.appendWithoutSession(newSettings, state) next } diff --git a/version.sbt b/version.sbt index 135f3ef5..99cfbd6c 100755 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "3.1-SNAPSHOT" +version in ThisBuild := "3.0-SNAPSHOT" From 432212729fcb99048ef81cb96759e887a1c29a75 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 16:53:50 -0700 Subject: [PATCH 16/19] WIP --- src/main/scala/xerial/sbt/Sonatype.scala | 45 +++++++++++++----------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 571336a6..c46575b6 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -20,12 +20,12 @@ import scala.concurrent.{Await, ExecutionContext, Future} object Sonatype extends AutoPlugin { trait SonatypeKeys { - val sonatypeRepository = settingKey[String]("Sonatype repository URL: e.g. https://oss.sonatype.org/service/local") - val sonatypeProfileName = settingKey[String]("Profile name at Sonatype: e.g. org.xerial") - val sonatypeCredentialHost = settingKey[String]("Credential host. Default is oss.sonatype.org") - val sonatypeDefaultResolver = settingKey[Resolver]("Default Sonatype Resolver") - val sonatypePublishTo = settingKey[Option[Resolver]]("Default Sonatype publishTo target") - val sonatypeStagingRepositoryProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") + val sonatypeRepository = settingKey[String]("Sonatype repository URL: e.g. https://oss.sonatype.org/service/local") + val sonatypeProfileName = settingKey[String]("Profile name at Sonatype: e.g. org.xerial") + val sonatypeCredentialHost = settingKey[String]("Credential host. Default is oss.sonatype.org") + val sonatypeDefaultResolver = settingKey[Resolver]("Default Sonatype Resolver") + val sonatypePublishTo = settingKey[Option[Resolver]]("Default Sonatype publishTo target") + val sonatypeTargetStagingProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") val sonatypeProjectHosting = settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") val sonatypePrepare = taskKey[StagingRepositoryProfile]( @@ -105,7 +105,7 @@ object Sonatype extends AutoPlugin { sonatypePublishTo := Some(sonatypeDefaultResolver.value), sonatypeDefaultResolver := { val sonatypeRepo = "https://oss.sonatype.org/" - val profileM = sonatypeStagingRepositoryProfile.?.value + val profileM = sonatypeTargetStagingProfile.?.value val staged = profileM.map { stagingRepoProfile => "releases" at sonatypeRepo + @@ -145,47 +145,48 @@ object Sonatype extends AutoPlugin { val rest = sonatypeService.value // Re-open or create a staging repository val repo = rest.openOrCreateByKey(sonatypeSessionName.value) - updatePublishTo(state.value, repo) + val next = updatePublishTo(state.value, repo) + state.value.setNext(next) repo }, sonatypeClose := { val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) val rest = sonatypeService.value val repo1 = rest.findTargetRepository(Close, repoID) val repo2 = rest.closeStage(repo1) val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) repo2 }, sonatypePromote := { val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) val rest = sonatypeService.value val repo1 = rest.findTargetRepository(Promote, repoID) val repo2 = rest.promoteStage(repo1) val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) repo2 }, sonatypeDrop := { val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) val rest = sonatypeService.value val repo1 = rest.findTargetRepository(Drop, repoID) val repo2 = rest.dropStage(repo1) val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) repo2 }, sonatypeRelease := { val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeStagingRepositoryProfile.?.value.map(_.repositoryId)) + val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) val rest = sonatypeService.value val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) val repo2 = rest.closeAndPromote(repo1) val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeStagingRepositoryProfile := repo2), s) + Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) repo2 }, sonatypeReleaseAll := { @@ -255,17 +256,19 @@ object Sonatype extends AutoPlugin { ) private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { - state.log.info(s"Updating publishTo settings ...") + val extracted = Project.extract(state) // accumulate changes for settings for current project and all aggregates + val resolver = "releases" at s"https://oss.sonatype.org/service/local/staging/deployByRepositoryId/${repo.repositoryId}" + state.log.info(s"Updating publishTo settings to ${resolver}") val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => Seq( - ref / sonatypeStagingRepositoryProfile := repo, - ref / publishTo := Some(sonatypeDefaultResolver.value) + ref / sonatypeTargetStagingProfile := repo, + ref / publishTo := Some(resolver) ) } ++ Seq( - sonatypeStagingRepositoryProfile := repo, - publishTo := Some(sonatypeDefaultResolver.value) + sonatypeTargetStagingProfile := repo, + publishTo := Some(resolver) ) val next = extracted.appendWithoutSession(newSettings, state) From dc3bdf22fa1312904ffd7dbe30c979a4157cf672 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 17:37:01 -0700 Subject: [PATCH 17/19] Use commands to chagne project settings --- build.sbt | 4 +- src/main/scala/xerial/sbt/Sonatype.scala | 244 ++++++++++++----------- 2 files changed, 127 insertions(+), 121 deletions(-) diff --git a/build.sbt b/build.sbt index d30cb902..f50e0a60 100755 --- a/build.sbt +++ b/build.sbt @@ -41,9 +41,9 @@ lazy val buildSettings: Seq[Setting[_]] = Seq( setReleaseVersion, commitReleaseVersion, tagRelease, - releaseStepTask(sonatypePrepare), + releaseStepCommand("sonatypePrepare"), releaseStepCommandAndRemaining("publishSigned"), - releaseStepInputTask(sonatypeRelease), + releaseStepCommand("sonatypeRelease"), setNextVersion, commitNextVersion, pushChanges diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index c46575b6..02deb5ce 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -28,28 +28,10 @@ object Sonatype extends AutoPlugin { val sonatypeTargetStagingProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") val sonatypeProjectHosting = settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") - val sonatypePrepare = taskKey[StagingRepositoryProfile]( - "Clean (if exists) and create a staging repository for releaing the current version, then update publishTo") - val sonatypeClean = - taskKey[Option[StagingRepositoryProfile]]("Clean a staging repository for the current version if it exists") - val sonatypeOpen = - taskKey[StagingRepositoryProfile]( - "Open (or create if not exists) to a staging repository for the current version, then update publishTo") - val sonatypeClose = - inputKey[StagingRepositoryProfile]("Close a stage and clear publishTo if it was set by sonatypeOpen") - val sonatypePromote = - inputKey[StagingRepositoryProfile]("Promote a staging repository") - val sonatypeDrop = - inputKey[StagingRepositoryProfile]("Drop a staging repository") - val sonatypeRelease = - inputKey[StagingRepositoryProfile]("Publish with sonatypeClose and sonatypePromote") val sonatypeService = taskKey[NexusRESTService]("Sonatype REST API client") val sonatypeSessionName = settingKey[String]("Used for identifying a sonatype staging repository") - val sonatypeReleaseAll = - inputKey[Seq[StagingRepositoryProfile]]("Publish all staging repositories to Maven central") - val sonatypeDropAll = inputKey[Seq[StagingRepositoryProfile]]("Drop all staging repositories") - val sonatypeLog = taskKey[Unit]("Show staging activity logs at Sonatype") + val sonatypeLog = taskKey[Unit]("Show staging activity logs at Sonatype") val sonatypeStagingRepositoryProfiles = taskKey[Seq[StagingRepositoryProfile]]("show the list of staging repository profiles") @@ -60,7 +42,7 @@ object Sonatype extends AutoPlugin { object autoImport extends SonatypeKeys {} - override def trigger = noTrigger + override def trigger = allRequirements override def projectSettings = sonatypeSettings import autoImport._ @@ -122,100 +104,17 @@ object Sonatype extends AutoPlugin { sonatypeService := { getNexusRestService(state.value, Some(sonatypeProfileName.value)) }, - sonatypePrepare := { - val descriptionKey = sonatypeSessionName.value - state.value.log.info(s"Preparing a new staging repository for ${descriptionKey}") - val rest = sonatypeService.value - // Drop a previous staging repository if exists - val dropTask = Future.apply(rest.dropIfExistsByKey(descriptionKey)) - // Create a new one - val createTask = Future.apply(rest.createStage(descriptionKey)) - // Run two tasks in parallel - val merged = dropTask.zip(createTask) - val (droppedRepo, createdRepo) = Await.result(merged, Duration.Inf) - updatePublishTo(state.value, createdRepo) - createdRepo - }, - sonatypeClean := { - val rest = sonatypeService.value - val descriptionKey = sonatypeSessionName.value - rest.dropIfExistsByKey(descriptionKey) - }, - sonatypeOpen := { - val rest = sonatypeService.value - // Re-open or create a staging repository - val repo = rest.openOrCreateByKey(sonatypeSessionName.value) - val next = updatePublishTo(state.value, repo) - state.value.setNext(next) - repo - }, - sonatypeClose := { - val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(Close, repoID) - val repo2 = rest.closeStage(repo1) - val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) - repo2 - }, - sonatypePromote := { - val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(Promote, repoID) - val repo2 = rest.promoteStage(repo1) - val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) - repo2 - }, - sonatypeDrop := { - val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(Drop, repoID) - val repo2 = rest.dropStage(repo1) - val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) - repo2 - }, - sonatypeRelease := { - val args = repositoryIdParser.parsed - val repoID = args.headOption.orElse(sonatypeTargetStagingProfile.?.value.map(_.repositoryId)) - val rest = sonatypeService.value - val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) - val repo2 = rest.closeAndPromote(repo1) - val s = state.value - Project.extract(s).appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), s) - repo2 - }, - sonatypeReleaseAll := { - val rest = sonatypeProfileParser.parsed match { - case Some(profileName) => - getNexusRestService(state.value, Some(profileName)) - case None => - sonatypeService.value - } - - val tasks = rest.stagingRepositoryProfiles().map { repo => - Future.apply(rest.closeAndPromote(repo)) - } - val merged = Future.sequence(tasks) - Await.result(merged, Duration.Inf) - }, - sonatypeDropAll := { - val rest = sonatypeProfileParser.parsed match { - case Some(profileName) => - getNexusRestService(state.value, Some(profileName)) - case None => - sonatypeService.value - } - val dropTasks = rest.stagingRepositoryProfiles().map { repo => - Future.apply(rest.dropStage(repo)) - } - val merged = Future.sequence(dropTasks) - Await.result(merged, Duration.Inf) - }, + commands ++= Seq( + sonatypePrepare, + sonatypeOpen, + sonatypeClose, + sonatypePromote, + sonatypeDrop, + sonatypeRelease, + sonatypeClean, + sonatypeReleaseAll, + sonatypeDropAll + ), sonatypeLog := { val rest = sonatypeService.value val alist = rest.activities @@ -255,26 +154,125 @@ object Sonatype extends AutoPlugin { } ) - private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { + private val sonatypePrepare = newCommand( + "sonatypePrepare", + "Clean (if exists) and create a staging repository for releasing the current version, then update publishTo") { + state: State => + val extracted = Project.extract(state) + val descriptionKey = extracted.get(sonatypeSessionName) + state.log.info(s"Preparing a new staging repository for ${descriptionKey}") + val rest: NexusRESTService = getNexusRestService(state) + // Drop a previous staging repository if exists + val dropTask = Future.apply(rest.dropIfExistsByKey(descriptionKey)) + // Create a new one + val createTask = Future.apply(rest.createStage(descriptionKey)) + // Run two tasks in parallel + val merged = dropTask.zip(createTask) + val (droppedRepo, createdRepo) = Await.result(merged, Duration.Inf) + updatePublishTo(state, createdRepo) + } + private val sonatypeOpen = newCommand( + "sonatypeOpen", + "Open (or create if not exists) to a staging repository for the current version, then update publishTo") { + state: State => + val rest = getNexusRestService(state) + // Re-open or create a staging repository + val descriptionKey = Project.extract(state).get(sonatypeSessionName) + val repo = rest.openOrCreateByKey(descriptionKey) + updatePublishTo(state, repo) + } + + private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { val extracted = Project.extract(state) // accumulate changes for settings for current project and all aggregates - val resolver = "releases" at s"https://oss.sonatype.org/service/local/staging/deployByRepositoryId/${repo.repositoryId}" - state.log.info(s"Updating publishTo settings to ${resolver}") + state.log.info(s"Updating publishTo settings...") val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => Seq( ref / sonatypeTargetStagingProfile := repo, - ref / publishTo := Some(resolver) + ref / publishTo := Some(sonatypeDefaultResolver.value) ) } ++ Seq( sonatypeTargetStagingProfile := repo, - publishTo := Some(resolver) + publishTo := Some(sonatypeDefaultResolver.value) ) val next = extracted.appendWithoutSession(newSettings, state) next } + private val sonatypeClose = commandWithRepositoryId("sonatypeClose", "") { (state: State, arg: Option[String]) => + val extracted = Project.extract(state) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val rest = getNexusRestService(state) + val repo1 = rest.findTargetRepository(Close, repoID) + val repo2 = rest.closeStage(repo1) + extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + } + + private val sonatypePromote = commandWithRepositoryId("sonatypePromote", "Promote a staging repository") { + (state: State, arg: Option[String]) => + val extracted = Project.extract(state) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val rest = getNexusRestService(state) + val repo1 = rest.findTargetRepository(Promote, repoID) + val repo2 = rest.promoteStage(repo1) + extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + } + + private val sonatypeDrop = commandWithRepositoryId("sonatypeDrop", "Drop a staging repository") { + (state: State, arg: Option[String]) => + val extracted = Project.extract(state) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val rest = getNexusRestService(state) + val repo1 = rest.findTargetRepository(Drop, repoID) + val repo2 = rest.dropStage(repo1) + extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + } + + private val sonatypeRelease = + commandWithRepositoryId("sonatypeRelease", "Publish with sonatypeClose and sonatypePromote") { + (state: State, arg: Option[String]) => + val extracted = Project.extract(state) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val rest = getNexusRestService(state) + val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) + val repo2 = rest.closeAndPromote(repo1) + extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + } + + private val sonatypeClean = + newCommand("sonatypeClean", "Clean a staging repository for the current version if it exists") { state: State => + val extracted = Project.extract(state) + val rest = getNexusRestService(state) + val descriptionKey = extracted.get(sonatypeSessionName) + rest.dropIfExistsByKey(descriptionKey) + state + } + + private val sonatypeReleaseAll = + commandWithRepositoryId("sonatypeReleaseAll", "Publish all staging repositories to Maven central") { + (state: State, arg: Option[String]) => + val rest = getNexusRestService(state, arg) + val tasks = rest.stagingRepositoryProfiles().map { repo => + Future.apply(rest.closeAndPromote(repo)) + } + val merged = Future.sequence(tasks) + Await.result(merged, Duration.Inf) + state + } + + private val sonatypeDropAll = commandWithRepositoryId("sonatypeDropAll", "Drop all staging repositories") { + (state: State, arg: Option[String]) => + val rest = getNexusRestService(state, arg) + val dropTasks = rest.stagingRepositoryProfiles().map { repo => + Future.apply(rest.dropStage(repo)) + } + val merged = Future.sequence(dropTasks) + Await.result(merged, Duration.Inf) + state + } + case class ProjectHosting( domain: String, user: String, @@ -332,4 +330,12 @@ object Sonatype extends AutoPlugin { extracted.get(sonatypeCredentialHost) ) } + + private def newCommand(name: String, briefHelp: String)(body: State => State) = { + Command.command(name, briefHelp, briefHelp)(body) + } + + private def commandWithRepositoryId(name: String, briefHelp: String) = + Command(name, (name, briefHelp), briefHelp)(_ => repositoryIdParser)(_) + } From 32ee13b95a29aa0d729498f279d737b5a6c833ab Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 18:18:27 -0700 Subject: [PATCH 18/19] Do not override publishTo directory --- README.md | 102 +++++++++------- build.sbt | 6 +- sonatype.sbt | 2 - src/main/scala/xerial/sbt/Sonatype.scala | 141 +++++++++++------------ 4 files changed, 131 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 375ee3e5..732ccf91 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,27 @@ sbt-sonatype plugin ====== -A sbt plugin for publishing your project to the Maven central repository through the REST API of Sonatype Nexus. Deploying artifacts to Sonatype repository is a requirement for synchronizing your projects to the [Maven central repository](http://repo1.maven.org/maven2/). __sbt-sonatype__ plugin enables two-step release of Scala/Java projects. +A sbt plugin for publishing your project to the Maven central repository through the REST API of Sonatype Nexus. Deploying artifacts to Sonatype repository is a requirement for synchronizing your projects to the [Maven central repository](http://repo1.maven.org/maven2/). __sbt-sonatype__ plugin enables three-step release of Scala/Java projects. + + * `sonatypePrepare` + * Prepare a staging repository at Sonatype. It will also clean up previously created staging repositories. This step is added since sbt-sonatype 3.0 to resume the entire release process from scratch. + * `publishSigned` (with [sbt-pgp plugin](http://www.scala-sbt.org/sbt-pgp/)) + * Upload GPG signed artifacts to Sonatype repository + * `sonatypeRelease` + * Perform the close and release steps in the Sonatype Nexus repository. + + After these steps, your project will be synchronized to the Maven central (usually) within ten minutes. No longer need to enter the web interface of + [Sonatype Nexus repository](http://oss.sonatype.org/) to performe these release steps. - * First `publishSigned` (with [sbt-pgp plugin](http://www.scala-sbt.org/sbt-pgp/)) - * Next `sonatypeRelease` to perform the close and release steps in the Sonatype Nexus repository. - * Done. Your project will be synchronized to the Maven central within tens of minutes. No longer need to enter the web interface of - [Sonatype Nexus repository](http://oss.sonatype.org/). - [Release notes](ReleaseNotes.md) - sbt-sonatype is available for sbt 1.x series. - You can also use sbt-sonatype for [publishing non-sbt projects](README.md#publishing-maven-projects) (e.g., Maven, Gradle, etc.) - ## Prerequisites * Create a Sonatype Repository account - * Follow the instruction in the [Central Repository documentation site](http://central.sonatype.org). + * Follow the instruction in the [Central Repository documentation site](http://central.sonatype.org). * Create a Sonatype account * Create a GPG key * Open a JIRA ticket to get a permission for synchronizing your project to the Central Repository (aka Maven Central). @@ -49,8 +54,11 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") ### build.sbt ```scala -// Add the default sonatype repository setting +// [Important] Use publishTo settings set by sbt-sonatype plugin publishTo := sonatypePublishTo.value + +// [Optional] If you need to manage unique session names, change this setting: +sonatypeSessionName := s"[sbt-sonatype] ${name.value} ${version.value}", ``` ### $HOME/.sbt/(sbt-version 0.13 or 1.0)/sonatype.sbt @@ -129,45 +137,56 @@ $ sbt sonatypeRelease ``` This command accesses [Sonatype Nexus REST API](https://oss.sonatype.org/nexus-staging-plugin/default/docs/index.html), then sends close and promote commands. -## Available Commands - -* __sonatypeList__ - * Show the list of staging repositories. -* __sonatypeOpen__ (description | sonatypeProfileName description) (since sbt-sonatype-1.1) - * Create a staging repository and set `sonatypeStagingRepositoryProfile` and `publishTo`. - * Although creating a staging repository does not result in email notifications, - the description will be reused for across lifecycle operations (Close, Promote, Drop) - to facilitate distinguishing email notifications sent by the repository by description. -* __sonatypeClose__ (repositoryId)? - * Close an open staging repository and set `sonatypeStagingRepositoryProfile` and - clear `publishTo` if it was set by __sonatypeOpen__. - * The `Staging Completed` email notification sent by the repository only includes the description - (if created with __sonatypeOpen__); it does not include the staging repository ID. -* __sonatypePromote__ (repositoryId)? - * Promote a closed staging repository and set `sonatypeStagingRepositoryProfile` and - clear `publishTo` if it was set by __sonatypeOpen__. - * The `Promotion Completed` email notification sent by the repository only includes the description - (if created with __sonatypeOpen__); it does not include the staging repository ID. -* __sonatypeDrop__ (repositoryId)? - * Drop an open or closed staging repository and set `sonatypeStagingRepositoryProfile` and - clear `publishTo` if it was set by __sonatypeOpen__. - * The email notification sent by the repository includes both the description - (if created with __sonatypeOpen__) and the staging repository ID. -* __sonatypeDropAll__ - * Drop all staging repositories. +## Commands + +### Basic Commands +* __sonatypePrepare__ + * Drop (if exists) and create a new staging repository using `sonatypeSessionName` as a unique key + * For cross-build projects, make sure running this command only once at the beginning of the release process. Then run `sonatypeOpen` for each build to reuse the already created stging repository. +* __sonatypeOpen__ + * Open the existing staging repository using `sonatypeSessionName` as a unique key. If it doesn't exist, create a new one. It will update`sonatypePublishTo` + * This command is useful to run `publishSigned` task in parallel. * __sonatypeRelease__ (repositoryId)? - * Close (if needed) and promote a staging repository and set `sonatypeStagingRepositoryProfile` and - clear `publishTo` if it was set by __sonatypeOpen__. - * The email notifications are those of __sonatypeClose__ (if applicable) and __sonatypePromote__. + * Close (if needed) and promote a staging repository. After this command, the uploaded artifacts will be synchronized to Maven central. + +### Batch Operations +* __sonatypeDropAll__ (sonatypeProfileName)? + * Drop all staging repositories. * __sonatypeReleaseAll__ (sonatypeProfileName)? * Close and promote all staging repositories (Useful for cross-building projects) + +## Others * __sonatypeStagingProfiles__ * Show the list of staging profiles, which include profileName information. * __sonatypeLog__ * Show the staging activity logs - -### Advanced Usage - +* __sonatypeClose__ + * Close the open staging repository (= requirement verification) +* __sonatypePromote__ + * Promote the closed staging repository (= sync to Maven central) +* __sonatypeDrop__ + * Drop an open or closed staging repository + +## Uploading Artifacts In Parallel + +Since sbt-sonatype 3.x, it supports session based release flows: + +### Single Module Projects + - sonatypePrepare + - publishSigned + - sonatypeRelease + +### Multi Module Projects + - Run `sonatypePrepare` in a single step. + - You must wait for the completion of this step + - Then, start uploading signed artifacts using multiple processes: + - P1: `; sonatypeOpen; publishSigned` + - P2: `; sonatypeOpen; publishSigned` + - P3: ... + - Wait for all upload completion + - Finally, run `sonatypeRelease` + +For sbt-sonatype 2.x: * [Example workflow for creating & publishing to a staging repository](workflow.md) ## Using with sbt-release plugin @@ -186,11 +205,12 @@ releaseProcess := Seq[ReleaseStep]( setReleaseVersion, commitReleaseVersion, tagRelease, + releaseStepCommand("sonatypePrepare"), // For non cross-build projects, use releaseStepCommand("publishSigned") releaseStepCommandAndRemaining("+publishSigned"), + releaseStepCommand("sonatypeRelease"), setNextVersion, commitNextVersion, - releaseStepCommand("sonatypeReleaseAll"), pushChanges ) ``` diff --git a/build.sbt b/build.sbt index f50e0a60..61eeade8 100755 --- a/build.sbt +++ b/build.sbt @@ -50,8 +50,6 @@ lazy val buildSettings: Seq[Setting[_]] = Seq( ) ) -val AIRFRAME_VERSION = "19.9.2" - // Project modules lazy val sbtSonatype = project @@ -62,8 +60,6 @@ lazy val sbtSonatype = buildSettings, testFrameworks += new TestFramework("wvlet.airspec.Framework"), libraryDependencies ++= Seq( - "org.apache.httpcomponents" % "httpclient" % "4.2.6", - "org.wvlet.airframe" %% "airframe-http-finagle" % AIRFRAME_VERSION, - "org.wvlet.airframe" %% "airspec" % AIRFRAME_VERSION % "test" + "org.apache.httpcomponents" % "httpclient" % "4.2.6" ) ) diff --git a/sonatype.sbt b/sonatype.sbt index c4d5d5e1..8b2be178 100644 --- a/sonatype.sbt +++ b/sonatype.sbt @@ -2,8 +2,6 @@ import xerial.sbt.Sonatype._ publishMavenStyle := true -enablePlugins(Sonatype) - sonatypeProfileName := "org.xerial" sonatypeProjectHosting := Some(GitHubHosting(user="xerial", repository="sbt-sonatype", email="leo@xerial.org")) developers := List( diff --git a/src/main/scala/xerial/sbt/Sonatype.scala b/src/main/scala/xerial/sbt/Sonatype.scala index 02deb5ce..25971b22 100755 --- a/src/main/scala/xerial/sbt/Sonatype.scala +++ b/src/main/scala/xerial/sbt/Sonatype.scala @@ -20,22 +20,15 @@ import scala.concurrent.{Await, ExecutionContext, Future} object Sonatype extends AutoPlugin { trait SonatypeKeys { - val sonatypeRepository = settingKey[String]("Sonatype repository URL: e.g. https://oss.sonatype.org/service/local") - val sonatypeProfileName = settingKey[String]("Profile name at Sonatype: e.g. org.xerial") - val sonatypeCredentialHost = settingKey[String]("Credential host. Default is oss.sonatype.org") - val sonatypeDefaultResolver = settingKey[Resolver]("Default Sonatype Resolver") - val sonatypePublishTo = settingKey[Option[Resolver]]("Default Sonatype publishTo target") - val sonatypeTargetStagingProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") + val sonatypeRepository = settingKey[String]("Sonatype repository URL: e.g. https://oss.sonatype.org/service/local") + val sonatypeProfileName = settingKey[String]("Profile name at Sonatype: e.g. org.xerial") + val sonatypeCredentialHost = settingKey[String]("Credential host. Default is oss.sonatype.org") + val sonatypeDefaultResolver = settingKey[Resolver]("Default Sonatype Resolver") + val sonatypePublishTo = settingKey[Option[Resolver]]("Default Sonatype publishTo target") + val sonatypeTargetRepositoryProfile = settingKey[StagingRepositoryProfile]("Stating repository profile") val sonatypeProjectHosting = settingKey[Option[ProjectHosting]]("Shortcut to fill in required Maven Central information") - val sonatypeService = taskKey[NexusRESTService]("Sonatype REST API client") val sonatypeSessionName = settingKey[String]("Used for identifying a sonatype staging repository") - - val sonatypeLog = taskKey[Unit]("Show staging activity logs at Sonatype") - - val sonatypeStagingRepositoryProfiles = - taskKey[Seq[StagingRepositoryProfile]]("show the list of staging repository profiles") - val sonatypeStagingProfiles = taskKey[Seq[StagingProfile]]("show the list of staging profiles") } object SonatypeKeys extends SonatypeKeys {} @@ -87,7 +80,7 @@ object Sonatype extends AutoPlugin { sonatypePublishTo := Some(sonatypeDefaultResolver.value), sonatypeDefaultResolver := { val sonatypeRepo = "https://oss.sonatype.org/" - val profileM = sonatypeTargetStagingProfile.?.value + val profileM = sonatypeTargetRepositoryProfile.?.value val staged = profileM.map { stagingRepoProfile => "releases" at sonatypeRepo + @@ -101,9 +94,6 @@ object Sonatype extends AutoPlugin { }) }, sonatypeSessionName := s"[sbt-sonatype] ${name.value} ${version.value}", - sonatypeService := { - getNexusRestService(state.value, Some(sonatypeProfileName.value)) - }, commands ++= Seq( sonatypePrepare, sonatypeOpen, @@ -113,45 +103,11 @@ object Sonatype extends AutoPlugin { sonatypeRelease, sonatypeClean, sonatypeReleaseAll, - sonatypeDropAll - ), - sonatypeLog := { - val rest = sonatypeService.value - val alist = rest.activities - val log = state.value.log - if (alist.isEmpty) - log.warn("No staging log is found") - for ((repo, activities) <- alist) { - log.info(s"Staging activities of $repo:") - for (a <- activities) { - a.log(log) - } - } - }, - sonatypeStagingRepositoryProfiles := { - val rest = sonatypeService.value - val repos = rest.stagingRepositoryProfiles() - val log = state.value.log - if (repos.isEmpty) - log.warn(s"No staging repository is found for ${rest.profileName}") - else { - log.info(s"Staging repository profiles (sonatypeProfileName:${rest.profileName}):") - log.info(repos.mkString("\n")) - } - repos - }, - sonatypeStagingProfiles := { - val rest = sonatypeService.value - val profiles = rest.stagingProfiles - val log = state.value.log - if (profiles.isEmpty) - log.warn(s"No staging profile is found for ${rest.profileName}") - else { - log.info(s"Staging profiles (sonatypeProfileName:${rest.profileName}):") - log.info(profiles.mkString("\n")) - } - profiles - } + sonatypeDropAll, + sonatypeLog, + sonatypeStagingRepositoryProfiles, + sonatypeStagingProfiles + ) ) private val sonatypePrepare = newCommand( @@ -169,7 +125,7 @@ object Sonatype extends AutoPlugin { // Run two tasks in parallel val merged = dropTask.zip(createTask) val (droppedRepo, createdRepo) = Await.result(merged, Duration.Inf) - updatePublishTo(state, createdRepo) + updatePublishSettings(state, createdRepo) } private val sonatypeOpen = newCommand( @@ -180,21 +136,19 @@ object Sonatype extends AutoPlugin { // Re-open or create a staging repository val descriptionKey = Project.extract(state).get(sonatypeSessionName) val repo = rest.openOrCreateByKey(descriptionKey) - updatePublishTo(state, repo) + updatePublishSettings(state, repo) } - private def updatePublishTo(state: State, repo: StagingRepositoryProfile): State = { + private def updatePublishSettings(state: State, repo: StagingRepositoryProfile): State = { val extracted = Project.extract(state) // accumulate changes for settings for current project and all aggregates - state.log.info(s"Updating publishTo settings...") + state.log.info(s"Updating sonatypePublishTo settings...") val newSettings: Seq[Def.Setting[_]] = extracted.currentProject.referenced.flatMap { ref => Seq( - ref / sonatypeTargetStagingProfile := repo, - ref / publishTo := Some(sonatypeDefaultResolver.value) + ref / sonatypeTargetRepositoryProfile := repo ) } ++ Seq( - sonatypeTargetStagingProfile := repo, - publishTo := Some(sonatypeDefaultResolver.value) + sonatypeTargetRepositoryProfile := repo ) val next = extracted.appendWithoutSession(newSettings, state) @@ -203,42 +157,42 @@ object Sonatype extends AutoPlugin { private val sonatypeClose = commandWithRepositoryId("sonatypeClose", "") { (state: State, arg: Option[String]) => val extracted = Project.extract(state) - val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetRepositoryProfile).map(_.repositoryId)) val rest = getNexusRestService(state) val repo1 = rest.findTargetRepository(Close, repoID) val repo2 = rest.closeStage(repo1) - extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + extracted.appendWithoutSession(Seq(sonatypeTargetRepositoryProfile := repo2), state) } private val sonatypePromote = commandWithRepositoryId("sonatypePromote", "Promote a staging repository") { (state: State, arg: Option[String]) => val extracted = Project.extract(state) - val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetRepositoryProfile).map(_.repositoryId)) val rest = getNexusRestService(state) val repo1 = rest.findTargetRepository(Promote, repoID) val repo2 = rest.promoteStage(repo1) - extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + extracted.appendWithoutSession(Seq(sonatypeTargetRepositoryProfile := repo2), state) } private val sonatypeDrop = commandWithRepositoryId("sonatypeDrop", "Drop a staging repository") { (state: State, arg: Option[String]) => val extracted = Project.extract(state) - val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetRepositoryProfile).map(_.repositoryId)) val rest = getNexusRestService(state) val repo1 = rest.findTargetRepository(Drop, repoID) val repo2 = rest.dropStage(repo1) - extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + extracted.appendWithoutSession(Seq(sonatypeTargetRepositoryProfile := repo2), state) } private val sonatypeRelease = commandWithRepositoryId("sonatypeRelease", "Publish with sonatypeClose and sonatypePromote") { (state: State, arg: Option[String]) => val extracted = Project.extract(state) - val repoID = arg.orElse(extracted.getOpt(sonatypeTargetStagingProfile).map(_.repositoryId)) + val repoID = arg.orElse(extracted.getOpt(sonatypeTargetRepositoryProfile).map(_.repositoryId)) val rest = getNexusRestService(state) val repo1 = rest.findTargetRepository(CloseAndPromote, repoID) val repo2 = rest.closeAndPromote(repo1) - extracted.appendWithoutSession(Seq(sonatypeTargetStagingProfile := repo2), state) + extracted.appendWithoutSession(Seq(sonatypeTargetRepositoryProfile := repo2), state) } private val sonatypeClean = @@ -273,6 +227,49 @@ object Sonatype extends AutoPlugin { state } + private val sonatypeLog = newCommand("sonatypeLog", "Show staging activity logs at Sonatype") { state: State => + val rest = getNexusRestService(state) + val alist = rest.activities + val log = state.log + if (alist.isEmpty) + log.warn("No staging log is found") + for ((repo, activities) <- alist) { + log.info(s"Staging activities of $repo:") + for (a <- activities) { + a.log(log) + } + } + state + } + + private val sonatypeStagingRepositoryProfiles = + newCommand("sonatypeStagingRepositoryProfiles", "Show the list of staging repository profiles") { state: State => + val rest = getNexusRestService(state) + val repos = rest.stagingRepositoryProfiles() + val log = state.log + if (repos.isEmpty) + log.warn(s"No staging repository is found for ${rest.profileName}") + else { + log.info(s"Staging repository profiles (sonatypeProfileName:${rest.profileName}):") + log.info(repos.mkString("\n")) + } + state + } + + private val sonatypeStagingProfiles = newCommand("sonatypeStagingProfiles", "Show the list of staging profiles") { + state: State => + val rest = getNexusRestService(state) + val profiles = rest.stagingProfiles + val log = state.log + if (profiles.isEmpty) + log.warn(s"No staging profile is found for ${rest.profileName}") + else { + log.info(s"Staging profiles (sonatypeProfileName:${rest.profileName}):") + log.info(profiles.mkString("\n")) + } + state + } + case class ProjectHosting( domain: String, user: String, From 3c7b4dedfb73e5dd7b7ba3e8ff99df568273e366 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Wed, 4 Sep 2019 18:20:25 -0700 Subject: [PATCH 19/19] Fix scripted tests --- src/sbt-test/sbt-sonatype/example/build.sbt | 15 +-------------- src/sbt-test/sbt-sonatype/operations/build.sbt | 2 -- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/sbt-test/sbt-sonatype/example/build.sbt b/src/sbt-test/sbt-sonatype/example/build.sbt index a526576c..e93a8cb6 100644 --- a/src/sbt-test/sbt-sonatype/example/build.sbt +++ b/src/sbt-test/sbt-sonatype/example/build.sbt @@ -1,29 +1,16 @@ organization := "org.xerial.example" - -enablePlugins(Sonatype) - sonatypeProfileName := "org.xerial" - publishMavenStyle := true - licenses := Seq("APL2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")) - homepage := Some(url("https://github.com/xerial/sbt-sonatype")) - scmInfo := Some( ScmInfo( url("https://github.com/xerial/sbt-sonatype"), "scm:git@github.com:xerial/sbt-sonatype.git" ) ) - developers := List( Developer(id = "leo", name = "Taro L. Saito", email = "leo@xerial.org", url = url("http://xerial.org/leo")) ) +publishTo := sonatypePublishTo.value -publishTo := Some( - if (isSnapshot.value) - Opts.resolver.sonatypeSnapshots - else - Opts.resolver.sonatypeStaging -) diff --git a/src/sbt-test/sbt-sonatype/operations/build.sbt b/src/sbt-test/sbt-sonatype/operations/build.sbt index 11b61972..11f2f9f5 100644 --- a/src/sbt-test/sbt-sonatype/operations/build.sbt +++ b/src/sbt-test/sbt-sonatype/operations/build.sbt @@ -1,5 +1,3 @@ -enablePlugins(Sonatype) - organization := System.getProperty("organization", "org.xerial.operations") sonatypeProfileName := System.getProperty("profile.name", "org.xerial") version := System.getProperty("version", "0.1")