From 746b5f3b845aa3554a771806df99ab9e36189752 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 19 Jun 2026 14:18:23 -0300 Subject: [PATCH 1/6] docs(release): publish all five wheels to PyPI on v1. tags RELEASE.md claimed only pipefy-cli and pipefy-mcp-server publish to PyPI (with pipefy-sdk held back), but release.yml stages and uploads all five wheels. The dependency closure forces this: cli and mcp depend on sdk and auth, which depend on infra, so a public pip install only resolves when every package is on PyPI. Reconcile the docs to match the workflow and note the single-batch upload and pending-publisher requirements. --- RELEASE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 7f8d2686..1eb3980c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -74,11 +74,12 @@ uvx --from "git+https://github.com/pipefy/ai-toolkit.git@vX.Y.Z" --refresh pipef ## v1.0 and later: GitHub Release + PyPI -Same steps as above. For tags whose name starts with **`v1.`** (for example `v1.0.0` or `v1.0.0rc1`), the release workflow also runs **Trusted Publishing** to upload the **`pipefy-cli`** and **`pipefy-mcp-server`** wheels to PyPI via `pypa/gh-action-pypi-publish` (the `pipefy-sdk` wheel is built for the GitHub Release but is **not** uploaded to PyPI until maintainers enable it in the workflow; see **Repository setup** below). +Same steps as above. For tags whose name starts with **`v1.`** (for example `v1.0.0` or `v1.0.0rc1`), the release workflow also runs **Trusted Publishing** to upload all five wheels (**`pipefy-cli`**, **`pipefy-mcp-server`**, **`pipefy-sdk`**, **`pipefy-auth`**, **`pipefy-infra`**) to PyPI via `pypa/gh-action-pypi-publish`. All five are required: `pipefy-cli` and `pipefy-mcp-server` depend on `pipefy-sdk` and `pipefy-auth`, which depend on `pipefy-infra`, so a public `pip install` only resolves if every package in that closure is on PyPI. See **Repository setup** below. **Repository setup (maintainers):** -- Configure [Trusted Publishers](https://docs.pypi.org/trusted-publishers/using-a-publisher/) on PyPI for **`pipefy-cli`** and **`pipefy-mcp-server`** (and `pipefy-sdk` later if you enable it in the workflow). +- Configure [Trusted Publishers](https://docs.pypi.org/trusted-publishers/using-a-publisher/) on PyPI for all five distributions: **`pipefy-cli`**, **`pipefy-mcp-server`**, **`pipefy-sdk`**, **`pipefy-auth`**, and **`pipefy-infra`**. The upload is a single batch, so if any of the five lacks a configured publisher, PyPI rejects that file and the release fails. +- For the first release, the projects do not exist on PyPI yet. Register each of the five as a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/); the project is created on the first successful publish. - No long-lived PyPI token is required when using OIDC; the workflow requests `id-token: write`. **After a v1.x tag:** @@ -96,4 +97,4 @@ Same steps as above. For tags whose name starts with **`v1.`** (for example `v1. | --- | --- | | `scripts/bump_version.py` | Reads the SDK `__version__`, applies the bump, writes the same value to SDK, MCP, CLI, Auth, and Infra `__init__.py` plus the root `pyproject.toml`'s `[project].version`, then runs `uv lock` to refresh `uv.lock`. Also exposes a `verify` mode that asserts every version-bearing file agrees. | | `.github/workflows/ci.yml` | Invokes `scripts/bump_version.py verify` to assert the five `__version__` strings and root `pyproject.toml` `[project].version` all match. | -| `.github/workflows/release.yml` | On `v*` tags: asserts the tag matches SDK `__version__`, extracts the matching `CHANGELOG.md` section as the GitHub Release body, builds wheels and sdists with `uv build --all-packages -o dist --wheel --sdist`, attaches wheels to the GitHub Release, and publishes CLI + MCP wheels to PyPI only when `github.ref_name` starts with `v1.`. | +| `.github/workflows/release.yml` | On `v*` tags: asserts the tag matches SDK `__version__`, extracts the matching `CHANGELOG.md` section as the GitHub Release body, builds wheels and sdists with `uv build --all-packages -o dist --wheel --sdist`, attaches wheels to the GitHub Release, and publishes all five wheels (CLI, MCP, SDK, Auth, Infra) to PyPI only when `github.ref_name` starts with `v1.`. | From 9732616618ed4597d3c4313fefdccfa52b90cd6f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 19 Jun 2026 14:35:16 -0300 Subject: [PATCH 2/6] ci(release): gate PyPI publish behind PYPI_PUBLISH_ENABLED, cover all tags Split PyPI publishing into a separate publish-pypi job that runs after the GitHub Release, scoped to a dedicated pypi environment with id-token: write. The release job keeps contents: write and uploads the built dist/ as an artifact the publish job consumes. Replace the v1.-only step gate with a job-level toggle, vars.PYPI_PUBLISH_ENABLED == 'true'. The version-based gate was only ever a stand-in for the PyPI account not being provisioned yet; the intended end state publishes every release tag, betas included. Off by default so nothing attempts PyPI until the account and the five pending publishers exist, then the sec team flips the variable with no code change. Reframe RELEASE.md around the toggle instead of the v0.x/v1 split. --- .github/workflows/release.yml | 29 ++++++++++++++++++++++------- RELEASE.md | 25 +++++++++++++++---------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2178f17d..dec79317 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,13 +5,11 @@ on: tags: - "v*" -permissions: - contents: write - id-token: write - jobs: release: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v6 - name: Set up uv @@ -87,8 +85,27 @@ jobs: with: files: dist/*.whl body_path: CHANGELOG_RELEASE_BODY.md + - name: Upload built distributions + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + if-no-files-found: error + + publish-pypi: + needs: release + if: vars.PYPI_PUBLISH_ENABLED == 'true' + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - name: Download built distributions + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ - name: Stage PyPI wheels (SDK + CLI + MCP + Auth + Infra) - if: startsWith(github.ref_name, 'v1.') && !contains(github.ref_name, '-') run: | mkdir -p dist-pypi shopt -s nullglob @@ -105,7 +122,6 @@ jobs: fi cp "${sdk[0]}" "${cli[0]}" "${mcp[0]}" "${auth[0]}" "${infra[0]}" dist-pypi/ - name: Smoke install staged wheels (clean venv) - if: startsWith(github.ref_name, 'v1.') && !contains(github.ref_name, '-') run: | python -m venv .venv-pypi-smoke .venv-pypi-smoke/bin/python -m pip install --upgrade pip @@ -113,7 +129,6 @@ jobs: .venv-pypi-smoke/bin/pipefy --help .venv-pypi-smoke/bin/pipefy-mcp-server --help - name: Upload to PyPI - if: startsWith(github.ref_name, 'v1.') && !contains(github.ref_name, '-') uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist-pypi/ diff --git a/RELEASE.md b/RELEASE.md index 1eb3980c..be0a15e0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,13 +2,16 @@ Workspace distributions (`pipefy-sdk`, `pipefy-mcp-server`, `pipefy-cli`, `pipefy-auth`, `pipefy-infra`) share a single **lockstep** version string in each package's `__init__.py`. CI fails if those values diverge. -## Pre-launch (v0.x): GitHub Release only +## Cutting a release -PyPI publishing is **disabled** for tags that do not start with `v1.`. Pre-release installs use git references (for example `uvx --from git+https://github.com//.git@vX.Y.Z --refresh pipefy-cli`). +Every `v*` tag builds all five wheels and sdists and creates a GitHub Release with the wheels attached. PyPI publishing runs on top of that, gated by the `PYPI_PUBLISH_ENABLED` repository variable (Settings > Secrets and variables > Actions > Variables): + +- **Unset or not `true`** (current state, while the PyPI account is being provisioned): GitHub Release only. Installs use git references, for example `uvx --from git+https://github.com//.git@vX.Y.Z --refresh pipefy-cli`. +- **`true`**: the `publish-pypi` job also uploads all five wheels to PyPI via Trusted Publishing, on **every** release tag including the `v0.2.0-beta.*` line. There is no version-based gate. Betas publish as PEP 440 pre-releases (a `v0.2.0-beta.4` tag ships `0.2.0b4`, which `pip` installs only with `--pre`). ### Public beta line (`v0.2.0-beta.*`) -The next **GitHub pre-release** after the standalone repo’s [`v0.1.0-beta.1`](https://github.com/pipefy/ai-toolkit/releases/tag/v0.1.0-beta.1) is the **`v0.2.0-beta.*`** series on this monorepo (first cut: **`v0.2.0-beta.1`** unless you intentionally reuse another suffix). Same mechanics as any other **v0.x** tag: attach wheels to the GitHub Release; **no PyPI** until **`v1.`**. +The next **GitHub pre-release** after the standalone repo's [`v0.1.0-beta.1`](https://github.com/pipefy/ai-toolkit/releases/tag/v0.1.0-beta.1) is the **`v0.2.0-beta.*`** series on this monorepo (first cut: **`v0.2.0-beta.1`** unless you intentionally reuse another suffix). Same mechanics as any other tag: wheels attach to the GitHub Release, and they also go to PyPI once `PYPI_PUBLISH_ENABLED` is `true`. The Release workflow requires the git tag (without leading `v`) to **exactly match** `__version__` in `packages/sdk/src/pipefy_sdk/__init__.py` (and the MCP/CLI/Auth/Infra copies). For example tag **`v0.2.0-beta.1`** implies **`__version__ = "0.2.0-beta.1"`** in all five packages before you push the tag (set via step 2 below using `version=0.2.0-beta.1`, or edit the five `__init__.py` files together). @@ -72,19 +75,21 @@ uvx --from "git+https://github.com/pipefy/ai-toolkit.git@vX.Y.Z" --refresh pipef # Expected: help text (server may block in stdio mode — Ctrl-C after banner) ``` -## v1.0 and later: GitHub Release + PyPI +## PyPI publishing -Same steps as above. For tags whose name starts with **`v1.`** (for example `v1.0.0` or `v1.0.0rc1`), the release workflow also runs **Trusted Publishing** to upload all five wheels (**`pipefy-cli`**, **`pipefy-mcp-server`**, **`pipefy-sdk`**, **`pipefy-auth`**, **`pipefy-infra`**) to PyPI via `pypa/gh-action-pypi-publish`. All five are required: `pipefy-cli` and `pipefy-mcp-server` depend on `pipefy-sdk` and `pipefy-auth`, which depend on `pipefy-infra`, so a public `pip install` only resolves if every package in that closure is on PyPI. See **Repository setup** below. +When `PYPI_PUBLISH_ENABLED` is `true`, the `publish-pypi` job runs after the GitHub Release on every release tag and uses **Trusted Publishing** to upload all five wheels (**`pipefy-cli`**, **`pipefy-mcp-server`**, **`pipefy-sdk`**, **`pipefy-auth`**, **`pipefy-infra`**) to PyPI via `pypa/gh-action-pypi-publish`. All five are required: `pipefy-cli` and `pipefy-mcp-server` depend on `pipefy-sdk` and `pipefy-auth`, which depend on `pipefy-infra`, so a public `pip install` only resolves if every package in that closure is on PyPI. See **Repository setup** below. **Repository setup (maintainers):** - Configure [Trusted Publishers](https://docs.pypi.org/trusted-publishers/using-a-publisher/) on PyPI for all five distributions: **`pipefy-cli`**, **`pipefy-mcp-server`**, **`pipefy-sdk`**, **`pipefy-auth`**, and **`pipefy-infra`**. The upload is a single batch, so if any of the five lacks a configured publisher, PyPI rejects that file and the release fails. -- For the first release, the projects do not exist on PyPI yet. Register each of the five as a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/); the project is created on the first successful publish. -- No long-lived PyPI token is required when using OIDC; the workflow requests `id-token: write`. +- For the first publish, the projects do not exist on PyPI yet. Register each of the five as a [pending publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/); the project is created on the first successful publish. Each publisher points at owner `pipefy`, repo `ai-toolkit`, workflow `release.yml`, environment `pypi`. +- No long-lived PyPI token is required when using OIDC; the `publish-pypi` job requests `id-token: write`. +- Publishing stays off until the `PYPI_PUBLISH_ENABLED` repository variable is set to `true`. Flip it on only after the five (pending) publishers exist, since one missing publisher fails the whole release. +- The `publish-pypi` job runs under the `pypi` GitHub Actions environment; add protection rules to that environment (required reviewers, allowed tags) if you want an approval gate before each upload. -**After a v1.x tag:** +**After a published tag:** -1. Confirm the new versions appear on PyPI for each published package. +1. Confirm the new versions appear on PyPI for each of the five packages. 2. Smoke-test a clean install, for example: ```bash @@ -97,4 +102,4 @@ Same steps as above. For tags whose name starts with **`v1.`** (for example `v1. | --- | --- | | `scripts/bump_version.py` | Reads the SDK `__version__`, applies the bump, writes the same value to SDK, MCP, CLI, Auth, and Infra `__init__.py` plus the root `pyproject.toml`'s `[project].version`, then runs `uv lock` to refresh `uv.lock`. Also exposes a `verify` mode that asserts every version-bearing file agrees. | | `.github/workflows/ci.yml` | Invokes `scripts/bump_version.py verify` to assert the five `__version__` strings and root `pyproject.toml` `[project].version` all match. | -| `.github/workflows/release.yml` | On `v*` tags: asserts the tag matches SDK `__version__`, extracts the matching `CHANGELOG.md` section as the GitHub Release body, builds wheels and sdists with `uv build --all-packages -o dist --wheel --sdist`, attaches wheels to the GitHub Release, and publishes all five wheels (CLI, MCP, SDK, Auth, Infra) to PyPI only when `github.ref_name` starts with `v1.`. | +| `.github/workflows/release.yml` | On `v*` tags, the `release` job asserts the tag matches SDK `__version__`, extracts the matching `CHANGELOG.md` section as the GitHub Release body, builds wheels and sdists with `uv build --all-packages -o dist --wheel --sdist`, and attaches wheels to the GitHub Release. A separate `publish-pypi` job (environment `pypi`) then uploads all five wheels (CLI, MCP, SDK, Auth, Infra) to PyPI via Trusted Publishing when the `PYPI_PUBLISH_ENABLED` repository variable is `true`. | From 23629f1ba02de6a6afc240e367bd6b136fd18c5d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 19 Jun 2026 14:41:10 -0300 Subject: [PATCH 3/6] refactor(release): drop redundant wheel re-validation, gate artifact upload The publish-pypi staging step re-globbed and re-asserted exactly one wheel per package, but the release job already verifies that before uploading the immutable artifact. Collapse the step to a wheels-only copy (dist-pypi still exists to exclude sdists from the PyPI upload). Gate the dist artifact upload on PYPI_PUBLISH_ENABLED so non-publishing releases don't pack and store an artifact nobody downloads. --- .github/workflows/release.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dec79317..c845e19a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,6 +86,7 @@ jobs: files: dist/*.whl body_path: CHANGELOG_RELEASE_BODY.md - name: Upload built distributions + if: vars.PYPI_PUBLISH_ENABLED == 'true' uses: actions/upload-artifact@v4 with: name: dist @@ -105,22 +106,12 @@ jobs: with: name: dist path: dist/ - - name: Stage PyPI wheels (SDK + CLI + MCP + Auth + Infra) + - name: Stage wheels for PyPI (exclude sdists) + # The release job already verified exactly one wheel per package before + # uploading the artifact, so just publish the wheels and skip the sdists. run: | mkdir -p dist-pypi - shopt -s nullglob - sdk=(dist/pipefy_sdk-*.whl) - cli=(dist/pipefy_cli-*.whl) - mcp=(dist/pipefy_mcp_server-*.whl) - auth=(dist/pipefy_auth-*.whl) - infra=(dist/pipefy_infra-*.whl) - if [ "${#sdk[@]}" -ne 1 ] || [ "${#cli[@]}" -ne 1 ] || [ "${#mcp[@]}" -ne 1 ] || [ "${#auth[@]}" -ne 1 ] || [ "${#infra[@]}" -ne 1 ]; then - echo "Expected one SDK, one CLI, one MCP, one Auth, and one Infra wheel in dist/" - echo "sdk=${#sdk[@]} cli=${#cli[@]} mcp=${#mcp[@]} auth=${#auth[@]} infra=${#infra[@]}" - ls -la dist || true - exit 1 - fi - cp "${sdk[0]}" "${cli[0]}" "${mcp[0]}" "${auth[0]}" "${infra[0]}" dist-pypi/ + cp dist/*.whl dist-pypi/ - name: Smoke install staged wheels (clean venv) run: | python -m venv .venv-pypi-smoke From 4b7366ef618d78f015e848eec6d2e608813c947a Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 19 Jun 2026 15:28:20 -0300 Subject: [PATCH 4/6] ci(release): harden publish job and bound the published wheel set - skip-existing on the PyPI upload so a re-pushed tag at an already-published version is idempotent instead of failing on PyPI's 400. - retention-days: 1 on the dist artifact (a pure intra-run handoff). - concurrency group so two tags pushed together don't race the artifact/version. - Verify expected wheels now also asserts the total wheel count, so a future workspace member can't be silently published by the cp dist/*.whl staging. - setup-python in publish-pypi (the smoke step no longer relies on the runner shim) and a nullglob guard with a clear message on the staging copy. --- .github/workflows/release.yml | 39 +++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c845e19a..700065e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,10 @@ on: tags: - "v*" +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: release: runs-on: ubuntu-latest @@ -71,7 +75,8 @@ jobs: from pathlib import Path dist = Path("dist") - for prefix in ("pipefy_sdk", "pipefy_mcp_server", "pipefy_cli", "pipefy_auth", "pipefy_infra"): + expected = ("pipefy_sdk", "pipefy_mcp_server", "pipefy_cli", "pipefy_auth", "pipefy_infra") + for prefix in expected: wheels = list(dist.glob(f"{prefix}-*.whl")) if len(wheels) != 1: print( @@ -79,6 +84,17 @@ jobs: file=sys.stderr, ) sys.exit(1) + # Bound the published set: any wheel beyond the five known packages + # (e.g. a new workspace member) must fail here rather than be silently + # published, since the publish step copies every wheel in dist/. + all_wheels = list(dist.glob("*.whl")) + if len(all_wheels) != len(expected): + print( + f"expected exactly {len(expected)} wheels in dist/, got " + f"{sorted(w.name for w in all_wheels)!r}", + file=sys.stderr, + ) + sys.exit(1) PY - name: Create GitHub Release uses: softprops/action-gh-release@v2 @@ -92,6 +108,7 @@ jobs: name: dist path: dist/ if-no-files-found: error + retention-days: 1 publish-pypi: needs: release @@ -101,17 +118,28 @@ jobs: permissions: id-token: write steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" - name: Download built distributions uses: actions/download-artifact@v4 with: name: dist path: dist/ - name: Stage wheels for PyPI (exclude sdists) - # The release job already verified exactly one wheel per package before - # uploading the artifact, so just publish the wheels and skip the sdists. + # The release job already verified exactly one wheel per package and no + # extras before uploading the artifact, so publish the wheels and skip + # the sdists. run: | + shopt -s nullglob + wheels=(dist/*.whl) + if [ ${#wheels[@]} -eq 0 ]; then + echo "no wheels in dist/ to publish" >&2 + exit 1 + fi mkdir -p dist-pypi - cp dist/*.whl dist-pypi/ + cp "${wheels[@]}" dist-pypi/ - name: Smoke install staged wheels (clean venv) run: | python -m venv .venv-pypi-smoke @@ -123,3 +151,6 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist-pypi/ + # Idempotent re-runs: a re-pushed tag at an already-published version + # skips existing files instead of failing the release on PyPI's 400. + skip-existing: true From 76d24c18c1ea055172d86231652a3dc27323bb4d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 19 Jun 2026 15:29:07 -0300 Subject: [PATCH 5/6] fix(packaging): pin workspace siblings to the lockstep version in wheels Published wheels declared bare, unpinned inter-package deps (pipefy-cli -> pipefy-sdk/pipefy-auth -> pipefy-infra), so once on PyPI a user could resolve pipefy-cli==X against a newer pipefy-sdk and get a mismatched, broken install. Pin each sibling to == so the closure installs in lockstep. bump_version.py now rewrites these pins on every bump and verify() asserts them, keeping the pins in lockstep with the shared version. The [tool.uv.sources] workspace mapping still overrides them for in-repo dev. --- packages/auth/pyproject.toml | 2 +- packages/cli/pyproject.toml | 4 +-- packages/mcp/pyproject.toml | 4 +-- packages/sdk/pyproject.toml | 2 +- scripts/bump_version.py | 49 +++++++++++++++++++++++++++++++++ tests/test_bump_version.py | 53 ++++++++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+), 6 deletions(-) diff --git a/packages/auth/pyproject.toml b/packages/auth/pyproject.toml index 9fe91bd4..0f95771c 100644 --- a/packages/auth/pyproject.toml +++ b/packages/auth/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "httpx-auth>=0.23.1", "keyring>=24", "keyrings.alt>=5", - "pipefy-infra", + "pipefy-infra==0.2.0-beta.3", "pydantic>=2.13.4,<3", "pydantic-settings>=2.8.1", ] diff --git a/packages/cli/pyproject.toml b/packages/cli/pyproject.toml index 740772b4..948de2b7 100644 --- a/packages/cli/pyproject.toml +++ b/packages/cli/pyproject.toml @@ -5,8 +5,8 @@ description = "Typer CLI for Pipefy (pipefy-sdk)." readme = "README.md" requires-python = ">=3.11" dependencies = [ - "pipefy-sdk", - "pipefy-auth", + "pipefy-sdk==0.2.0-beta.3", + "pipefy-auth==0.2.0-beta.3", "typer>=0.12", "rich>=13", "pydantic-settings>=2.8.1", diff --git a/packages/mcp/pyproject.toml b/packages/mcp/pyproject.toml index c3a356a0..7036442a 100644 --- a/packages/mcp/pyproject.toml +++ b/packages/mcp/pyproject.toml @@ -8,8 +8,8 @@ dependencies = [ "httpx>=0.27.0", "mcp[cli]>=1.25.0", # Workspace resolves versions in dev; PyPI releases stage these wheels alongside CLI/MCP (see release workflow). - "pipefy-auth", - "pipefy-sdk", + "pipefy-auth==0.2.0-beta.3", + "pipefy-sdk==0.2.0-beta.3", "pydantic>=2.13.4,<3", "pydantic-settings>=2.8.1", ] diff --git a/packages/sdk/pyproject.toml b/packages/sdk/pyproject.toml index d4f66456..170fa904 100644 --- a/packages/sdk/pyproject.toml +++ b/packages/sdk/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "gql[httpx]>=4.0.0,<5", "httpx>=0.27.0", "openpyxl>=3.1.5", - "pipefy-infra", + "pipefy-infra==0.2.0-beta.3", "pydantic>=2.13.4,<3", "pydantic-settings>=2.8.1", "rapidfuzz>=3.14.3", diff --git a/scripts/bump_version.py b/scripts/bump_version.py index e6b7ebcd..cc691b21 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -28,6 +28,17 @@ REPO_ROOT / "packages/infra/src/pipefy_infra/__init__.py", ) +# Each published package pins its workspace siblings to the exact lockstep +# version in its built metadata, so `pip install pipefy-cli==X` pulls +# pipefy-sdk==X rather than a newer sibling that happens to be on PyPI. The +# [tool.uv.sources] workspace mapping overrides these pins for in-repo dev. +WORKSPACE_DEP_PINS: dict[Path, tuple[str, ...]] = { + REPO_ROOT / "packages/sdk/pyproject.toml": ("pipefy-infra",), + REPO_ROOT / "packages/auth/pyproject.toml": ("pipefy-infra",), + REPO_ROOT / "packages/cli/pyproject.toml": ("pipefy-sdk", "pipefy-auth"), + REPO_ROOT / "packages/mcp/pyproject.toml": ("pipefy-sdk", "pipefy-auth"), +} + # Anchored to the [project] table. The middle alternation walks lines that # don't start with `[` (so a sibling [tool.X] header ends the run), but the # line CONTENTS can contain `[` (so `classifiers = [...]`, `dependencies = @@ -91,6 +102,34 @@ def write_version_to_all_files(new_version: str) -> None: ROOT_PYPROJECT.write_text(new_root, encoding="utf-8") +def workspace_dep_pin_re(dep_name: str) -> re.Pattern[str]: + """Match a quoted ``dep_name`` requirement, with or without an existing ``==`` pin. + + The name must fill the entire quoted string, so the unquoted + ``[tool.uv.sources]`` keys and a parenthesized mention inside a description + are not matched. A package's own ``name = "..."`` field IS a full quoted + string and would match; that collision is avoided by ``WORKSPACE_DEP_PINS`` + never listing a package's own name (a package cannot depend on itself). + """ + return re.compile(rf'(["\']){re.escape(dep_name)}(?:\s*==[^"\']*)?(["\'])') + + +def write_dep_pins(new_version: str) -> None: + """Pin each package's workspace-sibling dependencies to ``new_version``.""" + for path, deps in WORKSPACE_DEP_PINS.items(): + text = path.read_text(encoding="utf-8") + for dep in deps: + text, count = workspace_dep_pin_re(dep).subn( + rf"\g<1>{dep}=={new_version}\g<2>", + text, + count=1, + ) + if count != 1: + msg = f"Expected one {dep!r} dependency in {path}, replaced {count}" + raise ValueError(msg) + path.write_text(text, encoding="utf-8") + + def parse_core(version: str) -> tuple[int, int, int]: """Parse leading ``X.Y.Z`` from a version string.""" m = CORE_VER_RE.match(version.strip()) @@ -226,6 +265,15 @@ def verify_lockstep() -> int: return 1 raw[str(path.relative_to(REPO_ROOT))] = m.group(2) + for path, deps in WORKSPACE_DEP_PINS.items(): + text = path.read_text(encoding="utf-8") + for dep in deps: + m = re.search(rf'(["\']){re.escape(dep)}==([^"\']+)\1', text) + if not m: + print(f"missing pinned {dep!r} dependency in {path}", file=sys.stderr) + return 1 + raw[f"{path.relative_to(REPO_ROOT)}::{dep}"] = m.group(2) + try: raw["pyproject.toml"] = read_root_pyproject_version() except (KeyError, tomllib.TOMLDecodeError, OSError) as exc: @@ -285,6 +333,7 @@ def main() -> int: return 2 write_version_to_all_files(new_version) + write_dep_pins(new_version) refresh_lockfile() print(f"Bumped {current} -> {new_version}") return 0 diff --git a/tests/test_bump_version.py b/tests/test_bump_version.py index e7043ad9..c8cab735 100644 --- a/tests/test_bump_version.py +++ b/tests/test_bump_version.py @@ -85,3 +85,56 @@ def test_root_project_version_re_rejects_missing_version(pyproject: str) -> None r'\1"REPLACED"', pyproject, count=1 ) assert count == 0, f"expected no match in {pyproject!r}" + + +@pytest.mark.parametrize( + ("entry", "expected"), + [ + # Bare dependency gains a pin. + ('"pipefy-sdk"', '"pipefy-sdk==9.9.9"'), + # Already-pinned dependency is re-pinned to the new version. + ('"pipefy-sdk==0.2.0-beta.3"', '"pipefy-sdk==9.9.9"'), + # Single quotes are handled too. + ("'pipefy-sdk'", "'pipefy-sdk==9.9.9'"), + ], +) +def test_workspace_dep_pin_re_rewrites_dependency(entry: str, expected: str) -> None: + new_text, count = _bump.workspace_dep_pin_re("pipefy-sdk").subn( + r"\g<1>pipefy-sdk==9.9.9\g<2>", entry, count=1 + ) + assert count == 1 + assert new_text == expected + + +@pytest.mark.parametrize( + "text", + [ + # An unquoted [tool.uv.sources] key must not be pinned (no quotes). + "pipefy-sdk = { workspace = true }", + # A parenthesized mention inside a description must not be pinned + # (the name is not the whole quoted string). + 'description = "Typer CLI for Pipefy (pipefy-sdk)."', + ], +) +def test_workspace_dep_pin_re_leaves_non_dependencies_untouched(text: str) -> None: + _new_text, count = _bump.workspace_dep_pin_re("pipefy-sdk").subn( + r"\g<1>pipefy-sdk==9.9.9\g<2>", text, count=1 + ) + assert count == 0, f"unexpectedly matched in {text!r}" + + +def test_workspace_dep_pins_never_target_own_name() -> None: + """The own-name collision (name = "x" is a full quoted string) is avoided by + the pin map never listing a package's own distribution name.""" + own_name = { + "sdk": "pipefy-sdk", + "mcp": "pipefy-mcp-server", + "cli": "pipefy-cli", + "auth": "pipefy-auth", + "infra": "pipefy-infra", + } + for path, deps in _bump.WORKSPACE_DEP_PINS.items(): + pkg_dir = path.parent.name + assert own_name[pkg_dir] not in deps, ( + f"{path} would pin its own name {own_name[pkg_dir]!r}" + ) From e468625a2f7e88fce171af3820b31b4844831dcd Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 19 Jun 2026 15:29:07 -0300 Subject: [PATCH 6/6] docs: decouple PyPI availability from the v1.0 milestone Publishing is now controlled by the PYPI_PUBLISH_ENABLED toggle, not the v1.0 tag, so docs that tied PyPI to v1.0 were stale. Fix the RELEASE.md post-publish smoke command to allow pre-releases for beta tags, correct the sdk README's 'gated separately' claim to the single-batch model, and reword README/MIGRATION/ DEPRECATION/cli/mcp install notes to key on PyPI presence rather than v1.0. --- README.md | 4 ++-- RELEASE.md | 4 ++++ docs/DEPRECATION.md | 2 +- docs/MIGRATION.md | 2 +- packages/cli/README.md | 2 +- packages/mcp/README.md | 2 +- packages/sdk/README.md | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a4f709af..2c076012 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ Feedback and issues: [GitHub Issues](https://github.com/pipefy/ai-toolkit/issues ## Installation -> Pre-1.0 ships from this git repo via `uvx` and `uv tool install`. PyPI becomes the canonical source at **v1.0**. The current beta line is **`v0.2.0-beta.*`** (first tag: [`v0.2.0-beta.1`](https://github.com/pipefy/ai-toolkit/releases/tag/v0.2.0-beta.1)). Two install paths: the **Quick install** script below (resolves the latest GitHub Release at runtime and runs `uv tool install` for you), or **Claude Code** via the plugin marketplace. +> The toolkit ships from this git repo via `uvx` and `uv tool install`. PyPI is the canonical source once publishing is enabled (gated by a release-time toggle, see [`RELEASE.md`](RELEASE.md)); until these packages are on PyPI, use the git paths below. The current beta line is **`v0.2.0-beta.*`** (first tag: [`v0.2.0-beta.1`](https://github.com/pipefy/ai-toolkit/releases/tag/v0.2.0-beta.1)). Two install paths: the **Quick install** script below (resolves the latest GitHub Release at runtime and runs `uv tool install` for you), or **Claude Code** via the plugin marketplace. > -> The CLI snippets below pin **`@latest`**, a moving git tag the release flow updates to point at the most recent release. To pin a specific version, swap `@latest` for a version tag (e.g. `@v0.2.0-beta.2`). The `--with pipefy-sdk @ ...#subdirectory=packages/sdk` / `pipefy-auth @ ...#subdirectory=packages/auth` flags are required pre-1.0: this repo is a uv workspace, and the workspace members are not yet published to PyPI, so uv needs them named explicitly. The flags go away at v1.0 (PyPI install). +> The CLI snippets below pin **`@latest`**, a moving git tag the release flow updates to point at the most recent release. To pin a specific version, swap `@latest` for a version tag (e.g. `@v0.2.0-beta.2`). The `--with pipefy-sdk @ ...#subdirectory=packages/sdk` / `pipefy-auth @ ...#subdirectory=packages/auth` flags are required while the workspace members are not yet on PyPI: this repo is a uv workspace, so uv needs them named explicitly. The flags go away once the packages are published to PyPI. Two auth paths: diff --git a/RELEASE.md b/RELEASE.md index be0a15e0..7ca7c674 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -93,7 +93,11 @@ When `PYPI_PUBLISH_ENABLED` is `true`, the `publish-pypi` job runs after the Git 2. Smoke-test a clean install, for example: ```bash + # Final (non-prerelease) tags: uv tool install pipefy-cli + + # Beta tags publish PEP 440 pre-releases, so allow them explicitly: + uv tool install --prerelease=allow pipefy-cli ``` ## Automation reference diff --git a/docs/DEPRECATION.md b/docs/DEPRECATION.md index 6ac6c920..aeb61a1f 100644 --- a/docs/DEPRECATION.md +++ b/docs/DEPRECATION.md @@ -1,6 +1,6 @@ # Deprecation and versioning (post-1.0) -This policy applies **after v1.0.0**, when published `pipefy-cli` and `pipefy-mcp-server` follow public [semantic versioning](https://semver.org/) on PyPI. Before that milestone, interfaces may still change without the guarantees below. +This policy applies **after v1.0.0**, when `pipefy-cli` and `pipefy-mcp-server` follow public [semantic versioning](https://semver.org/). Before that milestone, interfaces may still change without the guarantees below, including any pre-release versions published to PyPI (the v1.0 stability guarantee is independent of when packages first appear on PyPI). ## Version bumps diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 60159e5c..3a90bc78 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -6,7 +6,7 @@ Existing users of `pipefy-mcp-server`: this guide covers what changed and what t ## Package name — unchanged -`pipefy-mcp-server` is the same PyPI package name as before. Your `pip install pipefy-mcp-server` or `uvx pipefy-mcp-server` still works. The existing PyPI version is frozen at the pre-monorepo release; new versions ship at v1.0 via the same name. +`pipefy-mcp-server` is the same PyPI package name as before. Your `pip install pipefy-mcp-server` or `uvx pipefy-mcp-server` still works. The existing PyPI version is frozen at the pre-monorepo release; new versions ship to the same name once PyPI publishing is enabled (see [`RELEASE.md`](../RELEASE.md)). **Pre-launch (v0.1 → v0.5):** install from git to get the latest: diff --git a/packages/cli/README.md b/packages/cli/README.md index a9f2b294..6518fda5 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -21,7 +21,7 @@ uv tool install \ "git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/cli" ``` -The `--with` flags are required pre-1.0 because the workspace members are not yet on PyPI. At v1.0 this collapses to `uv tool install pipefy-cli`. +The `--with` flags are required while the workspace members are not yet on PyPI. Once they are published, this collapses to `uv tool install pipefy-cli`. ## Quick start diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 5b02d7af..9edb2175 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -12,7 +12,7 @@ uvx \ --refresh pipefy-mcp-server ``` -The `--with pipefy-sdk` / `pipefy-auth` flags are required pre-1.0 because the workspace members are not yet on PyPI. At v1.0 this collapses to `uvx pipefy-mcp-server`. +The `--with pipefy-sdk` / `pipefy-auth` flags are required while the workspace members are not yet on PyPI. Once they are published, this collapses to `uvx pipefy-mcp-server`. For per-client wiring (Claude Code / Cursor / Claude Desktop / Codex), see [root `README.md#installation`](../../README.md#installation). diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 48caf416..8f183348 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -4,7 +4,7 @@ ## Status -Workspace-internal in v0.1; publishing **`pipefy-sdk`** to PyPI is gated separately from CLI/MCP (see **`RELEASE.md`** Trusted Publishing notes). +Workspace-internal in v0.1. When PyPI publishing is enabled, `pipefy-sdk` ships to PyPI in the same single batch as CLI, MCP, Auth, and Infra (all five share one lockstep version); it is not gated separately (see **`RELEASE.md`** Trusted Publishing notes). ## Usage (within the monorepo)