Skip to content

Commit 346541f

Browse files
authored
Merge pull request #540 from github/copilot/fix-539
feat: add assignee support to issue metrics reporting
2 parents 36809e1 + 9aa2fd3 commit 346541f

11 files changed

+465
-14
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe
147147
| ----------------------------- | -------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
148148
| `GH_ENTERPRISE_URL` | False | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com |
149149
| `RATE_LIMIT_BYPASS` | False | `false` | If set to `true`, the rate limit will be bypassed. This is useful if being run on an local GitHub server with rate limiting disabled. |
150+
| `HIDE_ASSIGNEE` | False | False | If set to `true`, the assignee will not be displayed in the generated Markdown file. |
150151
| `HIDE_AUTHOR` | False | False | If set to `true`, the author will not be displayed in the generated Markdown file. |
151152
| `HIDE_ITEMS_CLOSED_COUNT` | False | False | If set to `true`, the number of items closed metric will not be displayed in the generated Markdown file. |
152153
| `HIDE_LABEL_METRICS` | False | False | If set to `true`, the time in label metrics will not be displayed in the generated Markdown file. |

classes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class IssueWithMetrics:
1313
title (str): The title of the issue.
1414
html_url (str): The URL of the issue on GitHub.
1515
author (str): The author of the issue.
16+
assignee (str, optional): The primary assignee of the issue.
17+
assignees (list, optional): All assignees of the issue.
1618
time_to_first_response (timedelta, optional): The time it took to
1719
get the first response to the issue.
1820
time_to_close (timedelta, optional): The time it took to close the issue.
@@ -38,10 +40,14 @@ def __init__(
3840
labels_metrics=None,
3941
mentor_activity=None,
4042
created_at=None,
43+
assignee=None,
44+
assignees=None,
4145
):
4246
self.title = title
4347
self.html_url = html_url
4448
self.author = author
49+
self.assignee = assignee
50+
self.assignees = assignees or []
4551
self.time_to_first_response = time_to_first_response
4652
self.time_to_close = time_to_close
4753
self.time_to_answer = time_to_answer

