Skip to content

Commit e790952

Browse files
committed
feat: expose the deployed version via the API and show it in the GUI
1 parent cc84869 commit e790952

7 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
namespace API.Controllers.Responses;
3+
4+
/// <summary>The running build's identity, stamped at compile time by GitInfo + BuildInformation.</summary>
5+
public record VersionResponse(string Version, string Commit, string BuiltAt)
6+
{
7+
public static VersionResponse Current => new(
8+
ThisAssembly.Git.Tag is { Length: > 0 } tag ? tag : $"{ThisAssembly.Git.Branch}@{ThisAssembly.Git.Commit}",
9+
ThisAssembly.Git.Commit,
10+
BuildInformation.BuildAt.ToString("u"));
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using API.Controllers.Responses;
2+
using Asp.Versioning;
3+
using Microsoft.AspNetCore.Http.HttpResults;
4+
using Microsoft.AspNetCore.Mvc;
5+
using static Microsoft.AspNetCore.Http.StatusCodes;
6+
7+
namespace API.Controllers;
8+
9+
[ApiVersion(2)]
10+
[ApiController]
11+
[Route("v{v:apiVersion}/[controller]")]
12+
public class VersionController : ControllerBase
13+
{
14+
/// <summary>
15+
/// The running build's identity: release tag (or branch@commit for untagged builds), commit, build time.
16+
/// </summary>
17+
/// <response code="200"></response>
18+
[HttpGet]
19+
[ProducesResponseType<VersionResponse>(Status200OK, "application/json")]
20+
public Ok<VersionResponse> GetVersion() => TypedResults.Ok(VersionResponse.Current);
21+
}

api/API/openapi/API_v2.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4980,6 +4980,26 @@
49804980
}
49814981
}
49824982
},
4983+
"/v2/Version": {
4984+
"get": {
4985+
"tags": [
4986+
"Version"
4987+
],
4988+
"summary": "The running build's identity: release tag (or branch@commit for untagged builds), commit, build time.",
4989+
"responses": {
4990+
"200": {
4991+
"description": "",
4992+
"content": {
4993+
"application/json; x-version=2.0": {
4994+
"schema": {
4995+
"$ref": "#/components/schemas/VersionResponse"
4996+
}
4997+
}
4998+
}
4999+
}
5000+
}
5001+
}
5002+
},
49835003
"/v2/Series/{MangaId}/volumes": {
49845004
"get": {
49855005
"tags": [
@@ -7265,6 +7285,25 @@
72657285
"additionalProperties": false,
72667286
"description": "Entry for a manga with unresolved chapters or missing files."
72677287
},
7288+
"VersionResponse": {
7289+
"type": "object",
7290+
"properties": {
7291+
"version": {
7292+
"type": "string",
7293+
"nullable": true
7294+
},
7295+
"commit": {
7296+
"type": "string",
7297+
"nullable": true
7298+
},
7299+
"builtAt": {
7300+
"type": "string",
7301+
"nullable": true
7302+
}
7303+
},
7304+
"additionalProperties": false,
7305+
"description": "The running build's identity, stamped at compile time by GitInfo + BuildInformation."
7306+
},
72687307
"VolumeBundleState": {
72697308
"enum": [
72707309
"NotApplicable",
@@ -7396,6 +7435,9 @@
73967435
{
73977436
"name": "Settings"
73987437
},
7438+
{
7439+
"name": "Version"
7440+
},
73997441
{
74007442
"name": "Volume"
74017443
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Net.Http.Json;
2+
using System.Text.Json;
3+
using Xunit;
4+
5+
namespace API.Tests.Integration;
6+
7+
/// <summary>The deployed version must be discoverable from the GUI, so the API exposes its build identity.</summary>
8+
public class VersionEndpointTests : IAsyncLifetime
9+
{
10+
private readonly PostgresFixture _postgres = new();
11+
private string _dbName = null!;
12+
private KenkuApplicationFactory _app = null!;
13+
14+
public async Task InitializeAsync()
15+
{
16+
_dbName = await _postgres.CreateDatabaseAsync();
17+
_app = new KenkuApplicationFactory { PostgresConnectionString = _postgres.GetConnectionString(_dbName) };
18+
}
19+
20+
public async Task DisposeAsync()
21+
{
22+
_app.Dispose();
23+
await _postgres.DropDatabaseAsync(_dbName);
24+
}
25+
26+
[Fact]
27+
public async Task GetVersion_ReturnsTheBuildIdentity()
28+
{
29+
using var client = _app.CreateClient();
30+
31+
var json = await client.GetFromJsonAsync<JsonElement>("/v2/Version");
32+
33+
Assert.False(string.IsNullOrWhiteSpace(json.GetProperty("version").GetString()));
34+
Assert.False(string.IsNullOrWhiteSpace(json.GetProperty("commit").GetString()));
35+
Assert.False(string.IsNullOrWhiteSpace(json.GetProperty("builtAt").GetString()));
36+
}
37+
}

web/website/app/app.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<UNavigationMenu :items="primaryNav" orientation="vertical" variant="link" class="-mx-2.5" />
3030
<USeparator class="my-4" />
3131
<UNavigationMenu :items="devLinks" orientation="vertical" variant="link" class="-mx-2.5" />
32+
<AppVersion class="mt-4 block" />
3233
</template>
3334

3435
<template #right>
@@ -57,6 +58,8 @@
5758
<UDropdownMenu :items="devLinks as DropdownMenuItem[]" :content="{ align: 'end' }">
5859
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" aria-label="More" class="max-sm:hidden" />
5960
</UDropdownMenu>
61+
62+
<AppVersion class="max-lg:hidden" />
6063
</template>
6164
</UHeader>
6265

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<span v-if="version" class="font-mono text-xs text-dimmed" :title="`commit ${version.commit} · built ${version.builtAt}`">
3+
{{ version.version }}
4+
</span>
5+
</template>
6+
7+
<script setup lang="ts">
8+
const { data: version } = useApi('/v2/Version', { key: 'Version', server: false });
9+
</script>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime';
3+
import AppVersion from '~/components/AppVersion.vue';
4+
5+
registerEndpoint('/v2/Version', () => ({ version: 'v0.16.0', commit: 'abc1234', builtAt: '2026-06-11 14:00:00Z' }));
6+
7+
describe('AppVersion', () => {
8+
it('shows the deployed version from the API', async () => {
9+
const wrapper = await mountSuspended(AppVersion);
10+
11+
await vi.waitFor(() => expect(wrapper.text()).toContain('v0.16.0'));
12+
});
13+
});

0 commit comments

Comments
 (0)