diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml new file mode 100644 index 0000000..a53e0f7 --- /dev/null +++ b/.github/workflows/publish-python.yml @@ -0,0 +1,105 @@ +name: Publish Python packages to PyPI + +# Publishes two separate Python packages to PyPI via OIDC trusted publishing. +# No API token secrets required — auth is short-lived, exchanged at publish time. +# +# Tag conventions (PyPI publishing paths): +# v — amplifier-agent engine (root pyproject.toml) +# py-v — amplifier-agent-py wrapper (wrappers/python-py/pyproject.toml) +# +# Note: existing tag namespaces continue to work alongside these: +# wrapper-v* → npm trusted publish of the TypeScript wrapper (publish-wrapper.yml) +# v*/wrapper-v* → GitHub Release creation (release-notes.yml) +# +# Bootstrap: before the first release of each package, a PyPI pending trusted +# publisher must be configured for that package. See RELEASING.md for the +# one-time setup checklist. + +on: + push: + tags: + - 'v*' + - 'py-v*' + workflow_dispatch: + +concurrency: + group: publish-python-${{ github.ref }} + cancel-in-progress: false + +jobs: + # ─── Engine (amplifier-agent, root package) ───────────────────────────────── + publish-engine: + name: Publish amplifier-agent engine to PyPI + runs-on: ubuntu-latest + timeout-minutes: 15 + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') + # id-token:write is required for OIDC trusted publishing. + # contents:read is required for actions/checkout. + permissions: + contents: read + id-token: write + environment: pypi + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Verify tag matches pyproject.toml version + if: github.event_name != 'workflow_dispatch' + run: | + PKG_VERSION="$(python3 -c "import tomllib; data=tomllib.load(open('pyproject.toml','rb')); print(data['project']['version'])")" + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match pyproject.toml version ($PKG_VERSION)" + exit 1 + fi + echo "Publishing amplifier-agent@$PKG_VERSION" + + - name: Build sdist and wheel + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # ─── Python wrapper (amplifier-agent-py, wrappers/python-py/) ─────────────── + publish-wrapper: + name: Publish amplifier-agent-py wrapper to PyPI + runs-on: ubuntu-latest + timeout-minutes: 15 + if: startsWith(github.ref, 'refs/tags/py-v') + # id-token:write is required for OIDC trusted publishing. + # contents:read is required for actions/checkout. + permissions: + contents: read + id-token: write + environment: pypi + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Verify tag matches wrappers/python-py pyproject.toml version + run: | + PKG_VERSION="$(python3 -c "import tomllib; data=tomllib.load(open('wrappers/python-py/pyproject.toml','rb')); print(data['project']['version'])")" + TAG_VERSION="${GITHUB_REF_NAME#py-v}" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match wrappers/python-py version ($PKG_VERSION)" + exit 1 + fi + echo "Publishing amplifier-agent-py@$PKG_VERSION" + + # Build from the wrapper subdirectory. Because wrappers/python-py is a + # uv workspace member, uv places output in the workspace-root dist/ dir. + - name: Build sdist and wheel + run: uv build --directory wrappers/python-py + + # pypa/gh-action-pypi-publish defaults to dist/ — correct for this job + # since only wrapper artifacts are present (fresh checkout, no engine build). + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/AGENTS.md b/AGENTS.md index 3889d19..4a3c978 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,12 @@ before any change that touches `protocol/`, a wrapper, or a release tag. | `src/amplifier_agent_lib/` | Transport-free engine library (`Engine`, runtime, persistence, bundle, protocol) | | `src/amplifier_agent_cli/` | Click-based CLI adapter on top of the library | | `wrappers/typescript/` | `amplifier-agent-ts` — published to npm via OIDC on `wrapper-v*` tags | -| `wrappers/python/` | `amplifier-agent-py` — Python wrapper SDK (uv workspace member) | +| `wrappers/python-py/` | `amplifier-agent-py` — Python wrapper SDK (uv workspace member) | | `wrappers/conformance/` | YAML fixtures + Python and TS runners. **Cross-validates both wrappers.** | | `tests/` | Engine/CLI/persistence/migration tests. Integration tests are marked separately. | | `docs/designs/` | Dated design docs (`YYYY-MM-DD-slug.md`). Most non-trivial changes start here. | -| `.github/workflows/` | `ci.yml`, `publish-wrapper.yml`, `release-notes.yml` | +| `.github/workflows/` | `ci.yml`, `publish-wrapper.yml`, `publish-python.yml`, `release-notes.yml` | +| `RELEASING.md` | Release steps for all three artifacts + one-time PyPI trusted publisher setup | No `Makefile`, no `justfile`. Commands are direct `uv run` / `bun run` calls. @@ -80,13 +81,20 @@ you bump it: - Land all of these in **one PR**. Splitting them across PRs leaves `main` in a broken state where one wrapper rejects the engine. -### 2. Three artifacts, three tag namespaces +### 2. Three artifacts, multiple tag namespaces | Artifact | Tag prefix | Published to | |---|---|---| -| Python engine + CLI | `engine-v*` | git (uv tool install from tag) | -| TypeScript wrapper SDK | `wrapper-v*` | npm (OIDC, via `publish-wrapper.yml`) | -| Python wrapper SDK | `wrapper-py-v*` | git (used by Python hosts) | +| Python engine + CLI | `v*` | PyPI (OIDC, via `publish-python.yml`) + GitHub Release | +| TypeScript wrapper SDK | `wrapper-v*` | npm (OIDC, via `publish-wrapper.yml`) + GitHub Release | +| Python wrapper SDK | `py-v*` | PyPI (OIDC, via `publish-python.yml`) | + +> **Note:** Pre-PyPI era used `engine-v*` for git-only engine installs and +> `wrapper-py-v*` for git-only Python wrapper installs. Those old git refs still +> work but are superseded by the PyPI-based install paths above. + +See [`RELEASING.md`](RELEASING.md) for the full step-by-step release procedure and +the one-time PyPI trusted publisher setup checklist. Bumping a version means updating the *correct* `pyproject.toml` / `package.json` **and** the changelog **and** pushing the matching tag namespace. The wrong tag @@ -213,5 +221,5 @@ For a typical change: - For wire-protocol questions: `src/amplifier_agent_lib/protocol/methods.py` is the source of truth. - For wrapper behavior: `wrappers/conformance/` fixtures encode the contract. -- For release process: look at the most recent `chore(release)` PR for the - current pattern. +- For release process: see [`RELEASING.md`](RELEASING.md) for the full procedure + (PyPI tag conventions, trusted publisher setup, version verification steps). diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..23c4de9 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,118 @@ +# Releasing amplifier-agent + +This repo contains three independently-versioned artifacts, each with its own release path. + +| Artifact | Package name | Tag | Published to | +|---|---|---|---| +| Python engine + CLI | `amplifier-agent` | `v` | PyPI (OIDC, `publish-python.yml`) | +| Python wrapper SDK | `amplifier-agent-py` | `py-v` | PyPI (OIDC, `publish-python.yml`) | +| TypeScript wrapper SDK | `amplifier-agent-ts` | `wrapper-v` | npm (OIDC, `publish-wrapper.yml`) | + +GitHub Releases are auto-created by `release-notes.yml` for `v*` and `wrapper-v*` tags. + +--- + +## Engine release (`amplifier-agent`, PyPI) + +```bash +# 1. Bump version in the root pyproject.toml +# Edit [project] version = "X.Y.Z" + +# 2. Commit and merge to main +git add pyproject.toml +git commit -m "chore: bump amplifier-agent to X.Y.Z" +# PR + merge + +# 3. Push the release tag from the tip of main +git fetch origin +git checkout main && git pull +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +This triggers: +- `publish-python.yml` (job `publish-engine`) — builds and publishes `amplifier-agent` to PyPI +- `release-notes.yml` — creates a GitHub Release with generated changelog + +--- + +## Python wrapper release (`amplifier-agent-py`, PyPI) + +```bash +# 1. Bump version in wrappers/python-py/pyproject.toml +# Edit [project] version = "X.Y.Z" + +# 2. Commit and merge to main +git add wrappers/python-py/pyproject.toml +git commit -m "chore: bump amplifier-agent-py to X.Y.Z" +# PR + merge + +# 3. Push the wrapper release tag +git fetch origin +git checkout main && git pull +git tag py-vX.Y.Z +git push origin py-vX.Y.Z +``` + +This triggers `publish-python.yml` (job `publish-wrapper`) — builds and publishes +`amplifier-agent-py` to PyPI. + +--- + +## TypeScript wrapper release (`amplifier-agent-ts`, npm) + +```bash +# See wrappers/typescript/package.json for the version field. +git tag wrapper-vX.Y.Z +git push origin wrapper-vX.Y.Z +``` + +Triggers `publish-wrapper.yml` → npm OIDC publish. + +--- + +## One-time setup: PyPI trusted publishers + +Before the **first** PyPI release of each package, configure a *pending trusted publisher* +on PyPI. This only needs to be done once per package. + +### amplifier-agent (engine) + +At add a pending publisher: + +| Field | Value | +|---|---| +| PyPI project name | `amplifier-agent` | +| GitHub repository owner | `microsoft` | +| GitHub repository name | `amplifier-agent` | +| Workflow filename | `publish-python.yml` | +| Environment name | `pypi` | + +### amplifier-agent-py (Python wrapper) + +At add a second pending publisher: + +| Field | Value | +|---|---| +| PyPI project name | `amplifier-agent-py` | +| GitHub repository owner | `microsoft` | +| GitHub repository name | `amplifier-agent` | +| Workflow filename | `publish-python.yml` | +| Environment name | `pypi` | + +### GitHub Actions environment + +Create an environment named `pypi` in repo settings +(`Settings → Environments → New environment`). No secrets are needed. +Optional: add a required reviewer for an extra approval gate. + +> **Note:** The OIDC trusted-publisher handshake can only be proven by a real tag-triggered +> run after PyPI-side configuration. No local test can fully verify this step. + +--- + +## Cross-component version coordination + +When bumping the protocol version, see the **Cross-component invariants** section in +[`AGENTS.md`](AGENTS.md) — protocol bumps require coordinated wrapper updates and must +land in one PR. diff --git a/pyproject.toml b/pyproject.toml index 1a20e76..dd8a215 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires-python = '>=3.12' license = 'MIT' dependencies = [ 'click>=8.1', - 'amplifier-foundation', + 'amplifier-foundation>=1.0.0', 'pyyaml>=6.0', # G4: `tool-mcp` (declared in bundle.md) Python-imports `mcp` at mount time. # Declaring it here makes the canonical install command