Skip to content

Commit 4afea24

Browse files
authored
feat(metricsai): honor --all-authors for testing — count local-run classifier comments (#51)
* feat(metricsai): honor --all-authors in the testing module The security module already lets --all-authors lift the author allowlist so comments are matched on the Conventional-Comment label alone (mirrors the shell harvester's TARGET_USER=ANY). The testing module did not: it only counted classifier comments authored by METRICSAI_TESTING_GITHUB_AUTHORS (default github-actions[bot]), so a classifier comment posted under a developer's identity — the local/Path-A path — was silently dropped. Thread match_all_authors through the classifier source chain (fetch_classifier_comments → _scan_repo_classifier → _classify_classifier), mirroring the existing security chain, and wire settings.all_authors into the testing module's gather(). --all-authors now applies to both modules. Tests mirror the security match_all_authors coverage in test_github_source.py. * docs(test-classifier): note TARGET_USER=ANY / --all-authors for local-run metrics Local --post-comment runs post under the developer's identity, so the default author gate drops them. Note the escape hatch in the metrics README next to the TARGET_USER description (TARGET_USER=ANY for the shell harvester, --all-authors for metricsai), matching how security keeps author-matching in the metrics doc rather than the developer setup doc.
1 parent 503dc0f commit 4afea24

5 files changed

Lines changed: 57 additions & 7 deletions

File tree

metricsai/src/metricsai/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ def build_parser() -> argparse.ArgumentParser:
125125
parser.add_argument(
126126
"--all-authors",
127127
action="store_true",
128-
help="Count security-reviewer comments from any author, ignoring the --author / "
129-
"METRICSAI_SECURITY_GITHUB_AUTHORS allowlist (overrides METRICSAI_ALL_AUTHORS).",
128+
help="Count comments from any author (security and testing), matching on the "
129+
"Conventional-Comment label alone (overrides METRICSAI_ALL_AUTHORS).",
130130
)
131131
parser.add_argument(
132132
"--skip-sechub",

metricsai/src/metricsai/modules/testing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def gather(self, ctx: RunContext) -> dict[str, MetricValue]:
6060
authors=settings.testing_authors,
6161
start=start,
6262
end=end,
63+
match_all_authors=settings.all_authors,
6364
)
6465
return _aggregate_classifications(classifications)
6566

metricsai/src/metricsai/sources/github.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def fetch_classifier_comments(
152152
authors: Iterable[str],
153153
start: datetime,
154154
end: datetime,
155+
match_all_authors: bool = False,
155156
) -> list[Classification]:
156157
"""Fetch the AI test-classifier's comments and expand them to per-verdict records.
157158
@@ -167,6 +168,7 @@ def fetch_classifier_comments(
167168
:param authors: Comment author logins to keep (case-insensitive).
168169
:param start: Inclusive window start (timezone-aware UTC).
169170
:param end: Inclusive window end (timezone-aware UTC).
171+
:param match_all_authors: When ``True``, accept any author and ignore ``authors``.
170172
:returns: One record per classification across all matching comments.
171173
"""
172174
gh = Github(base_url=base_url, auth=Auth.Token(token))
@@ -176,14 +178,25 @@ def fetch_classifier_comments(
176178
logger.debug("Scanning %s for classifier comments", full_name)
177179
out.extend(
178180
_scan_repo_classifier(
179-
gh.get_repo(full_name), full_name, authors_lower, start=start, end=end
181+
gh.get_repo(full_name),
182+
full_name,
183+
authors_lower,
184+
start=start,
185+
end=end,
186+
match_all_authors=match_all_authors,
180187
)
181188
)
182189
return out
183190

184191

185192
def _scan_repo_classifier(
186-
repo: object, full_name: str, authors_lower: set[str], *, start: datetime, end: datetime
193+
repo: object,
194+
full_name: str,
195+
authors_lower: set[str],
196+
*,
197+
start: datetime,
198+
end: datetime,
199+
match_all_authors: bool = False,
187200
) -> list[Classification]:
188201
"""Scan one repository's PR issue + review comments for classifier verdicts.
189202
@@ -197,14 +210,21 @@ def _scan_repo_classifier(
197210
:param authors_lower: Lower-cased author logins to keep.
198211
:param start: Inclusive window start (UTC).
199212
:param end: Inclusive window end (UTC).
213+
:param match_all_authors: When ``True``, accept any author and ignore ``authors_lower``.
200214
:returns: The classifications harvested from this repository's classifier comments.
201215
"""
202216
matched: list[Classification] = []
203217
counts = {"issue": [0, 0], "review": [0, 0]}
204218

205219
def record(source: str, obj: object) -> None:
206220
counts[source][0] += 1
207-
records = _classify_classifier(obj, authors_lower=authors_lower, start=start, end=end)
221+
records = _classify_classifier(
222+
obj,
223+
authors_lower=authors_lower,
224+
start=start,
225+
end=end,
226+
match_all_authors=match_all_authors,
227+
)
208228
if records:
209229
matched.extend(records)
210230
counts[source][1] += 1
@@ -223,7 +243,12 @@ def record(source: str, obj: object) -> None:
223243

224244

225245
def _classify_classifier(
226-
obj: object, *, authors_lower: set[str], start: datetime, end: datetime
246+
obj: object,
247+
*,
248+
authors_lower: set[str],
249+
start: datetime,
250+
end: datetime,
251+
match_all_authors: bool = False,
227252
) -> list[Classification]:
228253
"""Expand one raw comment into its classifications, or ``[]`` if it does not count.
229254
@@ -236,14 +261,15 @@ def _classify_classifier(
236261
:param authors_lower: Lower-cased author logins to keep.
237262
:param start: Inclusive window start (UTC).
238263
:param end: Inclusive window end (UTC).
264+
:param match_all_authors: When ``True``, accept any author and ignore ``authors_lower``.
239265
:returns: One :class:`Classification` per verdict, or ``[]`` when the comment is skipped.
240266
"""
241267
body = getattr(obj, "body", None)
242268
login = _login(obj)
243269
when = getattr(obj, "created_at", None)
244270
if not body or login is None or when is None:
245271
return []
246-
if login.lower() not in authors_lower:
272+
if not match_all_authors and login.lower() not in authors_lower:
247273
return []
248274
timestamp = _as_utc(when)
249275
if not (start <= timestamp <= end):

metricsai/tests/test_github_source.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,25 @@ def test_classify_classifier_skips_wrong_label_author_window() -> None:
356356
assert github._classify_classifier(late, **gate) == []
357357

358358

359+
def test_classify_classifier_match_all_authors_accepts_any_author() -> None:
360+
# A classifier comment posted under a developer's identity (Path A) still counts.
361+
obj = _classifier_obj("someone-else", _classifier_body("APPLICATION_BUG"), _IN_WINDOW, "x")
362+
out = github._classify_classifier(
363+
obj, authors_lower={_CLASSIFIER}, start=_START, end=_END, match_all_authors=True
364+
)
365+
assert [c.verdict for c in out] == ["APPLICATION_BUG"]
366+
367+
368+
def test_classify_classifier_match_all_authors_still_enforces_window_and_label() -> None:
369+
# The author gate is lifted, but the window and label gates still apply.
370+
gate = {"authors_lower": {_CLASSIFIER}, "start": _START, "end": _END, "match_all_authors": True}
371+
when = datetime(2026, 6, 30, tzinfo=UTC)
372+
late = _classifier_obj("anyone", _classifier_body("TEST_BUG"), when, "x")
373+
assert github._classify_classifier(late, **gate) == []
374+
wrong_label = _classifier_obj("anyone", "security: leak", _IN_WINDOW, "x")
375+
assert github._classify_classifier(wrong_label, **gate) == []
376+
377+
359378
def test_fetch_classifier_comments_scans_both_surfaces(monkeypatch) -> None:
360379
issue = _classifier_obj(
361380
_CLASSIFIER,

testing/metrics/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ the body starts with `test-classifier:`. Reaction counts are read from the
6363
inlined `.reactions` summary that both the issue-comments and pull-comments list
6464
endpoints return on each comment — no extra per-comment reactions API call.
6565

66+
Set `TARGET_USER=ANY` to match on the `test-classifier:` label alone, for teams
67+
whose developers post the classifier locally (so the author is the developer, not
68+
a fixed bot). The `metricsai` tool exposes the same via `--all-authors`.
69+
6670
## Running it (TSV fallback — the default)
6771

6872
```bash

0 commit comments

Comments
 (0)