Skip to content

Commit

Permalink
Edit PR title if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
elegaanz committed Sep 25, 2024
1 parent 29e7d4d commit 1c7dc5e
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 14 deletions.
111 changes: 97 additions & 14 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use codespan_reporting::{
use eyre::Context;
use hook::CheckRunPayload;
use jwt_simple::prelude::*;
use tracing::{debug, info, warn};
use pr::{AnyPullRequest, MinimalPullRequest, PullRequest, PullRequestUpdate};
use tracing::{debug, error, info, warn};
use typst::syntax::{package::PackageSpec, FileId};

use crate::{check, world::SystemWorld};
Expand Down Expand Up @@ -86,9 +87,24 @@ async fn index() -> &'static str {
async fn force(
state: State<AppState>,
api_client: GitHub,
axum::extract::Path((install, sha)): axum::extract::Path<(String, String)>,
axum::extract::Path((install, pr)): axum::extract::Path<(String, usize)>,
) -> Result<&'static str, &'static str> {
debug!("Force review for {sha}");
debug!("Force review for #{pr}");
let repository = Repository::new("typst/packages").map_err(|e| {
error!("{}", e);
"Invalid repository path"
})?;

let pr = MinimalPullRequest { number: pr };
let full_pr = pr
.get_full(&api_client, repository.owner(), repository.name())
.await
.map_err(|e| {
error!("{}", e);
"Failed to fetch PR context"
})?;
let sha = full_pr.head.sha.clone();

github_hook(
state,
api_client,
Expand All @@ -97,11 +113,11 @@ async fn force(
installation: Installation {
id: str::parse(&install).map_err(|_| "Invalid installation ID")?,
},
repository: Repository::new("typst/packages").map_err(|e| {
debug!("{}", e);
"Invalid repository path"
})?,
check_suite: CheckSuite { head_sha: sha },
repository,
check_suite: CheckSuite {
head_sha: sha,
pull_requests: vec![AnyPullRequest::Full(full_pr)],
},
}),
)
.await
Expand All @@ -123,35 +139,58 @@ async fn github_hook(
api_client.auth_installation(&payload).await?;
debug!("Successfully authenticated application");

let (head_sha, repository, previous_check_run) = match payload {
let (head_sha, repository, pr, previous_check_run) = match payload {
HookPayload::CheckSuite(CheckSuitePayload {
action: CheckSuiteAction::Requested | CheckSuiteAction::Rerequested,
repository,
check_suite,
mut check_suite,
..
}) => (check_suite.head_sha, repository, None),
}) => (
check_suite.head_sha,
repository,
check_suite.pull_requests.pop(),
None,
),
HookPayload::CheckRun(CheckRunPayload {
action: CheckRunAction::Rerequested,
repository,
check_run,
mut check_run,
..
}) => (
check_run.check_suite.head_sha.clone(),
repository,
check_run.check_suite.pull_requests.pop(),
Some(check_run),
),
HookPayload::CheckRun(_) => return Ok(()),
_ => return Err(WebError::UnexpectedEvent),
};

debug!("Starting checks for {}", head_sha);
let pr = if let Some(pr) = pr {
pr.get_full(&api_client, repository.owner(), repository.name())
.await
.ok()
} else {
None
};

debug!(
"Starting checks for {}{}",
head_sha,
if let Some(ref pr) = pr {
format!(" (#{})", pr.number)
} else {
String::new()
}
);
tokio::spawn(async move {
async fn inner(
state: AppState,
head_sha: String,
api_client: GitHub,
repository: Repository,
previous_check_run: Option<CheckRun>,
pr: Option<PullRequest>,
) -> eyre::Result<()> {
let git_repo = GitRepo::open(Path::new(&state.git_dir));
git_repo.pull_main().await?;
Expand Down Expand Up @@ -180,6 +219,39 @@ async fn github_hook(
})
.collect::<HashSet<_>>();

if let Some(pr) = pr {
let mut package_names = touched_packages
.iter()
.map(|p| format!("{}:{}", p.name, p.version))
.collect::<Vec<_>>();
package_names.sort();
let last_package = package_names.pop();
let penultimate_package = package_names.pop();
let expected_pr_title = if let Some((penultimate_package, last_package)) =
penultimate_package.as_ref().zip(last_package.as_ref())
{
package_names.push(format!("{} and {}", penultimate_package, last_package));
Some(package_names.join(", "))
} else {
last_package
};
if let Some(expected_pr_title) = expected_pr_title {
if pr.title != expected_pr_title {
api_client
.update_pull_request(
repository.owner(),
repository.name(),
pr.number,
PullRequestUpdate {
title: expected_pr_title,
},
)
.await
.context("Failed to update pull request")?;
}
}
}

for ref package in touched_packages {
let check_run_name = format!(
"@{}/{}:{}",
Expand Down Expand Up @@ -322,7 +394,16 @@ async fn github_hook(
Ok(())
}

if let Err(e) = inner(state, head_sha, api_client, repository, previous_check_run).await {
if let Err(e) = inner(
state,
head_sha,
api_client,
repository,
previous_check_run,
pr,
)
.await
{
warn!("Error in hook handler: {:#}", e)
}
});
Expand Down Expand Up @@ -380,6 +461,8 @@ enum WebError {

impl IntoResponse for WebError {
fn into_response(self) -> axum::response::Response {
debug!("Web error: {:?}", &self);

Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("{:?}", self)))
Expand Down
5 changes: 5 additions & 0 deletions src/github/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use super::AppState;

pub mod check;
pub mod hook;
pub mod pr;

#[derive(Debug)]
pub enum ApiError {
Expand Down Expand Up @@ -137,6 +138,10 @@ impl GitHub {
Ok(())
}

fn get(&self, url: impl AsRef<str>) -> RequestBuilder {
self.with_headers(self.req.get(Self::url(url)))
}

fn patch(&self, url: impl AsRef<str>) -> RequestBuilder {
self.with_headers(self.req.patch(Self::url(url)))
}
Expand Down
3 changes: 3 additions & 0 deletions src/github/api/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ use std::fmt::Display;

use serde::{Deserialize, Serialize};

use super::pr::AnyPullRequest;

#[derive(Debug, Deserialize, Clone, Copy)]
#[serde(transparent)]
pub struct CheckSuiteId(#[allow(dead_code)] u64);

#[derive(Clone, Deserialize)]
pub struct CheckSuite {
pub head_sha: String,
pub pull_requests: Vec<AnyPullRequest>,
}

#[derive(Clone, Deserialize)]
Expand Down
85 changes: 85 additions & 0 deletions src/github/api/pr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};

use super::{ApiError, GitHub, OwnerId, RepoId};

#[derive(Clone, Deserialize)]
pub struct MinimalPullRequest {
pub number: usize,
}

impl MinimalPullRequest {
pub async fn get_full(
&self,
api: &GitHub,
owner: OwnerId,
repo: RepoId,
) -> Result<PullRequest, ApiError> {
Ok(api
.get(format!(
"repos/{owner}/{repo}/pulls/{pull_number}",
owner = owner,
repo = repo,
pull_number = self.number
))
.send()
.await?
.json()
.await?)
}
}

#[derive(Clone, Deserialize)]
pub struct PullRequest {
pub number: usize,
pub head: Commit,
pub title: String,
}

#[derive(Clone, Deserialize)]
#[serde(untagged)]
pub enum AnyPullRequest {
Full(PullRequest),
Minimal(MinimalPullRequest),
}

impl AnyPullRequest {
pub async fn get_full(
self,
api: &GitHub,
owner: OwnerId,
repo: RepoId,
) -> Result<PullRequest, ApiError> {
match self {
AnyPullRequest::Full(pr) => Ok(pr),
AnyPullRequest::Minimal(pr) => pr.get_full(api, owner, repo).await,
}
}
}

#[derive(Clone, Deserialize)]
pub struct Commit {
pub sha: String,
}

#[derive(Serialize)]
pub struct PullRequestUpdate {
pub title: String,
}

impl GitHub {
pub async fn update_pull_request(
&self,
owner: OwnerId,
repo: RepoId,
pr: usize,
update: PullRequestUpdate,
) -> Result<PullRequest, ApiError> {
Ok(self
.patch(format!("{}/{}/pulls/{}", owner, repo, pr))
.json(&update)
.send()
.await?
.json()
.await?)
}
}

0 comments on commit 1c7dc5e

Please sign in to comment.