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
32 changes: 31 additions & 1 deletion .agents/skills/changeset-versioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,40 @@ 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 that tag is not on
origin — the manual publish path pushes tags best-effort — the dispatch falls
back to the publishing run's commit SHA, which is the same 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 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 (`push: main` + `workflow_dispatch`)
- `.github/workflows/publish.yaml` — Release workflow (`push: main` + `workflow_dispatch`); also carries the HOP B/C downstream dispatches
- `.github/workflows/ci.yaml` — PR validation (lint, typecheck, test)

## Common Commands
Expand Down
85 changes: 85 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ jobs:
if: github.event_name == 'workflow_dispatch' && inputs.mode == 'publish' && !inputs.dry-run
run: pnpm exec changeset publish --no-git-checks

# `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
Comment thread
LukasParke marked this conversation as resolved.

# `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
Expand Down Expand Up @@ -195,3 +209,74 @@ 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. 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
# 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 }}
SOURCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
TAG="@openrouter/agent@${VERSION}"
Comment thread
LukasParke marked this conversation as resolved.
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
# 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]=${REF}" \
-f "client_payload[source_run_url]=${SOURCE_RUN_URL}"; then
Comment on lines +267 to +271

@devin-ai-integration devin-ai-integration Bot Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Nested client_payload[...] bracket syntax with gh api

HOP C builds the dispatch payload with -f 'client_payload[version]=...', -f 'client_payload[ref]=...', etc. This mirrors the pre-existing HOP B step (.github/workflows/publish.yaml:152-155), which uses the same bracket notation. Whether gh api expands client_payload[key]=value into a nested JSON object versus a flat top-level key literally named client_payload[key] depends on the gh CLI version's behavior. Since HOP B already relies on this and is presumably verified in production, HOP C is consistent with it and I did not flag it — but if HOP B were ever found to send a flat/literal key, HOP C would inherit the same issue for the ref payload the ports depend on.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this empirically rather than leaving it as an open question — the bracket notation does nest correctly.

Ran the same call shape against gh 2.96.0 with GH_DEBUG=api and the serialized request body was:

{
  "client_payload": { "ref": "@openrouter/agent@1.2.3", "version": "1.2.3" },
  "event_type": "..."
}

So -f 'client_payload[key]=value' expands into a nested object, not a flat literal key. HOP B's production behavior and HOP C's ref payload are both fine on this axis. Leaving the thread open for a human to resolve since it was a question rather than a defect.

Worth noting your -f vs -F distinction still matters and is handled: ref starts with @, so -F would have treated it as a filename. HOP C uses -f throughout.

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=${REF}, or wait for its weekly cron."
FAILED="$FAILED ${REPO}"
fi
done
Comment thread
LukasParke marked this conversation as resolved.

# Report, but never fail the release — npm is already updated.
if [ -n "$FAILED" ]; then
echo "Port dispatch incomplete:$FAILED"
fi