diff --git a/.github/actions/wait-for-pypi-version/action.yaml b/.github/actions/wait-for-pypi-version/action.yaml new file mode 100644 index 0000000..8a0b5d4 --- /dev/null +++ b/.github/actions/wait-for-pypi-version/action.yaml @@ -0,0 +1,93 @@ +name: 'Wait for PyPI version' +description: 'Wait for a specific package version to become available on PyPI or TestPyPI' +inputs: + repository: + description: 'PyPI repository type: "pypi" or "testpypi"' + required: true + package: + description: 'Package name' + required: true + version: + description: 'Package version to wait for' + required: true + max_attempts: + description: 'Maximum number of retry attempts' + required: false + default: '30' + wait_seconds: + description: 'Seconds to wait between attempts' + required: false + default: '10' + +runs: + using: composite + steps: + - name: Install requests + shell: bash + run: | + python -m pip install --upgrade pip + pip install requests + + - name: Wait for version to be available + shell: python + env: + REPOSITORY: ${{ inputs.repository }} + PACKAGE: ${{ inputs.package }} + VERSION: ${{ inputs.version }} + MAX_ATTEMPTS: ${{ inputs.max_attempts }} + WAIT_SECONDS: ${{ inputs.wait_seconds }} + run: | + import os + import sys + import time + + import requests + + repository = os.environ["REPOSITORY"].strip().lower() + package = os.environ["PACKAGE"] + version = os.environ["VERSION"] + max_attempts = int(os.environ.get("MAX_ATTEMPTS", "30")) + wait_seconds = int(os.environ.get("WAIT_SECONDS", "10")) + + if repository == "testpypi": + api_url = f"https://test.pypi.org/pypi/{package}/json" + repo_name = "TestPyPI" + elif repository == "pypi": + api_url = f"https://pypi.org/pypi/{package}/json" + repo_name = "PyPI" + else: + print( + f"ERROR: repository must be 'pypi' or 'testpypi', got {repository!r}", + file=sys.stderr, + ) + sys.exit(1) + + for attempt in range(max_attempts): + try: + r = requests.get(api_url, timeout=10) + r.raise_for_status() + data = r.json() + versions = data.get("releases", {}) + keys = list(versions.keys()) + print("Available versions:", keys[-10:]) # Show last 10 versions + if version in versions: + print(f"✓ Version {version} is available on {repo_name}") + print(f"Version {version} is now available on {repo_name}") + sys.exit(0) + print(f"✗ Version {version} is NOT available on {repo_name}") + except Exception as e: + print(f"Error checking version: {e}") + + current = attempt + 1 + print( + f"Attempt {current}/{max_attempts}: Version {version} not yet available " + f"on {repo_name}, waiting {wait_seconds} seconds..." + ) + time.sleep(wait_seconds) + + print( + f"ERROR: Version {version} did not become available on {repo_name} " + f"after {max_attempts} attempts", + file=sys.stderr, + ) + sys.exit(1) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index a3630d5..887200f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,4 +1,4 @@ -name: Build and upload to PyPi +name: Build and upload to PyPI on: push: @@ -8,52 +8,208 @@ on: types: - published +concurrency: + group: "${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }}" + cancel-in-progress: false + +defaults: + run: + shell: bash -l {0} + jobs: - test_pypi_push: + build: + name: Build package + if: github.repository == 'Becksteinlab/propkatraj' + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract-version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install "setuptools>=61.2,<81" build twine + + - name: Build package (wheel and sdist) + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Extract package version + id: extract-version + run: | + WHEEL_FILE=$(ls dist/*.whl) + VERSION=$(basename "$WHEEL_FILE" | sed -n 's/propkatraj-\([^-]*\)-.*/\1/p') + if [ -z "$VERSION" ]; then + pip install "$WHEEL_FILE" --quiet + VERSION=$(python -c "import propkatraj; print(propkatraj.__version__)") + pip uninstall -y propkatraj --quiet + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Extracted version: $VERSION" + + - name: Upload dist files + uses: actions/upload-artifact@v7 + with: + name: dist-files + path: dist/ + retention-days: 1 + + test-pytest: + name: Run tests + if: github.repository == 'Becksteinlab/propkatraj' + runs-on: ubuntu-latest + needs: build + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Download dist files + uses: actions/download-artifact@v8 + with: + name: dist-files + path: dist/ + + - name: Install wheel with test extras + run: | + python -m pip install --upgrade pip + WHEEL_FILE=$(ls dist/*.whl) + pip install "${WHEEL_FILE}[test]" + + - name: Test import + run: | + python -c "import propkatraj; print(f'Package {propkatraj.__version__} imported successfully')" + + - name: Run tests + run: pytest -v --pyargs propkatraj.tests + + deploy-testpypi: + name: Deploy to TestPyPI + if: | + github.repository == 'Becksteinlab/propkatraj' && + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + runs-on: ubuntu-latest + needs: [build, test-pytest] environment: name: TestPyPi url: https://test.pypi.org/p/propkatraj permissions: id-token: write + steps: + - name: Download dist files + uses: actions/download-artifact@v8 + with: + name: dist-files + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true + skip-existing: true + + deploy-pypi: + name: Deploy to PyPI if: | github.repository == 'Becksteinlab/propkatraj' && - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) - name: "TestPyPi: Build and upload pure Python wheels" + (github.event_name == 'release' && github.event.action == 'published') runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: testpypi_deploy - uses: MDAnalysis/pypi-deployment@main - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - with: - test_submission: true - package_name: 'propkatraj' - module_name: 'propkatraj' - test_deps: 'pytest MDAnalysisTests' - tests: true - - pypi_push: + needs: [build, test-pytest] environment: name: PyPi url: https://pypi.org/p/propkatraj permissions: id-token: write + steps: + - name: Download dist files + uses: actions/download-artifact@v8 + with: + name: dist-files + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + test-deployed-testpypi: + name: Test deployed package (TestPyPI) if: | github.repository == 'Becksteinlab/propkatraj' && - (github.event_name == 'release' && github.event.action == 'published') - name: "PyPi: Build and upload pure Python wheels" + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) runs-on: ubuntu-latest + needs: [build, deploy-testpypi] + steps: + - name: Checkout repository for actions + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Wait for version to be available on TestPyPI + uses: ./.github/actions/wait-for-pypi-version + with: + repository: testpypi + package: propkatraj + version: ${{ needs.build.outputs.version }} + + - name: Install from TestPyPI + run: | + python -m pip install --upgrade pip + pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "propkatraj[test]==${{ needs.build.outputs.version }}" + + - name: Test import + run: | + python -c "import propkatraj; print(f'Package {propkatraj.__version__} imported successfully from TestPyPI')" + + - name: Run tests + run: pytest -v --pyargs propkatraj.tests + test-deployed-pypi: + name: Test deployed package (PyPI) + if: | + github.repository == 'Becksteinlab/propkatraj' && + (github.event_name == 'release' && github.event.action == 'published') + runs-on: ubuntu-latest + needs: [build, deploy-pypi] steps: - - uses: actions/checkout@v4 - - - name: pypi_deploy - uses: MDAnalysis/pypi-deployment@main - if: github.event_name == 'release' && github.event.action == 'published' - with: - package_name: 'propkatraj' - module_name: 'propkatraj' - test_deps: 'pytest MDAnalysisTests' - tests: true + - name: Checkout repository for actions + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Wait for version to be available on PyPI + uses: ./.github/actions/wait-for-pypi-version + with: + repository: pypi + package: propkatraj + version: ${{ needs.build.outputs.version }} + + - name: Install from PyPI + run: | + python -m pip install --upgrade pip + pip install "propkatraj[test]==${{ needs.build.outputs.version }}" + + - name: Test import + run: | + python -c "import propkatraj; print(f'Package {propkatraj.__version__} imported successfully from PyPI')" + + - name: Run tests + run: pytest -v --pyargs propkatraj.tests