Releasing
Every artifact ships at one version: the npm package and the scaffolder, the source bundle on
GitHub, the two ESP-IDF components, the PlatformIO library, and the version pins in the READMEs and
this site. The single source of truth is the repo-root VERSION file, and tools/sync-version.mjs
propagates it everywhere it needs to appear.
Cutting a release
Day to day, add notes under ## [Unreleased] in CHANGELOG.md as changes land. A release then is:
node tools/release.mjs 0.15.0 # on master, nothing staged
git push --follow-tags
release.mjs does five things, in order: sets VERSION; runs sync-version.mjs --set, which
writes the version into every manifest and pin (twenty-four files at the time of writing); stamps the
changelog, promoting ## [Unreleased] to ## [0.15.0] - <date> with a compare link; commits
exactly those files as release: v0.15.0, never sweeping in unrelated changes; and tags v0.15.0.
--dry-run prints the plan and changes nothing. It refuses to run off master unless told
otherwise, because a tag cut on a feature branch would publish off-branch code.
What sync-version keeps in step:
| File | What carries the version |
|---|---|
packages/embedded-react/package.json, package-lock.json | The embedded-react package |
create-embedded-react/package.json | The scaffolder |
demos/*/package.json | Each demo's embedded-react dependency (the scaffolder's templates carry a placeholder it fills at scaffold time) |
library.json | PlatformIO |
engine/idf_component.yml | The engine's ESP-IDF component |
runtime/idf_component.yml | The runtime's ESP-IDF component, and the engine version it depends on |
engine/include/er_version.h | The engine's own version |
README.md, website/docs/getting-started/installation.mdx | The CMake, ESP-IDF and PlatformIO install pins |
website/package.json | The engine the playground runs |
examples/{esp32,rp2040}/*/CMakeLists.txt | The FetchContent tag a copied-out example fetches |
node tools/sync-version.mjs --check fails if any of them drifts, and CI runs it on every push,
so a hand-edited version cannot land. A listed file or package folder that's missing, or a version or
install pin that's no longer in the expected form, fails every mode before anything is written. When
you move a file or reword an install line, update sync-version.mjs to match.
The publish workflow
Pushing the tag runs .github/workflows/release.yml, which gates twice before publishing anything:
the tag must equal the committed VERSION, and the tagged commit must be on master. Then, channel
by channel:
- npm.
embedded-reactandcreate-embedded-react, through OIDC trusted publishing with provenance, no token. Each publish is idempotent: if that version is already on the registry the step skips, so a re-run after a transient failure never dies on "cannot publish over existing version". The simulator wasm is built with Emscripten in the same job and staged into the package before publishing. - ESP-IDF Component Registry, when
IDF_COMPONENT_API_TOKENis set:embedded-reactfromengine/, thenembedded-react-runtimefromruntime/, which depends on that engine version. - PlatformIO, from the root
library.json, whenPLATFORMIO_AUTH_TOKENis set. One library holds the engine and the runtime. - GitHub Release, with generated notes and a source tarball (
engine,runtime,backends,LICENSE,NOTICE,README.md) for the CMakeFetchContentchannel.
Each channel is gated on its own variable or secret, so they can be enabled one at a time. First-time setup for npm is the trusted publisher on npmjs.com, pointed at this repository and workflow, with the owner name in the exact case GitHub reports; a case mismatch makes the token exchange fail silently and npm fall back to asking for a token.
The docs site
The site deploys from master on any push that touches website/, the changelog, the roadmap or
the starter template, so documentation goes live when it merges, not when a release is cut. The one
thing that ties it to releases is the playground, which runs the published embedded-react at the
version website/package.json pins. release.mjs bumps that pin in the release commit; the docs
workflow then waits for that version to appear on npm (the publish runs in parallel from the same
push) before installing and building. The lockfile cannot carry the hash of an unpublished tarball,
so that push uses npm install and the lockfile is left one release behind. Pull requests use
npm ci, so the first one that touches the site after a release fails until the lockfile is
updated: run npm install in website/ and commit package-lock.json.
Version compatibility
Three things must agree, and each is checked rather than trusted:
- The container and the runtime. An
app.erpkgis stamped with the QuickJS version it was compiled for; the loader rejects a mismatch with an on-screen panel rather than running garbage. The tag is pinned in the examples'CMakeLists.txtand in the bridge; bump them together. - A Flow B package and the runtime. A package carries the format version and the engine ABI it was built for; the runtime refuses one it can't run, and says why.
- Bytecode and the QuickJS build. Bytecode is specific to the QuickJS version, which is why the package ships its own compiler through the prebuilt wasm and the firmware fetches a pinned tag.
Changelog conventions
One bullet per user-facing change, under ### Added, ### Changed or ### Fixed. The
[Unreleased] section is what the release stamps, so a change without a bullet there is invisible
in the release notes.