Skip to main content

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:

FileWhat carries the version
packages/embedded-react/package.json, package-lock.jsonThe embedded-react package
create-embedded-react/package.jsonThe scaffolder
demos/*/package.jsonEach demo's embedded-react dependency (the scaffolder's templates carry a placeholder it fills at scaffold time)
library.jsonPlatformIO
engine/idf_component.ymlThe engine's ESP-IDF component
runtime/idf_component.ymlThe runtime's ESP-IDF component, and the engine version it depends on
engine/include/er_version.hThe engine's own version
README.md, website/docs/getting-started/installation.mdxThe CMake, ESP-IDF and PlatformIO install pins
website/package.jsonThe engine the playground runs
examples/{esp32,rp2040}/*/CMakeLists.txtThe 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:

  1. npm. embedded-react and create-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.
  2. ESP-IDF Component Registry, when IDF_COMPONENT_API_TOKEN is set: embedded-react from engine/, then embedded-react-runtime from runtime/, which depends on that engine version.
  3. PlatformIO, from the root library.json, when PLATFORMIO_AUTH_TOKEN is set. One library holds the engine and the runtime.
  4. GitHub Release, with generated notes and a source tarball (engine, runtime, backends, LICENSE, NOTICE, README.md) for the CMake FetchContent channel.

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.erpkg is 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.txt and 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.