diff --git a/Dockerfile b/Dockerfile index a308f4b..3f9f4b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,7 @@ 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 . diff --git a/print-pkg-names.py b/print-pkg-names.py new file mode 100644 index 0000000..e2eeb82 --- /dev/null +++ b/print-pkg-names.py @@ -0,0 +1,35 @@ +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) diff --git a/requirements/runtime.in b/requirements/runtime.in index 50f52b6..5158d5c 100644 --- a/requirements/runtime.in +++ b/requirements/runtime.in @@ -12,3 +12,6 @@ requests # NOTE: Used to generate attestations. pypi-attestations ~= 0.0.11 sigstore ~= 3.2.0 + +# NOTE: Used to detect the PyPI package name from the distribution files +packaging diff --git a/requirements/runtime.txt b/requirements/runtime.txt index d50cd5b..f3f2206 100644 --- a/requirements/runtime.txt +++ b/requirements/runtime.txt @@ -64,7 +64,9 @@ multidict==6.0.5 nh3==0.2.17 # via readme-renderer packaging==24.1 - # via pypi-attestations + # via + # -r runtime.in + # pypi-attestations pkginfo==1.10.0 # via twine platformdirs==4.2.2 diff --git a/twine-upload.sh b/twine-upload.sh index 12b57b2..fce4517 100755 --- a/twine-upload.sh +++ b/twine-upload.sh @@ -41,6 +41,11 @@ 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 \ @@ -64,6 +69,27 @@ 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="::warning title=Create a Trusted Publisher::\ +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):" + +if [[ ! "${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 + TRUSTED_PUBLISHING_MAGIC_LINK_NUDGE="${MAGIC_LINK_MESSAGE}"$'\n'"${ALL_LINKS}" + echo "${MAGIC_LINK_MESSAGE}" >> $GITHUB_STEP_SUMMARY +fi + [[ "${INPUT_USER}" == "__token__" && -z "${INPUT_PASSWORD}" ]] \ && TRUSTED_PUBLISHING=true || TRUSTED_PUBLISHING=false @@ -96,6 +122,7 @@ elif [[ "${INPUT_USER}" == '__token__' ]]; then if [[ "${INPUT_REPOSITORY_URL}" =~ pypi\.org ]]; then echo "${TRUSTED_PUBLISHING_NUDGE}" + echo "${TRUSTED_PUBLISHING_MAGIC_LINK_NUDGE}" fi else echo \ @@ -105,6 +132,7 @@ else 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