How-to › Ship an SDK

How to fail a build when a generator customization becomes a fork#

Record what the generator shipped, compare the project against it on every build, and separate a file you forked from a file you added.

Audience
API producer
Level
advanced
Topic
Customize a generator without forking
Languages
TypeScript and JavaScript
Verified

Somebody fixes a bug by editing a component the generator ships. It works, it ships, and the next target add reverts it without a word. Six months later nobody can say which of the forty files under the customization directory came from the generator, which were edited, and which the team wrote themselves.

What you get

You will end up with a manifest of what the generator gave you, and an audit that classifies every file in the project against it. The gate then fails on the two classes that are drift. This is for you if your project carries generator-owned files that people edit.

Short answer

Record a checksum for every file the generator ships, at install time, and commit that manifest. On each build, compare the project against the manifest. A shipped file you changed is a fork, a shipped file you deleted is missing, and a file you added is your own. Fail on the first two, report the third.

You will need

Node 22 or later, and a project that carries files a generator shipped. The idea is the one package managers use for a lockfile: record what you were given, so a later comparison has something to be against.1 Subresource integrity makes the same move for a script you did not write.2 sdkgen ships the same check as voxgig-sdkgen doctor.

Voxgig maintains sdkgen. This page compares its own drift check with a manifest you keep, a manual diff, and letting version control track the vendor tree.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A committed checksum manifestAny generator, because it needs nothing from the tool beyond its outputA manifest to refresh on every update, and a file people must not hand-editThe generator already ships a drift check you trust
A manual diff against the shipped templatesA one-off investigation, when somebody already suspects a forkAttention, and a result that is stale the next afternoonThe question needs answering on every build
Git tracking of the vendor treeThe generator output is a checkout you can update and mergeMerge conflicts in generated files, which is the situation this avoidsThe output is written in place by a tool rather than pulled
voxgig-sdkgen doctorYour generator is sdkgen and you want the check it already knows how to doA check tied to one generator, which says nothing about the rest of the treeYou generate with something else, or with several tools

The manifest and a tool’s own check answer the same question and differ in coverage. A native check knows what the generator shipped and stops at the edge of what it manages. A manifest covers whatever you point it at, including the files a second generator or a vendored template wrote.

Run the native one if you have it. The manifest earns its place when a project carries output from more than one source, which is most projects past the first year.

Record what you were given

The manifest is written once per install and committed.

const manifest = record('scaffold')
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2) + '\n')
console.log(`recorded ${Object.keys(manifest).length} files`)

Write it at install and at every update, and never by hand. A manifest somebody edits to silence a finding is a manifest that no longer records anything, and the edit looks exactly like the fix.

Store a short hash rather than the file. The comparison needs to know that something changed, not what it was, and the generator still has the original.3

Separate a fork from an addition

Only two of the four verdicts stop a build.

export const blocking = (findings) =>
  findings.filter((f) => f.verdict === 'forked' || f.verdict === 'missing')

An additive file is the normal way to customize. A team that gets a build failure for writing their own component learns to switch the gate off, and then the gate catches nothing at all.

A missing file is drift as much as a fork. Deleting something the generator ships means the next regeneration puts it back, and whatever depended on its absence breaks then rather than now.

Check it worked

Audit a project against the manifest of what the generator shipped.

node demo.mjs
manifest: 6 files recorded at install
  orphaned    cmp/Legacy_ts.ts       installed by an older generator, no longer shipped
  forked      cmp/Main_ts.ts         edited since install, 00ed1f3057ca became 4af874f9e882
  additive    cmp/MeterBadge_ts.ts   project-owned, not drift
  missing     cmp/Readme_ts.ts       shipped by the generator and deleted here
  forked      tm/Makefile            edited since install, 0e2383f9bfde became bb684df0b89e
  retired     tm/Old.mk              installed once, no longer shipped, removed here
3 blocking, 3 informational

Three of the six findings will be reverted by the next regeneration, and three will not. That is the whole distinction, and it is what makes the gate usable: the team’s own component is reported and does not fail anything.

