Skip to content

Commit

Permalink
Make issues filter label configurable per repo (#597)
Browse files Browse the repository at this point in the history
Signed-off-by: Sergio Castaño Arteaga <[email protected]>
  • Loading branch information
tegioz authored May 21, 2024
1 parent 7e4eee7 commit 4ae0a8c
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 95 deletions.
176 changes: 90 additions & 86 deletions clotributor-registrar/src/registrar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,92 +12,6 @@ use tracing::{debug, error, info, instrument};
/// Maximum time that can take processing a foundation data file.
const FOUNDATION_TIMEOUT: u64 = 300;

/// Represents a foundation registered in the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Foundation {
pub foundation_id: String,
pub data_url: String,
}

/// Represents a project to be registered or updated.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Project {
pub name: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,

pub description: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub logo_dark_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub devstats_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub accepted_at: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub maturity: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub maintainers_wanted: Option<MaintainersWanted>,

#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,

pub repositories: Vec<Repository>,
}

impl Project {
/// Set the project's digest.
fn set_digest(&mut self) -> Result<()> {
let data = bincode::serialize(&self)?;
let digest = hex::encode(Sha256::digest(data));
self.digest = Some(digest);
Ok(())
}
}

/// Represents a project's repository.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Repository {
pub name: String,
pub url: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<Vec<String>>,
}

/// Defines if the project is looking for maintainers, as well as some extra
/// reference and contact information.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct MaintainersWanted {
enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
links: Option<Vec<Link>>,
#[serde(skip_serializing_if = "Option::is_none")]
contacts: Option<Vec<Contact>>,
}

/// Represents some information about a link.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Link {
title: Option<String>,
url: String,
}

/// Represents some information about a contact.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Contact {
github_handle: String,
}

/// Process foundations registered in the database.
#[instrument(skip_all, err)]
pub(crate) async fn run(cfg: &Config, db: DynDB) -> Result<()> {
Expand Down Expand Up @@ -217,6 +131,95 @@ async fn process_foundation(
Ok(())
}

/// Represents a foundation registered in the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Foundation {
pub foundation_id: String,
pub data_url: String,
}

/// Represents a project to be registered or updated.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Project {
pub name: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,

pub description: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub logo_dark_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub devstats_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub accepted_at: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub maturity: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub maintainers_wanted: Option<MaintainersWanted>,

#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,

pub repositories: Vec<Repository>,
}

impl Project {
/// Set the project's digest.
fn set_digest(&mut self) -> Result<()> {
let data = bincode::serialize(&self)?;
let digest = hex::encode(Sha256::digest(data));
self.digest = Some(digest);
Ok(())
}
}

/// Represents a project's repository.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Repository {
pub name: String,
pub url: String,

#[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<Vec<String>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub issues_filter_label: Option<String>,
}

/// Defines if the project is looking for maintainers, as well as some extra
/// reference and contact information.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct MaintainersWanted {
enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
links: Option<Vec<Link>>,
#[serde(skip_serializing_if = "Option::is_none")]
contacts: Option<Vec<Contact>>,
}

/// Represents some information about a link.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Link {
title: Option<String>,
url: String,
}

