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 truth | version.json at the repo root. |
| Per component | name, label, host, description, version, build. |
| Bump rule | Only the component that actually changed gets a build bump. |
Top-level build | max() of all component builds. |
| Propagation | scripts/sync-version.mjs mirrors values into each subproject's package.json. |
| Live read | GET 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:
{
"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 }
}
}versionis the human-facing semantic version (e.g.1.0).buildis a monotonically increasing integer — the deploy counter.name/label/host/descriptionare 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.
-- 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 (displayName ← name, plus description, version, build):
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.jsonand 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:
curl -s https://dash.busymate.net/api/version{
"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.