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
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| AWS SDK maintenance policy phases | You keep major versions side by side and retire old ones on a published clock | Phases describe how old a major version is, which tells a reader nothing about how well each language is kept | Your problem is uneven languages rather than ageing majors |
| OpenAPI Generator stability labels per generator | You want readers to see how settled each generator’s templates are | A label on the generator says nothing about your API, your tests, or your response times | You ship the generated code yourself and own its quality |
| Stripe official and community library lists | You support a few languages fully and want to point at others without owning them | Two tiers only, so a language is either fully supported or not supported at all | You 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.
Related how-tos
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
-
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
-
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.jsonis where to write one down. ↩︎ Back to text -
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
typescriptis EXPERIMENTAL, whiletypescript-fetchandtypescript-axiosbeside it carry none, which their own pages spell out as STABLE. ↩︎ Back to text