Skip to content

Commit 722c48a

Browse files
authored
feat(web): show the build commit and time in the footer (#3788)
1 parent 934d1bd commit 722c48a

10 files changed

Lines changed: 89 additions & 9 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { mkdtempSync, rmSync } from 'node:fs';
2+
import { tmpdir } from 'node:os';
3+
import { join } from 'node:path';
4+
import { describe, expect, it } from 'vitest';
5+
import { buildInfo } from './build-info';
6+
7+
describe('buildInfo', () => {
8+
const now = new Date('2027-10-15T09:00:00Z');
9+
10+
it('has the short commit and the time', () => {
11+
expect(buildInfo(process.cwd(), now)).toEqual({
12+
sha: expect.stringMatching(/^[0-9a-f]{7,}$/),
13+
time: '2027-10-15T09:00:00.000Z',
14+
});
15+
});
16+
17+
it('leaves out the commit outside a git checkout', () => {
18+
const dir = mkdtempSync(join(tmpdir(), 'hoverboard-build-'));
19+
try {
20+
expect(buildInfo(dir, now)).toEqual({ time: '2027-10-15T09:00:00.000Z' });
21+
} finally {
22+
rmSync(dir, { recursive: true, force: true });
23+
}
24+
});
25+
});

‎packages/web/build/build-info.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { execFileSync } from 'node:child_process';
2+
3+
export interface BuildInfo {
4+
/** The short commit SHA. Missing outside a git checkout. */
5+
sha?: string;
6+
/** When the build started, in ISO 8601. */
7+
time: string;
8+
}
9+
10+
const shortSha = (cwd: string): string | undefined => {
11+
try {
12+
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
13+
cwd,
14+
encoding: 'utf8',
15+
stdio: ['ignore', 'pipe', 'ignore'],
16+
}).trim();
17+
return sha || undefined;
18+
} catch {
19+
return undefined;
20+
}
21+
};
22+
23+
/** The commit and time of this build, which the footer shows. */
24+
export const buildInfo = (cwd = process.cwd(), now = new Date()): BuildInfo => {
25+
const sha = shortSha(cwd);
26+
return { ...(sha ? { sha } : {}), time: now.toISOString() };
27+
};

