SDK Contract & Release Fan-out¶
This page describes how downstream client SDKs (stromboli-go, stromboli-ts, mcp-server-stromboli, and the upcoming n8n-nodes-stromboli) stay in sync with the server's OpenAPI surface.
The contract is intentionally one-way: when a tag is pushed on this repo, a repository_dispatch fires to each registered SDK. The SDK receives the new spec, regenerates its typed client, and opens a PR titled chore: sync to stromboli vX.Y.Z. If the SDK's own CI passes, the PR can auto-merge. No human in the loop unless the regeneration introduces an actual breaking change.
Why a dispatch and not a monorepo¶
Each SDK has its own ecosystem expectations — npm publishes for TS, pkg.go.dev for Go, n8n's community-nodes registry, MCP's directory. A monorepo merge would force every consumer to adopt this repo's release cadence and tooling. Independent repos with a contract bus stay autonomous.
The protocol¶
When this repo's release workflow finishes successfully on a tag push, it sends one repository_dispatch per SDK:
POST /repos/{owner}/{sdk-repo}/dispatches
{
"event_type": "stromboli-released",
"client_payload": {
"version": "v0.5.3-alpha",
"swagger_url": "https://raw.githubusercontent.com/.../v0.5.3-alpha/docs/swagger/swagger.json",
"release_url": "https://github.com/.../releases/tag/v0.5.3-alpha"
}
}
The fan-out matrix lives in .github/workflows/release.yml under the notify-sdks job. Adding a new SDK repo is a one-line matrix entry on the server side.
Adding a new SDK to the fan-out¶
Two steps:
- Server side (this repo): add the repo name to the
notify-sdksjob's matrix. PR-able by anyone with write access here. - SDK side (the new repo): drop in the receiver workflow below.
The dispatch will fire whether or not the SDK has a receiver — clients that haven't wired one up just ignore the event.
Receiver template¶
Drop this into the SDK's .github/workflows/sync-stromboli.yml. The codegen step is the only part you customize per ecosystem.
Enable PR creation on the SDK repo first
GitHub Actions is not allowed to create or approve pull requests by default — even with pull-requests: write in the workflow permissions block. The peter-evans/create-pull-request step will fail with GitHub Actions is not permitted to create or approve pull requests until you flip a repo-level setting:
- Go to
Settings → Actions → Generalon the SDK repo - Under Workflow permissions, check Allow GitHub Actions to create and approve pull requests
- Save
This needs to be done once per SDK repo that adds a receiver. URL pattern: https://github.com/<owner>/<sdk>/settings/actions.
name: Sync OpenAPI from stromboli
on:
repository_dispatch:
types: [stromboli-released]
workflow_dispatch:
inputs:
version:
description: 'stromboli tag (e.g. v0.5.3-alpha)'
required: true
swagger_url:
description: 'Override swagger URL (optional)'
required: false
permissions:
contents: write
pull-requests: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Resolve dispatch payload
id: payload
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
SWAGGER_URL="${{ inputs.swagger_url }}"
if [ -z "$VERSION" ]; then
# Fall back to whatever stromboli last cut. Two gotchas:
# 1. We use curl + REST API, not `gh release view`,
# because GITHUB_TOKEN is scoped to THIS repo and
# cross-repo `gh` reads come back as 404.
# 2. We hit /releases (plural) not /releases/latest —
# the latter skips prereleases, so it 404s on a repo
# that only ships -alpha / -beta tags.
VERSION=$(curl -fsSL https://api.github.com/repos/tomblancdev/stromboli/releases | jq -r '[.[] | select(.draft == false)][0].tag_name')
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "::error::Could not resolve latest stromboli release tag"
exit 1
fi
fi
else
VERSION="${{ github.event.client_payload.version }}"
SWAGGER_URL="${{ github.event.client_payload.swagger_url }}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "swagger_url=$SWAGGER_URL" >> "$GITHUB_OUTPUT"
- name: Fetch new swagger
run: |
mkdir -p .stromboli-sync
URL="${{ steps.payload.outputs.swagger_url }}"
if [ -z "$URL" ]; then
URL="https://raw.githubusercontent.com/tomblancdev/stromboli/${{ steps.payload.outputs.version }}/docs/swagger/swagger.json"
fi
curl -fsSL "$URL" -o .stromboli-sync/swagger.json
jq -r '.info.version' .stromboli-sync/swagger.json
# ─── PER-ECOSYSTEM CODEGEN ─── (replace with whatever your client uses)
#
# Go SDK: swag init / openapi-generator-cli generate -g go
# TS SDK: npx openapi-typescript .stromboli-sync/swagger.json -o src/api.ts
# MCP: custom — usually re-emits tool descriptors from the spec
# n8n: regen the resource/operation descriptions
#
# The job opens a PR with whatever this step produces; if codegen
# bails, the PR doesn't open and the failure shows up as a CI run.
- name: Regenerate typed client
run: |
# TODO: ecosystem-specific codegen here
echo "implement me"
- name: Record compatibility marker
run: |
echo "${{ steps.payload.outputs.version }}" > STROMBOLI_COMPAT
- name: Open PR with regenerated client
uses: peter-evans/create-pull-request@v7
with:
commit-message: 'chore: sync to stromboli ${{ steps.payload.outputs.version }}'
title: 'chore: sync to stromboli ${{ steps.payload.outputs.version }}'
body: |
Automated regeneration triggered by stromboli release.
- **Server tag:** ${{ steps.payload.outputs.version }}
- **Spec source:** ${{ steps.payload.outputs.swagger_url }}
- **Release notes:** ${{ github.event.client_payload.release_url }}
If the diff is mechanical (new field added to a request/response),
this PR can auto-merge once CI is green. If the regeneration
introduces a breaking change in the public surface, hold for
human review.
branch: sync-stromboli-${{ steps.payload.outputs.version }}
delete-branch: true
labels: stromboli-sync, automated
Required repo secret¶
The fan-out job in this repo needs a token with repo scope on every target SDK. Two ways to provision:
| Option | Pros | Cons |
|---|---|---|
Personal Access Token (classic, repo scope, stored as SDK_DISPATCH_TOKEN) |
Simple, works today | Tied to a single account; expires |
GitHub App with actions: write + metadata: read on each SDK repo, generate an installation token at job time |
Auditable, tied to the org | More setup; needs a small token-mint step |
Start with a PAT (Settings → Secrets and variables → Actions → New repository secret, name SDK_DISPATCH_TOKEN). Move to a GitHub App if/when secret rotation becomes a chore.
If the secret is missing, the fan-out step logs a warning and skips — the release itself never fails because of the SDK side. That's deliberate: a release should never block on downstream tooling, only notify it.
Compatibility marker¶
Each SDK is encouraged to write the server tag it was last synced against to a STROMBOLI_COMPAT file (or the README). That way an SDK consumer can answer "does stromboli-go@v0.5.3 work with my stromboli@v0.5.3 server?" by checking one file.
The receiver template above sets it automatically.
Future layers (not yet implemented)¶
- Layer 2: version coupling. Have the SDK auto-tag its release at the same version (
stromboli-go@v0.5.3) once the sync PR merges, so the version mapping is unambiguous. - Layer 3: contract tests in this repo's CI. A matrix job that clones each SDK on every PR here, points its e2e tests at a localhost server built from the PR commit, and fails the PR if anything regresses. Catches breaking changes before they ever reach a tag.
Both are bigger lifts; land Layer 1 (this page) first and see how often Layers ⅔ would actually pay off.