Release & versioning

How components are versioned and built — the version.json model.

How Busymate's components are versioned and built — a single-source model where one file describes every component's identity, version, and build number.

Busymate DevTools is a monorepo of several components (the iOS app, dashboard, proxy-server, docs site, status board, backend, BusyBro, the bmc connector). One file, version.json at the repo root, is the single source of truth for all of them.

TL;DR

Source of truthversion.json at the repo root.
Per componentname, label, host, description, version, build.
Bump ruleOnly the component that actually changed gets a build bump.
Top-level buildmax() of all component builds.
Propagationscripts/sync-version.mjs mirrors values into each subproject's package.json.
Live readGET https://dash.busymate.net/api/version.

The version.json model

The file has a top-level identity plus a components map. Each component carries its own identity and a version + build:

json
{
  "name": "Busymate DevTools",
  "version": "1.0",
  "build": 332,
  "components": {
    "ios": {
      "name": "Busymate Helper",
      "label": "ios",
      "host": "ios.busymate.net",
      "description": "iOS network-debugging app …",
      "version": "1.0.1",
      "build": 170
    },
    "dashboard": { "label": "dash", "host": "dash.busymate.net", "version": "1.0", "build": 332 },
    "supabase":  { "label": "api",  "host": "api.busymate.net",  "version": "1.0", "build": 249 }
  }
}
  • version is the human-facing semantic version (e.g. 1.0).
  • build is a monotonically increasing integer — the deploy counter.
  • name / label / host / description are the component's identity, read by every surface that displays it.

The bump rule

When a component ships, only that component's build bumps — a dashboard-only fix bumps components.dashboard.build, an iOS-only fix bumps components.ios.build, and so on. Nothing else moves.

The top-level build is always max() of the component builds, kept as a single headline counter.

sql
-- conceptually
top_level.build = max(components[*].build)

This means a build number is meaningful per component — comparing components.dashboard.build across two deploys tells you exactly how many dashboard deploys happened, independent of other components.

Propagation

Editing version.json is step one; scripts/sync-version.mjs propagates the canonical values into each subproject's package.json mirror (displayNamename, plus description, version, build):

bash
node scripts/sync-version.mjs          # write the mirrors
node scripts/sync-version.mjs --check  # exit 1 if any mirror is stale (no writes)

Each app reads its identity at runtime from its own package.json — the dashboard and docs bundle displayName/description, bmc shows them in its --help banner, and proxy-server logs them at startup. So version.json is the one place to edit; everything else is generated.

Never hardcode a component name, description, or build number anywhere else — edit version.json and run the sync script.

Reading the current build at runtime

The dashboard exposes the live manifest at /api/version — always served fresh (no caching), so it reflects the actually-deployed build:

bash
curl -s https://dash.busymate.net/api/version
json
{
  "name": "Busymate DevTools",
  "version": "1.0",
  "build": 332,
  "components": {
    "dashboard": { "version": "1.0", "build": 332 },
    "ios":       { "version": "1.0.1", "build": 170 },
    "supabase":  { "version": "1.0", "build": 249 }
  }
}

The dashboard's own client polls this route to detect a new deploy and reload stale tabs; the iOS app reads components.ios.build to surface an update banner.

The full ship pipeline — how a bump flows through deploy, the release notifier, and the in-app update banner — lives under Under the Hood → Shipping pipeline.