‎packages/web/build/resolve-config.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,10 @@ describe('siteModule', () => {
609609
...resolveConfig({ paths: repoPaths, nodeEnv: 'production' }),
610610
contentTranslations: { es: { title: 'DevFest en español' } },
611611
};
612-
const plugin = siteModule(config, { siteDir: repoPaths.site });
612+
const plugin = siteModule(config, {
613+
siteDir: repoPaths.site,
614+
build: { sha: 'abc1234', time: '2027-10-15T09:00:00.000Z' },
615+
});
613616
const resolveId = plugin.resolveId as (id: string) => string | undefined;
614617
const load = plugin.load as (id: string) => Promise<string | undefined>;
615618

@@ -621,6 +624,9 @@ describe('siteModule', () => {
621624
'export const contentTranslations = {"es": () => import("virtual:hoverboard/content/es")};',
622625
);
623626
expect(code).toContain('export const heroIllustration = undefined;');
627+
expect(code).toContain(
628+
'export const build = {"sha":"abc1234","time":"2027-10-15T09:00:00.000Z"};',
629+
);
624630
expect(await load(resolveId('virtual:hoverboard/content/es')!)).toBe(
625631
'export default {"title":"DevFest en español"};\n',
626632
);

‎packages/web/build/site-module.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Types for `virtual:hoverboard/site`, which vite-plugin-site.ts serves. tsconfig.json maps it here.
2+
import type { BuildInfo } from './build-info';
23
import type { SiteConfig } from './resolve-config';
34

45
export declare const site: SiteConfig['site'];
@@ -7,3 +8,4 @@ export declare const resources: SiteConfig['resources'];
78
export declare const contentTranslations: Record<string, () => Promise<{ default: object }>>;
89
/** `heroSettings.home.illustration`, the SVG file's markup. */
910
export declare const heroIllustration: string | undefined;
11+
export declare const build: BuildInfo;

‎packages/web/build/vite-plugin-site.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import n from 'nunjucks';
55
import type { Plugin, PluginOption } from 'vite';
66
import copy from 'rollup-plugin-copy';
77
import { FEATURES, type Feature } from '../src/config/features';
8+
import { buildInfo } from './build-info';
89
import { fontModuleCode, fontModuleParts } from './fonts';
910
import {
1011
CONFIG_PATHS,
@@ -49,7 +50,12 @@ export const featureDefines = (features: Record<Feature, boolean>): Record<strin
4950
*/
5051
export const siteModule = (
5152
config: SiteConfig,
52-
{ defineFeatures = true, siteDir = CONFIG_PATHS.site, publicDir = CONFIG_PATHS.public } = {},
53+
{
54+
defineFeatures = true,
55+
siteDir = CONFIG_PATHS.site,
56+
publicDir = CONFIG_PATHS.public,
57+
build = buildInfo(),
58+
} = {},
5359
): Plugin => ({
5460
name: 'hoverboard-site-module',
5561
config: () => (defineFeatures ? { define: featureDefines(config.site.features) } : {}),
@@ -74,6 +80,7 @@ export const siteModule = (
7480
`export const resources = ${JSON.stringify(resources)};`,
7581
`export const contentTranslations = {${loaders.join(', ')}};`,
7682
`export const heroIllustration = ${JSON.stringify(heroIllustrationSvg(site, publicDir))};`,
83+
`export const build = ${JSON.stringify(build)};`,
7784
'',
7885
].join('\n');
7986
}

‎packages/web/src/components/footer/footer-nav.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import { screen, within } from '@testing-library/dom';
33
import { html } from 'lit';
44
import { fixture } from '../../../__tests__/helpers/fixtures';
55
import { setFeatures } from '../../../__tests__/helpers/features';
6-
import { organizer } from '../../config/site';
6+
import { build, organizer } from '../../config/site';
77
import './footer-nav';
88

99
describe('footer-nav', () => {
1010
it('defines a component', () => {
1111
expect(customElements.get('footer-nav')).toBeDefined();
1212
});
1313

14+
it('shows the commit and time of the build, in UTC', async () => {
15+
const { shadowRootForWithin } = await fixture(html`<footer-nav></footer-nav>`);
16+
const time = within(shadowRootForWithin).getByText(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC$/);
17+
18+
expect(time).toHaveAttribute('datetime', build.time);
19+
expect(within(shadowRootForWithin).getByText(build.sha ?? '', { exact: false })).toBeVisible();
20+
});
21+
1422
it('renders organizer logo and links', async () => {
1523
const { shadowRootForWithin } = await fixture(
1624
html`<footer-nav data-testid="footer"></footer-nav>`,

‎packages/web/src/components/footer/footer-nav.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { msg } from '@lit/localize';
22
import { css, html, nothing } from 'lit';
33
import { customElement } from 'lit/decorators.js';
44
import { ifDefined } from 'lit/directives/if-defined.js';
5-
import { organizer } from '../../config/site';
5+
import { build, organizer } from '../../config/site';
66
import { safeUrl } from '../../utils/safe-url';
77
import { navigationLabel } from '../shell/navigation-label';
88
import { ThemedElement } from '../themed-element';
@@ -70,6 +70,11 @@ export class FooterNav extends ThemedElement {
7070
? html`· <a class="coc" href="/coc">${navigationLabel('codeOfConduct')}</a>`
7171
: nothing
7272
}
73+
<span class="build"
74+
>· ${build.sha ? html`${build.sha} · ` : nothing}<time datetime="${build.time}"
75+
>${build.time.slice(0, 16).replace('T', ' ')} UTC</time
76+
></span
77+
>
7378
</div>
7479
</div>
7580
`;

‎packages/web/src/config/site.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// The only client entry point for site config. Read config from here, never from the data files.
22

33
import {
4+
build,
45
contentTranslations,
56
heroIllustration,
67
resources,
@@ -13,6 +14,8 @@ import { deepMerge } from './merge';
1314
export const { basepath, heroSettings, image, organizer, url } = settings;
1415
/** The site's own hero illustration's markup, from `heroSettings.home.illustration`. */
1516
export { heroIllustration };
17+
/** The commit and time of the build. */
18+
export { build };
1619
export const shortName = settings.shortName;
1720
/** `system` lets visitors pick a scheme in the footer. `light` and `dark` lock the site to one. */
1821
export const colorScheme = settings.theme.colorScheme as 'system' | 'light' | 'dark';

‎packages/web/src/data/build.ts‎

Lines changed: 0 additions & 2 deletions
This file was deleted.

‎packages/web/src/layouts/base.astro‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
import { ClientRouter } from 'astro:transitions';
33
import { fontCss, fontStylesheets } from 'virtual:hoverboard/fonts';
44
import { theme, themeCss } from 'virtual:hoverboard/layout';
5-
import { resources, site } from 'virtual:hoverboard/site';
5+
import { resources, site, build } from 'virtual:hoverboard/site';
66
import '../styles/base.css';
77
import { FooterBlock } from '../components/footer/footer-block';
88
import { AppHeader, demoBanner } from '../components/shell/app-header';
99
import { AppOverlays } from '../components/shell/app-overlays';
1010
import { loadContent } from '../data/content';
11-
import { BUILD_ID } from '../data/build';
1211
import '../data/markdown';
1312
import { seedPage } from '../data/page';
1413
import { openGraphLocale } from '../data/structured-data';
@@ -79,7 +78,7 @@ await demoBanner;
7978
<meta content={resources.keywords} name="keywords" />
8079
<meta content={site.organizer.name} name="author" />
8180
<meta content="Project Hoverboard" name="generator" />
82-
<meta content={BUILD_ID} name={BUILD_META} />
81+
<meta content={build.time} name={BUILD_META} />
8382

8483
<title>{title}</title>
8584

0 commit comments

Comments
 (0)