Skip to content

ci(release): publish Python packages with PyPI trusted publishing - #3146

Merged
jpr5 merged 1 commit into
mainfrom
ci/pypi-trusted-publishing
Oct 22, 2026
Merged

jpr5 merged 1 commit into
mainfrom
ci/pypi-trusted-publishing

Conversation

@jpr5

@jpr5 jpr5 commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

What

Switch Python publishing in release / publish (.github/workflows/publish-release.yml) from the long-lived PYPI_API_TOKEN secret to PyPI Trusted Publishing (GitHub OIDC).

  • Both Python publish steps (stable and prerelease canary) no longer set UV_PUBLISH_TOKEN.
  • Both now run uv publish --trusted-publishing always. With always, uv uses trusted publishing unconditionally, so a missing or mismatched publisher on pypi.org fails the step with PyPI's error. The existing PY_PUBLISH_FAILED gate then fails the job before any git tags or GitHub Releases are created.
  • The publish job already had permissions: id-token: write and environment: npm (npm OIDC uses the same job). Neither changes. The trusted publisher binds to this file name and this environment, and a comment now says so.
  • The PYPI_API_TOKEN secret is not deleted. See the rollout order below.

Out of scope: publish-python-preview.yml (TestPyPI previews for PR builds) still uses the repo-scoped TEST_PYPI_API_TOKEN. It is a different index, runs from workflow_run for fork PRs, and can publish packages that do not exist on TestPyPI yet (ag-ui-a2ui-toolkit, ag_ui_antigravity, ag-ui-claude-sdk, ag-ui-claude-managed-agents, ag-ui-langroid, ag_ui_watsonx). Under OIDC, those would each need a pending publisher first. That is tracked as a separate follow-up.

Trusted publisher to add on pypi.org

Add one GitHub publisher per project (Manage project → Publishing → Add a new publisher → GitHub). Every project uses the same values:

PyPI project Owner (GitHub) Repository Workflow file Environment
ag-ui-protocol ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-a2ui-toolkit ag-ui-protocol ag-ui publish-release.yml npm
ag_ui_adk ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-agent-spec ag-ui-protocol ag-ui publish-release.yml npm
ag_ui_antigravity ag-ui-protocol ag-ui publish-release.yml npm
ag_ui_strands ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-claude-sdk ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-claude-managed-agents ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-crewai ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-langgraph ag-ui-protocol ag-ui publish-release.yml npm
ag-ui-langroid ag-ui-protocol ag-ui publish-release.yml npm
ag_ui_watsonx ag-ui-protocol ag-ui publish-release.yml npm

The project list is every "ecosystem": "python" entry in scripts/release/release.config.json, matched against each pyproject.toml name. The PyPI JSON API (ownership.roles) shows all 12 projects have a single Owner: the PyPI user CopilotKit, with no PyPI organization. Only that account can add the publishers. The JSON API does not expose trusted-publisher config. That is visible only in the project's Publishing page. No latest release of any project has a PEP 740 provenance attestation (/integrity/.../provenance returns 404), which fits token uploads only.

RED: current state

  1. The workflow uses the token. On main, both Python publish steps set UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}. The secret is scoped to the npm environment, which has required reviewers copilotkit-admin and jpr5.
  2. The last real Python release used the token. Run 39558581493, job "Publish to npm and PyPI", at 12:06 AM PDT on Oct 21:
      UV_PUBLISH_TOKEN: ***
    === Publishing ag_ui_adk@0.11.0 from integrations/adk-middleware/python ===
    Publishing 2 files to https://upload.pypi.org/legacy/
    Uploading ag_ui_adk-0.11.0-py3-none-any.whl (132.3KiB)
    Uploading ag_ui_adk-0.11.0.tar.gz (124.5KiB)
    === Publishing ag-ui-claude-sdk@0.2.3 from integrations/claude-agent-sdk/python ===
    ...
    
  3. id-token: write was already present. The job already has id-token: write, so nothing had to be added. The same job already gets OIDC tokens in the npm environment. In the same run, at 11:59 PM PDT on Oct 20, npm published with OIDC: npm notice publish Signed provenance statement with source and build information from GitHub Actions / + @ag-ui/claude-agent-sdk@0.0.8.
  4. Local repro: the token path never requests OIDC. A probe ran the Publish Python packages (stable) step exactly as written in the YAML. It used uv 0.12.1, the version the workflow pins, with a simulated GitHub Actions OIDC environment. A local fake ACTIONS_ID_TOKEN_REQUEST_URL endpoint recorded requests. PyPI was real, every credential was fake, and the probe uploaded a dummy ag-ui-tp-probe-do-not-publish dist:
    ## step env keys: UV_PUBLISH_TOKEN, PY_PACKAGES
       if ! uv publish "${DIR}/dist/*"; then
    Uploading ag_ui_tp_probe_do_not_publish-0.0.0-py2.py3-none-any.whl (1.0KiB)
      Caused by: Server returned status code 403 Forbidden. Server says: 403 Invalid or non-existent authentication information.
    ## GitHub OIDC token requests seen by local endpoint: 0
    

