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
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Google SRE Production Readiness Review | 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 | 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 | 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:
{ "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
Related how-tos
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
-
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
-
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
-
npm’s
unpublishpolicy 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 -
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