diff --git a/.agents/skills/changeset-versioning/SKILL.md b/.agents/skills/changeset-versioning/SKILL.md index 4645a6a7..4f955185 100644 --- a/.agents/skills/changeset-versioning/SKILL.md +++ b/.agents/skills/changeset-versioning/SKILL.md @@ -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 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 92a35b68..3f5329f5 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -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 + # `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 @@ -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}" + 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 + 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 + + # Report, but never fail the release — npm is already updated. + if [ -n "$FAILED" ]; then + echo "Port dispatch incomplete:$FAILED" + fi