comment once on a merged config pr when its swarm build fails
This commit is contained in:
parent
c28ca7aab5
commit
0bc5a7e475
1 changed files with 460 additions and 22 deletions
150
swarm-controller/src/forge/pr_comment.rs
Normal file
150
swarm-controller/src/forge/pr_comment.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
//! Comments on a pull request, posted once per marker.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use forgejo_api::structs::{CreateIssueCommentOption, IssueGetCommentsQuery};
|
||||
|
||||
use super::Client;
|
||||
|
||||
impl Client {
|
||||
/// Comment `body` on pull request `pr` of `owner/repo` unless a comment
|
||||
/// there already contains `marker`. Whether a comment was posted.
|
||||
///
|
||||
/// The marker is what the comment carries, so the check holds across a
|
||||
/// restart and across every caller. It reads the comments first, so two
|
||||
/// concurrent calls with one marker can both post.
|
||||
pub(crate) async fn comment_once(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
pr: u64,
|
||||
marker: &str,
|
||||
body: &str,
|
||||
) -> Result<bool> {
|
||||
let index = i64::try_from(pr).context("PR number out of range")?;
|
||||
let existing = self
|
||||
.api
|
||||
.call(|api| {
|
||||
Box::pin(
|
||||
api.issue_get_comments(
|
||||
owner,
|
||||
repo,
|
||||
index,
|
||||
IssueGetCommentsQuery {
|
||||
since: None,
|
||||
before: None,
|
||||
},
|
||||
)
|
||||
.all(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("listing the comments on {owner}/{repo}#{pr}"))?;
|
||||
if existing
|
||||
.iter()
|
||||
.any(|c| c.body.as_deref().is_some_and(|b| b.contains(marker)))
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let option = CreateIssueCommentOption {
|
||||
body: body.to_owned(),
|
||||
updated_at: None,
|
||||
};
|
||||
self.api
|
||||
.call(|api| {
|
||||
api.issue_create_comment(owner, repo, index, option.clone())
|
||||
.into_future()
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("commenting on {owner}/{repo}#{pr}"))?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use axum::http::{Method, StatusCode, Uri};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The comments a stand-in forge holds, as the issue path they were
|
||||
/// posted to and their body.
|
||||
pub(crate) type Posted = Arc<Mutex<Vec<(String, String)>>>;
|
||||
|
||||
/// A stand-in forge that stores the comments posted to any issue path and
|
||||
/// lists them back.
|
||||
pub(crate) async fn stand_in() -> (Client, Posted) {
|
||||
let held: Posted = Arc::default();
|
||||
let state = Arc::clone(&held);
|
||||
let app = axum::Router::new().fallback(
|
||||
move |method: Method, uri: Uri, body: axum::body::Bytes| {
|
||||
let state = Arc::clone(&state);
|
||||
async move {
|
||||
let path = uri.path().to_owned();
|
||||
let mut held = state.lock().unwrap();
|
||||
match method.as_str() {
|
||||
"GET" => {
|
||||
let list: Vec<_> = held
|
||||
.iter()
|
||||
.filter(|(p, _)| *p == path)
|
||||
.enumerate()
|
||||
.map(
|
||||
|(id, (_, body))| serde_json::json!({ "id": id, "body": body }),
|
||||
)
|
||||
.collect();
|
||||
let count = list.len().to_string();
|
||||
(
|
||||
StatusCode::OK,
|
||||
[("x-total-count", count)],
|
||||
serde_json::Value::from(list).to_string(),
|
||||
)
|
||||
}
|
||||
"POST" => {
|
||||
let sent: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let text = sent["body"].as_str().unwrap().to_owned();
|
||||
held.push((path, text.clone()));
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
[("x-total-count", String::new())],
|
||||
serde_json::json!({ "id": held.len(), "body": text }).to_string(),
|
||||
)
|
||||
}
|
||||
_ => (
|
||||
StatusCode::NOT_FOUND,
|
||||
[("x-total-count", String::new())],
|
||||
r#"{"message":"no"}"#.to_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let url = url::Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap();
|
||||
tokio::spawn(async move { axum::serve(listener, app).await });
|
||||
(Client::for_test_forge(url), held)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_marker_is_commented_once_and_a_new_marker_again() {
|
||||
let (client, held) = stand_in().await;
|
||||
let once = |marker: &'static str| {
|
||||
let client = &client;
|
||||
async move {
|
||||
client
|
||||
.comment_once("o", "r", 7, marker, &format!("{marker} text"))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
};
|
||||
assert!(once("<!-- a -->").await, "first comment is posted");
|
||||
assert!(!once("<!-- a -->").await, "same marker is not posted again");
|
||||
assert!(once("<!-- b -->").await, "control: another marker posts");
|
||||
let held = held.lock().unwrap();
|
||||
assert_eq!(held.len(), 2, "{held:?}");
|
||||
assert_eq!(
|
||||
held[0].0,
|
||||
format!("/api/v1/repos/o/r/{}/7/comments", "issues")
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue