Skip to content

Commit ba61a1d

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

File tree

4 files changed

+154
-30
lines changed

4 files changed

+154
-30
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: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@
1616

1717
package org.scalasteward.core.forge.gitlab
1818

19-
import cats.{MonadThrow, Parallel}
2019
import cats.syntax.all._
20+
import cats.{MonadThrow, Parallel}
2121
import io.circe._
2222
import io.circe.generic.semiauto._
2323
import io.circe.syntax._
2424
import org.http4s.{Request, Status, Uri}
25-
import org.scalasteward.core.application.Config.{ForgeCfg, GitLabCfg}
25+
import org.scalasteward.core.application.Config.{ForgeCfg, GitLabCfg, MergeRequestApprovalsConfig}
2626
import org.scalasteward.core.data.Repo
2727
import org.scalasteward.core.forge.ForgeApiAlg
2828
import org.scalasteward.core.forge.data._
2929
import org.scalasteward.core.git.{Branch, Sha1}
3030
import org.scalasteward.core.util.uri.uriDecoder
31-
import org.scalasteward.core.util.{intellijThisImportIsUsed, HttpJsonClient, UnexpectedResponse}
31+
import org.scalasteward.core.util.{
32+
intellijThisImportIsUsed,
33+
HttpJsonClient,
34+
Nel,
35+
UnexpectedResponse
36+
}
3237
import org.typelevel.log4cats.Logger
3338

3439
final private[gitlab] case class ForkPayload(id: String, namespace: String)
@@ -45,6 +50,8 @@ final private[gitlab] case class MergeRequestPayload(
4550
target_branch: Branch
4651
)
4752

