Skip to content

Show release notes URL from POM in PR body #2863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ final class Context[F[_]](implicit
val logger: Logger[F],
val mavenAlg: MavenAlg[F],
val millAlg: MillAlg[F],
val nurtureAlg: NurtureAlg[F],
val pruningAlg: PruningAlg[F],
val pullRequestRepository: PullRequestRepository[F],
val refreshErrorAlg: RefreshErrorAlg[F],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,24 @@

package org.scalasteward.core.coursier

import cats.Parallel
import cats.effect.Async
import cats.implicits._
import cats.{Applicative, Parallel}
import coursier.cache.{CachePolicy, FileCache}
import coursier.core.{Authentication, Project}
import coursier.{Fetch, Info, Module, ModuleName, Organization}
import org.http4s.Uri
import coursier.{Fetch, Module, ModuleName, Organization}
import org.scalasteward.core.data.Resolver.Credentials
import org.scalasteward.core.data.{Dependency, Resolver, Scope, Version}
import org.scalasteward.core.data.{Dependency, Resolver, Version}
import org.scalasteward.core.util.uri
import org.typelevel.log4cats.Logger

/** An interface to [[https://get-coursier.io Coursier]] used for fetching dependency versions and
* metadata.
*/
trait CoursierAlg[F[_]] {
def getArtifactUrl(dependency: Scope.Dependency): F[Option[Uri]]
def getMetadata(dependency: Dependency, resolvers: List[Resolver]): F[DependencyMetadata]

def getVersions(dependency: Dependency, resolver: Resolver): F[List[Version]]

final def getArtifactIdUrlMapping(dependencies: Scope.Dependencies)(implicit
F: Applicative[F]
): F[Map[String, Uri]] =
dependencies.sequence
.traverseFilter(dep => getArtifactUrl(dep).map(_.map(dep.value.artifactId.name -> _)))
.map(_.toMap)
}

