diff --git a/src/config.rs b/src/config.rs index 8b6fa5f6..6a7480ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -46,6 +46,7 @@ pub(crate) struct Config { pub(crate) pr_tracking: Option, pub(crate) transfer: Option, pub(crate) merge_conflicts: Option, + pub(crate) bot_pull_requests: Option, } #[derive(PartialEq, Eq, Debug, serde::Deserialize)] @@ -402,6 +403,11 @@ pub(crate) struct MergeConflictConfig { pub unless: HashSet, } +#[derive(PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +#[serde(deny_unknown_fields)] +pub(crate) struct BotPullRequests {} + fn get_cached_config(repo: &str) -> Option, ConfigurationError>> { let cache = CONFIG_CACHE.read().unwrap(); cache.get(repo).and_then(|(config, fetch_time)| { @@ -580,6 +586,7 @@ mod tests { pr_tracking: None, transfer: None, merge_conflicts: None, + bot_pull_requests: None, } ); } @@ -641,6 +648,7 @@ mod tests { pr_tracking: None, transfer: None, merge_conflicts: None, + bot_pull_requests: None, } ); } diff --git a/src/github.rs b/src/github.rs index e05e0f31..cbf7ed78 100644 --- a/src/github.rs +++ b/src/github.rs @@ -189,6 +189,31 @@ impl GithubClient { .await .context("failed to create issue") } + + pub(crate) async fn set_pr_state( + &self, + repo: &IssueRepository, + number: u64, + state: PrState, + ) -> anyhow::Result<()> { + #[derive(serde::Serialize)] + struct Update { + state: PrState, + } + let url = format!("{}/pulls/{number}", repo.url(&self)); + self.send_req(self.patch(&url).json(&Update { state })) + .await + .context("failed to update pr state")?; + Ok(()) + } +} + +#[derive(Debug, serde::Serialize)] +pub(crate) enum PrState { + #[serde(rename = "open")] + Open, + #[serde(rename = "closed")] + Closed, } #[derive(Debug, serde::Deserialize)] diff --git a/src/handlers.rs b/src/handlers.rs index 759c5441..9218a210 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -25,6 +25,7 @@ impl fmt::Display for HandlerError { mod assign; mod autolabel; +mod bot_pull_requests; mod close; pub mod docs_update; mod github_releases; @@ -117,6 +118,16 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec { ); } + if config.as_ref().is_ok_and(|c| c.bot_pull_requests.is_some()) { + if let Err(e) = bot_pull_requests::handle(ctx, event).await { + log::error!( + "failed to process event {:?} with bot_pull_requests handler: {:?}", + event, + e + ) + } + } + if let Some(config) = config .as_ref() .ok() diff --git a/src/handlers/bot_pull_requests.rs b/src/handlers/bot_pull_requests.rs new file mode 100644 index 00000000..02e4ec2e --- /dev/null +++ b/src/handlers/bot_pull_requests.rs @@ -0,0 +1,40 @@ +use crate::github::{IssuesAction, PrState}; +use crate::{github::Event, handlers::Context}; + +pub(crate) async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> { + let Event::Issue(event) = event else { + return Ok(()); + }; + // Note that this filters out reopened too, which is what we'd expect when we set the state + // back to opened after closing. + if event.action != IssuesAction::Opened { + return Ok(()); + } + if !event.issue.is_pr() { + return Ok(()); + } + + // avoid acting on our own open events, otherwise we'll infinitely loop + if event.sender.login == ctx.username { + return Ok(()); + } + + // If it's not the github-actions bot, we don't expect this handler to be needed. Skip the + // event. + if event.sender.login != "app/github-actions" { + return Ok(()); + } + + ctx.github + .set_pr_state( + event.issue.repository(), + event.issue.number, + PrState::Closed, + ) + .await?; + ctx.github + .set_pr_state(event.issue.repository(), event.issue.number, PrState::Open) + .await?; + + Ok(()) +}