Skip to content

Building & releasing ​

This page covers how Noted is built, packaged, signed, and shipped, plus the continuous-integration gates that run on every push.

Prerequisites ​

  • Node.js 20 or later
  • npm 10 or later
  • To package the desktop app: macOS for the DMG (with Xcode command-line tools for signing), Windows for the NSIS installer, or Linux for the AppImage and .deb. electron-builder does not cross-compile, so each platform's installer is built on that platform — see Cross-platform builds (CI).

Install dependencies once:

bash
npm install

Development ​

bash
npm run dev

This builds the Electron main and preload bundles with esbuild, then starts the Vite renderer with hot-module reloading.

npm scripts ​

ScriptWhat it does
npm run devBuild main + preload, then start Vite
npm run buildFull production build and package (electron-builder)
npm run build:dmgBuild and package a macOS DMG (arm64)
npm run build:winBuild and package the Windows NSIS installer (x64 + arm64)
npm run build:linuxBuild and package the Linux AppImage and .deb (x64 + arm64)
npm run build:mainBundle the Electron main process (dist-electron/main.cjs)
npm run build:preloadBundle the preload script (dist-electron/preload.js)
npm run build:mcpBundle the MCP server (dist-mcp/index.cjs)
npm run lintESLint with cache
npm run lint:ciESLint without cache (CI)
npm run testRun the Vitest suite
npm run test:ciVerbose Vitest run
npm run test:coverageVitest with V8 coverage

The build pipeline ​

npm run build runs, in order:

  1. tsc -b — type-check the whole project.
  2. vite build — bundle the React renderer into dist/.
  3. build:main, build:preload, build:mcp — three esbuild passes that emit CommonJS bundles for the Electron main process, the preload bridge, and the standalone MCP server.
  4. electron-builder — package the app and produce installers (a DMG on macOS, an NSIS .exe on Windows, an AppImage and .deb on Linux).

The renderer, main process, preload bridge, and MCP server are separate bundles; see Architecture for how they fit together.

Packaging ​

electron-builder is configured in package.json under the build key. Each platform produces its own installers:

  • macOS — Noted-<version>-arm64.dmg (Apple Silicon) and Noted-<version>.dmg (Intel)
  • Windows — Noted-<version>-<arch>-setup.exe (NSIS, x64 and arm64)
  • Linux — Noted-<version>.AppImage / Noted-<version>-arm64.AppImage, and noted_<version>_<arch>.deb (x64 and arm64)

Packaged output lands in release/. The MCP server bundle is unpacked from the asar (asarUnpack) so it can be spawned as a child process.

Signing & notarization ​

Release builds are signed with an Apple Developer ID and notarized by Apple so they run without Gatekeeper warnings. Signing runs locally, never in CI, so no Apple credentials are ever exposed to the build servers.

Add your credentials to .env.local (copy from .env.local.example):

bash
APPLE_ID=you@example.com
APPLE_TEAM_ID=XXXXXXXXXX
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx

Then run:

bash
bash scripts/release.sh

The script:

  1. Loads the credentials from .env.local (override the path with APPLE_CREDS_ENV=/path/to/creds.env).
  2. Runs npm run build, which signs the app with the Developer ID and the hardened runtime enabled (entitlements in build/entitlements.mac.plist).
  3. Submits each version-scoped DMG to Apple with xcrun notarytool submit --wait.
  4. Staples the notarization ticket to each DMG with xcrun stapler staple, so the download passes Gatekeeper offline.

Why the DMG is notarized separately

electron-builder signs and staples the .app, but not the .dmg wrapper itself. scripts/release.sh owns DMG notarization, which is why mac.notarize is set to false in package.json.

After a successful macOS build, notarize and publish the DMGs, then tag the release to trigger the cross-platform CI build:

bash
bash scripts/release.sh          # build + notarize + staple the DMGs
git tag v<version>
git push --tags                  # triggers the Windows/Linux CI build below
gh release create v<version> release/Noted-<version>*.dmg

Cross-platform builds (CI) ​

Windows and Linux installers are built in CI, because electron-builder cannot cross-compile them from macOS. .github/workflows/release.yml runs on a v* tag (and on manual dispatch): it builds the Windows and Linux targets on their own runners with build:win / build:linux and uploads them — together with the latest-*.yml auto-update metadata — to the GitHub release. The macOS DMGs are built and notarized locally (above) and added to the same release.

SBOM and provenance ​

When a release is published, .github/workflows/provenance.yml downloads every installer attached to it (the CI-built Windows/Linux ones and the locally-built macOS ones) and adds:

  • sbom.cdx.json — a CycloneDX 1.5 SBOM of the dependency tree at the release tag. Dev dependencies are included on purpose: the renderer is bundled by Vite at build time, so much of what ships is declared there.
  • SHA256SUMS — checksums of the installers and the SBOM — and SHA256SUMS.sigstore.json, its keyless cosign signature.
  • GitHub artifact attestations (SLSA build provenance and SBOM) for each file listed in SHA256SUMS.

The workflow only runs once the release is published, so upload the macOS assets into the draft before clicking Publish. To regenerate the files for an existing release, run the workflow manually from the Actions tab with the tag.

What the attestation does and does not prove

It proves these exact bytes were published to this release by this repository's workflow. For Windows and Linux the build itself also ran in CI. The macOS builds are compiled and notarized on the maintainer's machine, because the Apple credentials never leave it — for those, the attestation covers the publication step, not the compile step.

Continuous integration ​

.github/workflows/ci.yml runs on every push and pull request to main, on a Node 20.x and 22.x matrix:

  1. npm ci
  2. npm run lint
  3. npx tsc -b
  4. npm test — the full Vitest suite, including a locale-parity test that fails if any of the six shipped locales is missing a key.
  5. Verify bundles — vite build plus the three esbuild bundles.

.github/workflows/e2e.yml additionally drives the real Electron app with Playwright on macOS, Windows and Linux (open a vault, edit, rename, search, quit-flush, update check), and uploads window screenshots and traces as artifacts when a run fails. Locally: npm run build:bundles && npm run test:e2e (set ELECTRON_RUN_AS_NODE away first if your shell exports it). The suite runs the unpackaged app, so the update check is exercised in its "packaged builds only" path, not against a real update feed.

CI never signs or notarizes; that is a local-only step.

Run the same gates locally before opening a pull request:

bash
npm run lint
npx tsc -b
npm test

Documentation site ​

This documentation is a VitePress site under docs/. It is deployed to GitHub Pages by .github/workflows/docs.yml whenever docs/** changes on main.

bash
cd docs
npm install
npm run dev      # local preview at http://localhost:5173
npm run build    # output in docs/.vitepress/dist

Local-first. No account. No telemetry. · Privacy & legal