object CoursierAlg {
Expand All @@ -50,64 +42,68 @@ object CoursierAlg {
parallel: Parallel[F],
F: Async[F]
): CoursierAlg[F] = {
val fetch: Fetch[F] = Fetch[F](FileCache[F]())
val fetch: Fetch[F] =
Fetch[F](FileCache[F]())

val cacheNoTtl: FileCache[F] =
FileCache[F]().withTtl(None).withCachePolicies(List(CachePolicy.Update))

new CoursierAlg[F] {
override def getArtifactUrl(dependency: Scope.Dependency): F[Option[Uri]] =
convertToCoursierTypes(dependency).flatMap((getArtifactUrlImpl _).tupled)
override def getMetadata(
dependency: Dependency,
resolvers: List[Resolver]
): F[DependencyMetadata] =
resolvers.traverseFilter(convertResolver(_).attempt.map(_.toOption)).flatMap {
repositories =>
val csrDependency = toCoursierDependency(dependency)
getMetadataImpl(csrDependency, repositories, DependencyMetadata.empty)
}

private def getArtifactUrlImpl(
private def getMetadataImpl(
dependency: coursier.Dependency,
repositories: List[coursier.Repository]
): F[Option[Uri]] = {
repositories: List[coursier.Repository],
acc: DependencyMetadata
): F[DependencyMetadata] = {
val fetchArtifacts = fetch
.withArtifactTypes(Set(coursier.Type.pom, coursier.Type.ivy))
.withDependencies(List(dependency))
.withRepositories(repositories)

fetchArtifacts.ioResult.attempt.flatMap {
case Left(throwable) =>
logger.debug(throwable)(s"Failed to fetch artifacts of $dependency").as(None)
logger.debug(throwable)(s"Failed to fetch artifacts of $dependency").as(acc)
case Right(result) =>
val maybeProject = result.resolution.projectCache
.get(dependency.moduleVersion)
.map { case (_, project) => project }
maybeProject.traverseFilter { project =>
getScmUrlOrHomePage(project.info) match {
case Some(url) => F.pure(Some(url))
case None =>
getParentDependency(project).traverseFilter(getArtifactUrlImpl(_, repositories))

maybeProject.fold(F.pure(acc)) { project =>
val metadata = acc.enrichWith(metadataFrom(project))
val recurse = Option.when(metadata.repoUrl.isEmpty)(())
(recurse >> parentOf(project)).fold(F.pure(metadata)) { parent =>
getMetadataImpl(parent, repositories, metadata)
}
}
}
}

override def getVersions(dependency: Dependency, resolver: Resolver): F[List[Version]] =
toCoursierRepository(resolver) match {
case Left(message) =>
logger.error(message) >> F.raiseError(new Throwable(message))
case Right(repository) =>
val module = toCoursierModule(dependency)
repository.versions(module, cacheNoTtl.fetch).run.flatMap {
case Left(message) =>
logger.debug(message) >> F.raiseError(new Throwable(message))
case Right((versions, _)) => F.pure(versions.available.map(Version.apply).sorted)
}
convertResolver(resolver).flatMap { repository =>
val module = toCoursierModule(dependency)
repository.versions(module, cacheNoTtl.fetch).run.flatMap {
case Left(message) =>
logger.debug(message) >> F.raiseError[List[Version]](new Throwable(message))
case Right((versions, _)) =>
F.pure(versions.available.map(Version.apply).sorted)
}
}

private def convertToCoursierTypes(
dependency: Scope.Dependency
): F[(coursier.Dependency, List[coursier.Repository])] =
dependency.resolvers.traverseFilter(convertResolver).map { repositories =>
(toCoursierDependency(dependency.value), repositories)
}

private def convertResolver(resolver: Resolver): F[Option[coursier.Repository]] =
private def convertResolver(resolver: Resolver): F[coursier.Repository] =
toCoursierRepository(resolver) match {
case Right(repository) => F.pure(Some(repository))
case Left(message) => logger.error(s"Failed to convert $resolver: $message").as(None)
case Right(repository) => F.pure(repository)
case Left(message) =>
logger.error(s"Failed to convert $resolver: $message") >>
F.raiseError[coursier.Repository](new Throwable(message))
}
}
}
Expand All @@ -127,40 +123,40 @@ object CoursierAlg {
private def toCoursierRepository(resolver: Resolver): Either[String, coursier.Repository] =
resolver match {
case Resolver.MavenRepository(_, location, creds, headers) =>
Right(
coursier.maven.MavenRepository
.apply(location, toCoursierAuthentication(creds, headers))
)
val authentication = toCoursierAuthentication(creds, headers)
Right(coursier.maven.MavenRepository.apply(location, authentication))
case Resolver.IvyRepository(_, pattern, creds, headers) =>
coursier.ivy.IvyRepository
.parse(pattern, authentication = toCoursierAuthentication(creds, headers))
val authentication = toCoursierAuthentication(creds, headers)
coursier.ivy.IvyRepository.parse(pattern, authentication = authentication)
}

private def toCoursierAuthentication(
credentials: Option[Credentials],
headers: List[Resolver.Header]
): Option[Authentication] =
if (credentials.isEmpty && headers.isEmpty) {
None
} else {
Some(
new Authentication(
credentials.fold("")(_.user),
credentials.map(_.pass),
headers.map(h => (h.key, h.value)),
optional = false,
None,
httpsOnly = true,
passOnRedirect = false
)
Option.when(credentials.nonEmpty || headers.nonEmpty) {
new Authentication(
credentials.fold("")(_.user),
credentials.map(_.pass),
headers.map(h => (h.key, h.value)),
optional = false,
realmOpt = None,
httpsOnly = true,
passOnRedirect = false
)
}

private def getParentDependency(project: Project): Option[coursier.Dependency] =
private def metadataFrom(project: Project): DependencyMetadata =
DependencyMetadata(
homePage = uri.fromStringWithScheme(project.info.homePage),
scmUrl = project.info.scm.flatMap(_.url).flatMap(uri.fromStringWithScheme),
releaseNotesUrl = project.properties
.collectFirst { case (key, value) if key.equalsIgnoreCase("info.releaseNotesUrl") => value }
.flatMap(uri.fromStringWithScheme)
)

private def parentOf(project: Project): Option[coursier.Dependency] =
project.parent.map { case (module, version) =>
coursier.Dependency(module, version).withTransitive(false)
}

private def getScmUrlOrHomePage(info: Info): Option[Uri] =
uri.findBrowsableUrl(info.scm.flatMap(_.url).toList :+ info.homePage)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2018-2022 Scala Steward contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.scalasteward.core.coursier

import cats.Monad
import cats.syntax.all._
import org.http4s.Uri
import org.scalasteward.core.util.uri

final case class DependencyMetadata(
homePage: Option[Uri],
scmUrl: Option[Uri],
releaseNotesUrl: Option[Uri]
) {
def enrichWith(other: DependencyMetadata): DependencyMetadata =
DependencyMetadata(
homePage = homePage.orElse(other.homePage),
scmUrl = scmUrl.orElse(other.scmUrl),
releaseNotesUrl = releaseNotesUrl.orElse(other.releaseNotesUrl)
)

def filterUrls[F[_]](f: Uri => F[Boolean])(implicit F: Monad[F]): F[DependencyMetadata] =
for {
homePage <- homePage.filterA(f)
scmUrl <- scmUrl.filterA(f)
releaseNotesUrl <- releaseNotesUrl.filterA(f)
} yield DependencyMetadata(homePage, scmUrl, releaseNotesUrl)

def repoUrl: Option[Uri] = {
val urls = scmUrl.toList ++ homePage.toList
urls.find(_.scheme.exists(uri.httpSchemes)).orElse(urls.headOption)
}
}

object DependencyMetadata {
val empty: DependencyMetadata =
DependencyMetadata(None, None, None)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package org.scalasteward.core.nurture

import cats.Id
import cats.{Applicative, Id}
import cats.effect.Concurrent
import cats.implicits._
import org.scalasteward.core.application.Config.VCSCfg
Expand All @@ -26,13 +26,12 @@ import org.scalasteward.core.data._
import org.scalasteward.core.edit.{EditAlg, EditAttempt}
import org.scalasteward.core.git.{Branch, Commit, GitAlg}
import org.scalasteward.core.repoconfig.PullRequestUpdateStrategy
import org.scalasteward.core.util.{Nel, UrlChecker}
import org.scalasteward.core.util.logger.LoggerOps
import org.scalasteward.core.util.{Nel, UrlChecker}
import org.scalasteward.core.vcs.data._
import org.scalasteward.core.vcs.{VCSApiAlg, VCSExtraAlg, VCSRepoAlg}
import org.scalasteward.core.{git, util, vcs}
import org.typelevel.log4cats.Logger
import cats.Applicative

final class NurtureAlg[F[_]](config: VCSCfg)(implicit
coursierAlg: CoursierAlg[F],
Expand Down Expand Up @@ -194,30 +193,39 @@ final class NurtureAlg[F[_]](config: VCSCfg)(implicit
_.updates.flatMap(dependenciesUpdatedWithNextAndCurrentVersion(_))
)

private def createPullRequest(data: UpdateData, edits: List[EditAttempt]): F[ProcessResult] =
private[nurture] def preparePullRequest(
data: UpdateData,
edits: List[EditAttempt]
): F[NewPullRequestData] =
for {
_ <- logger.info(s"Create PR ${data.updateBranch.name}")
_ <- F.unit
dependenciesWithNextVersion = dependenciesUpdatedWithNextAndCurrentVersion(data.update)
resolvers = data.repoData.cache.dependencyInfos.flatMap(_.resolvers)
dependencyScope = Scope(
value = dependenciesWithNextVersion.map { case (_, dependency) => dependency },
resolvers = resolvers
)
artifactIdToUrl <- coursierAlg.getArtifactIdUrlMapping(dependencyScope)
existingArtifactUrlsList <- artifactIdToUrl.toList.filterA { case (_, uri) =>
urlChecker.exists(uri)
}
existingArtifactUrlsMap = existingArtifactUrlsList.toMap
dependencyToMetadata <- dependenciesWithNextVersion
.traverse { case (_, dependency) =>
coursierAlg
.getMetadata(dependency, resolvers)
.flatMap(_.filterUrls(urlChecker.exists))
.tupleLeft(dependency)
}
.map(_.toMap)
artifactIdToUrl = dependencyToMetadata.toList.mapFilter { case (dependency, metadata) =>
metadata.repoUrl.tupleLeft(dependency.artifactId.name)
}.toMap
releaseRelatedUrls <- dependenciesWithNextVersion.flatTraverse {
case (currentVersion, dependency) =>
existingArtifactUrlsMap
.get(dependency.artifactId.name)
.toList
.traverse(uri =>
dependencyToMetadata.get(dependency).toList.flatTraverse { metadata =>
metadata.repoUrl.toList.traverse { uri =>
vcsExtraAlg
.getReleaseRelatedUrls(uri, currentVersion, dependency.version)
.getReleaseRelatedUrls(
uri,
metadata.releaseNotesUrl,
currentVersion,
dependency.version
)
.tupleLeft(dependency.artifactId.name)
)
}
}
}
filesWithOldVersion <-
data.update
Expand All @@ -229,11 +237,17 @@ final class NurtureAlg[F[_]](config: VCSCfg)(implicit
data,
branchName,
edits,
existingArtifactUrlsMap,
artifactIdToUrl,
releaseRelatedUrls.toMap,
filesWithOldVersion,
data.repoData.config.pullRequests.includeMatchedLabels
)
} yield requestData

private def createPullRequest(data: UpdateData, edits: List[EditAttempt]): F[ProcessResult] =
for {
_ <- logger.info(s"Create PR ${data.updateBranch.name}")
requestData <- preparePullRequest(data, edits)
pr <- vcsApiAlg.createPullRequest(data.repo, requestData)
_ <- vcsApiAlg
.labelPullRequest(data.repo, pr.number, requestData.labels)
Expand Down
10 changes: 4 additions & 6 deletions modules/core/src/main/scala/org/scalasteward/core/util/uri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,9 @@ object uri {
val withUserInfo: Optional[Uri, UserInfo] =
authorityWithUserInfo.compose(withAuthority)

private val httpSchemes: Set[Scheme] =
Set(Scheme.https, Scheme.http)
def fromStringWithScheme(s: String): Option[Uri] =
Uri.fromString(s).toOption.filter(_.scheme.isDefined)

def findBrowsableUrl(xs: List[String]): Option[Uri] = {
val urls = xs.flatMap(Uri.fromString(_).toList).filter(_.scheme.isDefined)
urls.find(_.scheme.exists(httpSchemes)).orElse(urls.headOption)
}
val httpSchemes: Set[Scheme] =
Set(Scheme.https, Scheme.http)
}
Loading