Include co-authors in release helper-generated contributor list (#6772)

This commit is contained in:
Jakub Kuczys
2026-05-22 07:44:44 +02:00
committed by GitHub
parent 1ad5723c0c
commit 0df902ae12
+127 -11
View File
@@ -4,7 +4,12 @@
This script mostly aims to help with the changelog-related tasks but it does also guide you
through the release process steps including running the 'Prepare release' workflow.
"""
from __future__ import annotations
import dataclasses
import enum
import functools
import json
import os
import pydoc
@@ -15,7 +20,7 @@ import time
import urllib.parse
import webbrowser
from collections import defaultdict
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, NamedTuple, Optional, Set
import click
import requests
@@ -83,6 +88,15 @@ query getMilestoneContributors(
}
}
}
mergeCommit {
authors(first: 100) {
nodes {
user {
login
}
}
}
}
}
pageInfo {
endCursor
@@ -228,9 +242,32 @@ def print_markdown(text: str) -> None:
rich.print(Markdown(text))
def cli_link(text: str, url: str) -> str:
return f"\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\"
def linkify_users(users: List[str], *, version: str = "") -> List[str]:
base_url = f"{GH_URL}/pulls?q="
if version:
base_url += f"milestone:{version}+"
return [
cli_link(login, f"{base_url}involves:{login}") for login in sorted(users, key=str.lower)
]
def linkify_issue_refs_cli(text: str) -> str:
return LINKIFY_ISSUE_REFS_RE.sub(
"\x1b]8;;" rf"{GH_URL}/issues/\1" "\x1b\\\\" r"\g<0>" "\x1b]8;;\x1b\\\\",
# OSC 8 - open hyperlink with no params
"\x1b]8;;"
# URI
# `\1` is substituted with the issue number (e.g. "123")
rf"{GH_URL}/issues/\1"
# ST (string terminator)
"\x1b\\\\"
# hyperlink text (`\g<0>` substituted with "#123")
r"\g<0>"
# OSC 8 - close hyperlink
"\x1b]8;;\x1b\\\\",
text,
)
@@ -965,18 +1002,45 @@ def cli_contributors(version: str, *, show_not_merged: bool = False) -> None:
def get_contributors(version: str, *, show_not_merged: bool = False) -> None:
print(*_get_contributors(version, show_not_merged=show_not_merged))
contribs = _get_contributors(version, show_not_merged=show_not_merged)
warning_threshold = 4
for pr_number, pr_contribs in contribs.pull_requests.items():
if len(pr_contribs.authors) < warning_threshold:
continue
authors = []
for author in pr_contribs.authors:
if author in contribs.reviewers:
continue
for pr_info in contribs.authors[author]:
nested_pr_contribs = contribs.pull_requests[pr_info.number]
if len(nested_pr_contribs.authors) < warning_threshold:
break
else:
authors.append(author)
if authors:
linkified_authors = ", ".join(linkify_users(authors, version=version))
print(
linkify_issue_refs_cli(
f"WARNING: Found over {warning_threshold} authors for PR #{pr_number},"
f" double check that the following contributed to this release:\n"
f"{linkified_authors}\n"
)
)
print("---\n")
print(*linkify_users(contribs.combined_contributors, version=version))
print("\n---")
def _get_contributors(version: str, *, show_not_merged: bool = False) -> List[str]:
def _get_contributors(version: str, *, show_not_merged: bool = False) -> Contributors:
after = None
has_next_page = True
authors: Dict[str, List[Tuple[int, str]]] = {}
reviewers: Dict[str, List[Tuple[int, str]]] = {}
authors: Dict[str, List[PullRequest]] = {}
reviewers: Dict[str, List[PullRequest]] = {}
token = get_github_token()
states = ["MERGED"]
if show_not_merged:
states.append("OPEN")
pr_contribs: Dict[int, PullRequestContributors] = {}
while has_next_page:
resp = requests.post(
"https://api.github.com/graphql",
@@ -998,19 +1062,71 @@ def _get_contributors(version: str, *, show_not_merged: bool = False) -> List[st
pull_requests = milestone_data["pullRequests"]
nodes = pull_requests["nodes"]
for pr_node in nodes:
pr_info = (pr_node["number"], pr_node["title"])
pr_author = pr_node["author"]["login"]
authors.setdefault(pr_author, []).append(pr_info)
pr_info = PullRequest(pr_node["number"], pr_node["title"])
reviews = pr_node["latestOpinionatedReviews"]["nodes"]
pr_reviewers = set()
for review_node in reviews:
review_author = review_node["author"]["login"]
reviewers.setdefault(review_author, []).append(pr_info)
if not review_author.endswith("[bot]"):
reviewers.setdefault(review_author, []).append(pr_info)
pr_reviewers.add(review_author)
merge_commit = pr_node["mergeCommit"]
author_logins = set()
if pr_node["author"] is not None:
author_logins.add(pr_node["author"]["login"])
if merge_commit is not None:
author_logins.update(
author_node["user"]["login"]
for author_node in merge_commit["authors"]["nodes"]
if author_node["user"] is not None
)
pr_authors = set()
for login in author_logins:
if not login.endswith("[bot]"):
authors.setdefault(login, []).append(pr_info)
pr_authors.add(login)
pr_contribs[pr_info.number] = PullRequestContributors(
pr_info.number, pr_authors, pr_reviewers
)
page_info = pull_requests["pageInfo"]
after = page_info["endCursor"]
has_next_page = page_info["hasNextPage"]
return sorted(authors.keys() | reviewers.keys(), key=lambda t: t[0].lower())
return Contributors(authors, reviewers, pr_contribs)
class PullRequest(NamedTuple):
number: int
title: str
@dataclasses.dataclass
class PullRequestContributors:
number: int
# list of logins
authors: Set[str]
reviewers: Set[str]
@functools.cached_property
def combined_contributors(self):
return sorted(self.authors | self.reviewers, key=str.lower)
@dataclasses.dataclass
class Contributors:
# login -> PullRequest
authors: Dict[str, List[PullRequest]]
reviewers: Dict[str, List[PullRequest]]
# PR number -> PullRequestContributors
pull_requests: Dict[int, PullRequestContributors]
@functools.cached_property
def combined_contributors(self):
return sorted(self.authors.keys() | self.reviewers.keys(), key=str.lower)
if __name__ == "__main__":