Skip to content

Commit 388cb24

Browse files
Add github-action PR open/closer
1 parent 1300896 commit 388cb24

File tree

6 files changed

+119
-9
lines changed

6 files changed

+119
-9
lines changed

Cargo.lock

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ postgres-types = { version = "0.2.4", features = ["derive"] }
4747
cron = { version = "0.12.0" }
4848
bytes = "1.1.0"
4949
structopt = "0.3.26"
50+
indexmap = "2.7.0"
5051

5152
[dependencies.serde]
5253
version = "1"

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub(crate) struct Config {
4646
pub(crate) pr_tracking: Option<ReviewPrefsConfig>,
4747
pub(crate) transfer: Option<TransferConfig>,
4848
pub(crate) merge_conflicts: Option<MergeConflictConfig>,
49+
pub(crate) bot_pull_requests: Option<BotPullRequests>,
4950
}
5051

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

367+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
368+
#[serde(rename_all = "kebab-case")]
369+
#[serde(deny_unknown_fields)]
370+
pub(crate) struct BotPullRequests {}
371+
366372
fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
367373
let cache = CONFIG_CACHE.read().unwrap();
368374
cache.get(repo).and_then(|(config, fetch_time)| {

src/github.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,31 @@ impl GithubClient {
189189
.await
190190
.context("failed to create issue")
191191
}
192+
193+
pub(crate) async fn set_pr_status(
194+
&self,
195+
repo: &IssueRepository,
196+
number: u64,
197+
status: PrStatus,
198+
) -> anyhow::Result<()> {
199+
#[derive(serde::Serialize)]
200+
struct Update {
201+
status: PrStatus,
202+
}
203+
let url = format!("{}/pulls/{number}", repo.url(&self));
204+
self.send_req(self.post(&url).json(&Update { status }))
205+
.await
206+
.context("failed to update pr state")?;
207+
Ok(())
208+
}
209+
}
210+
211+
#[derive(Debug, serde::Serialize)]
212+
pub(crate) enum PrStatus {
213+
#[serde(rename = "open")]
214+
Open,
215+
#[serde(rename = "closed")]
216+
Closed,
192217
}
193218

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

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

466-
#[derive(Debug, Clone, PartialEq, Eq)]
491+
#[derive(Hash, Debug, Clone, PartialEq, Eq)]
467492
pub struct IssueRepository {
468493
pub organization: String,
469494
pub repository: String,

src/handlers.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ impl fmt::Display for HandlerError {
2525

2626
mod assign;
2727
mod autolabel;
28+
mod bot_pull_requests;
2829
mod close;
2930
pub mod docs_update;
3031
mod github_releases;
@@ -117,6 +118,16 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
117118
);
118119
}
119120

121+
if config.as_ref().is_ok_and(|c| c.bot_pull_requests.is_some()) {
122+
if let Err(e) = bot_pull_requests::handle(ctx, event).await {
123+
log::error!(
124+
"failed to process event {:?} with bot_pull_requests handler: {:?}",
125+
event,
126+
e
127+
)
128+
}
129+
}
130+
120131
if let Some(config) = config
121132
.as_ref()
122133
.ok()

src/handlers/bot_pull_requests.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use indexmap::IndexSet;
2+
use std::sync::atomic::AtomicBool;
3+
use std::sync::{LazyLock, Mutex};
4+
5+
use crate::github::{IssueRepository, IssuesAction, PrStatus};
6+
use crate::{github::Event, handlers::Context};
7+
8+
pub(crate) async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
9+
let Event::Issue(event) = event else {
10+
return Ok(());
11+
};
12+
if event.action != IssuesAction::Opened {
13+
return Ok(());
14+
}
15+
if !event.issue.is_pr() {
16+
return Ok(());
17+
}
18+
19+
// avoid acting on our own open events, otherwise we'll infinitely loop
20+
if event.sender.login == ctx.username {
21+
return Ok(());
22+
}
23+
24+
// If it's not the github-actions bot, we don't expect this handler to be needed. Skip the
25+
// event.
26+
if event.sender.login != "github-actions" {
27+
return Ok(());
28+
}
29+
30+
if DISABLE.load(std::sync::atomic::Ordering::Relaxed) {
31+
tracing::warn!("skipping bot_pull_requests handler due to previous disable",);
32+
return Ok(());
33+
}
34+
35+
// Sanity check that our logic above doesn't cause us to act on PRs in a loop, by
36+
// tracking a window of PRs we've acted on. We can probably drop this if we don't see problems
37+
// in the first few days/weeks of deployment.
38+
{
39+
let mut touched = TOUCHED_PRS.lock().unwrap();
40+
if !touched.insert((event.issue.repository().clone(), event.issue.number)) {
41+
tracing::warn!("touching same PR twice despite username check: {:?}", event);
42+
DISABLE.store(true, std::sync::atomic::Ordering::Relaxed);
43+
return Ok(());
44+
}
45+
if touched.len() > 300 {
46+
touched.drain(..150);
47+
}
48+
}
49+
50+
ctx.github
51+
.set_pr_status(
52+
event.issue.repository(),
53+
event.issue.number,
54+
PrStatus::Closed,
55+
)
56+
.await?;
57+
ctx.github
58+
.set_pr_status(event.issue.repository(), event.issue.number, PrStatus::Open)
59+
.await?;
60+
61+
Ok(())
62+
}
63+
64+
static TOUCHED_PRS: LazyLock<Mutex<IndexSet<(IssueRepository, u64)>>> =
65+
LazyLock::new(|| std::sync::Mutex::new(IndexSet::new()));
66+
static DISABLE: AtomicBool = AtomicBool::new(false);

0 commit comments

Comments
 (0)