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
+126 -10
View File
@@ -4,7 +4,12 @@
This script mostly aims to help with the changelog-related tasks but it does also guide you 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. through the release process steps including running the 'Prepare release' workflow.
""" """
from __future__ import annotations
import dataclasses
import enum import enum
import functools
import json import json
import os import os
import pydoc import pydoc
@@ -15,7 +20,7 @@ import time
import urllib.parse import urllib.parse
import webbrowser import webbrowser
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Optional, Tuple from typing import Dict, List, NamedTuple, Optional, Set
import click import click
import requests import requests
@@ -83,6 +88,15 @@ query getMilestoneContributors(
} }
} }
} }
mergeCommit {
authors(first: 100) {
nodes {
user {
login
}
}
}
}
} }
pageInfo { pageInfo {
endCursor endCursor
@@ -228,9 +242,32 @@ def print_markdown(text: str) -> None:
rich.print(Markdown(text)) 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: def linkify_issue_refs_cli(text: str) -> str:
return LINKIFY_ISSUE_REFS_RE.sub( 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, 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: 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 after = None
has_next_page = True has_next_page = True
authors: Dict[str, List[Tuple[int, str]]] = {} authors: Dict[str, List[PullRequest]] = {}
reviewers: Dict[str, List[Tuple[int, str]]] = {} reviewers: Dict[str, List[PullRequest]] = {}
token = get_github_token() token = get_github_token()
states = ["MERGED"] states = ["MERGED"]
if show_not_merged: if show_not_merged:
states.append("OPEN") states.append("OPEN")
pr_contribs: Dict[int, PullRequestContributors] = {}
while has_next_page: while has_next_page:
resp = requests.post( resp = requests.post(
"https://api.github.com/graphql", "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"] pull_requests = milestone_data["pullRequests"]
nodes = pull_requests["nodes"] nodes = pull_requests["nodes"]
for pr_node in nodes: for pr_node in nodes:
pr_info = (pr_node["number"], pr_node["title"]) pr_info = PullRequest(pr_node["number"], pr_node["title"])
pr_author = pr_node["author"]["login"]
authors.setdefault(pr_author, []).append(pr_info)
reviews = pr_node["latestOpinionatedReviews"]["nodes"] reviews = pr_node["latestOpinionatedReviews"]["nodes"]
pr_reviewers = set()
for review_node in reviews: for review_node in reviews:
review_author = review_node["author"]["login"] review_author = review_node["author"]["login"]
if not review_author.endswith("[bot]"):
reviewers.setdefault(review_author, []).append(pr_info) 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"] page_info = pull_requests["pageInfo"]
after = page_info["endCursor"] after = page_info["endCursor"]
has_next_page = page_info["hasNextPage"] 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__": if __name__ == "__main__":