GREEN: after this change

  1. Same probe, same step, after the change:
    ## step env keys: PY_PACKAGES
       if ! uv publish --trusted-publishing always "${DIR}/dist/*"; then
    error: Failed to obtain token for trusted publishing
      Caused by: Server returned error code 422 Unprocessable Entity, and the OIDC has an unexpected format.
        Response: {"errors":[{"code":"invalid-payload","description":"malformed JWT"}],"message":"Token request failed"}
    ## step exit status: 0
    ## GITHUB_ENV: PY_PUBLISH_FAILED=true
    ## GitHub OIDC token requests seen by local endpoint: 1
       GET /oidc-token?api-version=2.0&audience=pypi HTTP/1.1  (Authorization: Bearer fake-runner-request-token)
    
    uv read the audience (pypi) from real pypi.org and asked the GitHub OIDC endpoint for an id-token. It then sent that token to real pypi.org /_/oidc/mint-token, which rejected the fake JWT. No upload was attempted. On failure, the step writes PY_PUBLISH_FAILED=true, which fails the job before tags are created.
  2. actionlint 1.7.12 passed on the full list of files that lint-release-workflows.yml checks (exit 0).
  3. zizmor 1.26.1 with .github/zizmor.yml reported no findings on publish-release.yml.
  4. verify-python-toolchain-pins.sh and verify-release-scope-dropdowns.sh passed, and node --test scripts/release/*.test.ts passed 252 of 252 tests with Python 3.12, the version CI uses.

What this does not prove: a real exchange with a GitHub-issued token. That needs the PyPI-side publishers above and the next release. The dry_run dispatch skips the publish job completely. A prerelease dispatch would publish real canary versions to PyPI, so it was not used.

Rollout order

  1. Add the trusted publisher on PyPI. As the CopilotKit PyPI owner, add the tuple above to all 12 projects.
  2. Merge this PR.
  3. The next release verifies it. The first release that bumps a Python package must show uv publish succeeding with no UV_PUBLISH_TOKEN in the step env. If a project was missed in step 1, its upload fails loudly and the job stops before tags are created. Add the publisher and re-run the job.
  4. Then delete the PYPI_API_TOKEN secret from the npm environment and revoke the token on pypi.org.

Drop the long-lived PYPI_API_TOKEN from both Python publish steps and run
uv publish with --trusted-publishing always. The publish job already has
id-token: write and environment: npm for npm OIDC, so each PyPI project's
trusted publisher binds to ag-ui-protocol/ag-ui, publish-release.yml,
environment npm.
@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@3146

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@3146

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@3146

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@3146

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@3146

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@3146

@ag-ui/adk-js

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk-js@3146

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@3146

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@3146

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@3146

@ag-ui/antigravity

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/antigravity@3146

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@3146

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@3146

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@3146

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@3146

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@3146

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@3146

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@3146

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@3146

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@3146

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@3146

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@3146

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@3146

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@3146

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@3146

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@3146

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@3146

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@3146

commit: 4892905

@jpr5
jpr5 marked this pull request as ready for review October 22, 2026 01:52
@jpr5
jpr5 requested a review from a team as a code owner October 22, 2026 01:52
@jpr5
jpr5 merged commit 47300a4 into main Oct 22, 2026
23 checks passed
@jpr5
jpr5 deleted the ci/pypi-trusted-publishing branch October 22, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant