Skip to content
Merged
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
56 changes: 22 additions & 34 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@ name: Publish
# Release procedure:
# 1. Land the version bump (pyproject.toml `version`). For a port sync that is
# done by scripts/upstream; otherwise edit it in a PR.
# 2. Run this workflow with target=testpypi to rehearse (optional but cheap).
# 3. Run with target=pypi, dry-run=true, and read the summary.
# 4. Run with target=pypi, dry-run=false to release.
# 2. Run with dry-run=true and read the summary.
# 3. Run with dry-run=false to release.
#
# There is deliberately no TestPyPI target. It required a second, separate
# trusted-publisher registration on test.pypi.org (its own site, own account, own
# publisher config), and having only the pypi.org one configured meant the
# rehearsal failed with `invalid-publisher` while the real path was fine — a
# rehearsal that fails for reasons the real run would not have is worse than no
# rehearsal. The dry run below covers what the rehearsal was actually for:
# everything except the upload itself.
#
# One-time setup on PyPI, before the first real publish — the workflow cannot do
# this for you:
# PyPI → the project (or "pending publisher" if it does not exist yet) →
# Publishing → add a GitHub trusted publisher with
# owner: OpenRouterTeam repo: python-agent
# workflow: publish.yaml environment: pypi
# Then create the `pypi` (and `testpypi`) environment in repo Settings, and set
# its deployment branch policy to `main`.
# Then create the `pypi` environment in repo Settings and set its deployment
# branch policy to `main`.
#
# That environment branch policy is the real ref restriction. The `if:` guard
# below stops accidents, not a determined actor: workflow_dispatch runs the
Expand All @@ -33,14 +40,6 @@ name: Publish
on:
workflow_dispatch:
inputs:
target:
description: "Index to publish to. Rehearse on testpypi first."
required: true
type: choice
options:
- testpypi
- pypi
default: testpypi
dry-run:
description: "Build and verify, but do not upload. Leave enabled until you have read the summary."
required: false
Expand All @@ -51,7 +50,7 @@ permissions:
contents: read

concurrency:
group: publish-${{ inputs.target }}
group: publish
cancel-in-progress: false

jobs:
Expand All @@ -61,7 +60,7 @@ jobs:
# Selects the trusted-publisher identity and, via its deployment branch
# policy, restricts which refs may publish. A dry run still targets the
# environment so an approval gate is exercised in rehearsal too.
environment: ${{ inputs.target }}
environment: pypi
permissions:
contents: read
id-token: write # OIDC token exchange for trusted publishing
Expand Down Expand Up @@ -155,11 +154,7 @@ jobs:
set -euo pipefail
VERSION="$(uv run python -c "import importlib.metadata as m; print(m.version('openrouter-agent-sdk'))")"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [ "${{ inputs.target }}" = "pypi" ]; then
INDEX="https://pypi.org/pypi/openrouter-agent-sdk/json"
else
INDEX="https://test.pypi.org/pypi/openrouter-agent-sdk/json"
fi
INDEX="https://pypi.org/pypi/openrouter-agent-sdk/json"
# Collision test done in Python, not by word-splitting a shell string:
# the shell form is subtly non-portable (zsh does not split unquoted
# variables the way bash does), and a guard that silently stops
Expand All @@ -175,21 +170,21 @@ jobs:
PY
status=$?
if [ "$status" -eq 2 ]; then
echo "::error::Version $VERSION is already published on ${{ inputs.target }}. A PyPI version can never be reused — bump the version in pyproject.toml."
echo "::error::Version $VERSION is already published on PyPI. A version can never be reused — bump the version in pyproject.toml."
exit 1
elif [ "$status" -ne 0 ]; then
echo "::error::Could not determine published versions (exit $status). Refusing to publish blind."
exit 1
fi
else
echo "Project not on ${{ inputs.target }} yet — this would be the first release."
echo "Project not on PyPI yet — this would be the first release."
fi
echo "Version $VERSION is publishable on ${{ inputs.target }}."
echo "Version $VERSION is publishable on PyPI."

- name: Summary
run: |
{
echo "## Publish ${{ inputs.target }}"
echo "## Publish to PyPI"
echo
echo "- Version: \`${{ steps.version.outputs.version }}\`"
echo "- Dry run: **${{ inputs.dry-run }}**"
Expand All @@ -199,7 +194,7 @@ jobs:
echo "Nothing was uploaded. Artifacts were built and verified only."
echo "Re-run with dry-run disabled to publish."
else
echo "Uploading to ${{ inputs.target }}."
echo "Uploading to PyPI."
fi
echo
echo '```'
Expand All @@ -211,18 +206,11 @@ jobs:
# be downloaded and inspected.
- uses: actions/upload-artifact@v4
with:
name: dist-${{ inputs.target }}-${{ steps.version.outputs.version }}
name: dist-${{ steps.version.outputs.version }}
path: dist/

- name: Publish to TestPyPI
if: inputs.target == 'testpypi' && inputs.dry-run == false
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
print-hash: true

- name: Publish to PyPI
if: inputs.target == 'pypi' && inputs.dry-run == false
if: inputs.dry-run == false
uses: pypa/gh-action-pypi-publish@release/v1
with:
print-hash: true
18 changes: 13 additions & 5 deletions PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,27 @@ this repo.

```
1. Land the version bump in pyproject.toml (a port sync does this).
2. Run Publish with target=testpypi to rehearse.
3. Run with target=pypi, dry-run=true — read the summary.
4. Run with target=pypi, dry-run=false to release.
2. Run Publish with dry-run=true — read the summary.
3. Run with dry-run=false to release.
```

There is deliberately **no TestPyPI rehearsal**. TestPyPI is a separate site with
its own account and its own trusted-publisher config, so it needed a second
registration; with only the pypi.org one in place the rehearsal failed with
`invalid-publisher` while the real path was fine. A rehearsal that fails for
reasons the real run would not is worse than no rehearsal. The dry run covers what
it was actually for — verify, build, `twine check --strict`, isolated wheel import,
and the already-published and ahead-of-release guards — everything except the
upload itself.

Before the first real publish, two things must be set up by hand — the workflow
cannot do them for you:

1. **On PyPI**: add a GitHub trusted publisher (owner `OpenRouterTeam`, repo
`python-agent`, workflow `publish.yaml`, environment `pypi`). If the project
does not exist yet, add it as a *pending* publisher.
2. **In repo Settings**: create the `pypi` and `testpypi` environments and set each
one's deployment branch policy to `main`.
2. **In repo Settings**: create the `pypi` environment and set its deployment
branch policy to `main`.

That branch policy is the real ref restriction. PyPI's trusted publisher pins
owner/repo/workflow/environment but carries no branch claim, and
Expand Down
Loading