How-to › Ship an SDK

How to set support tiers for your SDK languages#

Write each SDK language's tier as data, generate the CI matrix and the published support table from it, and fail any release that broke a tier's promise.

Audience
DevRel
Level
intermediate
Topic
Target another language
Verified

Read first: Evaluate a third-tier SDK target before you ship it

Your docs list five SDK languages as one row of identical badges. A customer files a bug against the Ruby SDK, which nobody has regenerated since the spec changed in March, because Ruby was dropped from CI after a flaky run. The badge promised the same care as TypeScript, so the customer learns the real arrangement from your issue tracker.

What you get

You will end up with a tier file that generates your CI matrix and your support table, and a check that fails any release that broke a promise. This is for you if you ship SDKs in more languages than you can treat equally.

Short answer

Define two or three tiers in one file. Give each concrete guarantees: how fast a spec change is regenerated, which runtime versions CI tests, which runtime features exist, and how fast an issue gets a reply. Generate the CI matrix and the published support table from that file, so neither drifts from it. Then check each release: every tier one language must build, pass its tests, and publish from the same commit.

You will need

Node 22 or later, a CI service that can take its matrix from an earlier job’s output, and a measured view of each target, which evaluating a third-tier SDK target produces. The workflow tests also need Ruby 3.2 or later. Verified 2026-10-15 against Node 22.22.0 and Ruby 3.3.6. Runtime end-of-life dates come from endoflife.date, saved in runtimes.json on 2026-10-15 so the sample runs offline.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
AWS SDK maintenance policy phasesYou keep major versions side by side and retire old ones on a published clockPhases describe how old a major version is, which tells a reader nothing about how well each language is keptYour problem is uneven languages rather than ageing majors
OpenAPI Generator stability labels per generatorYou want readers to see how settled each generator’s templates areA label on the generator says nothing about your API, your tests, or your response timesYou ship the generated code yourself and own its quality
Stripe official and community library listsYou support a few languages fully and want to point at others without owning themTwo tiers only, so a language is either fully supported or not supported at allYou want a middle tier you maintain on a slower clock

Stripe draws the line at ownership: its community page says Stripe does not support or review the libraries listed there, which leaves no middle tier. AWS draws it in time, with five lifecycle phases per major version, which say nothing about one language lagging another.1 OpenAPI Generator labels the generator rather than the client. The tiers here borrow Stripe’s line and add the middle tier it leaves out.

Write the promises down as data

Each tier is four promises and a list of CI steps. The runtime rule is a word, not a list of versions, so the versions follow the runtimes’ own support dates.

    "1": {
      "name": "Supported",
      "regenerate": "Same release as the spec change",
      "runtimes": "all",
      "features": "Every runtime feature the SDK documents",
      "issues": "Triaged within 2 business days in the SDK repository",
      "ci": ["build", "test", "publish"]
    },
    "2": {
      "name": "Maintained",
      "regenerate": "Within 14 days of the spec change",
      "runtimes": "ends",
      "features": "Retries, timeouts and pagination",
      "issues": "Triaged within 10 business days in the SDK repository",
      "ci": ["build", "test"]
    },

all tests every runtime version still inside its maintainers’ support, ends tests the oldest and newest of those, and newest tests one. The same file assigns each language a tier and a runtime.

Derive the matrix instead of typing it

The plan is computed from the tiers and the support dates:

node tiers.mjs plan
13 jobs on 2026-10-15, from tiers.json and runtimes.json
  typescript  tier 1  node 22 24 26                    build test publish
  python      tier 1  python 3.11 3.12 3.13 3.14 3.15  build test publish
  go          tier 2  go 1.26 1.27                     build test
  ruby        tier 3  ruby 4.0                         build

The snapshot lists Node 20 and Python 3.10, but the plan leaves them out because their support ended before 2026-10-15.2 A test moves the date to 2027-05-01 and watches Node 22 leave the matrix with no edit to tiers.json.

Every command plans for the date the snapshot was fetched, not for today. The matrix moves when somebody refreshes runtimes.json, and a rerun of the workflow for an old commit rebuilds the matrix it ran. A refresh can paste endoflife.date’s answer as it comes, newest first and with eol: false where no end date is set. The rules pick versions by position, so the plan sorts them first; read unsorted, that answer gave Ruby’s newest leg version 3.3.

CI reads the same plan. GitHub Actions accepts a whole matrix from an earlier job’s output through fromJSON, and continue-on-error lets the tier 3 legs fail without stopping a release:

  sdk:
    needs: plan
    runs-on: ubuntu-latest
    # A tier 3 leg may fail without stopping the release of tiers 1 and 2.
    continue-on-error: ${{ matrix.tier == 3 }}
    strategy:
      fail-fast: false
      matrix: ${{ fromJSON(needs.plan.outputs.legs) }}

