diff --git a/README.md b/README.md index 7ca60b55..45aadc0b 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,7 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - [`GH102`](https://learn.scientific-python.org/development/guides/gha-basic#GH102): Auto-cancel on repeated PRs - [`GH103`](https://learn.scientific-python.org/development/guides/gha-basic#GH103): At least one workflow with manual dispatch trigger - [`GH104`](https://learn.scientific-python.org/development/guides/gha-wheels#GH104): Use unique names for upload-artifact +- [`GH105`](https://learn.scientific-python.org/development/guides/gha-basic#GH105): Use Trusted Publishing instead of token-based publishing on PyPI - [`GH200`](https://learn.scientific-python.org/development/guides/gha-basic#GH200): Maintained by Dependabot - [`GH210`](https://learn.scientific-python.org/development/guides/gha-basic#GH210): Maintains the GitHub action versions with Dependabot - [`GH211`](https://learn.scientific-python.org/development/guides/gha-basic#GH211): Do not pin core actions as major versions diff --git a/docs/pages/guides/gha_basic.md b/docs/pages/guides/gha_basic.md index 183ff6db..cbdeb9c2 100644 --- a/docs/pages/guides/gha_basic.md +++ b/docs/pages/guides/gha_basic.md @@ -306,7 +306,8 @@ like the official actions and most other actions, but instead have `release/vX` branches that you can use. - [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish): - Publish Python packages to PyPI. Supports trusted publisher deployment. + Publish Python packages to PyPI. Prefer Trusted Publishing over token-based + uploads. - [re-actors/alls-green](https://github.com/re-actors/alls-green): Tooling to check to see if all jobs passed (supports allowed failures, too). diff --git a/docs/pages/guides/gha_pure.md b/docs/pages/guides/gha_pure.md index e5318427..37d72572 100644 --- a/docs/pages/guides/gha_pure.md +++ b/docs/pages/guides/gha_pure.md @@ -131,9 +131,10 @@ later in the upload action for the release job, as well). > The artifact it produces is named `Packages`, so that's what you need to use > later to publish. This will be used instead of the manual steps below. -And then, you need a release job: +And then, you need a release job. Trusted Publishing is more secure and +recommended {% rr GH105 %}: -{% tabs %} {% tab oidc Trusted Publishing %} +{% tabs %} {% tab oidc Trusted Publishing (recommended) %} {% raw %} @@ -194,10 +195,10 @@ publish: {% endraw %} -When you make a GitHub release in the web UI, we publish to PyPI. You'll need to -go to PyPI, generate a token for your user, and put it into `pypi_password` on -your repo's secrets page. Once you have a project, you should delete your -user-scoped token and generate a new project-scoped token. +If you cannot use Trusted Publishing, this publishes to PyPI with a token. +You'll need to go to PyPI, generate a token for your user, and put it into +`pypi_password` on your repo's secrets page. Once you have a project, you should +delete your user-scoped token and generate a new project-scoped token. {% endtab %} {% endtabs %} @@ -208,7 +209,7 @@ This can be used on almost any package with a standard exactly how to build your package, hence all packages build exactly via the same interface: -{% tabbodies %} {% tab oidc Trusted Publishing %} +{% tabbodies %} {% tab oidc Trusted Publishing (recommended) %} {% raw %} @@ -306,6 +307,11 @@ jobs: {% endraw %} +If you cannot use Trusted Publishing, this publishes to PyPI with a token. +You'll need to go to PyPI, generate a token for your user, and put it into +`pypi_password` on your repo's secrets page. Once you have a project, you should +delete your user-scoped token and generate a new project-scoped token. + {% endtab %} {% endtabbodies %} {% enddetails %} diff --git a/docs/pages/guides/gha_wheels.md b/docs/pages/guides/gha_wheels.md index bc1b2284..a46c138a 100644 --- a/docs/pages/guides/gha_wheels.md +++ b/docs/pages/guides/gha_wheels.md @@ -168,7 +168,9 @@ You can skip specifying the `build[uv]` build-frontend option and pre-installing ## Publishing -{% tabs %} {% tab oidc Trusted Publishing %} +Trusted Publishing is more secure and recommended {% rr GH105 %}: + +{% tabs %} {% tab oidc Trusted Publishing (recommended) %} {% raw %} @@ -232,10 +234,10 @@ upload_all: {% endraw %} -When you make a GitHub release in the web UI, we publish to PyPI. You'll need to -go to PyPI, generate a token for your user, and put it into `pypi_password` on -your repo's secrets page. Once you have a project, you should delete your -user-scoped token and generate a new project-scoped token. +If you cannot use Trusted Publishing, this publishes to PyPI with a token. +You'll need to go to PyPI, generate a token for your user, and put it into +`pypi_password` on your repo's secrets page. Once you have a project, you should +delete your user-scoped token and generate a new project-scoped token. {% endtab %} {% endtabs %} diff --git a/src/sp_repo_review/checks/github.py b/src/sp_repo_review/checks/github.py index f6bc481d..49f5f371 100644 --- a/src/sp_repo_review/checks/github.py +++ b/src/sp_repo_review/checks/github.py @@ -165,6 +165,31 @@ def check(workflows: dict[str, Any]) -> str: return "" +class GH105(GitHub): + "Use Trusted Publishing instead of token-based publishing on PyPI" + + requires = {"GH100"} + url = mk_url("gha-basic") + + @staticmethod + def check(workflows: dict[str, Any]) -> str: + errors = [] + for wname, workflow in workflows.items(): + for jname, job in workflow.get("jobs", {}).items(): + for step in job.get("steps", []): + uses = step.get("uses", "") + publish_with = step.get("with", {}) + if ( + uses.startswith("pypa/gh-action-pypi-publish") + and "password" in publish_with + ): + errors.append( + f"* Token-based publishing detected in `{wname}.yml:{jname}`. Trusted Publishing is recommended." + ) + continue + return "\n".join(errors) + + class GH200(GitHub): "Maintained by Dependabot" diff --git a/tests/test_github.py b/tests/test_github.py new file mode 100644 index 00000000..4ac3e33b --- /dev/null +++ b/tests/test_github.py @@ -0,0 +1,32 @@ +import yaml +from repo_review.testing import compute_check + + +def test_gh105_trusted_publishing() -> None: + workflows = yaml.safe_load( + """ + cd: + jobs: + publish: + steps: + - uses: pypa/gh-action-pypi-publish@release/v1 + """ + ) + assert compute_check("GH105", workflows=workflows).result + + +def test_gh105_token_based_upload() -> None: + workflows = yaml.safe_load( + """ + cd: + jobs: + publish: + steps: + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.pypi_password }} + """ + ) + res = compute_check("GH105", workflows=workflows) + assert not res.result + assert "Token-based publishing" in res.err_msg