The last two are why the manifest belongs in the ownership set rather than only in the comparison. Both are files an upgrade dropped from the scaffold. Audit the scaffold alone and Legacy_ts.ts reads as project-owned, because nothing still ships it, while Old.mk disappears from the report entirely. The output would then give no sign that a generator upgrade had rewritten the history of every file it stopped shipping.

node --test audit.test.mjs
1..7
# tests 7
# suites 0
# pass 7
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 135.022532

What sdkgen’s own check reports, recorded rather than re-run here. A project was generated with sdkgen 4.17.1, then one component and one template master were edited and one new component added. voxgig-sdkgen doctor reported a fork on src/cmp/ts/Main_ts.ts, an edited template master on tm/ts/Makefile, and src/cmp/ts/MeterBadge_ts.ts as additive and project-owned. It summarized as 1 forked, 1 edited, 0 stale, 0 missing and exited 1, drawing the same line between a fork and an addition. That run is not reproduced by this page’s gates, because it needs the generated project and its toolchain. The two command blocks are.

When it goes wrong

The audit reports every file as forked. The manifest was recorded from a different version than the one installed. Re-record on update, in the same step that installs.

A fork disappears from the report. Somebody re-recorded the manifest to make the build pass. Make the manifest a reviewed file, and treat a change to it in a pull request as the event it is. A manifest that moves in the same commit as a component is the thing to look at twice.

Nothing is ever reported and people are definitely editing. The audit is pointed at the generated output rather than at the customization tree. Point it where the generator writes files it also owns.

The gate fires on a line ending. A checkout normalized them. Compare normalized text, or set the repository’s attributes so both sides agree.

When not to do this

Do not gate on forks before you have somewhere else to put the change. A fork is usually a fix somebody needed, and failing the build without offering the model or the template hook as the alternative moves the argument rather than settling it. Add the escape hatch first, then the gate.

Do not audit generated output that nobody is meant to edit. That tree is rewritten every run, so a checksum of it changes constantly and says nothing. Audit the customization surface, which is the tree the generator writes once and then leaves to you.

Do not rely on sdkgen’s doctor alone once a second generator is in the project. It reports on the .sdk tree it manages, which is the right scope for it, and the files another tool wrote fall outside it, still yours to keep accounted for with a manifest.

Last verified

Verified 2026-09-14 against Node 22.22.2 and @voxgig/sdkgen 4.17.1. Both output blocks are what the preceding command printed.

Footnotes

  1. The lockfile has been recorded three ways. npm’s documentation numbers them: version 1 for npm 5 and 6, version 2 for npm 7 and 8, and version 3 for npm 9 onward. An unnumbered fourth it calls an ancient shrinkwrap file, from before npm 5. The same page promises that npm will attempt to get whatever data it can out of a lockfile it was not designed to read. The record of exactly what you were given has changed shape three times, then, and is read by a tool that promises to do its best with whatever it finds. ↩︎ Back to text

  2. Subresource integrity became a W3C Recommendation in June 2016. It requires every browser conforming to it to support SHA-256, SHA-384 and SHA-512, and names MD5 and SHA-1 as functions it does not recommend. It adds that, at the time of writing, SHA-384 is a good baseline. It then asks user agents to re-evaluate their supported hash functions on a regular basis, which is a standard allowing in advance for its own decay. The check confirms that the bytes fetched are the bytes named. Whether the bytes named were any good is a question the attribute has never claimed to answer. ↩︎ Back to text

  3. The twelve hexadecimal characters in the output are 48 bits, and the length has company. The Linux kernel’s patch rules ask for at least the first 12 characters of the SHA-1 of a commit in a Fixes: tag. Git itself, asked how far to abbreviate, computes a length from the approximate number of packed objects. Its documentation says the result is hopefully enough for abbreviated names to stay unique for some time, and hopefully is the manual’s own word. The manifest has an easier time of it: each hash needs only to differ from the one recorded beside its name. ↩︎ 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.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.