diff --git a/README.md b/README.md index e63018c..596bfc3 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,8 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `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. | | `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_FIRST_RESPONSE` | False | False | If set to `true`, the time to first response will not be displayed in the generated Markdown file. | -| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestmap will not be displayed in the generated Markdown file. | +| `HIDE_STATUS` | False | True | If set to `true`, the status column will not be shown | +| `HIDE_CREATED_AT` | False | True | If set to `true`, the creation timestamp will not be displayed in the generated Markdown file. | | `DRAFT_PR_TRACKING` | False | False | If set to `true`, draft PRs will be included in the metrics as a new column and in the summary stats. | | `IGNORE_USERS` | False | False | A comma separated list of users to ignore when calculating metrics. (ie. `IGNORE_USERS: 'user1,user2'`). To ignore bots, append `[bot]` to the user (ie. `IGNORE_USERS: 'github-actions[bot]'`) Users in this list will also have their authored issues and pull requests removed from the Markdown table. | | `ENABLE_MENTOR_COUNT` | False | False | If set to 'TRUE' count number of comments users left on discussions, issues and PRs and display number of active mentors | diff --git a/classes.py b/classes.py index 20ab9b3..d24f430 100644 --- a/classes.py +++ b/classes.py @@ -24,6 +24,7 @@ class IssueWithMetrics: label_metrics (dict, optional): A dictionary containing the label metrics mentor_activity (dict, optional): A dictionary containing active mentors created_at (datetime, optional): The time the issue was created. + status (str, optional): The status of the issue, e.g., "open", "closed as completed", """ # pylint: disable=too-many-instance-attributes @@ -42,6 +43,7 @@ def __init__( created_at=None, assignee=None, assignees=None, + status=None, ): self.title = title self.html_url = html_url @@ -55,3 +57,4 @@ def __init__( self.label_metrics = labels_metrics self.mentor_activity = mentor_activity self.created_at = created_at + self.status = status diff --git a/config.py b/config.py index 55768dc..475aa34 100644 --- a/config.py +++ b/config.py @@ -39,6 +39,8 @@ class EnvVars: hide_time_to_close (bool): If true, the time to close metric is hidden in the output hide_time_to_first_response (bool): If true, the time to first response metric is hidden in the output + hide_created_at (bool): If true, the created at timestamp is hidden in the output + hide_status (bool): If true, the status column is hidden in the output ignore_users (List[str]): List of usernames to ignore when calculating metrics labels_to_measure (List[str]): List of labels to measure how much time the label is applied enable_mentor_count (bool): If set to TRUE, compute number of mentors @@ -73,6 +75,7 @@ def __init__( hide_time_to_close: bool, hide_time_to_first_response: bool, hide_created_at: bool, + hide_status: bool, ignore_user: List[str], labels_to_measure: List[str], enable_mentor_count: bool, @@ -102,6 +105,7 @@ def __init__( self.hide_time_to_close = hide_time_to_close self.hide_time_to_first_response = hide_time_to_first_response self.hide_created_at = hide_created_at + self.hide_status = hide_status self.enable_mentor_count = enable_mentor_count self.min_mentor_comments = min_mentor_comments self.max_comments_eval = max_comments_eval @@ -130,6 +134,7 @@ def __repr__(self): f"{self.hide_time_to_close}," f"{self.hide_time_to_first_response}," f"{self.hide_created_at}," + f"{self.hide_status}," f"{self.ignore_users}," f"{self.labels_to_measure}," f"{self.enable_mentor_count}," @@ -238,6 +243,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_close = get_bool_env_var("HIDE_TIME_TO_CLOSE", False) hide_time_to_first_response = get_bool_env_var("HIDE_TIME_TO_FIRST_RESPONSE", False) hide_created_at = get_bool_env_var("HIDE_CREATED_AT", True) + hide_status = get_bool_env_var("HIDE_STATUS", True) enable_mentor_count = get_bool_env_var("ENABLE_MENTOR_COUNT", False) min_mentor_comments = os.getenv("MIN_MENTOR_COMMENTS", "10") max_comments_eval = os.getenv("MAX_COMMENTS_EVAL", "20") @@ -259,6 +265,7 @@ def get_env_vars(test: bool = False) -> EnvVars: hide_time_to_close, hide_time_to_first_response, hide_created_at, + hide_status, ignore_users_list, labels_to_measure_list, enable_mentor_count, diff --git a/issue_metrics.py b/issue_metrics.py index e7f5982..0c86912 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -175,8 +175,12 @@ def get_per_issue_metrics( issue_with_metrics.time_to_close = measure_time_to_close( issue, None ) + if not env_vars.hide_status: + issue_with_metrics.status = f"{issue.issue.state} as {issue.issue.state_reason}" # type: ignore elif issue.state == "open": # type: ignore num_issues_open += 1 + if not env_vars.hide_status: + issue_with_metrics.status = f"{issue.issue.state}" # type: ignore if not env_vars.hide_created_at: if isinstance(issue, github3.search.IssueSearchResult): # type: ignore issue_with_metrics.created_at = issue.issue.created_at # type: ignore diff --git a/markdown_writer.py b/markdown_writer.py index efaf0ac..7a88a8a 100644 --- a/markdown_writer.py +++ b/markdown_writer.py @@ -75,6 +75,10 @@ def get_non_hidden_columns(labels) -> List[str]: if not hide_time_to_answer: columns.append("Time to answer") + hide_status = env_vars.hide_status + if not hide_status: + columns.append("Status") + enable_time_in_draft = env_vars.draft_pr_tracking if enable_time_in_draft: columns.append("Time in draft") @@ -232,6 +236,8 @@ def write_to_markdown( file.write(f" {issue.label_metrics[label]} |") if "Created At" in columns: file.write(f" {issue.created_at} |") + if "Status" in columns: + file.write(f" {issue.status} |") file.write("\n") file.write( "\n_This report was generated with the \ @@ -324,6 +330,8 @@ def write_overall_metrics_tables( f"| {stats_time_in_labels['med'][label]} " f"| {stats_time_in_labels['90p'][label]} |\n" ) + if "Status" in columns: # Add logic for the 'status' column + file.write("| Status | | | |\n") file.write("\n") # Write count stats to a separate table diff --git a/test_assignee_functionality.py b/test_assignee_functionality.py index 1c12a9b..890fc62 100644 --- a/test_assignee_functionality.py +++ b/test_assignee_functionality.py @@ -75,6 +75,34 @@ def test_get_non_hidden_columns_hides_both_assignee_and_author(self): self.assertNotIn("Assignee", columns) self.assertNotIn("Author", columns) + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_STATUS": "false", + }, + clear=True, + ) + def test_get_non_hidden_columns_includes_status_by_default(self): + """Test that status column is included by default.""" + columns = get_non_hidden_columns(labels=None) + self.assertIn("Status", columns) + + @patch.dict( + os.environ, + { + "GH_TOKEN": "test_token", + "SEARCH_QUERY": "is:issue is:open repo:user/repo", + "HIDE_STATUS": "true", + }, + clear=True, + ) + def test_get_non_hidden_columns_hides_status_when_env_set(self): + """Test that status column is hidden when HIDE_STATUS is true.""" + columns = get_non_hidden_columns(labels=None) + self.assertNotIn("Status", columns) + def test_assignee_column_position(self): """Test that assignee column appears before author column.""" with patch.dict( diff --git a/test_config.py b/test_config.py index 537d157..49435fa 100644 --- a/test_config.py +++ b/test_config.py @@ -131,6 +131,7 @@ def test_get_env_vars_with_github_app(self): hide_time_to_close=False, hide_time_to_first_response=False, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, @@ -186,6 +187,7 @@ def test_get_env_vars_with_token(self): hide_time_to_close=False, hide_time_to_first_response=False, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, @@ -276,6 +278,7 @@ def test_get_env_vars_optional_values(self): hide_time_to_close=True, hide_time_to_first_response=True, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=["waiting-for-review", "waiting-for-manager"], enable_mentor_count=False, @@ -320,6 +323,7 @@ def test_get_env_vars_optionals_are_defaulted(self): hide_time_to_close=False, hide_time_to_first_response=False, hide_created_at=True, + hide_status=True, ignore_user=[], labels_to_measure=[], enable_mentor_count=False, diff --git a/test_markdown_writer.py b/test_markdown_writer.py index bf3612c..0fbcad3 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -23,6 +23,7 @@ "GH_TOKEN": "test_token", "DRAFT_PR_TRACKING": "True", "HIDE_CREATED_AT": "False", + "HIDE_STATUS": "False", }, ) class TestWriteToMarkdown(unittest.TestCase): @@ -128,20 +129,21 @@ def test_write_to_markdown(self): "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" "| Time in draft | 1 day, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" + "| Status | | | |\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 | Assignee | Author | Time to first response | Time to close |" - " Time to answer | Time in draft | Time spent in bug | Created At |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" + "| Title | URL | Assignee | Author | Time to first response | Time to close | " + "Time to answer | Status | Time in draft | Time spent in bug | Created At |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " - "1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 |\n" + "1 day, 0:00:00 | 4 days, 0:00:00 | -5 days, 0:00:00 | None |\n" "| Issue 2 | https://github.com/user/repo/issues/2 | None | [bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | 1 day, 0:00:00 | 2 days, 0:00:00 | -5 days, 0:00:00 | None |\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" ) @@ -182,6 +184,7 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): time_to_first_response=timedelta(days=3), time_to_close=timedelta(days=4), time_to_answer=timedelta(days=5), + time_in_draft=None, labels_metrics={"bug": timedelta(days=2)}, ), ] @@ -243,21 +246,22 @@ def test_write_to_markdown_with_vertical_bar_in_title(self): "| Time to answer | 4 days, 0:00:00 | 4 days, 0:00:00 | 4 days, 0:00:00 |\n" "| Time in draft | 1 day, 0:00:00 | 1 day, 0:00:00 | 1 day, 0:00:00 |\n" "| Time spent in bug | 1 day, 12:00:00 | 1 day, 12:00:00 | 1 day, 12:00:00 |\n" + "| Status | | | |\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 | Assignee | Author | Time to first response | Time to close |" - " Time to answer | Time in draft | Time spent in bug | Created At |\n" - "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" + "| Title | URL | Assignee | Author | Time to first response | Time to close | " + "Time to answer | Status | Time in draft | Time spent in bug | Created At |\n" + "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" "| Issue 1 | https://github.com/user/repo/issues/1 | [charlie](https://github.com/charlie) | " "[alice](https://github.com/alice) | 1 day, 0:00:00 | 2 days, 0:00:00 | 3 days, 0:00:00 | " - "1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 |\n" + "1 day, 0:00:00 | 1 day, 0:00:00 | -5 days, 0:00:00 | None |\n" "| feat| Issue 2 | https://github.com/user/repo/issues/2 | None | " "[bob](https://github.com/bob) | 3 days, 0:00:00 | " - "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 |\n\n" + "4 days, 0:00:00 | 5 days, 0:00:00 | None | 2 days, 0:00:00 | -5 days, 0:00:00 | None |\n\n" "_This report was generated with the [Issue Metrics Action](https://github.com/github/issue-metrics)_\n" ) self.assertEqual(content, expected_content) @@ -308,6 +312,7 @@ def test_write_to_markdown_no_issues(self): "HIDE_LABEL_METRICS": "True", "NON_MENTIONING_LINKS": "True", "GH_ENTERPRISE_URL": "https://ghe.com", + "HIDE_STATUS": "False", }, ) class TestWriteToMarkdownWithEnv(unittest.TestCase): @@ -400,7 +405,116 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): "| Number of items that remain open | 2 |\n" "| Number of most active mentors | 5 |\n" "| Total number of items created | 2 |\n\n" - "| Title | URL | Assignee | Author | Created At |\n" + "| Title | URL | Assignee | Author | Status | Created At |\n" + "| --- | --- | --- | --- | --- | --- |\n" + "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | " + "[alice](https://ghe.com/alice) | -5 days, 0:00:00 | None |\n" + "| Issue 2 | https://www.ghe.com/user/repo/issues/2 | None | [bob](https://ghe.com/bob) | -5 days, 0:00:00 | None |\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" + ) + self.assertEqual(content, expected_content) + os.remove("issue_metrics.md") + + @patch.dict( + os.environ, + { + "SEARCH_QUERY": "is:open repo:user/repo", + "GH_TOKEN": "test_token", + "HIDE_CREATED_AT": "False", + "HIDE_TIME_TO_FIRST_RESPONSE": "True", + "HIDE_TIME_TO_CLOSE": "True", + "HIDE_TIME_TO_ANSWER": "True", + "HIDE_LABEL_METRICS": "True", + "NON_MENTIONING_LINKS": "True", + "GH_ENTERPRISE_URL": "https://ghe.com", + "HIDE_STATUS": "True", # Status column should be hidden + }, + ) + def test_writes_markdown_file_with_hidden_status_column(self): + """ + Test that write_to_markdown writes the correct markdown file + when HIDE_STATUS is set to True, ensuring the Status column + is not present in the output. + """ + # Create mock data + issues_with_metrics = [ + IssueWithMetrics( + title="Issue 1", + html_url="https://ghe.com/user/repo/issues/1", + author="alice", + assignee="charlie", + assignees=["charlie"], + created_at=timedelta(days=-5), + time_to_first_response=timedelta(minutes=10), + time_to_close=timedelta(days=1), + time_to_answer=timedelta(hours=2), + time_in_draft=timedelta(days=1), + labels_metrics={ + "label1": timedelta(days=1), + }, + ), + IssueWithMetrics( + title="Issue 2", + html_url="https://ghe.com/user/repo/issues/2", + author="bob", + assignee=None, + assignees=[], + created_at=timedelta(days=-5), + time_to_first_response=timedelta(minutes=20), + time_to_close=timedelta(days=2), + time_to_answer=timedelta(hours=4), + labels_metrics={ + "label1": timedelta(days=1), + }, + ), + ] + average_time_to_first_response = timedelta(minutes=15) + average_time_to_close = timedelta(days=1.5) + average_time_to_answer = timedelta(hours=3) + average_time_in_draft = timedelta(days=1) + average_time_in_labels = { + "label1": timedelta(days=1), + } + num_issues_opened = 2 + num_issues_closed = 2 + num_mentor_count = 5 + ghe = "https://ghe.com" + + # 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, + average_time_in_draft=average_time_in_draft, + num_issues_opened=num_issues_opened, + num_issues_closed=num_issues_closed, + num_mentor_count=num_mentor_count, + labels=["label1"], + search_query="repo:user/repo is:issue", + hide_label_metrics=True, + hide_items_closed_count=True, + enable_mentor_count=True, + non_mentioning_links=True, + report_title="Issue Metrics", + output_file="issue_metrics.md", + ghe=ghe, + ) + + # 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 | Count |\n" + "| --- | ---: |\n" + "| Number of items that remain open | 2 |\n" + "| Number of most active mentors | 5 |\n" + "| Total number of items created | 2 |\n\n" + "| Title | URL | Assignee | Author | Created At |\n" # Status column should be missing "| --- | --- | --- | --- | --- |\n" "| Issue 1 | https://www.ghe.com/user/repo/issues/1 | [charlie](https://ghe.com/charlie) | " "[alice](https://ghe.com/alice) | -5 days, 0:00:00 |\n" @@ -410,3 +524,7 @@ def test_writes_markdown_file_with_non_hidden_columns_only(self): ) self.assertEqual(content, expected_content) os.remove("issue_metrics.md") + + +if __name__ == "__main__": + unittest.main()