# 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.

Source: https://voxgig.com/howto/set-support-tiers-for-sdk-languages

- Audience: devrel
- Level: intermediate
- Verified: 2026-10-15
- Published: 2026-10-15

## 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](/howto/evaluate-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](https://endoflife.date/nodejs), 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](https://docs.aws.amazon.com/sdkref/latest/guide/maint-policy.html) | 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](https://openapi-generator.tech/docs/generators/) | 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](https://docs.stripe.com/sdks/community) | 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.

```json title="tiers.json"
    "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:

```bash
node tiers.mjs plan
```

```text output
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`](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions), and
[`continue-on-error`](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations)
lets the tier 3 legs fail without stopping a release:

```yaml title="ci/sdk.yml"
  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:

```bash
node tiers.mjs table
```

```text output
| 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`](https://cli.github.com/manual/gh_run_view).

```bash
node tiers.mjs verify results/release-2.5.0.json
```

```text output
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.

```bash
node --test tiers.test.mjs
```

```text output
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

- [Evaluate a third-tier SDK target before you ship it](/howto/evaluate-a-third-tier-sdk-target)

- [Judge whether an unofficial SDK is safe to depend on](/howto/judge-an-unofficial-sdk-before-adoption)

- [Push one CI template change to five hundred repositories](/howto/push-one-ci-change-to-many-repositories)

## 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/`.

[^1]: The [maintenance policy](https://docs.aws.amazon.com/sdkref/latest/guide/maint-policy.html)
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.

[^2]: Stripe takes the opposite view of the same date. Its
[runtime policy](https://docs.stripe.com/sdks/versioning) 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.

[^3]: The label belongs to the generator, not the language. On the
[generators list](https://openapi-generator.tech/docs/generators/), 22 of the 80 client generators
carry a label. The one named plain `typescript` is
[EXPERIMENTAL](https://openapi-generator.tech/docs/generators/typescript/), while
`typescript-fetch` and `typescript-axios` beside it carry none, which their own pages spell out as
STABLE.