Skip to content

Commit c3a21a0

Browse files
authored
github: add GitHub API function to submit pull request reviews (#343)
* feat: add GitHub API function to approve pull requests * nit: update type to be consistent with sibling functions * chore: simplify approve error handling/body parsing * feat: expand functionality to submit review
1 parent 593065b commit c3a21a0

4 files changed

Lines changed: 187 additions & 1 deletion

File tree

runtime/plaid-stl/src/github/pull_request.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,122 @@ pub fn pull_request_request_reviewers(
161161
Ok(())
162162
}
163163

164+
/// The review action to submit as part of a pull request review.
165+
///
166+
/// GitHub requires a non-empty `body` alongside `RequestChanges` and `Comment`;
167+
/// it is optional for `Approve`.
168+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
169+
pub enum PullRequestReviewEvent {
170+
/// Approve the pull request.
171+
#[serde(rename = "APPROVE")]
172+
Approve,
173+
/// Request changes on the pull request. Requires a `body`.
174+
#[serde(rename = "REQUEST_CHANGES")]
175+
RequestChanges,
176+
/// Leave a review comment without approving or requesting changes. Requires a `body`.
177+
#[serde(rename = "COMMENT")]
178+
Comment,
179+
}
180+
181+
/// Request to submit a review on a pull request.
182+
#[derive(Serialize, Deserialize)]
183+
pub struct SubmitPullRequestReviewRequest {
184+
/// The account owner of the repository. The name is not case sensitive.
185+
pub owner: String,
186+
/// The name of the repository without the `.git` extension. The name is not case sensitive.
187+
pub repo: String,
188+
/// The number of the pull request to review.
189+
pub number: u32,
190+
/// The review action to submit.
191+
pub event: PullRequestReviewEvent,
192+
/// A comment to leave alongside the review. Required unless `event` is `Approve`.
193+
pub body: Option<String>,
194+
}
195+
196+
/// Submits a review on a pull request.
197+
///
198+
/// See the [GitHub API docs](https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#create-a-review-for-a-pull-request)
199+
/// for more details.
200+
///
201+
/// ## Arguments
202+
/// * `client_id` - Selects which configured GitHub client to use (supports multiple clients).
203+
/// * `owner` - The account or organization that owns the repository.
204+
/// * `repo` - The name of the repository.
205+
/// * `number` - The number of the pull request to review.
206+
/// * `event` - The review action to submit.
207+
/// * `body` - A comment to leave alongside the review. Required unless `event` is `Approve`.
208+
///
209+
/// ## Returns
210+
/// * `Ok(())` if the review was successfully submitted, or
211+
/// * `Err(PlaidFunctionError)` if the request fails.
212+
pub fn submit_pull_request_review(
213+
client_id: impl Display,
214+
owner: impl Display,
215+
repo: impl Display,
216+
number: u32,
217+
event: PullRequestReviewEvent,
218+
body: Option<impl Display>,
219+
) -> Result<(), PlaidFunctionError> {
220+
extern "C" {
221+
new_host_function!(github, submit_pull_request_review);
222+
}
223+
224+
let request = SubmitPullRequestReviewRequest {
225+
owner: owner.to_string(),
226+
repo: repo.to_string(),
227+
number,
228+
event,
229+
body: body.map(|b| b.to_string()),
230+
};
231+
232+
let wrapped = GithubApiWrapper {
233+
client_id: client_id.to_string(),
234+
params: request,
235+
};
236+
let request = serde_json::to_string(&wrapped).unwrap();
237+
238+
let res = unsafe {
239+
github_submit_pull_request_review(request.as_bytes().as_ptr(), request.as_bytes().len())
240+
};
241+
242+
// There was an error with the Plaid system. Maybe the API is not
243+
// configured.
244+
if res < 0 {
245+
return Err(res.into());
246+
}
247+
248+
Ok(())
249+
}
250+
251+
/// Approves a pull request by submitting an `APPROVE` review on it.
252+
///
253+
/// ## Arguments
254+
/// * `client_id` - Selects which configured GitHub client to use (supports multiple clients).
255+
/// * `owner` - The account or organization that owns the repository.
256+
/// * `repo` - The name of the repository.
257+
/// * `number` - The number of the pull request to approve.
258+
/// * `body` - An optional comment to leave alongside the approval.
259+
///
260+
/// ## Returns
261+
/// * `Ok(())` if the review was successfully submitted, or
262+
/// * `Err(PlaidFunctionError)` if the request fails.
263+
pub fn approve_pull_request(
264+
client_id: impl Display,
265+
owner: impl Display,
266+
repo: impl Display,
267+
number: u32,
268+
body: Option<impl Display>,
269+
) -> Result<(), PlaidFunctionError> {
270+
submit_pull_request_review(
271+
client_id,
272+
owner,
273+
repo,
274+
number,
275+
PullRequestReviewEvent::Approve,
276+
body,
277+
)
278+
}
279+
164280
/// Request to fetch pull requests from a repository.
165281
///
166282
/// Represents the top-level parameters needed to query pull requests

runtime/plaid/src/apis/github/pull_requests.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use plaid_stl::github::{
22
AddLabelsRequest, CreatePullRequestRequest, GetPullRequestOptions, GetPullRequestRequest,
3-
GithubApiWrapper, PullRequestRequestReviewers,
3+
GithubApiWrapper, PullRequestRequestReviewers, PullRequestReviewEvent,
4+
SubmitPullRequestReviewRequest,
45
};
56
use serde::Serialize;
67
use serde_json::json;
@@ -73,6 +74,54 @@ impl Github {
7374
}
7475
}
7576

