Skip to content

Commit 9295c9d

Browse files
author
Ender Tunc
committed
update configuration settings and integrate new API calls to gitlab flow
1 parent e8a1a8c commit 9295c9d

File tree

4 files changed

+153
-29
lines changed

4 files changed

+153
-29
lines changed

modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.monovore.decline.Opts.{flag, option, options}
2323
import com.monovore.decline._
2424
import org.http4s.Uri
2525
import org.http4s.syntax.literals._
26+
import org.scalasteward.core.application.Cli.gitlabMergeRequestApprovalsConfig
2627
import org.scalasteward.core.application.Config._
2728
import org.scalasteward.core.data.Resolver
2829
import org.scalasteward.core.forge.ForgeType
@@ -31,7 +32,9 @@ import org.scalasteward.core.forge.github.GitHubApp
3132
import org.scalasteward.core.git.Author
3233
import org.scalasteward.core.util.Nel
3334
import org.scalasteward.core.util.dateTime.renderFiniteDuration
35+
3436
import scala.concurrent.duration._
37+
import scala.util.{Failure, Success, Try}
3538

3639
object Cli {
3740
final case class EnvVar(name: String, value: String)
@@ -44,6 +47,21 @@ object Cli {
4447
val processTimeout = "process-timeout"
4548
}
4649

50+
implicit val mergeRequestApprovalsConfigArgument: Argument[MergeRequestApprovalsConfig] =
51+
Argument.from("approvals_rule_name=required_approvals") { s =>
52+
s.split(":").toList match {
53+
case approvalRuleName :: requiredApprovalsAsString :: Nil =>
54+
Try(requiredApprovalsAsString.trim.toInt) match {
55+
case Failure(_) =>
56+
s"[$requiredApprovalsAsString] is not a valid Integer".invalidNel
57+
case Success(requiredApprovals) =>
58+
new MergeRequestApprovalsConfig(approvalRuleName.trim, requiredApprovals).validNel
59+
}
60+
case _ =>
61+
s"The value is expected in the following format: APPROVALS_RULE_NAME:REQUIRED_APPROVALS.".invalidNel
62+
}
63+
}
64+
4765
implicit val envVarArgument: Argument[EnvVar] =
4866
Argument.from("name=value") { s =>
4967
s.trim.split('=').toList match {
@@ -276,7 +294,7 @@ object Cli {
276294

277295
private val gitlabRequiredReviewers: Opts[Option[Int]] =
278296
option[Int](
279-
"gitlab-required-reviewers",
297+
"gitlabRequiredReviewers",
280298
"When set, the number of required reviewers for a merge request will be set to this number (non-negative integer). Is only used in the context of gitlab-merge-when-pipeline-succeeds being enabled, and requires that the configured access token have the appropriate privileges. Also requires a Gitlab Premium subscription."
281299
).validate("Required reviewers must be non-negative")(_ >= 0).orNone
282300

@@ -286,10 +304,31 @@ object Cli {
286304
"Flag indicating if a merge request should remove the source branch when merging."
287305
).orFalse
288306

289-
private val gitLabCfg: Opts[GitLabCfg] =
290-
(gitlabMergeWhenPipelineSucceeds, gitlabRequiredReviewers, gitlabRemoveSourceBranch).mapN(
291-
GitLabCfg.apply
307+
private val gitlabMergeRequestApprovalsConfig: Opts[Option[Nel[MergeRequestApprovalsConfig]]] =
308+
options[MergeRequestApprovalsConfig](
309+
"merge-request-level-approval-rule",
310+
s"Additional repo config file $multiple"
292311
)
312+
// ToDo better message
313+
.validate("")(_.forall(_.requiredApproves >= 0) == true)
314+
.orNone
315+
316+
private val gitlabReviewersAndApprovalsConfig
317+
: Opts[Option[Either[Int, Nel[MergeRequestApprovalsConfig]]]] =
318+
((gitlabRequiredReviewers, gitlabMergeRequestApprovalsConfig).tupled.mapValidated {
319+
case (None, None) => None.validNel
320+
case (None, Some(gitlabMergeRequestApprovalsConfig)) =>
321+
Some(gitlabMergeRequestApprovalsConfig.asRight[Int]).validNel
322+
case (Some(requiredReviewers), None) => Some(Left(requiredReviewers)).validNel
323+
case (Some(_), Some(_)) =>
324+
s"You can't use both --gitlabRequiredReviewers and --merge-request-level-approval-rule at the same time".invalidNel
325+
})
326+
327+
private val gitLabCfg: Opts[GitLabCfg] =
328+
(gitlabMergeWhenPipelineSucceeds, gitlabReviewersAndApprovalsConfig, gitlabRemoveSourceBranch)
329+
.mapN(
330+
GitLabCfg.apply
331+
)
293332

294333
private val githubAppId: Opts[Long] =
295334
option[Long](

modules/core/src/main/scala/org/scalasteward/core/application/Config.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,11 @@ object Config {
156156
final case class GitHubCfg(
157157
) extends ForgeSpecificCfg
158158

159+
final case class MergeRequestApprovalsConfig(approvalRuleName: String, requiredApproves: Int)
160+
159161
final case class GitLabCfg(
160162
mergeWhenPipelineSucceeds: Boolean,
161-
requiredReviewers: Option[Int],
163+
requiredReviewers: Option[Either[Int, Nel[MergeRequestApprovalsConfig]]],
162164
removeSourceBranch: Boolean
163165
) extends ForgeSpecificCfg
164166

modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@ import io.circe._
2323
import io.circe.generic.semiauto._
2424
import io.circe.syntax._
2525
import org.http4s.{Request, Status, Uri}
26-
import org.scalasteward.core.application.Config.{ForgeCfg, GitLabCfg}
26+
import org.scalasteward.core.application.Config.{ForgeCfg, GitLabCfg, MergeRequestApprovalsConfig}
2727
import org.scalasteward.core.data.Repo
2828
import org.scalasteward.core.forge.ForgeApiAlg
2929
import org.scalasteward.core.forge.data._
3030
import org.scalasteward.core.git.{Branch, Sha1}
3131
import org.scalasteward.core.util.uri.uriDecoder
32-
import org.scalasteward.core.util.{intellijThisImportIsUsed, HttpJsonClient, UnexpectedResponse}
32+
import org.scalasteward.core.util.{
33+
intellijThisImportIsUsed,
34+
HttpJsonClient,
35+
Nel,
36+
UnexpectedResponse
37+
}
3338
import org.typelevel.log4cats.Logger
3439

3540
import scala.concurrent.duration.{Duration, DurationInt}
@@ -48,6 +53,8 @@ final private[gitlab] case class MergeRequestPayload(
4853
target_branch: Branch
4954
)
5055

56+
final private[gitlab] case class UpdateMergeRequestLevelApprovalRulePayload(approvals_required: Int)
57+
5158
private[gitlab] object MergeRequestPayload {
5259
def apply(
5360
id: String,
@@ -87,6 +94,11 @@ final private[gitlab] case class MergeRequestApprovalsOut(
8794
approvalsRequired: Int
8895
)
8996

97+
final private[gitlab] case class MergeRequestLevelApprovalRuleOut(
98+
id: Int,
99+
name: String
100+
)
101+
90102
final private[gitlab] case class CommitId(id: Sha1) {
91103
val commitOut: CommitOut = CommitOut(id)
92104
}
@@ -102,6 +114,8 @@ private[gitlab] object GitLabJsonCodec {
102114
intellijThisImportIsUsed(uriDecoder)
103115

104116
implicit val forkPayloadEncoder: Encoder[ForkPayload] = deriveEncoder
117+
implicit val updateMergeRequestLevelApprovalRulePayloadEncoder
118+
: Encoder[UpdateMergeRequestLevelApprovalRulePayload] = deriveEncoder
105119
implicit val userOutDecoder: Decoder[UserOut] = Decoder.instance {
106120
_.downField("username").as[String].map(UserOut(_))
107121
}
@@ -140,6 +154,14 @@ private[gitlab] object GitLabJsonCodec {
140154
} yield MergeRequestApprovalsOut(requiredReviewers)
141155
}
142156

157+
implicit val mergeRequestLevelApprovalRuleOutDecoder: Decoder[MergeRequestLevelApprovalRuleOut] =
158+
Decoder.instance { c =>
159+
for {
160+
id <- c.downField("id").as[Int]
161+
name <- c.downField("string").as[String]
162+
} yield MergeRequestLevelApprovalRuleOut(id, name)
163+
}
164+
143165
implicit val projectIdDecoder: Decoder[ProjectId] = deriveDecoder
144166
implicit val mergeRequestPayloadEncoder: Encoder[MergeRequestPayload] =
145167
deriveEncoder[MergeRequestPayload].mapJson(_.dropNullValues)
@@ -240,7 +262,13 @@ final class GitLabApiAlg[F[_]: Parallel](
240262
for {
241263
mr <- mergeRequest
242264
mrWithStatus <- waitForMergeRequestStatus(mr.iid)
243-
_ <- maybeSetReviewers(repo, mrWithStatus)
265+
_ <- gitLabCfg.requiredReviewers match {
266+
case Some(Right(approvalRules)) =>
267+
setApprovalRules(repo, mrWithStatus, approvalRules)
268+
case Some(Left(requiredReviewers)) =>
269+
setReviewers(repo, mrWithStatus, requiredReviewers)
270+
case None => F.unit
271+
}
244272
mergedUponSuccess <- mergePipelineUponSuccess(repo, mrWithStatus)
245273
} yield mergedUponSuccess
246274
}
@@ -270,29 +298,74 @@ final class GitLabApiAlg[F[_]: Parallel](
270298
case mr =>
271299
logger.info(s"Unable to automatically merge ${mr.webUrl}").map(_ => mr)
272300
}
301+
import cats.implicits._
273302

274-
private def maybeSetReviewers(repo: Repo, mrOut: MergeRequestOut): F[MergeRequestOut] =
275-
gitLabCfg.requiredReviewers match {
276-
case Some(requiredReviewers) =>
277-
for {
278-
_ <- logger.info(
279-
s"Setting number of required reviewers on ${mrOut.webUrl} to $requiredReviewers"
303+
private def setReviewers(
304+
repo: Repo,
305+
mrOut: MergeRequestOut,
306+
requiredReviewers: Int
307+
): F[MergeRequestOut] =
308+
for {
309+
_ <- logger.info(
310+
s"Setting number of required reviewers on ${mrOut.webUrl} to $requiredReviewers"
311+
)
312+
_ <-
313+
client
314+
.put[MergeRequestApprovalsOut](
315+
url.requiredApprovals(repo, mrOut.iid, requiredReviewers),
316+
modify(repo)
280317
)
281-
_ <-
282-
client
283-
.put[MergeRequestApprovalsOut](
284-
url.requiredApprovals(repo, mrOut.iid, requiredReviewers),
285-
modify(repo)
286-
)
287-
.map(_ => ())
288-
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
289-
logger
290-
.warn(s"Unexpected response setting required reviewers: $status: $body")
291-
.as(())
292-
}
293-
} yield mrOut
294-
case None => F.pure(mrOut)
295-
}
318+
.map(_ => ())
319+
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
320+
logger
321+
.warn(s"Unexpected response setting required reviewers: $status: $body")
322+
.as(())
323+
}
324+
} yield mrOut
325+
326+
private def setApprovalRules(
327+
repo: Repo,
328+
mrOut: MergeRequestOut,
329+
approvalsConfig: Nel[MergeRequestApprovalsConfig]
330+
): F[MergeRequestOut] =
331+
for {
332+
_ <- logger.info(
333+
s"Adjusting merge request approvals rules on ${mrOut.webUrl} with following config: $approvalsConfig"
334+
)
335+
activeApprovalRules <-
336+
client
337+
.get[List[MergeRequestLevelApprovalRuleOut]](
338+
url.listMergeRequestLevelApprovalRules(repo, mrOut.iid),
339+
modify(repo)
340+
)
341+
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
342+
// ToDo better log
343+
logger
344+
.warn(s"Unexpected response setting required reviewers: $status: $body")
345+
.as(List.empty)
346+
}
347+
approvalRuleNamesFromConfig = approvalsConfig.map(_.approvalRuleName)
348+
approvalRulesToUpdate = activeApprovalRules.intersect(approvalRuleNamesFromConfig.toList)
349+
_ <-
350+
approvalRulesToUpdate.map { mergeRequestApprovalConfig =>
351+
client
352+
.putWithBody[Unit, UpdateMergeRequestLevelApprovalRulePayload](
353+
url.updateMergeRequestLevelApprovalRule(
354+
repo,
355+
mrOut.iid,
356+
mergeRequestApprovalConfig.id
357+
),
358+
UpdateMergeRequestLevelApprovalRulePayload(mergeRequestApprovalConfig.id),
359+
modify(repo)
360+
)
361+
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
362+
// ToDo better log
363+
logger
364+
.warn(s"Unexpected response setting required reviewers: $status: $body")
365+
.as(List.empty)
366+
}
367+
}.sequence
368+
} yield mrOut
296369

297370
private def getUsernameToUserIdsMapping(repo: Repo, usernames: Set[String]): F[Map[String, Int]] =
298371
usernames.toList

modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/Url.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.scalasteward.core.forge.gitlab
1818

1919
import org.http4s.Uri
20+
import org.scalasteward.core.application.Config.MergeRequestApprovalsConfig
2021
import org.scalasteward.core.data.Repo
2122
import org.scalasteward.core.forge.data.PullRequestNumber
2223
import org.scalasteward.core.git.Branch
@@ -47,6 +48,15 @@ class Url(apiHost: Uri) {
4748
(existingMergeRequest(repo, number) / "approvals")
4849
.withQueryParam("approvals_required", approvalsRequired)
4950

51+
def listMergeRequestLevelApprovalRules(repo: Repo, number: PullRequestNumber): Uri =
52+
existingMergeRequest(repo, number) / "approval_rules"
53+
54+
def updateMergeRequestLevelApprovalRule(
55+
repo: Repo,
56+
number: PullRequestNumber,
57+
approvalRuleId: Int
58+
): Uri = existingMergeRequest(repo, number) / "approval_rules" / approvalRuleId
59+
5060
def listMergeRequests(repo: Repo, source: String, target: String): Uri =
5161
mergeRequest(repo)
5262
.withQueryParam("source_branch", source)

0 commit comments

Comments
 (0)