diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index 14fd1cc94..7ddc41e6e 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -406,6 +406,14 @@ jobs: TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }} run: node tools/tmdb/inject-tmdb-key.mjs + - name: Inject build commit + # Shows " ()" 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 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ed059fefc..45ebf9fd3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -109,5 +109,7 @@ jobs: push: ${{ steps.docker-meta.outputs.publish == 'true' }} tags: ${{ steps.docker-meta.outputs.tags }} platforms: ${{ steps.docker-meta.outputs.platforms }} + build-args: | + BUILD_COMMIT=${{ github.event.pull_request.head.sha || github.sha }} cache-from: type=gha cache-to: type=gha,mode=max,ignore-error=true diff --git a/CLAUDE.md b/CLAUDE.md index d3d18d399..ff9f9e8fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -871,6 +871,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 `" ()"`. 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` diff --git a/apps/web/src/app/settings/settings-about-section.component.html b/apps/web/src/app/settings/settings-about-section.component.html index 184672bcc..c5ab3ddaf 100644 --- a/apps/web/src/app/settings/settings-about-section.component.html +++ b/apps/web/src/app/settings/settings-about-section.component.html @@ -19,7 +19,17 @@

{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}

- {{ version() }} + + {{ version() }} + @if (buildCommitShort(); as commit) { + ({{ commit }}) + } + {{ updateMessage() }}
diff --git a/apps/web/src/app/settings/settings-about-section.component.spec.ts b/apps/web/src/app/settings/settings-about-section.component.spec.ts index 2ca97c487..ce5c87265 100644 --- a/apps/web/src/app/settings/settings-about-section.component.spec.ts +++ b/apps/web/src/app/settings/settings-about-section.component.spec.ts @@ -132,3 +132,60 @@ describe('SettingsAboutSectionComponent app updates', () => { expect(getButton(fixture, 'app-update-open-release')).toBeTruthy(); }); }); + +describe('SettingsAboutSectionComponent version display', () => { + let fixture: ComponentFixture; + + 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(); + }); +}); diff --git a/apps/web/src/app/settings/settings-about-section.component.ts b/apps/web/src/app/settings/settings-about-section.component.ts index ed510ab46..eb02ce4e7 100644 --- a/apps/web/src/app/settings/settings-about-section.component.ts +++ b/apps/web/src/app/settings/settings-about-section.component.ts @@ -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(); readonly isDesktop = input(false); readonly version = input(); + readonly buildCommit = input(); readonly updateMessage = input(); readonly appUpdateStatus = input(null); + readonly buildCommitShort = computed(() => { + const commit = this.buildCommit()?.trim(); + + return commit ? commit.slice(0, 7) : undefined; + }); + readonly checkForAppUpdate = output(); readonly downloadAppUpdate = output(); readonly installAppUpdate = output(); diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index c1bbd7758..0ef375d3d 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -113,6 +113,7 @@ [activeSection]="activeSection()" [isDesktop]="isDesktop" [version]="version" + [buildCommit]="buildCommit" [updateMessage]="updateMessage" [appUpdateStatus]="appUpdateStatus()" (checkForAppUpdate)="checkForAppUpdate()" diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 2b67bf278..258e2a1f4 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -45,6 +45,7 @@ import { Theme, VideoPlayer, } from '@iptvnator/shared/interfaces'; +import { BUILD_COMMIT } from '../../environments/build-commit'; import { SettingsStore } from '../services/settings-store.service'; import { SettingsService } from './../services/settings.service'; import { SettingsAboutSectionComponent } from './settings-about-section.component'; @@ -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( diff --git a/apps/web/src/environments/build-commit.ts b/apps/web/src/environments/build-commit.ts new file mode 100644 index 000000000..066dc233e --- /dev/null +++ b/apps/web/src/environments/build-commit.ts @@ -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 = ''; diff --git a/docker/Dockerfile b/docker/Dockerfile index c00b232d9..53368c8c7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,6 +15,11 @@ RUN corepack enable && pnpm install --frozen-lockfile --ignore-scripts COPY . . +# CI passes the git commit so Settings > About shows " ()"; +# the inject script is a no-op when BUILD_COMMIT is empty (local builds). +ARG BUILD_COMMIT="" +RUN node tools/build/inject-build-commit.mjs + RUN pnpm nx build web --configuration=pwa RUN pnpm nx build web-backend diff --git a/tools/build/inject-build-commit.mjs b/tools/build/inject-build-commit.mjs new file mode 100644 index 000000000..fa6fcb914 --- /dev/null +++ b/tools/build/inject-build-commit.mjs @@ -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= 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}.`);