aboutsummaryrefslogtreecommitdiff
path: root/plugins/sonatype-release/src/sonatype/SonatypeHttpApi.scala
blob: e90b81dcff9478358196708159bdbe019da84a9c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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)
    )
}