---
name: pypi
description: Bump (patch by default, minor when completing a milestone), tag, build, publish turbine-data + the three integration packages to PyPI; on a minor release, close the matching vX.Y milestone and roll unfinished work to the next. PYPI_TOKEN from .env.
---

```bash
set -e
LATEST=$(git tag --sort=-v:refname | head -1)
# BUMP=minor when this release COMPLETES a milestone (e.g. 0.12.2 -> 0.13.0, milestone v0.13).
# BUMP=patch (default) for a mid-cycle bug-fix release (0.13.0 -> 0.13.1).
BUMP="${BUMP:-patch}"
if [ "$BUMP" = "minor" ]; then
  V=$(echo "$LATEST" | awk -F. '{print $1"."$2+1".0"}')
else
  V=$(echo "$LATEST" | awk -F. '{print $1"."$2"."$3+1}')
fi
uv run pytest -q --no-cov
git tag -a "$V" -m "Release $V"
set -a && . ./.env && set +a
export NO_PROXY='*'

for PKG in . integrations/turbine-client integrations/dagster-turbine integrations/airflow-turbine; do
  echo "Building and publishing $PKG ($V)"
  (cd "$PKG" && rm -rf dist && uv build && UV_PUBLISH_TOKEN="$PYPI_TOKEN" uv publish)
done

git push origin "$V"
echo "Released $V"
```

## After publishing: close the milestone (on a minor / milestone release)

A **minor** release (`BUMP=minor`) completes its milestone — the milestone name is the version's
`vX.Y`, so `0.13.0` completes **`v0.13`**. A **patch** release is mid-cycle and closes nothing.

On a minor release, roll unfinished work forward, then close:

1. Shipped milestone = `v<major>.<minor>` of `$V` (`0.13.0` → `v0.13`); next = `v<major>.<minor+1>`
   (`v0.14`). Resolve their ids and list any open items still on the shipped one:
   ```
   glab api "groups/1254326/milestones?state=active"
   glab api "groups/1254326/milestones/<SHIPPED_MS_ID>/issues?state=opened"
   ```
   Open *epics* carrying the milestone won't appear here — check the epics board for any still-open epic on it.
2. Ensure the next milestone exists, else create it (no due date):
   ```
   glab api -X POST "groups/1254326/milestones" -f title="v0.14"
   ```
3. Move each unfinished item to the next milestone:
   - issue: `glab api -X PUT "projects/20196/issues/<iid>" -F milestone_id=<NEXT_MS_ID>`
   - epic:  `glab api graphql -f query='mutation { workItemUpdate(input:{id:"gid://gitlab/WorkItem/<work_item_id>", milestoneWidget:{milestoneId:"gid://gitlab/Milestone/<NEXT_MS_ID>"}}) { errors } }'`
4. Close the shipped milestone:
   ```
   glab api -X PUT "groups/1254326/milestones/<SHIPPED_MS_ID>" -f state_event=close
   ```

The closed milestone then shows exactly what shipped; everything unfinished rides forward on `v0.14`.

Notes:

- The integration `pyproject.toml`s use `[tool.hatch.version] source = "vcs"` with `raw-options = { root = "../.." }`, so the single repo-root tag drives all four wheel versions.
- `NO_PROXY='*'` dodges a phantom SOCKS proxy in `uv publish` (see [[reference-pypi-release-gotchas]]).
- A dirty working tree poisons hatch-vcs; the loop assumes the working tree is clean. Set `SETUPTOOLS_SCM_PRETEND_VERSION="$V"` if you must publish from a dirty tree.
- All four wheels publish under the same tag — clients track the same version line as the engine.
