feat(about): show build commit next to the app version

Settings > About now renders "<version> (<short-sha>)" with the full
SHA in the tooltip, so bug reports from test and nightly builds
identify the exact commit. The commit is injected at CI build time into
apps/web/src/environments/build-commit.ts (same placeholder pattern as
the TMDB key inject); PR builds use the real head SHA instead of the
ephemeral merge commit. Local/dev builds keep the plain version.

The semver version itself deliberately stays untouched: a "-sha"
suffix would flip electron-updater into prerelease mode and leak into
installer/artifact version fields.

Requested by WolfganP in #1202.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-19 08:05:44 +02:00
1 parent 2f8aee72df
commit 2546bb9bfd
9 files changed
+155 -2

No files matched your search

+8
View File
@@ -396,6 +396,14 @@ jobs:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
run: node tools/tmdb/inject-tmdb-key.mjs
- name: Inject build commit
# Shows "<version> (<sha>)" in Settings > About so bug reports
# from test builds identify the exact commit. PR builds use the
# head SHA — github.sha would be the ephemeral merge commit.
env:
BUILD_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
run: node tools/build/inject-build-commit.mjs
- name: Build frontend
run: pnpm nx build web --skip-nx-cache
+3
View File
@@ -866,6 +866,9 @@ Build configurations in `apps/web/project.json`:
**Factory Pattern Implementation**:
The factory pattern ensures a single codebase works in both environments without conditional checks scattered throughout the application. All environment-specific logic is encapsulated in the service implementations.
**Build Commit In About**:
CI injects the git commit into `apps/web/src/environments/build-commit.ts` via `tools/build/inject-build-commit.mjs` (same placeholder pattern as the TMDB key inject); `Settings > About` then shows `"<version> (<short-sha>)"`. The semver version itself deliberately stays untouched — a `-sha` suffix would flip electron-updater into prerelease mode and leak into installer/artifact version fields. Local/dev builds keep the placeholder empty and show the plain version.
### Testing Strategy
- **Unit tests**: Jest with `jest-preset-angular` and `ng-mocks`
@@ -19,7 +19,17 @@
<p>{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}</p>
</div>
<div class="setting-item__control version-block">
<span>{{ version() }}</span>
<span data-test-id="app-version">
{{ version() }}
@if (buildCommitShort(); as commit) {
<span
class="build-commit"
data-test-id="app-build-commit"
[title]="buildCommit()"
>({{ commit }})</span
>
}
</span>
<small>{{ updateMessage() }}</small>
</div>
</div>
@@ -132,3 +132,60 @@ describe('SettingsAboutSectionComponent app updates', () => {
expect(getButton(fixture, 'app-update-open-release')).toBeTruthy();
});
});
describe('SettingsAboutSectionComponent version display', () => {
let fixture: ComponentFixture<SettingsAboutSectionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
SettingsAboutSectionComponent,
TranslateModule.forRoot(),
],
}).compileComponents();
fixture = TestBed.createComponent(SettingsAboutSectionComponent);
fixture.componentRef.setInput('activeSection', 'about');
fixture.componentRef.setInput('version', '0.23.0');
});
function getVersionBlock() {
return fixture.nativeElement.querySelector(
'[data-test-id="app-version"]'
) as HTMLElement;
}
function getCommitMarker() {
return fixture.nativeElement.querySelector(
'[data-test-id="app-build-commit"]'
) as HTMLElement | null;
}
it('shows the shortened build commit next to the version on CI builds', () => {
fixture.componentRef.setInput(
'buildCommit',
'949ea520aa11bb22cc33dd44ee55ff6677889900'
);
fixture.detectChanges();
expect(getVersionBlock().textContent).toContain('0.23.0');
expect(getCommitMarker()?.textContent).toBe('(949ea52)');
expect(getCommitMarker()?.getAttribute('title')).toBe(
'949ea520aa11bb22cc33dd44ee55ff6677889900'
);
});
it('shows the plain version when no build commit was injected', () => {
fixture.detectChanges();
expect(getVersionBlock().textContent).toContain('0.23.0');
expect(getCommitMarker()).toBeNull();
});
it('treats a whitespace-only commit as absent', () => {
fixture.componentRef.setInput('buildCommit', ' ');
fixture.detectChanges();
expect(getCommitMarker()).toBeNull();
});
});
@@ -18,15 +18,25 @@ import {
imports: [MatButtonModule, MatIconModule, TranslateModule],
templateUrl: './settings-about-section.component.html',
encapsulation: ViewEncapsulation.None,
styles: [':host { display: contents; }'],
styles: [
':host { display: contents; }',
'.version-block .build-commit { opacity: 0.65; font-size: 0.85em; }',
],
})
export class SettingsAboutSectionComponent {
readonly activeSection = input.required<string>();
readonly isDesktop = input(false);
readonly version = input<string | undefined>();
readonly buildCommit = input<string | undefined>();
readonly updateMessage = input<string | undefined>();
readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(null);
readonly buildCommitShort = computed(() => {
const commit = this.buildCommit()?.trim();
return commit ? commit.slice(0, 7) : undefined;
});
readonly checkForAppUpdate = output<void>();
readonly downloadAppUpdate = output<void>();
readonly installAppUpdate = output<void>();
@@ -113,6 +113,7 @@
[activeSection]="activeSection()"
[isDesktop]="isDesktop"
[version]="version"
[buildCommit]="buildCommit"
[updateMessage]="updateMessage"
[appUpdateStatus]="appUpdateStatus()"
(checkForAppUpdate)="checkForAppUpdate()"
@@ -33,6 +33,7 @@ import {
selectIsEpgAvailable,
} from '@iptvnator/m3u-state';
import { take } from 'rxjs';
import { BUILD_COMMIT } from '../../environments/build-commit';
import { DataService, RuntimeCapabilitiesService } from '@iptvnator/services';
import {
EmbeddedMpvSupport,
@@ -200,6 +201,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
/** Current version of the app */
version = '';
/** Git commit the app was built from (CI builds only) */
readonly buildCommit = BUILD_COMMIT;
/** Update message to show */
updateMessage = '';
readonly appUpdateStatus = signal<ElectronBridgeAppUpdateStatus | null>(
+10
View File
@@ -0,0 +1,10 @@
/**
* Git commit the app was built from. Populated by
* tools/build/inject-build-commit.mjs during CI builds; stays empty for
* local/dev builds, in which case Settings > About shows the plain version.
*
* Intentionally separate from AppConfig.version: appending the SHA to the
* semver itself would flip electron-updater into prerelease mode and leak
* into installer/artifact version fields.
*/
export const BUILD_COMMIT = '';
+50
View File
@@ -0,0 +1,50 @@
/**
* Injects the git commit SHA from the BUILD_COMMIT environment variable into
* the frontend BUILD_COMMIT constant before a CI build.
*
* The commit is shown next to the app version in Settings > About so bug
* reports from test and nightly builds identify the exact commit. The semver
* version itself intentionally stays untouched: a `-sha` suffix would flip
* electron-updater into prerelease mode and leak into installer/artifact
* version fields. Without BUILD_COMMIT the script is a no-op — local and dev
* builds show the plain version.
*
* Usage (CI, before `nx build web`):
* BUILD_COMMIT=<git-sha> node tools/build/inject-build-commit.mjs
*/
import { readFileSync, writeFileSync } from 'node:fs';
const CONFIG_PATH = 'apps/web/src/environments/build-commit.ts';
const MARKER = "export const BUILD_COMMIT = '';";
const commit = (process.env.BUILD_COMMIT ?? '').trim().toLowerCase();
if (!commit) {
console.warn(
'BUILD_COMMIT is not set — Settings > About will show the plain version.'
);
process.exit(0);
}
if (!/^[0-9a-f]{7,40}$/.test(commit)) {
console.error(
'BUILD_COMMIT must be a 7-40 character hex git SHA; aborting.'
);
process.exit(1);
}
const source = readFileSync(CONFIG_PATH, 'utf8');
if (!source.includes(MARKER)) {
console.error(
`Expected placeholder not found in ${CONFIG_PATH}. ` +
'Update tools/build/inject-build-commit.mjs if the constant moved.'
);
process.exit(1);
}
writeFileSync(
CONFIG_PATH,
source.replace(MARKER, `export const BUILD_COMMIT = '${commit}';`)
);
console.log(`Injected build commit ${commit} into ${CONFIG_PATH}.`);