Skip to content

Commit 9ca2935

Browse files
authored
Support creation of pull request reviews (#564)
1 parent 2876ea7 commit 9ca2935

8 files changed

Lines changed: 162 additions & 8 deletions

File tree

github4s/src/main/scala/github4s/Encoders.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ object Encoders {
3737
implicit val encodePrrStatus: Encoder[PullRequestReviewState] =
3838
Encoder.encodeString.contramap(_.value)
3939

40+
implicit val encodePrrEvent: Encoder[PullRequestReviewEvent] =
41+
Encoder.encodeString.contramap(_.value)
42+
4043
implicit val encodeEditGistFile: Encoder[EditGistFile] = {
4144
deriveEncoder[EditGistFile].mapJsonObject(
4245
_.filter(e => !(e._1.equals("filename") && e._2.isNull))
@@ -69,4 +72,6 @@ object Encoders {
6972
deriveEncoder[NewReleaseRequest]
7073
implicit val encoderNewStatusRequest: Encoder[NewStatusRequest] = deriveEncoder[NewStatusRequest]
7174
implicit val encoderMilestoneData: Encoder[MilestoneData] = deriveEncoder[MilestoneData]
75+
implicit val encodeNewPullRequestReview: Encoder[CreatePRReviewRequest] =
76+
deriveEncoder[CreatePRReviewRequest]
7277
}

github4s/src/main/scala/github4s/algebras/PullRequests.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,22 @@ trait PullRequests[F[_]] {
138138
headers: Map[String, String] = Map()
139139
): F[GHResponse[PullRequestReview]]
140140

141+
/**
142+
* Create a review for a pull request
143+
*
144+
* @param owner Owner of the repo
145+
* @param repo Name of the repo
146+
* @param pullRequest ID number of the PR to get reviews for
147+
* @param createPRReviewRequest Data to create a review
148+
* @param headers Optional user header to include in the request
149+
* @return a GHResponse with the created review
150+
*/
151+
def createReview(
152+
owner: String,
153+
repo: String,
154+
pullRequest: Int,
155+
createPRReviewRequest: CreatePRReviewRequest,
156+
headers: Map[String, String] = Map()
157+
): F[GHResponse[PullRequestReview]]
158+
141159
}

github4s/src/main/scala/github4s/domain/PullRequest.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,19 @@ final case object PRRStateChangesRequested extends PullRequestReviewState("CHANG
122122
final case object PRRStateCommented extends PullRequestReviewState("COMMENTED")
123123
final case object PRRStatePending extends PullRequestReviewState("PENDING")
124124
final case object PRRStateDismissed extends PullRequestReviewState("DISMISSED")
125+
126+
final case class CreatePRReviewRequest(
127+
commit_id: Option[String] = None,
128+
body: String,
129+
event: PullRequestReviewEvent = PRREventPending,
130+
comments: List[CreateReviewComment] = Nil
131+
)
132+
133+
sealed abstract class PullRequestReviewEvent(val value: String)
134+
135+
final case object PRREventApprove extends PullRequestReviewEvent("APPROVE")
136+
final case object PRREventRequestChanges extends PullRequestReviewEvent("REQUEST_CHANGES")
137+
final case object PRREventComment extends PullRequestReviewEvent("COMMENT")
138+
final case object PRREventPending extends PullRequestReviewEvent("PENDING")
139+
140+
case class CreateReviewComment(path: String, position: Int, body: String)

github4s/src/main/scala/github4s/interpreters/PullRequestsInterpreter.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,19 @@ class PullRequestsInterpreter[F[_]](implicit client: HttpClient[F], accessToken:
111111
s"repos/$owner/$repo/pulls/$pullRequest/reviews/$review",
112112
headers
113113
)
114+
115+
override def createReview(
116+
owner: String,
117+
repo: String,
118+
pullRequest: Int,
119+
createPRReviewRequest: CreatePRReviewRequest,
120+
headers: Map[String, String]
121+
): F[GHResponse[PullRequestReview]] =
122+
client
123+
.post[CreatePRReviewRequest, PullRequestReview](
124+
accessToken,
125+
s"repos/$owner/$repo/pulls/$pullRequest/reviews",
126+
headers,
127+
createPRReviewRequest
128+
)
114129
}

github4s/src/test/scala/github4s/integration/PullRequestsSpec.scala

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package github4s.integration
1818

1919
import cats.effect.IO
20-
import github4s.GHError.NotFoundError
20+
import github4s.GHError.{JsonParsingError, NotFoundError}
2121
import github4s.Github
2222
import github4s.domain._
2323
import github4s.utils.{BaseIntegrationSpec, Integration}
@@ -254,4 +254,45 @@ trait PullRequestsSpec extends BaseIntegrationSpec {
254254
response.statusCode shouldBe notFoundStatusCode
255255
}
256256

257+
"PullRequests >> CreateReview" should "return a created review" taggedAs Integration in {
258+
val response = clientResource
259+
.use { client =>
260+
Github[IO](client, accessToken).pullRequests
261+
.createReview(
262+
validRepoOwner,
263+
validRepoName,
264+
validPullRequestNumber,
265+
validCreatePRReviewRequest,
266+
headers = headerUserAgent
267+
)
268+
}
269+
.unsafeRunSync()
270+
271+
testIsRight[PullRequestReview](
272+
response,
273+
r => {
274+
r.body shouldBe validCreatePRReviewRequest.body
275+
r.state shouldBe PRRStateApproved
276+
}
277+
)
278+
response.statusCode shouldBe okStatusCode
279+
}
280+
281+
it should "return an error when invalid review data was passed" taggedAs Integration in {
282+
val response = clientResource
283+
.use { client =>
284+
Github[IO](client, accessToken).pullRequests
285+
.createReview(
286+
validRepoOwner,
287+
validRepoName,
288+
validPullRequestNumber,
289+
invalidCreatePRReviewRequest,
290+
headers = headerUserAgent
291+
)
292+
}
293+
.unsafeRunSync()
294+
295+
testIsLeft[JsonParsingError, PullRequestReview](response)
296+
response.statusCode shouldBe unprocessableEntityStatusCode
297+
}
257298
}

github4s/src/test/scala/github4s/unit/PullRequestsSpec.scala

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class PullRequestsSpec extends BaseSpec {
133133

134134
}
135135

136-
"GHPullRequests.listReviews" should "call to httpClient.post with the right parameters" in {
136+
"PullRequests.listReviews" should "call to httpClient.get with the right parameters" in {
137137
val response: IO[GHResponse[List[PullRequestReview]]] =
138138
IO(GHResponse(List(pullRequestReview).asRight, okStatusCode, Map.empty))
139139

@@ -154,7 +154,7 @@ class PullRequestsSpec extends BaseSpec {
154154

155155
}
156156

157-
"GHPullRequests.getReview" should "call to httpClient.post with the right parameters" in {
157+
"PullRequests.getReview" should "call to httpClient.get with the right parameters" in {
158158
val response: IO[GHResponse[PullRequestReview]] =
159159
IO(GHResponse(pullRequestReview.asRight, okStatusCode, Map.empty))
160160

@@ -176,4 +176,25 @@ class PullRequestsSpec extends BaseSpec {
176176

177177
}
178178

179+
"PullRequests.createReview" should "call to httpClient.post with the right parameters" in {
180+
val response: IO[GHResponse[PullRequestReview]] =
181+
IO(GHResponse(pullRequestReview.asRight, okStatusCode, Map.empty))
182+
183+
implicit val httpClientMock = httpClientMockPost[CreatePRReviewRequest, PullRequestReview](
184+
url = s"repos/$validRepoOwner/$validRepoName/pulls/$validPullRequestNumber/reviews",
185+
req = validCreatePRReviewRequest,
186+
response = response
187+
)
188+
189+
val pullRequests = new PullRequestsInterpreter[IO]
190+
191+
pullRequests.createReview(
192+
validRepoOwner,
193+
validRepoName,
194+
validPullRequestNumber,
195+
validCreatePRReviewRequest,
196+
headerUserAgent
197+
)
198+
}
199+
179200
}

github4s/src/test/scala/github4s/utils/TestData.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ trait TestData {
6969
val validSinceInt = 100
7070
val invalidSinceInt = 999999999
7171

72-
val okStatusCode = 200
73-
val createdStatusCode = 201
74-
val noContentStatusCode = 204
75-
val unauthorizedStatusCode = 401
76-
val notFoundStatusCode = 404
72+
val okStatusCode = 200
73+
val createdStatusCode = 201
74+
val noContentStatusCode = 204
75+
val unauthorizedStatusCode = 401
76+
val notFoundStatusCode = 404
77+
val unprocessableEntityStatusCode = 422
7778

7879
val validAnonParameter = "true"
7980
val invalidAnonParameter = "X"
@@ -146,6 +147,11 @@ trait TestData {
146147
val validNewPullRequestIssue = NewPullRequestIssue(31)
147148
val invalidNewPullRequestIssue = NewPullRequestIssue(5)
148149

150+
val validCreatePRReviewRequest =
151+
CreatePRReviewRequest(None, "LGFM", PRREventApprove, Nil)
152+
val invalidCreatePRReviewRequest =
153+
CreatePRReviewRequest(None, "", PRREventRequestChanges, Nil)
154+
149155
val validPath = "project/plugins.sbt"
150156

151157
val validStatusState = "success"

microsite/docs/pull_request.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ with Github4s, you can interact with:
1717
- [Reviews](#reviews)
1818
- [List reviews](#list-pull-request-reviews)
1919
- [Get a review](#get-an-individual-review)
20+
- [Create a review](#create-a-review)
2021

2122
The following examples assume the following code:
2223

@@ -219,6 +220,37 @@ The `result` on the right is the matching [PullRequestReview][pr-scala].
219220

220221
See [the API doc](https://developer.github.com/v3/pulls/reviews/#get-a-single-review) for full reference.
221222

223+
### Create a review
224+
225+
You can create a review for a pull request using `createReview`; it takes as arguments:
226+
227+
- the repository coordinates (`owner` and `name` of the repository).
228+
- the pull request id.
229+
- `commit_id` (as part of the `CreatePRReviewRequest` object): The SHA of the commit that needs a review. Defaults to the most recent commit.
230+
- `body` (as part of the `CreatePRReviewRequest` object): Required when using REQUEST_CHANGES or COMMENT for the event parameter. The body text of the pull request review.
231+
- `event` (as part of the `CreatePRReviewRequest` object): The review action you want to perform. By leaving this blank, you set the review action state to PENDING.
232+
- `comments` (as part of the `CreatePRReviewRequest` object): An optional list of draft review comments.
233+
234+
```scala mdoc:compile-only
235+
import github4s.domain.{CreatePRReviewRequest, PRREventApprove}
236+
237+
val createReviewData = gh.pullRequests.createReview(
238+
"47deg",
239+
"github4s",
240+
139,
241+
CreatePRReviewRequest(Some("commit_id"), "body", PRREventApprove)
242+
)
243+
val response = createReviewData.unsafeRunSync()
244+
response.result match {
245+
case Left(e) => println(s"Something went wrong: ${e.getMessage}")
246+
case Right(r) => println(r)
247+
}
248+
```
249+
250+
The `result` on the right is the created [PullRequestReview][pr-scala].
251+
252+
See [the API doc](https://developer.github.com/v3/pulls/reviews/#create-a-review-for-a-pull-request) for full reference.
253+
222254
As you can see, a few features of the pull request endpoint are missing. As a result, if you'd like
223255
to see a feature supported, feel free to create an issue and/or a pull request!
224256

0 commit comments

Comments
 (0)