From 16934620eee64adaebbfdfe7945b73047f12514b Mon Sep 17 00:00:00 2001 From: Sebastian Spier Date: Sun, 29 Oct 2023 09:39:36 +0100 Subject: [PATCH 1/5] WIP: First working implementation of a customizable REPORT_TITLE --- README.md | 1 + issue_metrics.py | 9 ++- markdown_writer.py | 8 ++- test_markdown_writer.py | 125 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4c25205..1826108 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,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. | +| `REPORT_TITLE` | False | "Issue Metrics" | A custom title that will be printed at the very top of the report. _Note:_ Most useful when combining multiple reports into a single GitHub issue. | | `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. | diff --git a/issue_metrics.py b/issue_metrics.py index ef3c81b..1f4f66e 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -52,6 +52,7 @@ def get_env_vars() -> tuple[str, str, List[str]]: 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 + str: the title to use at the very top of the markdown output """ search_query = os.getenv("SEARCH_QUERY") if not search_query: @@ -67,7 +68,11 @@ def get_env_vars() -> tuple[str, str, List[str]]: else: ignore_users = [] - return search_query, token, ignore_users + report_title = os.getenv("REPORT_TITLE") + if not report_title: + report_title = "Issue Metrics" + + return search_query, token, ignore_users, report_title def search_issues( @@ -272,6 +277,7 @@ def main(): search_query = env_vars[0] token = env_vars[1] ignore_users = env_vars[2] + report_title = env_vars[3] # Get the repository owner and name from the search query owner = get_owner(search_query) @@ -351,6 +357,7 @@ def main(): num_issues_closed, labels, search_query, + report_title, ) diff --git a/markdown_writer.py b/markdown_writer.py index 889a525..2b66016 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -78,6 +78,7 @@ def write_to_markdown( num_issues_closed: Union[int, None], labels=None, search_query=None, + report_title=None, hide_label_metrics=False, ) -> None: """Write the issues with metrics to a markdown file. @@ -91,11 +92,12 @@ def write_to_markdown( average_time_in_labels (dict): A dictionary containing the average time spent in each label. file (file object, optional): The file object to write to. If not provided, a file named "issue_metrics.md" will be created. - num_issues_opened (int): The Number of items that remain opened. + num_issues_opened (int): The number of items that remain opened. num_issues_closed (int): The number of issues that were closed. labels (List[str]): A list of the labels that are used in the issues. search_query (str): The search query used to find the issues. - hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output + report_title (str): The title to use at the top of the markdown output. + hide_label_metrics (bool): Represents whether the user has chosen to hide label metrics in the output. Returns: None. @@ -103,7 +105,7 @@ def write_to_markdown( """ columns = get_non_hidden_columns(labels) with open("issue_metrics.md", "w", encoding="utf-8") as file: - file.write("# Issue Metrics\n\n") + file.write(f"# {report_title}\n\n") # If all the metrics are None, then there are no issues if not issues_with_metrics or len(issues_with_metrics) == 0: diff --git a/test_markdown_writer.py b/test_markdown_writer.py index f741e66..d62bbdf 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -85,6 +85,7 @@ def test_write_to_markdown(self): num_issues_closed=num_issues_closed, labels=["bug"], search_query="is:issue is:open label:bug", + report_title="Issue Metrics", ) # Check that the function writes the correct markdown file @@ -181,6 +182,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, labels=["bug"], + report_title="Issue Metrics", ) # Check that the function writes the correct markdown file @@ -216,7 +218,17 @@ def test_write_to_markdown_no_issues(self): """Test that write_to_markdown writes the correct markdown file when no issues are found.""" # Call the function with no issues with patch("builtins.open", mock_open()) as mock_open_file: - write_to_markdown(None, None, None, None, None, None, None) + # write_to_markdown(None, None, None, None, None, None, None) + write_to_markdown( + issues_with_metrics=None, + average_time_to_first_response=None, + average_time_to_close=None, + average_time_to_answer=None, + average_time_in_labels=None, + num_issues_opened=None, + num_issues_closed=None, + report_title="Issue Metrics", + ) # Check that the file was written correctly expected_output = [ @@ -303,6 +315,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): labels=["label1"], search_query="repo:user/repo is:issue", hide_label_metrics=True, + report_title="Issue Metrics", ) # Check that the function writes the correct markdown file @@ -324,3 +337,113 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): ) self.assertEqual(content, expected_content) os.remove("issue_metrics.md") + + +class TestWriteToMarkdownWithOutputCustomization(unittest.TestCase): + """Test the write_to_markdown function with the REPORT_TITLE environment variables set.""" + + def setUp(self): + os.environ["REPORT_TITLE"] = "My Custom Report Title" + # os.environ["HIDE_TIME_TO_CLOSE"] = "True" + # os.environ["HIDE_TIME_TO_ANSWER"] = "True" + # os.environ["HIDE_LABEL_METRICS"] = "True" + + def tearDown(self): + # Unset the environment variables + os.environ.pop("REPORT_TITLE") + # os.environ.pop("HIDE_TIME_TO_CLOSE") + # os.environ.pop("HIDE_TIME_TO_ANSWER") + # os.environ.pop("HIDE_LABEL_METRICS") + + def test_writes_markdown_file_with_output_customization(self): + """ + Test that write_to_markdown writes the correct + markdown file. + """ + + # Create mock data + issues_with_metrics = [ + IssueWithMetrics( + "Issue 1", + "https://github.com/user/repo/issues/1", + "alice", + timedelta(days=1), + timedelta(days=2), + timedelta(days=3), + {"bug": timedelta(days=1)}, + ), + 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), + {"bug": timedelta(days=2)}, + ), + ] + average_time_to_first_response = { + "avg": timedelta(days=2), + "med": timedelta(days=2), + "90p": timedelta(days=2), + } + average_time_to_close = { + "avg": timedelta(days=3), + "med": timedelta(days=3), + "90p": timedelta(days=3), + } + average_time_to_answer = { + "avg": timedelta(days=4), + "med": timedelta(days=4), + "90p": timedelta(days=4), + } + average_time_in_labels = { + "avg": {"bug": "1 day, 12:00:00"}, + "med": {"bug": "1 day, 12:00:00"}, + "90p": {"bug": "1 day, 12:00:00"}, + } + + num_issues_opened = 2 + num_issues_closed = 1 + + # Call the function + write_to_markdown( + issues_with_metrics=issues_with_metrics, + average_time_to_first_response=average_time_to_first_response, + average_time_to_close=average_time_to_close, + average_time_to_answer=average_time_to_answer, + average_time_in_labels=average_time_in_labels, + num_issues_opened=num_issues_opened, + num_issues_closed=num_issues_closed, + labels=["bug"], + report_title="My Custom Report Title", + ) + + # Check that the function writes the correct markdown file + with open("issue_metrics.md", "r", encoding="utf-8") as file: + content = file.read() + expected_content = ( + "# My Custom Report Title\n\n" + "| Metric | Average | Median | 90th percentile |\n" + "| --- | --- | --- | ---: |\n" + "| Time to first response | 2 days, 0:00:00 | 2 days, 0:00:00 | 2 days, 0:00:00 |\n" + "| Time to close | 3 days, 0:00:00 | 3 days, 0:00:00 | 3 days, 0:00:00 |\n" + "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" + "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" + "\n" + "| Metric | Count |\n" + "| --- | ---: |\n" + "| Number of items that remain open | 2 |\n" + "| Number of items closed | 1 |\n" + "| Total number of items created | 2 |\n\n" + "| 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 | 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 | 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" + ) + self.assertEqual(content, expected_content) + os.remove("issue_metrics.md") \ No newline at end of file From ae1df11e4910852edfa9d51d1913b06a31582515 Mon Sep 17 00:00:00 2001 From: Sebastian Spier Date: Sun, 29 Oct 2023 09:40:14 +0100 Subject: [PATCH 2/5] Lint fixes --- test_markdown_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_markdown_writer.py b/test_markdown_writer.py index d62bbdf..ccf8b5a 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -446,4 +446,4 @@ def test_writes_markdown_file_with_output_customization(self): "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) self.assertEqual(content, expected_content) - os.remove("issue_metrics.md") \ No newline at end of file + os.remove("issue_metrics.md") From 5c5cd84b95caffd33fc84b832bed9a79b5a16949 Mon Sep 17 00:00:00 2001 From: Sebastian Spier Date: Sun, 29 Oct 2023 11:56:30 +0100 Subject: [PATCH 3/5] Remove outdated documentation related to a file object passed to write_to_markdown() --- markdown_writer.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/markdown_writer.py b/markdown_writer.py index 2b66016..35c8c12 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -12,8 +12,7 @@ average_time_to_close: timedelta, average_time_to_answer: timedelta, num_issues_opened: int, - num_issues_closed: int, - file: file object = None + num_issues_closed: int ) -> None: Write the issues with metrics to a markdown file. get_non_hidden_columns( @@ -90,8 +89,6 @@ def write_to_markdown( average_time_to_close (datetime.timedelta): The average time to close for the issues. average_time_to_answer (datetime.timedelta): The average time to answer the discussions. average_time_in_labels (dict): A dictionary containing the average time spent in each label. - file (file object, optional): The file object to write to. If not provided, - a file named "issue_metrics.md" will be created. num_issues_opened (int): The number of items that remain opened. num_issues_closed (int): The number of issues that were closed. labels (List[str]): A list of the labels that are used in the issues. From 8b227f0b10540313b55bdff776efeeb203aab967 Mon Sep 17 00:00:00 2001 From: Sebastian Spier Date: Tue, 31 Oct 2023 11:37:53 +0100 Subject: [PATCH 4/5] Moving default for REPORT_TITLE to write_to_markdown() signature --- issue_metrics.py | 3 --- markdown_writer.py | 4 ++-- test_markdown_writer.py | 11 ----------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/issue_metrics.py b/issue_metrics.py index 1f4f66e..c874e40 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -69,9 +69,6 @@ def get_env_vars() -> tuple[str, str, List[str]]: ignore_users = [] report_title = os.getenv("REPORT_TITLE") - if not report_title: - report_title = "Issue Metrics" - return search_query, token, ignore_users, report_title diff --git a/markdown_writer.py b/markdown_writer.py index 35c8c12..f91df70 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -77,7 +77,7 @@ def write_to_markdown( num_issues_closed: Union[int, None], labels=None, search_query=None, - report_title=None, + report_title="Issue Metrics", hide_label_metrics=False, ) -> None: """Write the issues with metrics to a markdown file. @@ -100,7 +100,7 @@ def write_to_markdown( None. """ - columns = get_non_hidden_columns(labels) + columns = get_non_hidden_columns(labels) with open("issue_metrics.md", "w", encoding="utf-8") as file: file.write(f"# {report_title}\n\n") diff --git a/test_markdown_writer.py b/test_markdown_writer.py index ccf8b5a..9800f61 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -85,7 +85,6 @@ def test_write_to_markdown(self): num_issues_closed=num_issues_closed, labels=["bug"], search_query="is:issue is:open label:bug", - report_title="Issue Metrics", ) # Check that the function writes the correct markdown file @@ -182,7 +181,6 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): num_issues_opened=num_issues_opened, num_issues_closed=num_issues_closed, labels=["bug"], - report_title="Issue Metrics", ) # Check that the function writes the correct markdown file @@ -227,7 +225,6 @@ def test_write_to_markdown_no_issues(self): average_time_in_labels=None, num_issues_opened=None, num_issues_closed=None, - report_title="Issue Metrics", ) # Check that the file was written correctly @@ -315,7 +312,6 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): labels=["label1"], search_query="repo:user/repo is:issue", hide_label_metrics=True, - report_title="Issue Metrics", ) # Check that the function writes the correct markdown file @@ -344,16 +340,9 @@ class TestWriteToMarkdownWithOutputCustomization(unittest.TestCase): def setUp(self): os.environ["REPORT_TITLE"] = "My Custom Report Title" - # os.environ["HIDE_TIME_TO_CLOSE"] = "True" - # os.environ["HIDE_TIME_TO_ANSWER"] = "True" - # os.environ["HIDE_LABEL_METRICS"] = "True" def tearDown(self): - # Unset the environment variables os.environ.pop("REPORT_TITLE") - # os.environ.pop("HIDE_TIME_TO_CLOSE") - # os.environ.pop("HIDE_TIME_TO_ANSWER") - # os.environ.pop("HIDE_LABEL_METRICS") def test_writes_markdown_file_with_output_customization(self): """ From 98d984216ac7878c3b3e37486f6a03f28242072d Mon Sep 17 00:00:00 2001 From: Sebastian Spier Date: Tue, 31 Oct 2023 11:56:59 +0100 Subject: [PATCH 5/5] Remove whitespace --- markdown_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markdown_writer.py b/markdown_writer.py index f91df70..c95bed0 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -100,7 +100,7 @@ def write_to_markdown( None. """ - columns = get_non_hidden_columns(labels) + columns = get_non_hidden_columns(labels) with open("issue_metrics.md", "w", encoding="utf-8") as file: file.write(f"# {report_title}\n\n")