autopub 1.0.0a31__py3-none-any.whl → 1.0.0a33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autopub/plugins/github.py +358 -0
- {autopub-1.0.0a31.dist-info → autopub-1.0.0a33.dist-info}/METADATA +2 -1
- {autopub-1.0.0a31.dist-info → autopub-1.0.0a33.dist-info}/RECORD +6 -5
- {autopub-1.0.0a31.dist-info → autopub-1.0.0a33.dist-info}/LICENSE +0 -0
- {autopub-1.0.0a31.dist-info → autopub-1.0.0a33.dist-info}/WHEEL +0 -0
- {autopub-1.0.0a31.dist-info → autopub-1.0.0a33.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,358 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import pathlib
|
4
|
+
import textwrap
|
5
|
+
from functools import cached_property
|
6
|
+
from typing import Optional, TypedDict
|
7
|
+
|
8
|
+
from github import Github
|
9
|
+
from github.PullRequest import PullRequest
|
10
|
+
from github.Repository import Repository
|
11
|
+
|
12
|
+
from autopub.exceptions import AutopubException
|
13
|
+
from autopub.plugins import AutopubPlugin
|
14
|
+
from autopub.types import ReleaseInfo
|
15
|
+
|
16
|
+
|
17
|
+
class PRContributors(TypedDict):
|
18
|
+
pr_author: str
|
19
|
+
additional_contributors: set[str]
|
20
|
+
reviewers: set[str]
|
21
|
+
|
22
|
+
|
23
|
+
class Sponsors(TypedDict):
|
24
|
+
sponsors: set[str]
|
25
|
+
private_sponsors: int
|
26
|
+
|
27
|
+
|
28
|
+
class GithubPlugin(AutopubPlugin):
|
29
|
+
# TODO: think about the config
|
30
|
+
def __init__(self) -> None:
|
31
|
+
super().__init__()
|
32
|
+
|
33
|
+
self.github_token = os.environ.get("GITHUB_TOKEN")
|
34
|
+
|
35
|
+
if not self.github_token:
|
36
|
+
raise AutopubException("GITHUB_TOKEN environment variable is required")
|
37
|
+
|
38
|
+
self.repository_name = os.environ.get("GITHUB_REPOSITORY")
|
39
|
+
# TODO: maybe this should be in a config file?
|
40
|
+
self.discussion_category_name = os.environ.get("DISCUSSION_CATEGORY_NAME", "Announcements")
|
41
|
+
|
42
|
+
@cached_property
|
43
|
+
def _github(self) -> Github:
|
44
|
+
return Github(self.github_token)
|
45
|
+
|
46
|
+
@cached_property
|
47
|
+
def _event_data(self) -> Optional[dict]:
|
48
|
+
event_path = os.environ.get("GITHUB_EVENT_PATH")
|
49
|
+
if not event_path:
|
50
|
+
return None
|
51
|
+
|
52
|
+
with open(event_path) as f:
|
53
|
+
return json.load(f)
|
54
|
+
|
55
|
+
@cached_property
|
56
|
+
def repository(self) -> Repository:
|
57
|
+
return self._github.get_repo(self.repository_name)
|
58
|
+
|
59
|
+
@cached_property
|
60
|
+
def pull_request(self) -> Optional[PullRequest]:
|
61
|
+
pr_number = self._get_pr_number()
|
62
|
+
|
63
|
+
if pr_number is None:
|
64
|
+
return None
|
65
|
+
|
66
|
+
return self.repository.get_pull(pr_number)
|
67
|
+
|
68
|
+
def _get_pr_number(self) -> Optional[int]:
|
69
|
+
if not self._event_data:
|
70
|
+
return None
|
71
|
+
|
72
|
+
if self._event_data.get("event_name") in [
|
73
|
+
"pull_request",
|
74
|
+
"pull_request_target",
|
75
|
+
]:
|
76
|
+
return self._event_data["pull_request"]["number"]
|
77
|
+
|
78
|
+
if self._event_data.get("pull_request"):
|
79
|
+
return self._event_data["pull_request"]["number"]
|
80
|
+
|
81
|
+
sha = self._event_data["commits"][0]["id"]
|
82
|
+
|
83
|
+
commit = self.repository.get_commit(sha)
|
84
|
+
|
85
|
+
pulls = commit.get_pulls()
|
86
|
+
|
87
|
+
try:
|
88
|
+
first_pr = pulls[0]
|
89
|
+
except IndexError:
|
90
|
+
return None
|
91
|
+
|
92
|
+
return first_pr.number
|
93
|
+
|
94
|
+
def _update_or_create_comment(
|
95
|
+
self, text: str, marker: str = "<!-- autopub-comment -->"
|
96
|
+
) -> None:
|
97
|
+
"""Update or create a comment on the current PR with the given text."""
|
98
|
+
print(f"Updating or creating comment on PR {self.pull_request} in {self.repository}")
|
99
|
+
|
100
|
+
# Look for existing autopub comment
|
101
|
+
comment_body = f"{marker}\n{text}"
|
102
|
+
|
103
|
+
# Search for existing comment
|
104
|
+
for comment in self.pull_request.get_issue_comments():
|
105
|
+
if marker in comment.body:
|
106
|
+
# Update existing comment
|
107
|
+
comment.edit(comment_body)
|
108
|
+
return
|
109
|
+
|
110
|
+
# Create new comment if none exists
|
111
|
+
self.pull_request.create_issue_comment(comment_body)
|
112
|
+
|
113
|
+
def _get_sponsors(self) -> Sponsors:
|
114
|
+
query_organisation = """
|
115
|
+
query GetSponsors($organization: String!) {
|
116
|
+
organization(login: $organization) {
|
117
|
+
sponsorshipsAsMaintainer(
|
118
|
+
first: 100
|
119
|
+
includePrivate: true
|
120
|
+
activeOnly: true
|
121
|
+
) {
|
122
|
+
nodes {
|
123
|
+
privacyLevel
|
124
|
+
sponsorEntity {
|
125
|
+
__typename
|
126
|
+
... on User {
|
127
|
+
login
|
128
|
+
}
|
129
|
+
... on Organization {
|
130
|
+
login
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
"""
|
138
|
+
|
139
|
+
query_user = """
|
140
|
+
query GetSponsors($user: String!) {
|
141
|
+
user(login: $user) {
|
142
|
+
sponsorshipsAsMaintainer(
|
143
|
+
first: 100
|
144
|
+
includePrivate: true
|
145
|
+
activeOnly: true
|
146
|
+
) {
|
147
|
+
nodes {
|
148
|
+
privacyLevel
|
149
|
+
sponsorEntity {
|
150
|
+
__typename
|
151
|
+
... on User {
|
152
|
+
login
|
153
|
+
}
|
154
|
+
... on Organization {
|
155
|
+
login
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
"""
|
163
|
+
|
164
|
+
# TODO: there might be some permission issues in some cases
|
165
|
+
if self.repository.organization:
|
166
|
+
_, response = self._github.requester.graphql_query(
|
167
|
+
query_organisation, {"organization": self.repository.organization.login}
|
168
|
+
)
|
169
|
+
|
170
|
+
data = response["data"]["organization"]["sponsorshipsAsMaintainer"]["nodes"]
|
171
|
+
else:
|
172
|
+
_, response = self._github.requester.graphql_query(
|
173
|
+
query_user, {"user": self.repository.owner.login}
|
174
|
+
)
|
175
|
+
|
176
|
+
data = response["data"]["user"]["sponsorshipsAsMaintainer"]["nodes"]
|
177
|
+
|
178
|
+
sponsors = set()
|
179
|
+
private_sponsors = 0
|
180
|
+
|
181
|
+
for node in data:
|
182
|
+
if node["privacyLevel"] == "PUBLIC":
|
183
|
+
sponsors.add(node["sponsorEntity"]["login"])
|
184
|
+
else:
|
185
|
+
private_sponsors += 1
|
186
|
+
|
187
|
+
return Sponsors(
|
188
|
+
sponsors=sponsors,
|
189
|
+
private_sponsors=private_sponsors,
|
190
|
+
)
|
191
|
+
|
192
|
+
def _get_discussion_category_id(self) -> str:
|
193
|
+
|
194
|
+
query = """
|
195
|
+
query GetDiscussionCategoryId($owner: String!, $repositoryName: String!) {
|
196
|
+
repository(owner: $owner, name: $repositoryName) {
|
197
|
+
discussionCategories(first:100) {
|
198
|
+
nodes {
|
199
|
+
name
|
200
|
+
id
|
201
|
+
}
|
202
|
+
}
|
203
|
+
}
|
204
|
+
}
|
205
|
+
"""
|
206
|
+
|
207
|
+
_, response = self._github.requester.graphql_query(
|
208
|
+
query,
|
209
|
+
{
|
210
|
+
"owner": self.repository.owner.login,
|
211
|
+
"repositoryName": self.repository.name,
|
212
|
+
},
|
213
|
+
)
|
214
|
+
|
215
|
+
for node in response["data"]["repository"]["discussionCategories"]["nodes"]:
|
216
|
+
if node["name"] == self.discussion_category_name:
|
217
|
+
return node["id"]
|
218
|
+
|
219
|
+
raise AutopubException(f"Discussion category {self.discussion_category_name} not found")
|
220
|
+
|
221
|
+
def _create_discussion(self, release_info: ReleaseInfo) -> None:
|
222
|
+
mutation = """
|
223
|
+
mutation CreateDiscussion($repositoryId: ID!, $categoryId: ID!, $body: String!, $title: String!) {
|
224
|
+
createDiscussion(input: {repositoryId: $repositoryId, categoryId: $categoryId, body: $body, title: $title}) {
|
225
|
+
discussion {
|
226
|
+
id
|
227
|
+
}
|
228
|
+
}
|
229
|
+
}
|
230
|
+
"""
|
231
|
+
|
232
|
+
self._github.requester.graphql_query(
|
233
|
+
mutation,
|
234
|
+
{
|
235
|
+
# TODO: repo.node_id is not yet been published to pypi
|
236
|
+
"repositoryId": self.repository.raw_data["node_id"],
|
237
|
+
"categoryId": self._get_discussion_category_id(),
|
238
|
+
"body": self._get_release_message(release_info),
|
239
|
+
"title": f"Release {release_info.version}",
|
240
|
+
},
|
241
|
+
)
|
242
|
+
|
243
|
+
def _get_pr_contributors(self) -> PRContributors:
|
244
|
+
pr: PullRequest = self.pull_request
|
245
|
+
|
246
|
+
pr_author = pr.user.login
|
247
|
+
pr_contributors = PRContributors(
|
248
|
+
pr_author=pr_author,
|
249
|
+
additional_contributors=set(),
|
250
|
+
reviewers=set(),
|
251
|
+
)
|
252
|
+
|
253
|
+
for commit in pr.get_commits():
|
254
|
+
if commit.author.login != pr_author:
|
255
|
+
pr_contributors["additional_contributors"].add(commit.author.login)
|
256
|
+
|
257
|
+
for commit_message in commit.commit.message.split("\n"):
|
258
|
+
if commit_message.startswith("Co-authored-by:"):
|
259
|
+
author = commit_message.split(":")[1].strip()
|
260
|
+
author_login = author.split(" ")[0]
|
261
|
+
|
262
|
+
if author_login != pr_author:
|
263
|
+
pr_contributors["additional_contributors"].add(author_login)
|
264
|
+
|
265
|
+
for review in pr.get_reviews():
|
266
|
+
if review.user.login != pr_author:
|
267
|
+
pr_contributors["reviewers"].add(review.user.login)
|
268
|
+
|
269
|
+
return pr_contributors
|
270
|
+
|
271
|
+
def on_release_notes_valid(
|
272
|
+
self, release_info: ReleaseInfo
|
273
|
+
) -> None: # pragma: no cover
|
274
|
+
assert self.pull_request is not None
|
275
|
+
|
276
|
+
contributors = self._get_pr_contributors()
|
277
|
+
sponsors = self._get_sponsors()
|
278
|
+
discussion_category_id = self._get_discussion_category_id()
|
279
|
+
|
280
|
+
message = textwrap.dedent(
|
281
|
+
f"""
|
282
|
+
## {release_info.version}
|
283
|
+
|
284
|
+
{release_info.release_notes}
|
285
|
+
|
286
|
+
This release was contributed by {contributors} in #{self.pull_request.number}
|
287
|
+
|
288
|
+
Sponsors: {sponsors}
|
289
|
+
|
290
|
+
Discussion: {discussion_category_id}
|
291
|
+
"""
|
292
|
+
)
|
293
|
+
|
294
|
+
self._update_or_create_comment(message)
|
295
|
+
|
296
|
+
def on_release_notes_invalid(
|
297
|
+
self, exception: AutopubException
|
298
|
+
) -> None: # pragma: no cover
|
299
|
+
# TODO: better message
|
300
|
+
self._update_or_create_comment(str(exception))
|
301
|
+
|
302
|
+
def _get_release_message(self, release_info: ReleaseInfo) -> str:
|
303
|
+
assert self.pull_request is not None
|
304
|
+
|
305
|
+
contributors = self._get_pr_contributors()
|
306
|
+
sponsors = self._get_sponsors()
|
307
|
+
|
308
|
+
message = textwrap.dedent(
|
309
|
+
f"""
|
310
|
+
## {release_info.version}
|
311
|
+
|
312
|
+
{release_info.release_notes}
|
313
|
+
|
314
|
+
This release was contributed by @{contributors['pr_author']} in #{self.pull_request.number}
|
315
|
+
"""
|
316
|
+
)
|
317
|
+
|
318
|
+
if contributors["additional_contributors"]:
|
319
|
+
additional_contributors = [f"@{contributor}" for contributor in contributors["additional_contributors"]]
|
320
|
+
message += f"\n\nAdditional contributors: {', '.join(additional_contributors)}"
|
321
|
+
|
322
|
+
if contributors["reviewers"]:
|
323
|
+
reviewers = [f"@{reviewer}" for reviewer in contributors["reviewers"]]
|
324
|
+
message += f"\n\nReviewers: {', '.join(reviewers)}"
|
325
|
+
|
326
|
+
if sponsors["sponsors"]:
|
327
|
+
sponsors = [f"@{sponsor}" for sponsor in sponsors["sponsors"]]
|
328
|
+
message += f"\n\nThanks to {', '.join(sponsors)}"
|
329
|
+
if sponsors["private_sponsors"]:
|
330
|
+
message += f" and the {sponsors['private_sponsors']} private sponsor(s)"
|
331
|
+
|
332
|
+
message += " for making this release possible ✨"
|
333
|
+
|
334
|
+
return message
|
335
|
+
|
336
|
+
def _create_release(self, release_info: ReleaseInfo) -> None:
|
337
|
+
message = self._get_release_message(release_info)
|
338
|
+
|
339
|
+
release = self.repository.create_git_release(
|
340
|
+
tag=release_info.version,
|
341
|
+
name=release_info.version,
|
342
|
+
message=message,
|
343
|
+
)
|
344
|
+
|
345
|
+
for asset in pathlib.Path("dist").glob("*"):
|
346
|
+
if asset.suffix in [".tar.gz", ".whl"]:
|
347
|
+
release.upload_asset(str(asset))
|
348
|
+
|
349
|
+
def post_publish(self, release_info: ReleaseInfo) -> None:
|
350
|
+
text = f"This PR was published as {release_info.version}"
|
351
|
+
assert self.pull_request is not None
|
352
|
+
|
353
|
+
self._update_or_create_comment(
|
354
|
+
text, marker="<!-- autopub-comment-published -->"
|
355
|
+
)
|
356
|
+
|
357
|
+
self._create_release(release_info)
|
358
|
+
self._create_discussion(release_info)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: autopub
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.0a33
|
4
4
|
Summary: Automatic package release upon pull request merge
|
5
5
|
Home-page: https://github.com/autopub/autopub
|
6
6
|
Author: Justin Mayer
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Provides-Extra: github
|
16
16
|
Requires-Dist: dunamai (>=1.23.0,<2.0.0)
|
17
17
|
Requires-Dist: pydantic (>=2.10.5,<3.0.0)
|
18
|
+
Requires-Dist: pygithub (>=2.5.0,<3.0.0)
|
18
19
|
Requires-Dist: python-frontmatter (>=1.1.0,<2.0.0)
|
19
20
|
Requires-Dist: rich (>=13.9.4,<14.0.0)
|
20
21
|
Requires-Dist: tomlkit (>=0.13.2,<0.14.0)
|
@@ -5,14 +5,15 @@ autopub/plugin_loader.py,sha256=i8nz0cmu5eaSRJ2Hx5KxbA-tOMR2mjcz8fdRg4zkGLE,1813
|
|
5
5
|
autopub/plugins/__init__.py,sha256=57ewn1lhZYKKuVL49l3cgqAN9LQfCRzdsgSZimIAkls,2030
|
6
6
|
autopub/plugins/bump_version.py,sha256=-tTGQR0v8K3Bto1Y8UcmkEs4WnExeF1QyHKHPUKgll8,1565
|
7
7
|
autopub/plugins/git.py,sha256=d0SMLc6hwuk0eymj8aHyu3_cEd-7x4fhkwu35wPPV4k,1054
|
8
|
+
autopub/plugins/github.py,sha256=5oPTaEuIqwVO2syce7ehDq5g6FTC49rWZWxyJHFO4vg,11824
|
8
9
|
autopub/plugins/pdm.py,sha256=Pczye06fKg8_HMJDkEfMXQyvao9rZ7sqzTHFd6lLEpU,532
|
9
10
|
autopub/plugins/poetry.py,sha256=d2LvW9RI7ZB3reBOXbcp1mqWmzQ06Uyg_T-MxTvlSBg,517
|
10
11
|
autopub/plugins/update_changelog.py,sha256=g_6flOP5wocZbjOaYSayWxobL3ld8f0wT78nFtAIkFc,1586
|
11
12
|
autopub/plugins/uv.py,sha256=goo8QxaD3FVJ1c3xOSmN1hikZTCUXN8jWNac1S5uDDY,1089
|
12
13
|
autopub/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
14
|
autopub/types.py,sha256=gY1WR93XZVFS7vf5JMSmL_h5z7zO51-rtmZ6MYsh3so,1043
|
14
|
-
autopub-1.0.
|
15
|
-
autopub-1.0.
|
16
|
-
autopub-1.0.
|
17
|
-
autopub-1.0.
|
18
|
-
autopub-1.0.
|
15
|
+
autopub-1.0.0a33.dist-info/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
16
|
+
autopub-1.0.0a33.dist-info/METADATA,sha256=bPk5ktZXuRBe0fQ9Tlzw4htTm9q14Yjr81nAOp0aAFE,992
|
17
|
+
autopub-1.0.0a33.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
18
|
+
autopub-1.0.0a33.dist-info/entry_points.txt,sha256=oeTav5NgCxif6mcZ_HeVGgGv5LzS4DwdI01nr4bO1IM,43
|
19
|
+
autopub-1.0.0a33.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|