Skip to content

Commit

Permalink
introduce configuration to define fw lite platform releases and how r…
Browse files Browse the repository at this point in the history
…elease assets are matched per platform, add a platform as a parameter to the http api (#1371)

* introduce configuration to define fw lite platform releases and how release assets are matched per platform, add a platform as a parameter to the http api
  • Loading branch information
hahn-kev authored Jan 14, 2025
1 parent a8d082f commit 20f623a
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 31 deletions.
22 changes: 22 additions & 0 deletions backend/LexBoxApi/Config/FwLiteReleaseConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using LexBoxApi.Controllers;
using LexCore.Entities;

namespace LexBoxApi.Config;

public class FwLiteReleaseConfig
{
public Dictionary<FwLitePlatform, FwLitePlatformConfig> Platforms { get; set; } = new();
}

public class FwLitePlatformConfig
{
[Required]
public required string FileNameRegex { get; init; }

[field: AllowNull]
public Regex FileName => field ??= new Regex(FileNameRegex);
}
20 changes: 11 additions & 9 deletions backend/LexBoxApi/Controllers/FwLiteReleaseController.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using LexBoxApi.Config;
using LexBoxApi.Otel;
using LexBoxApi.Services.FwLiteReleases;
using LexCore.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;

namespace LexBoxApi.Controllers;

Expand All @@ -19,10 +17,11 @@ public class FwLiteReleaseController(FwLiteReleaseService releaseService) : Cont
[HttpGet("download-latest")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DownloadLatest()
public async Task<ActionResult> DownloadLatest([FromQuery] FwLitePlatform platform = FwLitePlatform.Windows)
{
using var activity = LexBoxActivitySource.Get().StartActivity();
var latestRelease = await releaseService.GetLatestRelease();
activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString());
var latestRelease = await releaseService.GetLatestRelease(platform);
if (latestRelease is null)
{
activity?.SetStatus(ActivityStatusCode.Error, "Latest release not found");
Expand All @@ -37,23 +36,26 @@ public async Task<ActionResult> DownloadLatest()
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async ValueTask<ActionResult<FwLiteRelease>> LatestRelease(string? appVersion = null)
public async ValueTask<ActionResult<FwLiteRelease>> LatestRelease([FromQuery] FwLitePlatform platform =
FwLitePlatform.Windows, string? appVersion = null)
{
using var activity = LexBoxActivitySource.Get().StartActivity();
activity?.AddTag(FwLiteReleaseService.FwLiteClientVersionTag, appVersion ?? "unknown");
var latestRelease = await releaseService.GetLatestRelease();
activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString());
var latestRelease = await releaseService.GetLatestRelease(platform);
activity?.AddTag(FwLiteReleaseService.FwLiteReleaseVersionTag, latestRelease?.Version);
if (latestRelease is null) return NotFound();
return latestRelease;
}

[HttpGet("should-update")]
[AllowAnonymous]
public async Task<ActionResult<ShouldUpdateResponse>> ShouldUpdate([FromQuery] string appVersion)
public async Task<ActionResult<ShouldUpdateResponse>> ShouldUpdate([FromQuery] string appVersion, [FromQuery] FwLitePlatform platform = FwLitePlatform.Windows)
{
using var activity = LexBoxActivitySource.Get().StartActivity();
activity?.AddTag(FwLiteReleaseService.FwLiteClientVersionTag, appVersion);
var response = await releaseService.ShouldUpdate(appVersion);
activity?.AddTag(FwLiteReleaseService.FwLitePlatformTag, platform.ToString());
var response = await releaseService.ShouldUpdate(platform, appVersion);
activity?.AddTag(FwLiteReleaseService.FwLiteReleaseVersionTag, response.Release?.Version);
return response;
}
Expand Down
1 change: 1 addition & 0 deletions backend/LexBoxApi/LexBoxApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
<UserSecretsId>7392cddf-9b3b-441c-9316-203bb5c4a6bc</UserSecretsId>
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
<LangVersion>preview</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 4 additions & 0 deletions backend/LexBoxApi/LexBoxKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public static void AddLexBoxApi(this IServiceCollection services,
.BindConfiguration("HealthChecks")
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<FwLiteReleaseConfig>()
.BindConfiguration("FwLiteRelease")
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddHttpClient();
services.AddServiceDiscovery();
services.AddHttpClient<FwHeadlessClient>(client => client.BaseAddress = new ("http://fwHeadless"))
Expand Down
34 changes: 21 additions & 13 deletions backend/LexBoxApi/Services/FwLiteReleases/FwLiteReleaseService.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
using System.Diagnostics;
using LexBoxApi.Controllers;
using LexBoxApi.Config;
using LexBoxApi.Otel;
using LexCore.Entities;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options;

namespace LexBoxApi.Services.FwLiteReleases;

public class FwLiteReleaseService(IHttpClientFactory factory, HybridCache cache)
public class FwLiteReleaseService(IHttpClientFactory factory, HybridCache cache, IOptions<FwLiteReleaseConfig> config)
{
private const string GithubLatestRelease = "GithubLatestRelease";
public const string FwLiteClientVersionTag = "app.fw-lite.client.version";
public const string FwLitePlatformTag = "app.fw-lite.platform";
public const string FwLiteReleaseVersionTag = "app.fw-lite.release.version";

public async ValueTask<FwLiteRelease?> GetLatestRelease(CancellationToken token = default)
public async ValueTask<FwLiteRelease?> GetLatestRelease(FwLitePlatform platform, CancellationToken token = default)
{
return await cache.GetOrCreateAsync(GithubLatestRelease,
return await cache.GetOrCreateAsync($"{GithubLatestRelease}|{platform}",
platform,
FetchLatestReleaseFromGithub,
new HybridCacheEntryOptions() { Expiration = TimeSpan.FromDays(1) },
cancellationToken: token);
cancellationToken: token, tags: [GithubLatestRelease]);
}

public async ValueTask<ShouldUpdateResponse> ShouldUpdate(string appVersion)
public async ValueTask<ShouldUpdateResponse> ShouldUpdate(FwLitePlatform platform, string appVersion)
{
var latestRelease = await GetLatestRelease();
var latestRelease = await GetLatestRelease(platform);
if (latestRelease is null) return new ShouldUpdateResponse(null);

var shouldUpdateToRelease = ShouldUpdateToRelease(appVersion, latestRelease.Version);
Expand All @@ -36,12 +39,18 @@ public static bool ShouldUpdateToRelease(string appVersion, string latestVersion

public async ValueTask InvalidateReleaseCache()
{
await cache.RemoveAsync(GithubLatestRelease);
await cache.RemoveByTagAsync(GithubLatestRelease);
}

private async ValueTask<FwLiteRelease?> FetchLatestReleaseFromGithub(CancellationToken token)
private async ValueTask<FwLiteRelease?> FetchLatestReleaseFromGithub(FwLitePlatform platform, CancellationToken token)
{
var platformConfig = config.Value.Platforms.GetValueOrDefault(platform);
if (platformConfig is null)
{
throw new ArgumentException($"No config for platform {platform}");
}
using var activity = LexBoxActivitySource.Get().StartActivity();
activity?.AddTag(FwLitePlatformTag, platform.ToString());
var response = await factory.CreateClient("Github")
.SendAsync(new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/sillsdev/languageforge-lexbox/releases")
Expand Down Expand Up @@ -74,12 +83,11 @@ public async ValueTask InvalidateReleaseCache()
continue;
}

var msixBundle = release.Assets.FirstOrDefault(a =>
a.Name.EndsWith(".msixbundle", StringComparison.InvariantCultureIgnoreCase));
if (msixBundle is not null)
var releaseAsset = release.Assets.FirstOrDefault(a => platformConfig.FileName.IsMatch(a.Name));
if (releaseAsset is not null)
{
activity?.AddTag(FwLiteReleaseVersionTag, release.TagName);
return new FwLiteRelease(release.TagName, msixBundle.BrowserDownloadUrl);
return new FwLiteRelease(release.TagName, releaseAsset.BrowserDownloadUrl);
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions backend/LexBoxApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,17 @@
"fwHeadless": {
"http": ["fw-headless"]
}
},
"FwLiteRelease": {
"Platforms": {
"Windows": {
// ends with .msixbundle regex
"FileNameRegex": "(?i)\\.msixbundle$"
},
"Linux": {
// ends with linux.zip regex
"FileNameRegex": "(?i)linux\\.zip$"
}
}
}
}
12 changes: 12 additions & 0 deletions backend/LexCore/Entities/FwLiteRelease.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace LexCore.Entities;

public record FwLiteRelease(string Version, string Url);

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum FwLitePlatform
{
Windows,
Linux,
Android,
// ReSharper disable once InconsistentNaming
iOS,
Mac
}

public record ShouldUpdateResponse(FwLiteRelease? Release)
{
[MemberNotNullWhen(true, nameof(Release))]
Expand Down
30 changes: 21 additions & 9 deletions backend/Testing/LexCore/Services/FwLiteReleaseServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using FluentAssertions;
using LexBoxApi;
using LexBoxApi.Config;
using LexBoxApi.Services.FwLiteReleases;
using LexCore.Entities;
using Microsoft.Extensions.DependencyInjection;
using Testing.Fixtures;

Expand All @@ -14,17 +16,27 @@ public FwLiteReleaseServiceTests()
{
//disable warning about hybrid cache being experimental
#pragma warning disable EXTEXP0018
var services = new ServiceCollection().AddSingleton<FwLiteReleaseService>().AddHttpClient()
.AddHybridCache()
.Services.BuildServiceProvider();
var services = new ServiceCollection()
.AddSingleton<FwLiteReleaseService>()
.AddHttpClient()
.AddOptions<FwLiteReleaseConfig>().Configure(config =>
{
config.Platforms.Add(FwLitePlatform.Windows, new FwLitePlatformConfig() { FileNameRegex = "(?i)\\.msixbundle$" });
config.Platforms.Add(FwLitePlatform.Linux, new FwLitePlatformConfig() { FileNameRegex = "(?i)linux\\.zip$" });
})
.Services
.AddHybridCache()
.Services.BuildServiceProvider();
#pragma warning restore EXTEXP0018
_fwLiteReleaseService = services.GetRequiredService<FwLiteReleaseService>();
}

[Fact]
public async Task CanGetLatestRelease()
[Theory]
[InlineData(FwLitePlatform.Windows)]
[InlineData(FwLitePlatform.Linux)]
public async Task CanGetLatestRelease(FwLitePlatform platform)
{
var latestRelease = await _fwLiteReleaseService.GetLatestRelease(default);
var latestRelease = await _fwLiteReleaseService.GetLatestRelease(platform);
latestRelease.Should().NotBeNull();
latestRelease.Version.Should().NotBeNullOrEmpty();
latestRelease.Url.Should().NotBeNullOrEmpty();
Expand All @@ -34,7 +46,7 @@ public async Task CanGetLatestRelease()
[InlineData("v2024-11-20-d04e9b96")]
public async Task IsConsideredAnOldVersion(string appVersion)
{
var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(appVersion);
var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLitePlatform.Windows, appVersion);
shouldUpdate.Should().NotBeNull();
shouldUpdate.Release.Should().NotBeNull();
shouldUpdate.Update.Should().BeTrue();
Expand All @@ -43,9 +55,9 @@ public async Task IsConsideredAnOldVersion(string appVersion)
[Fact]
public async Task ShouldUpdateWithLatestVersionShouldReturnFalse()
{
var latestRelease = await _fwLiteReleaseService.GetLatestRelease(default);
var latestRelease = await _fwLiteReleaseService.GetLatestRelease(FwLitePlatform.Windows);
latestRelease.Should().NotBeNull();
var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(latestRelease.Version);
var shouldUpdate = await _fwLiteReleaseService.ShouldUpdate(FwLitePlatform.Windows, latestRelease.Version);
shouldUpdate.Should().NotBeNull();
shouldUpdate.Release.Should().BeNull();
shouldUpdate.Update.Should().BeFalse();
Expand Down

0 comments on commit 20f623a

Please sign in to comment.