-
Notifications
You must be signed in to change notification settings - Fork 0
216 lines (196 loc) · 9.49 KB
/
Copy pathpublish.yaml
File metadata and controls
216 lines (196 loc) · 9.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
name: Publish
# Publishes `openrouter-agent-sdk` to PyPI using trusted publishing (OIDC) — no
# long-lived API token is stored in this repo.
#
# Publishing is irreversible: a version number can never be reused on PyPI, even
# after a yank. So this workflow is manual-only, defaults to a dry run, and
# refuses to publish a version that already exists on the index.
#
# 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 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` 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
# workflow file from the selected ref, so a branch whose copy drops the guard
# would ignore it. PyPI's trusted publisher pins owner/repo/workflow/environment
# and carries no branch claim, so the environment policy is what actually binds
# publishing to main.
on:
workflow_dispatch:
inputs:
dry-run:
description: "Build and verify, but do not upload. Leave enabled until you have read the summary."
required: false
default: true
type: boolean
permissions:
contents: read
concurrency:
group: publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 20
# 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: pypi
permissions:
contents: read
id-token: write # OIDC token exchange for trusted publishing
# Real publishes only from main; dry runs allowed anywhere so a PR branch can
# verify the artifact without ever reaching the upload step.
if: github.ref == 'refs/heads/main' || inputs.dry-run
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: uv sync --frozen --all-extras
# A broken release is worse than a late one, so re-run the gate here rather
# than trusting that CI passed on some earlier commit. This is the same
# script that gates the port sync.
- name: Verify (lint, types, tests, coverage floor, required API)
run: ./.upstreamer/scripts/verify.sh
# The port tracks upstream HEAD, so it is routinely AHEAD of the release
# whose version number pyproject.toml carries. Publishing from that state
# would ship unreleased upstream work as a released version — permanently,
# since a PyPI version can never be reused.
#
# verify.sh reports this, but only when a sync run has left an upstream
# checkout in tmp/. There is none here, so check it explicitly against the
# public repo rather than letting the guard be silent at the one moment it
# matters most.
- name: Refuse to publish a version the port is ahead of
run: |
set -euo pipefail
VERSION="$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)".*/\1/')"
PORTED="$(grep -m1 '^upstream_commit:' .upstreamer/state.yaml | awk '{print $2}')"
echo "declared version: $VERSION"
echo "ported commit: $PORTED"
rm -rf /tmp/upstream-check
git clone -q --filter=blob:none --no-checkout \
https://github.com/OpenRouterTeam/typescript-agent.git /tmp/upstream-check
git -C /tmp/upstream-check fetch -q --tags origin
TAG="@openrouter/agent@${VERSION}"
if ! git -C /tmp/upstream-check rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::error::Upstream has no release tag $TAG. This package's version tracks the ported @openrouter/agent version, so publishing $VERSION means upstream released it. Wait for the upstream release, or correct the version."
exit 1
fi
AHEAD="$(git -C /tmp/upstream-check rev-list --count "refs/tags/$TAG..$PORTED" 2>/dev/null || echo 0)"
if [ "${AHEAD:-0}" -gt 0 ]; then
echo "::error::The ported commit is $AHEAD commit(s) ahead of the $TAG release tag. Publishing $VERSION now would ship unreleased upstream work under a released version number, and a PyPI version can never be reused. Publish from a commit level with a release tag, or wait for upstream to release what the port has reached."
exit 1
fi
echo "Ported tree is level with $TAG — $VERSION is honest to publish."
- name: Build sdist and wheel
run: |
set -euo pipefail
rm -rf dist
uv build --out-dir dist
ls -l dist
# Catches the metadata problems PyPI rejects on upload — a malformed
# long_description is the classic one, and it fails *after* the version is
# burned if you find out at upload time.
- name: Check metadata renders for PyPI
run: uv run --with twine twine check --strict dist/*
# Proves the artifact, not the source tree: installs the built wheel with
# the repo off sys.path.
- name: Import the public API from the built wheel
run: |
set -euo pipefail
wheel=$(ls dist/*.whl)
uv run --isolated --no-project --with "$wheel" python -c "
from openrouter_agent import call_model, OpenRouter, tool, ModelResult
import importlib.metadata as md
print('imported openrouter-agent-sdk', md.version('openrouter-agent-sdk'))"
# PyPI rejects a re-upload of an existing version with a 400. Failing here
# instead makes the cause obvious ("you forgot to bump") and keeps the
# error out of the upload step.
- name: Confirm this version is not already published
id: version
run: |
set -euo pipefail
VERSION="$(uv run python -c "import importlib.metadata as m; print(m.version('openrouter-agent-sdk'))")"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
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
# matching is worse than no guard — it would wave through the exact
# re-upload it exists to catch. Exit 2 = already published.
if curl -fsSL "$INDEX" -o /tmp/index.json 2>/dev/null; then
python3 - "$VERSION" <<'PY'
import json, sys
version = sys.argv[1]
releases = json.load(open("/tmp/index.json")).get("releases", {})
print("already published:", " ".join(sorted(releases)) or "<none>")
sys.exit(2 if version in releases else 0)
PY
status=$?
if [ "$status" -eq 2 ]; then
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 PyPI yet — this would be the first release."
fi
echo "Version $VERSION is publishable on PyPI."
- name: Summary
run: |
{
echo "## Publish to PyPI"
echo
echo "- Version: \`${{ steps.version.outputs.version }}\`"
echo "- Dry run: **${{ inputs.dry-run }}**"
echo "- Ref: \`${{ github.ref }}\`"
echo
if [ "${{ inputs.dry-run }}" = "true" ]; then
echo "Nothing was uploaded. Artifacts were built and verified only."
echo "Re-run with dry-run disabled to publish."
else
echo "Uploading to PyPI."
fi
echo
echo '```'
ls -l dist
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# Keep the artifacts from a dry run so the exact files that would ship can
# be downloaded and inspected.
- uses: actions/upload-artifact@v4
with:
name: dist-${{ steps.version.outputs.version }}
path: dist/
- name: Publish to PyPI
if: inputs.dry-run == false
uses: pypa/gh-action-pypi-publish@release/v1
with:
print-hash: true