diff --git a/.env-example b/.env-example index dcafeb3..20d550e 100644 --- a/.env-example +++ b/.env-example @@ -1,2 +1,5 @@ GH_TOKEN = " " SEARCH_QUERY = "repo:owner/repo is:open is:issue" +HIDE_TIME_TO_FIRST_RESPONSE = False +HIDE_TIME_TO_CLOSE = False +HIDE_TIME_TO_ANSWER = False diff --git a/README.md b/README.md index 051f6ca..5cb410c 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,11 @@ Below are the allowed configuration options: | field | required | default | description | |-----------------------|----------|---------|-------------| -| `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:` entry or an `org:` entry. For discussions, include `type:discussions` in the query. | +| `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:` entry or an `org:` entry. For discussions, include `type:discussions` in the query. | +| `HIDE_TIME_TO_FIRST_RESPONSE` | False | False | If set to true, the time to first response will not be displayed in the generated markdown file. | +| `HIDE_TIME_TO_CLOSE` | False | False | If set to true, the time to close will not be displayed in the generated markdown file. | +| `HIDE_TIME_TO_ANSWER` | False | False | If set to true, the time to answer a discussion will not be displayed in the generated markdown file. | ### Example workflows @@ -196,6 +199,7 @@ jobs: ## Example issue_metrics.md output +Here is the output with no hidden columns: ```markdown # Issue Metrics @@ -209,13 +213,31 @@ jobs: | 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 | ``` +Here is the output with all hidable columns hidden: +```markdown +# Issue Metrics + +| Metric | Value | +| --- | ---: | +| Number of items that remain open | 2 | +| 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 | 2:26:07 | + +``` + ## Local usage without Docker 1. Copy `.env-example` to `.env` diff --git a/markdown_writer.py b/markdown_writer.py index e9be688..b0eac1d 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -16,14 +16,49 @@ file: file object = None ) -> None: Write the issues with metrics to a markdown file. - + get_non_hidden_columns( + average_time_to_first_response: timedelta, + average_time_to_close: timedelta, + average_time_to_answer: timedelta + ) -> List[str]: + Get the columns that are not hidden. """ + +import os from datetime import timedelta from typing import List, Union from classes import IssueWithMetrics +def get_non_hidden_columns() -> List[str]: + """ + Get a list of the columns that are not hidden. + + Args: + None + + Returns: + List[str]: A list of the columns that are not hidden. + + """ + columns = ["Title", "URL"] + # Find the number of columns and which are to be hidden + 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") + + hide_time_to_close = os.getenv("HIDE_TIME_TO_CLOSE") + if not hide_time_to_close: + columns.append("Time to close") + + hide_time_to_answer = os.getenv("HIDE_TIME_TO_ANSWER") + if not hide_time_to_answer: + columns.append("Time to answer") + + return columns + + def write_to_markdown( issues_with_metrics: Union[List[IssueWithMetrics], None], average_time_to_first_response: Union[timedelta, None], @@ -50,40 +85,58 @@ def write_to_markdown( None. """ + columns = get_non_hidden_columns() + + # If all the metrics are None, then there are no issues if not issues_with_metrics or len(issues_with_metrics) == 0: with file or open("issue_metrics.md", "w", encoding="utf-8") as file: file.write("no issues found for the given search criteria\n\n") - else: - # Sort the issues by time to first response - issues_with_metrics.sort( - key=lambda x: x.time_to_first_response or timedelta.max - ) - with file or open("issue_metrics.md", "w", encoding="utf-8") as file: - file.write("# Issue Metrics\n\n") - file.write("| Metric | Value |\n") - file.write("| --- | ---: |\n") + return + + # Sort the issues by time to first response + issues_with_metrics.sort(key=lambda x: x.time_to_first_response or timedelta.max) + with file or open("issue_metrics.md", "w", encoding="utf-8") as file: + file.write("# Issue Metrics\n\n") + + # Write first table with overall metrics + file.write("| Metric | Value |\n") + file.write("| --- | ---: |\n") + if "Time to first response" in columns: file.write( f"| Average time to first response | {average_time_to_first_response} |\n" ) + if "Time to close" in columns: file.write(f"| Average time to close | {average_time_to_close} |\n") + if "Time to answer" in columns: file.write(f"| Average time to answer | {average_time_to_answer} |\n") - file.write(f"| Number of items that remain open | {num_issues_opened} |\n") - file.write(f"| Number of items closed | {num_issues_closed} |\n") - file.write( - f"| Total number of items created | {len(issues_with_metrics)} |\n\n" - ) - file.write( - "| Title | URL | Time to first response | Time to close | Time to answer |\n" - ) - file.write("| --- | --- | ---: | ---: | ---: |\n") - for issue in issues_with_metrics: - file.write( - f"| " - f"{issue.title} | " - f"{issue.html_url} | " - f"{issue.time_to_first_response} |" - f" {issue.time_to_close} |" - f" {issue.time_to_answer} |" - f"\n" - ) - print("Wrote issue metrics to issue_metrics.md") + file.write(f"| Number of items that remain open | {num_issues_opened} |\n") + file.write(f"| Number of items closed | {num_issues_closed} |\n") + file.write( + f"| Total number of items created | {len(issues_with_metrics)} |\n\n" + ) + + # Write second table with individual issue/pr/discussion metrics + # First write the header + file.write("|") + for column in columns: + file.write(f" {column} |") + file.write("\n") + + # Then write the column dividers + file.write("|") + for _ in columns: + file.write(" --- |") + file.write("\n") + + # Then write the issues/pr/discussions row by row + for issue in issues_with_metrics: + file.write(f"| " f"{issue.title} | " f"{issue.html_url} |") + if "Time to first response" in columns: + file.write(f" {issue.time_to_first_response} |") + if "Time to close" in columns: + file.write(f" {issue.time_to_close} |") + if "Time to answer" in columns: + file.write(f" {issue.time_to_answer} |") + file.write("\n") + + print("Wrote issue metrics to issue_metrics.md") diff --git a/test_markdown_writer.py b/test_markdown_writer.py index ade903c..0e64ff5 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -72,7 +72,7 @@ def test_write_to_markdown(self): "| Number of items closed | 1 |\n" "| Total number of items created | 2 |\n\n" "| Title | URL | Time to first response | Time to close | Time to answer |\n" - "| --- | --- | ---: | ---: | ---: |\n" + "| --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | 1 day, 0:00:00 | " "2 days, 0:00:00 | 3 days, 0:00:00 |\n" "| Issue 2 | https://github.com/user/repo/issues/2 | 3 days, 0:00:00 | " @@ -93,3 +93,76 @@ def test_write_to_markdown_no_issues(self): "issue_metrics.md", "w", encoding="utf-8" ) mock_open_file().write.assert_called_once_with(expected_output) + + +class TestWriteToMarkdownWithEnv(unittest.TestCase): + """Test the write_to_markdown function with the HIDE* environment variables set.""" + + def setUp(self): + # Set the HIDE* environment variables to True + os.environ["HIDE_TIME_TO_FIRST_RESPONSE"] = "True" + os.environ["HIDE_TIME_TO_CLOSE"] = "True" + os.environ["HIDE_TIME_TO_ANSWER"] = "True" + + def tearDown(self): + # Unset the HIDE* environment variables + os.environ.pop("HIDE_TIME_TO_FIRST_RESPONSE") + os.environ.pop("HIDE_TIME_TO_CLOSE") + os.environ.pop("HIDE_TIME_TO_ANSWER") + + def test_writes_markdown_file_with_non_hidden_columns_only(self): + """ + Test that write_to_markdown writes the correct + markdown file with non-hidden columns only. + """ + + # Create mock data + issues_with_metrics = [ + IssueWithMetrics( + title="Issue 1", + html_url="https://github.com/user/repo/issues/1", + time_to_first_response=timedelta(minutes=10), + time_to_close=timedelta(days=1), + time_to_answer=timedelta(hours=2), + ), + IssueWithMetrics( + title="Issue 2", + html_url="https://github.com/user/repo/issues/2", + time_to_first_response=timedelta(minutes=20), + time_to_close=timedelta(days=2), + time_to_answer=timedelta(hours=4), + ), + ] + average_time_to_first_response = timedelta(minutes=15) + average_time_to_close = timedelta(days=1.5) + average_time_to_answer = timedelta(hours=3) + num_issues_opened = 2 + num_issues_closed = 1 + + # Call the function + write_to_markdown( + issues_with_metrics, + average_time_to_first_response, + average_time_to_close, + average_time_to_answer, + num_issues_opened, + num_issues_closed, + ) + + # 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 = ( + "# Issue Metrics\n\n" + "| Metric | Value |\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 |\n" + "| --- | --- |\n" + "| Issue 1 | https://github.com/user/repo/issues/1 |\n" + "| Issue 2 | https://github.com/user/repo/issues/2 |\n" + ) + self.assertEqual(content, expected_content) + os.remove("issue_metrics.md")