diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..13e6c85 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,136 @@ +name: release + +# Builds sdist + wheel and publishes to PyPI on `v*` tags using PyPI Trusted +# Publishing (OIDC) — no long-lived API token stored in this repo. On pull +# requests the same build runs as a dry-run: build, `twine check`, install +# the wheel into a throwaway venv, and import `Wave` from it. That dry-run +# job never touches PyPI (no `id-token` permission, no publish step). +# +# One-time setup before the first `v*` tag: register this repo + workflow as +# a Trusted Publisher on the `wave-sdk` PyPI project (see AGENTS.md / PR body +# for the exact fields — this workflow cannot self-register). + +on: + pull_request: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: build + dry-run check + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Install build tooling + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build sdist + wheel + run: python -m build + + - name: twine check + run: twine check dist/* + + - name: Create fresh venv (no repo on sys.path) + run: python -m venv "$RUNNER_TEMP/dry-run" + + - name: Install the built wheel + run: | + WHEEL=$(ls dist/*.whl) + "$RUNNER_TEMP/dry-run/bin/pip" install --upgrade pip + "$RUNNER_TEMP/dry-run/bin/pip" install "$WHEEL" + + - name: Import check (installed wheel, run away from the repo) + working-directory: ${{ runner.temp }}/dry-run + run: | + bin/python -c " + from wave_sdk import Wave + import wave_sdk + print('wave_sdk', wave_sdk.__version__, 'imported OK from', wave_sdk.__file__) + print('Wave facade:', Wave) + " + + - name: Upload dist + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: dist + path: dist/ + retention-days: 7 + + publish: + name: publish to PyPI + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + name: pypi + url: https://pypi.org/project/wave-sdk/ + permissions: + # OIDC token for PyPI Trusted Publishing — no API token secret needed. + id-token: write + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - name: Install build tooling + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build sdist + wheel + run: python -m build + + - name: Verify tag matches package version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "tag v$TAG_VERSION does not match pyproject.toml version $PKG_VERSION" + exit 1 + fi + + - name: Publish to PyPI (Trusted Publishing, OIDC) + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + + - name: Post-publish verification (PyPI + fresh install) + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + for i in 1 2 3 4 5 6 7 8; do + AVAILABLE=$(pip index versions wave-sdk 2>/dev/null | grep -o "$TAG_VERSION" || true) + if [ -n "$AVAILABLE" ]; then break; fi + echo "waiting for PyPI to index wave-sdk==$TAG_VERSION (attempt $i)" + sleep 15 + done + python -m venv "$RUNNER_TEMP/verify" + "$RUNNER_TEMP/verify/bin/pip" install --upgrade pip + "$RUNNER_TEMP/verify/bin/pip" install "wave-sdk==$TAG_VERSION" + "$RUNNER_TEMP/verify/bin/python" -c " + from wave_sdk import Wave + import wave_sdk + assert wave_sdk.__version__ == '$TAG_VERSION', wave_sdk.__version__ + print('verified wave-sdk', wave_sdk.__version__, 'installed from PyPI, Wave facade OK') + "