Skip to content

Semantic Versioning ​

Implements: REL-01, REL-05, REL-06, GATE-03 · requirements stated in CHARTER.md Part 8

How to decide whether a change to @lgriffin/esi.ts is a major, minor or patch release, and how to say so in a commit so the published version is right. RELEASE.md covers what happens after the commits land: release-please, the changelog, publishing and signing.

The version number is a promise to every consumer. ^9.9.0 in someone's package.json means "install any 9.x.y release without my code breaking". A breaking change released as a minor breaks every one of those installs on their next npm install. A harmless change released as a major makes consumers read migration notes that do not exist. Both are bugs.


The three numbers ​

MAJOR.MINOR.PATCH, following Semantic Versioning 2.0.0.

BumpWhenCommit
MajorCode that compiled and ran against the previous release can fail against this onetype!: subject and a BREAKING CHANGE: footer
MinorNew capability; everything that worked before still works the same wayfeat:
PatchA fix or internal change with no new capability and nothing removedfix:, perf:, refactor:, chore:, docs:, test:

The package is past 1.0, and release-please runs with bump-minor-pre-major: false, so these rules apply exactly as written. There is no "it's only a minor break".


What the public contract is ​

Everything a consumer can reach without importing from dist/ by path:

  • Every entry point in package.json exports: ., ./schemas, ./errors, ./testing, ./client, ./sde, ./sde/memory, and ./package.json. ./testing (TestDataFactory and friends) is public; consumers use it in their own tests.
  • The types those entry points export. That includes interfaces consumers implement, such as ILogger, ICircuitBreaker, IDeduplicator and IRetryStrategy, as well as ones they only call.
  • Runtime behaviour documented in the guides. This covers which error class is thrown and its retryable flag (ERRORS.md), what a response schema accepts (RUNTIME-VALIDATION.md), pagination results (PAGINATION.md), default configuration values, and log events other code may key on (LOGGING.md).
  • The environment. engines.node, the oldest supported TypeScript (5.4, checked by the consumer contract), dependencies whose types appear in the public API (zod), and peerDependencies (js-yaml, adm-zip), including their ranges.

Not public: anything not reachable from an entry point, file layout under src/, test files, CI, generated files' internal shape where no exported type depends on it, and log message wording.


Deciding ​

Ask: could code that works against the last release stop compiling, throw, or return something different? If yes, the change is major. The tables below cover the cases that come up in this repository.

Exports and signatures ​

ChangeBumpWhy
Add an export, a client method, a sub-path export or an endpointminorNothing existing changes
Remove or rename an export, method, sub-path or endpointmajorImporters stop compiling
Add an optional parameter or an optional config fieldminorExisting calls still type-check and behave the same
Add a required parameter, or make an optional one requiredmajorExisting calls stop compiling
Narrow a parameter type (number | string → number)majorCallers passing the removed type stop compiling
Widen a parameter type (number → number | string)minorExisting calls still compile
Widen a return type (T → T | undefined)majorCallers must handle a case they never saw
Narrow a return type (T | undefined → T)minorExisting handling still compiles
Add a required member to an interface consumers implement (ILogger)majorTheir implementations stop compiling. The API report shows only an added line, so the gate cannot catch this one
Add an optional member to such an interfaceminorExisting implementations still satisfy it
Add a required member to a type the library only returnsminorConsumers read it; they never construct it

Runtime behaviour ​

ChangeBumpWhy
Throw a different error class, or flip retryable, for an existing casemajorinstanceof checks and retry logic in consumer code change behaviour
Tighten a Zod response schema (a field becomes required, an enum loses a member)majorResponses that validated before now throw EsiValidationError at runtime
Make a response field optional in a Zod schemamajorThe inferred type widens to T | undefined, so consumer code that reads the field can stop compiling. This holds even when ESI already omits the field
Add a member to an ESI enum union in a response schemaminorESI enums are open by design (CHARTER principle 3): consumers must not switch on them exhaustively. z.looseObject keeps unknown fields regardless
Change a default that alters results: retry count, cache on or off, validation on or off, datasourcemajorSame code, different outcome
Change a default that only alters timing or resource use, within documented boundspatchResults are the same
Fix a bug so the documented behaviour now happenspatchThe contract was already stated; the code now meets it
Fix a bug that consumers could reasonably depend on (an error that was swallowed now throws)major, or patch with a changelog warningDecide in the pull request and write the reason down; when unsure, ask the maintainer
Performance improvement with identical resultspatchperf:

Environment and dependencies ​

ChangeBumpWhy
Drop a Node line (raise engines.node)majorREL-05; installs on that line stop working
Raise the oldest supported TypeScript (OLDEST_TYPESCRIPT)majorConsumers on that version stop compiling
Major version of a dependency whose types are public (zod 4 → 5)majorConsumers' schemas and inferred types change
Raise the minimum of a peer dependency (js-yaml)majorExisting installs no longer satisfy it
Add a new required runtime dependency or peer dependencymajorExisting installs may break or warn
Remove an optional peer dependency that no public export needsminorExisting installs keep working; nothing public loads it
Patch or minor dependency update with no public type changepatchchore(deps):

Changes that come from ESI itself ​

CCP changes the ESI spec on its own schedule, and npm run generate:types follows. Classify the resulting library change by the tables above, not by what CCP called it:

  • ESI adds a field or an endpoint: minor.
  • ESI removes an endpoint: deprecate it first (below). Removing the client method is a major release, even though the endpoint no longer works, because removal is what stops consumers' code compiling.
  • ESI stops always sending a field the schema requires: make it optional, as fix(schemas)!:. It fixes validation failures consumers already see, but the inferred type widens, so it is still a major release. Say both in the BREAKING CHANGE: footer.

