Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version> (<short-commit>)` into
`dist/sql.html` (graceful `v<version>` 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`.
Expand Down
26 changes: 25 additions & 1 deletion build/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,32 @@

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<version> (<short-commit>)`, or
// just `v<version>` when this isn't a git checkout (offline tarball, CI export).
// 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 = 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}`;
}

async function main() {
const result = await build({
entryPoints: [resolve(root, 'src/main.js')],
Expand All @@ -24,7 +44,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');

Expand Down
4 changes: 3 additions & 1 deletion build/bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
7 changes: 6 additions & 1 deletion src/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' —
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions tests/helpers/fake-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions tests/unit/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down