config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class EnvVars:
3030
authentication
3131
gh_token (str | None): GitHub personal access token (PAT) for API authentication
3232
ghe (str): The GitHub Enterprise URL to use for authentication
33+
hide_assignee (bool): If true, the assignee's information is hidden in the output
3334
hide_author (bool): If true, the author's information is hidden in the output
3435
hide_items_closed_count (bool): If true, the number of items closed metric is hidden
3536
in the output
@@ -64,6 +65,7 @@ def __init__(
6465
gh_app_enterprise_only: bool,
6566
gh_token: str | None,
6667
ghe: str | None,
68+
hide_assignee: bool,
6769
hide_author: bool,
6870
hide_items_closed_count: bool,
6971
hide_label_metrics: bool,
@@ -92,6 +94,7 @@ def __init__(
9294
self.ghe = ghe
9395
self.ignore_users = ignore_user
9496
self.labels_to_measure = labels_to_measure
97+
self.hide_assignee = hide_assignee
9598
self.hide_author = hide_author
9699
self.hide_items_closed_count = hide_items_closed_count
97100
self.hide_label_metrics = hide_label_metrics
@@ -119,6 +122,7 @@ def __repr__(self):
119122
f"{self.gh_app_enterprise_only},"
120123
f"{self.gh_token},"
121124
f"{self.ghe},"
125+
f"{self.hide_assignee},"
122126
f"{self.hide_author},"
123127
f"{self.hide_items_closed_count}),"
124128
f"{self.hide_label_metrics},"
@@ -226,6 +230,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
226230
draft_pr_tracking = get_bool_env_var("DRAFT_PR_TRACKING", False)
227231

228232
# Hidden columns
233+
hide_assignee = get_bool_env_var("HIDE_ASSIGNEE", False)
229234
hide_author = get_bool_env_var("HIDE_AUTHOR", False)
230235
hide_items_closed_count = get_bool_env_var("HIDE_ITEMS_CLOSED_COUNT", False)
231236
hide_label_metrics = get_bool_env_var("HIDE_LABEL_METRICS", False)
@@ -246,6 +251,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
246251
gh_app_enterprise_only,
247252
gh_token,
248253
ghe,
254+
hide_assignee,
249255
hide_author,
250256
hide_items_closed_count,
251257
hide_label_metrics,

issue_metrics.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ def get_per_issue_metrics(
8585
None,
8686
None,
8787
)
88+
# Discussions typically don't have assignees in the same way as issues/PRs
89+
issue_with_metrics.assignee = None
90+
issue_with_metrics.assignees = []
8891
if env_vars.hide_time_to_first_response is False:
8992
issue_with_metrics.time_to_first_response = (
9093
measure_time_to_first_response(None, issue, ignore_users)
@@ -119,6 +122,20 @@ def get_per_issue_metrics(
119122
author=issue.user["login"], # type: ignore
120123
)
121124

125+
# Extract assignee information from the issue
126+
issue_dict = issue.issue.as_dict() # type: ignore
127+
assignee = None
128+
assignees = []
129+
130+
if issue_dict.get("assignee"):
131+
assignee = issue_dict["assignee"]["login"]
132+
133+
if issue_dict.get("assignees"):
134+
assignees = [a["login"] for a in issue_dict["assignees"]]
135+
136+
issue_with_metrics.assignee = assignee
137+
issue_with_metrics.assignees = assignees
138+
122139
# Check if issue is actually a pull request
123140
pull_request, ready_for_review_at = None, None
124141
if issue.issue.pull_request_urls: # type: ignore

json_writer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ def write_to_json(
177177
"title": issue.title,
178178
"html_url": issue.html_url,
179179
"author": issue.author,
180+
"assignee": issue.assignee,
181+
"assignees": issue.assignees,
180182
"time_to_first_response": str(issue.time_to_first_response),
181183
"time_to_close": str(issue.time_to_close),
182184
"time_to_answer": str(issue.time_to_answer),

markdown_writer.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ def get_non_hidden_columns(labels) -> List[str]:
5555
env_vars = get_env_vars()
5656

5757
# Find the number of columns and which are to be hidden
58+
hide_assignee = env_vars.hide_assignee
59+
if not hide_assignee:
60+
columns.append("Assignee")
61+
5862
hide_author = env_vars.hide_author
5963
if not hide_author:
6064
columns.append("Author")
@@ -203,6 +207,15 @@ def write_to_markdown(
203207
)
204208
else:
205209
file.write(f"| {issue.title} | {issue.html_url} |")
210+
if "Assignee" in columns:
211+
if issue.assignees:
212+
assignee_links = [
213+
f"[{assignee}](https://{endpoint}/{assignee})"
214+
for assignee in issue.assignees
215+
]
216+
file.write(f" {', '.join(assignee_links)} |")
217+
else:
218+
file.write(" None |")
206219
if "Author" in columns:
207220
file.write(f" [{issue.author}](https://{endpoint}/{issue.author}) |")
208221
if "Time to first response" in columns:

test_assignee_functionality.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Test assignee functionality added to issue metrics."""
2+
3+
import os
4+
import unittest
5+
from unittest.mock import patch
6+
7+
from classes import IssueWithMetrics
8+
from markdown_writer import get_non_hidden_columns
9+
10+
11+
class TestAssigneeFunctionality(unittest.TestCase):
12+
"""Test suite for the assignee functionality."""
13+
14+
@patch.dict(
15+
os.environ,
16+
{
17+
"GH_TOKEN": "test_token",
18+
"SEARCH_QUERY": "is:issue is:open repo:user/repo",
19+
"HIDE_ASSIGNEE": "false",
20+
"HIDE_AUTHOR": "false",
21+
},
22+
clear=True,
23+
)
24+
def test_get_non_hidden_columns_includes_assignee_by_default(self):
25+
"""Test that assignee column is included by default."""
26+
columns = get_non_hidden_columns(labels=None)
27+
self.assertIn("Assignee", columns)
28+
self.assertIn("Author", columns)
29+
30+
@patch.dict(
31+
os.environ,
32+
{
33+
"GH_TOKEN": "test_token",
34+
"SEARCH_QUERY": "is:issue is:open repo:user/repo",
35+
"HIDE_ASSIGNEE": "true",
36+
"HIDE_AUTHOR": "false",
37+
},
38+
clear=True,
39+
)
40+
def test_get_non_hidden_columns_hides_assignee_when_env_set(self):
41+
"""Test that assignee column is hidden when HIDE_ASSIGNEE is true."""
42+
columns = get_non_hidden_columns(labels=None)
43+
self.assertNotIn("Assignee", columns)
44+
self.assertIn("Author", columns)
45+
46+
@patch.dict(
47+
os.environ,
48+
{
49+
"GH_TOKEN": "test_token",
50+
"SEARCH_QUERY": "is:issue is:open repo:user/repo",
51+
"HIDE_ASSIGNEE": "false",
52+
"HIDE_AUTHOR": "true",
53+
},
54+
clear=True,
55+
)
56+
def test_get_non_hidden_columns_shows_assignee_but_hides_author(self):
57+
"""Test that assignee can be shown while author is hidden."""
58+
columns = get_non_hidden_columns(labels=None)
59+
self.assertIn("Assignee", columns)
60+
self.assertNotIn("Author", columns)
61+
62+
@patch.dict(
63+
os.environ,
64+
{
65+
"GH_TOKEN": "test_token",
66+
"SEARCH_QUERY": "is:issue is:open repo:user/repo",
67+
"HIDE_ASSIGNEE": "true",
68+
"HIDE_AUTHOR": "true",
69+
},
70+
clear=True,
71+
)
72+
def test_get_non_hidden_columns_hides_both_assignee_and_author(self):
73+
"""Test that both assignee and author can be hidden."""
74+
columns = get_non_hidden_columns(labels=None)
75+
self.assertNotIn("Assignee", columns)
76+
self.assertNotIn("Author", columns)
77+
78+
def test_assignee_column_position(self):
79+
"""Test that assignee column appears before author column."""
80+
with patch.dict(
81+
os.environ,
82+
{
83+
"GH_TOKEN": "test_token",
84+
"SEARCH_QUERY": "is:issue is:open repo:user/repo",
85+
"HIDE_ASSIGNEE": "false",
86+
"HIDE_AUTHOR": "false",
87+
},
88+
clear=True,
89+
):
90+
columns = get_non_hidden_columns(labels=None)
91+
assignee_index = columns.index("Assignee")
92+
author_index = columns.index("Author")
93+
self.assertLess(
94+
assignee_index,
95+
author_index,
96+
"Assignee column should appear before Author column",
97+
)
98+
99+
def test_multiple_assignees_rendering_logic(self):
100+
"""Test that multiple assignees are rendered correctly in assignee column."""
101+
102+
# Test the assignee rendering logic directly
103+
endpoint = "github.com"
104+
columns = ["Title", "URL", "Assignee", "Author"]
105+
106+
# Initialize variables
107+
multiple_output = ""
108+
single_output = ""
109+
none_output = ""
110+
111+
# Test case 1: Multiple assignees
112+
issue_multiple = IssueWithMetrics(
113+
title="Test Issue with Multiple Assignees",
114+
html_url="https://github.com/test/repo/issues/1",
115+
author="testuser",
116+
assignee="alice",
117+
assignees=["alice", "bob", "charlie"],
118+
)
119+
120+
# Simulate the new rendering logic
121+
if "Assignee" in columns:
122+
if issue_multiple.assignees:
123+
assignee_links = [
124+
f"[{assignee}](https://{endpoint}/{assignee})"
125+
for assignee in issue_multiple.assignees
126+
]
127+
multiple_output = f" {', '.join(assignee_links)} |"
128+
else:
129+
multiple_output = " None |"
130+
131+
expected_multiple = (
132+
" [alice](https://github.com/alice), [bob](https://github.com/bob), "
133+
"[charlie](https://github.com/charlie) |"
134+
)
135+
self.assertEqual(
136+
multiple_output,
137+
expected_multiple,
138+
"Multiple assignees should be rendered as comma-separated links",
139+
)
140+
141+
# Test case 2: Single assignee
142+
issue_single = IssueWithMetrics(
143+
title="Test Issue with Single Assignee",
144+
html_url="https://github.com/test/repo/issues/2",
145+
author="testuser",
146+
assignee="alice",
147+
assignees=["alice"],
148+
)
149+
150+
if "Assignee" in columns:
151+
if issue_single.assignees:
152+
assignee_links = [
153+
f"[{assignee}](https://{endpoint}/{assignee})"
154+
for assignee in issue_single.assignees
155+
]
156+
single_output = f" {', '.join(assignee_links)} |"
157+
else:
158+
single_output = " None |"
159+
160+
expected_single = " [alice](https://github.com/alice) |"
161+
self.assertEqual(
162+
single_output,
163+
expected_single,
164+
"Single assignee should be rendered as a single link",
165+
)
166+
167+
# Test case 3: No assignees
168+
issue_none = IssueWithMetrics(
169+
title="Test Issue with No Assignees",
170+
html_url="https://github.com/test/repo/issues/3",
171+
author="testuser",
172+
assignee=None,
173+
assignees=[],
174+
)
175+
176+
if "Assignee" in columns:
177+
if issue_none.assignees:
178+
assignee_links = [
179+
f"[{assignee}](https://{endpoint}/{assignee})"
180+
for assignee in issue_none.assignees
181+
]
182+
none_output = f" {', '.join(assignee_links)} |"
183+
else:
184+
none_output = " None |"
185+
186+
expected_none = " None |"
187+
self.assertEqual(
188+
none_output, expected_none, "No assignees should be rendered as 'None'"
189+
)
190+
191+
print(f"✅ Multiple assignees test: {expected_multiple}")
192+
print(f"✅ Single assignee test: {expected_single}")
193+
print(f"✅ No assignees test: {expected_none}")
194+
195+
196+
if __name__ == "__main__":
197+
unittest.main()

0 commit comments

Comments
 (0)