ci: add PyPI release workflow - #10
Conversation
Tag-triggered (v*): asserts the tag matches the pyproject version, builds with uv, re-runs the wheel-contents guard, then publishes to PyPI via trusted publishing (OIDC, pypi environment).
| - name: Assert tag matches pyproject version | ||
| run: | | ||
| TAG="${GITHUB_REF_NAME#v}" | ||
| VERSION="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')" | ||
| if [ "$TAG" != "$VERSION" ]; then | ||
| echo "Tag v$TAG does not match pyproject version $VERSION" >&2 | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
nit: extend the assertion to cover dbt/adapters/hotdata/__version__.py (not blocking).
The repo declares the version in two places. dbt/adapters/hotdata/__version__.py:1 also holds 0.2.0. This step compares the tag against pyproject.toml only.
A maintainer can bump pyproject.toml, tag, and pass this guard while __version__.py stays at the old value. The wheel then ships metadata version 0.3.0 next to a module reporting 0.2.0. dbt-core imports dbt.adapters.<name>.__version__ to print installed adapter versions, so dbt --version shows the stale number to users.
That module is not optional. Both workflows list it as a required wheel file, so the file reaches every install.
Suggested addition after the existing comparison:
MODVER="$(python3 -c 'ns={}; exec(open("dbt/adapters/hotdata/__version__.py").read(), ns); print(ns["version"])')"
if [ "$MODVER" != "$VERSION" ]; then
echo "__version__.py $MODVER does not match pyproject version $VERSION" >&2
exit 1
fi| required = [ | ||
| "dbt/adapters/hotdata/__init__.py", | ||
| "dbt/adapters/hotdata/__version__.py", | ||
| "dbt/adapters/hotdata/client.py", | ||
| "dbt/adapters/hotdata/column.py", | ||
| "dbt/adapters/hotdata/connections.py", | ||
| "dbt/adapters/hotdata/credentials.py", | ||
| "dbt/adapters/hotdata/impl.py", | ||
| "dbt/adapters/hotdata/relation.py", | ||
| "dbt/adapters/hotdata/seeds.py", | ||
| "dbt/include/hotdata/__init__.py", | ||
| "dbt/include/hotdata/dbt_project.yml", | ||
| "dbt/include/hotdata/profile_template.yml", | ||
| "dbt/include/hotdata/macros/adapters.sql", | ||
| "dbt/include/hotdata/macros/materializations/table.sql", | ||
| "dbt/include/hotdata/macros/materializations/incremental.sql", | ||
| "dbt/include/hotdata/macros/materializations/seed.sql", | ||
| "dbt/include/hotdata/macros/materializations/view.sql", | ||
| "dbt/include/hotdata/macros/materializations/snapshot.sql", | ||
| ] |
There was a problem hiding this comment.
nit: extract this list into a script shared with ci.yml (not blocking).
.github/workflows/ci.yml:48-67 holds an identical 18-entry list. Two copies drift apart over time.
Consider a future macro file added to the CI list but missed here. The release guard silently stops covering that file. The release guard is the more important of the two, because it inspects the exact artifact that ships to PyPI.
Move the script to scripts/assert_wheel_contents.py. Then both workflows run python3 scripts/assert_wheel_contents.py and share one list.
Summary
Adds
.github/workflows/release.yml, triggered by pushing av*tag:pyproject.toml's version (guards against tagging without bumping), runsuv build, and re-runs the same wheel-contents assertion as CI before anything shipsdist/to PyPI via trusted publishing (OIDC throughpypa/gh-action-pypi-publish), gated on thepypiGitHub environment — no API token secret neededOne-time setup required before the first release
dbt-hotdataproject name (pypi.org → Publishing → "Add a new pending publisher"): ownerhotdata-dev, repositorydbt-hotdata, workflowrelease.yml, environmentpypi. This claims the name on first publish.pypienvironment (optionally restricted to protected tags and with required reviewers for an approval gate).Then releasing is: bump
versioninpyproject.toml, merge, tagvX.Y.Z, push the tag.Once the first release lands,
hotdata-dev/data-engineeringcan swap its git pin for a plaindbt-hotdata>=X.Y.ZPyPI dependency.