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
22 changed files with 298 additions and 1707 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,58 +0,0 @@
---
name: 🏗️
on: # yamllint disable-line rule:truthy
pull_request:
push:
branches: ["release/*", "unstable/*"]
workflow_dispatch:
inputs:
tag:
description: Docker image tag
required: true
type: string
jobs:
smoke-test:
uses: ./.github/workflows/reusable-smoke-test.yml
build-and-push:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs:
- smoke-test
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
DOCKER_TAG="${DOCKER_TAG/'/'/'-'}"
DOCKER_TAG_MAJOR=$(echo "$DOCKER_TAG" | cut -d '.' -f 1)
DOCKER_TAG_MAJOR_MINOR=$(echo "$DOCKER_TAG" | cut -d '.' -f 1-2)
IMAGE="ghcr.io/$GITHUB_REPOSITORY:${DOCKER_TAG}"
IMAGE_MAJOR="ghcr.io/$GITHUB_REPOSITORY:${DOCKER_TAG_MAJOR}"
IMAGE_MAJOR_MINOR="ghcr.io/$GITHUB_REPOSITORY:${DOCKER_TAG_MAJOR_MINOR}"
IMAGE_SHA="ghcr.io/$GITHUB_REPOSITORY:${GITHUB_SHA}"
echo "IMAGE=$IMAGE" >>"$GITHUB_ENV"
echo "IMAGE_MAJOR=$IMAGE_MAJOR" >>"$GITHUB_ENV"
echo "IMAGE_MAJOR_MINOR=$IMAGE_MAJOR_MINOR" >>"$GITHUB_ENV"
echo "IMAGE_SHA=$IMAGE_SHA" >>"$GITHUB_ENV"
docker build . \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from $IMAGE \
--tag $IMAGE
docker tag $IMAGE $IMAGE_MAJOR
docker tag $IMAGE $IMAGE_MAJOR_MINOR
docker tag $IMAGE $IMAGE_SHA
env:
DOCKER_TAG: ${{ inputs.tag || github.ref_name }}
- name: Log in to GHCR
run: >-
echo ${{ secrets.GITHUB_TOKEN }} |
docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
- name: Push Docker image to GHCR
run: |
docker push $IMAGE
docker push $IMAGE_MAJOR
docker push $IMAGE_MAJOR_MINOR
docker push $IMAGE_SHA
-145
View File
@@ -1,145 +0,0 @@
---
name: ♻️ 🧪
on: # yamllint disable-line rule:truthy
workflow_call:
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:
fail-fast:
strategy:
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 2
steps:
- name: Check out the action locally
uses: actions/checkout@v4
with:
path: test
- name: Fail-fast in unsupported environments
continue-on-error: true
id: fail-fast
uses: ./test
- name: Error if action did not fail-fast in unsupported environments
if: steps.fail-fast.outcome == 'success'
run: |
>&2 echo This action should fail-fast in unsupported environments.
exit 1
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@v4
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: Create the Rust package directory
run: mkdir -pv rust-example
- name: Initialize a Rust project
run: cargo init
working-directory: rust-example
- name: Populate the Rust package `pyproject.toml`
run: echo "$CONTENTS" > pyproject.toml
env:
CONTENTS: |
[build-system]
requires = [
"maturin ~=1.0",
]
build-backend = "maturin"
working-directory: rust-example
- name: Build the stub package sdist and wheel distributions
run: python3 -m build -o ../dist/
working-directory: rust-example
- name: Register the stub package in devpi
run: |
for dist in dist/*.tar.gz
do
echo "Registering ${dist}..."
twine register "${dist}"
done
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.29.2
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.6.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.3.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
+4 -19
View File
@@ -1,35 +1,20 @@
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"
LABEL "homepage" "https://github.com/marketplace/actions/pypi-publish"
LABEL "org.opencontainers.image.source" "https://github.com/pypa/gh-action-pypi-publish"
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 print-pkg-names.py .
COPY oidc-exchange.py .
COPY attestations.py .
RUN chmod +x twine-upload.sh
ENTRYPOINT ["/app/twine-upload.sh"]
+45 -237
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,137 +7,67 @@ 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].
> [!TIP]
> A limited number of usage scenarios is supported, including the
> [PyPA guide] example. See the [non-goals] for more detail.
## 🌇 `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
```yml
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
```
_(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.
> **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 job:
filter to the step:
```yml
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
```
### Generating and uploading attestations
So the full step would look like:
> [!IMPORTANT]
> Support for generating and uploading [digital attestations] is currently
> experimental and limited only to Trusted Publishing flows using PyPI or TestPyPI.
> Support for this feature is not yet stable; the settings and behavior described
> below may change without prior notice.
> [!NOTE]
> Generating and uploading digital attestations currently requires
> authentication with a [trusted publisher].
Generating signed [digital attestations] for all the distribution files
and uploading them all together is now on by default for all projects
using Trusted Publishing. To disable it, set `attestations` as follows:
```yml
with:
attestations: false
- 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 attestation objects are created using [Sigstore] for each
distribution package, signing them with the identity provided
by the GitHub's OIDC token associated with the current workflow. This means
both the trusted publishing authentication and the attestations are tied to the
same identity.
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
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.
They are typically expected to do this in a _separate GitHub Actions
CI/CD job_ running before the one where they call this action and having
restricted privileges.
> [!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.
@@ -162,100 +89,32 @@ restricted privileges.
> sharing the built dists across stages and jobs. Then, use the `needs`
> setting to order the build, test and publish stages.
The expected environment for running `pypi-publish` is the
GitHub-provided Ubuntu VM. We are running a smoke-test against
`ubuntu-latest` in CI but any currently available numbered versions
should do. We'll consider them supported for as long as GitHub itself
supports them.
Running the action in a job that has a `container:` set is not
supported. It might work for you but you're on your own when it breaks.
If you feel the need to use it, it's likely that you're not following
the recommendation of invoking the build automation in a separate job,
which is considered a security issue (especially, when using [Trusted
Publishing][trusted publisher] that may cause privilege escalation and
would enable the attackers to impersonate the GitHub-backed identity of
the repository through transitive build dependency poisoning). The
solution is to have one job (or multiple, in case of projects with
C-extensions) for building the distribution packages, followed by
another that publishes them.
Self-hosted runners are best effort, provided no other unsupported
things influence them. We are unable to test this in CI and they may
break. This is often the case when using custom runtimes and not the
official GitHub-provided VMs. In general, if you follow the
recommendation of building in a separate job, you shouldn't need to run
this action within a self-hosted runner — it should be possible to
build your dists in a self-hosted runner, save them as a GitHub Actions
artifact in that job, and then invoke the publishing job that would run
within GitHub-provided runners, downloading the artifact with the dists
and publishing them. Such separation is the _recommended_/**supported**
way of handling this scenario.
Our understandng is that Trusted publishing is expected to work on
self-hosted runners. It is backed by OIDC. If it doesn't work, you
should probably ask GitHub if you missed something. We wouldn't be able
to assist here.
Trusted Publishing cannot be tested in CI at the moment, sadly. It is
supported and bugs should be reported but it may take time to sort out
as it often requires cross-project collaboration to debug (sometimes,
problems occur due to changes in PyPI and not in the action).
The only case that is explicitly unsupported at the moment is [Trusted
Publishing][trusted publisher] in reusable workflows. This requires
support on the PyPI side and is being worked on. Please, do not report
bugs related to this case. The current recommendation is to put
everything else you want into a reusable workflow but keep the job
calling `pypi-publish` in a top-level one.
Invoking `pypi-publish` from composite actions is unsupported. It is not
tested. GitHub Runners have limitations and bugs in this case. But more
importantly, this is usually an indication of using it insecurely. When
using [Trusted Publishing][trusted publisher], it is imperative to keep
build machinery invocation in a separate job with restrictive privileges
as [Trusted Publishing][trusted publisher] itself requires elevated
permissions to make use of OIDC. Our observation is that the users
sometimes create in-project composite actions that invoke building and
publishing in the same job. As such, we don't seek to support such a
dangerous configuration in the first place. The solution is pretty much
the same as with the previous problem — use a separate job with
dedicated and scoped privileges just for publishing; and invoke that
in-project composite action from a different job.
And finally, invoking `pypi-publish` more than once in the same job is
not considered supported. It may work in a limited number of scenarios
but please, don't do this. If you want to publish to several indexes,
build the dists in one job and add several publishing jobs, one per
upload.
## Advanced release management
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
@@ -268,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
@@ -279,7 +140,7 @@ check with:
```yml
with:
verify-metadata: false
verify_metadata: false
```
### Tolerating release package file duplicates
@@ -289,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
@@ -318,57 +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/build-and-push-docker-image.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
[non-goals]: #Non-goals
[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_]:
@@ -377,18 +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
[digital attestations]: https://peps.python.org/pep-0740/
[Sigstore]: https://www.sigstore.dev/
[trusted publisher]: #trusted-publishing
+173 -150
View File
@@ -4,177 +4,200 @@ 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'
attestations:
description: >-
[EXPERIMENTAL]
Enable experimental support for PEP 740 attestations.
Only works with PyPI and TestPyPI via Trusted Publishing.
required: false
default: 'true'
default: false
branding:
color: yellow
icon: upload-cloud
runs:
using: composite
steps:
- name: Fail-fast in unsupported environments
if: runner.os != 'Linux'
run: |
>&2 echo This action is only able to run under GNU/Linux environments
exit 1
shell: bash -eEuo pipefail {0}
- name: Reset path if needed
run: |
# Reset path if needed
# https://github.com/pypa/gh-action-pypi-publish/issues/112
if [[ $PATH != *"/usr/bin"* ]]; then
echo "\$PATH=$PATH. Resetting \$PATH for GitHub Actions."
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
echo "PATH=$PATH" >>"$GITHUB_ENV"
echo "$PATH" >>"$GITHUB_PATH"
echo "\$PATH reset. \$PATH=$PATH"
fi
shell: bash
- name: Set repo and ref from which to run Docker container action
id: set-repo-and-ref
run: |
# Set repo and ref from which to run Docker container action
# to handle cases in which `github.action_` context is not set
# https://github.com/actions/runner/issues/2473
REF=${{ env.ACTION_REF || env.PR_REF || github.ref_name }}
REPO=${{ env.ACTION_REPO || env.PR_REPO || github.repository }}
REPO_ID=${{ env.PR_REPO_ID || github.repository_id }}
echo "ref=$REF" >>"$GITHUB_OUTPUT"
echo "repo=$REPO" >>"$GITHUB_OUTPUT"
echo "repo-id=$REPO_ID" >>"$GITHUB_OUTPUT"
shell: bash
env:
ACTION_REF: ${{ github.action_ref }}
ACTION_REPO: ${{ github.action_repository }}
PR_REF: ${{ github.event.pull_request.head.ref }}
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PR_REPO_ID: ${{ github.event.pull_request.base.repo.id }}
- name: Discover pre-installed Python
id: pre-installed-python
run: |
# 🔎 Discover pre-installed Python
echo "python-path=$(command -v python3 || :)" | tee -a "${GITHUB_OUTPUT}"
shell: bash
- name: Install Python 3
if: steps.pre-installed-python.outputs.python-path == ''
id: new-python
uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Create Docker container action
run: |
# Create Docker container action
${{
steps.pre-installed-python.outputs.python-path == ''
&& steps.new-python.outputs.python-path
|| steps.pre-installed-python.outputs.python-path
}} '${{ github.action_path }}/create-docker-action.py'
env:
REF: ${{ steps.set-repo-and-ref.outputs.ref }}
REPO: ${{ steps.set-repo-and-ref.outputs.repo }}
REPO_ID: ${{ steps.set-repo-and-ref.outputs.repo-id }}
shell: bash
- name: Run Docker container
# The generated trampoline action must exist in the allowlisted
# runner-defined working directory so it can be referenced by the
# relative path starting with `./`.
#
# This mutates the end-user's workspace slightly but uses a path
# that is unlikely to clash with somebody else's use.
#
# We cannot use randomized paths because the composite action
# syntax does not allow accessing variables in `uses:`. This
# means that we end up having to hardcode this path both here and
# in `create-docker-action.py`.
uses: ./.github/.tmp/.generated-actions/run-pypi-publish-in-docker-container
with:
user: ${{ inputs.user }}
password: ${{ inputs.password }}
repository-url: ${{ inputs.repository-url || inputs.repository_url }}
packages-dir: ${{ inputs.packages-dir || inputs.packages_dir }}
verify-metadata: ${{ inputs.verify-metadata || inputs.verify_metadata }}
skip-existing: ${{ inputs.skip-existing || inputs.skip_existing }}
verbose: ${{ inputs.verbose }}
print-hash: ${{ inputs.print-hash || inputs.print_hash }}
attestations: ${{ inputs.attestations }}
using: docker
image: Dockerfile
args:
- ${{ inputs.user }}
- ${{ inputs.password }}
- ${{ inputs.repository_url }}
- ${{ inputs.packages_dir }}
- ${{ inputs.verify_metadata }}
- ${{ inputs.skip_existing }}
- ${{ inputs.verbose }}
- ${{ inputs.print_hash }}
-115
View File
@@ -1,115 +0,0 @@
import logging
import os
import sys
from pathlib import Path
from typing import NoReturn
from pypi_attestations import Attestation, Distribution
from sigstore.oidc import IdentityError, IdentityToken, detect_credential
from sigstore.sign import Signer, SigningContext
# Be very verbose.
sigstore_logger = logging.getLogger('sigstore')
sigstore_logger.setLevel(logging.DEBUG)
sigstore_logger.addHandler(logging.StreamHandler())
_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 = """
Attestation generation failure:
{message}
You're seeing this because the action attempted to generated PEP 740
attestations for its inputs, but failed to do so.
"""
# Rendered if OIDC identity token retrieval fails for any reason.
_TOKEN_RETRIEVAL_FAILED_MESSAGE = """
OpenID Connect token retrieval failed: {identity_error}
This failure occurred after a successful Trusted Publishing Flow,
suggesting a transient error.
""" # 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::Attestation generation failure: {msg}', file=sys.stderr)
sys.exit(1)
def debug(msg: str):
print(f'::debug::{msg}', file=sys.stderr)
def collect_dists(packages_dir: Path) -> list[Path]:
# Collect all sdists and wheels.
dist_paths = [sdist.resolve() for sdist in packages_dir.glob('*.tar.gz')]
dist_paths.extend(sdist.resolve() for sdist in packages_dir.glob('*.zip'))
dist_paths.extend(whl.resolve() for whl in packages_dir.glob('*.whl'))
# Make sure everything that looks like a dist actually is one.
# We do this up-front to prevent partial signing.
if (invalid_dists := [path for path in dist_paths if not path.is_file()]):
invalid_dist_list = ', '.join(map(str, invalid_dists))
die(
'The following paths look like distributions but '
f'are not actually files: {invalid_dist_list}',
)
return dist_paths
def attest_dist(dist_path: Path, signer: Signer) -> None:
# We are the publishing step, so there should be no pre-existing publish
# attestation. The presence of one indicates user confusion.
attestation_path = Path(f'{dist_path}.publish.attestation')
if attestation_path.exists():
die(f'{dist_path} already has a publish attestation: {attestation_path}')
dist = Distribution.from_file(dist_path)
attestation = Attestation.sign(signer, dist)
attestation_path.write_text(attestation.model_dump_json(), encoding='utf-8')
debug(f'saved publish attestation: {dist_path=} {attestation_path=}')
def get_identity_token() -> IdentityToken:
# Will raise `sigstore.oidc.IdentityError` if it fails to get the token
# from the environment or if the token is malformed.
# NOTE: audience is always sigstore.
oidc_token = detect_credential()
return IdentityToken(oidc_token)
def main() -> None:
packages_dir = Path(sys.argv[1])
try:
identity = get_identity_token()
except IdentityError as identity_error:
# NOTE: We only perform attestations in trusted publishing flows, so we
# don't need to re-check for the "PR from fork" error mode, only
# generic token retrieval errors. We also render a simpler error,
# since permissions can't be to blame at this stage.
die(_TOKEN_RETRIEVAL_FAILED_MESSAGE.format(identity_error=identity_error))
dist_paths = collect_dists(packages_dir)
with SigningContext.production().signer(identity, cache=True) as s:
debug(f'attesting to dists: {dist_paths}')
for dist_path in dist_paths:
attest_dist(dist_path, s)
if __name__ == '__main__':
main()
-91
View File
@@ -1,91 +0,0 @@
import json
import os
import pathlib
DESCRIPTION = 'description'
REQUIRED = 'required'
REF = os.environ['REF']
REPO = os.environ['REPO']
REPO_ID = os.environ['REPO_ID']
REPO_ID_GH_ACTION = '178055147'
ACTION_SHELL_CHECKOUT_PATH = pathlib.Path(__file__).parent.resolve()
def set_image(ref: str, repo: str, repo_id: str) -> str:
if repo_id == REPO_ID_GH_ACTION:
return str(ACTION_SHELL_CHECKOUT_PATH / 'Dockerfile')
docker_ref = ref.replace('/', '-')
return f'docker://ghcr.io/{repo}:{docker_ref}'
image = set_image(REF, REPO, REPO_ID)
action = {
'name': '🏃',
DESCRIPTION: (
'Run Docker container to upload Python distribution packages to PyPI'
),
'inputs': {
'user': {DESCRIPTION: 'PyPI user', REQUIRED: False},
'password': {
DESCRIPTION: 'Password for your PyPI user or an access token',
REQUIRED: False,
},
'repository-url': {
DESCRIPTION: 'The repository URL to use',
REQUIRED: False,
},
'packages-dir': {
DESCRIPTION: 'The target directory for distribution',
REQUIRED: False,
},
'verify-metadata': {
DESCRIPTION: 'Check metadata before uploading',
REQUIRED: False,
},
'skip-existing': {
DESCRIPTION: (
'Do not fail if a Python package distribution'
' exists in the target package index'
),
REQUIRED: False,
},
'verbose': {DESCRIPTION: 'Show verbose output.', REQUIRED: False},
'print-hash': {
DESCRIPTION: 'Show hash values of files to be uploaded',
REQUIRED: False,
},
'attestations': {
DESCRIPTION: (
'[EXPERIMENTAL]'
' Enable experimental support for PEP 740 attestations.'
' Only works with PyPI and TestPyPI via Trusted Publishing.'
),
REQUIRED: False,
},
},
'runs': {
'using': 'docker',
'image': image,
},
}
# The generated trampoline action must exist in the allowlisted
# runner-defined working directory so it can be referenced by the
# relative path starting with `./`.
#
# This mutates the end-user's workspace slightly but uses a path
# that is unlikely to clash with somebody else's use.
#
# We cannot use randomized paths because the composite action
# syntax does not allow accessing variables in `uses:`. This
# means that we end up having to hardcode this path both here and
# in `action.yml`.
action_path = pathlib.Path(
'.github/.tmp/.generated-actions/'
'run-pypi-publish-in-docker-container/action.yml',
)
action_path.parent.mkdir(parents=True, exist_ok=True)
action_path.write_text(json.dumps(action, ensure_ascii=False), encoding='utf-8')
-275
View File
@@ -1,275 +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}`
* `workflow_ref`: `{workflow_ref}`
* `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.
You can monitor PyPI's status here: https://status.python.org/
""" # 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'),
workflow_ref=_get('workflow_ref'),
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
+8 -8
View File
@@ -2,17 +2,17 @@ import hashlib
import pathlib
import sys
packages_dir = pathlib.Path(sys.argv[1]).resolve()
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("")
-35
View File
@@ -1,35 +0,0 @@
import pathlib
import sys
from packaging import utils
def debug(msg: str):
print(f'::debug::{msg.title()}', file=sys.stderr)
def safe_parse_pkg_name(file_path: pathlib.Path) -> str | None:
if file_path.suffix == '.whl':
try:
return utils.parse_wheel_filename(file_path.name)[0]
except utils.InvalidWheelFilename:
debug(f'Invalid wheel filename: {file_path.name}')
return None
elif file_path.suffix == '.gz':
try:
return utils.parse_sdist_filename(file_path.name)[0]
except utils.InvalidSdistFilename:
debug(f'Invalid sdist filename: {file_path.name}')
return None
return None
packages_dir = pathlib.Path(sys.argv[1]).resolve()
pkg_names = {
pkg_name for file_path in packages_dir.iterdir() if
(pkg_name := safe_parse_pkg_name(file_path)) is not None
}
for package_name in sorted(pkg_names):
print(package_name)
-20
View File
@@ -1,20 +0,0 @@
###############################################################################
# #
# This file is only meant to exclude broken dependency versions, not feature #
# dependencies. #
# #
# GUIDELINES: #
# 1. Only list PyPI project versions that need to be excluded using `!=` #
# and `<`. #
# 2. It is allowed to have transitive dependency limitations in this file. #
# 3. Apply bare minimum constraints under narrow conditions, use #
# environment markers if possible. E.g. `; python_version < "3.12"`. #
# 4. Whenever there are no constraints, let the file and this header #
# remain in Git. #
# #
###############################################################################
# NOTE: 1.12.0 and later enable support for metadata 2.4
# NOTE: This can be dropped once twine stops using pkginfo
# Ref: https://github.com/pypa/twine/pull/1180
pkginfo >= 1.12.0
-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
-20
View File
@@ -1,20 +0,0 @@
-c runtime-constraints.in # limits known broken versions
# NOTE: v6 is needed to support metadata v2.4
twine >= 6.0
# NOTE: Used to detect an ambient OIDC credential for OIDC publishing,
# NOTE: as well as PEP 740 attestations.
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
# NOTE: Used to generate attestations.
pypi-attestations ~= 0.0.15
sigstore ~= 3.5.1
# NOTE: Used to detect the PyPI package name from the distribution files
packaging
-153
View File
@@ -1,153 +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
betterproto==2.0.0b6
# via sigstore-protobuf-specs
certifi==2024.2.2
# via requests
cffi==1.16.0
# via cryptography
charset-normalizer==3.3.2
# via requests
cryptography==42.0.7
# via
# pyopenssl
# pypi-attestations
# secretstorage
# sigstore
dnspython==2.6.1
# via email-validator
docutils==0.21.2
# via readme-renderer
email-validator==2.1.1
# via pydantic
grpclib==0.4.7
# via betterproto
h2==4.1.0
# via grpclib
hpack==4.0.0
# via h2
hyperframe==6.0.1
# via h2
id==1.4.0
# via
# -r runtime.in
# sigstore
idna==3.7
# via
# email-validator
# requests
jaraco-classes==3.4.0
# via keyring
jaraco-context==5.3.0
# via keyring
jaraco-functools==4.0.1
# via keyring
jeepney==0.8.0
# via
# keyring
# secretstorage
keyring==25.2.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
# jaraco-functools
multidict==6.0.5
# via grpclib
nh3==0.2.17
# via readme-renderer
packaging==24.1
# via
# -r runtime.in
# pypi-attestations
# twine
pkginfo==1.12.0
# via
# -c runtime-constraints.in
# twine
platformdirs==4.2.2
# via sigstore
pyasn1==0.6.0
# via
# pypi-attestations
# sigstore
pycparser==2.22
# via cffi
pydantic==2.7.1
# via
# id
# pypi-attestations
# sigstore
# sigstore-rekor-types
pydantic-core==2.18.2
# via pydantic
pygments==2.18.0
# via
# readme-renderer
# rich
pyjwt==2.8.0
# via sigstore
pyopenssl==24.1.0
# via sigstore
pypi-attestations==0.0.15
# via -r runtime.in
python-dateutil==2.9.0.post0
# via betterproto
readme-renderer==43.0
# via twine
requests==2.32.3
# via
# -r runtime.in
# id
# requests-toolbelt
# sigstore
# tuf
# twine
requests-toolbelt==1.0.0
# via twine
rfc3986==2.0.0
# via twine
rfc8785==0.1.2
# via sigstore
rich==13.7.1
# via
# sigstore
# twine
secretstorage==3.3.3
# via keyring
securesystemslib==1.0.0
# via tuf
sigstore==3.5.1
# via
# -r runtime.in
# pypi-attestations
sigstore-protobuf-specs==0.3.2
# via
# pypi-attestations
# sigstore
sigstore-rekor-types==0.0.13
# via sigstore
six==1.16.0
# via python-dateutil
tuf==5.0.0
# via sigstore
twine==6.0.1
# via -r runtime.in
typing-extensions==4.11.0
# via
# pydantic
# pydantic-core
urllib3==2.2.1
# via
# requests
# twine
+47 -170
View File
@@ -1,172 +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')"
INPUT_ATTESTATIONS="$(get-normalized-input 'attestations')"
REPOSITORY_NAME="$(echo ${GITHUB_REPOSITORY} | cut -d'/' -f2)"
WORKFLOW_FILENAME="$(echo ${GITHUB_WORKFLOW_REF} | cut -d'/' -f5- | cut -d'@' -f1)"
PACKAGE_NAMES=()
while IFS='' read -r line; do PACKAGE_NAMES+=("$line"); done < <(python /app/print-pkg-names.py "${INPUT_PACKAGES_DIR%%/}")
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"
ATTESTATIONS_WITHOUT_TP_WARNING="::warning title=attestations input ignored::\
The workflow was run with the 'attestations: true' input, but an explicit \
password was also set, disabling Trusted Publishing. As a result, the \
attestations input is ignored."
ATTESTATIONS_WRONG_INDEX_WARNING="::warning title=attestations input ignored::\
The workflow was run with 'attestations: true' input, but the specified \
repository URL does not support PEP 740 attestations. As a result, the \
attestations input is ignored."
MAGIC_LINK_MESSAGE="A new Trusted Publisher for the currently running \
publishing workflow can be created by accessing the following link(s) while \
logged-in as an owner of the package(s):"
[[ "${INPUT_USER}" == "__token__" && -z "${INPUT_PASSWORD}" ]] \
&& TRUSTED_PUBLISHING=true || TRUSTED_PUBLISHING=false
if [[ "${TRUSTED_PUBLISHING}" == true || ! "${INPUT_REPOSITORY_URL}" =~ pypi\.org || ${#PACKAGE_NAMES[@]} -eq 0 ]] ; then
TRUSTED_PUBLISHING_MAGIC_LINK_NUDGE=""
else
if [[ "${INPUT_REPOSITORY_URL}" =~ test\.pypi\.org ]] ; then
INDEX_URL="https://test.pypi.org"
else
INDEX_URL="https://pypi.org"
fi
ALL_LINKS=""
for PACKAGE_NAME in "${PACKAGE_NAMES[@]}"; do
LINK="- ${INDEX_URL}/manage/project/${PACKAGE_NAME}/settings/publishing/?provider=github&owner=${GITHUB_REPOSITORY_OWNER}&repository=${REPOSITORY_NAME}&workflow_filename=${WORKFLOW_FILENAME}"
ALL_LINKS+="$LINK"$'\n'
done
# Construct the summary message without the warning header
MAGIC_LINK_MESSAGE_WITH_LINKS="${MAGIC_LINK_MESSAGE}"$'\n'"${ALL_LINKS}"
echo "${MAGIC_LINK_MESSAGE_WITH_LINKS}" >> $GITHUB_STEP_SUMMARY
# The actual nudge in the log is formatted as a warning
TRUSTED_PUBLISHING_MAGIC_LINK_NUDGE="::warning title=Create a Trusted Publisher::${MAGIC_LINK_MESSAGE_WITH_LINKS}"
fi
if [[ "${INPUT_ATTESTATIONS}" != "false" ]] ; then
# Setting `attestations: true` without Trusted Publishing indicates
# user confusion, since attestations (currently) require it.
if ! "${TRUSTED_PUBLISHING}" ; then
echo "${ATTESTATIONS_WITHOUT_TP_WARNING}"
INPUT_ATTESTATIONS="false"
fi
# Setting `attestations: true` with an index other than PyPI or TestPyPI
# indicates user confusion, since attestations are not supported on other
# indices presently.
if [[ ! "${INPUT_REPOSITORY_URL}" =~ pypi\.org ]] ; then
echo "${ATTESTATIONS_WRONG_INDEX_WARNING}"
INPUT_ATTESTATIONS="false"
fi
fi
if "${TRUSTED_PUBLISHING}" ; 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}"
echo "${TRUSTED_PUBLISHING_MAGIC_LINK_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}"
echo "${TRUSTED_PUBLISHING_MAGIC_LINK_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 && \
@@ -186,24 +72,15 @@ 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
TWINE_EXTRA_ARGS="--verbose $TWINE_EXTRA_ARGS"
fi
if [[ ${INPUT_ATTESTATIONS,,} != "false" ]] ; then
# NOTE: Intentionally placed after `twine check`, to prevent attestation
# NOTE: generation on distributions with invalid metadata.
echo "::notice::Generating and uploading digital attestations"
python /app/attestations.py "${INPUT_PACKAGES_DIR%%/}"
TWINE_EXTRA_ARGS="--attestations $TWINE_EXTRA_ARGS"
fi
if [[ ${INPUT_PRINT_HASH,,} != "false" || ${INPUT_VERBOSE,,} != "false" ]] ; then
python /app/print-hash.py ${INPUT_PACKAGES_DIR%%/}
fi