Skip to content

Commit

Permalink
Merge pull request #1477 from qdraw/feature/202403_in_app_update_mess…
Browse files Browse the repository at this point in the history
…age_by_version

add fallback for github && tests
  • Loading branch information
qdraw authored Mar 14, 2024
2 parents e0c006a + 91b89cc commit b99c333
Show file tree
Hide file tree
Showing 25 changed files with 9,502 additions and 238 deletions.
68 changes: 68 additions & 0 deletions documentation/static/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5110,6 +5110,74 @@
"parameters": [],
"extensions": {}
},
"/api/health/release-info": {
"operations": {
"Get": {
"tags": [
{
"name": "HealthCheckForUpdates",
"extensions": {},
"unresolvedReference": false
}
],
"summary": "Get more info to show about the release",
"parameters": [
{
"unresolvedReference": false,
"name": "v",
"in": 0,
"required": false,
"deprecated": false,
"allowEmptyValue": false,
"explode": false,
"allowReserved": false,
"schema": {
"type": "string",
"default": {
"primitiveType": 4,
"anyType": 0,
"value": ""
},
"readOnly": false,
"writeOnly": false,
"allOf": [],
"oneOf": [],
"anyOf": [],
"required": [],
"properties": {},
"additionalPropertiesAllowed": true,
"enum": [],
"nullable": false,
"deprecated": false,
"extensions": {},
"unresolvedReference": false
},
"examples": {},
"content": {},
"extensions": {}
}
],
"responses": {
"200": {
"description": "result",
"headers": {},
"content": {},
"links": {},
"extensions": {},
"unresolvedReference": false
}
},
"callbacks": {},
"deprecated": false,
"security": [],
"servers": [],
"extensions": {}
}
},
"servers": [],
"parameters": [],
"extensions": {}
},
"/search": {
"operations": {
"Post": {
Expand Down
5 changes: 3 additions & 2 deletions history.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ Semantic Versioning 2.0.0 is from version 0.1.6+
## List of versions

## version 0.6.0-beta.4 - _(Unreleased)_ - 2024-03-? {#v0.6.0-beta.4}
- [x] (Changed) Back-end Upgrade to .NET 8 - SDK 8.0.202 (Runtime: 8.0.3) (PR #1464)

- TODO: update fallback / custom update messages
- [x] (Changed) Back-end Upgrade to .NET 8 - SDK 8.0.202 (Runtime: 8.0.3) (PR #1464)
- [x] (Fixed) _Back-end_ Latest version check from 0.5.10+ is not working due order check (PR #1477)
- [x] (Fixed) _Back-end_ Update fallback and custom update messages (PR #1477)

## version 0.6.0-beta.3 - 2024-03-11 {#v0.6.0-beta.3}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using starsky.feature.health.UpdateCheck.Models;

namespace starsky.feature.health.UpdateCheck.Interfaces
{
public interface ICheckForUpdates
{
Task<KeyValuePair<UpdateStatus, string>> IsUpdateNeeded(string currentVersion = "");
Task<(UpdateStatus, string?)> IsUpdateNeeded(string currentVersion = "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace starsky.feature.health.UpdateCheck.Interfaces;

public interface ISpecificVersionReleaseInfo
{
Task<string> SpecificVersionMessage(string? versionToCheckFor);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
Expand All @@ -15,48 +14,57 @@
using starsky.foundation.platform.VersionHelpers;

[assembly: InternalsVisibleTo("starskytest")]

namespace starsky.feature.health.UpdateCheck.Services
{
[Service(typeof(ICheckForUpdates), InjectionLifetime = InjectionLifetime.Singleton)]
public class CheckForUpdates : ICheckForUpdates
{
internal const string GithubStarskyReleaseApi = "https://api.github.com/repos/qdraw/starsky/releases";
internal const string GithubStarskyReleaseApi =
"https://api.github.com/repos/qdraw/starsky/releases";

internal const string GithubStarskyReleaseMirrorApi =
"https://qdraw.nl/special/starsky/releases";

internal const string QueryCheckForUpdatesCacheName = "CheckForUpdates";

private readonly AppSettings? _appSettings;
private readonly IMemoryCache? _cache;
private readonly IHttpClientHelper _httpClientHelper;

public CheckForUpdates(IHttpClientHelper httpClientHelper, AppSettings? appSettings, IMemoryCache? cache)
public CheckForUpdates(IHttpClientHelper httpClientHelper, AppSettings? appSettings,
IMemoryCache? cache)
{
_httpClientHelper = httpClientHelper;
_appSettings = appSettings;
_cache = cache;
}

internal const string QueryCheckForUpdatesCacheName = "CheckForUpdates";

/// <summary>
///
/// </summary>
/// <param name="currentVersion">defaults to _appSettings</param>
/// <returns></returns>
[SuppressMessage("Usage", "S2589:cache & appSettings null")]
public async Task<KeyValuePair<UpdateStatus, string>> IsUpdateNeeded(string currentVersion = "")
public async Task<(UpdateStatus, string?)> IsUpdateNeeded(
string currentVersion = "")
{
if ( _appSettings == null || _appSettings.CheckForUpdates == false )
return new KeyValuePair<UpdateStatus, string>(UpdateStatus.Disabled, "");
{
return ( UpdateStatus.Disabled, string.Empty );
}

currentVersion = string.IsNullOrWhiteSpace(currentVersion)
? _appSettings.AppVersion : currentVersion;
? _appSettings.AppVersion
: currentVersion;

// The CLI programs uses no cache
if ( _cache == null || _appSettings?.AddMemoryCache != true )
if ( _cache == null || _appSettings.AddMemoryCache != true )
{
return Parse(await QueryIsUpdateNeededAsync(), currentVersion);
}

if ( _cache.TryGetValue(QueryCheckForUpdatesCacheName,
out var cacheResult) && cacheResult != null )
out var cacheResult) && cacheResult != null )
{
return Parse(( List<ReleaseModel> )cacheResult, currentVersion);
}
Expand All @@ -69,42 +77,59 @@ public async Task<KeyValuePair<UpdateStatus, string>> IsUpdateNeeded(string curr
return Parse(( List<ReleaseModel>? )cacheResult, currentVersion);
}


internal async Task<List<ReleaseModel>?> QueryIsUpdateNeededAsync()
{
// argument check is done in QueryIsUpdateNeeded
var (key, value) = await _httpClientHelper.ReadString(GithubStarskyReleaseApi);
return !key ? new List<ReleaseModel>() :
JsonSerializer.Deserialize<List<ReleaseModel>>(value, DefaultJsonSerializer.CamelCase);
if ( !key )
{
( key, value ) = await _httpClientHelper.ReadString(GithubStarskyReleaseMirrorApi);
}

return !key
? new List<ReleaseModel>()
: JsonSerializer.Deserialize<List<ReleaseModel>>(value,
DefaultJsonSerializer.CamelCase);
}

internal static KeyValuePair<UpdateStatus, string> Parse(IEnumerable<ReleaseModel>? releaseModelList,
/// <summary>
/// Parse the result from the API
/// </summary>
/// <param name="releaseModelList">inputModel</param>
/// <param name="currentVersion">The current Version</param>
/// <returns>Status and LatestVersion</returns>
internal static (UpdateStatus, string) Parse(IEnumerable<ReleaseModel>? releaseModelList,
string currentVersion)
{
var orderedReleaseModelList =
releaseModelList?.OrderByDescending(p => p.TagName);
// remove v at start
releaseModelList?.OrderByDescending(p => SemVersion.Parse(p.TagName, false));

var tagName = orderedReleaseModelList?
.FirstOrDefault(p => p is { Draft: false, PreRelease: false })?.TagName;

if ( string.IsNullOrWhiteSpace(tagName) ||
!tagName.StartsWith('v') )
!tagName.StartsWith('v') )
{
return new KeyValuePair<UpdateStatus, string>(UpdateStatus.NoReleasesFound, string.Empty);
return ( UpdateStatus.NoReleasesFound, string.Empty );
}

try
{
var latestVersion = SemVersion.Parse(tagName.Remove(0, 1));
// remove v at start
var latestVersion = SemVersion.Parse(tagName);
var currentVersionObject = SemVersion.Parse(currentVersion);
var isNewer = latestVersion > currentVersionObject;
var status = isNewer ? UpdateStatus.NeedToUpdate : UpdateStatus.CurrentVersionIsLatest;
return new KeyValuePair<UpdateStatus, string>(status, latestVersion.ToString());
var status = isNewer
? UpdateStatus.NeedToUpdate
: UpdateStatus.CurrentVersionIsLatest;
return ( status, latestVersion.ToString() );
}
catch ( ArgumentException )
{
return new KeyValuePair<UpdateStatus, string>(UpdateStatus.InputNotValid, string.Empty);
return ( UpdateStatus.InputNotValid, string.Empty );
}

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using starsky.feature.health.UpdateCheck.Interfaces;
using starsky.foundation.http.Interfaces;
using starsky.foundation.injection;
using starsky.foundation.platform.Interfaces;
using starsky.foundation.platform.Models;

namespace starsky.feature.health.UpdateCheck.Services;

[Service(typeof(ISpecificVersionReleaseInfo), InjectionLifetime = InjectionLifetime.Singleton)]
public class SpecificVersionReleaseInfo : ISpecificVersionReleaseInfo
{
internal const string GetSpecificVersionReleaseInfoCacheName = "GetSpecificVersionReleaseInfo";

internal const string SpecificVersionReleaseInfoUrl = "qdraw.nl/special/starsky/releaseinfo";

private readonly IHttpClientHelper _httpClientHelper;
private readonly IMemoryCache? _cache;
private readonly IWebLogger _webLogger;
private readonly AppSettings? _appSettings;

public SpecificVersionReleaseInfo(IHttpClientHelper httpClientHelper, AppSettings? appSettings,
IMemoryCache? cache, IWebLogger webLogger)
{
_appSettings = appSettings;
_httpClientHelper = httpClientHelper;
_cache = cache;
_webLogger = webLogger;
}

public async Task<string> SpecificVersionMessage(string? versionToCheckFor)
{
if ( string.IsNullOrWhiteSpace(versionToCheckFor) )
{
return string.Empty;
}

if ( _cache == null || _appSettings?.AddMemoryCache != true )
{
var specificVersionReleaseInfoContent =
await QuerySpecificVersionInfo();
return Parse(specificVersionReleaseInfoContent, versionToCheckFor);
}

if ( _cache.TryGetValue(GetSpecificVersionReleaseInfoCacheName,
out var cacheResult) && cacheResult != null )
{
return Parse(( string )cacheResult, versionToCheckFor);
}

cacheResult = await QuerySpecificVersionInfo();
_cache.Set(GetSpecificVersionReleaseInfoCacheName, cacheResult,
new TimeSpan(48, 0, 0));

return Parse(( string )cacheResult, versionToCheckFor);
}

internal string Parse(string json, string? versionToCheckFor)
{
if ( string.IsNullOrWhiteSpace(json) )
{
return string.Empty;
}

versionToCheckFor ??= string.Empty;
Dictionary<string, Dictionary<string, string>>? dict = null;

try
{
dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(json);
}
catch ( JsonException e )
{
_webLogger.LogError("[SpecificVersionReleaseInfo] Json parse error: " + e.Message);
}

if ( dict?.TryGetValue(versionToCheckFor, out var valueDict) is not true )
return string.Empty;

var outputValue = valueDict.TryGetValue("en", out var languageValue)
? ConvertMarkdownLinkToHtml(languageValue)
: string.Empty;

return outputValue;
}

internal static string ConvertMarkdownLinkToHtml(string markdown)
{
// Regular expression to match Markdown links
// [Link text Here](https://link-url-here.org)
const string pattern = @"\[(.*?)\]\((.*?)\)";

// Replace Markdown links with HTML anchor tags
return Regex.Replace(markdown, pattern,
"<a target=\"_blank\" ref=\"nofollow\" href=\"$2\">$1</a>", RegexOptions.None,
TimeSpan.FromMilliseconds(100));
}

internal async Task<string> QuerySpecificVersionInfo()
{
// argument check is done in QueryIsUpdateNeeded
var (key, value) = await _httpClientHelper.ReadString(
"https://" + SpecificVersionReleaseInfoUrl);

if ( key ) return value;
_webLogger.LogInformation($"[SpecificVersionReleaseInfo] {value} [end]");
return string.Empty;
}
}
Loading

1 comment on commit b99c333

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.