Skip to content

Commit

Permalink
Add github-action PR open/closer
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark-Simulacrum committed Dec 8, 2024
1 parent 1300896 commit 3558ef1
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 9 deletions.
17 changes: 9 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ postgres-types = { version = "0.2.4", features = ["derive"] }
cron = { version = "0.12.0" }
bytes = "1.1.0"
structopt = "0.3.26"
indexmap = "2.7.0"

[dependencies.serde]
version = "1"
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub(crate) struct Config {
pub(crate) pr_tracking: Option<ReviewPrefsConfig>,
pub(crate) transfer: Option<TransferConfig>,
pub(crate) merge_conflicts: Option<MergeConflictConfig>,
pub(crate) bot_pull_requests: Option<BotPullRequests>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
Expand Down Expand Up @@ -363,6 +364,11 @@ pub(crate) struct MergeConflictConfig {
pub unless: HashSet<String>,
}

#[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<Result<Arc<Config>, ConfigurationError>> {
let cache = CONFIG_CACHE.read().unwrap();
cache.get(repo).and_then(|(config, fetch_time)| {
Expand Down Expand Up @@ -541,6 +547,7 @@ mod tests {
pr_tracking: None,
transfer: None,
merge_conflicts: None,
bot_pull_requests: None,
}
);
}
Expand Down
27 changes: 26 additions & 1 deletion src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ impl GithubClient {
.await
.context("failed to create issue")
}

pub(crate) async fn set_pr_status(
&self,
repo: &IssueRepository,
number: u64,
status: PrStatus,
) -> anyhow::Result<()> {
#[derive(serde::Serialize)]
struct Update {
status: PrStatus,
}
let url = format!("{}/pulls/{number}", repo.url(&self));
self.send_req(self.post(&url).json(&Update { status }))
.await
.context("failed to update pr state")?;
Ok(())
}
}

#[derive(Debug, serde::Serialize)]
pub(crate) enum PrStatus {
#[serde(rename = "open")]
Open,
#[serde(rename = "closed")]
Closed,
}

#[derive(Debug, serde::Deserialize)]
Expand Down Expand Up @@ -463,7 +488,7 @@ impl fmt::Display for AssignmentError {

impl std::error::Error for AssignmentError {}

#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Hash, Debug, Clone, PartialEq, Eq)]
pub struct IssueRepository {
pub organization: String,
pub repository: String,
Expand Down
11 changes: 11 additions & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -117,6 +118,16 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
);
}

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()
Expand Down
66 changes: 66 additions & 0 deletions src/handlers/bot_pull_requests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use indexmap::IndexSet;
use std::sync::atomic::AtomicBool;
use std::sync::{LazyLock, Mutex};

use crate::github::{IssueRepository, IssuesAction, PrStatus};
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(());
};
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 != "github-actions" {
return Ok(());
}

if DISABLE.load(std::sync::atomic::Ordering::Relaxed) {
tracing::warn!("skipping bot_pull_requests handler due to previous disable",);
return Ok(());
}

// Sanity check that our logic above doesn't cause us to act on PRs in a loop, by
// tracking a window of PRs we've acted on. We can probably drop this if we don't see problems
// in the first few days/weeks of deployment.
{
let mut touched = TOUCHED_PRS.lock().unwrap();
if !touched.insert((event.issue.repository().clone(), event.issue.number)) {
tracing::warn!("touching same PR twice despite username check: {:?}", event);
DISABLE.store(true, std::sync::atomic::Ordering::Relaxed);
return Ok(());
}
if touched.len() > 300 {
touched.drain(..150);
}
}

ctx.github
.set_pr_status(
event.issue.repository(),
event.issue.number,
PrStatus::Closed,
)
.await?;
ctx.github
.set_pr_status(event.issue.repository(), event.issue.number, PrStatus::Open)
.await?;

Ok(())
}

static TOUCHED_PRS: LazyLock<Mutex<IndexSet<(IssueRepository, u64)>>> =
LazyLock::new(|| std::sync::Mutex::new(IndexSet::new()));
static DISABLE: AtomicBool = AtomicBool::new(false);

0 comments on commit 3558ef1

Please sign in to comment.