Skip to content

Commit 2144a75

Browse files
traskCopilot
andauthored
Show human authors for Copilot draft pull requests (#484)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fb463d90-5190-4a95-b3d4-aa7e117f2a38
1 parent c9e6a93 commit 2144a75

9 files changed

Lines changed: 306 additions & 52 deletions

File tree

‎.github/scripts/pull-request-dashboard/CONTEXT.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ reads a canonical `PullRequestSource` and produces either `EvaluationSuccess` or
88
`EvaluationFailure`. A success carries typed `DashboardFacts`, the final route,
99
diagnostics, pending actions, and top-level history. A failure carries a failure
1010
route and error, so failure routes cannot be combined with successful results.
11-
Evaluation owns effective-author resolution, activity, discussions,
11+
Evaluation uses the shared effective-author resolver and owns activity, discussions,
1212
classification, lifecycle and routing decisions, reviewer projection, command
1313
acknowledgements, author-action links, nudge episodes, and PR-specific failure
1414
shaping. It has no accepted-state or state-branch side effects.
@@ -21,6 +21,12 @@ reviews, review requests, review threads, checks, and non-blocking failures.
2121
`github_cli.py` remains the transport layer; its response dictionaries do not
2222
cross into evaluation or domain modules.
2323

24+
`pull_request_author.py` resolves Copilot authors to a human assignee or
25+
first-commit committer. Evaluation and draft publication share this policy.
26+
Publication uses assignees from the open-PR listing and fetches only the first
27+
commit when a Copilot draft has no eligible assignee. Drafts remain outside
28+
evaluation and routing.
29+
2430
`dashboard_contracts.py` defines the immutable in-memory boundary. A
2531
`StoredDashboardResult` projects an evaluation success down to the fields that
2632
survive refreshes. `DashboardState` holds those projections and the initial

‎.github/scripts/pull-request-dashboard/github_cli.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ def list_open_prs(repo: str) -> list[dict[str, Any]]:
796796
"number": pull["number"],
797797
"title": pull["title"],
798798
"author": pull.get("user"),
799+
"assignees": pull.get("assignees") or [],
799800
"isDraft": pull.get("draft", False),
800801
"createdAt": pull.get("created_at"),
801802
"url": pull.get("html_url"),

‎.github/scripts/pull-request-dashboard/publish_dashboard.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@
1313
from github_cli import (
1414
detect_repo,
1515
fetch_latest_draft_transitions,
16+
gh_api,
1617
gh_graphql,
1718
list_open_prs,
1819
normalize_repo,
1920
repo_state_key,
2021
run_gh,
2122
)
2223
from dashboard_contracts import StoredDashboardResult
24+
from pull_request_author import effective_author, is_copilot_author
25+
from pull_request_source import normalize_commits, normalize_pull_request
2326
from render import render_pr_tables
2427
from state import (
2528
dashboard_markdown_path,
@@ -178,6 +181,20 @@ def publishable_prs(
178181
]
179182

180183

184+
def add_draft_authors(repo: str, prs: list[dict[str, Any]]) -> None:
185+
for pr in prs:
186+
if not pr.get("isDraft"):
187+
continue
188+
metadata = normalize_pull_request(pr)
189+
author = effective_author(metadata)
190+
if is_copilot_author(author):
191+
commits = normalize_commits(
192+
gh_api(f"/repos/{repo}/pulls/{metadata.number}/commits?per_page=1")
193+
)
194+
author = effective_author(metadata, commits)
195+
pr["effectiveAuthor"] = author
196+
197+
181198
def add_draft_since(repo: str, prs: list[dict[str, Any]]) -> None:
182199
drafts = [pr for pr in prs if pr.get("isDraft")]
183200
if not drafts:
@@ -201,6 +218,7 @@ def render_dashboard_markdown(
201218

202219
prs = list_open_prs(repo)
203220
if not large_repo:
221+
add_draft_authors(repo, prs)
204222
add_draft_since(repo, prs)
205223
open_non_draft_pr_numbers = {p["number"] for p in prs if not p.get("isDraft")}
206224
results = results_from_dashboard_state(dashboard_state, open_non_draft_pr_numbers)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Resolve the human behind a Copilot-authored pull request."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
7+
from pull_request_source import Commit, PullRequestMetadata
8+
from utils import is_unattended_author_login, normalize_author_identity
9+
10+
11+
# GitHub reports Copilot PR authors as `app/copilot-swe-agent` or `Copilot`,
12+
# while REST committers can use the bare `copilot` slug.
13+
_COPILOT_COMMITTER_IDENTITIES = {"copilot"}
14+
_COPILOT_PR_AUTHOR_IDENTITIES = {"copilot-swe-agent", "copilot"}
15+
16+
17+
def is_copilot_author(login: str) -> bool:
18+
return normalize_author_identity(login) in _COPILOT_PR_AUTHOR_IDENTITIES
19+
20+
21+
def _human_author_for_copilot_pr(
22+
pr: PullRequestMetadata,
23+
commits: Sequence[Commit],
24+
) -> str:
25+
for assignee in pr.assignees:
26+
login = assignee.login
27+
if (
28+
login
29+
and not is_copilot_author(login)
30+
and not is_unattended_author_login(login)
31+
):
32+
return login
33+
34+
if not commits:
35+
return ""
36+
committer = commits[0].committer
37+
login = committer.login
38+
if (
39+
not login
40+
or normalize_author_identity(login) in _COPILOT_COMMITTER_IDENTITIES
41+
or is_unattended_author_login(login)
42+
or committer.is_bot
43+
or committer.is_copilot_reviewer
44+
):
45+
return ""
46+
return login
47+
48+
49+
def effective_author(
50+
pr: PullRequestMetadata,
51+
commits: Sequence[Commit] = (),
52+
) -> str:
53+
author = pr.author.login
54+
if is_copilot_author(author):
55+
human_author = _human_author_for_copilot_pr(pr, commits)
56+
if human_author:
57+
return human_author
58+
return author

‎.github/scripts/pull-request-dashboard/pull_request_evaluation.py‎

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
resolve_discussions,
5555
)
5656
from github_cli import TransientGhError
57+
from pull_request_author import effective_author
5758
from pull_request_source import (
5859
Actor,
5960
IssueComment,
@@ -86,15 +87,6 @@
8687
)
8788

8889

89-
# Copilot appears under two slugs: `gh pr view`'s `author` field reports
90-
# `app/copilot-swe-agent`, while the Pulls/commits endpoint's `committer.login`
91-
# field can report the bare `copilot` slug. Either slug can name Copilot as the
92-
# author, so the author set carries both while the committer set carries only
93-
# the bare slug. These sets hold the identities `normalize_author_identity`
94-
# returns, without the `app/` prefix or the `[bot]` suffix. Do not treat either
95-
# slug as the human author behind a Copilot-authored PR.
96-
_COPILOT_COMMITTER_IDENTITIES = {"copilot"}
97-
_COPILOT_PR_AUTHOR_IDENTITIES = {"copilot-swe-agent", "copilot"}
9890
_MAINTENANCE_APP_IDENTITIES = {"dependabot", "otelbot", "renovate"}
9991

10092

@@ -144,45 +136,6 @@ def __post_init__(self) -> None:
144136
raise ValueError("pull request number must be positive")
145137

146138

147-
def _human_author_for_copilot_pr(source: PullRequestSource) -> str:
148-
assignees = [
149-
assignee.login
150-
for assignee in source.pull_request.assignees
151-
]
152-
for login in assignees:
153-
identity = normalize_author_identity(login)
154-
if (
155-
login
156-
and identity not in _COPILOT_PR_AUTHOR_IDENTITIES
157-
and not is_unattended_author_login(login)
158-
):
159-
return login
160-
161-
commits = source.commits
162-
if not commits:
163-
return ""
164-
committer = commits[0].committer
165-
login = committer.login
166-
if (
167-
not login
168-
or normalize_author_identity(login) in _COPILOT_COMMITTER_IDENTITIES
169-
or is_unattended_author_login(login)
170-
or committer.is_bot
171-
or committer.is_copilot_reviewer
172-
):
173-
return ""
174-
return login
175-
176-
177-
def _effective_author(source: PullRequestSource) -> str:
178-
author = source.pull_request.author.login
179-
if normalize_author_identity(author) in _COPILOT_PR_AUTHOR_IDENTITIES:
180-
human_author = _human_author_for_copilot_pr(source)
181-
if human_author:
182-
return human_author
183-
return author
184-
185-
186139
def _compute_facts(
187140
source: PullRequestSource,
188141
author: str,
@@ -472,7 +425,7 @@ async def evaluate_pull_request(
472425
pr_title=pr.title,
473426
pr_url=pr.url,
474427
)
475-
author = _effective_author(pr_source)
428+
author = effective_author(pr_source.pull_request, pr_source.commits)
476429
activity = build_activity_timeline(
477430
ActivityInput(pr_source, author, config.approver_logins)
478431
)

‎.github/scripts/pull-request-dashboard/render.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def render_draft_pr_section(
7474
lines.append("| PR | Author | Draft age |")
7575
lines.append("|---|---|:---:|")
7676
for pr in drafts:
77-
author = actor_login(pr.get("author") or {})
77+
author = pr.get("effectiveAuthor") or actor_login(pr.get("author") or {})
7878
draft_age = activity_age(
7979
parse_ts(pr.get("draftSince") or pr.get("createdAt") or "")
8080
)

‎.github/scripts/pull-request-dashboard/test_github_cli.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def test_list_open_prs_uses_paginated_rest_api(self, gh_api) -> None:
571571
"number": number,
572572
"title": f"PR {number}",
573573
"user": {"login": "author"},
574+
"assignees": [{"login": "human-author"}] if number == 501 else None,
574575
"draft": number == 501,
575576
"created_at": "2026-07-01T00:00:00Z",
576577
"updated_at": "2026-07-17T00:00:00Z",
@@ -594,6 +595,7 @@ def test_list_open_prs_uses_paginated_rest_api(self, gh_api) -> None:
594595
"number": 501,
595596
"title": "PR 501",
596597
"author": {"login": "author"},
598+
"assignees": [{"login": "human-author"}],
597599
"isDraft": True,
598600
"createdAt": "2026-07-01T00:00:00Z",
599601
"url": "https://example.test/pull/501",
@@ -602,6 +604,7 @@ def test_list_open_prs_uses_paginated_rest_api(self, gh_api) -> None:
602604
prs[-1],
603605
)
604606
self.assertEqual([], prs[0]["labels"])
607+
self.assertEqual([], prs[0]["assignees"])
605608
gh_api.assert_called_once_with(
606609
"/repos/open-telemetry/example/pulls?state=open&per_page=100",
607610
paginate=True,

0 commit comments

Comments
 (0)