From 9f9b4bdad731e006eeb78c9176b7499fa24158f8 Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Sun, 30 Jul 2023 02:42:18 +0900 Subject: [PATCH 1/9] add: ignore_users --- issue_metrics.py | 10 +++++++--- time_to_first_response.py | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 03e8686..8ca94d5 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -13,7 +13,7 @@ Searches for issues in a GitHub repository that match the given search query. auth_to_github() -> github3.GitHub: Connect to GitHub API with token authentication. get_per_issue_metrics(issues: Union[List[dict], List[github3.issues.Issue]], - discussions: bool = False) -> tuple[List, int, int]: + discussions: bool = False), labels: Union[List[str], None] = None, ignore_users: List[str] = [] -> tuple[List, int, int]: Calculate the metrics for each issue in a list of GitHub issues. get_owner(search_query: str) -> Union[str, None]]: Get the owner from the search query. @@ -125,6 +125,7 @@ def get_per_issue_metrics( issues: Union[List[dict], List[github3.search.IssueSearchResult]], # type: ignore discussions: bool = False, labels: Union[List[str], None] = None, + ignore_users: List[str] = [], ) -> tuple[List, int, int]: """ Calculate the metrics for each issue/pr/discussion in a list provided. @@ -135,6 +136,7 @@ def get_per_issue_metrics( discussions (bool, optional): Whether the issues are discussions or not. Defaults to False. labels (List[str]): A list of labels to measure time spent in. Defaults to empty list. + ignore_users (List[str]): A list of users to ignore when calculating metrics. Returns: tuple[List[IssueWithMetrics], int, int]: A tuple containing a @@ -157,7 +159,7 @@ def get_per_issue_metrics( None, ) issue_with_metrics.time_to_first_response = measure_time_to_first_response( - None, issue + None, issue, ignore_users ) issue_with_metrics.time_to_answer = measure_time_to_answer(issue) if issue["closedAt"]: @@ -175,7 +177,7 @@ def get_per_issue_metrics( None, ) issue_with_metrics.time_to_first_response = measure_time_to_first_response( - issue, None + issue, None, ignore_users ) if labels: issue_with_metrics.label_metrics = get_label_metrics(issue, labels) @@ -280,6 +282,8 @@ def main(): issues, discussions="type:discussions" in search_query, labels=labels, + # FIXME: ignore_users should be a list of usernames + ignore_users=[], ) average_time_to_first_response = get_average_time_to_first_response( diff --git a/time_to_first_response.py b/time_to_first_response.py index 347e431..c8c17b2 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -27,12 +27,14 @@ def measure_time_to_first_response( issue: Union[github3.issues.Issue, None], # type: ignore discussion: Union[dict, None], + ignore_users: List[str], ) -> Union[timedelta, None]: """Measure the time to first response for a single issue or a discussion. Args: issue (Union[github3.issues.Issue, None]): A GitHub issue. discussion (Union[dict, None]): A GitHub discussion. + ignore_users (List[str]): A list of GitHub usernames to ignore. Returns: Union[timedelta, None]: The time to first response for the issue/discussion. From 2f13e13673cbe94c998c8bfa5b3ea7667fe71598 Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Sun, 30 Jul 2023 02:46:12 +0900 Subject: [PATCH 2/9] fix: test green: measure_time_to_first_response args changed --- time_to_first_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_to_first_response.py b/time_to_first_response.py index c8c17b2..3433fa6 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -27,7 +27,7 @@ def measure_time_to_first_response( issue: Union[github3.issues.Issue, None], # type: ignore discussion: Union[dict, None], - ignore_users: List[str], + ignore_users: List[str] = [], ) -> Union[timedelta, None]: """Measure the time to first response for a single issue or a discussion. From 527c8ca1aaa84624c2c433571feec3dfaf09ed23 Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Sun, 30 Jul 2023 03:05:45 +0900 Subject: [PATCH 3/9] feat: ignore user --- time_to_first_response.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/time_to_first_response.py b/time_to_first_response.py index 3433fa6..b3c20c1 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -51,6 +51,8 @@ def measure_time_to_first_response( number=1, sort="created", direction="asc" ) # type: ignore for comment in comments: + if comment.user.login in ignore_users: + continue first_comment_time = comment.created_at # Check if the issue is actually a pull request @@ -59,6 +61,8 @@ def measure_time_to_first_response( pull_request = issue.issue.pull_request() review_comments = pull_request.reviews(number=1) # type: ignore for review_comment in review_comments: + if review_comment.user.login in ignore_users: + continue first_review_comment_time = review_comment.submitted_at # Figure out the earliest response timestamp From af917d1bc190865914e5a579bfb1100a62d2b05b Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Sun, 30 Jul 2023 03:19:07 +0900 Subject: [PATCH 4/9] feat: parse ignore-users --- .env-example | 1 + issue_metrics.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.env-example b/.env-example index 29a21d2..03429dd 100644 --- a/.env-example +++ b/.env-example @@ -1,3 +1,4 @@ GH_TOKEN = " " SEARCH_QUERY = "repo:owner/repo is:open is:issue" LABELS_TO_MEASURE = "waiting-for-review,waiting-for-manager" +IGNORE_USERS = "user1,user2" diff --git a/issue_metrics.py b/issue_metrics.py index 8ca94d5..410e028 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -41,13 +41,14 @@ ) -def get_env_vars() -> tuple[str, str]: +def get_env_vars() -> tuple[str, str, List[str]]: """ Get the environment variables for use in the script. Returns: str: the search query used to filter issues, prs, and discussions str: the github token used to authenticate to github.com + List[str]: a list of users to ignore when calculating metrics """ search_query = os.getenv("SEARCH_QUERY") if not search_query: @@ -57,7 +58,13 @@ def get_env_vars() -> tuple[str, str]: if not token: raise ValueError("GITHUB_TOKEN environment variable not set") - return search_query, token + ignore_users = os.getenv("IGNORE_USERS") + if ignore_users: + ignore_users = ignore_users.split(",") + else: + ignore_users = [] + + return search_query, token, ignore_users def search_issues( @@ -240,6 +247,7 @@ def main(): env_vars = get_env_vars() search_query = env_vars[0] token = env_vars[1] + ignore_users = env_vars[2] # Get the repository owner and name from the search query owner = get_owner(search_query) @@ -282,8 +290,7 @@ def main(): issues, discussions="type:discussions" in search_query, labels=labels, - # FIXME: ignore_users should be a list of usernames - ignore_users=[], + ignore_users=ignore_users, ) average_time_to_first_response = get_average_time_to_first_response( From 108c61fb4e3a807ce02a4462899fb72d76f6b3a6 Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Sun, 30 Jul 2023 03:22:08 +0900 Subject: [PATCH 5/9] docs: update README --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 83e395e..7892ee5 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Below are the allowed configuration options: | `HIDE_TIME_TO_CLOSE` | False | | If set to any value, the time to close will not be displayed in the generated markdown file. | | `HIDE_TIME_TO_ANSWER` | False | | If set to any value, the time to answer a discussion will not be displayed in the generated markdown file. | | `HIDE_LABEL_METRICS` | False | | If set to any value, the time in label metrics will not be displayed in the generated markdown file. | +| `IGNORE_USERS` | False | | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`) | ### Example workflows @@ -73,7 +74,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Get dates for last month @@ -84,7 +85,7 @@ jobs: # Calculate the last day of the previous month last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - + #Set an environment variable with the date range echo "$first_day..$last_day" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" @@ -121,7 +122,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Run issue-metrics tool @@ -168,7 +169,7 @@ Both issues and pull requests opened in May 2023: Both issues and pull requests closed in May 2023 (may have been open in May or earlier): - `repo:owner/repo closed:2023-05-01..2023-05-31` -OK, but what if I want both open or closed issues and pull requests? Due to limitations in issue search (no ability for OR logic), you will need to run the action twice, once for opened and once for closed. Here is an example workflow that does this: +OK, but what if I want both open or closed issues and pull requests? Due to limitations in issue search (no ability for OR logic), you will need to run the action twice, once for opened and once for closed. Here is an example workflow that does this: ```yaml name: Monthly issue metrics @@ -187,7 +188,7 @@ jobs: runs-on: ubuntu-latest steps: - + - name: Run issue-metrics tool for issues and prs opened in May 2023 uses: github/issue-metrics@v2 env: @@ -201,7 +202,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} content-filepath: ./issue_metrics.md assignees: - + - name: Run issue-metrics tool for issues and prs closed in May 2023 uses: github/issue-metrics@v2 env: @@ -237,7 +238,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Run issue-metrics tool @@ -377,7 +378,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Get dates for last month @@ -385,10 +386,10 @@ jobs: run: | # Calculate the first day of the previous month first_day=$(date -d "last month" +%Y-%m-01) - + # Calculate the last day of the previous month last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - + #Set an environment variable with the date range echo "$first_day..$last_day" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" @@ -412,7 +413,7 @@ jobs: title: Monthly issue metrics report token: ${{ secrets.GITHUB_TOKEN }} content-filepath: ./issue_metrics.md - assignees: ${{ env.TEAM_MEMBERS }} + assignees: ${{ env.TEAM_MEMBERS }} ``` ## Local usage without Docker From 786fca095dccbdbaa3695dd5d0d7c5f5e8dd23b7 Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Sun, 30 Jul 2023 03:41:05 +0900 Subject: [PATCH 6/9] modify: empty --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7892ee5..c162a2e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Get dates for last month @@ -85,7 +85,7 @@ jobs: # Calculate the last day of the previous month last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - + #Set an environment variable with the date range echo "$first_day..$last_day" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" @@ -122,7 +122,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Run issue-metrics tool @@ -169,7 +169,7 @@ Both issues and pull requests opened in May 2023: Both issues and pull requests closed in May 2023 (may have been open in May or earlier): - `repo:owner/repo closed:2023-05-01..2023-05-31` -OK, but what if I want both open or closed issues and pull requests? Due to limitations in issue search (no ability for OR logic), you will need to run the action twice, once for opened and once for closed. Here is an example workflow that does this: +OK, but what if I want both open or closed issues and pull requests? Due to limitations in issue search (no ability for OR logic), you will need to run the action twice, once for opened and once for closed. Here is an example workflow that does this: ```yaml name: Monthly issue metrics @@ -188,7 +188,7 @@ jobs: runs-on: ubuntu-latest steps: - + - name: Run issue-metrics tool for issues and prs opened in May 2023 uses: github/issue-metrics@v2 env: @@ -202,7 +202,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} content-filepath: ./issue_metrics.md assignees: - + - name: Run issue-metrics tool for issues and prs closed in May 2023 uses: github/issue-metrics@v2 env: @@ -238,7 +238,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Run issue-metrics tool @@ -378,7 +378,7 @@ jobs: build: name: issue metrics runs-on: ubuntu-latest - + steps: - name: Get dates for last month @@ -386,10 +386,10 @@ jobs: run: | # Calculate the first day of the previous month first_day=$(date -d "last month" +%Y-%m-01) - + # Calculate the last day of the previous month last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - + #Set an environment variable with the date range echo "$first_day..$last_day" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" @@ -413,7 +413,7 @@ jobs: title: Monthly issue metrics report token: ${{ secrets.GITHUB_TOKEN }} content-filepath: ./issue_metrics.md - assignees: ${{ env.TEAM_MEMBERS }} + assignees: ${{ env.TEAM_MEMBERS }} ``` ## Local usage without Docker @@ -426,4 +426,4 @@ jobs: ## License -[MIT](LICENSE) +[MIT](LICENSE) \ No newline at end of file From afac3c0406540f9136267b6744f3d7223016e7d0 Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Tue, 1 Aug 2023 03:11:32 +0900 Subject: [PATCH 7/9] add: test case: one ignored, one not ignored --- test_time_to_first_response.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test_time_to_first_response.py b/test_time_to_first_response.py index 7e4fecd..79bf353 100644 --- a/test_time_to_first_response.py +++ b/test_time_to_first_response.py @@ -61,6 +61,31 @@ def test_measure_time_to_first_response_no_comments(self): # Check the results self.assertEqual(result, expected_result) + def test_measure_time_to_first_response_ignore_users(self): + """Test that measure_time_to_first_response ignores comments from ignored users.""" + # Set up the mock GitHub issues + mock_issue1 = MagicMock() + mock_issue1.comments = 1 + mock_issue1.created_at = "2023-01-01T00:00:00Z" + + # Set up the mock GitHub comments (one ignored, one not ignored) + mock_comment1 = MagicMock() + mock_comment1.user.login = "ignored_user" + mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z") + + mock_comment2 = MagicMock() + mock_comment2.user.login = "not_ignored_user" + mock_comment2.created_at = datetime.fromisoformat("2023-01-03T00:00:00Z") + + mock_issue1.issue.comments.return_value = [mock_comment1, mock_comment2] + + # Call the function + result = measure_time_to_first_response(mock_issue1, None, ["ignored_user"]) + expected_result = timedelta(days=2) + + # Check the results + self.assertEqual(result, expected_result) + class TestGetAverageTimeToFirstResponse(unittest.TestCase): """Test the get_average_time_to_first_response function.""" From 0c03676434c8c74c30241b05194f7f87f98ee45e Mon Sep 17 00:00:00 2001 From: Junya Okabe Date: Tue, 1 Aug 2023 03:30:14 +0900 Subject: [PATCH 8/9] add: test case: all ignored users --- test_time_to_first_response.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test_time_to_first_response.py b/test_time_to_first_response.py index 79bf353..0cd4843 100644 --- a/test_time_to_first_response.py +++ b/test_time_to_first_response.py @@ -86,6 +86,31 @@ def test_measure_time_to_first_response_ignore_users(self): # Check the results self.assertEqual(result, expected_result) + def test_measure_time_to_first_response_only_ignored_users(self): + """Test that measure_time_to_first_response returns empty for an issue with only ignored users.""" + # Set up the mock GitHub issues + mock_issue1 = MagicMock() + mock_issue1.comments = 1 + mock_issue1.created_at = "2023-01-01T00:00:00Z" + + # Set up the mock GitHub comments (all ignored) + mock_comment1 = MagicMock() + mock_comment1.user.login = "ignored_user" + mock_comment1.created_at = datetime.fromisoformat("2023-01-02T00:00:00Z") + + mock_comment2 = MagicMock() + mock_comment2.user.login = "ignored_user2" + mock_comment2.created_at = datetime.fromisoformat("2023-01-03T00:00:00Z") + + mock_issue1.issue.comments.return_value = [mock_comment1, mock_comment2] + + # Call the function + result = measure_time_to_first_response(mock_issue1, None, ["ignored_user", "ignored_user2"]) + expected_result = None + + # Check the results + self.assertEqual(result, expected_result) + class TestGetAverageTimeToFirstResponse(unittest.TestCase): """Test the get_average_time_to_first_response function.""" From fd029d1e7c6359e3895cc66766cca754c148ad3e Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Tue, 1 Aug 2023 16:03:06 -0700 Subject: [PATCH 9/9] fix: request more than one comment into the iterator Signed-off-by: Zack Koppert --- time_to_first_response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time_to_first_response.py b/time_to_first_response.py index b3c20c1..a5295ca 100644 --- a/time_to_first_response.py +++ b/time_to_first_response.py @@ -48,7 +48,7 @@ def measure_time_to_first_response( # Get the first comment time if issue: comments = issue.issue.comments( - number=1, sort="created", direction="asc" + number=20, sort="created", direction="asc" ) # type: ignore for comment in comments: if comment.user.login in ignore_users: @@ -59,7 +59,7 @@ def measure_time_to_first_response( # so we may also get the first review comment time if issue.issue.pull_request_urls: pull_request = issue.issue.pull_request() - review_comments = pull_request.reviews(number=1) # type: ignore + review_comments = pull_request.reviews(number=50) # type: ignore for review_comment in review_comments: if review_comment.user.login in ignore_users: continue