77+
/// Submits a review on a pull request.
78+
pub async fn submit_pull_request_review(
79+
&self,
80+
params: &str,
81+
module: Arc<PlaidModule>,
82+
) -> Result<u32, ApiError> {
83+
#[derive(Serialize)]
84+
struct SubmitReview {
85+
event: PullRequestReviewEvent,
86+
#[serde(skip_serializing_if = "Option::is_none")]
87+
body: Option<String>,
88+
}
89+
90+
let request: GithubApiWrapper<SubmitPullRequestReviewRequest> =
91+
serde_json::from_str(params).map_err(|_| ApiError::BadRequest)?;
92+
93+
let owner = self.validate_org(&request.params.owner)?;
94+
let repo = self.validate_repository_name(&request.params.repo)?;
95+
let number = request.params.number;
96+
let event = request.params.event;
97+
self.validate_review_body(event, request.params.body.as_deref())?;
98+
99+
info!("Submitting {event:?} review on pull request [{owner}/{repo}/{number}] on behalf of {module}");
100+
101+
let address = format!("/repos/{owner}/{repo}/pulls/{number}/reviews");
102+
let request_body = SubmitReview {
103+
event,
104+
body: request.params.body,
105+
};
106+
107+
match self
108+
.make_generic_post_request(&request.client_id, address, request_body, module)
109+
.await
110+
{
111+
Ok((status, Ok(_))) => {
112+
if status == 200 {
113+
Ok(0)
114+
} else {
115+
Err(ApiError::GitHubError(GitHubError::UnexpectedStatusCode(
116+
status,
117+
)))
118+
}
119+
}
120+
Ok((_, Err(e))) => Err(e),
121+
Err(e) => Err(e),
122+
}
123+
}
124+
76125
/// Creates a pull request in a specified repository.
77126
pub async fn create_pull_request(
78127
&self,

runtime/plaid/src/apis/github/validators.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::collections::HashMap;
22

3+
use plaid_stl::github::PullRequestReviewEvent;
4+
35
use crate::apis::ApiError;
46

57
use super::{GitHubError, Github};
@@ -108,4 +110,21 @@ impl Github {
108110
// Repo IDs are positive integers
109111
self.validate_pint(repo_id)
110112
}
113+
114+
/// Ensure a pull request review has a body when GitHub requires one.
115+
/// A non-empty `body` is mandatory for `RequestChanges` and `Comment`;
116+
/// it is optional for `Approve`.
117+
pub fn validate_review_body(
118+
&self,
119+
event: PullRequestReviewEvent,
120+
body: Option<&str>,
121+
) -> Result<(), ApiError> {
122+
match (event, body) {
123+
(PullRequestReviewEvent::Approve, _) => Ok(()),
124+
(_, Some(body)) if !body.is_empty() => Ok(()),
125+
(_, _) => Err(ApiError::GitHubError(GitHubError::InvalidInput(format!(
126+
"a non-empty body is required for review event {event:?}"
127+
)))),
128+
}
129+
}
111130
}

runtime/plaid/src/functions/api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ impl_new_function!(
436436
pull_request_request_reviewers,
437437
DISALLOW_IN_TEST_MODE
438438
);
439+
impl_new_function!(github, submit_pull_request_review, DISALLOW_IN_TEST_MODE);
439440
impl_new_function_with_error_buffer!(github, get_weekly_commit_count, ALLOW_IN_TEST_MODE);
440441
impl_new_function_with_error_buffer!(github, get_reference, ALLOW_IN_TEST_MODE);
441442
impl_new_function!(github, create_reference, DISALLOW_IN_TEST_MODE);
@@ -891,6 +892,7 @@ define_api_functions! {
891892
"github_delete_deploy_key" => github_delete_deploy_key,
892893
"github_create_deploy_key" => github_create_deploy_key,
893894
"github_pull_request_request_reviewers" => github_pull_request_request_reviewers,
895+
"github_submit_pull_request_review" => github_submit_pull_request_review,
894896
"github_require_signed_commits" => github_require_signed_commits,
895897
"github_get_weekly_commit_count" => github_get_weekly_commit_count,
896898
"github_add_repo_to_team" => github_add_repo_to_team,

0 commit comments

Comments
 (0)