/// Represents some information about a contact.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Contact {
github_handle: String,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -419,6 +422,7 @@ mod tests {
name: "artifact-hub".to_string(),
url: "https://github.com/artifacthub/hub".to_string(),
exclude: None,
issues_filter_label: None,
}],
maintainers_wanted: None,
}),
Expand Down
2 changes: 2 additions & 0 deletions clotributor-tracker/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ impl DB for PgDB {
r.languages,
r.stars,
r.digest,
r.issues_filter_label,
p.name as project_name,
p.foundation_id
from repository r
Expand All @@ -84,6 +85,7 @@ impl DB for PgDB {
languages: row.get("languages"),
stars: row.get("stars"),
digest: row.get("digest"),
issues_filter_label: row.get("issues_filter_label"),
project_name: row.get("project_name"),
foundation_id: row.get("foundation_id"),
})
Expand Down
21 changes: 19 additions & 2 deletions clotributor-tracker/src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use time::{
/// GitHub GraphQL API URL.
const GITHUB_GRAPHQL_API_URL: &str = "https://api.github.com/graphql";

/// Label used to filter the issues we want to track.
const DEFAULT_ISSUES_FILTER_LABEL: &str = "help wanted";

lazy_static! {
static ref GITHUB_REPO_URL: Regex =
Regex::new("^https://github.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/?$")
Expand Down Expand Up @@ -112,7 +115,12 @@ impl repo_view::RepoViewRepository {
#[cfg_attr(test, automock)]
pub(crate) trait GH {
/// Get repository information from GitHub.
async fn repository(&self, token: &str, url: &str) -> Result<repo_view::RepoViewRepository>;
async fn repository(
&self,
token: &str,
url: &str,
issues_filter_label: &Option<String>,
) -> Result<repo_view::RepoViewRepository>;
}

/// GH implementation backed by the GitHub GraphQL API.
Expand All @@ -127,16 +135,25 @@ impl GHGraphQL {

#[async_trait]
impl GH for GHGraphQL {
async fn repository(&self, token: &str, url: &str) -> Result<repo_view::RepoViewRepository> {
async fn repository(
&self,
token: &str,
url: &str,
issues_filter_label: &Option<String>,
) -> Result<repo_view::RepoViewRepository> {
// Do request to GraphQL API
let http_client = setup_http_client(token)?;
let (owner, repo) = get_owner_and_repo(url)?;
let issues_since = OffsetDateTime::now_utc()
.saturating_sub(365.days())
.format(&Iso8601::DEFAULT)?;
let issues_label = issues_filter_label
.as_deref()
.unwrap_or(DEFAULT_ISSUES_FILTER_LABEL);
let vars = repo_view::Variables {
repo,
owner,
issues_label: issues_label.to_string(),
issues_since,
};
let req_body = &RepoView::build_query(vars);
Expand Down
4 changes: 2 additions & 2 deletions clotributor-tracker/src/graphql/repo_view.graphql
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
query RepoView($repo: String!, $owner: String!, $issues_since: DateTime!) {
query RepoView($repo: String!, $owner: String!, $issues_label: String!, $issues_since: DateTime!) {
repository(name: $repo, owner: $owner) {
description
homepageUrl
issues (
first: 50,
filterBy: {
assignee: null,
labels: ["help wanted"],
labels: [$issues_label],
since: $issues_since,
states: [OPEN]
},
Expand Down
13 changes: 8 additions & 5 deletions clotributor-tracker/src/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ async fn track_repository(
debug!("started");

// Fetch repository data from GitHub
let gh_repo = gh.repository(&gh_token, &repo.url).await?;
let gh_repo = gh
.repository(&gh_token, &repo.url, &repo.issues_filter_label)
.await?;

// Update repository's GitHub data in db if needed
let changed = repo.update_gh_data(&gh_repo)?;
Expand Down Expand Up @@ -169,6 +171,7 @@ pub(crate) struct Repository {
pub languages: Option<Vec<String>>,
pub stars: Option<i32>,
pub digest: Option<String>,
pub issues_filter_label: Option<String>,
pub project_name: String,
pub foundation_id: String,
}
Expand Down Expand Up @@ -647,9 +650,9 @@ mod tests {
}])))
});
gh.expect_repository()
.with(eq(TOKEN1), eq(REPOSITORY_URL))
.with(eq(TOKEN1), eq(REPOSITORY_URL), eq(None))
.times(1)
.returning(|_, _| Box::pin(future::ready(Err(format_err!(FAKE_ERROR)))));
.returning(|_, _, _| Box::pin(future::ready(Err(format_err!(FAKE_ERROR)))));

let result = run(&cfg, Arc::new(db), Arc::new(gh)).await;
assert_eq!(result.unwrap_err().root_cause().to_string(), FAKE_ERROR);
Expand All @@ -672,9 +675,9 @@ mod tests {
}])))
});
gh.expect_repository()
.with(eq(TOKEN1), eq(REPOSITORY_URL))
.with(eq(TOKEN1), eq(REPOSITORY_URL), eq(None))
.times(1)
.returning(|_, _| {
.returning(|_, _, _| {
Box::pin(future::ready(Ok(RepoViewRepository {
description: Some("description".to_string()),
homepage_url: None,
Expand Down
3 changes: 3 additions & 0 deletions database/migrations/functions/projects/register_project.sql
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,18 @@ begin
insert into repository (
name,
url,
issues_filter_label,
project_id
) values (
v_repository->>'name',
v_repository->>'url',
nullif(v_repository->>'issues_filter_label', ''),
v_project_id
)
on conflict (project_id, url) do update
set
name = excluded.name,
issues_filter_label = excluded.issues_filter_label,
updated_at = current_timestamp;
end loop;

Expand Down
5 changes: 5 additions & 0 deletions database/migrations/schema/003_issues_filter_label.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
alter table repository add column issues_filter_label text;

---- create above / drop below ----

alter table repository drop column issues_filter_label;

0 comments on commit 4ae0a8c

Please sign in to comment.