All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.com.vanniktech.maven.publish.nexus.Nexus.kt Maven / Gradle / Ivy

package com.vanniktech.maven.publish.nexus

import java.io.IOException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

class Nexus(
  private val baseUrl: String,
  private val username: String,
  password: String,
  userAgentName: String,
  userAgentVersion: String,
  okhttpTimeoutSeconds: Long,
  private val closeTimeoutSeconds: Long,
) {
  private val service by lazy {
    val okHttpClient = OkHttpClient.Builder()
      .addInterceptor(NexusOkHttpInterceptor(username, password, userAgentName, userAgentVersion))
      .connectTimeout(okhttpTimeoutSeconds, TimeUnit.SECONDS)
      .readTimeout(okhttpTimeoutSeconds, TimeUnit.SECONDS)
      .writeTimeout(okhttpTimeoutSeconds, TimeUnit.SECONDS)
      .build()
    val retrofit = Retrofit.Builder()
      .addConverterFactory(MoshiConverterFactory.create())
      .client(okHttpClient)
      .baseUrl(baseUrl)
      .build()

    retrofit.create(NexusService::class.java)
  }

  private fun getProfiles(): List? {
    val stagingProfilesResponse = service.getStagingProfiles().execute()

    if (!stagingProfilesResponse.isSuccessful) {
      throw IOException("Cannot get stagingProfiles for account $username: ${stagingProfilesResponse.errorBody()?.string()}")
    }

    return stagingProfilesResponse.body()?.data
  }

  private fun findStagingProfile(group: String): StagingProfile {
    val allProfiles = getProfiles() ?: emptyList()

    if (allProfiles.isEmpty()) {
      throw IllegalArgumentException("No staging profiles found in account $username. Make sure you called \"./gradlew publish\".")
    }

    if (allProfiles.size == 1) {
      return allProfiles[0]
    }

    val candidateProfiles = allProfiles.filter { group == it.name }
      .ifEmpty { allProfiles.filter { group.startsWith(it.name) } }
      .ifEmpty { allProfiles.filter { group.commonPrefixWith(it.name).isNotEmpty() } }

    if (candidateProfiles.isEmpty()) {
      throw IllegalArgumentException(
        "No matching staging profile found in account $username. It is expected that the account contains a staging " +
          "profile that matches or is the start of $group. " +
          "Available profiles are: ${allProfiles.joinToString(separator = ", ") { it.name }}",
      )
    }

    if (candidateProfiles.size > 1) {
      throw IllegalArgumentException(
        "More than 1 matching staging profile found in account $username. " +
          "Available profiles are: ${allProfiles.joinToString(separator = ", ") { it.name }}",
      )
    }

    return candidateProfiles[0]
  }

  private fun createStagingRepository(group: String, profile: StagingProfile): String {
    println("Creating repository in profile: ${profile.name}")

    val response = service.createRepository(profile.id, CreateRepositoryInput(CreateRepositoryInputData("Repository for $group"))).execute()
    if (!response.isSuccessful) {
      throw IOException("Cannot create repository: ${response.errorBody()?.string()}")
    }

    val id = response.body()?.data?.stagedRepositoryId ?: throw IOException("Did not receive created repository")

    println("Created staging repository $id")

    return id
  }

  fun createRepositoryForGroup(group: String): String {
    val profile = findStagingProfile(group)
    return createStagingRepository(group, profile)
  }

  private fun getProfileRepositories(): List? {
    val profileRepositoriesResponse = service.getProfileRepositories().execute()

    if (!profileRepositoriesResponse.isSuccessful) {
      throw IOException("Cannot get profileRepositories for account $username: ${profileRepositoriesResponse.errorBody()?.string()}")
    }

    return profileRepositoriesResponse.body()?.data
  }

  private fun findStagingRepository(): Repository {
    val allRepositories = getProfileRepositories() ?: emptyList()

    if (allRepositories.isEmpty()) {
      throw IllegalArgumentException(
        "No matching staging repository found in account $username. You can can explicitly choose one by " +
          "passing it as an option like this \"./gradlew closeAndReleaseRepository --repository=comexample-123\". " +
          "Available repositories are: ${allRepositories.joinToString(separator = ", ") { it.repositoryId }}",
      )
    }

    if (allRepositories.size > 1) {
      throw IllegalArgumentException(
        "More than 1 matching staging repository found in account $username. You can can explicitly choose " +
          "one by passing it as an option like this \"./gradlew closeAndReleaseRepository --repository comexample-123\". " +
          "Available repositories are: ${allRepositories.joinToString(separator = ", ") { it.repositoryId }}",
      )
    }
    return allRepositories[0]
  }

  private fun getStagingRepository(repositoryId: String): Repository {
    val repositoryResponse = service.getRepository(repositoryId).execute()

    if (!repositoryResponse.isSuccessful) {
      throw IOException("Cannot get repository with id $repositoryId for account $username: ${repositoryResponse.errorBody()?.string()}")
    }

    return repositoryResponse.body()
      ?: throw IOException("Could not get repository with id $repositoryId for account $username")
  }

  private fun closeStagingRepository(stagingRepository: Repository) {
    val repositoryId = stagingRepository.repositoryId

    if (stagingRepository.type == "closed") {
      if (stagingRepository.transitioning) {
        waitForClose(stagingRepository.repositoryId)
      } else {
        println("Repository $repositoryId already closed")
      }
      return
    }

    if (stagingRepository.type != "open") {
      throw IllegalArgumentException("Repository $repositoryId is of type '${stagingRepository.type}' and not 'open'")
    }

    println("Closing repository: $repositoryId")
    val response = service.closeRepository(TransitionRepositoryInput(TransitionRepositoryInputData(listOf(repositoryId)))).execute()
    if (!response.isSuccessful) {
      throw IOException("Cannot close repository: ${response.errorBody()?.string()}")
    }

    waitForClose(repositoryId)
    println("Repository $repositoryId closed")
  }

  private fun waitForClose(repositoryId: String) {
    val startMillis = System.currentTimeMillis()

    val waitingChars = listOf(
      PROGRESS_1,
      PROGRESS_2,
      PROGRESS_3,
      PROGRESS_4,
      PROGRESS_5,
      PROGRESS_6,
      PROGRESS_7,
    )
    var i = 0
    while (true) {
      if (System.currentTimeMillis() - startMillis > TimeUnit.SECONDS.toMillis(closeTimeoutSeconds)) {
        throw IOException("Timeout waiting for repository close")
      }

      print("\r${waitingChars[i++ % waitingChars.size]} waiting for close...")
      System.out.flush()

      Thread.sleep(CLOSE_WAIT_INTERVAL_MILLIS)

      val repository = try {
        getStagingRepository(repositoryId)
      } catch (e: IOException) {
        System.err.println("Exception trying to get repository status: ${e.message}")
        null
      } catch (e: TimeoutException) {
        System.err.println("Exception trying to get repository status: ${e.message}")
        null
      }

      if (repository?.type == "closed" && !repository.transitioning) {
        break
      }
      if (repository?.type == "open" && !repository.transitioning && repository.notifications > 0) {
        val properties = try {
          val response = service.getRepositoryActivity(repositoryId).execute()
          if (response.isSuccessful) {
            response.body()?.find { it.name == "close" }
              ?.events?.find { it.name == "ruleFailed" }
              ?.properties?.filter { it.name == "failureMessage" }
          } else {
            emptyList()
          }
        } catch (_: IOException) {
          emptyList()
        }

        if (properties.isNullOrEmpty()) {
          val url = baseUrl.toHttpUrl().newBuilder("/#stagingRepositories").toString()
          throw IOException("Closing the repository failed. ${repository.notifications} messages are available on $url")
        } else {
          val message = properties.joinToString("\n") { it.value }
          throw IOException("Closing the repository failed with the following errors:\n$message")
        }
      }
    }
  }

  fun closeCurrentStagingRepository(): String {
    val stagingRepository = findStagingRepository()
    closeStagingRepository(stagingRepository)
    return stagingRepository.repositoryId
  }

  fun closeStagingRepository(repositoryId: String) {
    val stagingRepository = getStagingRepository(repositoryId)
    closeStagingRepository(stagingRepository)
  }

  fun releaseStagingRepository(repositoryId: String) {
    println("Releasing repository: $repositoryId")
    val response = service.releaseRepository(
      TransitionRepositoryInput(
        TransitionRepositoryInputData(
          stagedRepositoryIds = listOf(repositoryId),
          autoDropAfterRelease = true,
        ),
      ),
    ).execute()

    if (!response.isSuccessful) {
      throw IOException("Cannot release repository: ${response.errorBody()?.string()}")
    }

    println("Repository $repositoryId released")
  }

  fun dropStagingRepository(repositoryId: String) {
    val response = service.dropRepository(
      TransitionRepositoryInput(
        TransitionRepositoryInputData(
          stagedRepositoryIds = listOf(repositoryId),
        ),
      ),
    ).execute()

    if (!response.isSuccessful) {
      throw IOException("Cannot drop repository: ${response.errorBody()?.string()}")
    }
  }

  fun dropCurrentStagingRepository() {
    val stagingRepository = findStagingRepository()
    dropStagingRepository(stagingRepository.repositoryId)
  }

  companion object {
    private const val PROGRESS_1 = "\u2839"
    private const val PROGRESS_2 = "\u2838"
    private const val PROGRESS_3 = "\u2834"
    private const val PROGRESS_4 = "\u2826"
    private const val PROGRESS_5 = "\u2807"
    private const val PROGRESS_6 = "\u280F"
    private const val PROGRESS_7 = "\u2819"

    private const val CLOSE_WAIT_INTERVAL_MILLIS = 10_000L
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy