Build versioning
Every build of the Helm Worker carries a frozen identity — version, git_sha, built_at. Every audit row records the build that produced it. Two consequences: a forensic investigation can ask "which build was running when this event happened" from the audit chain alone; and the operator can see at a glance whether they're on the build they expect.
The build hook + audit column shipped together. Migration 013 added build_version to audit_events. Pre-versioning rows stay NULL — the absence of a version is itself the label for "before this slice landed."
The build identity
A single JSON object at the repo root, refreshed on every wrangler dev and wrangler deploy:
// build_info.json
{
"version": "v0.4.0",
"slice": 4,
"patch": 0,
"git_sha": "b6794e5",
"built_at": "2026-05-11T21:30:11-07:00",
"environment": "local"
}
version—v0.<slice>.<patch>. The slice number bumps when a new vertical slice ships ("Alpha-quality"). The patch number bumps for everything else.slice— the integer the version string encodesgit_sha— short SHA of the current HEAD at build timebuilt_at— ISO 8601 with local TZ offsetenvironment—local/staging/productionfromHELM_ENV
How it stays current
A wrangler.jsonc build hook runs node scripts/build_info.js before every wrangler dev startup and every wrangler deploy:
// wrangler.jsonc
"build": {
"command": "node scripts/build_info.js"
}
The script reads the current version from build_info.json, captures git rev-parse --short HEAD, takes a fresh timestamp, and overwrites the file. npm run predev and npm run predeploy hooks in package.json re-run the script if you start the dev server or deploy without going through wrangler directly.
Bumping the version is explicit:
npm run version:patch # v0.4.0 -> v0.4.1 (everything that isn't a new slice)
npm run version:slice # v0.4.x -> v0.5.0 (next slice goes Alpha)
scripts/bump_version.js reads build_info.json, increments the right component, and writes it back. The next dev/deploy picks up the new version on its first build-hook run.
How the Worker uses it
The worker imports build_info.json at module load:
// src/index.js
import buildInfo from '../build_info.json';
const BUILD_VERSION = buildInfo.version;
esbuild inlines the JSON at bundle time, so BUILD_VERSION is frozen at the build that produced the worker artifact. A running worker can't drift — its version is whatever the build said it was. Hot reloads in dev pick up a new value because the build hook ran first.
Two places use it:
/api/build-info — returns the full buildInfo object. The frontend fetches it once on app load, stashes on window.helmBuildInfo. The Sys Admin user-menu dropdown renders a version block; the Beta Comments capture snapshots the version at click time so every feedback item knows which build the operator was on.
recordAuditEvent — passes BUILD_VERSION as a constant to every audit INSERT. Mutations, auth events, and the manual client channel (POST /api/audit/manual used by Beta Comments) all stamp the build version on the audit_events row.
The audit column
Migration 013 added the column + a filtered index:
-- migrations/013_audit_build_version.sql
ALTER TABLE audit_events ADD COLUMN build_version TEXT;
CREATE INDEX IF NOT EXISTS idx_audit_events_build_version
ON audit_events(build_version)
WHERE build_version IS NOT NULL;
The filtered index excludes pre-versioning rows (NULL) — they don't need to be in the index because forensic queries by version skip them by definition.
Schema spec (Helm/kvick_helm_d1_schema.md is the source of truth) documents the column and includes a migration note about the ALTER path:
Existing local DBs pick them up via the ALTER in 013; fresh installs apply 001 (no column) → ... → 013 (adds column) → end state matches the spec. Don't regenerate 001 from this spec, or fresh installs will conflict at 013.
Forensic queries
The whole point: investigations can pivot on build version.
"Show me every event from build v0.4.12"
SELECT id, at, action, summary
FROM audit_events
WHERE build_version = 'v0.4.12'
ORDER BY at;
The filtered index makes this fast.
"When did v0.4.12 first appear and when did it stop?"
SELECT build_version, MIN(at) AS first_seen, MAX(at) AS last_seen, COUNT(*) AS event_count
FROM audit_events
WHERE build_version IS NOT NULL
GROUP BY build_version
ORDER BY first_seen DESC;
Useful for "did the bug land in v0.4.10 or v0.4.11?"
"Which builds did this customer's records flow through?"
SELECT DISTINCT build_version
FROM audit_events
WHERE context_customer_id = ?
ORDER BY build_version;
What this is not
- Not a release tracker. The version isn't tied to git tags or release notes. It's the build that ran. Multiple commits can carry the same patch version if no one bumped between them.
- Not a feature flag system. Build versioning records what ran; it doesn't change what runs. Toggling behavior is a separate concern.
- Not a continuous deployment trigger. The version doesn't auto-bump on every commit. Bumps are explicit (
npm run version:patch/version:slice) because version is a claim, not a side-effect.
Why this is in slice 4's prep, not its own slice
The build-versioning machinery is small (~150 lines across scripts/, wrangler.jsonc, src/index.js, and migration 013). It doesn't have its own UI surface beyond the user-menu version pill. It's infrastructure that money-handling slices need before they ship — the audit chain becomes meaningfully more useful with build identity attached, and the slices that carry money are the ones we most need to investigate forensically.
Folded into slice 4 (service-ticket completion) as a prep deliverable; lands once and stays.
See also
- Audit-everything — the chain that build identity attaches to
- Slice 1 — Identity & Audit — where
audit_eventsis owned - Observability — build_version is a forensic dimension alongside request_id
- ADR-0005: Tamper-chain audit log — the chain whose rows now carry build identity
- Beta feedback system — captures build_version on every comment for "this looked wrong on Tuesday" precision