Lekhini follows Semantic Versioning (SemVer). This document is the canonical reference for what counts as a major / minor / patch bump and how to cut a release.
The version shown in Settings → About comes from package.json, so
the only source of truth for a version number is that file.
Given a version MAJOR.MINOR.PATCH:
Reserved for changes that meaningfully break what users or downstream consumers depend on. For a desktop app like Lekhini, that means:
- The persisted state file (
config.jsonunder the Lekhini user-data directory) becomes incompatible with the previous schema in a way that hydration can't auto-migrate. - Hotkey bindings change in a way that removes existing combinations.
- A tool is removed or its on-screen semantics change drastically (e.g., "pencil" no longer produces graphite-textured strokes).
- The exported screenshot format / file location changes.
Backwards-compatible feature additions or non-trivial UX improvements:
- A new drawing tool / shape.
- A new profile.
- A new setting or persisted preference (with sensible defaults).
- A meaningful change to the rendering engine that improves quality without breaking the look of existing strokes.
- New keyboard shortcuts that don't displace existing ones.
Bug fixes and small polish that don't change behavior intentionally:
- Fixing a stroke rendering glitch.
- Fixing a layout / sizing bug.
- Performance improvements.
- Dependency bumps that don't change behavior.
- Documentation-only changes.
Releases are tag-driven. One command bumps the version, rolls the changelog, commits, tags, and pushes — then CI builds every OS and publishes the GitHub Release. You do not build or upload anything by hand.
- Make sure the branch is clean and green (
npm run typecheck, and CI on the latest commit is passing). Land all release-worthy changes first, with notes under## [Unreleased]inCHANGELOG.md. - Run the release script with the bump type:
This (see
npm run release # patch (X.Y.Z+1) — the default npm run release:minor # X.Y+1.0 npm run release:major # X+1.0.0 # or an exact version: bash scripts/release.sh 1.4.0
scripts/release.sh):- refuses to run on a dirty tree,
- validates with
npm run prebuild(typecheck + build), - bumps
package.json+package-lock.json(no tag yet), - rolls
CHANGELOG.md:[Unreleased]→ a dated[X.Y.Z]section and updates the link refs (scripts/update-changelog.mjs), - commits
chore(release): vX.Y.Z, tagsvX.Y.Z, and pushes both.
- The pushed tag triggers
.github/workflows/release.yml, which:- builds installers on macOS, Windows, and Linux in parallel
(
npm run release:ci→electron-builder --publish always), - uploads them plus the
latest*.ymlupdate manifests to a draft GitHub Release for the tag, - flips the release public once all three OSes succeed.
- builds installers on macOS, Windows, and Linux in parallel
(
- Watch it at https://github.com/opensourcebharat/lekhini/actions. When green, the release is live and installed apps will auto-update.
The workflow signs + notarizes macOS builds only when these repo
secrets exist; otherwise the macOS build is unsigned (and macOS
auto-update stays disabled until they're added — Windows/Linux are
unaffected): CSC_LINK, CSC_KEY_PASSWORD, APPLE_ID,
APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID.
To produce installers without releasing, use npm run build (current
OS) or npm run build:mac|win|linux. These write to release/ and do
not publish.
Installed apps check GitHub Releases via electron-updater
(src/main/updater.ts), download in the background, and apply on quit.
Users control this in Settings → Updates (toggle, manual check,
restart-to-update). Because the feed is GitHub Releases, every public
release is automatically an update for existing installs — so prefer
small, frequent patch releases in the early stage.
- Format:
vMAJOR.MINOR.PATCH(with leadingv). - Pre-releases:
vX.Y.Z-rc.1,vX.Y.Z-beta.2, etc. - Never re-use a tag. If something breaks immediately after release,
bump the patch version and ship
vX.Y.(Z+1)rather than re-tagging.
For a small project, main is the release branch. Tag releases directly
on main. If a hotfix becomes necessary after a release, branch from
the tag (git checkout -b hotfix-X.Y.Z+1 vX.Y.Z), apply the fix, tag
and release from there, then merge back into main.
After a release, the version shown in Settings → About in a fresh
build should match the tag. The CI pipeline should also reject a build
where package.json and the current tag disagree (future work — not
yet enforced).