The publish job lists sdk in its needs, so it runs only after every leg that must pass has passed, within one run on one commit. Each leg also needs a setup step that installs its own runtime version, or every leg tests the runner’s default. This workflow was never run for this page. The tests parse it with Ruby’s YAML library and check its shape against tiers.json.

Publish the table from the same file

Readers decide at the install instructions, so the support table belongs there, generated rather than written:

node tiers.mjs table
| Language | Tier | Tested on | Regenerated | Features | Issues |
| --- | --- | --- | --- | --- | --- |
| typescript | 1, Supported | node 22, 24, 26 | Same release as the spec change | Every runtime feature the SDK documents | Triaged within 2 business days in the SDK repository |
| python | 1, Supported | python 3.11, 3.12, 3.13, 3.14, 3.15 | Same release as the spec change | Every runtime feature the SDK documents | Triaged within 2 business days in the SDK repository |
| go | 2, Maintained | go 1.26, 1.27 | Within 14 days of the spec change | Retries, timeouts and pagination | Triaged within 10 business days in the SDK repository |
| ruby | 3, Community | ruby 4.0 | When a contributor asks | Requests and responses only | Community forum, no response time |

Check each release kept its promises

A green workflow is not the same claim as a kept tier. verify compares one release’s job results with the plan, and treats a missing job and a job from another commit as broken. The results come from the run itself, for example gh run view --json headSha,jobs.

node tiers.mjs verify results/release-2.5.0.json
release 2.5.0 at 7d3a0b5: 3 broken promises
  tier 1  typescript             publish: ran on c41f9e2, not 7d3a0b5
  tier 1  python python 3.15     test: failed
  tier 3  ruby ruby 4.0          build: no result

The TypeScript package went out from the previous release’s commit. Python’s newest runtime failed its tests. The Ruby leg was deleted from the workflow, so CI had nothing to fail. The command exits 1, and the release note should say which promise broke.

Check it worked

Thirteen tests pin the plan, the date rule, the release check, and the shape of the workflow.

node --test tiers.test.mjs
1..13
# tests 13
# suites 0
# pass 13
# fail 0
# cancelled 0
# skipped 0
# todo 0

Two of them run checkWorkflow on the parsed ci/sdk.yml. Excluding Python from the matrix, skipping its tests with a step condition, or deleting its setup step each fails the check, as does letting tier 2 fail.

When it goes wrong

A tier one package ships from an old commit. The publish job reused an artifact, or ran from a branch that had not merged. Compare every result’s commit with the release commit, as verify does, rather than trusting that one run means one commit.

A language drops out of testing without any job failing. Somebody removed its leg to make CI green, which works because CI cannot fail a job it no longer runs. Count planned jobs against results, so that an absent job counts as a broken promise, and run the workflow check, which also catches a step condition that skips one language.

A tier 2 failure blocks the tier 1 release. In this workflow publish needs every leg that is not allowed to fail, and tier 2 legs are among them. If tier 1 should ship regardless, give tier 2 its own job and let publish need only the tier 1 legs.

The table and the matrix disagree. One of them was typed by hand. Generate both from tiers.json.

When not to do this

Do not publish a tier you cannot staff. A two-day triage promise needs a named person per language, and the table makes the promise in public.

Do not let a generator’s label stand in for your tier. OpenAPI Generator’s label grades its templates, so a STABLE generator can still produce a client your API breaks.3

Do not keep a third tier for a language nobody asks about. A community tier still costs a build per release; delete the target and link a community library instead, as Stripe does.

Last verified

Verified 2026-10-15 against Node 22.22.0 and Ruby 3.3.6. Every output block is what the command preceding it printed. The workflow in ci/sdk.yml was parsed and checked by the tests and was not run; the CI results are fixtures in results/.

Footnotes

  1. The maintenance policy says the lifecycle consists of five phases and numbers them from zero, so Developer Preview is Phase 0 and End-of-Support is Phase 4. General Availability lasts at least 24 months, the maintenance announcement comes at least six months ahead, and maintenance itself runs 12 months by default. ↩︎ Back to text

  2. Stripe takes the opposite view of the same date. Its runtime policy marks a language version deprecated at end of life and keeps supporting it for one to two years. Python 3.10 reaches end of life in October 2026, and Stripe keeps it through the March and September 2027 API releases and drops it in March 2028. A grace window is a policy choice, and the runtime rule in tiers.json is where to write one down. ↩︎ Back to text

  3. The label belongs to the generator, not the language. On the generators list, 22 of the 80 client generators carry a label. The one named plain typescript is EXPERIMENTAL, while typescript-fetch and typescript-axios beside it carry none, which their own pages spell out as STABLE. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.