diff options
Diffstat (limited to 'plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala')
-rw-r--r-- | plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala b/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala new file mode 100644 index 0000000..e90b81d --- /dev/null +++ b/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala @@ -0,0 +1,215 @@ +package cbt.sonatype + +import java.util.Base64 + +import scala.xml.XML + +/** + * Interface for Sonatype staging plugin HTTP API. + * All resources are described here: + * https://oss.sonatype.org/nexus-staging-plugin/default/docs/index.html + * + * Publish proccess via HTTP API described here: + * https://support.sonatype.com/hc/en-us/articles/213465868-Uploading-to-a-Staging-Repository-via-REST-API?page=1#comment_204178478 + */ +private final class SonatypeHttpApi(sonatypeURI: String, sonatypeCredentials: String, profileName: String)(log: String => Unit) { + import HttpUtils._ + + private val base64Credentials = new String(Base64.getEncoder.encode(sonatypeCredentials.getBytes)) + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles.html + def getStagingProfile: StagingProfile = { + log(s"Retrieving info for profile: $profileName") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/profiles", + headers = Map("Authorization" -> s"Basic $base64Credentials") + ) + + val currentProfile = (XML.loadString(response) \\ "stagingProfile" find { profile => + (profile \ "name").headOption.exists(_.text == profileName) + }).getOrElse(throw new Exception(s"Failed to get profile with name: $profileName")) + + StagingProfile( + id = (currentProfile \ "id").head.text, + name = (currentProfile \ "name").head.text, + repositoryTargetId = (currentProfile \ "repositoryTargetId").head.text, + resourceURI = (currentProfile \ "resourceURI").head.text + ) + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profile_repositories_-profileIdKey-.html + def getStagingRepos(profile: StagingProfile): Seq[StagingRepository] = { + log(s"Retrieving staging repositories for profile: $profileName") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/profile_repositories/${profile.id}", + headers = Map( + "Authorization" -> s"Basic $base64Credentials" + ) + ) + + (XML.loadString(response) \\ "stagingProfileRepository") map extractStagingRepository + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_repository_-repositoryIdKey-.html + private def getStagingRepoById(repoId: StagingRepositoryId): StagingRepository = { + log(s"Retrieving staging repo with id: ${repoId.repositoryId}") + val (_, response) = GET( + uri = s"$sonatypeURI/staging/repository/${repoId.repositoryId}", + headers = Map( + "Authorization" -> s"Basic $base64Credentials" + ) + ) + + extractStagingRepository(XML.loadString(response)) + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_start.html + def createStagingRepo(profile: StagingProfile): StagingRepositoryId = { + log(s"Creating staging repositories for profile: $profileName") + val (responseCode, response) = POST( + uri = profile.resourceURI + "/start", + body = createRequestBody("Create staging repository [CBT]").getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + + require(responseCode == 201, s"Create staging repo response code. Expected: 201, got: $responseCode") + + val optRepositoryId = (XML.loadString(response) \ "data" \ "stagedRepositoryId").headOption.map(e => StagingRepositoryId(e.text)) + + optRepositoryId.getOrElse(throw new Exception(s"Malformed response. Failed to get id of created staging repo")) + } + + def finishRelease(repo: StagingRepository, profile: StagingProfile): Unit = { + val repoId = StagingRepositoryId(repo.repositoryId) + repo.state match { + case Open => + closeStagingRepo(profile, repoId) + promoteStagingRepo(profile, repoId) + dropStagingRepo(profile, repoId) + case Closed => + promoteStagingRepo(profile, repoId) + dropStagingRepo(profile, repoId) + case Released => + dropStagingRepo(profile, repoId) + case Unknown(status) => + throw new Exception(s"Got repo in status: ${status}, can't finish release.") + } + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_finish.html + private def closeStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Closing staging repo: ${repoId.repositoryId}") + val (responseCode, _) = POST( + uri = profile.resourceURI + "/finish", + body = promoteRequestBody( + repoId.repositoryId, + "Close staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + + require(responseCode == 201, s"Close staging repo response code. Expected: 201, got: $responseCode") + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_promote.html + // You can promote repository only when it is in "closed" state. + private def promoteStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Promoting staging repo: ${repoId.repositoryId}") + val responseCode = withRetry { + // need to get fresh info about this repo + val repoState = try getStagingRepoById(repoId) catch { + case e: Exception => + throw new Exception(s"Repository with id ${repoId.repositoryId} not found. Maybe it was dropped already", e) + } + + if(repoState.state == Closed) { + val (code, _) = POST( + uri = profile.resourceURI + "/promote", + body = promoteRequestBody( + repoId.repositoryId, + "Promote staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + code + } else { + throw new Exception(s"Can't promote, repository ${repoId.repositoryId} is not in closed state yet!") + } + } + + require(responseCode == 201, s"Promote staging repo response code. Expected: 201, got: $responseCode") + } + + // https://oss.sonatype.org/nexus-staging-plugin/default/docs/path__staging_profiles_-profileIdKey-_drop.html + // It's safe to drop repository in "released" state. + private def dropStagingRepo(profile: StagingProfile, repoId: StagingRepositoryId): Unit = { + log(s"Dropping staging repo: ${repoId.repositoryId}") + val responseCode = withRetry { + // need to get fresh info about this repo + val repoState = try getStagingRepoById(repoId) catch { + case e: Exception => + throw new Exception(s"Repository with id ${repoId.repositoryId} not found. Maybe it was dropped already", e) + } + + if (repoState.state == Released) { + val (code, _) = POST( + uri = profile.resourceURI + "/drop", + body = promoteRequestBody( + repoId.repositoryId, + "Drop staging repository [CBT]", + profile.repositoryTargetId + ).getBytes, + headers = Map( + "Authorization" -> s"Basic $base64Credentials", + "Content-Type" -> "application/xml" + ) + ) + code + } else { + throw new Exception(s"Can't drop, repository ${repoId.repositoryId} is not in released state yet!") + } + } + require(responseCode == 201, s"Drop staging repo response code. Expected: 201, got: $responseCode") + } + + private def promoteRequestBody(repoId: String, description: String, targetRepoId: String) = + s""" + |<promoteRequest> + | <data> + | <stagedRepositoryId>$repoId</stagedRepositoryId> + | <description>$description</description> + | <targetRepositoryId>$targetRepoId</targetRepositoryId> + | </data> + |</promoteRequest> + """.stripMargin + + + private def createRequestBody(description: String) = + s""" + |<promoteRequest> + | <data> + | <description>$description</description> + | </data> + |</promoteRequest> + """.stripMargin + + private def extractStagingRepository(repo: xml.Node): StagingRepository = + StagingRepository( + (repo \ "profileId").head.text, + (repo \ "profileName").head.text, + (repo \ "repositoryId").head.text, + RepositoryState.fromString((repo \ "type").head.text) + ) +} + |