Skip to content

Commit 4937a5b

Browse files
authored
feat: add a check for trusted publishing (#764)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
1 parent 10e253a commit 4937a5b

6 files changed

Lines changed: 80 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1
357357
- [`GH102`](https://learn.scientific-python.org/development/guides/gha-basic#GH102): Auto-cancel on repeated PRs
358358
- [`GH103`](https://learn.scientific-python.org/development/guides/gha-basic#GH103): At least one workflow with manual dispatch trigger
359359
- [`GH104`](https://learn.scientific-python.org/development/guides/gha-wheels#GH104): Use unique names for upload-artifact
360+
- [`GH105`](https://learn.scientific-python.org/development/guides/gha-basic#GH105): Use Trusted Publishing instead of token-based publishing on PyPI
360361
- [`GH200`](https://learn.scientific-python.org/development/guides/gha-basic#GH200): Maintained by Dependabot
361362
- [`GH210`](https://learn.scientific-python.org/development/guides/gha-basic#GH210): Maintains the GitHub action versions with Dependabot
362363
- [`GH211`](https://learn.scientific-python.org/development/guides/gha-basic#GH211): Do not pin core actions as major versions

docs/pages/guides/gha_basic.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,8 @@ like the official actions and most other actions, but instead have `release/vX`
306306
branches that you can use.
307307

308308
- [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish):
309-
Publish Python packages to PyPI. Supports trusted publisher deployment.
309+
Publish Python packages to PyPI. Prefer Trusted Publishing over token-based
310+
uploads.
310311
- [re-actors/alls-green](https://github.com/re-actors/alls-green): Tooling to
311312
check to see if all jobs passed (supports allowed failures, too).
312313

docs/pages/guides/gha_pure.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,10 @@ later in the upload action for the release job, as well).
131131
> The artifact it produces is named `Packages`, so that's what you need to use
132132
> later to publish. This will be used instead of the manual steps below.
133133

134-
And then, you need a release job:
134+
And then, you need a release job. Trusted Publishing is more secure and
135+
recommended {% rr GH105 %}:
135136

136-
{% tabs %} {% tab oidc Trusted Publishing %}
137+
{% tabs %} {% tab oidc Trusted Publishing (recommended) %}
137138

138139
{% raw %}
139140

@@ -194,10 +195,10 @@ publish:
194195

195196
{% endraw %}
196197

197-
When you make a GitHub release in the web UI, we publish to PyPI. You'll need to
198-
go to PyPI, generate a token for your user, and put it into `pypi_password` on
199-
your repo's secrets page. Once you have a project, you should delete your
200-
user-scoped token and generate a new project-scoped token.
198+
If you cannot use Trusted Publishing, this publishes to PyPI with a token.
199+
You'll need to go to PyPI, generate a token for your user, and put it into
200+
`pypi_password` on your repo's secrets page. Once you have a project, you should
201+
delete your user-scoped token and generate a new project-scoped token.
201202

202203
{% endtab %} {% endtabs %}
203204

@@ -208,7 +209,7 @@ This can be used on almost any package with a standard
208209
exactly how to build your package, hence all packages build exactly via the same
209210
interface:
210211

211-
{% tabbodies %} {% tab oidc Trusted Publishing %}
212+
{% tabbodies %} {% tab oidc Trusted Publishing (recommended) %}
212213

213214
{% raw %}
214215

@@ -306,6 +307,11 @@ jobs:
306307

307308
{% endraw %}
308309

310+
If you cannot use Trusted Publishing, this publishes to PyPI with a token.
311+
You'll need to go to PyPI, generate a token for your user, and put it into
312+
`pypi_password` on your repo's secrets page. Once you have a project, you should
313+
delete your user-scoped token and generate a new project-scoped token.
314+
309315
{% endtab %} {% endtabbodies %}
310316

311317
{% enddetails %}

docs/pages/guides/gha_wheels.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ You can skip specifying the `build[uv]` build-frontend option and pre-installing
168168

169169
## Publishing
170170

171-
{% tabs %} {% tab oidc Trusted Publishing %}
171+
Trusted Publishing is more secure and recommended {% rr GH105 %}:
172+
173+
{% tabs %} {% tab oidc Trusted Publishing (recommended) %}
172174

173175
{% raw %}
174176

@@ -232,10 +234,10 @@ upload_all:
232234

233235
{% endraw %}
234236

235-
When you make a GitHub release in the web UI, we publish to PyPI. You'll need to
236-
go to PyPI, generate a token for your user, and put it into `pypi_password` on
237-
your repo's secrets page. Once you have a project, you should delete your
238-
user-scoped token and generate a new project-scoped token.
237+
If you cannot use Trusted Publishing, this publishes to PyPI with a token.
238+
You'll need to go to PyPI, generate a token for your user, and put it into
239+
`pypi_password` on your repo's secrets page. Once you have a project, you should
240+
delete your user-scoped token and generate a new project-scoped token.
239241

240242
{% endtab %} {% endtabs %}
241243

src/sp_repo_review/checks/github.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,31 @@ def check(workflows: dict[str, Any]) -> str:
165165
return ""
166166

167167

168+
class GH105(GitHub):
169+
"Use Trusted Publishing instead of token-based publishing on PyPI"
170+
171+
requires = {"GH100"}
172+
url = mk_url("gha-basic")
173+
174+
@staticmethod
175+
def check(workflows: dict[str, Any]) -> str:
176+
errors = []
177+
for wname, workflow in workflows.items():
178+
for jname, job in workflow.get("jobs", {}).items():
179+
for step in job.get("steps", []):
180+
uses = step.get("uses", "")
181+
publish_with = step.get("with", {})
182+
if (
183+
uses.startswith("pypa/gh-action-pypi-publish")
184+
and "password" in publish_with
185+
):
186+
errors.append(
187+
f"* Token-based publishing detected in `{wname}.yml:{jname}`. Trusted Publishing is recommended."
188+
)
189+
continue
190+
return "\n".join(errors)
191+
192+
168193
class GH200(GitHub):
169194
"Maintained by Dependabot"
170195

tests/test_github.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import yaml
2+
from repo_review.testing import compute_check
3+
4+
5+
def test_gh105_trusted_publishing() -> None:
6+
workflows = yaml.safe_load(
7+
"""
8+
cd:
9+
jobs:
10+
publish:
11+
steps:
12+
- uses: pypa/gh-action-pypi-publish@release/v1
13+
"""
14+
)
15+
assert compute_check("GH105", workflows=workflows).result
16+
17+
18+
def test_gh105_token_based_upload() -> None:
19+
workflows = yaml.safe_load(
20+
"""
21+
cd:
22+
jobs:
23+
publish:
24+
steps:
25+
- uses: pypa/gh-action-pypi-publish@release/v1
26+
with:
27+
password: ${{ secrets.pypi_password }}
28+
"""
29+
)
30+
res = compute_check("GH105", workflows=workflows)
31+
assert not res.result
32+
assert "Token-based publishing" in res.err_msg

0 commit comments

Comments
 (0)