From 3f3e55f01e7ab3ae634023f04376fcd7ab0eeb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Wed, 22 May 2024 09:40:29 +0200 Subject: [PATCH] Add labels to issues and PRs (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #396 Signed-off-by: Sergio CastaƱo Arteaga --- src/cfg.rs | 4 +- src/github.rs | 58 +++++++++++++++++++++++++ src/processor.rs | 109 +++++++++++++++++++++++++++++++++++++++++++---- src/results.rs | 4 +- 4 files changed, 163 insertions(+), 12 deletions(-) diff --git a/src/cfg.rs b/src/cfg.rs index 9bc7cf2..42b5bf2 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -1,5 +1,5 @@ use crate::github::{DynGH, File, TeamSlug, UserName}; -use anyhow::{format_err, Result}; +use anyhow::{bail, Result}; use ignore::gitignore::GitignoreBuilder; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, time::Duration}; @@ -119,7 +119,7 @@ impl CfgProfile { .and_then(|allowed_voters| allowed_voters.teams.as_ref()) { if !teams.is_empty() { - return Err(format_err!(ERR_TEAMS_NOT_ALLOWED)); + bail!(ERR_TEAMS_NOT_ALLOWED); } } } diff --git a/src/github.rs b/src/github.rs index 0e226b8..056ebb3 100644 --- a/src/github.rs +++ b/src/github.rs @@ -39,6 +39,16 @@ pub(crate) type UserName = String; #[async_trait] #[cfg_attr(test, automock)] pub(crate) trait GH { + /// Add labels to the provided issue. + async fn add_labels( + &self, + inst_id: u64, + owner: &str, + repo: &str, + issue_number: i64, + labels: &[&str], + ) -> Result<()>; + /// Create a check run for the head commit in the provided pull request. async fn create_check_run( &self, @@ -119,6 +129,16 @@ pub(crate) trait GH { body: &str, ) -> Result; + /// Remove label from the provided issue. + async fn remove_label( + &self, + inst_id: u64, + owner: &str, + repo: &str, + issue_number: i64, + label: &str, + ) -> Result<()>; + /// Check if the user given is a collaborator of the provided repository. async fn user_is_collaborator( &self, @@ -150,6 +170,27 @@ impl GHApi { #[async_trait] impl GH for GHApi { + /// [GH::add_labels] + async fn add_labels( + &self, + inst_id: u64, + owner: &str, + repo: &str, + issue_number: i64, + labels: &[&str], + ) -> Result<()> { + let client = self.app_client.installation(InstallationId(inst_id)); + let labels = labels + .iter() + .map(ToString::to_string) + .collect::>(); + client + .issues(owner, repo) + .add_labels(issue_number as u64, &labels) + .await?; + Ok(()) + } + /// [GH::create_check_run] async fn create_check_run( &self, @@ -383,6 +424,23 @@ impl GH for GHApi { Ok(comment.id.0 as i64) } + /// [GH::remove_label] + async fn remove_label( + &self, + inst_id: u64, + owner: &str, + repo: &str, + issue_number: i64, + label: &str, + ) -> Result<()> { + let client = self.app_client.installation(InstallationId(inst_id)); + client + .issues(owner, repo) + .remove_label(issue_number as u64, label) + .await?; + Ok(()) + } + /// [GH::user_is_collaborator] async fn user_is_collaborator( &self, diff --git a/src/processor.rs b/src/processor.rs index e2e7fdb..e26d1e5 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -40,6 +40,12 @@ const STATUS_CHECK_FREQUENCY: Duration = Duration::from_secs(60 * 30); /// How often we try to auto close votes that have already passed. const AUTO_CLOSE_FREQUENCY: Duration = Duration::from_secs(60 * 60 * 24); +/// Label used to tag issues/prs where a vote has been created. +const GITVOTE_LABEL: &str = "gitvote"; + +/// Label used to tag issues/prs where a vote is open. +const VOTE_OPEN_LABEL: &str = "vote open"; + /// A votes processor is in charge of creating the requested votes, stopping /// them at the scheduled time and publishing results, etc. pub(crate) struct Processor { @@ -313,6 +319,17 @@ impl Processor { .await?; } + // Add "vote open" label to issue/pr + self.gh + .add_labels( + inst_id, + owner, + repo, + i.issue_number, + &[GITVOTE_LABEL, VOTE_OPEN_LABEL], + ) + .await?; + debug!(?vote_id, "created"); Ok(()) } @@ -352,15 +369,23 @@ impl Processor { .post_comment(inst_id, owner, repo, i.issue_number, &body) .await?; - // Create check run if needed - if cancelled_vote_id.is_some() && i.is_pull_request { - let check_details = CheckDetails { - status: "completed".to_string(), - conclusion: Some("success".to_string()), - summary: "Vote cancelled".to_string(), - }; + // Create check run and remove "vote open" label if the vote was cancelled + if cancelled_vote_id.is_some() { + // Create check run if the vote is on a pull request + if i.is_pull_request { + let check_details = CheckDetails { + status: "completed".to_string(), + conclusion: Some("success".to_string()), + summary: "Vote cancelled".to_string(), + }; + self.gh + .create_check_run(inst_id, owner, repo, i.issue_number, &check_details) + .await?; + } + + // Remove "vote open" label from issue/pr self.gh - .create_check_run(inst_id, owner, repo, i.issue_number, &check_details) + .remove_label(inst_id, owner, repo, i.issue_number, VOTE_OPEN_LABEL) .await?; } @@ -469,6 +494,11 @@ impl Processor { .await?; } + // Remove "vote open" label from issue/pr + self.gh + .remove_label(inst_id, owner, repo, vote.issue_number, VOTE_OPEN_LABEL) + .await?; + debug!("closed"); Ok(Some(())) } @@ -834,6 +864,15 @@ mod tests { && body == expected_body.as_str() }) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(COMMENT_ID)))); + gh.expect_add_labels() + .withf(|inst_id, owner, repo, issue_number, labels| { + *inst_id == INST_ID + && owner == ORG + && repo == REPO + && *issue_number == ISSUE_NUM + && labels == vec![GITVOTE_LABEL, VOTE_OPEN_LABEL] + }) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); votes_processor @@ -899,6 +938,15 @@ mod tests { }), ) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); + gh.expect_add_labels() + .withf(|inst_id, owner, repo, issue_number, labels| { + *inst_id == INST_ID + && owner == ORG + && repo == REPO + && *issue_number == ISSUE_NUM + && labels == vec![GITVOTE_LABEL, VOTE_OPEN_LABEL] + }) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); votes_processor @@ -1007,6 +1055,15 @@ mod tests { && body == expected_body.as_str() }) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(COMMENT_ID)))); + gh.expect_remove_label() + .with( + eq(INST_ID), + eq(ORG), + eq(REPO), + eq(ISSUE_NUM), + eq(VOTE_OPEN_LABEL), + ) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let event = setup_test_issue_comment_event(); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); @@ -1049,6 +1106,15 @@ mod tests { }), ) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); + gh.expect_remove_label() + .with( + eq(INST_ID), + eq(ORG), + eq(REPO), + eq(ISSUE_NUM), + eq(VOTE_OPEN_LABEL), + ) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let event = setup_test_pr_event(); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); @@ -1216,6 +1282,15 @@ mod tests { && body == expected_body.as_str() }) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(COMMENT_ID)))); + gh.expect_remove_label() + .with( + eq(INST_ID), + eq(ORG), + eq(REPO), + eq(ISSUE_NUM), + eq(VOTE_OPEN_LABEL), + ) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); votes_processor.close_finished_vote().await.unwrap(); @@ -1256,6 +1331,15 @@ mod tests { }), ) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); + gh.expect_remove_label() + .with( + eq(INST_ID), + eq(ORG), + eq(REPO), + eq(ISSUE_NUM), + eq(VOTE_OPEN_LABEL), + ) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); votes_processor.close_finished_vote().await.unwrap(); @@ -1300,6 +1384,15 @@ mod tests { }), ) .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); + gh.expect_remove_label() + .with( + eq(INST_ID), + eq(ORG), + eq(REPO), + eq(ISSUE_NUM), + eq(VOTE_OPEN_LABEL), + ) + .returning(|_, _, _, _, _| Box::pin(future::ready(Ok(())))); let votes_processor = Processor::new(Arc::new(db), Arc::new(gh)); votes_processor.close_finished_vote().await.unwrap(); diff --git a/src/results.rs b/src/results.rs index 6e02f7e..33e21f6 100644 --- a/src/results.rs +++ b/src/results.rs @@ -2,7 +2,7 @@ use crate::{ cfg::CfgProfile, github::{DynGH, UserName}, }; -use anyhow::{format_err, Result}; +use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; @@ -76,7 +76,7 @@ impl VoteOption { REACTION_IN_FAVOR => Self::InFavor, REACTION_AGAINST => Self::Against, REACTION_ABSTAIN => Self::Abstain, - _ => return Err(format_err!("reaction not supported")), + _ => bail!("reaction not supported"), }; Ok(vote_option) }