Publish opik-hermes (release) by @JetoPistola #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish | |
| run-name: "Publish opik-hermes (${{ github.event_name }}) by @${{ github.actor }}" | |
| # Two targets — deliberate, like the other Opik packages (no publish on every | |
| # push): | |
| # - TestPyPI: via manual `workflow_dispatch` with target=testpypi. Run it | |
| # from any branch to exercise the full build→upload→install flow before a | |
| # real release. Bump the version in pyproject.toml first so the upload | |
| # doesn't collide (TestPyPI/PyPI reject re-uploading a version — | |
| # skip-existing turns that into a no-op rather than a failure). | |
| # - PyPI: on a published GitHub Release (the real release), or dispatch with | |
| # target=pypi. Uses the org PYPI_API_TOKEN, same as the other Opik packages. | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| target: | |
| description: "Where to publish" | |
| type: choice | |
| options: [testpypi, pypi] | |
| default: testpypi | |
| permissions: | |
| contents: read | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| # On a real release the git tag (vX.Y.Z) and pyproject.toml's version | |
| # must agree — the wheel is built from pyproject, so a mismatch would | |
| # tag one version while `skip-existing` silently no-ops the upload of the | |
| # already-published one. Fail loudly instead. | |
| - name: Tag matches pyproject version (release only) | |
| if: github.event_name == 'release' | |
| run: | | |
| FILE_VER=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") | |
| TAG_VER="${GITHUB_REF_NAME#v}" | |
| if [ "$FILE_VER" != "$TAG_VER" ]; then | |
| echo "::error title=Version mismatch::Release tag v$TAG_VER != pyproject version $FILE_VER. Bump pyproject.toml to match the tag before publishing." | |
| exit 1 | |
| fi | |
| echo "OK: tag v$TAG_VER matches pyproject version $FILE_VER" | |
| - name: Build sdist + wheel | |
| run: | | |
| pip install -U pip build | |
| python -m build --sdist --wheel --outdir dist/ . | |
| - name: Verify wheel exposes the plugin entry point | |
| run: | | |
| python - <<'PY' | |
| import glob, zipfile | |
| wheel = glob.glob("dist/*.whl")[0] | |
| names = zipfile.ZipFile(wheel).namelist() | |
| assert "opik_hermes/__init__.py" in names, "plugin module missing from wheel" | |
| body = zipfile.ZipFile(wheel).read( | |
| next(n for n in names if n.endswith("entry_points.txt")) | |
| ).decode() | |
| assert "hermes_agent.plugins" in body and "opik_hermes:register" in body, ( | |
| "hermes_agent.plugins entry point missing" | |
| ) | |
| print("OK:", wheel) | |
| PY | |
| # --- Decide target ----------------------------------------------------- | |
| # release -> pypi | |
| # workflow_dispatch-> the chosen input | |
| # push (branch/PR) -> testpypi | |
| - name: Resolve publish target | |
| id: target | |
| run: | | |
| if [ "${{ github.event_name }}" = "release" ]; then | |
| echo "to=pypi" >> "$GITHUB_OUTPUT" | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "to=${{ inputs.target }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "to=testpypi" >> "$GITHUB_OUTPUT" | |
| fi | |
| # --- Token preflight --------------------------------------------------- | |
| # Fail fast with an actionable message rather than reaching the upload | |
| # step with a missing/malformed token. Probes the matching upload | |
| # endpoint: 400/422 = auth OK + empty payload (success for a preflight), | |
| # 401/403 = token rejected. | |
| - name: Token preflight | |
| env: | |
| TARGET: ${{ steps.target.outputs.to }} | |
| PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} | |
| TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} | |
| run: | | |
| set -e | |
| if [ "$TARGET" = "pypi" ]; then | |
| TOKEN="$PYPI_TOKEN"; URL="https://upload.pypi.org/legacy/"; NAME="PYPI_API_TOKEN" | |
| else | |
| TOKEN="$TEST_PYPI_TOKEN"; URL="https://test.pypi.org/legacy/"; NAME="TEST_PYPI_API_TOKEN" | |
| fi | |
| if [ -z "${TOKEN:-}" ]; then | |
| echo "::error title=Publish preflight::$NAME is empty. Add it to the repo/org secrets before publishing to $TARGET." | |
| exit 1 | |
| fi | |
| if [[ "$TOKEN" != pypi-* ]]; then | |
| echo "::error title=Publish preflight::$NAME does not start with 'pypi-'." | |
| exit 1 | |
| fi | |
| CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ | |
| -u "__token__:${TOKEN}" -H "User-Agent: opik-hermes-release-preflight" "$URL" || echo "000") | |
| echo "Preflight HTTP $CODE against $URL" | |
| case "$CODE" in | |
| 400|422|200) echo "Token accepted by $TARGET." ;; | |
| 401|403) echo "::error title=Publish preflight::$NAME rejected by $TARGET (HTTP $CODE)."; exit 1 ;; | |
| *) echo "::warning title=Publish preflight::Unexpected HTTP $CODE; proceeding to upload." ;; | |
| esac | |
| - name: Publish to TestPyPI | |
| if: steps.target.outputs.to == 'testpypi' | |
| uses: pypa/gh-action-pypi-publish@v1.12.4 | |
| with: | |
| password: ${{ secrets.TEST_PYPI_API_TOKEN }} | |
| repository-url: https://test.pypi.org/legacy/ | |
| skip-existing: true | |
| - name: Publish to PyPI | |
| if: steps.target.outputs.to == 'pypi' | |
| uses: pypa/gh-action-pypi-publish@v1.12.4 | |
| with: | |
| password: ${{ secrets.PYPI_API_TOKEN }} | |
| skip-existing: true |