diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2178f17d..dcfe004d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,13 @@ permissions: contents: write id-token: write +# Serialize tag-push runs so two near-simultaneous releases don't race the +# `git push origin main` in the manifest-patch step (the loser would fail +# non-fast-forward after the GitHub Release is already published). +concurrency: + group: release-main-patch + cancel-in-progress: false + jobs: release: runs-on: ubuntu-latest @@ -87,6 +94,81 @@ jobs: with: files: dist/*.whl body_path: CHANGELOG_RELEASE_BODY.md + - name: Patch plugin manifests on main to new tag's wheel URLs + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin main + git checkout -B main origin/main + python <<'PY' + import json + import os + import re + import sys + from pathlib import Path + + tag = os.environ["GITHUB_REF_NAME"] + ver = tag.removeprefix("v") + # Tag-to-wheel-version normalization. Covers the prerelease forms used in RELEASE.md + # (`-beta.N`, `-rc.N`, `-alpha.N`); any other shape fails loudly so a future + # tag form doesn't silently produce a URL that points at a non-existent wheel. + prerelease = {"beta": "b", "rc": "rc", "alpha": "a"} + stable = re.fullmatch(r"\d+(?:\.\d+)*", ver) + pre = re.fullmatch(r"(\d+(?:\.\d+)*)-(beta|rc|alpha)\.(\d+)", ver) + if stable: + pep = ver + elif pre: + pep = f"{pre.group(1)}{prerelease[pre.group(2)]}{pre.group(3)}" + else: + sys.exit( + f"Tag {tag!r} not recognized: expected `vN.N.N` or `vN.N.N-{{beta,rc,alpha}}.N`. " + "Extend the normalization in .github/workflows/release.yml if the release flow " + "adopts a new prerelease form." + ) + repo = os.environ["GITHUB_REPOSITORY"] + base = f"https://github.com/{repo}/releases/download/{tag}" + + url_pattern = re.compile( + r"https://github\.com/[\w./-]+/releases/download/v[\w.+\-]+/(pipefy_\w+)-[\w.+]+-py3-none-any\.whl" + ) + + def rewrite(match: re.Match[str]) -> str: + pkg = match.group(1) + return f"{base}/{pkg}-{pep}-py3-none-any.whl" + + mcp_json = Path(".mcp.json") + old = mcp_json.read_text(encoding="utf-8") + new, n = url_pattern.subn(rewrite, old) + if n == 0: + sys.exit( + f".mcp.json on main has no wheel URLs matching {url_pattern.pattern!r}; " + "the release flow expects releases/download/v/pipefy_-*.whl URLs " + "already in place (a no-op rewrite would silently ship stale URLs)." + ) + mcp_json.write_text(new, encoding="utf-8") + + for path, key_path in [ + (".claude-plugin/plugin.json", ("version",)), + (".claude-plugin/marketplace.json", ("plugins", 0, "version")), + ]: + p = Path(path) + data = json.loads(p.read_text(encoding="utf-8")) + obj = data + for k in key_path[:-1]: + obj = obj[k] + obj[key_path[-1]] = ver + p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + PY + git add .mcp.json .claude-plugin/plugin.json .claude-plugin/marketplace.json + if ! git diff --cached --quiet; then + git commit -m "chore(release): point plugin manifests at ${GITHUB_REF_NAME} wheels" + git push origin main + else + echo "No manifest changes to commit" + fi + # Restore the working tree to the tag SHA so any subsequent step that reads tracked + # files sees the release commit, not main's tip. + git checkout "${GITHUB_SHA}" - name: Stage PyPI wheels (SDK + CLI + MCP + Auth + Infra) if: startsWith(github.ref_name, 'v1.') && !contains(github.ref_name, '-') run: | diff --git a/.mcp.json b/.mcp.json index 9081cc5c..24060dc4 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,11 +4,13 @@ "command": "uvx", "args": [ "--with", - "pipefy-sdk @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/sdk", + "https://github.com/pipefy/ai-toolkit/releases/download/v0.2.0-beta.2/pipefy_sdk-0.2.0b2-py3-none-any.whl", "--with", - "pipefy-auth @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/auth", + "https://github.com/pipefy/ai-toolkit/releases/download/v0.2.0-beta.2/pipefy_auth-0.2.0b2-py3-none-any.whl", + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download/v0.2.0-beta.2/pipefy_infra-0.2.0b2-py3-none-any.whl", "--from", - "git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/mcp", + "https://github.com/pipefy/ai-toolkit/releases/download/v0.2.0-beta.2/pipefy_mcp_server-0.2.0b2-py3-none-any.whl", "pipefy-mcp-server" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d898ca..674ccbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Infra**: `security.assert_hostname_resolves_to_public_ips` raises `ValueError` instead of silently passing when `socket.getaddrinfo` returns an empty address list. `UnicodeError` from IDN encoding (e.g. labels > 63 chars) is now caught alongside `socket.gaierror` and surfaced as a `ValueError` with the same `Could not resolve hostname` shape. - **Auth / SDK**: every `PIPEFY_*` env var is validated against a semantically meaningful pattern at settings construction. URL env vars (`PIPEFY_BASE_URL`, `PIPEFY_AUTH_URL`) require `https?://` plus non-whitespace; credential fields (`PIPEFY_TOKEN`, `PIPEFY_SERVICE_ACCOUNT_CLIENT_ID`, `PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET`, `PIPEFY_AUTH_CLIENT_ID`) reject leading / trailing whitespace; `PIPEFY_ORG_ID` must be an ASCII numeric string. There is no longer an empty-string opt-out for any tier — unset the variable to fall back to the default, or use `PIPEFY_DISABLE_STORED_SESSION=1` to turn the stored-session tier off explicitly (see the **Added** entry above). - **SDK / MCP / CLI**: renamed `--graphql-url` CLI flag to `--base-url` to match the new env-var shape. -- **Install snippets**: every `git+https://github.com/gbrlcustodio/pipefy-mcp-server` URL in the root `README.md`, `packages/mcp/README.md`, `packages/cli/README.md`, the shipping `.mcp.json`, and `commands/install.md` now pins `@latest` — a moving git tag the release flow updates to point at the most recent release. Users get release-stable installs by default; `@v0.2.0-beta.1`-style pins remain available for reproducibility. The release process documented in `RELEASE.md` now includes `git tag -f latest && git push --force-with-lease origin latest` as the step that rolls new installs forward. +- **Install snippets**: every `git+https://github.com/gbrlcustodio/pipefy-mcp-server` URL in the root `README.md`, `packages/mcp/README.md`, and `packages/cli/README.md` now pins `@latest` — a moving git tag the release flow updates to point at the most recent release. Users get release-stable installs by default; `@v0.2.0-beta.1`-style pins remain available for reproducibility. The release process documented in `RELEASE.md` now includes `git tag -f latest && git push --force-with-lease origin latest` as the step that rolls new installs forward. +- **Plugin / MCP**: `.mcp.json` resolves through pre-built wheels attached to each GitHub Release (`releases/download/v/pipefy_--py3-none-any.whl`) instead of `uvx --from git+@latest#subdirectory=...`. Drops the full git clone + setuptools build pass on every plugin install or `--refresh`. The release workflow auto-patches `.mcp.json`, `.claude-plugin/plugin.json`, and `.claude-plugin/marketplace.json` to the just-cut tag on each `v*` push and commits the result to `main`. `commands/install.md` (the `/pipefy:install` slash command) now delegates to `install.sh`, which resolves the latest release at runtime, so it no longer needs per-tag rewriting. The paste-into-config MCP-client wiring blocks (Cursor, Claude Desktop, Codex) switch to release-wheel URLs with `` / `` placeholders in `packages/mcp/README.md`'s new **Cursor / Claude Desktop / Codex: paste-into-config wiring** subsection; the curl installer ([#231](https://github.com/gbrlcustodio/pipefy-mcp-server/issues/231)) is the recommended path and writes these configs automatically. Closes #234. ### Deprecated diff --git a/README.md b/README.md index 86cb45d3..3142e316 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ After install, run `pipefy auth login` to authenticate (`--device` on headless s /pipefy:login ``` -`/plugin install pipefy` registers the MCP server and the `/pipefy:install` + `/pipefy:login` slash commands. `/pipefy:install` runs `uv tool install` once to put `pipefy` on PATH (idempotent). `/pipefy:login` runs the OAuth browser flow. For hand-wired setups (paste-into-config blocks per client, the macOS `errSecParam` keychain note, the local-clone alternative for contributors), see [`packages/mcp/README.md`](packages/mcp/README.md). +`/plugin install pipefy` registers the MCP server and the `/pipefy:install` + `/pipefy:login` slash commands. `/pipefy:install` runs the `install.sh` installer (resolves the latest GitHub Release and installs its wheels via `uv tool install`) to put `pipefy` on PATH (idempotent). `/pipefy:login` runs the OAuth browser flow. For hand-wired setups (paste-into-config blocks per client, the macOS `errSecParam` keychain note, the local-clone alternative for contributors), see [`packages/mcp/README.md`](packages/mcp/README.md). ### CLI diff --git a/RELEASE.md b/RELEASE.md index afdeada3..0270a5d6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -36,13 +36,13 @@ The Release workflow requires the git tag (without leading `v`) to **exactly mat git tag vX.Y.Z && git push origin main && git push origin vX.Y.Z ``` -6. Roll the `latest` moving tag to point at the same commit. The README install snippets and shipping `.mcp.json` pin `@latest`, so this is what makes new installs pick up the release: +6. Roll the `latest` moving tag to point at the same commit. The README MCP-client wiring snippets and the `packages/{cli,mcp}/README.md` install commands pin `@latest`, so this is what makes new installs from those copy-paste paths pick up the release: ```bash git tag -f latest vX.Y.Z && git push --force-with-lease origin latest ``` -7. Wait for the **Release** workflow (`.github/workflows/release.yml`) to finish. +7. Wait for the **Release** workflow (`.github/workflows/release.yml`) to finish. As part of the run it auto-commits a `chore(release): point plugin manifests at vX.Y.Z wheels` patch to `main`, pointing `.mcp.json`, `.claude-plugin/plugin.json`, and `.claude-plugin/marketplace.json` at the just-uploaded wheels. The next `dev → main` release PR will conflict on these three files; resolve by taking dev's version (it already carries the bump for the next beta) and letting the workflow re-patch on tag push. 8. Confirm the GitHub Release lists the built wheels (`pipefy_cli-*.whl`, `pipefy_mcp_server-*.whl`, `pipefy_sdk-*.whl`, `pipefy_auth-*.whl`, and `pipefy_infra-*.whl`). Optionally verify install from the tag, for example: ```bash diff --git a/commands/install.md b/commands/install.md index b9e50ec8..530c2066 100644 --- a/commands/install.md +++ b/commands/install.md @@ -1,6 +1,6 @@ --- name: install -description: Install the Pipefy CLI (pipefy-cli + pipefy-auth + pipefy-sdk) as a persistent uv tool. +description: Install the Pipefy CLI (pipefy-auth + pipefy-cli + pipefy-infra + pipefy-sdk) as a persistent uv tool. disable-model-invocation: true --- @@ -9,10 +9,10 @@ If `command -v pipefy` succeeds, surface `pipefy --version` and stop. Otherwise prompt the user to confirm running: ``` -uv tool install --force \ - --with "pipefy-sdk @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/sdk" \ - --with "pipefy-auth @ git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/auth" \ - "git+https://github.com/pipefy/ai-toolkit@latest#subdirectory=packages/cli" +curl -fsSL https://raw.githubusercontent.com/pipefy/ai-toolkit/main/install.sh \ + | sh -s -- --yes --no-skills --client none ``` +The installer resolves the latest GitHub Release at runtime and runs `uv tool install` with the discovered wheel URLs. `--client none` skips MCP-client config writes; the Claude Code plugin's `.mcp.json` already wires the MCP server. + Verify with `pipefy --version`. diff --git a/packages/mcp/README.md b/packages/mcp/README.md index b5855db7..17e6103e 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -106,6 +106,90 @@ If you have a clone of this repo and want the MCP server to use it directly (wit This form also works as a per-project `.mcp.json` if your team shares a clone. Committing `.mcp.json` without secrets (placeholders or env injection) keeps team setups consistent. +### Cursor / Claude Desktop / Codex: paste-into-config wiring + +The [curl installer](https://github.com/pipefy/ai-toolkit/issues/231) writes these configs for you. The blocks below document the shape for users who prefer to wire by hand. Replace `` with the latest release tag (e.g. `v0.2.0-beta.2`) and `` with its PEP 440 wheel form (e.g. `0.2.0b2`); see [Releases](https://github.com/pipefy/ai-toolkit/releases) for current values. + +#### Cursor + +Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project): + +```json +{ + "mcpServers": { + "pipefy": { + "command": "uvx", + "args": [ + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_sdk--py3-none-any.whl", + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_auth--py3-none-any.whl", + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_infra--py3-none-any.whl", + "--from", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_mcp_server--py3-none-any.whl", + "pipefy-mcp-server" + ], + "env": { + "PIPEFY_SERVICE_ACCOUNT_CLIENT_ID": "", + "PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET": "" + } + } + } +} +``` + +#### Claude Desktop + +Config file location: + +| OS | Path | +|----|------| +| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | + +```json +{ + "mcpServers": { + "pipefy": { + "command": "uvx", + "args": [ + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_sdk--py3-none-any.whl", + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_auth--py3-none-any.whl", + "--with", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_infra--py3-none-any.whl", + "--from", + "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_mcp_server--py3-none-any.whl", + "pipefy-mcp-server" + ], + "env": { + "PIPEFY_SERVICE_ACCOUNT_CLIENT_ID": "", + "PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET": "" + } + } + } +} +``` + +#### Codex + +Edit `~/.codex/config.toml`: + +```toml +[mcp_servers.pipefy] +command = "uvx" +args = [ + "--with", "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_sdk--py3-none-any.whl", + "--with", "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_auth--py3-none-any.whl", + "--with", "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_infra--py3-none-any.whl", + "--from", "https://github.com/pipefy/ai-toolkit/releases/download//pipefy_mcp_server--py3-none-any.whl", + "pipefy-mcp-server", +] +env = { PIPEFY_SERVICE_ACCOUNT_CLIENT_ID = "", PIPEFY_SERVICE_ACCOUNT_CLIENT_SECRET = "" } +``` + ### Legacy environment variables `PIPEFY_OAUTH_CLIENT` and `PIPEFY_OAUTH_SECRET` still resolve to the new `PIPEFY_SERVICE_ACCOUNT_*` names with a one-shot stderr deprecation warning. The aliases will be removed in a later `0.2.0-beta.x` release. The `PIPEFY_OAUTH_URL` alias was dropped — set `PIPEFY_BASE_URL` instead. Migration notes: [`docs/MIGRATION.md#service-account-env-var-rename`](../../docs/MIGRATION.md#service-account-env-var-rename).