From 524329fef91aff6ffe6e06c5248510778c383a51 Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Mon, 29 Jun 2026 12:42:03 +0200 Subject: [PATCH 1/2] feat: surface an in-app version/build stamp (version + commit) (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app version lived only in package.json — not baked into the build nor shown in the UI — so a bug report couldn't be tied to an exact build of the self-contained dist/sql.html copied onto clusters. - build/build.mjs: bake `v ()` into the bundle via a __ASB_BUILD__ token replace (same seam as the styles/script splices); read version from package.json + `git rev-parse --short HEAD`, with a graceful `v` fallback when there's no git checkout. - src/main.js: pass the placeholder through the createApp(env) seam. - src/ui/app.js: expose `app.build` (env.build || 'dev'); render a small `.um-build` line in the user-menu footer. - src/styles.css: muted/mono `.um-build` rule matching the menu footer. - tests: stub `build` in fake-app and assert the user-menu line renders. - CHANGELOG: note under [Unreleased]. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PgV4UResR7braUkAq7VaCr --- CHANGELOG.md | 3 +++ build/build.mjs | 20 +++++++++++++++++++- src/main.js | 2 +- src/styles.css | 5 +++++ src/ui/app.js | 7 ++++++- tests/helpers/fake-app.js | 1 + tests/unit/app.test.js | 1 + 7 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 721a80f..cbecf14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ auto-generated per-PR notes; this file is the curated, human-readable history. ## [Unreleased] ### Added +- In-app build stamp: the build bakes `v ()` into + `dist/sql.html` (graceful `v` fallback when not a git checkout) and + shows it in the user menu, so a bug report can be tied to an exact build (#74). - `NOTICE` + `THIRD-PARTY-NOTICES.md`, and the bundled Chart.js / dagre (MIT) notices are now embedded in the built `dist/sql.html`. - `CONTRIBUTING.md` and this `CHANGELOG.md`. diff --git a/build/build.mjs b/build/build.mjs index fb3916f..5572f3f 100644 --- a/build/build.mjs +++ b/build/build.mjs @@ -8,12 +8,26 @@ import { build } from 'esbuild'; import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, '..'); +// The build stamp shown in the UI (user menu) and grep-able in dist/sql.html, so +// a bug report can be tied to an exact build: `v ()`, or +// just `v` when this isn't a git checkout (offline tarball, CI export). +// version is the single source of truth in package.json; commit comes from git. +async function buildStamp() { + const { version } = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')); + let commit = ''; + try { + commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root }).toString().trim(); + } catch { /* not a git checkout — fall back to version only */ } + return commit ? `v${version} (${commit})` : `v${version}`; +} + async function main() { const result = await build({ entryPoints: [resolve(root, 'src/main.js')], @@ -24,7 +38,11 @@ async function main() { write: false, legalComments: 'none', }); - const script = result.outputFiles[0].text; + // Replace the `__ASB_BUILD__` placeholder (a string literal in src/main.js) + // with the build stamp before the bundle is inlined — same token-replace seam + // as the styles/script splices below. replaceAll is robust to either quote + // style minify may emit around the literal. + const script = result.outputFiles[0].text.replaceAll('__ASB_BUILD__', await buildStamp()); const styles = await readFile(resolve(root, 'src/styles.css'), 'utf8'); const template = await readFile(resolve(here, 'template.html'), 'utf8'); diff --git a/src/main.js b/src/main.js index 945c35f..2dd0a13 100644 --- a/src/main.js +++ b/src/main.js @@ -87,7 +87,7 @@ export async function bootstrap(app, env) { /* c8 ignore start -- browser entry side-effect, exercised via the live app */ if (typeof document !== 'undefined' && !globalThis.__ASB_NO_AUTOSTART__) { - const app = createApp({ Chart, Dagre }); + const app = createApp({ Chart, Dagre, build: '__ASB_BUILD__' }); document.addEventListener('keydown', (e) => handleKeydown(e, app)); bootstrap(app, { location: window.location, diff --git a/src/styles.css b/src/styles.css index 75cc72b..e6190a1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -295,6 +295,11 @@ body { .user-menu .um-item:hover { background: var(--bg-hover); } .user-menu .um-item.danger { color: #ef4444; } .user-menu .um-item.danger:hover { background: color-mix(in oklab, #ef4444 12%, transparent); } +.user-menu .um-build { + margin-top: 3px; padding: 7px 9px; border-top: 1px solid var(--border-faint); + font-size: 10.5px; color: var(--fg-faint); font-family: var(--mono); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} /* ------------ header File menu + library title ------------ */ .hd-divider { width: 1px; height: 18px; background: var(--border); flex-shrink: 0; } diff --git a/src/ui/app.js b/src/ui/app.js index 1a6507a..0f5bacf 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -64,6 +64,10 @@ export function createApp(env = {}) { // child tab can inline the page's CSS (about:blank ships none of it). openWindow: env.openWindow || ((...a) => win.open(...a)), stylesText: env.stylesText || (doc.querySelector('style') ? doc.querySelector('style').textContent : ''), + // Build stamp ("v0.1.4 (abc1234)") injected at build time via main.js; shown + // in the user menu so a bug report can be tied to a build. 'dev' in tests / + // an un-built run where the placeholder was never replaced. + build: env.build || 'dev', }; // Two ways to be signed in: OAuth (a JWT bearer, the default) or 'basic' — @@ -787,7 +791,8 @@ export function createApp(env = {}) { let close; const menu = h('div', { class: 'user-menu' }, h('div', { class: 'um-id' }, app.email()), - h('button', { class: 'um-item danger', onclick: () => { close(); app.signOut(); } }, Icon.logout(), h('span', null, 'Log out'))); + h('button', { class: 'um-item danger', onclick: () => { close(); app.signOut(); } }, Icon.logout(), h('span', null, 'Log out')), + h('div', { class: 'um-build', title: 'App version / build' }, app.build)); ({ close } = anchoredPopover(menu, app.dom.userBtn, 'userMenu')); } app.openUserMenu = openUserMenu; diff --git a/tests/helpers/fake-app.js b/tests/helpers/fake-app.js index b38fcad..f0c3d2c 100644 --- a/tests/helpers/fake-app.js +++ b/tests/helpers/fake-app.js @@ -31,6 +31,7 @@ export function makeApp(over = {}) { cssVar: () => '', // blank → chartColors() uses its dark-theme fallbacks chart: null, host: () => 'test.host', + build: 'v0.0.0-test', activeTab: () => activeTab(state), isSignedIn: () => true, email: () => 'me@example.com', diff --git a/tests/unit/app.test.js b/tests/unit/app.test.js index fe78d8f..4aed1bd 100644 --- a/tests/unit/app.test.js +++ b/tests/unit/app.test.js @@ -135,6 +135,7 @@ describe('renderApp shell', () => { const menu = document.querySelector('.user-menu'); expect(menu).not.toBeNull(); expect(menu.querySelector('.um-id').textContent).toBe('me@example.com'); + expect(menu.querySelector('.um-build').textContent).toBe(app.build); // build stamp ('dev' here) menu.querySelector('.um-item.danger').dispatchEvent(new Event('click', { bubbles: true })); expect(app.token).toBeNull(); expect(e.sessionStorage.getItem('oauth_id_token')).toBeNull(); From cd90a84df6b9c6a2de8612216dc27d9ffebb9739 Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Mon, 29 Jun 2026 12:46:57 +0200 Subject: [PATCH 2/2] fix(build): keep stamp in lockstep with release version + mark dirty trees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review follow-ups on the build-stamp change: - build.mjs honors $ASB_VERSION over package.json, and bundle.sh passes the resolved release version (release.yml calls `bundle.sh ${GITHUB_REF_NAME#v}`). Previously the bundle's VERSION file used the tag while sql.html's stamp re-read package.json — a `bundle.sh ` with an unbumped package.json would ship two divergent versions in one artifact. - Append `-dirty` (via `git status --porcelain`) when the working tree differs from HEAD, so a hand-built artifact (e.g. a manual `kubectl cp dist/sql.html`) isn't mistaken for the clean commit it sits on. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01PgV4UResR7braUkAq7VaCr --- build/build.mjs | 10 ++++++++-- build/bundle.sh | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/build/build.mjs b/build/build.mjs index 5572f3f..c513761 100644 --- a/build/build.mjs +++ b/build/build.mjs @@ -18,12 +18,18 @@ const root = resolve(here, '..'); // The build stamp shown in the UI (user menu) and grep-able in dist/sql.html, so // a bug report can be tied to an exact build: `v ()`, or // just `v` when this isn't a git checkout (offline tarball, CI export). -// version is the single source of truth in package.json; commit comes from git. +// A dirty working tree appends `-dirty` so a hand-built artifact (e.g. a manual +// `kubectl cp dist/sql.html`) is never mistaken for the clean commit it sits on. +// Version source: $ASB_VERSION when set (bundle.sh passes the release tag so the +// stamp and the bundle's VERSION file stay in lockstep), else package.json. async function buildStamp() { - const { version } = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')); + const version = process.env.ASB_VERSION + || JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')).version; let commit = ''; try { commit = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root }).toString().trim(); + // `git status --porcelain` is empty iff the tree exactly matches HEAD. + if (execFileSync('git', ['status', '--porcelain'], { cwd: root }).toString().trim()) commit += '-dirty'; } catch { /* not a git checkout — fall back to version only */ } return commit ? `v${version} (${commit})` : `v${version}`; } diff --git a/build/bundle.sh b/build/bundle.sh index 537ae1e..7489317 100755 --- a/build/bundle.sh +++ b/build/bundle.sh @@ -22,7 +22,9 @@ OUT="$ROOT/dist" STAGE="$OUT/bundle/altinity-sql-browser" echo "==> Building SPA" -node "$ROOT/build/build.mjs" +# Pass the resolved version through so the in-HTML build stamp matches the +# VERSION file written below (build.mjs honors $ASB_VERSION over package.json). +ASB_VERSION="$VERSION" node "$ROOT/build/build.mjs" echo "==> Staging bundle ($VERSION)" rm -rf "$OUT/bundle"