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

Source: https://voxgig.com/howto/production-readiness-checklist-for-a-new-sdk

- Audience: platform-team
- Level: advanced
- Verified: 2026-10-21
- Published: 2026-10-19

## 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](https://sre.google/sre-book/evolving-sre-engagement-model/) in the
SRE book.[^1]

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [Google SRE Production Readiness Review](https://sre.google/sre-book/evolving-sre-engagement-model/) | A team that already runs services under SRE and wants the SDK reviewed in the same vocabulary | Six areas written for a running service, so capacity planning and emergency response need translating into registry terms | The reviewers have never run a PRR, and the SDK has no on-call to hand it to |
| [Home-grown checklist enforced in CI](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) | A list that fits this SDK exactly, with every item that can be automated run as a required status check | Your own maintenance: an item nobody automates rots into a tick, and the list is only as good as its owners | You need a result somebody outside the team will recognize without reading the list |
| [OpenSSF Scorecard](https://github.com/ossf/scorecard/blob/main/docs/checks.md) | Security hygiene of the repository scored the same way for every public project, with a badge to show | Only security hygiene, read from the repository: nothing about docs, tests, or whether the client matches the spec | The 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:

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

```js title="review.mjs"
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
}
```

```bash
node validate.mjs
```

```text output
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](https://reproducible-builds.org/docs/definition/): the same source, environment, and
instructions give bit-for-bit identical artifacts.

```bash
node sdk/build.mjs && node sdk/build.mjs
```

```text output
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](https://keepachangelog.com/en/1.1.0/) sets out. The semver check parses
the version under [semver 2.0.0](https://semver.org/spec/v2.0.0.html). The gate check reads the
release workflow and insists that the publish job names an environment, because that is where
GitHub attaches
[required reviewers](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments)
to a job.

```js title="checks.mjs"
  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:

```yaml title="sdk/.github/workflows/release.yml"
  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](https://docs.npmjs.com/generating-provenance-statements/)
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.

```bash
node review.mjs
```

```text output
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](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches)
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`](https://nodejs.org/api/test.html). 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.

```bash
node --test review.test.mjs
```

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

```bash
node pitfall.mjs
```

```text output
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](https://github.com/ossf/scorecard/blob/main/docs/checks.md)
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](https://docs.npmjs.com/policies/unpublish/)
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]

## Related how-tos

- [Write a CVE response runbook for an SDK](/howto/write-a-cve-response-runbook-for-an-sdk)

- [Ship a changelog inside your published package](/howto/ship-a-changelog-inside-the-package)

- [Choose a release automation tool for an SDK repository](/howto/choose-a-release-automation-tool-for-an-sdk)

- [Automate dependency updates with Dependabot or Renovate](/howto/automate-dependency-updates-with-dependabot-or-renovate)

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

[^1]: The SRE book's [pages on the engagement model](https://sre.google/sre-book/evolving-sre-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.

[^2]: Scorecard's [Maintained check](https://github.com/ossf/scorecard/blob/main/docs/checks.md)
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.

[^3]: npm's [`unpublish` policy](https://docs.npmjs.com/policies/unpublish/) 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](https://docs.npmjs.com/deprecating-and-undeprecating-packages-or-package-versions/)
is what remains: a message at install time, with the bytes left in place.

[^4]: Yanking on PyPI comes from [PEP 592](https://peps.python.org/pep-0592/), 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](https://go.dev/ref/mod#go-mod-file-retract) records the withdrawal in a later
version of the module itself, so the retraction is a release.