Release your fork
A release of the engine is a git tag plus a tarball an operator can install from with nothing but Node and Docker. npm run release cuts one from a clean checkout; it decides nothing about the version, it reads the version you set and refuses to continue when the tree, the changelog or the tag does not match it. This page reads scripts/release.mjs step by step, then shows how a fork stays on top of upstream releases.
What npm run release does
Section titled “What npm run release does”The script is scripts/release.mjs, driven by scripts/__tests__/release.spec.ts against a scratch repository. It runs from the repository root and stops at the first refusal, each of which prints what to do:
- The current directory must be the git top level. Anywhere else:
Run the release from the repository root. package.jsonmust carry a semantic version (1.2.0, or1.2.0-rc.1). The tag isvplus that version.git status --porcelainmust be empty. The tarball archivesHEAD, so an uncommitted change would be a release nobody can reproduce.- The first
##section ofCHANGELOG.mdmust be titled with that version.## [1.2.0]and## v1.2.0are accepted;## Unreleasedabove the version’s section is not, because a newer entry left under a working title would ship under the old notes. The refusal readsCHANGELOG.md's first section is "## Unreleased", not "## 1.2.0". - The tag must not exist yet. Bump the version or delete a tag cut by mistake.
Then it prints the plan: the commit, the bundle path, the tarball path and the tag. With --dry-run it stops here, having written nothing and built nothing. Without it:
npm run build:setupbundles the install stepper totools/setup/dist/index.js(tools/setup/build.mjs); everything the stepper imports is inlined, so a box runs it with Node alone.git archive --format=tar HEADis extracted into a staging directory, the bundle is copied in attools/setup/dist/index.js, and its modification time is set to now. The wrappersetup.shrebuilds the stepper when any file undertools/setup/src,tools/setup/build.mjsorlibs/shared/common/srcis newer than the bundle, andgit archivestamps every file with the commit time, so the fresh stamp is what stops a tarball user’s first./setup.shfrom trying to build.- The staged tree is packed as
dist/release/themerchantengine-v<version>.tar.gz.--out <dir>moves the directory;dist/is git-ignored. - An annotated tag
v<version>is created, unless--no-tag.
The closing lines are the two steps it leaves to you:
git push origin v1.2.0# attach dist/release/themerchantengine-v1.2.0.tar.gz to the v1.2.0 release on the repository hostThe flags: --dry-run runs the checks and prints the plan; --no-tag writes the tarball without tagging; --out <dir> sets the tarball directory.
What the tarball holds and what it leaves out
Section titled “What the tarball holds and what it leaves out”Only tracked files are archived. No .env file, no .setup-state.json, no node_modules, no build output other than the one stepper bundle can travel, whatever is lying in the working tree. scripts/__tests__/release.spec.ts asserts that on a scratch repository that has all three planted.
.gitattributes then marks what git archive drops with export-ignore. At this commit that is the project’s planning record, its agent configuration, the CI answers document, the security audits, the manual QA records, the studio brand book, the token table and the spec of the client-reference guard, and the test license’s private key:
- the
specsdirectory (the planning record), the.claudedirectory (the agent configuration),.session-runner/,ci/,docs/audit/ docs/security-audit-report.md,docs/security-research.mddocs/admin-manual-qa-findings.md,docs/storefront-manual-qa-findings*.mddocs/badii-studio-brand-guidelines.mdscripts/verify-no-client-refs.tokens.json,scripts/__tests__/verify-no-client-refs.spec.tstest/fixtures/license/private.pem
The same spec runs git archive on the real repository and refuses any of those paths in the listing, and requires setup.sh, setup.ps1, tools/setup/build.mjs, deploy/templates/compose.local.yml, test/fixtures/license/public.pem and prisma/schema.prisma to be present.
So a licensee’s tarball holds the three applications, the shared libraries, prisma/ with the schema, the seeds and the index SQL, deploy/ with the templates and the operator manual, docs/ less the files above (this documentation included), scripts/ less the two guard files, test/ with the public license key and the fixtures, the two wrappers, the built stepper, LICENSE, COMMERCIAL-LICENSE.md, CHANGELOG.md and README.md. On that tree npm run verify:no-client-refs reports that its token table is not there and that the gate runs in the engine repository only; every other gate on Testing and gates runs.
If your fork adds files that must not reach a customer, add them to .gitattributes with export-ignore and extend the forbidden pattern in scripts/__tests__/release.spec.ts, so the archive spec fails when the attribute is lost.
Version and changelog convention
Section titled “Version and changelog convention”- The version is the
versionfield ofpackage.json, semantic, and a person sets it. The script does not bump. CHANGELOG.mdopens with the project name as its#heading, then one## <version>section per release, newest first, with### Major Changes,### Minor Changesand### Patch Changessubsections as the release needs them. Changesets is wired (npm run changeset,npm run changeset:version, config in.changeset/config.json), andchangeset versionwrites a section in that shape and bumpspackage.jsontogether, which is why the refusal names it as one way to title the entry.- While work is in progress the top section may be titled
## Unreleased. Retitle it with the version, commit, and only then run the release. The script’s dry run is the cheapest way to check the three files agree:npm run release -- --dry-run. - A release that changes the schema destructively says so in its entry. An operator reads the entry before an update, because the installer’s schema push is additive and there is no automated schema rollback; Update the store explains the operator’s side.
Keep a fork on a release tag
Section titled “Keep a fork on a release tag”A fork carries its own seeds, its own env examples and its own templates on top of the engine. Rebase it on upstream tags, not on a moving branch, so what you ship is a known release plus your commits.
git remote add upstream https://github.com/<upstream-owner>/themerchantengine.gitgit fetch upstream --tagsgit checkout -b rebase/v1.2.0 my-fork-maingit rebase v1.2.0Conflicts land in the files a fork edits most. Resolve each one and continue with git rebase --continue:
prisma/seed.tsandprisma/seeds/(store-config/,fixture-catalogue/,demo-dashboard.ts): keep upstream’s new rows and your values. The seeds upsert and never overwrite a row an operator changed, so a merged seed is safe to re-run on an installed store.prisma/permission-catalogue.tsandprisma/notification-template-defaults.ts: a new upstream permission or template is a row your store needs; keep both sides..env.exampleand.env.production.example: keep every new upstream variable, with your defaults. The installer rewrites the rendered env file from its answers on every run, so a value that must survive belongs indeploy/templates/, not in a hand edit on the box.deploy/templates/nginx/and the fourcompose.*.ymltemplates: a hand-edited vhost is lost on the next setup run, so this is where a fork’s edge change lives. After a merge,npm run test:scriptsrunsscripts/__tests__/edge-headers.spec.tsandscripts/__tests__/storefront-proxy.spec.tsagainst the result, and the CI jobnginx-templatesparses every rendered vhost.CHANGELOG.md: keep upstream’s sections and add your fork’s entry as the first section when you cut your own release.
Then bring your development database and your build up to the new tree:
npm cinpx prisma generatenpm run db:push # prisma db push, then the search index, then the order-history backfillnpm run db:fts # after any bare prisma db push you ran by handThe installer and the test harnesses push the schema with prisma db push; they do not replay prisma/migrations/, and prisma migrate dev cannot replay them either, because the chain does not start from an empty database (see Add a model). After a rebase, run npm run db:push and then npm run db:fts, because the search column, its trigger and its indexes live in prisma/search_fts_setup.sql and every push that touches the product table leaves them to be re-applied. A version bump in package.json also changes the info.version of the two generated OpenAPI documents under docs/site/reference/_generated/, so run npm run docs:generate and commit the result with the bump, or the docs-generate-check job refuses the push.
Run the gates before the rebase branch replaces your main branch: the per-application commands on Testing and gates, npm run verify:no-client-refs if your fork keeps the token table, and npm run release -- --dry-run. On an installed store the update is a re-run of the installer on the new checkout, engine box first; Update the store has the order and the by-hand steps.
What a fork keeps from the license
Section titled “What a fork keeps from the license”Forking changes nothing about the terms. The engine is published under the PolyForm Noncommercial License 1.0.0 (LICENSE), a store that accepts money needs a commercial license (COMMERCIAL-LICENSE.md), and both files ship in every tarball your fork cuts. The API still verifies a license file at boot in production with the public key in its build, so a fork’s production install needs a license like any other. Get a license has the rules, the tiers and the expiry states.