53+
final private[gitlab] case class UpdateMergeRequestLevelApprovalRulePayload(approvals_required: Int)
54+
4855
private[gitlab] object MergeRequestPayload {
4956
def apply(
5057
id: String,
@@ -84,6 +91,11 @@ final private[gitlab] case class MergeRequestApprovalsOut(
8491
approvalsRequired: Int
8592
)
8693

94+
final private[gitlab] case class MergeRequestLevelApprovalRuleOut(
95+
id: Int,
96+
name: String
97+
)
98+
8799
final private[gitlab] case class CommitId(id: Sha1) {
88100
val commitOut: CommitOut = CommitOut(id)
89101
}
@@ -99,6 +111,8 @@ private[gitlab] object GitLabJsonCodec {
99111
intellijThisImportIsUsed(uriDecoder)
100112

101113
implicit val forkPayloadEncoder: Encoder[ForkPayload] = deriveEncoder
114+
implicit val updateMergeRequestLevelApprovalRulePayloadEncoder
115+
: Encoder[UpdateMergeRequestLevelApprovalRulePayload] = deriveEncoder
102116
implicit val userOutDecoder: Decoder[UserOut] = Decoder.instance {
103117
_.downField("username").as[String].map(UserOut(_))
104118
}
@@ -137,6 +151,14 @@ private[gitlab] object GitLabJsonCodec {
137151
} yield MergeRequestApprovalsOut(requiredReviewers)
138152
}
139153

154+
implicit val mergeRequestLevelApprovalRuleOutDecoder: Decoder[MergeRequestLevelApprovalRuleOut] =
155+
Decoder.instance { c =>
156+
for {
157+
id <- c.downField("id").as[Int]
158+
name <- c.downField("string").as[String]
159+
} yield MergeRequestLevelApprovalRuleOut(id, name)
160+
}
161+
140162
implicit val projectIdDecoder: Decoder[ProjectId] = deriveDecoder
141163
implicit val mergeRequestPayloadEncoder: Encoder[MergeRequestPayload] =
142164
deriveEncoder[MergeRequestPayload].mapJson(_.dropNullValues)
@@ -222,7 +244,13 @@ final class GitLabApiAlg[F[_]: Parallel](
222244
for {
223245
mr <- mergeRequest
224246
mrWithStatus <- waitForMergeRequestStatus(mr.iid)
225-
_ <- maybeSetReviewers(repo, mrWithStatus)
247+
_ <- gitLabCfg.requiredReviewers match {
248+
case Some(Right(approvalRules)) =>
249+
setApprovalRules(repo, mrWithStatus, approvalRules)
250+
case Some(Left(requiredReviewers)) =>
251+
setReviewers(repo, mrWithStatus, requiredReviewers)
252+
case None => F.unit
253+
}
226254
mergedUponSuccess <- mergePipelineUponSuccess(repo, mrWithStatus)
227255
} yield mergedUponSuccess
228256
}
@@ -252,29 +280,74 @@ final class GitLabApiAlg[F[_]: Parallel](
252280
case mr =>
253281
logger.info(s"Unable to automatically merge ${mr.webUrl}").map(_ => mr)
254282
}
283+
import cats.implicits._
255284

256-
private def maybeSetReviewers(repo: Repo, mrOut: MergeRequestOut): F[MergeRequestOut] =
257-
gitLabCfg.requiredReviewers match {
258-
case Some(requiredReviewers) =>
259-
for {
260-
_ <- logger.info(
261-
s"Setting number of required reviewers on ${mrOut.webUrl} to $requiredReviewers"
285+
private def setReviewers(
286+
repo: Repo,
287+
mrOut: MergeRequestOut,
288+
requiredReviewers: Int
289+
): F[MergeRequestOut] =
290+
for {
291+
_ <- logger.info(
292+
s"Setting number of required reviewers on ${mrOut.webUrl} to $requiredReviewers"
293+
)
294+
_ <-
295+
client
296+
.put[MergeRequestApprovalsOut](
297+
url.requiredApprovals(repo, mrOut.iid, requiredReviewers),
298+
modify(repo)
262299
)
263-
_ <-
264-
client
265-
.put[MergeRequestApprovalsOut](
266-
url.requiredApprovals(repo, mrOut.iid, requiredReviewers),
267-
modify(repo)
268-
)
269-
.map(_ => ())
270-
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
271-
logger
272-
.warn(s"Unexpected response setting required reviewers: $status: $body")
273-
.as(())
274-
}
275-
} yield mrOut
276-
case None => F.pure(mrOut)
277-
}
300+
.map(_ => ())
301+
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
302+
logger
303+
.warn(s"Unexpected response setting required reviewers: $status: $body")
304+
.as(())
305+
}
306+
} yield mrOut
307+
308+
private def setApprovalRules(
309+
repo: Repo,
310+
mrOut: MergeRequestOut,
311+
approvalsConfig: Nel[MergeRequestApprovalsConfig]
312+
): F[MergeRequestOut] =
313+
for {
314+
_ <- logger.info(
315+
s"Adjusting merge request approvals rules on ${mrOut.webUrl} with following config: $approvalsConfig"
316+
)
317+
activeApprovalRules <-
318+
client
319+
.get[List[MergeRequestLevelApprovalRuleOut]](
320+
url.listMergeRequestLevelApprovalRules(repo, mrOut.iid),
321+
modify(repo)
322+
)
323+
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
324+
// ToDo better log
325+
logger
326+
.warn(s"Unexpected response setting required reviewers: $status: $body")
327+
.as(List.empty)
328+
}
329+
approvalRuleNamesFromConfig = approvalsConfig.map(_.approvalRuleName)
330+
approvalRulesToUpdate = activeApprovalRules.intersect(approvalRuleNamesFromConfig.toList)
331+
_ <-
332+
approvalRulesToUpdate.map { mergeRequestApprovalConfig =>
333+
client
334+
.putWithBody[Unit, UpdateMergeRequestLevelApprovalRulePayload](
335+
url.updateMergeRequestLevelApprovalRule(
336+
repo,
337+
mrOut.iid,
338+
mergeRequestApprovalConfig.id
339+
),
340+
UpdateMergeRequestLevelApprovalRulePayload(mergeRequestApprovalConfig.id),
341+
modify(repo)
342+
)
343+
.recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
344+
// ToDo better log
345+
logger
346+
.warn(s"Unexpected response setting required reviewers: $status: $body")
347+
.as(List.empty)
348+
}
349+
}.sequence
350+
} yield mrOut
278351

279352
private def getUsernameToUserIdsMapping(repo: Repo, usernames: Set[String]): F[Map[String, Int]] =
280353
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)