From 2bc1b29ea46bc9bfb91089125ce82e4c124d8d4c Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Sun, 13 Jun 2021 18:26:45 +0200 Subject: [PATCH 1/7] only create webhook if it doesn't already exist, refactor main --- .../src/main/resources/reflect-config.json | 27 ++++++++---- .../main/scala/org/polyvariant/Gitlab.scala | 39 ++++++++++++++++- .../src/main/scala/org/polyvariant/Main.scala | 43 +++++++++++++------ build.sbt | 3 ++ 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/bootstrap/src/main/resources/reflect-config.json b/bootstrap/src/main/resources/reflect-config.json index 866c93ba..24a1407f 100644 --- a/bootstrap/src/main/resources/reflect-config.json +++ b/bootstrap/src/main/resources/reflect-config.json @@ -2,15 +2,6 @@ { "name": "byte[]" }, - { - "name": "cats.Later", - "fields": [ - { - "name": "0bitmap$1", - "allowUnsafeAccess": true - } - ] - }, { "name": "cats.effect.unsafe.IORuntimeCompanionPlatform", "fields": [ @@ -135,6 +126,24 @@ } ] }, + { + "name": "org.polyvariant.Gitlab$$anon$2", + "fields": [ + { + "name": "0bitmap$1", + "allowUnsafeAccess": true + } + ] + }, + { + "name": "org.polyvariant.Gitlab$Webhook$", + "fields": [ + { + "name": "0bitmap$2", + "allowUnsafeAccess": true + } + ] + }, { "name": "sttp.model.Header$", "fields": [ diff --git a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala index 1d89678b..53446cda 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala @@ -2,10 +2,11 @@ package org.polyvariant import cats.implicits.* -import scala.util.chaining._ +import scala.util.chaining.* import io.pg.gitlab.graphql.* import sttp.model.Uri import sttp.client3.* +import sttp.client3.circe.* import caliban.client.SelectionBuilder import caliban.client.CalibanClientError.DecodingError import io.pg.gitlab.graphql.MergeRequest @@ -20,15 +21,20 @@ import io.pg.gitlab.graphql.UserCore import caliban.client.Operations.IsOperation import sttp.model.Method import cats.MonadThrow +import io.circe.* +import io.circe.generic.semiauto.* trait Gitlab[F[_]] { def mergeRequests(projectId: Long): F[List[Gitlab.MergeRequestInfo]] def deleteMergeRequest(projectId: Long, mergeRequestId: Long): F[Unit] def createWebhook(projectId: Long, pitgullUrl: Uri): F[Unit] + def listWebhooks(projectId: Long): F[List[Gitlab.Webhook]] } object Gitlab { + def apply[F[_]](using ev: Gitlab[F]): Gitlab[F] = ev + def sttpInstance[F[_]: Logger: MonadThrow]( baseUri: Uri, accessToken: String @@ -89,10 +95,41 @@ object Gitlab { .contentType("application/json") ) } yield () + + def listWebhooks(projectId: Long): F[List[Webhook]] = for { + _ <- Logger[F].debug(s"Listing webhooks for $projectId") + response <- runRequest( + basicRequest.get( + baseUri + .addPath( + Seq( + "api", + "v4", + "projects", + projectId.toString, + "hooks" + ) + ) + ) + .response(asJson[List[Webhook]]) + .contentType("application/json") + ) + result <- MonadThrow[F].fromEither(response) + _ <- Logger[F].debug(result.toString) + } yield result } } + final case class Webhook( + id: Long, + url: String + ) + + object Webhook { + given Codec[Webhook] = deriveCodec + } + final case class MergeRequestInfo( projectId: Long, mergeRequestIid: Long, diff --git a/bootstrap/src/main/scala/org/polyvariant/Main.scala b/bootstrap/src/main/scala/org/polyvariant/Main.scala index 52623280..fef62c19 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Main.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Main.scala @@ -12,6 +12,7 @@ import sttp.monad.MonadError import cats.MonadThrow import org.polyvariant.Config.ArgumentsParsingException import cats.effect.std.Console +import cats.Monad object Main extends IOApp { @@ -20,35 +21,51 @@ object Main extends IOApp { Logger[F].info(s"ID: ${mr.mergeRequestIid} by: ${mr.authorUsername}") }.void - private def readConsent[F[_]: Console: Applicative]: F[Boolean] = - Console[F].readLine.map(_.toLowerCase == "y") + private def readConsent[F[_]: Console: MonadThrow]: F[Unit] = + MonadThrow[F] + .ifM(Console[F].readLine.map(_.toLowerCase == "y"))( + ifTrue = MonadThrow[F].pure(()), + ifFalse = MonadThrow[F].raiseError(new Exception("User rejected deletion")) + ) private def qualifyMergeRequestsForDeletion(botUserName: String, mergeRequests: List[MergeRequestInfo]): List[MergeRequestInfo] = mergeRequests.filter(_.authorUsername == botUserName) + private def deleteMergeRequests[F[_]: Gitlab: Logger: Applicative](project: Long, mergeRequests: List[MergeRequestInfo]): F[Unit] = + mergeRequests.traverse(mr => Gitlab[F].deleteMergeRequest(project, mr.mergeRequestIid)).void + + private def createWebhook[F[_]: Gitlab: Logger: Applicative](project: Long, webhook: Uri): F[Unit] = + Logger[F].info("Creating webhook") *> + Gitlab[F].createWebhook(project, webhook) *> + Logger[F].info("Webhook created") + + private def configureWebhooks[F[_]: Gitlab: Logger: Monad](project: Long, webhook: Uri): F[Unit] = for { + hooks <- Gitlab[F].listWebhooks(project).map(_.filter(_.url == webhook.toString)) + _ <- Monad[F] + .ifM(hooks.nonEmpty.pure[F])( + ifTrue = Logger[F].success("Webhook already exists"), + ifFalse = createWebhook(project, webhook) + ) + } yield () + private def program[F[_]: Logger: Console: Async: MonadThrow](args: List[String]): F[Unit] = { given SttpBackend[Identity, Any] = HttpURLConnectionBackend() val parsedArgs = Args.parse(args) for { config <- Config.fromArgs(parsedArgs) _ <- Logger[F].info("Starting pitgull bootstrap!") - gitlab = Gitlab.sttpInstance[F](config.gitlabUri, config.token) - mrs <- gitlab.mergeRequests(config.project) + given Gitlab[F] = Gitlab.sttpInstance[F](config.gitlabUri, config.token) + mrs <- Gitlab[F].mergeRequests(config.project) _ <- Logger[F].info(s"Merge requests found: ${mrs.length}") _ <- printMergeRequests(mrs) botMrs = qualifyMergeRequestsForDeletion(config.botUser, mrs) _ <- Logger[F].info(s"Will delete merge requests: ${botMrs.map(_.mergeRequestIid).mkString(", ")}") _ <- Logger[F].info("Do you want to proceed? y/Y") - _ <- MonadThrow[F] - .ifM(readConsent)( - ifTrue = MonadThrow[F].pure(()), - ifFalse = MonadThrow[F].raiseError(new Exception("User rejected deletion")) - ) - _ <- botMrs.traverse(mr => gitlab.deleteMergeRequest(config.project, mr.mergeRequestIid)) + _ <- readConsent + _ <- deleteMergeRequests(config.project, botMrs) _ <- Logger[F].info("Done processing merge requests") - _ <- Logger[F].info("Creating webhook") - _ <- gitlab.createWebhook(config.project, config.pitgullWebhookUrl) - _ <- Logger[F].info("Webhook created") + _ <- Logger[F].info("Configuring webhook") + _ <- configureWebhooks(config.project, config.pitgullWebhookUrl) _ <- Logger[F].success("Bootstrap finished") } yield () } diff --git a/build.sbt b/build.sbt index d14e4dd6..0a68a5e6 100644 --- a/build.sbt +++ b/build.sbt @@ -115,6 +115,9 @@ lazy val bootstrap = project "org.typelevel" %% "cats-effect" % "3.1.1", "com.kubukoz" %% "caliban-gitlab" % "0.1.0", "com.softwaremill.sttp.client3" %% "core" % "3.3.6", + "com.softwaremill.sttp.client3" %% "circe" % "3.3.6", + "io.circe" %% "circe-core" % "0.14.1", + "io.circe" %% "circe-generic" % "0.14.1", crossPlugin("com.kubukoz" % "better-tostring" % "0.3.3") ), publish / skip := true, From 195ed04f6964e376dee438918b8614e772e176df Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Sun, 13 Jun 2021 18:39:10 +0200 Subject: [PATCH 2/7] scalafmt --- .../main/scala/org/polyvariant/Gitlab.scala | 108 +++++++++--------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala index 53446cda..183122dd 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala @@ -42,7 +42,11 @@ object Gitlab { using backend: SttpBackend[Identity, Any] // FIXME: https://github.com/polyvariant/pitgull/issues/265 ): Gitlab[F] = { def runRequest[O](request: Request[O, Any]): F[O] = - request.header("Private-Token", accessToken).send(backend).pure[F].map(_.body) // FIXME - change in https://github.com/polyvariant/pitgull/issues/265 + request + .header("Private-Token", accessToken) + .send(backend) + .pure[F] + .map(_.body) // FIXME - change in https://github.com/polyvariant/pitgull/issues/265 def runGraphQLQuery[A: IsOperation, B](a: SelectionBuilder[A, B]): F[B] = runRequest(a.toRequest(baseUri.addPath("api", "graphql"))).rethrow @@ -58,64 +62,66 @@ object Gitlab { } def deleteMergeRequest(projectId: Long, mergeRequestId: Long): F[Unit] = for { - _ <- Logger[F].debug(s"Request to remove $mergeRequestId") + _ <- Logger[F].debug(s"Request to remove $mergeRequestId") result <- runRequest( - basicRequest.delete( - baseUri - .addPath( - Seq( - "api", - "v4", - "projects", - projectId.toString, - "merge_requests", - mergeRequestId.toString - ) - ) - ) - ) + basicRequest.delete( + baseUri + .addPath( + Seq( + "api", + "v4", + "projects", + projectId.toString, + "merge_requests", + mergeRequestId.toString + ) + ) + ) + ) } yield () - + def createWebhook(projectId: Long, pitgullUrl: Uri): F[Unit] = for { - _ <- Logger[F].debug(s"Creating webhook to $pitgullUrl") + _ <- Logger[F].debug(s"Creating webhook to $pitgullUrl") result <- runRequest( - basicRequest.post( - baseUri - .addPath( - Seq( - "api", - "v4", - "projects", - projectId.toString, - "hooks" - ) - ) - ) - .body(s"""{"merge_requests_events": true, "pipeline_events": true, "note_events": true, "url": "$pitgullUrl"}""") - .contentType("application/json") - ) + basicRequest + .post( + baseUri + .addPath( + Seq( + "api", + "v4", + "projects", + projectId.toString, + "hooks" + ) + ) + ) + .body(s"""{"merge_requests_events": true, "pipeline_events": true, "note_events": true, "url": "$pitgullUrl"}""") + .contentType("application/json") + ) } yield () def listWebhooks(projectId: Long): F[List[Webhook]] = for { - _ <- Logger[F].debug(s"Listing webhooks for $projectId") + _ <- Logger[F].debug(s"Listing webhooks for $projectId") response <- runRequest( - basicRequest.get( - baseUri - .addPath( - Seq( - "api", - "v4", - "projects", - projectId.toString, - "hooks" - ) - ) - ) - .response(asJson[List[Webhook]]) - .contentType("application/json") - ) - result <- MonadThrow[F].fromEither(response) - _ <- Logger[F].debug(result.toString) + basicRequest + .get( + baseUri + .addPath( + Seq( + "api", + "v4", + "projects", + projectId.toString, + "hooks" + ) + ) + ) + .response(asJson[List[Webhook]]) + .contentType("application/json") + ) + result <- MonadThrow[F].fromEither(response) + _ <- Logger[F].debug(result.toString) } yield result } From 0f4686091f57fc2d94044ab5800c82959f6fe212 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Sun, 13 Jun 2021 21:55:09 +0200 Subject: [PATCH 3/7] use derives for codecs --- .../src/main/scala/org/polyvariant/Gitlab.scala | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala index 183122dd..20f15de1 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala @@ -22,7 +22,6 @@ import caliban.client.Operations.IsOperation import sttp.model.Method import cats.MonadThrow import io.circe.* -import io.circe.generic.semiauto.* trait Gitlab[F[_]] { def mergeRequests(projectId: Long): F[List[Gitlab.MergeRequestInfo]] @@ -118,11 +117,9 @@ object Gitlab { ) ) .response(asJson[List[Webhook]]) - .contentType("application/json") - ) - result <- MonadThrow[F].fromEither(response) - _ <- Logger[F].debug(result.toString) - } yield result + ).flatMap(_.liftTo[F]) + _ <- Logger[F].debug(response.toString) + } yield response } } @@ -130,11 +127,7 @@ object Gitlab { final case class Webhook( id: Long, url: String - ) - - object Webhook { - given Codec[Webhook] = deriveCodec - } + ) derives Codec.AsObject final case class MergeRequestInfo( projectId: Long, From 4e87359e395ad49639e0fb8a6506fefca5073421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pawlik?= Date: Sun, 13 Jun 2021 21:55:46 +0200 Subject: [PATCH 4/7] Update bootstrap/src/main/scala/org/polyvariant/Main.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Kozłowski --- bootstrap/src/main/scala/org/polyvariant/Main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/src/main/scala/org/polyvariant/Main.scala b/bootstrap/src/main/scala/org/polyvariant/Main.scala index fef62c19..c4b864e3 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Main.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Main.scala @@ -23,7 +23,7 @@ object Main extends IOApp { private def readConsent[F[_]: Console: MonadThrow]: F[Unit] = MonadThrow[F] - .ifM(Console[F].readLine.map(_.toLowerCase == "y"))( + .ifM(Console[F].readLine.map(_.trim.toLowerCase == "y"))( ifTrue = MonadThrow[F].pure(()), ifFalse = MonadThrow[F].raiseError(new Exception("User rejected deletion")) ) From 9edc7c291557a83c0c07609d8dacc614e014ee81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pawlik?= Date: Sun, 13 Jun 2021 21:56:03 +0200 Subject: [PATCH 5/7] Update bootstrap/src/main/scala/org/polyvariant/Main.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Kozłowski --- bootstrap/src/main/scala/org/polyvariant/Main.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/src/main/scala/org/polyvariant/Main.scala b/bootstrap/src/main/scala/org/polyvariant/Main.scala index c4b864e3..8afbbab5 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Main.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Main.scala @@ -48,7 +48,7 @@ object Main extends IOApp { ) } yield () - private def program[F[_]: Logger: Console: Async: MonadThrow](args: List[String]): F[Unit] = { + private def program[F[_]: Logger: Console: Async](args: List[String]): F[Unit] = { given SttpBackend[Identity, Any] = HttpURLConnectionBackend() val parsedArgs = Args.parse(args) for { From 7b4cd9878ae6ea88b67191f3ebe84bc8ff187191 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Sun, 13 Jun 2021 21:57:36 +0200 Subject: [PATCH 6/7] drop circe generic --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index 0a68a5e6..b9274883 100644 --- a/build.sbt +++ b/build.sbt @@ -117,7 +117,6 @@ lazy val bootstrap = project "com.softwaremill.sttp.client3" %% "core" % "3.3.6", "com.softwaremill.sttp.client3" %% "circe" % "3.3.6", "io.circe" %% "circe-core" % "0.14.1", - "io.circe" %% "circe-generic" % "0.14.1", crossPlugin("com.kubukoz" % "better-tostring" % "0.3.3") ), publish / skip := true, From 4c542c6ac7c42d19c86ce24243a86b65650200c0 Mon Sep 17 00:00:00 2001 From: Michal Pawlik Date: Sun, 13 Jun 2021 22:04:41 +0200 Subject: [PATCH 7/7] fix reflect-config.json --- bootstrap/src/main/resources/reflect-config.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bootstrap/src/main/resources/reflect-config.json b/bootstrap/src/main/resources/reflect-config.json index 24a1407f..8a2ce523 100644 --- a/bootstrap/src/main/resources/reflect-config.json +++ b/bootstrap/src/main/resources/reflect-config.json @@ -2,6 +2,15 @@ { "name": "byte[]" }, + { + "name": "cats.Later", + "fields": [ + { + "name": "0bitmap$1", + "allowUnsafeAccess": true + } + ] + }, { "name": "cats.effect.unsafe.IORuntimeCompanionPlatform", "fields": [