From e181a6dfdf7d13302798755bdfa57032c87ffffc Mon Sep 17 00:00:00 2001 From: LukasParke <5702154+LukasParke@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:17:47 -0500 Subject: [PATCH 1/3] ci(release): dispatch the Python and Go ports on publish (HOP C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python and Go ports of @openrouter/agent track this repo as their reference spec, but nothing told them when a version shipped. Both were generated by hand against 0.7.2 and have sat a minor version behind since — missing HooksManager, versioned state serialization, and the #61-#68 fixes. Adds a HOP C dispatch beside the existing HOP B monorepo dispatch. On a real publish, python-agent and go-agent each receive openrouter-agent-published and open a PR porting the delta. Their pipelines gate the result on a mechanical verifier plus a behavioral parity eval before advancing sync state, so a bad port cannot land silently. Details worth noting: - Sends the release TAG (@openrouter/agent@X.Y.Z), not a branch, so a port reproduces the exact published tree rather than whatever main drifted to. - continue-on-error: the packages are already on npm when this runs, so a failed dispatch must not turn a successful release red. It warns instead, and names the manual recovery path. - !cancelled() so a failed HOP B dispatch doesn't also skip the ports. - Partial failure is tolerated: one port failing still dispatches the other. Reuses the same GH_TOKEN PAT as HOP B, which additionally needs contents:write on OpenRouterTeam/python-agent and OpenRouterTeam/go-agent. Also documents the full publish fan-out in the changeset-versioning skill, since a breaking callModel change now produces port PRs in two other repos. Companion PRs: OpenRouterTeam/python-agent#19, OpenRouterTeam/go-agent#1 --- .agents/skills/changeset-versioning/SKILL.md | 30 ++++++++++- .github/workflows/publish.yaml | 52 ++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/.agents/skills/changeset-versioning/SKILL.md b/.agents/skills/changeset-versioning/SKILL.md index 957f0384..58371329 100644 --- a/.agents/skills/changeset-versioning/SKILL.md +++ b/.agents/skills/changeset-versioning/SKILL.md @@ -77,10 +77,38 @@ Changelogs are auto-generated by `@changesets/changelog-github` and include: The changelog is written to `CHANGELOG.md` during the version step. +## What a publish triggers downstream + +A real `@openrouter/agent` publish fans out to three repos via +`repository_dispatch` (event type `openrouter-agent-published`). All of these run +*after* npm already has the package, so none of them can fail the release: + +| Hop | Target | Effect | +| --- | --- | --- | +| B | `openrouter-web` | Bumps the pinned `@openrouter/agent` used by server tools | +| C | `python-agent` | Opens a PR porting the release delta into the Python port | +| C | `go-agent` | Opens a PR porting the release delta into the Go port | + +The port repos treat this repo as their **reference spec**: they run +[Upstreamer](https://github.com/mountgram/upstreamer) against the release tag and +gate the generated port on a mechanical verifier plus a behavioral parity eval +before advancing their sync state. Their contracts live at +`.upstreamer/upstreamer.md` in each repo. + +Hop C is dispatched with the release tag (e.g. `@openrouter/agent@0.8.0`), not a +branch, so a port reproduces the exact published tree. If a dispatch fails it logs +a warning rather than failing the release — recover by running the port repo's +**Upstreamer Port** workflow manually with that tag as `ref`, or wait for its +weekly cron. + +Practical consequence: a breaking change to the `callModel` surface will produce +port PRs in two other repos on release. If the ports need a coordinated change, +sequence it the same way `@openrouter/sdk` coordination works. + ## Configuration - `.changeset/config.json` — Changesets configuration -- `.github/workflows/publish.yaml` — Release workflow (workflow_dispatch) +- `.github/workflows/publish.yaml` — Release workflow (workflow_dispatch); also carries the HOP B/C downstream dispatches - `.github/workflows/ci.yaml` — PR validation (lint, typecheck, test) ## Common Commands diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 12f15184..12e98882 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -147,3 +147,55 @@ jobs: -F "client_payload[version]=${{ steps.published.outputs.version }}" \ -F "client_payload[source_run_url]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" echo "Dispatched openrouter-agent-published (version ${{ steps.published.outputs.version }}) to openrouter-web" + + # HOP C trigger: tell the Python and Go ports of @openrouter/agent that a + # new version shipped, so each opens a PR porting the delta. Those repos run + # Upstreamer against this repo as their reference spec (see their + # .upstreamer/upstreamer.md) and gate the result on a mechanical verifier + # plus a parity eval before advancing their sync state. + # + # Deliberately release-triggered rather than a cron over this repo's main: + # ports track published versions, so the delta they see always lands on a + # release boundary instead of a mid-flight commit. + # + # `ref` is the release tag changesets created, so a port reproduces the exact + # published tree rather than whatever main has drifted to since. + # + # This step must not fail the release: the packages are already on npm by + # now, so a red job here would misreport a successful publish. A failed + # dispatch is a warning — re-run it from the port repo's own + # "Upstreamer Port" workflow (workflow_dispatch), or let its weekly cron + # pick the change up. + # + # `!cancelled()` keeps this independent of the HOP B step above: a failed + # monorepo dispatch should not also stop the ports from being told. + - name: Dispatch port repos + if: ${{ !cancelled() && !inputs.dry-run && steps.published.outputs.version != '' }} + continue-on-error: true + env: + # Same cross-repo PAT as HOP B; additionally needs contents:write on + # OpenRouterTeam/python-agent and OpenRouterTeam/go-agent. + GH_TOKEN: ${{ secrets.GH_TOKEN }} + VERSION: ${{ steps.published.outputs.version }} + run: | + set -uo pipefail + TAG="@openrouter/agent@${VERSION}" + FAILED="" + + for REPO in python-agent go-agent; do + if gh api "repos/OpenRouterTeam/${REPO}/dispatches" \ + -f event_type=openrouter-agent-published \ + -F "client_payload[version]=${VERSION}" \ + -F "client_payload[ref]=${TAG}" \ + -F "client_payload[source_run_url]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"; then + echo "Dispatched openrouter-agent-published ($TAG) to ${REPO}" + else + echo "::warning::Failed to dispatch to OpenRouterTeam/${REPO}. Trigger its Upstreamer Port workflow manually with ref=${TAG}, or wait for its weekly cron." + FAILED="$FAILED ${REPO}" + fi + done + + # Report, but never fail the release — npm is already updated. + if [ -n "$FAILED" ]; then + echo "Port dispatch incomplete:$FAILED" + fi From d259bfc80ecf33d37ecd26d977ac782abe458bc7 Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:42:35 -0500 Subject: [PATCH 2/3] fix(release): address review findings on HOP C dispatch - Use -f (--raw-field) for all dispatch payload fields: -F treats values starting with @ (the release tag) as filenames, which made every port dispatch fail before the request was sent - Push release tags on the manual mode=publish path so the dispatched ref actually exists on the remote - Restore set -e; the gh api call sits in an if-condition and is already -e-exempt, so the rest of the script stays strict - Pass source_run_url via env like VERSION instead of inline ${{ }} Co-Authored-By: Claude Fable 5 --- .github/workflows/publish.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 12e98882..e55e2679 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -90,6 +90,13 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + # `changeset publish` only creates release tags locally; on the push path + # changesets/action pushes them, but nothing does here. Push them so the + # HOP C dispatch below names a ref the port repos can actually resolve. + - name: Push release tags (manual publish) + if: github.event_name == 'workflow_dispatch' && inputs.mode == 'publish' && !inputs.dry-run + run: git push origin --tags + # `changeset publish` has no native --dry-run. Fall back to pnpm's # recursive dry-run, which simulates publishing every workspace package # rather than only the ones changesets would pick. Output set may be @@ -177,17 +184,21 @@ jobs: # OpenRouterTeam/python-agent and OpenRouterTeam/go-agent. GH_TOKEN: ${{ secrets.GH_TOKEN }} VERSION: ${{ steps.published.outputs.version }} + SOURCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - set -uo pipefail + set -euo pipefail TAG="@openrouter/agent@${VERSION}" FAILED="" for REPO in python-agent go-agent; do + # -f (--raw-field) everywhere: -F treats values starting with `@` + # (like TAG) as filenames to read, which would error out before the + # request is even sent. if gh api "repos/OpenRouterTeam/${REPO}/dispatches" \ -f event_type=openrouter-agent-published \ - -F "client_payload[version]=${VERSION}" \ - -F "client_payload[ref]=${TAG}" \ - -F "client_payload[source_run_url]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"; then + -f "client_payload[version]=${VERSION}" \ + -f "client_payload[ref]=${TAG}" \ + -f "client_payload[source_run_url]=${SOURCE_RUN_URL}"; then echo "Dispatched openrouter-agent-published ($TAG) to ${REPO}" else echo "::warning::Failed to dispatch to OpenRouterTeam/${REPO}. Trigger its Upstreamer Port workflow manually with ref=${TAG}, or wait for its weekly cron." From d5c0db5a2c019d6d99d5317e1e90aea1f308dc24 Mon Sep 17 00:00:00 2001 From: LukasParke Date: Wed, 29 Jul 2026 11:17:35 -0500 Subject: [PATCH 3/3] fix(release): fall back to commit SHA when the release tag is not on origin Address review findings from cortex and Devin on the HOP C dispatch: - Make the manual-publish tag push non-fatal. It runs after packages are already on npm, so a rejected push (tag protection, or a re-run where the tag exists at a different commit) turned a successful publish red and, because the HOP B/C steps carry no status guard, skipped both dispatches. - Verify the tag is on origin before dispatching it, falling back to the run's commit SHA. Previously the manual path could dispatch a ref the port repos cannot resolve, making them fail on checkout instead of degrading. --- .github/workflows/publish.yaml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e55e2679..69ae612d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -93,8 +93,15 @@ jobs: # `changeset publish` only creates release tags locally; on the push path # changesets/action pushes them, but nothing does here. Push them so the # HOP C dispatch below names a ref the port repos can actually resolve. + # + # Best-effort: the packages are already on npm by the time this runs, so a + # rejected push (tag protection ruleset, or a re-run where the tag exists + # on origin at a different commit) must not turn a successful publish red + # — and must not skip the HOP B/C dispatch steps below. HOP C independently + # verifies the tag is on the remote and falls back to the commit SHA. - name: Push release tags (manual publish) if: github.event_name == 'workflow_dispatch' && inputs.mode == 'publish' && !inputs.dry-run + continue-on-error: true run: git push origin --tags # `changeset publish` has no native --dry-run. Fall back to pnpm's @@ -166,7 +173,9 @@ jobs: # release boundary instead of a mid-flight commit. # # `ref` is the release tag changesets created, so a port reproduces the exact - # published tree rather than whatever main has drifted to since. + # published tree rather than whatever main has drifted to since. If that tag + # never reached origin, the step falls back to this run's commit SHA — same + # tree, still resolvable. # # This step must not fail the release: the packages are already on npm by # now, so a red job here would misreport a successful publish. A failed @@ -190,6 +199,19 @@ jobs: TAG="@openrouter/agent@${VERSION}" FAILED="" + # The tag is only usable as a ref if it actually reached origin. On the + # push path changesets/action pushes it; on the manual publish path the + # push is best-effort and may have been rejected or skipped. Dispatching + # an unresolvable ref would make the ports fail on checkout rather than + # degrade, so fall back to this run's commit SHA — which points at the + # same published tree. + if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + REF="$TAG" + else + REF="${{ github.sha }}" + echo "::warning::${TAG} is not on origin; dispatching ref=${REF} instead." + fi + for REPO in python-agent go-agent; do # -f (--raw-field) everywhere: -F treats values starting with `@` # (like TAG) as filenames to read, which would error out before the @@ -197,11 +219,11 @@ jobs: if gh api "repos/OpenRouterTeam/${REPO}/dispatches" \ -f event_type=openrouter-agent-published \ -f "client_payload[version]=${VERSION}" \ - -f "client_payload[ref]=${TAG}" \ + -f "client_payload[ref]=${REF}" \ -f "client_payload[source_run_url]=${SOURCE_RUN_URL}"; then - echo "Dispatched openrouter-agent-published ($TAG) to ${REPO}" + echo "Dispatched openrouter-agent-published (ref ${REF}) to ${REPO}" else - echo "::warning::Failed to dispatch to OpenRouterTeam/${REPO}. Trigger its Upstreamer Port workflow manually with ref=${TAG}, or wait for its weekly cron." + echo "::warning::Failed to dispatch to OpenRouterTeam/${REPO}. Trigger its Upstreamer Port workflow manually with ref=${REF}, or wait for its weekly cron." FAILED="$FAILED ${REPO}" fi done