How-to › Document and support developers

How to write a production-readiness checklist for a new SDK#

Write the checklist your team signs before a first public SDK release: an owner and an exit criterion on every item, and a script that fails the items it can read.

Audience
Platform team
Level
advanced
Topic
Measure and audit developer experience
Verified

The launch review for your first SDK is a page of ticks that name no owner. The release ships with a candidate version string and no changelog entry for it. Two weeks later a dependency CVE lands, so the team looks for the runbook for pulling a release, which turns out to be one sentence long.

What you get

You will end up with a checklist whose every item has an owner, an exit criterion, and a check, and a review script that blocks the publish while any item fails. This is for you if you sign off a first public SDK release.

Short answer

Write one checklist item per launch risk, each with a named owner, an exit criterion, and either a check CI runs or a sign-off a person records. Cover the build, the language conventions, three test layers, the docs each registry shows, the release pipeline, the CVE runbook, and the support path. Run the list as a script that prints every item with its result and blocks the publish while any item fails.

You will need

Node 22 or later, a repository holding the SDK with branch protection on its default branch, and a release pipeline that can publish to the registry. Verified 2026-10-21 against Node 22.22.0. The sample runs the checklist against a fixture repository for a Node SDK, with a lockfile, three test directories, a release workflow, and a Scorecard result in the shape of scorecard --format json. The review borrows its shape from the Production Readiness Review in the SRE book.1

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
Google SRE Production Readiness ReviewA team that already runs services under SRE and wants the SDK reviewed in the same vocabularySix areas written for a running service, so capacity planning and emergency response need translating into registry termsThe reviewers have never run a PRR, and the SDK has no on-call to hand it to
Home-grown checklist enforced in CIA list that fits this SDK exactly, with every item that can be automated run as a required status checkYour own maintenance: an item nobody automates rots into a tick, and the list is only as good as its ownersYou need a result somebody outside the team will recognize without reading the list
OpenSSF ScorecardSecurity hygiene of the repository scored the same way for every public project, with a badge to showOnly security hygiene, read from the repository: nothing about docs, tests, or whether the client matches the specThe repository is private or not on GitHub, where several checks cannot run

The PRR is the most thorough of the three and the least portable. Its six areas describe a service with a pager, but a library has neither traffic nor an on-call rota. Scorecard is automated and public, and it scores only the repository’s security hygiene. The home-grown list is the one that fits, and it is also the one that rots. That is why every item below that can be automated is run by a script rather than ticked by a person.

Write each item as an owner, an exit criterion, and a check

An item on this list is a sentence a named person can be asked about, with a condition that says when it is done and a way to find out. Three fields carry that: owner, exit, and check. The check names a function the script runs, or manual, in which case the item passes only with a recorded sign-off. The fourteen items cover the build, the language conventions, the three test layers, the registry docs, the release pipeline, the supply chain, the CVE runbook, and the support path.

The first three items, one of them manual:

  { "id": "build.lockfile", "area": "build", "item": "The lockfile is committed and current", "owner": "release-eng",
    "exit": "package-lock.json is committed with lockfileVersion 3", "check": "lockfile" },
  { "id": "build.reproducible", "area": "build", "item": "Two builds of the same commit produce the same bytes", "owner": "release-eng",
    "exit": "two runs of the build give one SHA-256", "check": "reproducible" },
  { "id": "conventions.idiomatic", "area": "conventions", "item": "A reviewer fluent in the language signed off the public surface", "owner": "node-guild",
    "exit": "a written sign-off from a named reviewer on the exported API", "check": "manual", "signoff": { "by": "m.okafor", "on": "2026-09-22" } },

The validator in review.mjs refuses a list before any check runs if an item lacks one of the three fields, repeats an id, or names a check that does not exist:

export function validate(items) {
  const problems = []
  const seen = new Set()
  for (const it of items) {
    const id = it.id ?? '(no id)'
    const missing = REQUIRED.filter((k) => !it[k])
    if (missing.length) problems.push(`${id}: no ${missing.join(', no ')}`)
    if (seen.has(id)) problems.push(`${id}: duplicate id`)
    seen.add(id)
    if (it.check && it.check !== 'manual' && !Object.hasOwn(checks, it.check)) problems.push(`${id}: unknown check ${it.check}`)
  }
  return problems
}
node validate.mjs
checklist.json: 14 items
  owners: 6 distinct, every item has one
  exit criteria: 14
  automated checks: 12
  manual sign-offs: 2, 1 recorded
valid: the review can start

Six owners across fourteen items is the shape to aim for: a team per area rather than a person, so that the review survives a holiday.

Let the script fail the items it can read

