Releasing
Releases are cut with Nx Release using version plans, because commit subjects here aren't conventional commits. A plan is a small Markdown file that says what gets which bump and why; nx release applies the plans, writes the changelogs, commits and tags; pushing the tag makes .github/workflows/release.yml build and publish.
The VS Code extension and the desktop app are released together: one version, one v<version> tag, one GitHub release holding the .vsix and the macOS dmgs, with that version's changelog as its notes.
Release groups
nx.json defines two groups, by tag:
| Group | Projects | Tag | Publishes to |
|---|---|---|---|
apps |
apps/vscode (publish:vscode) and apps/desktop (publish:desktop), fixed: one version for both |
v<version> |
A GitHub release with the .vsix and a dmg each for Apple silicon and Intel; the Visual Studio Marketplace and Open VSX |
npm |
packages/indexer, apps/server and any other publish:npm project, versioned independently |
<project>-v<version> |
npm, with provenance |
A dependent is never bumped for a dependency's release (updateDependents: never), and a minor bump below 1.0 is applied as a minor, not a patch (adjustSemverBumpsForZeroMajorVersion: false). Each project gets its own CHANGELOG.md, without author lines; there is no workspace changelog.
Cutting a release
-
With the change worth releasing, write a plan and commit it in the same commit:
yarn nx release plan patch --groups=apps -m "Fix the file card's position after a live update" yarn nx release plan minor --projects=indexer -m "…"This writes
.nx/version-plans/version-plan-<time>.md. The message becomes the changelog entry and the GitHub release notes, so write it for users. Name theappsgroup rather than one app: a plan naming onlyvscodestill bumps both, but leaves the desktop app's changelog a bare "version bump only" line. Add--only-touched=falsewhen the change touched none of the apps' files. -
Check, then release:
node .claude/skills/release/scripts/check-release.mjs # the group, the plans, the files the packages take from git, the workflow yarn nx release --dry-run --skip-publish # what would happen yarn nx release --skip-publish node .claude/skills/release/scripts/check-release.mjs # again: the tag, its files and its notesnx releasebumps both apps to the same version, updatesyarn.lock, writes each project'sCHANGELOG.md, deletes the applied plans, commits (the subjectRelease, with a line per release group in the body) and tagsv<version>. The commit must carry no attribution trailer. A bump without a plan isyarn nx release patch --groups=apps.release.gitis set at the top level, so theversionandchangelogsubcommands refuse to run on their own. -
To publish, push the commit and the tag:
git push origin main git push origin v<version>
What the release workflow does
release.yml runs on tags v* and *-v*.
-
v<version>, in four jobs; nothing is released unless every one succeeds:- check: the tag must match both
apps/vscode/package.jsonandapps/desktop/package.json;tools/scripts/release-notes.mjsmust find the version in the changelogs (a tag not cut bynx releasestops here) and writes the notes to the job summary;yarn typecheck. - vscode (Ubuntu):
yarn package, uploading the.vsixas an artifact. - desktop (macOS, beside vscode):
yarn desktop:package --mac dmg --arm64 --x64, uploadingorbit-code-<version>-mac-arm64.dmgandorbit-code-<version>-mac-x64.dmg. - release:
tools/scripts/github-release.mjscreates the GitHub releasev<version>with the three files. Its notes are that version's entries from both changelogs, each once, then the downloads, then GitHub's compare link. After that,apps/vscode/scripts/publish.mjspublishes the.vsixto the Marketplace withVSCE_PATand to Open VSX withOVSX_PAT, skipping a registry without its secret. Re-running a failed release job uploads to the release a previous run created.
The dmgs are unsigned: macOS gets an ad hoc signature so it runs on Apple silicon, and asks before the first launch. Windows and Linux installers aren't released;
yarn desktop:packagemakes them on those platforms, and CI builds the AppImage as a check. - check: the tag must match both
-
<project>-v<version>:yarn typecheck, that project's production build, thennx release publish --projects=<project>to npm with provenance (NPM_TOKEN).nx release publishdoesn't run a target's dependencies, hence the explicit build;--projectsand--groupscan't be combined.
The server's first release
@orbit-code/server (apps/server) is released on its own, starting at 0.1.3. Its manifest holds 0.1.2, a version never published, and Nx reads the current version from the manifest, so the first plan is a patch:
yarn nx release plan patch --projects=server -m "…"
yarn nx run server:build -c production && yarn nx release publish --projects=server --dry-run # the tarball
yarn nx release --dry-run --skip-publish --projects=server # 0.1.2 → 0.1.3, apps/server/CHANGELOG.md, tag server-v0.1.3
yarn nx release --skip-publish --projects=server
git push origin main server-v0.1.3
Don't commit a server plan before the server does what its README promises: nx release without --projects applies every pending plan, so the next apps release would version and tag the server too.
The published package is apps/server/dist, written by its build: server.mjs (the orbit-server bin), indexer.mjs and webview.js, with the manifest, README and LICENSE, and no dependencies.
Packaging locally
yarn package # a production build, then vsce → dist/apps/vscode/orbit-code-<version>.vsix
yarn install-local # package, install into VS Code with --force, then the dev build again; reload open windows
yarn desktop:package --mac dmg --arm64 --x64 # what the release builds: both dmgs in dist/apps/desktop
yarn desktop:package # the platform's default targets (dmg and zip, NSIS, AppImage)
node tools/scripts/release-notes.mjs v<version> # the release notes
node tools/scripts/github-release.mjs v<version> <files>… --dry-run # what the release job would upload
node apps/vscode/scripts/publish.mjs --dry-run
- The
.vsixholds only whatapps/vscode/.vscodeignorewhitelists: the manifest, README, LICENSE, CHANGELOG,media/, and the three bundles. Everything the extension runs is bundled, so vsce runs with--no-dependencies.package.mjsrefuses adist/that still holds source maps, the sign of a dev build. vsce and ovsx aren't dependencies:yarn dlxfetches the versions pinned inapps/vscode/scripts/vsce.mjs. - The desktop bundle holds
app.asar(the manifest,main.js,preload.js,webview.js, the icons) anddist/indexer.mjsunpacked beside it, since a worker can't start from inside an asar. electron-builder is pinned inapps/desktop/scripts/builder.mjs. Signing and notarization are follow-ups described inapps/desktop/README.md. - A file the packages take from the repository (an icon, the README) must be committed before tagging: CI builds the tag, not your working tree.
check-release.mjschecks this. - A publishable npm package publishes a
dist/whose manifest its build writes (name, version, description, license, repository,binorexports); the source manifest staysprivate: truewith source exports.packages/indexer/build.mjsis the model. Provenance needs the repository to be public andrepositoryin the published manifest.
Before the first publish from a fork
- Set the extension's
publisherinapps/vscode/package.jsonto your Marketplace publisher, and create an Open VSX namespace of the same name. - Add the secrets the jobs read:
VSCE_PAT,OVSX_PAT,NPM_TOKEN. The GitHub releases use the workflow's own token. WithoutVSCE_PATandOVSX_PAT, upload the.vsixfrom the GitHub release by hand. - The
@orbit-codescope on npm must belong to the account or organization behindNPM_TOKEN. Neither@orbit-code/indexernor@orbit-code/serverhas been published yet. - Dry-run everything:
yarn nx release --dry-run --skip-publish,yarn package && node apps/vscode/scripts/publish.mjs --dry-run, andyarn nx run indexer:build -c production && yarn nx release publish --projects=indexer --dry-run(the same forserver).