-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement logic to find old E-needs-mcve issues to close
This is the first commit in a series of commits to implement the automatic triaging of old E-needs-mcve issues that was proposed and discussed in [t-release/triage] and cross-posted to [T-compiler]. This commit only implements the logic to find what the issues to close, and prints that info to stdout for inspection. Think of it as a dry run. After we have convinced ourselves this logic works as it should, we can implement the final steps: * Actually close the issue * Report closes to "triagebot closed issues" topic in "t-release/triage" Zulip [t-release/triage]: https://rust-lang.zulipchat.com/#narrow/stream/242269-t-release.2Ftriage/topic/auto-close.20E-needs-mcve/near/400273684 [t-compiler]: https://rust-lang.zulipchat.com/#narrow/stream/131828-t-compiler/topic/auto.20closing.20E-meeds-mcve/near/399663832
- Loading branch information
Showing
5 changed files
with
338 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
name: Automatic triage | ||
on: | ||
workflow_dispatch: {} | ||
schedule: | ||
- cron: "0 12 * * 1" # Every Monday at 12:00 UTC | ||
|
||
jobs: | ||
automatic-triage: | ||
name: Automatic triage | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@master | ||
- run: cargo run --bin automatic-triage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
use reqwest::Client; | ||
use triagebot::github::GithubClient; | ||
|
||
mod old_label; | ||
|
||
#[tokio::main(flavor = "current_thread")] | ||
async fn main() -> anyhow::Result<()> { | ||
dotenv::dotenv().ok(); | ||
tracing_subscriber::fmt::init(); | ||
|
||
let client = GithubClient::new_with_default_token(Client::new()); | ||
|
||
old_label::triage_old_label( | ||
"rust-lang", | ||
"rust", | ||
"E-needs-mcve", | ||
"triaged", // Exclude e.g. label "AsyncAwait-Triaged" | ||
chrono::Duration::days(30 * 12 * 4), | ||
&client, | ||
) | ||
.await?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
use chrono::{DateTime, Duration, Utc}; | ||
use triagebot::github::GithubClient; | ||
|
||
use github_graphql::old_label_queries::*; | ||
use github_graphql::queries::*; | ||
use triagebot::github::issues_with_label; | ||
|
||
struct AnalyzedIssue { | ||
number: i32, | ||
url: String, | ||
time_until_close: Duration, | ||
} | ||
|
||
pub async fn triage_old_label( | ||
repository_owner: &str, | ||
repository_name: &str, | ||
label: &str, | ||
exclude_labels_containing: &str, | ||
minimum_age: Duration, | ||
client: &GithubClient, | ||
) -> anyhow::Result<()> { | ||
let now = chrono::Utc::now(); | ||
|
||
let mut issues = issues_with_label(repository_owner, repository_name, label, client) | ||
.await? | ||
.into_iter() | ||
.filter(|issue| filter_excluded_labels(issue, exclude_labels_containing)) | ||
.map(|issue| { | ||
// If an issue is actively discussed, there is no limit on the age of the | ||
// label. We don't want to close issues that people are actively commenting on. | ||
// So require the last comment to also be old. | ||
let last_comment_age = last_comment_age(&issue, &now); | ||
|
||
let label_age = label_age(&issue, label, &now); | ||
|
||
AnalyzedIssue { | ||
number: issue.number, | ||
url: issue.url.0, | ||
time_until_close: minimum_age - std::cmp::min(label_age, last_comment_age), | ||
} | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
issues.sort_by_key(|issue| std::cmp::Reverse(issue.time_until_close)); | ||
|
||
for issue in issues { | ||
if issue.time_until_close.num_days() > 0 { | ||
println!( | ||
"{} will be closed after {} months", | ||
issue.url, | ||
issue.time_until_close.num_days() / 30 | ||
); | ||
} else { | ||
println!( | ||
"{} will be closed now (FIXME: Actually implement closing)", | ||
issue.url, | ||
); | ||
close_issue(issue.number, client).await; | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
fn filter_excluded_labels(issue: &OldLabelCandidateIssue, exclude_labels_containing: &str) -> bool { | ||
!issue.labels.as_ref().unwrap().nodes.iter().any(|label| { | ||
label | ||
.name | ||
.to_lowercase() | ||
.contains(exclude_labels_containing) | ||
}) | ||
} | ||
|
||
fn last_comment_age(issue: &OldLabelCandidateIssue, now: &DateTime<Utc>) -> Duration { | ||
let last_comment_at = issue | ||
.comments | ||
.nodes | ||
.last() | ||
.map(|c| c.created_at) | ||
.unwrap_or_else(|| issue.created_at); | ||
|
||
*now - last_comment_at | ||
} | ||
|
||
pub fn label_age(issue: &OldLabelCandidateIssue, label: &str, now: &DateTime<Utc>) -> Duration { | ||
let timeline_items = &issue.timeline_items.as_ref().unwrap(); | ||
|
||
if timeline_items.page_info.has_next_page { | ||
eprintln!( | ||
"{} has more than 250 `LabeledEvent`s. We need to implement paging!", | ||
issue.url.0 | ||
); | ||
return Duration::days(30 * 999999); | ||
} | ||
|
||
let mut last_labeled_at = None; | ||
|
||
// The way the GraphQL query is constructed guarantees that we see the | ||
// oldest event first, so we can simply iterate sequentially. And we don't | ||
// need to bother with UnlabeledEvent since in the query we require the | ||
// label to be present, so we know it has not been unlabeled in the last | ||
// event. | ||
for timeline_item in &timeline_items.nodes { | ||
if let IssueTimelineItems::LabeledEvent(LabeledEvent { | ||
label: Label { name }, | ||
created_at, | ||
}) = timeline_item | ||
{ | ||
if name == label { | ||
last_labeled_at = Some(created_at); | ||
} | ||
} | ||
} | ||
|
||
now.signed_duration_since( | ||
*last_labeled_at.expect("The GraphQL query only includes issues that has the label"), | ||
) | ||
} | ||
|
||
async fn close_issue(_number: i32, _client: &GithubClient) { | ||
// FIXME: Actually close the issue | ||
// FIXME: Report to "triagebot closed issues" topic in "t-release/triage" Zulip | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters