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

@@ -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 = '';