Compare commits

..

4 Commits

Author SHA1 Message Date
Hugo van Kemenade c1b3402824 Fix a deprecation msg typo in the ancient branch: "migrading"->"migrating" (#212) 2024-02-27 04:59:28 +01:00
Sviatoslav Sydorenko c5d8ac5008 Add deprecationMessage to all inputs @ master 2023-07-13 17:06:59 +02:00
Sviatoslav Sydorenko ffa46275f9 Render a depecation message in job summary. 2023-07-13 17:06:59 +02:00
Sviatoslav Sydorenko 9b8e7336db 📝Inform about deprecation of master branch
This patch suggests the users to use `release/v1`.

Resolves #83
2022-07-25 17:29:21 +02:00
17 changed files with 302 additions and 992 deletions
+5 -15
View File
@@ -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
-32
View File
@@ -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.
+14
View File
@@ -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/
...
-4
View File
@@ -1,4 +0,0 @@
[tool.pip-tools]
allow-unsafe = true
resolver = "backtracking"
strip-extras = true
-137
View File
@@ -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
...
+2 -10
View File
@@ -1,10 +1,2 @@
---
extends: default
rules:
indentation:
level: error
indent-sequences: false
...
indentation:
indent-sequences: false
+3 -15
View File
@@ -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"]
+57 -144
View File
@@ -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
View File
@@ -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 }}
-272
View File
@@ -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
View File
@@ -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
View File
@@ -1 +0,0 @@
pip-with-requires-python
-12
View File
@@ -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
-9
View File
@@ -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
-80
View File
@@ -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
View File
@@ -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