aboutsummaryrefslogtreecommitdiff
path: root/plugins/sonatype-release/src/sonatype/SonatypeLib.scala
blob: 4666950f047e4e6ba237f4c815e49cf3d84b9e21 (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
package cbt.sonatype

import java.io.File
import java.net.URL
import java.nio.file.Files._
import java.nio.file.Paths

import cbt.{ ExitCode, Lib }

/**
  * Sonatype release process is:
  * • get your profile info to publish artifacts
  * • open staging repository to publish artifacts
  * • publish signed artifacts and signatures to staging repository
  * • close staging repository
  * • promote staging repository
  * • drop staging repository
  */

object SonatypeLib {

  val sonatypeServiceURI: String = "https://oss.sonatype.org/service/local"

  val sonatypeSnapshotsURI: String = "https://oss.sonatype.org/content/repositories/snapshots"

  /**
    * login:password for Sonatype access.
    * Order of credentials lookup:
    * • environment variables SONATYPE_USERNAME and SONATYPE_PASSWORD
    * • ~/.cbt/sonatype-credentials
    */
  def sonatypeCredentials: String = {
    def fromEnv = for {
      username <- Option(System.getenv("SONATYPE_USERNAME"))
      password <- Option(System.getenv("SONATYPE_PASSWORD"))
    } yield s"$username:$password"

    def fromFile = {
      for {
        home <- Option(System.getProperty("user.home"))
        credsPath = Paths.get(home, ".cbt", "sonatype-credentials")
      } yield new String(readAllBytes(credsPath)).trim
    }

    fromEnv
      .orElse(fromFile)
      .getOrElse(throw new Exception(
        "No Sonatype credentials found! You can provide them via SONATYPE_USERNAME, SONATYPE_PASSWORD env variables, " +
          "or in ~/.cbt/sonatype-credentials file as login:password"
      ))
  }
}

final class SonatypeLib(
  sonatypeServiceURI: String,
  sonatypeSnapshotsURI: String,
  sonatypeCredentials: String,
  profileName: String
)( lib: Lib, log: String => Unit ) {

  private val sonatypeApi = new SonatypeHttpApi(sonatypeServiceURI, sonatypeCredentials, profileName)(sonatypeLogger)

  /*
   * Signed publish steps:
   * • create new staging repo
   * • create artifacts and sign them
   * • publish jars to created repo
  */
  def sonatypePublishSigned(
    sourceFiles: Seq[File],
    artifacts: Seq[File],
    groupId: String,
    artifactId: String,
    version: String,
    isSnapshot: Boolean,
    scalaMajorVersion: String
  ): ExitCode = {
    if(sourceFiles.nonEmpty) {
      System.err.println(lib.blue("Staring publishing to Sonatype."))

      val profile = getStagingProfile()

      val deployURI = (if (isSnapshot) {
        sonatypeSnapshotsURI
      } else {
        val repoId = sonatypeApi.createStagingRepo(profile)
        s"${sonatypeServiceURI}/staging/deployByRepositoryId/${repoId.repositoryId}"
      }) + s"/${groupId.replace(".", "/")}/${artifactId}_${scalaMajorVersion}/${version}"

      lib.publishSigned(
        artifacts = artifacts,
        url = new URL(deployURI),
        credentials = Some(sonatypeCredentials)
      )
      System.err.println(lib.green("Successfully published artifacts to Sonatype."))
      ExitCode.Success
    } else {
      System.err.println(lib.red("Sources are empty, won't publish empty jar."))
      ExitCode.Failure
    }
  }

  /**
    * Release is:
    * • find staging repo related to current profile;
    * • close this staging repo;
    * • wait until this repo is released;
    * • drop this repo.
    */
  def sonatypeRelease(
    groupId: String,
    artifactId: String,
    version: String
  ): ExitCode = {
    val profile = getStagingProfile()

    sonatypeApi.getStagingRepos(profile).toList match {
      case Nil =>
        System.err.println(lib.red("No staging repositories found, you need to publish artifacts first."))
        ExitCode.Failure
      case repo :: Nil =>
        sonatypeApi.finishRelease(repo, profile)
        System.err.println(lib.green(s"Successfully released ${groupId}/${artifactId} v:${version}"))
        ExitCode.Success
      case repos =>
        val showRepo = { r: StagingRepository => s"${r.repositoryId} in state: ${r.state}" }
        val toRelease = lib.pickOne(lib.blue(s"More than one staging repo found. Select one of them:"), repos)(showRepo)

        toRelease map { repo =>
          sonatypeApi.finishRelease(repo, profile)
          System.err.println(lib.green(s"Successfully released ${groupId}/${artifactId} v:${version}"))
          ExitCode.Success
        } getOrElse {
          System.err.println(lib.red("Wrong repository number, try again please."))
          ExitCode.Failure
        }
    }
  }

  private def getStagingProfile() =
    try {
      sonatypeApi.getStagingProfile
    } catch {
      case e: Exception => throw new Exception(s"Failed to get info for profile: $profileName", e)
    }

  private def sonatypeLogger: String => Unit = lib.logger.log("Sonatype", _)

}