From 8067dad90b9981defdbc04b964020d37b368970c Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Fri, 6 Mar 2026 18:08:56 +0000 Subject: [PATCH 1/4] Add workflow to test PyPI yank capability via org secret --- .github/workflows/test_pypi_yank.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/test_pypi_yank.yml diff --git a/.github/workflows/test_pypi_yank.yml b/.github/workflows/test_pypi_yank.yml new file mode 100644 index 000000000..ebb7e158b --- /dev/null +++ b/.github/workflows/test_pypi_yank.yml @@ -0,0 +1,19 @@ +name: Test PyPI yank capability + +on: + workflow_dispatch: + +jobs: + test-yank: + runs-on: ubuntu-latest + steps: + - name: Install twine + run: pip install twine + + - name: Attempt to yank policyengine-uk 0.35.0 + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI }} + run: | + pip install twine + twine yank policyengine-uk 0.35.0 --reason "Test: verifying org secret has yank permissions" || echo "YANK FAILED (exit $?)" From 8ec28abd32fc8e8f2ec31e19a8f8f905f91c3530 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Fri, 6 Mar 2026 18:14:21 +0000 Subject: [PATCH 2/4] Trigger workflow on branch push for testing --- .github/workflows/test_pypi_yank.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test_pypi_yank.yml b/.github/workflows/test_pypi_yank.yml index ebb7e158b..b1f6a3496 100644 --- a/.github/workflows/test_pypi_yank.yml +++ b/.github/workflows/test_pypi_yank.yml @@ -2,6 +2,9 @@ name: Test PyPI yank capability on: workflow_dispatch: + push: + branches: + - test-pypi-yank jobs: test-yank: From 6fdf663c3ae0855fb2d2374f9f911b6f18e868fb Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Fri, 6 Mar 2026 18:15:00 +0000 Subject: [PATCH 3/4] Fix: use PyPI REST API directly for yank (twine doesn't support yank) --- .github/workflows/test_pypi_yank.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_pypi_yank.yml b/.github/workflows/test_pypi_yank.yml index b1f6a3496..1dcafa455 100644 --- a/.github/workflows/test_pypi_yank.yml +++ b/.github/workflows/test_pypi_yank.yml @@ -15,8 +15,19 @@ jobs: - name: Attempt to yank policyengine-uk 0.35.0 env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI }} + PYPI_TOKEN: ${{ secrets.PYPI }} run: | - pip install twine - twine yank policyengine-uk 0.35.0 --reason "Test: verifying org secret has yank permissions" || echo "YANK FAILED (exit $?)" + STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST "https://pypi.org/pypi/policyengine-uk/0.35.0/yank" \ + -H "Authorization: Bearer $PYPI_TOKEN" \ + -d "reason=Test%3A+verifying+org+secret+has+yank+permissions") + echo "HTTP status: $STATUS" + if [ "$STATUS" = "200" ]; then + echo "YANK SUCCEEDED - token has yank permissions" + elif [ "$STATUS" = "403" ]; then + echo "YANK FAILED 403 - token lacks permission or wrong scope" + elif [ "$STATUS" = "401" ]; then + echo "YANK FAILED 401 - token authentication failed" + else + echo "YANK FAILED with unexpected status $STATUS" + fi From 199413f9b5c8d8fa6a63cb275029bf0a72c50697 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Fri, 6 Mar 2026 18:16:54 +0000 Subject: [PATCH 4/4] Expand to test all PyPI token capabilities: auth, upload, yank routes --- .github/workflows/test_pypi_yank.yml | 107 +++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test_pypi_yank.yml b/.github/workflows/test_pypi_yank.yml index 1dcafa455..94b57cd40 100644 --- a/.github/workflows/test_pypi_yank.yml +++ b/.github/workflows/test_pypi_yank.yml @@ -1,4 +1,4 @@ -name: Test PyPI yank capability +name: Test PyPI token capabilities on: workflow_dispatch: @@ -7,27 +7,102 @@ on: - test-pypi-yank jobs: - test-yank: + test-capabilities: runs-on: ubuntu-latest + env: + PYPI_TOKEN: ${{ secrets.PYPI }} steps: - - name: Install twine - run: pip install twine + - name: Install dependencies + run: pip install twine requests - - name: Attempt to yank policyengine-uk 0.35.0 - env: - PYPI_TOKEN: ${{ secrets.PYPI }} + - name: "Test 1: Verify token is valid (check auth via upload endpoint)" run: | - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -X POST "https://pypi.org/pypi/policyengine-uk/0.35.0/yank" \ - -H "Authorization: Bearer $PYPI_TOKEN" \ - -d "reason=Test%3A+verifying+org+secret+has+yank+permissions") - echo "HTTP status: $STATUS" + # POST with no files - a valid token gets 400 (bad request, missing file) + # an invalid token gets 403 + STATUS=$(curl -s -o /tmp/auth_test.txt -w "%{http_code}" \ + -X POST "https://upload.pypi.org/legacy/" \ + -u "__token__:$PYPI_TOKEN") + echo "Auth check status: $STATUS" + cat /tmp/auth_test.txt + if [ "$STATUS" = "400" ]; then + echo "RESULT: Token authenticated OK (400 = auth passed, bad request)" + elif [ "$STATUS" = "403" ]; then + echo "RESULT: Token REJECTED (403 = wrong token or no permission)" + else + echo "RESULT: Unexpected status $STATUS" + fi + + - name: "Test 2: Attempt upload of a dummy/existing version (tests publish scope)" + run: | + # We'll try uploading a fake dist — if token has upload scope we get 400 (bad file) + # If token is read-only we get 403 + echo "dummy" > /tmp/fake.tar.gz + STATUS=$(curl -s -o /tmp/upload_test.txt -w "%{http_code}" \ + -X POST "https://upload.pypi.org/legacy/" \ + -u "__token__:$PYPI_TOKEN" \ + -F ":action=file_upload" \ + -F "protocol_version=1" \ + -F "name=policyengine-uk" \ + -F "version=0.35.0" \ + -F "filetype=sdist" \ + -F "pyversion=source" \ + -F "content=@/tmp/fake.tar.gz;type=application/octet-stream") + echo "Upload test status: $STATUS" + cat /tmp/upload_test.txt + if [ "$STATUS" = "400" ]; then + echo "RESULT: Token has upload permission (400 = auth OK, file invalid)" + elif [ "$STATUS" = "403" ]; then + echo "RESULT: Token LACKS upload permission (403)" + fi + + - name: "Test 3: Attempt yank via warehouse internal route" + run: | + # PyPI warehouse exposes a CSRF-protected manage route; tokens don't work here + # but we test to confirm the response + STATUS=$(curl -s -o /tmp/yank_test.txt -w "%{http_code}" \ + -X POST "https://pypi.org/manage/project/policyengine-uk/release/0.35.0/yank/" \ + -H "Authorization: token $PYPI_TOKEN" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "yanked_reason=test") + echo "Yank attempt status: $STATUS" + cat /tmp/yank_test.txt | python3 -c "import sys; content=sys.stdin.read(); print(content[:300])" if [ "$STATUS" = "200" ]; then - echo "YANK SUCCEEDED - token has yank permissions" + echo "RESULT: YANK SUCCEEDED" + elif [ "$STATUS" = "302" ]; then + echo "RESULT: Redirect (may have worked or redirected to login)" elif [ "$STATUS" = "403" ]; then - echo "YANK FAILED 403 - token lacks permission or wrong scope" + echo "RESULT: Forbidden - token not accepted for yank" elif [ "$STATUS" = "401" ]; then - echo "YANK FAILED 401 - token authentication failed" + echo "RESULT: Unauthorized - token rejected" else - echo "YANK FAILED with unexpected status $STATUS" + echo "RESULT: Status $STATUS - token likely not accepted for web management routes" fi + + - name: "Test 4: Attempt yank via upload.pypi.org with :action=yank" + run: | + STATUS=$(curl -s -o /tmp/yank2_test.txt -w "%{http_code}" \ + -X POST "https://upload.pypi.org/legacy/" \ + -u "__token__:$PYPI_TOKEN" \ + -F ":action=yank" \ + -F "name=policyengine-uk" \ + -F "version=0.35.0" \ + -F "yanked_reason=test+yank+capability") + echo "Yank via upload endpoint status: $STATUS" + cat /tmp/yank2_test.txt | python3 -c "import sys; content=sys.stdin.read(); print(content[:300])" + if [ "$STATUS" = "200" ]; then + echo "RESULT: YANK SUCCEEDED via upload endpoint" + elif [ "$STATUS" = "400" ]; then + echo "RESULT: Auth OK but action not supported (400)" + elif [ "$STATUS" = "403" ]; then + echo "RESULT: Forbidden" + else + echo "RESULT: Status $STATUS" + fi + + - name: Summary + run: | + echo "==============================" + echo "Test summary:" + echo "See individual steps above for RESULT lines." + echo "Token prefix (first 10 chars):" + echo "$PYPI_TOKEN" | cut -c1-15