strawberry-autopub-plugins 0.1.0__tar.gz
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.
- strawberry_autopub_plugins-0.1.0/.gitignore +4 -0
- strawberry_autopub_plugins-0.1.0/PKG-INFO +179 -0
- strawberry_autopub_plugins-0.1.0/README.md +169 -0
- strawberry_autopub_plugins-0.1.0/pyproject.toml +28 -0
- strawberry_autopub_plugins-0.1.0/src/strawberry_autopub_plugins/__init__.py +6 -0
- strawberry_autopub_plugins-0.1.0/src/strawberry_autopub_plugins/invite_contributors.py +213 -0
- strawberry_autopub_plugins-0.1.0/src/strawberry_autopub_plugins/typefully.py +214 -0
- strawberry_autopub_plugins-0.1.0/tests/test_invite_contributors.py +146 -0
- strawberry_autopub_plugins-0.1.0/tests/test_typefully.py +348 -0
- strawberry_autopub_plugins-0.1.0/uv.lock +855 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strawberry-autopub-plugins
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AutoPub plugins maintained by Strawberry GraphQL
|
|
5
|
+
Author: Strawberry GraphQL
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: autopub<2.0.0,>=1.0.0a58
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# strawberry-autopub-plugins
|
|
12
|
+
|
|
13
|
+
AutoPub plugins maintained by Strawberry GraphQL.
|
|
14
|
+
|
|
15
|
+
## Included plugins
|
|
16
|
+
|
|
17
|
+
- `InviteContributorsPlugin` (`strawberry_autopub_plugins.invite_contributors:InviteContributorsPlugin`)
|
|
18
|
+
- Invites pull request contributors to a GitHub organization after `autopub publish`.
|
|
19
|
+
- Can also add invited users to a GitHub team.
|
|
20
|
+
- `TypefullyPlugin` (`strawberry_autopub_plugins.typefully:TypefullyPlugin`)
|
|
21
|
+
- Creates Typefully drafts or scheduled posts for release announcements.
|
|
22
|
+
- Supports per-platform templates for `x`, `linkedin`, `threads`, `bluesky`, and `mastodon`.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install strawberry-autopub-plugins
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Add one or more plugin paths to your AutoPub config:
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[tool.autopub]
|
|
36
|
+
plugins = [
|
|
37
|
+
"poetry",
|
|
38
|
+
"github",
|
|
39
|
+
"strawberry_autopub_plugins.invite_contributors:InviteContributorsPlugin",
|
|
40
|
+
"strawberry_autopub_plugins.typefully:TypefullyPlugin",
|
|
41
|
+
]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Plugin config is keyed by each plugin's `id`:
|
|
45
|
+
|
|
46
|
+
- `invite_contributors`
|
|
47
|
+
- `typefully`
|
|
48
|
+
|
|
49
|
+
## InviteContributorsPlugin
|
|
50
|
+
|
|
51
|
+
Plugin path:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
strawberry_autopub_plugins.invite_contributors:InviteContributorsPlugin
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Required environment variables:
|
|
58
|
+
|
|
59
|
+
- `GITHUB_TOKEN`
|
|
60
|
+
- `GITHUB_REPOSITORY`
|
|
61
|
+
|
|
62
|
+
Optional environment variables:
|
|
63
|
+
|
|
64
|
+
- `GITHUB_EVENT_PATH`
|
|
65
|
+
|
|
66
|
+
`GITHUB_TOKEN` must be able to invite users to the target organization.
|
|
67
|
+
|
|
68
|
+
Example config:
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[tool.autopub.plugin_config.invite_contributors]
|
|
72
|
+
organization = "strawberry-graphql"
|
|
73
|
+
team-slug = "strawberry-contributors"
|
|
74
|
+
role = "direct_member"
|
|
75
|
+
skip-bots = true
|
|
76
|
+
include-co-authors = true
|
|
77
|
+
exclude-users = ["renovate[bot]"]
|
|
78
|
+
dry-run = false
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
|
|
83
|
+
- `organization`: Target GitHub organization. If omitted, the plugin falls back to the repository organization.
|
|
84
|
+
- `team-slug`: Optional team slug to add invited contributors to.
|
|
85
|
+
- `role`: One of `direct_member`, `admin`, or `billing_manager`. Default: `direct_member`.
|
|
86
|
+
- `skip-bots`: Skip logins ending in `[bot]`. Default: `true`.
|
|
87
|
+
- `include-co-authors`: Include `Co-authored-by:` trailers found in commit messages. Default: `true`.
|
|
88
|
+
- `exclude-users`: Additional usernames to skip. Defaults to `dependabot-preview[bot]`, `dependabot-preview`, `dependabot`, and `dependabot[bot]`.
|
|
89
|
+
- `dry-run`: Print which users would be invited without sending invitations. Default: `false`.
|
|
90
|
+
|
|
91
|
+
## TypefullyPlugin
|
|
92
|
+
|
|
93
|
+
Plugin path:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
strawberry_autopub_plugins.typefully:TypefullyPlugin
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Required environment variables:
|
|
100
|
+
|
|
101
|
+
- `TYPEFULLY_API_KEY`
|
|
102
|
+
|
|
103
|
+
Optional environment variables:
|
|
104
|
+
|
|
105
|
+
- `TYPEFULLY_SOCIAL_SET_ID`
|
|
106
|
+
|
|
107
|
+
You can provide the social set ID either through `social-set-id` in config or `TYPEFULLY_SOCIAL_SET_ID`.
|
|
108
|
+
|
|
109
|
+
Example config:
|
|
110
|
+
|
|
111
|
+
```toml
|
|
112
|
+
[tool.autopub.plugin_config.typefully]
|
|
113
|
+
social-set-id = "abc-123"
|
|
114
|
+
platforms = ["x", "linkedin", "bluesky"]
|
|
115
|
+
project-name = "Strawberry"
|
|
116
|
+
message-template = "{project_name} {version} has been released!\n\n{release_notes}"
|
|
117
|
+
publish-mode = "draft"
|
|
118
|
+
tags = ["release", "python"]
|
|
119
|
+
max-length = 280
|
|
120
|
+
truncation-suffix = "..."
|
|
121
|
+
dry-run = false
|
|
122
|
+
|
|
123
|
+
[tool.autopub.plugin_config.typefully.platform-templates]
|
|
124
|
+
x = "{project_name} {version} is out now.\n\n{release_notes}"
|
|
125
|
+
linkedin = "{project_name} {version} has been released.\n\n{release_notes}"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Options:
|
|
129
|
+
|
|
130
|
+
- `social-set-id`: Typefully social set to post into. Required unless `TYPEFULLY_SOCIAL_SET_ID` is set.
|
|
131
|
+
- `platforms`: Platforms to enable. Supported values: `x`, `linkedin`, `threads`, `bluesky`, `mastodon`. Default: `["x"]`.
|
|
132
|
+
- `message-template`: Default template for all platforms. Default: `{project_name} {version} has been released!\n\n{release_notes}`.
|
|
133
|
+
- `platform-templates`: Per-platform template overrides.
|
|
134
|
+
- `project-name`: Value exposed to templates as `{project_name}`.
|
|
135
|
+
- `publish-mode`: One of `draft`, `now`, `next-free-slot`, or `scheduled`. Default: `draft`.
|
|
136
|
+
- `publish-at`: Required when `publish-mode = "scheduled"`.
|
|
137
|
+
- `tags`: Optional Typefully tags to attach to the draft.
|
|
138
|
+
- `max-length`: Maximum post length before truncation. Default: `280`.
|
|
139
|
+
- `truncation-suffix`: Suffix appended after truncation. Default: `...`.
|
|
140
|
+
- `dry-run`: Print the request body without calling the Typefully API. Default: `false`.
|
|
141
|
+
|
|
142
|
+
Template variables:
|
|
143
|
+
|
|
144
|
+
- `{project_name}`
|
|
145
|
+
- `{version}`
|
|
146
|
+
- `{release_type}`
|
|
147
|
+
- `{release_notes}`
|
|
148
|
+
- `{previous_version}`
|
|
149
|
+
|
|
150
|
+
Release-specific override from `RELEASE.md` frontmatter:
|
|
151
|
+
|
|
152
|
+
```md
|
|
153
|
+
---
|
|
154
|
+
release type: patch
|
|
155
|
+
social_message: |
|
|
156
|
+
Strawberry {version} is out now.
|
|
157
|
+
|
|
158
|
+
Highlights:
|
|
159
|
+
{release_notes}
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
- Fixed X
|
|
163
|
+
- Added Y
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When `social_message` is present in AutoPub frontmatter, the plugin uses it as the message template for all configured platforms and still expands the same template variables listed above.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv sync
|
|
172
|
+
uv run pytest
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
When changing dependencies, update the lockfile:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
uv lock
|
|
179
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# strawberry-autopub-plugins
|
|
2
|
+
|
|
3
|
+
AutoPub plugins maintained by Strawberry GraphQL.
|
|
4
|
+
|
|
5
|
+
## Included plugins
|
|
6
|
+
|
|
7
|
+
- `InviteContributorsPlugin` (`strawberry_autopub_plugins.invite_contributors:InviteContributorsPlugin`)
|
|
8
|
+
- Invites pull request contributors to a GitHub organization after `autopub publish`.
|
|
9
|
+
- Can also add invited users to a GitHub team.
|
|
10
|
+
- `TypefullyPlugin` (`strawberry_autopub_plugins.typefully:TypefullyPlugin`)
|
|
11
|
+
- Creates Typefully drafts or scheduled posts for release announcements.
|
|
12
|
+
- Supports per-platform templates for `x`, `linkedin`, `threads`, `bluesky`, and `mastodon`.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install strawberry-autopub-plugins
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Add one or more plugin paths to your AutoPub config:
|
|
23
|
+
|
|
24
|
+
```toml
|
|
25
|
+
[tool.autopub]
|
|
26
|
+
plugins = [
|
|
27
|
+
"poetry",
|
|
28
|
+
"github",
|
|
29
|
+
"strawberry_autopub_plugins.invite_contributors:InviteContributorsPlugin",
|
|
30
|
+
"strawberry_autopub_plugins.typefully:TypefullyPlugin",
|
|
31
|
+
]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Plugin config is keyed by each plugin's `id`:
|
|
35
|
+
|
|
36
|
+
- `invite_contributors`
|
|
37
|
+
- `typefully`
|
|
38
|
+
|
|
39
|
+
## InviteContributorsPlugin
|
|
40
|
+
|
|
41
|
+
Plugin path:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
strawberry_autopub_plugins.invite_contributors:InviteContributorsPlugin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Required environment variables:
|
|
48
|
+
|
|
49
|
+
- `GITHUB_TOKEN`
|
|
50
|
+
- `GITHUB_REPOSITORY`
|
|
51
|
+
|
|
52
|
+
Optional environment variables:
|
|
53
|
+
|
|
54
|
+
- `GITHUB_EVENT_PATH`
|
|
55
|
+
|
|
56
|
+
`GITHUB_TOKEN` must be able to invite users to the target organization.
|
|
57
|
+
|
|
58
|
+
Example config:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
[tool.autopub.plugin_config.invite_contributors]
|
|
62
|
+
organization = "strawberry-graphql"
|
|
63
|
+
team-slug = "strawberry-contributors"
|
|
64
|
+
role = "direct_member"
|
|
65
|
+
skip-bots = true
|
|
66
|
+
include-co-authors = true
|
|
67
|
+
exclude-users = ["renovate[bot]"]
|
|
68
|
+
dry-run = false
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
|
|
73
|
+
- `organization`: Target GitHub organization. If omitted, the plugin falls back to the repository organization.
|
|
74
|
+
- `team-slug`: Optional team slug to add invited contributors to.
|
|
75
|
+
- `role`: One of `direct_member`, `admin`, or `billing_manager`. Default: `direct_member`.
|
|
76
|
+
- `skip-bots`: Skip logins ending in `[bot]`. Default: `true`.
|
|
77
|
+
- `include-co-authors`: Include `Co-authored-by:` trailers found in commit messages. Default: `true`.
|
|
78
|
+
- `exclude-users`: Additional usernames to skip. Defaults to `dependabot-preview[bot]`, `dependabot-preview`, `dependabot`, and `dependabot[bot]`.
|
|
79
|
+
- `dry-run`: Print which users would be invited without sending invitations. Default: `false`.
|
|
80
|
+
|
|
81
|
+
## TypefullyPlugin
|
|
82
|
+
|
|
83
|
+
Plugin path:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
strawberry_autopub_plugins.typefully:TypefullyPlugin
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Required environment variables:
|
|
90
|
+
|
|
91
|
+
- `TYPEFULLY_API_KEY`
|
|
92
|
+
|
|
93
|
+
Optional environment variables:
|
|
94
|
+
|
|
95
|
+
- `TYPEFULLY_SOCIAL_SET_ID`
|
|
96
|
+
|
|
97
|
+
You can provide the social set ID either through `social-set-id` in config or `TYPEFULLY_SOCIAL_SET_ID`.
|
|
98
|
+
|
|
99
|
+
Example config:
|
|
100
|
+
|
|
101
|
+
```toml
|
|
102
|
+
[tool.autopub.plugin_config.typefully]
|
|
103
|
+
social-set-id = "abc-123"
|
|
104
|
+
platforms = ["x", "linkedin", "bluesky"]
|
|
105
|
+
project-name = "Strawberry"
|
|
106
|
+
message-template = "{project_name} {version} has been released!\n\n{release_notes}"
|
|
107
|
+
publish-mode = "draft"
|
|
108
|
+
tags = ["release", "python"]
|
|
109
|
+
max-length = 280
|
|
110
|
+
truncation-suffix = "..."
|
|
111
|
+
dry-run = false
|
|
112
|
+
|
|
113
|
+
[tool.autopub.plugin_config.typefully.platform-templates]
|
|
114
|
+
x = "{project_name} {version} is out now.\n\n{release_notes}"
|
|
115
|
+
linkedin = "{project_name} {version} has been released.\n\n{release_notes}"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
|
|
120
|
+
- `social-set-id`: Typefully social set to post into. Required unless `TYPEFULLY_SOCIAL_SET_ID` is set.
|
|
121
|
+
- `platforms`: Platforms to enable. Supported values: `x`, `linkedin`, `threads`, `bluesky`, `mastodon`. Default: `["x"]`.
|
|
122
|
+
- `message-template`: Default template for all platforms. Default: `{project_name} {version} has been released!\n\n{release_notes}`.
|
|
123
|
+
- `platform-templates`: Per-platform template overrides.
|
|
124
|
+
- `project-name`: Value exposed to templates as `{project_name}`.
|
|
125
|
+
- `publish-mode`: One of `draft`, `now`, `next-free-slot`, or `scheduled`. Default: `draft`.
|
|
126
|
+
- `publish-at`: Required when `publish-mode = "scheduled"`.
|
|
127
|
+
- `tags`: Optional Typefully tags to attach to the draft.
|
|
128
|
+
- `max-length`: Maximum post length before truncation. Default: `280`.
|
|
129
|
+
- `truncation-suffix`: Suffix appended after truncation. Default: `...`.
|
|
130
|
+
- `dry-run`: Print the request body without calling the Typefully API. Default: `false`.
|
|
131
|
+
|
|
132
|
+
Template variables:
|
|
133
|
+
|
|
134
|
+
- `{project_name}`
|
|
135
|
+
- `{version}`
|
|
136
|
+
- `{release_type}`
|
|
137
|
+
- `{release_notes}`
|
|
138
|
+
- `{previous_version}`
|
|
139
|
+
|
|
140
|
+
Release-specific override from `RELEASE.md` frontmatter:
|
|
141
|
+
|
|
142
|
+
```md
|
|
143
|
+
---
|
|
144
|
+
release type: patch
|
|
145
|
+
social_message: |
|
|
146
|
+
Strawberry {version} is out now.
|
|
147
|
+
|
|
148
|
+
Highlights:
|
|
149
|
+
{release_notes}
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
- Fixed X
|
|
153
|
+
- Added Y
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
When `social_message` is present in AutoPub frontmatter, the plugin uses it as the message template for all configured platforms and still expands the same template variables listed above.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uv sync
|
|
162
|
+
uv run pytest
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When changing dependencies, update the lockfile:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
uv lock
|
|
169
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strawberry-autopub-plugins"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AutoPub plugins maintained by Strawberry GraphQL"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Strawberry GraphQL" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"autopub>=1.0.0a58,<2.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/strawberry_autopub_plugins"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from github import Github
|
|
9
|
+
from github.GithubException import GithubException
|
|
10
|
+
from github.Organization import Organization
|
|
11
|
+
from github.PullRequest import PullRequest
|
|
12
|
+
from github.Repository import Repository
|
|
13
|
+
from github.Team import Team
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from autopub.exceptions import AutopubException
|
|
17
|
+
from autopub.plugins import AutopubPlugin
|
|
18
|
+
from autopub.types import ReleaseInfo
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
KNOWN_BOT_EXCLUSIONS = [
|
|
22
|
+
"dependabot-preview[bot]",
|
|
23
|
+
"dependabot-preview",
|
|
24
|
+
"dependabot",
|
|
25
|
+
"dependabot[bot]",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InviteContributorsConfig(BaseModel):
|
|
30
|
+
organization: str | None = None
|
|
31
|
+
team_slug: str | None = Field(default=None, validation_alias="team-slug")
|
|
32
|
+
role: Literal["direct_member", "admin", "billing_manager"] = "direct_member"
|
|
33
|
+
skip_bots: bool = Field(default=True, validation_alias="skip-bots")
|
|
34
|
+
include_co_authors: bool = Field(
|
|
35
|
+
default=True,
|
|
36
|
+
validation_alias="include-co-authors",
|
|
37
|
+
)
|
|
38
|
+
exclude_users: list[str] = Field(
|
|
39
|
+
default_factory=lambda: list(KNOWN_BOT_EXCLUSIONS),
|
|
40
|
+
validation_alias="exclude-users",
|
|
41
|
+
)
|
|
42
|
+
dry_run: bool = Field(default=False, validation_alias="dry-run")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InviteContributorsPlugin(AutopubPlugin):
|
|
46
|
+
"""Invite PR contributors to a GitHub organization (and optional team)."""
|
|
47
|
+
|
|
48
|
+
id = "invite_contributors"
|
|
49
|
+
Config = InviteContributorsConfig
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.github_token = os.environ.get("GITHUB_TOKEN")
|
|
53
|
+
self.repository_name = os.environ.get("GITHUB_REPOSITORY")
|
|
54
|
+
|
|
55
|
+
if not self.github_token:
|
|
56
|
+
raise AutopubException("GITHUB_TOKEN environment variable is required")
|
|
57
|
+
|
|
58
|
+
if not self.repository_name:
|
|
59
|
+
raise AutopubException("GITHUB_REPOSITORY environment variable is required")
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def _github(self) -> Github:
|
|
63
|
+
return Github(self.github_token)
|
|
64
|
+
|
|
65
|
+
@cached_property
|
|
66
|
+
def _event_data(self) -> dict | None:
|
|
67
|
+
event_path = os.environ.get("GITHUB_EVENT_PATH")
|
|
68
|
+
|
|
69
|
+
if not event_path:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
with open(event_path) as f:
|
|
73
|
+
return json.load(f)
|
|
74
|
+
|
|
75
|
+
@cached_property
|
|
76
|
+
def repository(self) -> Repository:
|
|
77
|
+
return self._github.get_repo(self.repository_name)
|
|
78
|
+
|
|
79
|
+
@cached_property
|
|
80
|
+
def pull_request(self) -> PullRequest | None:
|
|
81
|
+
pr_number = self._get_pr_number()
|
|
82
|
+
|
|
83
|
+
if pr_number is None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return self.repository.get_pull(pr_number)
|
|
87
|
+
|
|
88
|
+
def _get_pr_number(self) -> int | None:
|
|
89
|
+
if not self._event_data:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
if self._event_data.get("pull_request"):
|
|
93
|
+
return self._event_data["pull_request"]["number"]
|
|
94
|
+
|
|
95
|
+
if self._event_data.get("head_commit"):
|
|
96
|
+
sha = self._event_data["head_commit"]["id"]
|
|
97
|
+
else:
|
|
98
|
+
commits = self._event_data.get("commits", [])
|
|
99
|
+
if not commits:
|
|
100
|
+
return None
|
|
101
|
+
sha = commits[0]["id"]
|
|
102
|
+
|
|
103
|
+
commit = self.repository.get_commit(sha)
|
|
104
|
+
pulls = commit.get_pulls()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
return pulls[0].number
|
|
108
|
+
except IndexError:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _get_pr_contributors(self, pr: PullRequest) -> set[str]:
|
|
112
|
+
contributors: set[str] = {pr.user.login}
|
|
113
|
+
|
|
114
|
+
for commit in pr.get_commits():
|
|
115
|
+
author = getattr(commit, "author", None)
|
|
116
|
+
if author and getattr(author, "login", None):
|
|
117
|
+
contributors.add(author.login)
|
|
118
|
+
|
|
119
|
+
if self.config.include_co_authors:
|
|
120
|
+
for line in commit.commit.message.splitlines():
|
|
121
|
+
if not line.startswith("Co-authored-by:"):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
trailer_value = line.split(":", 1)[1].strip()
|
|
125
|
+
login = trailer_value.split(" ", 1)[0].lstrip("@")
|
|
126
|
+
|
|
127
|
+
if login:
|
|
128
|
+
contributors.add(login)
|
|
129
|
+
|
|
130
|
+
return contributors
|
|
131
|
+
|
|
132
|
+
def _filter_contributors(self, contributors: set[str]) -> list[str]:
|
|
133
|
+
excluded_users = set(self.config.exclude_users)
|
|
134
|
+
filtered = []
|
|
135
|
+
|
|
136
|
+
for login in sorted(contributors):
|
|
137
|
+
if login in excluded_users:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if self.config.skip_bots and login.endswith("[bot]"):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
filtered.append(login)
|
|
144
|
+
|
|
145
|
+
return filtered
|
|
146
|
+
|
|
147
|
+
def _resolve_organization(self) -> Organization:
|
|
148
|
+
if self.config.organization:
|
|
149
|
+
return self._github.get_organization(self.config.organization)
|
|
150
|
+
|
|
151
|
+
if self.repository.organization:
|
|
152
|
+
return self._github.get_organization(self.repository.organization.login)
|
|
153
|
+
|
|
154
|
+
raise AutopubException(
|
|
155
|
+
"No organization configured. Set tool.autopub.plugin_config"
|
|
156
|
+
".invite_contributors.organization"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _resolve_team(self, organization: Organization) -> Team | None:
|
|
160
|
+
if not self.config.team_slug:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
return organization.get_team_by_slug(self.config.team_slug)
|
|
164
|
+
|
|
165
|
+
def _invite_login(self, organization: Organization, team: Team | None, login: str) -> None:
|
|
166
|
+
user = self._github.get_user(login)
|
|
167
|
+
|
|
168
|
+
invite_kwargs: dict[str, object] = {
|
|
169
|
+
"user": user,
|
|
170
|
+
"role": self.config.role,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if team is not None:
|
|
174
|
+
invite_kwargs["teams"] = [team]
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
organization.invite_user(**invite_kwargs)
|
|
178
|
+
except GithubException as exc:
|
|
179
|
+
# 422 usually means already invited or already a member.
|
|
180
|
+
if exc.status == 422:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
message = str(exc)
|
|
184
|
+
if isinstance(exc.data, dict):
|
|
185
|
+
message = exc.data.get("message", message)
|
|
186
|
+
|
|
187
|
+
raise AutopubException(f"Failed to invite @{login}: {message}") from exc
|
|
188
|
+
|
|
189
|
+
def post_publish(self, release_info: ReleaseInfo) -> None:
|
|
190
|
+
del release_info
|
|
191
|
+
|
|
192
|
+
pr = self.pull_request
|
|
193
|
+
if pr is None:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
contributors = self._get_pr_contributors(pr)
|
|
197
|
+
contributors_to_invite = self._filter_contributors(contributors)
|
|
198
|
+
|
|
199
|
+
if not contributors_to_invite:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
organization = self._resolve_organization()
|
|
203
|
+
team = self._resolve_team(organization)
|
|
204
|
+
|
|
205
|
+
for login in contributors_to_invite:
|
|
206
|
+
if self.config.dry_run:
|
|
207
|
+
print(f"[invite_contributors] would invite @{login}")
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
self._invite_login(organization, team, login)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = ["InviteContributorsPlugin"]
|