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.
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ __pycache__/
4
+ *.pyc
@@ -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,6 @@
1
+ """AutoPub plugins for Strawberry GraphQL."""
2
+
3
+ from strawberry_autopub_plugins.invite_contributors import InviteContributorsPlugin
4
+ from strawberry_autopub_plugins.typefully import TypefullyPlugin
5
+
6
+ __all__ = ["InviteContributorsPlugin", "TypefullyPlugin"]
@@ -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"]