Every item that reads a file is a function in checks.mjs, and each returns the same shape: a pass, or a failure with a detail the owner can act on. The reproducible-build item is the clearest case. The fixture’s build concatenates the sources in a fixed order and stamps nothing into the output, so two runs give one hash. That is the definition: the same source, environment, and instructions give bit-for-bit identical artifacts.

node sdk/build.mjs && node sdk/build.mjs
bundle sha256 86db392876dbb91fce37ba76d0c1507f23b22f61ddb0e1b208818e1fd0bea87c
bundle sha256 86db392876dbb91fce37ba76d0c1507f23b22f61ddb0e1b208818e1fd0bea87c

The changelog check wants an Unreleased section and a heading for the manifest’s version, the shape Keep a Changelog sets out. The semver check parses the version under semver 2.0.0. The gate check reads the release workflow and insists that the publish job names an environment, because that is where GitHub attaches required reviewers to a job.

  changelog(dir) {
    const text = read(dir, 'CHANGELOG.md')
    const { version } = manifest(dir)
    if (!/^## \[Unreleased\]/m.test(text)) return fail('no Unreleased section')
    return text.includes(`## [${version}]`) ? pass(`entry for ${version}`) : fail(`no entry for ${version}`)
  },

  semver(dir) {
    const { version } = manifest(dir)
    return SEMVER.test(version) ? pass(version) : fail(`${version} is not semver`)
  },

  // A text check over the workflow: the publish job names an environment, which is where GitHub
  // attaches required reviewers. Whether a reviewer is configured is a setting, not a file. The
  // job runs from its key to the next key at its own depth, so another job's environment is not
  // this one's, and the key counts only at the job's own level, as a name or as a mapping.
  gated(dir) {
    const lines = read(dir, '.github/workflows/release.yml').split('\n')
    const start = lines.findIndex((l) => /^  publish:\s*$/.test(l))
    const end = lines.findIndex((l, i) => i > start && /^ {0,2}[^\s#]/.test(l))
    const job = start < 0 ? '' : lines.slice(start + 1, end < 0 ? lines.length : end).join('\n')
    const env = /^ {4}environment:[ \t]*([^\s#]+)/m.exec(job) ?? /^ {4}environment:[ \t]*\n(?: {6}.*\n)*? {6}name:[ \t]*([^\s#]+)/m.exec(job)
    return env ? pass(`publish job runs in environment ${env[1]}`) : fail('publish job names no environment, so nobody approves it')
  },

The Scorecard item reads four scores from the tool’s JSON output and applies a floor of 8 to each.2 The gate check reads this publish job:

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    permissions:
      id-token: write
      contents: read

With required reviewers configured on that environment, the job waits until one of them approves, and only then receives the publish token. The npm publish --provenance step in the same job signs the package through Sigstore and links it to the workflow that built it.

Run the review and read the blocked items as a work list

review.mjs validates the list, runs every item against the repository, and prints one line per item with the owner beside the result. Failing items are printed again at the end with their exit criteria: the work list for the week.

node review.mjs
production-readiness review: @acme/billing 1.0.0-rc.2

result item                   owner        detail
pass   build.lockfile         release-eng  lockfileVersion 3
pass   build.reproducible     release-eng  sha256 86db392876db twice
pass   conventions.idiomatic  node-guild   signed off by m.okafor
pass   conventions.manifest   node-guild   exports, files, engines
pass   tests.layers           sdk-team     unit, integration, contract
pass   tests.contract         sdk-team     3 operations, 3 methods
pass   docs.readme            docs         Install, First call, Errors, Support
FAIL   docs.changelog         docs         no entry for 1.0.0-rc.2
pass   release.semver         release-eng  1.0.0-rc.2
pass   release.gated          release-eng  publish job runs in environment production
FAIL   supply.scorecard       security     Signed-Releases 0/10, 0 out of the last 2 releases have a total of 0 signed artifacts
pass   runbook.cve            security     Triage, Fix, Yank, Notify
pass   support.path           devrel       README Support section, bugs.url https://github.com/acme/billing-node/issues
FAIL   support.rota           devrel       no sign-off recorded

11 of 14 pass. The review is blocked by 3 items, each with an owner:
  docs.changelog (docs): CHANGELOG.md has an Unreleased section and a heading for the manifest version
  supply.scorecard (security): Branch-Protection, Signed-Releases, Dependency-Update-Tool, and Maintained each score 8 or more
  support.rota (devrel): a rota with names for the first eight weeks after launch

Each of the three blocking items names who unblocks it. The changelog has no entry for the version in the manifest, which is the docs owner’s task before the tag. Scorecard scores Signed-Releases at 0 because the two candidate releases on GitHub carry no signature asset. That is the security owner’s call: attach the provenance to the release, or record why the item is waived. The support rota has no recorded sign-off.

The exit code is 1 while anything fails, so the same script can hold the publish. Run it as a job that the publish job lists under needs, because a required status check guards merges into a branch, not the tag push that starts this workflow. Wired that way, the publish job cannot start until the review passes, and the review cannot pass until a named person has done something.

Check it worked

Sixteen tests pin the rules with node:test. The ones that matter most edit a scratch copy of the fixture. A timestamp added to the build fails the reproducible item, a renamed operationId fails the contract item, and a Scorecard score raised to 8 clears its item. Others refuse a sign-off that names nobody, a check named constructor, and a gate met by another job’s environment.

node --test review.test.mjs
1..16
# tests 16
# suites 0
# pass 16
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 305.438606

If # pass 16 becomes fifteen after an edit to checklist.json, the blocked set changed.

When it goes wrong

The launch goes wrong under a checklist that was fully ticked, because the list had no owners and no exit criteria. A tick meant that somebody felt fine about the item on the day. The same fourteen items as a wall of ticks:

node pitfall.mjs
ticks.json: 14 of 14 items ticked
  items with an owner: 0
  items with an exit criterion: 0
  items with a check: 0
  a wall of ticks can block nothing, because nothing in it can fail

validate(ticks.json): 14 problems, the review cannot start
  build.lockfile: no owner, no exit, no check
  build.reproducible: no owner, no exit, no check
  conventions.idiomatic: no owner, no exit, no check
  ... and 11 more

The validator refuses the file before any check runs. A list that cannot fail is not reviewed, it is read aloud.

The reproducible item passes, but the bundle built in CI differs from the one built on a laptop. The build stamped a path or a host name into it, which is identical across two runs on one machine, so the in-process check cannot see it. Build on two runners in CI and compare the hashes.

Scorecard scores Signed-Releases at 0 for a package that was published with provenance. The check reads the release assets on GitHub for signature files such as .sigstore or .intoto.jsonl, not the registry. npm provenance lives on the registry side, so attach the provenance file to the GitHub release as well, or waive the item in writing.

When not to do this

Do not adopt the six areas of the PRR as your headings. Capacity planning and emergency response are questions about a running service, and an SDK team that answers them for a library answers “not applicable” six times and learns nothing. Borrow the discipline of a named owner and an exit criterion, and write the areas for a package.

Do not let Scorecard stand in for the list. It scores security hygiene from the repository. A repository can score 10 on every check while shipping a client that disagrees with the spec, a README with no first call, and a changelog that stops one version short.

Do not run the review after the publish. A list consulted once the package is on the registry can only produce regret, because npm’s unpublish policy closes most of the exits within 72 hours.3 Yanking on PyPI and retracting a Go module version exist for the same reason, and each is a runbook entry rather than a plan.4

Last verified

Verified 2026-10-21 against Node 22.22.0. Every output block is what the command preceding it printed. The SDK is a fixture repository written for this page, its Scorecard result is a constructed file in the shape of the tool’s JSON output, and Scorecard was not run. The PRR, the Scorecard checks, GitHub environments, and the registry policies are cited from their documentation.

Footnotes

  1. The SRE book’s pages on the engagement model list what a PRR covers: system architecture and interservice dependencies, instrumentation and monitoring, emergency response, capacity planning, change management, and performance. They also say that a PRR is considered a prerequisite for an SRE team to accept responsibility for managing the production aspects of a service. An SDK has no pager to hand over, so the only thing its review can hold is the publish button. ↩︎ Back to text

  2. Scorecard’s Maintained check gives the top score for at least one commit per week over the previous 90 days. Its documentation then concedes that a lack of active maintenance is not necessarily always a problem. The example offered is a library that determines if an integer is even, which would not need maintenance unless the language definition changed. There is no check for whether a project is that library. ↩︎ Back to text

  3. npm’s unpublish policy allows a package to be removed within 72 hours of publishing, as long as nothing on the registry depends on it. After that it allows one only when no package depends on it, it had fewer than 300 downloads over the last week, and it has a single owner. Deprecation is what remains: a message at install time, with the bytes left in place. ↩︎ Back to text

  4. Yanking on PyPI comes from PEP 592, which marks a file so that an installer should not generally select it, except under specific scenarios. The PEP adds that the yanked attribute is not immutable: it may be rescinded and set again, and API users must be able to cope with that. Go’s retract directive records the withdrawal in a later version of the module itself, so the retraction is a release. ↩︎ 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.