Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 56 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ on:
tags:
- "v*"

permissions:
contents: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Set up uv
Expand Down Expand Up @@ -73,47 +75,82 @@ 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(
f"expected exactly one wheel matching {prefix}-*.whl, got {wheels!r}",
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
with:
files: dist/*.whl
body_path: CHANGELOG_RELEASE_BODY.md
- name: Stage PyPI wheels (SDK + CLI + MCP + Auth + Infra)
if: startsWith(github.ref_name, 'v1.') && !contains(github.ref_name, '-')
- name: Upload built distributions
if: vars.PYPI_PUBLISH_ENABLED == 'true'
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
if-no-files-found: error
retention-days: 1

publish-pypi:
needs: release
if: vars.PYPI_PUBLISH_ENABLED == 'true'
runs-on: ubuntu-latest
environment: pypi
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 and no
# extras before uploading the artifact, so 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
wheels=(dist/*.whl)
if [ ${#wheels[@]} -eq 0 ]; then
echo "no wheels in dist/ to publish" >&2
exit 1
fi
cp "${sdk[0]}" "${cli[0]}" "${mcp[0]}" "${auth[0]}" "${infra[0]}" dist-pypi/
mkdir -p dist-pypi
cp "${wheels[@]}" 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
.venv-pypi-smoke/bin/pip install dist-pypi/*.whl
.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/
# 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
30 changes: 20 additions & 10 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<owner>/<repo>.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/<owner>/<repo>.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 repos [`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).

Expand Down Expand Up @@ -72,22 +75,29 @@ 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 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).
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 **`pipefy-cli`** and **`pipefy-mcp-server`** (and `pipefy-sdk` later if you enable it in the workflow).
- No long-lived PyPI token is required when using OIDC; the workflow requests `id-token: write`.
- 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 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
# 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
Expand All @@ -96,4 +106,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, 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`. |
2 changes: 1 addition & 1 deletion docs/DEPRECATION.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion packages/auth/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
4 changes: 2 additions & 2 deletions packages/mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading