diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 704ef862..87a0d456 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -232,7 +232,7 @@ jobs: shell: sh env: SET_ENV_SCRIPT: IyAvLy8gc2NyaXB0CiMgcmVxdWlyZXMtcHl0aG9uID0gIj09My4xMiIKIyBkZXBlbmRlbmNpZXMgPSBbCiMgICAgICJweXlhbWw9PTYuMC4yIiwKIyBdCiMgLy8vCmltcG9ydCBqc29uCmltcG9ydCBvcwppbXBvcnQgc3lzCgppbXBvcnQgeWFtbAoKR0lUSFVCX0VOViA9IG9zLmdldGVudigiR0lUSFVCX0VOViIpCmlmIEdJVEhVQl9FTlYgaXMgTm9uZToKICAgIHJhaXNlIFZhbHVlRXJyb3IoIkdJVEhVQl9FTlYgbm90IHNldC4gTXVzdCBiZSBydW4gaW5zaWRlIEdpdEh1YiBBY3Rpb25zLiIpCgpERUxJTUlURVIgPSAiRU9GIgoKCmRlZiBzZXRfZW52KGVudik6CgogICAgZW52ID0geWFtbC5sb2FkKGVudiwgTG9hZGVyPXlhbWwuQmFzZUxvYWRlcikKICAgIHByaW50KGpzb24uZHVtcHMoZW52LCBpbmRlbnQ9MikpCgogICAgaWYgbm90IGlzaW5zdGFuY2UoZW52LCBkaWN0KToKICAgICAgICB0aXRsZSA9ICJgZW52YCBtdXN0IGJlIG1hcHBpbmciCiAgICAgICAgbWVzc2FnZSA9IGYiYGVudmAgbXVzdCBiZSBtYXBwaW5nIG9mIGVudiB2YXJpYWJsZXMgdG8gdmFsdWVzLCBnb3QgdHlwZSB7dHlwZShlbnYpfSIKICAgICAgICBwcmludChmIjo6ZXJyb3IgdGl0bGU9e3RpdGxlfTo6e21lc3NhZ2V9IikKICAgICAgICBleGl0KDEpCgogICAgZm9yIGssIHYgaW4gZW52Lml0ZW1zKCk6CgogICAgICAgIGlmIG5vdCBpc2luc3RhbmNlKHYsIHN0cik6CiAgICAgICAgICAgIHRpdGxlID0gImBlbnZgIHZhbHVlcyBtdXN0IGJlIHN0cmluZ3MiCiAgICAgICAgICAgIG1lc3NhZ2UgPSBmImBlbnZgIHZhbHVlcyBtdXN0IGJlIHN0cmluZ3MsIGJ1dCB2YWx1ZSBvZiB7a30gaGFzIHR5cGUge3R5cGUodil9IgogICAgICAgICAgICBwcmludChmIjo6ZXJyb3IgdGl0bGU9e3RpdGxlfTo6e21lc3NhZ2V9IikKICAgICAgICAgICAgZXhpdCgxKQoKICAgICAgICB2ID0gdi5zcGxpdCgiXG4iKQoKICAgICAgICB3aXRoIG9wZW4oR0lUSFVCX0VOViwgImEiKSBhcyBmOgogICAgICAgICAgICBpZiBsZW4odikgPT0gMToKICAgICAgICAgICAgICAgIGYud3JpdGUoZiJ7a309e3ZbMF19XG4iKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgZm9yIGxpbmUgaW4gdjoKICAgICAgICAgICAgICAgICAgICBhc3NlcnQgbGluZS5zdHJpcCgpICE9IERFTElNSVRFUgogICAgICAgICAgICAgICAgZi53cml0ZShmIntrfTw8e0RFTElNSVRFUn1cbiIpCiAgICAgICAgICAgICAgICBmb3IgbGluZSBpbiB2OgogICAgICAgICAgICAgICAgICAgIGYud3JpdGUoZiJ7bGluZX1cbiIpCiAgICAgICAgICAgICAgICBmLndyaXRlKGYie0RFTElNSVRFUn1cbiIpCgogICAgICAgIHByaW50KGYie2t9IHdyaXR0ZW4gdG8gR0lUSFVCX0VOViIpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHNldF9lbnYoc3lzLmFyZ3ZbMV0pCg== - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 lfs: true diff --git a/.github/workflows/publish_pure_python.yml b/.github/workflows/publish_pure_python.yml index 938fcf3c..2ff89963 100644 --- a/.github/workflows/publish_pure_python.yml +++ b/.github/workflows/publish_pure_python.yml @@ -109,7 +109,7 @@ jobs: shell: sh env: SET_ENV_SCRIPT: IyAvLy8gc2NyaXB0CiMgcmVxdWlyZXMtcHl0aG9uID0gIj09My4xMiIKIyBkZXBlbmRlbmNpZXMgPSBbCiMgICAgICJweXlhbWw9PTYuMC4yIiwKIyBdCiMgLy8vCmltcG9ydCBqc29uCmltcG9ydCBvcwppbXBvcnQgc3lzCgppbXBvcnQgeWFtbAoKR0lUSFVCX0VOViA9IG9zLmdldGVudigiR0lUSFVCX0VOViIpCmlmIEdJVEhVQl9FTlYgaXMgTm9uZToKICAgIHJhaXNlIFZhbHVlRXJyb3IoIkdJVEhVQl9FTlYgbm90IHNldC4gTXVzdCBiZSBydW4gaW5zaWRlIEdpdEh1YiBBY3Rpb25zLiIpCgpERUxJTUlURVIgPSAiRU9GIgoKCmRlZiBzZXRfZW52KGVudik6CgogICAgZW52ID0geWFtbC5sb2FkKGVudiwgTG9hZGVyPXlhbWwuQmFzZUxvYWRlcikKICAgIHByaW50KGpzb24uZHVtcHMoZW52LCBpbmRlbnQ9MikpCgogICAgaWYgbm90IGlzaW5zdGFuY2UoZW52LCBkaWN0KToKICAgICAgICB0aXRsZSA9ICJgZW52YCBtdXN0IGJlIG1hcHBpbmciCiAgICAgICAgbWVzc2FnZSA9IGYiYGVudmAgbXVzdCBiZSBtYXBwaW5nIG9mIGVudiB2YXJpYWJsZXMgdG8gdmFsdWVzLCBnb3QgdHlwZSB7dHlwZShlbnYpfSIKICAgICAgICBwcmludChmIjo6ZXJyb3IgdGl0bGU9e3RpdGxlfTo6e21lc3NhZ2V9IikKICAgICAgICBleGl0KDEpCgogICAgZm9yIGssIHYgaW4gZW52Lml0ZW1zKCk6CgogICAgICAgIGlmIG5vdCBpc2luc3RhbmNlKHYsIHN0cik6CiAgICAgICAgICAgIHRpdGxlID0gImBlbnZgIHZhbHVlcyBtdXN0IGJlIHN0cmluZ3MiCiAgICAgICAgICAgIG1lc3NhZ2UgPSBmImBlbnZgIHZhbHVlcyBtdXN0IGJlIHN0cmluZ3MsIGJ1dCB2YWx1ZSBvZiB7a30gaGFzIHR5cGUge3R5cGUodil9IgogICAgICAgICAgICBwcmludChmIjo6ZXJyb3IgdGl0bGU9e3RpdGxlfTo6e21lc3NhZ2V9IikKICAgICAgICAgICAgZXhpdCgxKQoKICAgICAgICB2ID0gdi5zcGxpdCgiXG4iKQoKICAgICAgICB3aXRoIG9wZW4oR0lUSFVCX0VOViwgImEiKSBhcyBmOgogICAgICAgICAgICBpZiBsZW4odikgPT0gMToKICAgICAgICAgICAgICAgIGYud3JpdGUoZiJ7a309e3ZbMF19XG4iKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgZm9yIGxpbmUgaW4gdjoKICAgICAgICAgICAgICAgICAgICBhc3NlcnQgbGluZS5zdHJpcCgpICE9IERFTElNSVRFUgogICAgICAgICAgICAgICAgZi53cml0ZShmIntrfTw8e0RFTElNSVRFUn1cbiIpCiAgICAgICAgICAgICAgICBmb3IgbGluZSBpbiB2OgogICAgICAgICAgICAgICAgICAgIGYud3JpdGUoZiJ7bGluZX1cbiIpCiAgICAgICAgICAgICAgICBmLndyaXRlKGYie0RFTElNSVRFUn1cbiIpCgogICAgICAgIHByaW50KGYie2t9IHdyaXR0ZW4gdG8gR0lUSFVCX0VOViIpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHNldF9lbnYoc3lzLmFyZ3ZbMV0pCg== - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 lfs: true diff --git a/.github/workflows/test_publish_to_testpypi.yml b/.github/workflows/test_publish_to_testpypi.yml new file mode 100644 index 00000000..c5b410e1 --- /dev/null +++ b/.github/workflows/test_publish_to_testpypi.yml @@ -0,0 +1,21 @@ +name: Test publish to Test PyPI + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test_publish: + uses: ./.github/workflows/publish.yml + with: + test_groups: test, concurrency + test_extras: recommended + test_command: pytest --pyargs test_package + repository_url: https://test.pypi.org/legacy/ + upload_to_pypi: 'true' + timeout-minutes: 30 + secrets: + pypi_token: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/a/b/c/test.txt b/a/b/c/test.txt new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index 5dda3f19..574c5b08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,11 @@ test = [ "hypothesis>=6.113.0", "pytest>=8.3.5", ] +publish = [ + {include-group = "test"}, + "build>=1.0.0", + "twine>=5.0.0", +] [tool.setuptools] include-package-data = false diff --git a/test_package/tests/test_publish_pypi.py b/test_package/tests/test_publish_pypi.py new file mode 100644 index 00000000..9f6ffeb5 --- /dev/null +++ b/test_package/tests/test_publish_pypi.py @@ -0,0 +1,81 @@ +import os +import subprocess +import sys +from pathlib import Path + + +def test_build_package(): + """Test that the package can be built successfully.""" + result = subprocess.run( + [sys.executable, "-m", "build", "--outdir", "dist"], + capture_output=True, + text=True + ) + assert result.returncode == 0, f"Build failed: {result.stderr}" + + # Check that distribution files were created + dist_dir = Path("dist") + assert dist_dir.exists(), "dist directory not created" + + dist_files = list(dist_dir.glob("*.whl")) + list(dist_dir.glob("*.tar.gz")) + assert len(dist_files) > 0, "No distribution files created" + + +def test_publish_to_test_pypi(): + """Test publishing package to Test PyPI. + + This test requires TWINE_USERNAME and TWINE_PASSWORD environment variables + to be set for Test PyPI authentication. + + To run this test: + 1. Set TWINE_USERNAME (usually '__token__') + 2. Set TWINE_PASSWORD (your Test PyPI API token) + 3. Run: pytest test_package/tests/test_publish_pypi.py::test_publish_to_test_pypi + """ + # Skip if credentials are not available + if not os.getenv("TWINE_USERNAME") or not os.getenv("TWINE_PASSWORD"): + import pytest + pytest.skip("TWINE_USERNAME and TWINE_PASSWORD not set") + + # First build the package + build_result = subprocess.run( + [sys.executable, "-m", "build", "--outdir", "dist"], + capture_output=True, + text=True + ) + assert build_result.returncode == 0, f"Build failed: {build_result.stderr}" + + # Upload to Test PyPI + upload_result = subprocess.run( + [ + sys.executable, "-m", "twine", "upload", + "--repository", "testpypi", + "--skip-existing", + "dist/*" + ], + capture_output=True, + text=True + ) + + # Check result (skip-existing means it's ok if already uploaded) + assert upload_result.returncode == 0, f"Upload failed: {upload_result.stderr}" + print(f"Upload output: {upload_result.stdout}") + + +def test_check_package_metadata(): + """Test that package metadata is valid for PyPI.""" + # Build first + subprocess.run( + [sys.executable, "-m", "build", "--outdir", "dist"], + capture_output=True, + text=True + ) + + # Check with twine + result = subprocess.run( + [sys.executable, "-m", "twine", "check", "dist/*"], + capture_output=True, + text=True + ) + assert result.returncode == 0, f"Package check failed: {result.stderr}" + assert "PASSED" in result.stdout, "Package metadata validation failed" diff --git a/tox.ini b/tox.ini index bfb25691..407ff4b4 100644 --- a/tox.ini +++ b/tox.ini @@ -63,3 +63,17 @@ commands = conda: python -c "import os, sys; assert os.path.exists(os.path.join(sys.prefix, 'conda-meta', 'history'))" conda: micromamba list pytest --pyargs test_package {posargs} + +[testenv:publish-test] +description = test building and publishing to Test PyPI +skip_install = false +dependency_groups = publish +passenv = + TWINE_USERNAME + TWINE_PASSWORD +commands = + # Run build and metadata check tests (always) + pytest test_package/tests/test_publish_pypi.py::test_build_package -v + pytest test_package/tests/test_publish_pypi.py::test_check_package_metadata -v + # Run actual publish test (only if credentials are set) + pytest test_package/tests/test_publish_pypi.py::test_publish_to_test_pypi -v