Deprecate before you remove ​

A removal should never be the first a consumer hears of it.

  1. In a minor release: mark the export @deprecated in JSDoc, naming the replacement. For an endpoint whose ESI operation is deprecated upstream, add DeprecationInfo (message, replacedBy, sunsetDate) to its definition, so createClient logs a warning on each call. A hand-written method logs through logWarn(this._client, …) (DESIGN-RULES.md §3.6). Record it in the changelog under Deprecated.
  2. Keep it working for at least one minor release.
  3. In the next major release: remove it, with a BREAKING CHANGE: footer that names the replacement.

When ESI has already removed the endpoint and every call fails, steps 1 and 3 can land in consecutive releases. The method still stays until a major release.

Adding @deprecated changes no signature and no behaviour, but it is a change to the contract consumers read, so it goes in as feat: (11.0.0's feat(client): deprecate EsiApiFactory's named methods … is the precedent) and reaches a minor release and the changelog. Lifting a deprecation is the same kind of change, also feat: in a minor release (12.2.0 lifted 12.1.0's). The API report rewrites each declaration's comment line to // @public @deprecated and back, which the api-semver gate reads as a changed line, so the commit carries an API-Compatible: trailer saying only tags changed.

The custom clients are never deprecated. EsiClient, getDefaultClient, EsiClientBuilder, CustomEsiClient, EsiApiFactory with createClient(type), BaseEsiClient, ApiClientType and every domain client are the product (charter posture 1). ARCH-14 fails a change that marks one @deprecated, removes it or replaces it with the generated surface (tests/tdd/scripts/custom-client-identity.test.ts); changing that starts with a charter amendment, not with this process. CLIENT-ARCHITECTURE.md records the decision.


Saying it in the commit ​

release-please reads commit messages on master and nothing else. The only way to get the version right is to write the commit right. commitlint checks the format; it cannot check honesty.

A breaking change ​

Put ! after the type and add a BREAKING CHANGE: footer that tells a consumer what to do:

text
feat(market)!: require the order type in getMarketOrders

Fetching both sides of the book by default doubled the payload for the
common case of reading one side.

BREAKING CHANGE: getMarketOrders(regionId) is now
getMarketOrders(regionId, orderType), where orderType is 'buy', 'sell' or
'all'. Pass 'all' to keep the previous behaviour.

The ! triggers the major bump. The footer is the migration note that release-please copies into the changelog. Either one alone bumps the version, but a ! without a footer ships a major release with no explanation.

A compatible change the API report flags ​

Some compatible changes still remove or rewrite a line in etc/esi.ts.api.md, such as an optional parameter added to an existing signature. Say why it is compatible with an API-Compatible: trailer:

text
feat(market): accept an optional type filter in getMarketOrders

API-Compatible: the new parameter is optional, so existing calls still type-check and return the same orders.

Which commit carries the marker ​

  • A pull request that contains the break carries the marker on the commit that introduces it.
  • Never mark a change breaking "to be safe". An unnecessary major release is also a wrong version.
  • Never hide a break inside a chore:, refactor: or test: commit. The type describes the change to consumers, not the kind of work.

Merging the pull request ​

GitHub offers three ways to merge a pull request, and they give release-please different commits to read:

ButtonWhat lands on masterWhat release-please reads
Merge commitEvery commit from the branch, unchanged, plus one extra "Merge pull request #N" commit that joins the branch to masterEach branch commit; the merge commit is ignored
Squash and mergeOne new commit containing all the branch's changes. With several commits, its title is the pull request title; with one commit, it is that commit's messageThat single commit
Rebase and mergeEvery commit from the branch, replayed on top of master, with no merge commitEach branch commit

The consequences:

  • Merge commit or rebase: each commit's type and markers reach the changelog as written. Use one of these when a pull request mixes types, for example a fix: alongside test: commits, so each gets its own changelog entry.
  • Squash: the pull request title becomes the only commit title. It must be a conventional commit, and it must carry ! when any commit in the pull request is breaking. Write any BREAKING CHANGE: footer into the squash dialog's message box too. Squash also collapses the changelog to one entry.

What checks it ​

CheckWhat it catchesWhere
commitlintA commit message that is not a conventional commitcommit-msg hook
API surface diff (GATE-03)An exported shape changed without etc/esi.ts.api.md being regenerated (npm run api-report)ci.yml API Surface Check
publicApiSurface.test.tsA root export renamed or removednpm test
api-semver gate (REL-06), added by #310The API report lost or changed a line, but no commit is type!:, has a BREAKING CHANGE: footer or an API-Compatible: trailer; or a declared break spans several commits and the pull request title is not type!:ci.yml, npm run api-report:semver
ReviewEverything else in Deciding: runtime behaviour, schema tightening, defaults, required members on implemented interfaces, sub-paths the report does not cover, dependency changesAGENTS.md Reviewer Checklist

The API report covers only the root entry point, and it cannot see runtime behaviour. The gate is a tripwire for the common case, not a proof. The author is responsible for the classification, and the reviewer checks it.


Checklist for every pull request ​

  1. Does the change touch anything in the public contract? If not, it is a patch-level type (fix:, refactor:, chore: and so on) and you are done.
  2. Classify it with the tables in Deciding.
  3. If an exported shape changed, run npm run api-report and commit etc/esi.ts.api.md.
  4. Breaking: type!: plus a BREAKING CHANGE: footer with the migration. If it removes something, was it deprecated in an earlier minor release? If not, deprecate first unless ESI has already removed it.
  5. Compatible but flagged by the report: an API-Compatible: trailer with the reason.
  6. Squash-merging several commits: pull request title in conventional form, with ! if anything is breaking.

Released under the GPL-3.0-or-later license.