From fc7df7c3276ef93b754f5e9620de8e0717e012b8 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Tue, 6 Aug 2024 15:47:48 +0200 Subject: [PATCH 1/5] Add nudge message with magic link to create new Trusted Publisher --- Dockerfile | 1 + print-pkg-name.py | 31 +++++++++++++++++++++++++++++++ requirements/runtime.in | 3 +++ twine-upload.sh | 20 ++++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 print-pkg-name.py diff --git a/Dockerfile b/Dockerfile index a308f4b..f519d85 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-name.py . COPY oidc-exchange.py . COPY attestations.py . diff --git a/print-pkg-name.py b/print-pkg-name.py new file mode 100644 index 0000000..a975385 --- /dev/null +++ b/print-pkg-name.py @@ -0,0 +1,31 @@ +import pathlib +import sys + +from packaging import utils + + +def debug(msg: str): + print(f'::debug::{msg.title()}', file=sys.stderr) + + +packages_dir = pathlib.Path(sys.argv[1]).resolve().absolute() + +wheel_file_names = [ + f.name for f in packages_dir.iterdir() if f.suffix == '.whl' +] +sdist_file_names = [ + f.name for f in packages_dir.iterdir() if f.suffix == '.gz' +] + +# Parse the package name from the distribution files and print it. On error, +# don't print anything. +if wheel_file_names: + try: + print(utils.parse_wheel_filename(wheel_file_names[0])[0]) + except utils.InvalidWheelFilename: + debug(f'Invalid wheel filename: {wheel_file_names[0]}') +elif sdist_file_names: + try: + print(utils.parse_sdist_filename(sdist_file_names[0])[0]) + except utils.InvalidSdistFilename: + debug(f'Invalid sdist filename: {sdist_file_names[0]}') 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/twine-upload.sh b/twine-upload.sh index 12b57b2..9fcfd91 100755 --- a/twine-upload.sh +++ b/twine-upload.sh @@ -41,6 +41,10 @@ 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_NAME="$(python /app/print-pkg-name.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 +68,20 @@ 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." +if [[ ! "${INPUT_REPOSITORY_URL}" =~ pypi\.org || -z "${PACKAGE_NAME}" ]] ; 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 + TRUSTED_PUBLISHING_MAGIC_LINK_NUDGE="::warning title=Create a Trusted Publisher::\ +A new Trusted Publisher for the currently running publishing workflow can be created \ +by accessing the following link while logged-in as a maintainer of the package: \ +${INDEX_URL}/manage/project/${PACKAGE_NAME}/settings/publishing/?provider=github&owner=${GITHUB_REPOSITORY_OWNER}&repository=${REPOSITORY_NAME}&workflow_filename=${WORKFLOW_FILENAME}" +fi + [[ "${INPUT_USER}" == "__token__" && -z "${INPUT_PASSWORD}" ]] \ && TRUSTED_PUBLISHING=true || TRUSTED_PUBLISHING=false @@ -96,6 +114,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 +124,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 From 7d02474954789c110d09bb1072e58dae03ff56b9 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 7 Aug 2024 17:54:38 +0200 Subject: [PATCH 2/5] Support multiple packages --- Dockerfile | 2 +- print-pkg-name.py | 31 ------------------------------- print-pkg-names.py | 33 +++++++++++++++++++++++++++++++++ twine-upload.sh | 14 ++++++++++---- 4 files changed, 44 insertions(+), 36 deletions(-) delete mode 100644 print-pkg-name.py create mode 100644 print-pkg-names.py diff --git a/Dockerfile b/Dockerfile index f519d85..3f9f4b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ WORKDIR /app COPY LICENSE.md . COPY twine-upload.sh . COPY print-hash.py . -COPY print-pkg-name.py . +COPY print-pkg-names.py . COPY oidc-exchange.py . COPY attestations.py . diff --git a/print-pkg-name.py b/print-pkg-name.py deleted file mode 100644 index a975385..0000000 --- a/print-pkg-name.py +++ /dev/null @@ -1,31 +0,0 @@ -import pathlib -import sys - -from packaging import utils - - -def debug(msg: str): - print(f'::debug::{msg.title()}', file=sys.stderr) - - -packages_dir = pathlib.Path(sys.argv[1]).resolve().absolute() - -wheel_file_names = [ - f.name for f in packages_dir.iterdir() if f.suffix == '.whl' -] -sdist_file_names = [ - f.name for f in packages_dir.iterdir() if f.suffix == '.gz' -] - -# Parse the package name from the distribution files and print it. On error, -# don't print anything. -if wheel_file_names: - try: - print(utils.parse_wheel_filename(wheel_file_names[0])[0]) - except utils.InvalidWheelFilename: - debug(f'Invalid wheel filename: {wheel_file_names[0]}') -elif sdist_file_names: - try: - print(utils.parse_sdist_filename(sdist_file_names[0])[0]) - except utils.InvalidSdistFilename: - debug(f'Invalid sdist filename: {sdist_file_names[0]}') diff --git a/print-pkg-names.py b/print-pkg-names.py new file mode 100644 index 0000000..d01d186 --- /dev/null +++ b/print-pkg-names.py @@ -0,0 +1,33 @@ +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().absolute() + +pkg_names = {safe_parse_pkg_name(f) for f in packages_dir.iterdir()} +pkg_names.discard(None) + +for p in pkg_names: + print(p) diff --git a/twine-upload.sh b/twine-upload.sh index 9fcfd91..c8b224d 100755 --- a/twine-upload.sh +++ b/twine-upload.sh @@ -43,7 +43,8 @@ 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_NAME="$(python /app/print-pkg-name.py ${INPUT_PACKAGES_DIR%%/})" +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 \ @@ -68,7 +69,7 @@ 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." -if [[ ! "${INPUT_REPOSITORY_URL}" =~ pypi\.org || -z "${PACKAGE_NAME}" ]] ; then +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 @@ -76,10 +77,15 @@ else 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="::warning title=Create a Trusted Publisher::\ A new Trusted Publisher for the currently running publishing workflow can be created \ -by accessing the following link while logged-in as a maintainer of the package: \ -${INDEX_URL}/manage/project/${PACKAGE_NAME}/settings/publishing/?provider=github&owner=${GITHUB_REPOSITORY_OWNER}&repository=${REPOSITORY_NAME}&workflow_filename=${WORKFLOW_FILENAME}" +by accessing the following link(s) while logged-in as a maintainer of the package(s): \" +${ALL_LINKS}" fi [[ "${INPUT_USER}" == "__token__" && -z "${INPUT_PASSWORD}" ]] \ From 5e98273064240b9ce290be026bb0866e73d7a74d Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 7 Aug 2024 18:47:15 +0200 Subject: [PATCH 3/5] Add magic links to step summary --- print-pkg-names.py | 2 +- requirements/runtime.txt | 4 +++- twine-upload.sh | 12 +++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/print-pkg-names.py b/print-pkg-names.py index d01d186..c13d0e8 100644 --- a/print-pkg-names.py +++ b/print-pkg-names.py @@ -24,7 +24,7 @@ def safe_parse_pkg_name(file_path: pathlib.Path) -> str | None: return None -packages_dir = pathlib.Path(sys.argv[1]).resolve().absolute() +packages_dir = pathlib.Path(sys.argv[1]).resolve() pkg_names = {safe_parse_pkg_name(f) for f in packages_dir.iterdir()} pkg_names.discard(None) 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 c8b224d..fce4517 100755 --- a/twine-upload.sh +++ b/twine-upload.sh @@ -69,6 +69,10 @@ 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 @@ -79,13 +83,11 @@ else 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}" + 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="::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 a maintainer of the package(s): \" -${ALL_LINKS}" + 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}" ]] \ From ed33c5173d5ceae01f8339754b83a43a047d0ba5 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 4 Sep 2024 15:50:57 +0200 Subject: [PATCH 4/5] Update print-pkg-names.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- print-pkg-names.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/print-pkg-names.py b/print-pkg-names.py index c13d0e8..bf3ff99 100644 --- a/print-pkg-names.py +++ b/print-pkg-names.py @@ -26,8 +26,10 @@ def safe_parse_pkg_name(file_path: pathlib.Path) -> str | None: packages_dir = pathlib.Path(sys.argv[1]).resolve() -pkg_names = {safe_parse_pkg_name(f) for f in packages_dir.iterdir()} -pkg_names.discard(None) +pkg_names = { + pkg_name for file_path in packages_dir.iterdir() + if (pkg_name := safe_parse_pkg_name(file_path)) is not None +} -for p in pkg_names: - print(p) +for package_name in sorted(pkg_names): + print(package_name) From 199297163e5734127a12b854de5edc6ee20c10c3 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Wed, 4 Sep 2024 15:54:59 +0200 Subject: [PATCH 5/5] Fix lint warning --- print-pkg-names.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/print-pkg-names.py b/print-pkg-names.py index bf3ff99..e2eeb82 100644 --- a/print-pkg-names.py +++ b/print-pkg-names.py @@ -27,8 +27,8 @@ def safe_parse_pkg_name(file_path: pathlib.Path) -> str | 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 + 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):