diff --git a/README.md b/README.md index b4f3c3a..e3cfe63 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The metrics that are measured are: | Time to answer | (Discussions only) The time between when a discussion is created and when it is answered. | | Time in label | The time between when a label has a specific label applied to an issue/pull request/discussion and when it is removed. This requires the LABELS_TO_MEASURE env variable to be set. | -*For pull requests, these metrics exclude the time the PR was in draft mode. +*For pull requests, these metrics exclude the time the PR was in draft mode. *For Issue and pull requests, issue/pull request author's own comments and comments by bots are excluded. This action was developed by the GitHub OSPO for our own use and developed in a way that we could open source it that it might be useful to you as well! If you want to know more about how we use it, reach out in an issue in this repository. @@ -51,6 +51,7 @@ Below are the allowed configuration options: | `GH_TOKEN` | True | | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | | `SEARCH_QUERY` | True | | The query by which you can filter issues/prs which must contain a `repo:`, `org:`, `owner:`, or a `user:` entry. For discussions, include `type:discussions` in the query. | | `LABELS_TO_MEASURE` | False | | A comma separated list of labels to measure how much time the label is applied. If not provided, no labels durations will be measured. Not compatible with discussions at this time. | +| `HIDE_AUTHOR` | False | | If set to any value, the author will not be displayed in the generated markdown file. | | `HIDE_TIME_TO_FIRST_RESPONSE` | False | | If set to any value, the time to first response will not be displayed in the generated markdown file. | | `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. | @@ -324,10 +325,10 @@ then the report will look like this: | Number of items closed | 1 | | Total number of items created | 3 | -| Title | URL | Time to first response | Time to close | Time to answer | Time spent in waiting-for-manager-approval | Time spent in waiting-for-security-review | -| --- | --- | --- | --- | --- | --- | --- | -| Pull Request Title 1 | https://github.com/user/repo/pulls/1 | 0:05:26 | None | None | None | None | -| Issue Title 2 | https://github.com/user/repo/issues/2 | 2:26:07 | None | None | 0:00:41 | 2 days, 4:25:03 | +| Title | URL | Author | Time to first response | Time to close | Time to answer | Time spent in waiting-for-manager-approval | Time spent in waiting-for-security-review | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Pull Request Title 1 | https://github.com/user/repo/pulls/1 | alice | 0:05:26 | None | None | None | None | +| Issue Title 2 | https://github.com/user/repo/issues/2 | bob | 2:26:07 | None | None | 0:00:41 | 2 days, 4:25:03 | ``` @@ -346,11 +347,11 @@ Here is the output with no hidden columns: | Number of items closed | 1 | | Total number of items created | 3 | -| Title | URL | Time to first response | Time to close | Time to answer | -| --- | --- | --- | --- | --- | -| Discussion Title 1 | https://github.com/user/repo/discussions/1 | 0:00:41 | 6 days, 7:08:52 | 1 day | -| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | 0:05:26 | None | None | -| Issue Title 3 | https://github.com/user/repo/issues/3 | 2:26:07 | None | None | +| Title | URL | Author | Time to first response | Time to close | Time to answer | +| --- | --- | --- | --- | --- | --- | +| Discussion Title 1 | https://github.com/user/repo/discussions/1 | None | 0:00:41 | 6 days, 7:08:52 | 1 day | +| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | bob | 0:05:26 | None | None | +| Issue Title 3 | https://github.com/user/repo/issues/3 | carol | 2:26:07 | None | None | ``` @@ -364,11 +365,11 @@ Here is the output with all hidable columns hidden: | Number of items closed | 1 | | Total number of items created | 3 | -| Title | URL | -| --- | --- | -| Discussion Title 1 | https://github.com/user/repo/discussions/1 | -| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | -| Issue Title 3 | https://github.com/user/repo/issues/3 | +| Title | URL | Author | +| --- | --- | --- | +| Discussion Title 1 | https://github.com/user/repo/discussions/1 | None | +| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | bob | +| Issue Title 3 | https://github.com/user/repo/issues/3 | carol | ``` diff --git a/classes.py b/classes.py index 8550275..3867f26 100644 --- a/classes.py +++ b/classes.py @@ -12,6 +12,7 @@ class IssueWithMetrics: Attributes: title (str): The title of the issue. html_url (str): The URL of the issue on GitHub. + author (str): The author of the issue. time_to_first_response (timedelta, optional): The time it took to get the first response to the issue. time_to_close (timedelta, optional): The time it took to close the issue. @@ -25,6 +26,7 @@ def __init__( self, title, html_url, + author, time_to_first_response=None, time_to_close=None, time_to_answer=None, @@ -32,6 +34,7 @@ def __init__( ): self.title = title self.html_url = html_url + self.author = author self.time_to_first_response = time_to_first_response self.time_to_close = time_to_close self.time_to_answer = time_to_answer diff --git a/issue_metrics.py b/issue_metrics.py index 33d534e..d9102a6 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -176,6 +176,7 @@ def get_per_issue_metrics( None, None, None, + None, ) issue_with_metrics.time_to_first_response = measure_time_to_first_response( None, issue, ignore_users @@ -190,6 +191,7 @@ def get_per_issue_metrics( issue_with_metrics = IssueWithMetrics( issue.title, # type: ignore issue.html_url, # type: ignore + issue.user["login"], # type: ignore None, None, None, diff --git a/json_writer.py b/json_writer.py index 81dafeb..283f09f 100644 --- a/json_writer.py +++ b/json_writer.py @@ -48,6 +48,7 @@ def write_to_json( { "title": "Issue 1", "html_url": "https://github.com/owner/repo/issues/1", + "author": "author", "time_to_first_response": "3 days, 0:00:00", "time_to_close": "6 days, 0:00:00", "time_to_answer": "None", @@ -58,6 +59,7 @@ def write_to_json( { "title": "Issue 2", "html_url": "https://github.com/owner/repo/issues/2", + "author": "author", "time_to_first_response": "2 days, 0:00:00", "time_to_close": "4 days, 0:00:00", "time_to_answer": "1 day, 0:00:00", @@ -100,6 +102,7 @@ def write_to_json( { "title": issue.title, "html_url": issue.html_url, + "author": issue.author, "time_to_first_response": str(issue.time_to_first_response), "time_to_close": str(issue.time_to_close), "time_to_answer": str(issue.time_to_answer), diff --git a/markdown_writer.py b/markdown_writer.py index 7e23593..82ed856 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -44,6 +44,10 @@ def get_non_hidden_columns(labels) -> List[str]: """ columns = ["Title", "URL"] # Find the number of columns and which are to be hidden + hide_author = os.getenv("HIDE_AUTHOR") + if not hide_author: + columns.append("Author") + hide_time_to_first_response = os.getenv("HIDE_TIME_TO_FIRST_RESPONSE") if not hide_time_to_first_response: columns.append("Time to first response") @@ -142,6 +146,8 @@ def write_to_markdown( issue.title = issue.title.strip() file.write(f"| " f"{issue.title} | " f"{issue.html_url} |") + if "Author" in columns: + file.write(f" {issue.author} |") if "Time to first response" in columns: file.write(f" {issue.time_to_first_response} |") if "Time to close" in columns: diff --git a/test_issue_metrics.py b/test_issue_metrics.py index ee50085..7b6d965 100644 --- a/test_issue_metrics.py +++ b/test_issue_metrics.py @@ -167,11 +167,13 @@ def test_main( ( "Issue 1", "https://github.com/user/repo/issues/1", + "alice", timedelta(days=1, hours=2, minutes=30), ), ( "Issue 2", "https://github.com/user/repo/issues/2", + "bob", timedelta(days=3, hours=4, minutes=30), ), ] @@ -231,6 +233,7 @@ def test_get_per_issue_metrics(self): mock_issue1 = MagicMock( title="Issue 1", html_url="https://github.com/user/repo/issues/1", + author="alice", state="open", comments=1, created_at="2023-01-01T00:00:00Z", @@ -244,6 +247,7 @@ def test_get_per_issue_metrics(self): mock_issue2 = MagicMock( title="Issue 2", html_url="https://github.com/user/repo/issues/2", + author="bob", state="closed", comments=1, created_at="2023-01-01T00:00:00Z", @@ -276,6 +280,7 @@ def test_get_per_issue_metrics(self): IssueWithMetrics( "Issue 1", "https://github.com/user/repo/issues/1", + "alice", timedelta(days=1), None, None, @@ -284,6 +289,7 @@ def test_get_per_issue_metrics(self): IssueWithMetrics( "Issue 2", "https://github.com/user/repo/issues/2", + "bob", timedelta(days=2), timedelta(days=3), None, @@ -320,6 +326,9 @@ def setUp(self): self.issue1 = { "title": "Issue 1", "url": "github.com/user/repo/issues/1", + "user": { + "login": "alice", + }, "createdAt": "2023-01-01T00:00:00Z", "comments": { "nodes": [ @@ -335,6 +344,9 @@ def setUp(self): self.issue2 = { "title": "Issue 2", "url": "github.com/user/repo/issues/2", + "user": { + "login": "bob", + }, "createdAt": "2023-01-01T00:00:00Z", "comments": {"nodes": [{"createdAt": "2023-01-03T00:00:00Z"}]}, "answerChosenAt": "2023-01-05T00:00:00Z", diff --git a/test_json_writer.py b/test_json_writer.py index e68fa42..1a23e83 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -16,6 +16,7 @@ def test_write_to_json(self): IssueWithMetrics( title="Issue 1", html_url="https://github.com/owner/repo/issues/1", + author="alice", time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=6), time_to_answer=None, @@ -26,6 +27,7 @@ def test_write_to_json(self): IssueWithMetrics( title="Issue 2", html_url="https://github.com/owner/repo/issues/2", + author="bob", time_to_first_response=timedelta(days=2), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=1), @@ -50,6 +52,7 @@ def test_write_to_json(self): { "title": "Issue 1", "html_url": "https://github.com/owner/repo/issues/1", + "author": "alice", "time_to_first_response": "3 days, 0:00:00", "time_to_close": "6 days, 0:00:00", "time_to_answer": "None", @@ -58,6 +61,7 @@ def test_write_to_json(self): { "title": "Issue 2", "html_url": "https://github.com/owner/repo/issues/2", + "author": "bob", "time_to_first_response": "2 days, 0:00:00", "time_to_close": "4 days, 0:00:00", "time_to_answer": "1 day, 0:00:00", diff --git a/test_labels.py b/test_labels.py index af882f1..a266647 100644 --- a/test_labels.py +++ b/test_labels.py @@ -75,7 +75,7 @@ def setUp(self): self.issues_with_metrics = MagicMock() self.issues_with_metrics = [ IssueWithMetrics( - "issue1", "url1", None, None, None, {"bug": timedelta(days=2)} + "issue1", "url1", "alice", None, None, None, {"bug": timedelta(days=2)} ), ] diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 40cb85d..11efeef 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -32,6 +32,7 @@ def test_write_to_markdown(self): IssueWithMetrics( "Issue 1", "https://github.com/user/repo/issues/1", + "alice", timedelta(days=1), timedelta(days=2), timedelta(days=3), @@ -40,6 +41,7 @@ def test_write_to_markdown(self): IssueWithMetrics( "Issue 2\r", "https://github.com/user/repo/issues/2", + "bob", timedelta(days=3), timedelta(days=4), timedelta(days=5), @@ -80,12 +82,12 @@ def test_write_to_markdown(self): "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Time to first response | Time to close |" + "| Title | URL | Author | Time to first response | Time to close |" " Time to answer | Time spent in bug |\n" - "| --- | --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | 1 day, 0:00:00 | " + "| --- | --- | --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | alice | 1 day, 0:00:00 | " "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 |\n" - "| Issue 2 | https://github.com/user/repo/issues/2 | 3 days, 0:00:00 | " + "| Issue 2 | https://github.com/user/repo/issues/2 | bob | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | 2 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `is:issue is:open label:bug`\n" @@ -107,6 +109,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): IssueWithMetrics( "Issue 1", "https://github.com/user/repo/issues/1", + "alice", timedelta(days=1), timedelta(days=2), timedelta(days=3), @@ -115,6 +118,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): IssueWithMetrics( "feat| Issue 2", # title contains a vertical bar "https://github.com/user/repo/issues/2", + "bob", timedelta(days=3), timedelta(days=4), timedelta(days=5), @@ -154,12 +158,12 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Time to first response | Time to close |" + "| Title | URL | Author | Time to first response | Time to close |" " Time to answer | Time spent in bug |\n" - "| --- | --- | --- | --- | --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 | 1 day, 0:00:00 | " + "| --- | --- | --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | alice | 1 day, 0:00:00 | " "2 days, 0:00:00 | 3 days, 0:00:00 | 1 day, 0:00:00 |\n" - "| feat| Issue 2 | https://github.com/user/repo/issues/2 | 3 days, 0:00:00 | " + "| feat| Issue 2 | https://github.com/user/repo/issues/2 | bob | 3 days, 0:00:00 | " "4 days, 0:00:00 | 5 days, 0:00:00 | 2 days, 0:00:00 |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) @@ -208,6 +212,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): IssueWithMetrics( title="Issue 1", html_url="https://github.com/user/repo/issues/1", + author="alice", time_to_first_response=timedelta(minutes=10), time_to_close=timedelta(days=1), time_to_answer=timedelta(hours=2), @@ -218,6 +223,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): IssueWithMetrics( title="Issue 2", html_url="https://github.com/user/repo/issues/2", + author="bob", time_to_first_response=timedelta(minutes=20), time_to_close=timedelta(days=2), time_to_answer=timedelta(hours=4), @@ -258,10 +264,10 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Number of items that remain open | 2 |\n" "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL |\n" - "| --- | --- |\n" - "| Issue 1 | https://github.com/user/repo/issues/1 |\n" - "| Issue 2 | https://github.com/user/repo/issues/2 |\n\n" + "| Title | URL | Author |\n" + "| --- | --- | --- |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 | alice |\n" + "| Issue 2 | https://github.com/user/repo/issues/2 | bob |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" "Search query used to find these items: `repo:user/repo is:issue`\n" ) diff --git a/test_time_to_answer.py b/test_time_to_answer.py index 55541bc..7097f95 100644 --- a/test_time_to_answer.py +++ b/test_time_to_answer.py @@ -35,8 +35,8 @@ def test_returns_none_for_list_with_no_time_to_answer(self): """ # Arrange issues_with_metrics = [ - IssueWithMetrics("issue1", None), - IssueWithMetrics("issue2", None), + IssueWithMetrics("issue1", None, None), + IssueWithMetrics("issue2", None, None), ] # Act @@ -53,9 +53,9 @@ def test_returns_average_time_to_answer(self): # Arrange issues_with_metrics = [ - IssueWithMetrics("issue1", "url1", None, None, timedelta(seconds=10)), - IssueWithMetrics("issue2", "url2", None, None, timedelta(seconds=20)), - IssueWithMetrics("issue3", "url3", None, None, timedelta(seconds=30)), + IssueWithMetrics("issue1", "url1", "alice", None, None, timedelta(seconds=10)), + IssueWithMetrics("issue2", "url2", "bob", None, None, timedelta(seconds=20)), + IssueWithMetrics("issue3", "url3", "carol", None, None, timedelta(seconds=30)), ] # Act diff --git a/test_time_to_close.py b/test_time_to_close.py index 5f7631b..d4f1bea 100644 --- a/test_time_to_close.py +++ b/test_time_to_close.py @@ -27,17 +27,19 @@ def test_get_average_time_to_close(self): IssueWithMetrics( "Issue 1", "https://github.com/user/repo/issues/1", + "alice", None, timedelta(days=2), ), IssueWithMetrics( "Issue 2", "https://github.com/user/repo/issues/2", + "bob", None, timedelta(days=4), ), IssueWithMetrics( - "Issue 3", "https://github.com/user/repo/issues/3", None, None + "Issue 3", "https://github.com/user/repo/issues/3", "carol", None, None ), ] @@ -51,13 +53,13 @@ def test_get_average_time_to_close_no_issues(self): # Create mock data issues_with_metrics = [ IssueWithMetrics( - "Issue 1", "https://github.com/user/repo/issues/1", None, None + "Issue 1", "https://github.com/user/repo/issues/1", "alice", None, None ), IssueWithMetrics( - "Issue 2", "https://github.com/user/repo/issues/2", None, None + "Issue 2", "https://github.com/user/repo/issues/2", "bob", None, None ), IssueWithMetrics( - "Issue 3", "https://github.com/user/repo/issues/3", None, None + "Issue 3", "https://github.com/user/repo/issues/3", "carol", None, None ), ] diff --git a/test_time_to_first_response.py b/test_time_to_first_response.py index dc8c6b5..a5132bb 100644 --- a/test_time_to_first_response.py +++ b/test_time_to_first_response.py @@ -325,12 +325,12 @@ def test_get_average_time_to_first_response(self): # Create mock data issues_with_metrics = [ IssueWithMetrics( - "Issue 1", "https://github.com/user/repo/issues/1", timedelta(days=1) + "Issue 1", "https://github.com/user/repo/issues/1", "alice", timedelta(days=1) ), IssueWithMetrics( - "Issue 2", "https://github.com/user/repo/issues/2", timedelta(days=2) + "Issue 2", "https://github.com/user/repo/issues/2", "bob", timedelta(days=2) ), - IssueWithMetrics("Issue 3", "https://github.com/user/repo/issues/3", None), + IssueWithMetrics("Issue 3", "https://github.com/user/repo/issues/3", "carol", None), ] # Call the function and check the result @@ -343,8 +343,8 @@ def test_get_average_time_to_first_response_with_all_none(self): # Create mock data with all None issues_with_metrics = [ - IssueWithMetrics("Issue 1", "https://github.com/user/repo/issues/1", None), - IssueWithMetrics("Issue 2", "https://github.com/user/repo/issues/2", None), + IssueWithMetrics("Issue 1", "https://github.com/user/repo/issues/1", "alice", None), + IssueWithMetrics("Issue 2", "https://github.com/user/repo/issues/2", "bob", None), ] # Call the function and check the result