mirror of
https://gitcode.com/gh_mirrors/gh/gh-action-pypi-publish.git
synced 2026-07-08 12:23:09 +00:00
Compare commits
4 Commits
release/v1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c1b3402824 | |||
| c5d8ac5008 | |||
| ffa46275f9 | |||
| 9b8e7336db |
+5
-15
@@ -1,16 +1,6 @@
|
||||
---
|
||||
|
||||
custom:
|
||||
- https://www.comebackalive.in.ua/donate
|
||||
- https://github.com/vshymanskyy/StandWithUkraine#for-maintainers-and-authors
|
||||
- https://www.paypal.me/webknjazCZ
|
||||
- https://webknjaz.me
|
||||
|
||||
github:
|
||||
- webknjaz
|
||||
|
||||
github: [webknjaz]
|
||||
patreon: webknjaz
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: webknjaz
|
||||
|
||||
liberapay: webknjaz
|
||||
|
||||
...
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
custom: https://www.paypal.me/webknjazCZ
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
**⚠️ Please do not file public GitHub issues for security
|
||||
vulnerabilities as they are open for everyone to see! ⚠️**
|
||||
|
||||
We encourage responsible disclosure practices for security
|
||||
vulnerabilities.
|
||||
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Always update to the latest version of
|
||||
this Action to keep up with security patches.
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you've found a security-related bug, we
|
||||
prefer that you fill out a [vulnerability report on GitHub]
|
||||
directly.
|
||||
|
||||
[vulnerability report on GitHub]:
|
||||
//github.com/pypa/gh-action-pypi-publish/security/advisories/new
|
||||
|
||||
|
||||
## Don't have a GitHub account?
|
||||
|
||||
Alternatively, drop an email to
|
||||
``wk+gh-action-pypi-publish-security`` at ``sydorenko`` dot
|
||||
``org`` dot ``ua`` instead of filing a ticket or posting to
|
||||
_any_ public groups. We will try to assess the problem in
|
||||
timely manner and disclose it in a responsible way.
|
||||
@@ -0,0 +1,14 @@
|
||||
Security Policy
|
||||
---------------
|
||||
|
||||
Supported Versions
|
||||
==================
|
||||
|
||||
Always update to the latest version of
|
||||
this Action to keep up with security patches.
|
||||
|
||||
Reporting a Vulnerability
|
||||
=========================
|
||||
|
||||
Email to ``wk+gh-action-pypi-publish-security``
|
||||
at ``sydorenko`` dot ``org`` dot ``ua``.
|
||||
@@ -1,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
|
||||
@@ -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/
|
||||
|
||||
...
|
||||
@@ -1,4 +0,0 @@
|
||||
[tool.pip-tools]
|
||||
allow-unsafe = true
|
||||
resolver = "backtracking"
|
||||
strip-extras = true
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
default_language_version: python3.11
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/asottile/add-trailing-comma.git
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
|
||||
- repo: https://github.com/PyCQA/isort.git
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
args:
|
||||
- --honor-noqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks.git
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema.git
|
||||
rev: 0.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
|
||||
|
||||
...
|
||||
@@ -1,10 +1,2 @@
|
||||
---
|
||||
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
indentation:
|
||||
level: error
|
||||
indent-sequences: false
|
||||
|
||||
...
|
||||
indentation:
|
||||
indent-sequences: false
|
||||
|
||||
+4
-19
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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
@@ -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("")
|
||||
|
||||
@@ -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)
|
||||
@@ -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 +0,0 @@
|
||||
pip-with-requires-python
|
||||
@@ -1,12 +0,0 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --config=../.pip-tools.toml --output-file=runtime-prerequisites.txt --strip-extras runtime-prerequisites.in
|
||||
#
|
||||
pip-with-requires-python==1.0.1
|
||||
# via -r runtime-prerequisites.in
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==24.0
|
||||
# via pip-with-requires-python
|
||||
@@ -1,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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user