-
-
Notifications
You must be signed in to change notification settings - Fork 89
/
twine-upload.sh
executable file
·214 lines (181 loc) · 8.02 KB
/
twine-upload.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#! /bin/bash
if [[ -n "${DEBUG}" ]]
then
set -x
fi
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}"
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
fi
if ( ! ls -A ${INPUT_PACKAGES_DIR%%/}/*.tar.gz &> /dev/null && \
! ls -A ${INPUT_PACKAGES_DIR%%/}/*.whl &> /dev/null )
then
echo \
::warning file='# >>' PyPA publish to PyPI GHA'%3A' \
MISSING DISTS \
'<< ':: \
It looks like there are no Python distribution packages to \
publish in the directory "'${INPUT_PACKAGES_DIR%%/}/'". \
Please verify that they are in place should you face this \
problem.
fi
if [[ ${INPUT_VERIFY_METADATA,,} != "false" ]] ; then
twine check ${INPUT_PACKAGES_DIR%%/}/*
fi
TWINE_EXTRA_ARGS=--disable-progress-bar
if [[ ${INPUT_SKIP_EXISTING,,} != "false" ]] ; then
TWINE_EXTRA_ARGS="${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
TWINE_USERNAME="$INPUT_USER" \
TWINE_PASSWORD="$INPUT_PASSWORD" \
TWINE_REPOSITORY_URL="$INPUT_REPOSITORY_URL" \
exec twine upload ${TWINE_EXTRA_ARGS} ${INPUT_PACKAGES_DIR%%/}/*