diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b1ce6af..047161a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.24.9 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.24.9 with: sarif_file: results.sarif diff --git a/Dockerfile b/Dockerfile index f7112a5..26a5817 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.13-slim@sha256:f2fdaec50160418e0c2867ba3e254755edd067171725886d5d303fd7057bbf81 +FROM python:3.13-slim@sha256:6544e0e002b40ae0f59bc3618b07c1e48064c4faed3a15ae2fbd2e8f663e8283 LABEL com.github.actions.name="issue-metrics" \ com.github.actions.description="Gather metrics on issues/prs/discussions such as time to first response, count of issues opened, closed, etc." \ com.github.actions.icon="check-square" \ 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/requirements-test.txt b/requirements-test.txt index b69e850..3b294cb 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,9 @@ black==25.1.0 -flake8==7.2.0 +flake8==7.3.0 mypy==1.16.1 mypy-extensions==1.1.0 pylint==3.3.7 -pytest==8.4.0 +pytest==8.4.1 pytest-cov==6.2.1 types-pytz==2025.2.0.20250516 types-requests==2.32.4.20250611 diff --git a/requirements.txt b/requirements.txt index efe5a96..2d48a84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ github3.py==4.0.1 numpy==2.2.4 -python-dotenv==1.1.0 +python-dotenv==1.1.1 pytz==2025.2 requests==2.32.4 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() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy