mirror of
https://gitcode.com/gh_mirrors/gh/gh-action-pypi-publish.git
synced 2026-07-07 20:09:27 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1b3402824 | |||
| c5d8ac5008 | |||
| ffa46275f9 | |||
| 9b8e7336db |
+5
-15
@@ -1,16 +1,6 @@
|
||||
---
|
||||
|
||||
custom:
|
||||
- https://www.comebackalive.in.ua/donate
|
||||
- https://github.com/vshymanskyy/StandWithUkraine#for-maintainers-and-authors
|
||||
- https://www.paypal.me/webknjazCZ
|
||||
- https://webknjaz.me
|
||||
|
||||
github:
|
||||
- webknjaz
|
||||
|
||||
github: [webknjaz]
|
||||
patreon: webknjaz
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: webknjaz
|
||||
|
||||
liberapay: webknjaz
|
||||
|
||||
...
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
custom: https://www.paypal.me/webknjazCZ
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
**⚠️ Please do not file public GitHub issues for security
|
||||
vulnerabilities as they are open for everyone to see! ⚠️**
|
||||
|
||||
We encourage responsible disclosure practices for security
|
||||
vulnerabilities.
|
||||
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Always update to the latest version of
|
||||
this Action to keep up with security patches.
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you've found a security-related bug, we
|
||||
prefer that you fill out a [vulnerability report on GitHub]
|
||||
directly.
|
||||
|
||||
[vulnerability report on GitHub]:
|
||||
//github.com/pypa/gh-action-pypi-publish/security/advisories/new
|
||||
|
||||
|
||||
## Don't have a GitHub account?
|
||||
|
||||
Alternatively, drop an email to
|
||||
``wk+gh-action-pypi-publish-security`` at ``sydorenko`` dot
|
||||
``org`` dot ``ua`` instead of filing a ticket or posting to
|
||||
_any_ public groups. We will try to assess the problem in
|
||||
timely manner and disclose it in a responsible way.
|
||||
@@ -0,0 +1,14 @@
|
||||
Security Policy
|
||||
---------------
|
||||
|
||||
Supported Versions
|
||||
==================
|
||||
|
||||
Always update to the latest version of
|
||||
this Action to keep up with security patches.
|
||||
|
||||
Reporting a Vulnerability
|
||||
=========================
|
||||
|
||||
Email to ``wk+gh-action-pypi-publish-security``
|
||||
at ``sydorenko`` dot ``org`` dot ``ua``.
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
|
||||
name: 🧪
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
devpi-password: abcd1234
|
||||
devpi-username: root
|
||||
devpi-port: 3141
|
||||
|
||||
FORCE_COLOR: 1 # Request colored output from CLI tools supporting it
|
||||
MYPY_FORCE_COLOR: 1 # MyPy's color enforcement
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||
PIP_NO_PYTHON_VERSION_WARNING: 1
|
||||
PIP_NO_WARN_SCRIPT_LOCATION: 1
|
||||
PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest`
|
||||
TOX_PARALLEL_NO_SPINNER: 1
|
||||
TOX_TESTENV_PASSENV: >- # Make tox-wrapped tools see color requests
|
||||
FORCE_COLOR
|
||||
MYPY_FORCE_COLOR
|
||||
NO_COLOR
|
||||
PY_COLORS
|
||||
PYTEST_THEME
|
||||
PYTEST_THEME_MODE
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
devpi:
|
||||
image: muccg/devpi
|
||||
env:
|
||||
DEVPI_PASSWORD: ${{ env.devpi-password }}
|
||||
ports:
|
||||
- 3141
|
||||
|
||||
timeout-minutes: 2
|
||||
|
||||
steps:
|
||||
- name: Check out the action locally
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: test
|
||||
- name: Install the packaging-related tools
|
||||
run: python3 -m pip install build twine
|
||||
env:
|
||||
PIP_CONSTRAINT: test/requirements/runtime.txt
|
||||
- name: Create the stub package importable directory
|
||||
run: mkdir -pv src/test_package
|
||||
- name: Populate the stub package `__init__.py`
|
||||
run: echo '__version__ = "0.1"' > src/test_package/__init__.py
|
||||
- name: Populate the stub package `README.md`
|
||||
run: echo "# Test Package" > README.md
|
||||
- name: Populate the stub package `pyproject.toml`
|
||||
run: echo "$CONTENTS" > pyproject.toml
|
||||
env:
|
||||
CONTENTS: |
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools == 65.6.3",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "test-package"
|
||||
version = "0.1"
|
||||
readme = "README.md"
|
||||
- name: Build the stub package sdist and wheel distributions
|
||||
run: python3 -m build
|
||||
- name: Register the stub package in devpi
|
||||
run: twine register dist/*.tar.gz
|
||||
env:
|
||||
TWINE_USERNAME: ${{ env.devpi-username }}
|
||||
TWINE_PASSWORD: ${{ env.devpi-password }}
|
||||
TWINE_REPOSITORY_URL: >-
|
||||
http://localhost:${{
|
||||
job.services.devpi.ports[env.devpi-port]
|
||||
}}/${{
|
||||
env.devpi-username
|
||||
}}/public/
|
||||
- name: ✅ Smoke-test the locally checked out action
|
||||
uses: ./test
|
||||
env:
|
||||
DEBUG: >-
|
||||
true
|
||||
PATH: utter-nonsense
|
||||
with:
|
||||
user: ${{ env.devpi-username }}
|
||||
password: ${{ env.devpi-password }}
|
||||
repository-url: >-
|
||||
http://devpi:${{ env.devpi-port }}/${{ env.devpi-username }}/public/
|
||||
|
||||
...
|
||||
@@ -1,4 +0,0 @@
|
||||
[tool.pip-tools]
|
||||
allow-unsafe = true
|
||||
resolver = "backtracking"
|
||||
strip-extras = true
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
default_language_version: python3.11
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/asottile/add-trailing-comma.git
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
|
||||
- repo: https://github.com/PyCQA/isort.git
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
args:
|
||||
- --honor-noqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks.git
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema.git
|
||||
rev: 0.28.1
|
||||
hooks:
|
||||
- id: check-github-actions
|
||||
- id: check-github-workflows
|
||||
- id: check-jsonschema
|
||||
name: Check GitHub Workflows set timeout-minutes
|
||||
args:
|
||||
- --builtin-schema
|
||||
- github-workflows-require-timeout
|
||||
files: ^\.github/workflows/[^/]+$
|
||||
types:
|
||||
- yaml
|
||||
- id: check-readthedocs
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks.git
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
# Side-effects:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: mixed-line-ending
|
||||
# Non-modifying checks:
|
||||
- id: name-tests-test
|
||||
files: >-
|
||||
^tests/[^_].*\.py$
|
||||
- id: check-added-large-files
|
||||
- id: check-byte-order-marker
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
- id: check-json
|
||||
- id: check-symlinks
|
||||
- id: check-yaml
|
||||
- id: detect-private-key
|
||||
# Heavy checks:
|
||||
- id: check-ast
|
||||
- id: debug-statements
|
||||
language_version: python3
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
hooks:
|
||||
- id: codespell
|
||||
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.35.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
files: \.(yaml|yml)$
|
||||
types:
|
||||
- file
|
||||
- yaml
|
||||
args:
|
||||
- --strict
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8.git
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args:
|
||||
- --ignore
|
||||
# NOTE: WPS326: Found implicit string concatenation
|
||||
# NOTE: WPS332: Found walrus operator
|
||||
- >-
|
||||
D100,
|
||||
D101,
|
||||
D103,
|
||||
D107,
|
||||
E402,
|
||||
E501,
|
||||
WPS102,
|
||||
WPS110,
|
||||
WPS111,
|
||||
WPS305,
|
||||
WPS326,
|
||||
WPS332,
|
||||
WPS347,
|
||||
WPS360,
|
||||
WPS421,
|
||||
WPS422,
|
||||
WPS432,
|
||||
WPS433,
|
||||
WPS437,
|
||||
WPS440,
|
||||
WPS441,
|
||||
WPS453,
|
||||
additional_dependencies:
|
||||
- flake8-2020 ~= 1.7.0
|
||||
- flake8-pytest-style ~= 1.6.0
|
||||
- wemake-python-styleguide ~= 0.19.0
|
||||
language_version: python3.11 # flake8-commas doesn't work w/ Python 3.12
|
||||
|
||||
- repo: https://github.com/PyCQA/pylint.git
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: pylint
|
||||
args:
|
||||
- --disable
|
||||
- >-
|
||||
import-error,
|
||||
invalid-name,
|
||||
line-too-long,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
protected-access,
|
||||
super-init-not-called,
|
||||
unused-argument,
|
||||
wrong-import-position,
|
||||
- --output-format
|
||||
- colorized
|
||||
|
||||
...
|
||||
@@ -1,10 +1,2 @@
|
||||
---
|
||||
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
indentation:
|
||||
level: error
|
||||
indent-sequences: false
|
||||
|
||||
...
|
||||
indentation:
|
||||
indent-sequences: false
|
||||
|
||||
+3
-15
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.9-slim
|
||||
|
||||
LABEL "maintainer" "Sviatoslav Sydorenko <wk+pypa@sydorenko.org.ua>"
|
||||
LABEL "repository" "https://github.com/pypa/gh-action-pypi-publish"
|
||||
@@ -7,26 +7,14 @@ LABEL "homepage" "https://github.com/pypa/gh-action-pypi-publish"
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
ENV PIP_NO_CACHE_DIR 1
|
||||
ENV PIP_ROOT_USER_ACTION ignore
|
||||
|
||||
ENV PATH "/root/.local/bin:${PATH}"
|
||||
ENV PYTHONPATH "/root/.local/lib/python3.12/site-packages"
|
||||
|
||||
COPY requirements requirements
|
||||
RUN \
|
||||
PIP_CONSTRAINT=requirements/runtime-prerequisites.txt \
|
||||
pip install --user --upgrade --no-cache-dir \
|
||||
-r requirements/runtime-prerequisites.in && \
|
||||
PIP_CONSTRAINT=requirements/runtime.txt \
|
||||
pip install --user --upgrade --no-cache-dir --prefer-binary \
|
||||
-r requirements/runtime.in
|
||||
pip install --upgrade --no-cache-dir pip-with-requires-python && \
|
||||
pip install --upgrade --no-cache-dir --prefer-binary twine
|
||||
|
||||
WORKDIR /app
|
||||
COPY LICENSE.md .
|
||||
COPY twine-upload.sh .
|
||||
COPY print-hash.py .
|
||||
COPY oidc-exchange.py .
|
||||
|
||||
RUN chmod +x twine-upload.sh
|
||||
ENTRYPOINT ["/app/twine-upload.sh"]
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
[![SWUbanner]][SWUdocs]
|
||||
|
||||
[![🧪 GitHub Actions CI/CD workflow tests badge]][GHA workflow runs list]
|
||||
[![pre-commit.ci status badge]][pre-commit.ci results page]
|
||||
|
||||
# PyPI publish GitHub Action
|
||||
|
||||
This action allows you to upload your [Python distribution packages]
|
||||
@@ -10,95 +7,59 @@ in the `dist/` directory to PyPI.
|
||||
This text suggests a minimalistic usage overview. For more detailed
|
||||
walkthrough check out the [PyPA guide].
|
||||
|
||||
If you have any feedback regarding specific action versions, please leave
|
||||
comments in the corresponding [per-release announcement discussions].
|
||||
|
||||
|
||||
## 🌇 `master` branch sunset ❗
|
||||
|
||||
The `master` branch version has been sunset. Please, change the GitHub
|
||||
Action version you use from `master` to `release/v1` or use an exact
|
||||
tag, or opt-in to [use a full Git commit SHA] and Dependabot.
|
||||
tag, or a full Git commit SHA.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Trusted publishing
|
||||
To use the action add the following step to your workflow file (e.g.
|
||||
`.github/workflows/main.yml`)
|
||||
|
||||
> [!NOTE]
|
||||
> Trusted publishing cannot be used from within a reusable workflow at this
|
||||
> time. It is recommended to instead create a non-reusable workflow that contains a
|
||||
> job calling your reusable workflow, and then do the trusted publishing step from
|
||||
> a separate job within that non-reusable workflow. Alternatively, you can still
|
||||
> use a username/token inside the reusable workflow.
|
||||
|
||||
> [!NOTE]
|
||||
> Trusted publishing is sometimes referred to by its
|
||||
> underlying technology -- OpenID Connect, or OIDC for short.
|
||||
> If you see references to "OIDC publishing" in the context of PyPI,
|
||||
> this is what they're referring to.
|
||||
|
||||
This example jumps right into the current best practice. If you want to
|
||||
use API tokens directly or a less secure username and password, check out
|
||||
[how to specify username and password].
|
||||
|
||||
This action supports PyPI's [trusted publishing]
|
||||
implementation, which allows authentication to PyPI without a manually
|
||||
configured API token or username/password combination. To perform
|
||||
[trusted publishing] with this action, your project's
|
||||
publisher must already be [configured on PyPI].
|
||||
|
||||
To enter the trusted publishing flow, configure this action's job with the
|
||||
`id-token: write` permission and **without** an explicit username or password:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci-cd.yml
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/<your-pypi-project-name>
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
# retrieve your distributions here
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Pro tip: instead of using branch pointers, like `unstable/v1`, pin versions of
|
||||
> Actions that you use to tagged versions or sha1 commit identifiers.
|
||||
> This will make your workflows more secure and better reproducible, saving you
|
||||
> from sudden and unpleasant surprises.
|
||||
|
||||
Other indices that support trusted publishing can also be used, like TestPyPI:
|
||||
|
||||
```yaml
|
||||
- name: Publish package distributions to TestPyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
```
|
||||
_(don't forget to update the environment name to `testpypi` or similar!)_
|
||||
|
||||
> [!NOTE]
|
||||
> Pro tip: only set the `id-token: write` permission in the job that does
|
||||
> publishing, not globally. Also, try to separate building from publishing
|
||||
> — this makes sure that any scripts maliciously injected into the build
|
||||
> or test environment won't be able to elevate privileges while flying under
|
||||
> the radar.
|
||||
|
||||
A common use case is to upload packages only on a tagged commit, to do so add a
|
||||
filter to the job:
|
||||
|
||||
```yml
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
- name: Publish a Python distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
```
|
||||
|
||||
> **Pro tip**: instead of using branch pointers, like `master`, pin versions of
|
||||
Actions that you use to tagged versions or sha1 commit identifiers. This will
|
||||
make your workflows more secure and better reproducible, saving you from sudden
|
||||
and unpleasant surprises.
|
||||
|
||||
A common use case is to upload packages only on a tagged commit, to do so add a
|
||||
filter to the step:
|
||||
|
||||
|
||||
```yml
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
```
|
||||
|
||||
So the full step would look like:
|
||||
|
||||
|
||||
```yml
|
||||
- name: Publish package
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
```
|
||||
|
||||
The example above uses the new [API token][PyPI API token] feature of
|
||||
PyPI, which is recommended to restrict the access the action has.
|
||||
|
||||
The secret used in `${{ secrets.PYPI_API_TOKEN }}` needs to be created on the
|
||||
settings page of your project on GitHub. See [Creating & using secrets].
|
||||
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -106,8 +67,7 @@ This GitHub Action [has nothing to do with _building package
|
||||
distributions_]. Users are responsible for preparing dists for upload
|
||||
by putting them into the `dist/` folder prior to running this Action.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Since this GitHub Action is docker-based, it can only
|
||||
> **IMPORTANT**: Since this GitHub Action is docker-based, it can only
|
||||
> be used from within GNU/Linux based jobs in GitHub Actions CI/CD
|
||||
> workflows. This is by design and is unlikely to change due to a number
|
||||
> of considerations we rely on.
|
||||
@@ -135,28 +95,26 @@ by putting them into the `dist/` folder prior to running this Action.
|
||||
For best results, figure out what kind of workflow fits your
|
||||
project's specific needs.
|
||||
|
||||
For example, you could implement a parallel job that
|
||||
For example, you could implement a parallel workflow that
|
||||
pushes every commit to TestPyPI or your own index server,
|
||||
like `devpi`. For this, you'd need to (1) specify a custom
|
||||
`repository-url` value and (2) generate a unique version
|
||||
`repository_url` value and (2) generate a unique version
|
||||
number for each upload so that they'd not create a conflict.
|
||||
The latter is possible if you use `setuptools_scm` package but
|
||||
you could also invent your own solution based on the distance
|
||||
to the latest tagged commit.
|
||||
|
||||
You'll need to create another token for a separate host and then [save it as a
|
||||
GitHub repo secret][Creating & using secrets] under an environment used in
|
||||
your job. Though, passing a password would disable the secretless [trusted
|
||||
publishing] so it's better to configure it instead, when publishing to TestPyPI
|
||||
and not something custom.
|
||||
You'll need to create another token for a separate host and then
|
||||
[save it as a GitHub repo secret][Creating & using secrets].
|
||||
|
||||
The action invocation in this case would look like:
|
||||
```yml
|
||||
- name: Publish package to TestPyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
```
|
||||
|
||||
### Customizing target package dists directory
|
||||
@@ -169,7 +127,9 @@ would now look like:
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: custom-dir/
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: custom-dir/
|
||||
```
|
||||
|
||||
### Disabling metadata verification
|
||||
@@ -180,7 +140,7 @@ check with:
|
||||
|
||||
```yml
|
||||
with:
|
||||
verify-metadata: false
|
||||
verify_metadata: false
|
||||
```
|
||||
|
||||
### Tolerating release package file duplicates
|
||||
@@ -190,18 +150,17 @@ may hit race conditions. For example, when publishing from multiple CIs
|
||||
or even having workflows with the same steps triggered within GitHub
|
||||
Actions CI/CD for different events concerning the same high-level act.
|
||||
|
||||
To facilitate this use-case, you may use `skip-existing` (disabled by
|
||||
To facilitate this use-case, you may use `skip_existing` (disabled by
|
||||
default) setting as follows:
|
||||
|
||||
```yml
|
||||
with:
|
||||
skip-existing: true
|
||||
skip_existing: true
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Pro tip: try to avoid enabling this setting where possible. If you
|
||||
> have steps for publishing to both PyPI and TestPyPI, consider only using
|
||||
> it for the latter, having the former fail loudly on duplicates.
|
||||
> **Pro tip**: try to avoid enabling this setting where possible. If you
|
||||
have steps for publishing to both PyPI and TestPyPI, consider only using
|
||||
it for the latter, having the former fail loudly on duplicates.
|
||||
|
||||
### For Debugging
|
||||
|
||||
@@ -219,55 +178,15 @@ It will show SHA256, MD5, BLAKE2-256 values of files to be uploaded.
|
||||
|
||||
```yml
|
||||
with:
|
||||
print-hash: true
|
||||
print_hash: true
|
||||
```
|
||||
|
||||
### Specifying a different username
|
||||
|
||||
The default username value is `__token__`. If you publish to a custom
|
||||
registry that does not provide API tokens, like `devpi`, you may need to
|
||||
specify a custom username and password pair. This is how it's done.
|
||||
|
||||
```yml
|
||||
with:
|
||||
user: guido
|
||||
password: ${{ secrets.DEVPI_PASSWORD }}
|
||||
```
|
||||
|
||||
The secret used in `${{ secrets.DEVPI_PASSWORD }}` needs to be created on the
|
||||
environment page under the settings of your project on GitHub.
|
||||
See [Creating & using secrets].
|
||||
|
||||
In the past, when publishing to PyPI, the most secure way of the access scoping
|
||||
for automatic publishing was to use the [API tokens][PyPI API token] feature of
|
||||
PyPI. One would make it project-scoped and save as an environment-bound secret
|
||||
in their GitHub repository settings, naming it `${{ secrets.PYPI_API_TOKEN }}`,
|
||||
for example. See [Creating & using secrets]. While still secure,
|
||||
[trusted publishing] is now encouraged over API tokens as a best practice
|
||||
on supported platforms (like GitHub).
|
||||
|
||||
## License
|
||||
|
||||
The Dockerfile and associated scripts and documentation in this project
|
||||
are released under the [BSD 3-clause license](LICENSE.md).
|
||||
|
||||
|
||||
[🧪 GitHub Actions CI/CD workflow tests badge]:
|
||||
https://github.com/pypa/gh-action-pypi-publish/actions/workflows/self-smoke-test-action.yml/badge.svg?branch=unstable%2Fv1&event=push
|
||||
[GHA workflow runs list]:
|
||||
https://github.com/pypa/gh-action-pypi-publish/actions/workflows/self-smoke-test-action.yml?query=branch%3Aunstable%2Fv1
|
||||
|
||||
[pre-commit.ci results page]:
|
||||
https://results.pre-commit.ci/latest/github/pypa/gh-action-pypi-publish/unstable/v1
|
||||
[pre-commit.ci status badge]:
|
||||
https://results.pre-commit.ci/badge/github/pypa/gh-action-pypi-publish/unstable/v1.svg
|
||||
|
||||
[use a full Git commit SHA]:
|
||||
https://julienrenaux.fr/2019/12/20/github-actions-security-risk/
|
||||
|
||||
[per-release announcement discussions]:
|
||||
https://github.com/pypa/gh-action-pypi-publish/discussions/categories/announcements
|
||||
|
||||
[Creating & using secrets]:
|
||||
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets
|
||||
[has nothing to do with _building package distributions_]:
|
||||
@@ -276,14 +195,8 @@ https://github.com/pypa/gh-action-pypi-publish/issues/11#issuecomment-530480449
|
||||
https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
|
||||
[PyPI API token]: https://pypi.org/help/#apitoken
|
||||
[Python distribution packages]:
|
||||
https://packaging.python.org/glossary/#term-Distribution-Package
|
||||
https://packaging.python.org/glossary/#term-distribution-package
|
||||
[SWUbanner]:
|
||||
https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg
|
||||
[SWUdocs]:
|
||||
https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md
|
||||
|
||||
[warehouse#12965]: https://github.com/pypi/warehouse/issues/12965
|
||||
[trusted publishing]: https://docs.pypi.org/trusted-publishers/
|
||||
[configured on PyPI]: https://docs.pypi.org/trusted-publishers/adding-a-publisher/
|
||||
|
||||
[how to specify username and password]: #specifying-a-different-username
|
||||
|
||||
+167
-61
@@ -4,82 +4,188 @@ description: Upload Python distribution packages to PyPI
|
||||
inputs:
|
||||
user:
|
||||
description: PyPI user
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
default: __token__
|
||||
password:
|
||||
description: Password for your PyPI user or an access token
|
||||
required: false
|
||||
repository-url: # Canonical alias for `repository_url`
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: true
|
||||
repository_url:
|
||||
description: The repository URL to use
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
repository_url: # DEPRECATED ALIAS; TODO: Remove in v3+
|
||||
description: >-
|
||||
[DEPRECATED]
|
||||
The repository URL to use
|
||||
deprecationMessage: >-
|
||||
The inputs have been normalized to use kebab-case.
|
||||
Use `repository-url` instead.
|
||||
required: false
|
||||
default: https://upload.pypi.org/legacy/
|
||||
packages-dir: # Canonical alias for `packages_dir`
|
||||
packages_dir:
|
||||
description: The target directory for distribution
|
||||
required: false
|
||||
# default: dist # TODO: uncomment once alias removed
|
||||
packages_dir: # DEPRECATED ALIAS; TODO: Remove in v3+
|
||||
description: >-
|
||||
[DEPRECATED]
|
||||
The target directory for distribution
|
||||
deprecationMessage: >-
|
||||
The inputs have been normalized to use kebab-case.
|
||||
Use `packages-dir` instead.
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
default: dist
|
||||
verify-metadata: # Canonical alias for `verify_metadata`
|
||||
verify_metadata:
|
||||
description: Check metadata before uploading
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
# default: 'true' # TODO: uncomment once alias removed
|
||||
verify_metadata: # DEPRECATED ALIAS; TODO: Remove in v3+
|
||||
description: >-
|
||||
[DEPRECATED]
|
||||
Check metadata before uploading
|
||||
deprecationMessage: >-
|
||||
The inputs have been normalized to use kebab-case.
|
||||
Use `verify-metadata` instead.
|
||||
required: false
|
||||
default: 'true'
|
||||
skip-existing: # Canonical alias for `skip_existing`
|
||||
default: true
|
||||
skip_existing:
|
||||
description: >-
|
||||
Do not fail if a Python package distribution
|
||||
exists in the target package index
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
# default: 'false' # TODO: uncomment once alias removed
|
||||
skip_existing: # DEPRECATED ALIAS; TODO: Remove in v3+
|
||||
description: >-
|
||||
[DEPRECATED]
|
||||
Do not fail if a Python package distribution
|
||||
exists in the target package index
|
||||
deprecationMessage: >-
|
||||
The inputs have been normalized to use kebab-case.
|
||||
Use `skip-existing` instead.
|
||||
required: false
|
||||
default: 'false'
|
||||
default: false
|
||||
verbose:
|
||||
description: Show verbose output.
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
default: 'false'
|
||||
print-hash: # Canonical alias for `print_hash`
|
||||
default: false
|
||||
print_hash:
|
||||
description: Show hash values of files to be uploaded
|
||||
deprecationMessage: >
|
||||
UNSUPPORTED GITHUB ACTION VERSION
|
||||
|
||||
|
||||
You are using `pypa/gh-action-pypi-publish@master`.
|
||||
The `master` branch of this project has been sunset and will not
|
||||
receive any updates, not even security bug fixes. Please, make
|
||||
sure to use a supported version. If you want to pin to v1 major
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you
|
||||
feel adventurous, you may opt to use use
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more
|
||||
general recommendation is to pin to exact tags or commit SHAs.
|
||||
|
||||
|
||||
Please also consider migrating your setup to use secretless publishing:
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
|
||||
|
||||
https://stand-with-ukraine.pp.ua
|
||||
required: false
|
||||
# default: 'false' # TODO: uncomment once alias removed
|
||||
print_hash: # DEPRECATED ALIAS; TODO: Remove in v3+
|
||||
description: >-
|
||||
[DEPRECATED]
|
||||
Show hash values of files to be uploaded
|
||||
deprecationMessage: >-
|
||||
The inputs have been normalized to use kebab-case.
|
||||
Use `print-hash` instead.
|
||||
required: false
|
||||
default: 'false'
|
||||
default: false
|
||||
branding:
|
||||
color: yellow
|
||||
icon: upload-cloud
|
||||
@@ -89,9 +195,9 @@ runs:
|
||||
args:
|
||||
- ${{ inputs.user }}
|
||||
- ${{ inputs.password }}
|
||||
- ${{ inputs.repository-url }}
|
||||
- ${{ inputs.packages-dir }}
|
||||
- ${{ inputs.verify-metadata }}
|
||||
- ${{ inputs.skip-existing }}
|
||||
- ${{ inputs.repository_url }}
|
||||
- ${{ inputs.packages_dir }}
|
||||
- ${{ inputs.verify_metadata }}
|
||||
- ${{ inputs.skip_existing }}
|
||||
- ${{ inputs.verbose }}
|
||||
- ${{ inputs.print-hash }}
|
||||
- ${{ inputs.print_hash }}
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import id # pylint: disable=redefined-builtin
|
||||
import requests
|
||||
|
||||
_GITHUB_STEP_SUMMARY = Path(os.getenv('GITHUB_STEP_SUMMARY'))
|
||||
|
||||
# The top-level error message that gets rendered.
|
||||
# This message wraps one of the other templates/messages defined below.
|
||||
_ERROR_SUMMARY_MESSAGE = """
|
||||
Trusted publishing exchange failure:
|
||||
|
||||
{message}
|
||||
|
||||
You're seeing this because the action wasn't given the inputs needed to
|
||||
perform password-based or token-based authentication. If you intended to
|
||||
perform one of those authentication methods instead of trusted
|
||||
publishing, then you should double-check your secret configuration and variable
|
||||
names.
|
||||
|
||||
Read more about trusted publishers at https://docs.pypi.org/trusted-publishers/
|
||||
|
||||
Read more about how this action uses trusted publishers at
|
||||
https://github.com/marketplace/actions/pypi-publish#trusted-publishing
|
||||
"""
|
||||
|
||||
# Rendered if OIDC identity token retrieval fails for any reason.
|
||||
_TOKEN_RETRIEVAL_FAILED_MESSAGE = """
|
||||
OpenID Connect token retrieval failed: {identity_error}
|
||||
|
||||
This generally indicates a workflow configuration error, such as insufficient
|
||||
permissions. Make sure that your workflow has `id-token: write` configured
|
||||
at the job level, e.g.:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
id-token: write
|
||||
```
|
||||
|
||||
Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings.
|
||||
""" # noqa: S105; not a password
|
||||
|
||||
# Specialization of the token retrieval failure case, when we know that
|
||||
# the failure cause is use within a third-party PR.
|
||||
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE = """
|
||||
OpenID Connect token retrieval failed: {identity_error}
|
||||
|
||||
The workflow context indicates that this action was called from a
|
||||
pull request on a fork. GitHub doesn't give these workflows OIDC permissions,
|
||||
even if `id-token: write` is explicitly configured.
|
||||
|
||||
To fix this, change your publishing workflow to use an event that
|
||||
forks of your repository cannot trigger (such as tag or release
|
||||
creation, or a manually triggered workflow dispatch).
|
||||
""" # noqa: S105; not a password
|
||||
|
||||
# Rendered if the package index refuses the given OIDC token.
|
||||
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE = """
|
||||
Token request failed: the server refused the request for the following reasons:
|
||||
|
||||
{reasons}
|
||||
|
||||
This generally indicates a trusted publisher configuration error, but could
|
||||
also indicate an internal error on GitHub or PyPI's part.
|
||||
|
||||
{rendered_claims}
|
||||
""" # noqa: S105; not a password
|
||||
|
||||
_RENDERED_CLAIMS = """
|
||||
The claims rendered below are **for debugging purposes only**. You should **not**
|
||||
use them to configure a trusted publisher unless they already match your expectations.
|
||||
|
||||
If a claim is not present in the claim set, then it is rendered as `MISSING`.
|
||||
|
||||
* `sub`: `{sub}`
|
||||
* `repository`: `{repository}`
|
||||
* `repository_owner`: `{repository_owner}`
|
||||
* `repository_owner_id`: `{repository_owner_id}`
|
||||
* `job_workflow_ref`: `{job_workflow_ref}`
|
||||
* `ref`: `{ref}`
|
||||
|
||||
See https://docs.pypi.org/trusted-publishers/troubleshooting/ for more help.
|
||||
|
||||
"""
|
||||
|
||||
# Rendered if the package index's token response isn't valid JSON.
|
||||
_SERVER_TOKEN_RESPONSE_MALFORMED_JSON = """
|
||||
Token request failed: the index produced an unexpected
|
||||
{status_code} response.
|
||||
|
||||
This strongly suggests a server configuration or downtime issue; wait
|
||||
a few minutes and try again.
|
||||
""" # noqa: S105; not a password
|
||||
|
||||
# Rendered if the package index's token response isn't a valid API token payload.
|
||||
_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE = """
|
||||
Token response error: the index gave us an invalid response.
|
||||
|
||||
This strongly suggests a server configuration or downtime issue; wait
|
||||
a few minutes and try again.
|
||||
""" # noqa: S105; not a password
|
||||
|
||||
|
||||
def die(msg: str) -> NoReturn:
|
||||
with _GITHUB_STEP_SUMMARY.open('a', encoding='utf-8') as io:
|
||||
print(_ERROR_SUMMARY_MESSAGE.format(message=msg), file=io)
|
||||
|
||||
# HACK: GitHub Actions' annotations don't work across multiple lines naively;
|
||||
# translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work.
|
||||
# See: https://github.com/actions/toolkit/issues/193
|
||||
msg = msg.replace('\n', '%0A')
|
||||
print(f'::error::Trusted publishing exchange failure: {msg}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def debug(msg: str):
|
||||
print(f'::debug::{msg.title()}', file=sys.stderr)
|
||||
|
||||
|
||||
def get_normalized_input(name: str) -> str | None:
|
||||
name = f'INPUT_{name.upper()}'
|
||||
if val := os.getenv(name):
|
||||
return val
|
||||
return os.getenv(name.replace('-', '_'))
|
||||
|
||||
|
||||
def assert_successful_audience_call(resp: requests.Response, domain: str):
|
||||
if resp.ok:
|
||||
return
|
||||
|
||||
match resp.status_code:
|
||||
case HTTPStatus.FORBIDDEN:
|
||||
# This index supports OIDC, but forbids the client from using
|
||||
# it (either because it's disabled, ratelimited, etc.)
|
||||
die(
|
||||
f'audience retrieval failed: repository at {domain} has trusted publishing disabled',
|
||||
)
|
||||
case HTTPStatus.NOT_FOUND:
|
||||
# This index does not support OIDC.
|
||||
die(
|
||||
'audience retrieval failed: repository at '
|
||||
f'{domain} does not indicate trusted publishing support',
|
||||
)
|
||||
case other:
|
||||
status = HTTPStatus(other)
|
||||
# Unknown: the index may or may not support OIDC, but didn't respond with
|
||||
# something we expect. This can happen if the index is broken, in maintenance mode,
|
||||
# misconfigured, etc.
|
||||
die(
|
||||
'audience retrieval failed: repository at '
|
||||
f'{domain} responded with unexpected {other}: {status.phrase}',
|
||||
)
|
||||
|
||||
|
||||
def render_claims(token: str) -> str:
|
||||
_, payload, _ = token.split('.', 2)
|
||||
|
||||
# urlsafe_b64decode needs padding; JWT payloads don't contain any.
|
||||
payload += '=' * (4 - (len(payload) % 4))
|
||||
claims = json.loads(base64.urlsafe_b64decode(payload))
|
||||
|
||||
def _get(name: str) -> str: # noqa: WPS430
|
||||
return claims.get(name, 'MISSING')
|
||||
|
||||
return _RENDERED_CLAIMS.format(
|
||||
sub=_get('sub'),
|
||||
repository=_get('repository'),
|
||||
repository_owner=_get('repository_owner'),
|
||||
repository_owner_id=_get('repository_owner_id'),
|
||||
job_workflow_ref=_get('job_workflow_ref'),
|
||||
ref=_get('ref'),
|
||||
)
|
||||
|
||||
|
||||
def event_is_third_party_pr() -> bool:
|
||||
# Non-`pull_request` events cannot be from third-party PRs.
|
||||
if os.getenv('GITHUB_EVENT_NAME') != 'pull_request':
|
||||
return False
|
||||
|
||||
event_path = os.getenv('GITHUB_EVENT_PATH')
|
||||
if not event_path:
|
||||
# No GITHUB_EVENT_PATH indicates a weird GitHub or runner bug.
|
||||
debug('unexpected: no GITHUB_EVENT_PATH to check')
|
||||
return False
|
||||
|
||||
try:
|
||||
event = json.loads(Path(event_path).read_bytes())
|
||||
except json.JSONDecodeError:
|
||||
debug('unexpected: GITHUB_EVENT_PATH does not contain valid JSON')
|
||||
return False
|
||||
|
||||
try:
|
||||
return event['pull_request']['head']['repo']['fork']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
repository_url = get_normalized_input('repository-url')
|
||||
repository_domain = urlparse(repository_url).netloc
|
||||
token_exchange_url = f'https://{repository_domain}/_/oidc/mint-token'
|
||||
|
||||
# Indices are expected to support `https://{domain}/_/oidc/audience`,
|
||||
# which tells OIDC exchange clients which audience to use.
|
||||
audience_url = f'https://{repository_domain}/_/oidc/audience'
|
||||
audience_resp = requests.get(audience_url, timeout=5) # S113 wants a timeout
|
||||
assert_successful_audience_call(audience_resp, repository_domain)
|
||||
|
||||
oidc_audience = audience_resp.json()['audience']
|
||||
|
||||
debug(f'selected trusted publishing exchange endpoint: {token_exchange_url}')
|
||||
|
||||
try:
|
||||
oidc_token = id.detect_credential(audience=oidc_audience)
|
||||
except id.IdentityError as identity_error:
|
||||
cause_msg_tmpl = (
|
||||
_TOKEN_RETRIEVAL_FAILED_FORK_PR_MESSAGE if event_is_third_party_pr()
|
||||
else _TOKEN_RETRIEVAL_FAILED_MESSAGE
|
||||
)
|
||||
for_cause_msg = cause_msg_tmpl.format(identity_error=identity_error)
|
||||
die(for_cause_msg)
|
||||
|
||||
# Now we can do the actual token exchange.
|
||||
mint_token_resp = requests.post(
|
||||
token_exchange_url,
|
||||
json={'token': oidc_token},
|
||||
timeout=5, # S113 wants a timeout
|
||||
)
|
||||
|
||||
try:
|
||||
mint_token_payload = mint_token_resp.json()
|
||||
except requests.JSONDecodeError:
|
||||
# Token exchange failure normally produces a JSON error response, but
|
||||
# we might have hit a server error instead.
|
||||
die(
|
||||
_SERVER_TOKEN_RESPONSE_MALFORMED_JSON.format(
|
||||
status_code=mint_token_resp.status_code,
|
||||
),
|
||||
)
|
||||
|
||||
# On failure, the JSON response includes the list of errors that
|
||||
# occurred during minting.
|
||||
if not mint_token_resp.ok:
|
||||
reasons = '\n'.join(
|
||||
f'* `{error["code"]}`: {error["description"]}'
|
||||
for error in mint_token_payload['errors']
|
||||
)
|
||||
|
||||
rendered_claims = render_claims(oidc_token)
|
||||
|
||||
die(
|
||||
_SERVER_REFUSED_TOKEN_EXCHANGE_MESSAGE.format(
|
||||
reasons=reasons,
|
||||
rendered_claims=rendered_claims,
|
||||
),
|
||||
)
|
||||
|
||||
pypi_token = mint_token_payload.get('token')
|
||||
if pypi_token is None:
|
||||
die(_SERVER_TOKEN_RESPONSE_MALFORMED_MESSAGE)
|
||||
|
||||
# Mask the newly minted PyPI token, so that we don't accidentally leak it in logs.
|
||||
print(f'::add-mask::{pypi_token}', file=sys.stderr)
|
||||
|
||||
# This final print will be captured by the subshell in `twine-upload.sh`.
|
||||
print(pypi_token)
|
||||
Regular → Executable
+7
-7
@@ -4,15 +4,15 @@ import sys
|
||||
|
||||
packages_dir = pathlib.Path(sys.argv[1]).resolve().absolute()
|
||||
|
||||
print('Showing hash values of files to be uploaded:')
|
||||
print("Showing hash values of files to be uploaded:")
|
||||
|
||||
for file_object in packages_dir.iterdir():
|
||||
sha256 = hashlib.sha256()
|
||||
md5 = hashlib.md5() # noqa: S324; only use for reference
|
||||
md5 = hashlib.md5()
|
||||
blake2_256 = hashlib.blake2b(digest_size=256 // 8)
|
||||
|
||||
print(file_object)
|
||||
print('')
|
||||
print("")
|
||||
|
||||
content = file_object.read_bytes()
|
||||
|
||||
@@ -20,7 +20,7 @@ for file_object in packages_dir.iterdir():
|
||||
md5.update(content)
|
||||
blake2_256.update(content)
|
||||
|
||||
print(f'SHA256: {sha256.hexdigest()}')
|
||||
print(f'MD5: {md5.hexdigest()}')
|
||||
print(f'BLAKE2-256: {blake2_256.hexdigest()}')
|
||||
print('')
|
||||
print(f"SHA256: {sha256.hexdigest()}")
|
||||
print(f"MD5: {md5.hexdigest()}")
|
||||
print(f"BLAKE2-256: {blake2_256.hexdigest()}")
|
||||
print("")
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pip-with-requires-python
|
||||
@@ -1,12 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --config=../.pip-tools.toml --output-file=runtime-prerequisites.txt --strip-extras runtime-prerequisites.in
|
||||
#
|
||||
pip-with-requires-python==1.0.1
|
||||
# via -r runtime-prerequisites.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==24.0
|
||||
# via pip-with-requires-python
|
||||
@@ -1,9 +0,0 @@
|
||||
twine
|
||||
|
||||
# NOTE: Used to detect an ambient OIDC credential for OIDC publishing.
|
||||
id ~= 1.0
|
||||
|
||||
# NOTE: This is pulled in transitively through `twine`, but we also declare
|
||||
# NOTE: it explicitly here because `oidc-exchange.py` uses it.
|
||||
# Ref: https://github.com/di/id
|
||||
requests
|
||||
@@ -1,80 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --config=../.pip-tools.toml --output-file=runtime.txt --strip-extras runtime.in
|
||||
#
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
cffi==1.16.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
cryptography==42.0.5
|
||||
# via secretstorage
|
||||
docutils==0.20.1
|
||||
# via readme-renderer
|
||||
id==1.3.0
|
||||
# via -r runtime.in
|
||||
idna==3.7
|
||||
# via requests
|
||||
importlib-metadata==7.0.2
|
||||
# via twine
|
||||
jaraco-classes==3.3.1
|
||||
# via keyring
|
||||
jeepney==0.8.0
|
||||
# via
|
||||
# keyring
|
||||
# secretstorage
|
||||
keyring==24.3.1
|
||||
# via twine
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
more-itertools==10.2.0
|
||||
# via jaraco-classes
|
||||
nh3==0.2.15
|
||||
# via readme-renderer
|
||||
pkginfo==1.10.0
|
||||
# via twine
|
||||
pycparser==2.21
|
||||
# via cffi
|
||||
pydantic==2.6.3
|
||||
# via id
|
||||
pydantic-core==2.16.3
|
||||
# via pydantic
|
||||
pygments==2.17.2
|
||||
# via
|
||||
# readme-renderer
|
||||
# rich
|
||||
readme-renderer==43.0
|
||||
# via twine
|
||||
requests==2.31.0
|
||||
# via
|
||||
# -r runtime.in
|
||||
# id
|
||||
# requests-toolbelt
|
||||
# twine
|
||||
requests-toolbelt==1.0.0
|
||||
# via twine
|
||||
rfc3986==2.0.0
|
||||
# via twine
|
||||
rich==13.7.1
|
||||
# via twine
|
||||
secretstorage==3.3.3
|
||||
# via keyring
|
||||
twine==5.0.0
|
||||
# via -r runtime.in
|
||||
typing-extensions==4.10.0
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
urllib3==2.2.1
|
||||
# via
|
||||
# requests
|
||||
# twine
|
||||
zipp==3.17.0
|
||||
# via importlib-metadata
|
||||
+47
-96
@@ -1,107 +1,58 @@
|
||||
#! /bin/bash
|
||||
|
||||
if [[ -n "${DEBUG}" ]]
|
||||
then
|
||||
set -x
|
||||
fi
|
||||
|
||||
#! /usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
|
||||
# NOTE: These variables are needed to combat GitHub passing broken env vars
|
||||
# NOTE: from the runner VM host runtime.
|
||||
# Ref: https://github.com/pypa/gh-action-pypi-publish/issues/112
|
||||
export HOME="/root" # So that `python -m site` doesn't get confused
|
||||
export PATH="/usr/bin:${PATH}" # To find `id`
|
||||
. /etc/profile # Makes python and other executables findable
|
||||
export PATH="$(python -m site --user-base)/bin:${PATH}"
|
||||
export PYTHONPATH="$(python -m site --user-site):${PYTHONPATH}"
|
||||
echo \
|
||||
'# UNSUPPORTED GITHUB ACTION VERSION' \
|
||||
'\n\n' \
|
||||
You are using `pypa/gh-action-pypi-publish@master`. \
|
||||
The `master` branch of this project has been sunset and will not \
|
||||
receive any updates, not even security bug fixes. Please, make \
|
||||
sure to use a supported version. If you want to pin to v1 major \
|
||||
version, use `pypa/gh-action-pypi-publish@release/v1`. If you \
|
||||
feel adventurous, you may opt to use use \
|
||||
`pypa/gh-action-pypi-publish@unstable/v1` instead. A more \
|
||||
general recommendation is to pin to exact tags or commit shas. \
|
||||
'\n\n' \
|
||||
'\n\n' \
|
||||
'### Oh, and while you are here — #StandWithUkraine' \
|
||||
'\n\n' \
|
||||
'[![SWUbanner]][SWUdocs]' \
|
||||
'\n\n' \
|
||||
'[SWUbanner]:' \
|
||||
'https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg' \
|
||||
'\n\n' \
|
||||
'[SWUdocs]:' \
|
||||
'https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md' \
|
||||
>> "${GITHUB_STEP_SUMMARY}"
|
||||
echo \
|
||||
::warning file='# >>' PyPA publish to PyPI GHA'%3A' \
|
||||
UNSUPPORTED GITHUB ACTION VERSION \
|
||||
'<< ':: \
|
||||
You are using '"pypa/gh-action-pypi-publish@master"'. \
|
||||
The '"master"' branch of this project has been sunset and will not \
|
||||
receive any updates, not even security bug fixes. Please, make \
|
||||
sure to use a supported version. If you want to pin to v1 major \
|
||||
version, use '"pypa/gh-action-pypi-publish@release/v1"'. If you \
|
||||
feel adventurous, you may opt to use use \
|
||||
'"pypa/gh-action-pypi-publish@unstable/v1"' instead. A more \
|
||||
general recommendation is to pin to exact tags or commit shas.
|
||||
|
||||
|
||||
function get-normalized-input() {
|
||||
local var_name=${1}
|
||||
python -c \
|
||||
'
|
||||
from os import getenv
|
||||
from sys import argv
|
||||
envvar_name = f"INPUT_{argv[1].upper()}"
|
||||
print(
|
||||
getenv(envvar_name) or getenv(envvar_name.replace("-", "_")) or "",
|
||||
end="",
|
||||
)
|
||||
' \
|
||||
"${var_name}"
|
||||
}
|
||||
|
||||
|
||||
INPUT_REPOSITORY_URL="$(get-normalized-input 'repository-url')"
|
||||
INPUT_PACKAGES_DIR="$(get-normalized-input 'packages-dir')"
|
||||
INPUT_VERIFY_METADATA="$(get-normalized-input 'verify-metadata')"
|
||||
INPUT_SKIP_EXISTING="$(get-normalized-input 'skip-existing')"
|
||||
INPUT_PRINT_HASH="$(get-normalized-input 'print-hash')"
|
||||
|
||||
PASSWORD_DEPRECATION_NUDGE="::error title=Password-based uploads disabled::\
|
||||
As of 2024, PyPI requires all users to enable Two-Factor \
|
||||
Authentication. This consequently requires all users to switch \
|
||||
to either Trusted Publishers (preferred) or API tokens for package \
|
||||
uploads. Read more: \
|
||||
https://blog.pypi.org/posts/2023-05-25-securing-pypi-with-2fa/"
|
||||
|
||||
TRUSTED_PUBLISHING_NUDGE="::warning title=Upgrade to Trusted Publishing::\
|
||||
Trusted Publishers allows publishing packages to PyPI from automated \
|
||||
environments like GitHub Actions without needing to use username/password \
|
||||
combinations or API tokens to authenticate with PyPI. Read more: \
|
||||
https://docs.pypi.org/trusted-publishers"
|
||||
|
||||
if [[ "${INPUT_USER}" == "__token__" && -z "${INPUT_PASSWORD}" ]] ; then
|
||||
# No password supplied by the user implies that we're in the OIDC flow;
|
||||
# retrieve the OIDC credential and exchange it for a PyPI API token.
|
||||
echo "::debug::Authenticating to ${INPUT_REPOSITORY_URL} via Trusted Publishing"
|
||||
INPUT_PASSWORD="$(python /app/oidc-exchange.py)"
|
||||
elif [[ "${INPUT_USER}" == '__token__' ]]; then
|
||||
echo \
|
||||
'::debug::Using a user-provided API token for authentication' \
|
||||
"against ${INPUT_REPOSITORY_URL}"
|
||||
|
||||
if [[ "${INPUT_REPOSITORY_URL}" =~ pypi\.org ]]; then
|
||||
echo "${TRUSTED_PUBLISHING_NUDGE}"
|
||||
fi
|
||||
else
|
||||
echo \
|
||||
'::debug::Using a username + password pair for authentication' \
|
||||
"against ${INPUT_REPOSITORY_URL}"
|
||||
|
||||
if [[ "${INPUT_REPOSITORY_URL}" =~ pypi\.org ]]; then
|
||||
echo "${PASSWORD_DEPRECATION_NUDGE}"
|
||||
echo "${TRUSTED_PUBLISHING_NUDGE}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[
|
||||
"$INPUT_USER" == "__token__" &&
|
||||
! "$INPUT_PASSWORD" =~ ^pypi-
|
||||
]]
|
||||
then
|
||||
if [[ -z "$INPUT_PASSWORD" ]]; then
|
||||
echo \
|
||||
::warning file='# >>' PyPA publish to PyPI GHA'%3A' \
|
||||
EMPTY TOKEN \
|
||||
'<< ':: \
|
||||
It looks like you have not passed a password or it \
|
||||
is otherwise empty. Please verify that you have passed it \
|
||||
directly or, preferably, through a secret.
|
||||
else
|
||||
echo \
|
||||
::warning file='# >>' PyPA publish to PyPI GHA'%3A' \
|
||||
POTENTIALLY INVALID TOKEN \
|
||||
'<< ':: \
|
||||
It looks like you are trying to use an API token to \
|
||||
authenticate in the package index and your token value does \
|
||||
not start with '"pypi-"' as it typically should. This may \
|
||||
cause an authentication error. Please verify that you have \
|
||||
copied your token properly if such an error occurs.
|
||||
fi
|
||||
echo \
|
||||
::warning file='# >>' PyPA publish to PyPI GHA'%3A' \
|
||||
POTENTIALLY INVALID TOKEN \
|
||||
'<< ':: \
|
||||
It looks like you are trying to use an API token to \
|
||||
authenticate in the package index and your token value does \
|
||||
not start with '"pypi-"' as it typically should. This may \
|
||||
cause an authentication error. Please verify that you have \
|
||||
copied your token properly if such an error occurs.
|
||||
fi
|
||||
|
||||
if ( ! ls -A ${INPUT_PACKAGES_DIR%%/}/*.tar.gz &> /dev/null && \
|
||||
@@ -121,9 +72,9 @@ if [[ ${INPUT_VERIFY_METADATA,,} != "false" ]] ; then
|
||||
twine check ${INPUT_PACKAGES_DIR%%/}/*
|
||||
fi
|
||||
|
||||
TWINE_EXTRA_ARGS=--disable-progress-bar
|
||||
TWINE_EXTRA_ARGS=
|
||||
if [[ ${INPUT_SKIP_EXISTING,,} != "false" ]] ; then
|
||||
TWINE_EXTRA_ARGS="${TWINE_EXTRA_ARGS} --skip-existing"
|
||||
TWINE_EXTRA_ARGS=--skip-existing
|
||||
fi
|
||||
|
||||
if [[ ${INPUT_VERBOSE,,} != "false" ]] ; then
|
||||
|
||||
Reference in New Issue
Block a user