diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dd364be --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: Release + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Verify release tag matches package version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + python - <<'PY' + import os + import tomllib + from pathlib import Path + + pyproject = tomllib.loads(Path("pyproject.toml").read_text()) + version = pyproject["project"]["version"] + expected_tag = f"v{version}" + release_tag = os.environ["RELEASE_TAG"] + + if release_tag != expected_tag: + raise SystemExit( + f"Release tag {release_tag!r} must match pyproject version {version!r} " + f"as {expected_tag!r}." + ) + PY + + - name: Build and check package + run: | + python -m pip install --upgrade pip + python -m pip install build twine + python -m build --sdist --wheel + python -m twine check dist/* + + - name: Upload distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + if-no-files-found: error + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/dbt-spec-kit/ + permissions: + contents: read + id-token: write + steps: + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb5df3..7415ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Jaffle-shop AI SDLC walkthrough using the upstream dbt Labs project as the onboarding demo. - Team onboarding playbook for introducing dbt-spec-kit to analytics engineering teams. - Documentation tests for README links, walkthrough commands, and OSS file presence. +- PyPI Trusted Publishing release workflow and maintainer release runbook. ### Changed - Reworked README around the jaffle-shop quickstart, team adoption path, and CI trust boundary. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 824df61..648d68b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,14 @@ All three must pass before opening a PR. Use the pull request template and include the smallest reproducible example for behavior changes. +## Release changes + +Release PRs must keep `pyproject.toml`, `CHANGELOG.md`, and the Git tag aligned. Publishing is +handled by `.github/workflows/release.yml` through PyPI Trusted Publishing, so maintainers should +not add PyPI API tokens or passwords to GitHub secrets. + +See `docs/releasing.md` for the full release runbook. + ## How to add a warehouse preset 1. Create `presets//constitution-additions.md`. Append-only — do not repeal base principles. diff --git a/README.md b/README.md index 5a903a0..d70739b 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,15 @@ See the full walkthrough: [Jaffle-shop AI SDLC walkthrough](docs/jaffle-shop-ai- ## Install -Requires Python 3.11+. Recommended via [uv](https://docs.astral.sh/uv/): +Requires Python 3.11+. Recommended via [uv](https://docs.astral.sh/uv/). + +After the first PyPI release: + +```bash +uvx --from dbt-spec-kit dbt-specify init my-project --warehouse snowflake +``` + +From the GitHub source before the PyPI release: ```bash uvx --from git+https://github.com/duckcode-ai/dbt-spec-kit.git \ @@ -66,7 +74,7 @@ uvx --from git+https://github.com/duckcode-ai/dbt-spec-kit.git \ Persistent install: ```bash -uv tool install dbt-spec-kit --from git+https://github.com/duckcode-ai/dbt-spec-kit.git +uv tool install dbt-spec-kit dbt-specify --version ``` @@ -121,6 +129,7 @@ Use `dbt-specify ci` when the lifecycle and dbt artifact checks should block a P - [Enterprise CI](docs/enterprise-ci.md) - [Brownfield onboarding](docs/brownfield-onboarding.md) - [EARS cheatsheet](docs/ears-cheatsheet.md) +- [Releasing to PyPI](docs/releasing.md) - [Snowflake guide](docs/warehouse-guides/snowflake.md) - [Databricks guide](docs/warehouse-guides/databricks.md) - [Trino guide](docs/warehouse-guides/trino.md) diff --git a/docs/enterprise-ci.md b/docs/enterprise-ci.md index 8f974ad..92900dd 100644 --- a/docs/enterprise-ci.md +++ b/docs/enterprise-ci.md @@ -41,6 +41,14 @@ See `.dbt-specify/templates/ci/github-actions-dbt-specify.yml` after running ini `templates/ci/github-actions-dbt-specify.yml` in this repo, for a reusable starting point. The workflow assumes your dbt adapter and dbt profile are already available through your CI setup. +## Package release workflow + +This repository publishes the `dbt-spec-kit` Python package with PyPI Trusted Publishing through +`.github/workflows/release.yml`. The workflow builds source and wheel distributions, checks them +with Twine, and publishes from the GitHub `pypi` environment without storing a PyPI token. + +See [Releasing to PyPI](releasing.md) for the maintainer runbook. + ## PR evidence example For the jaffle-shop semantic mart walkthrough, the PR should include: diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..61a5b91 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,83 @@ +# Releasing to PyPI + +dbt-spec-kit publishes to PyPI through PyPI Trusted Publishing. The release workflow does not use a +PyPI password or API token. GitHub Actions requests a short-lived OIDC token, PyPI verifies the +repository, workflow, and environment claims, and then accepts the package upload. + +## One-time setup + +Configure these two systems before publishing the first release. + +### PyPI + +Create a pending Trusted Publisher for the package: + +- PyPI project name: `dbt-spec-kit` +- Owner: `duckcode-ai` +- Repository name: `dbt-spec-kit` +- Workflow name: `release.yml` +- Environment name: `pypi` + +Pending publishers can create the PyPI project on first publish, but they do not reserve the name +before that first successful upload. + +### GitHub + +In the GitHub repository, create an environment named `pypi`. + +Recommended controls: + +- Require reviewer approval for deployments to the environment. +- Limit who can approve the environment to the package maintainers. +- Restrict deployments to protected release refs when the repository policy supports it. + +The workflow file must live at `.github/workflows/release.yml`, because PyPI checks the workflow +filename against the Trusted Publisher configuration. + +## Release process + +1. Update `pyproject.toml` to the release version. +2. Move the relevant `CHANGELOG.md` notes from `Unreleased` into the release section. +3. Merge the release PR to `main`. +4. Create and push a tag that exactly matches the package version, prefixed with `v`. + +```bash +git checkout main +git pull --ff-only origin main +git tag v1.0.0 +git push origin v1.0.0 +``` + +5. Create a GitHub Release from that tag and publish it. +6. Approve the `pypi` environment deployment if GitHub asks for approval. +7. Confirm the package is visible at . + +The release workflow verifies that the GitHub Release tag matches the `pyproject.toml` version. A +release tagged `v1.0.0` must publish package version `1.0.0`; mismatches fail before upload. + +## Install after release + +Once the first PyPI release is published, users can install from PyPI: + +```bash +uvx --from dbt-spec-kit dbt-specify --version +uvx --from dbt-spec-kit dbt-specify init my-project --warehouse snowflake +``` + +Persistent install: + +```bash +uv tool install dbt-spec-kit +dbt-specify --version +``` + +`pipx install dbt-spec-kit` also works for teams that do not use `uv`. + +## If publishing fails + +- `invalid-publisher`: Check the PyPI Trusted Publisher values exactly match `duckcode-ai`, + `dbt-spec-kit`, `release.yml`, and `pypi`. +- `tag mismatch`: Update `pyproject.toml` or create the correct `vX.Y.Z` tag. +- `file already exists`: PyPI does not allow replacing an existing version. Bump the version and + publish a new release. +- `environment approval pending`: Approve the `pypi` deployment in GitHub Actions. diff --git a/tests/test_docs.py b/tests/test_docs.py index 66e732d..8031b1c 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -41,11 +41,22 @@ def test_launch_ready_oss_files_exist() -> None: ".github/pull_request_template.md", ".github/ISSUE_TEMPLATE/bug_report.md", ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/workflows/release.yml", + "docs/releasing.md", ] for relative_path in required_paths: assert (ROOT / relative_path).exists(), f"Missing OSS file: {relative_path}" +def test_release_workflow_uses_trusted_publishing() -> None: + workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text() + assert "pypa/gh-action-pypi-publish@release/v1" in workflow + assert "id-token: write" in workflow + assert "name: pypi" in workflow + assert "python -m build --sdist --wheel" in workflow + assert "python -m twine check dist/*" in workflow + + def _markdown_links(text: str) -> list[str]: return re.findall(r"(?