Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Areas/Api/Controllers/VersionController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Wayfarer.Services;

namespace Wayfarer.Areas.Api.Controllers;

/// <summary>
/// Exposes the compiled Wayfarer application version.
/// </summary>
[ApiController]
[Area("Api")]
[Route("api/version")]
public sealed class VersionController : ControllerBase
{
private readonly IAppVersionProvider _appVersionProvider;

/// <summary>
/// Creates the version API endpoint.
/// </summary>
/// <param name="appVersionProvider">The provider for the compiled application version.</param>
public VersionController(IAppVersionProvider appVersionProvider)
{
_appVersionProvider = appVersionProvider;
}

/// <summary>
/// Gets the compiled Wayfarer application version.
/// </summary>
[HttpGet]
[AllowAnonymous]
public IActionResult Get()
{
return Ok(new { version = _appVersionProvider.Version });
}
}
37 changes: 37 additions & 0 deletions CommandLine/AppVersionCli.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Wayfarer.Services;

namespace Wayfarer.CommandLine;

/// <summary>
/// Handles app-version CLI commands that do not require web host startup.
/// </summary>
public static class AppVersionCli
{
/// <summary>
/// Handles the version command when present.
/// </summary>
/// <param name="args">The process arguments.</param>
/// <param name="appVersionProvider">The provider for the compiled application version.</param>
/// <param name="output">The writer used for command output.</param>
/// <param name="error">The writer used for command errors.</param>
/// <param name="exitCode">The command exit code when handled.</param>
/// <returns>True when this handler consumed the command; otherwise false.</returns>
public static bool TryHandle(
string[] args,
IAppVersionProvider appVersionProvider,
TextWriter output,
TextWriter error,
out int exitCode)
{
_ = error;
exitCode = 0;

if (args.Length == 0 || !string.Equals(args[0], "version", StringComparison.OrdinalIgnoreCase))
{
return false;
}

output.WriteLine($"Wayfarer {appVersionProvider.Version}");
return true;
}
}
44 changes: 44 additions & 0 deletions Middleware/AppVersionHeaderMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Wayfarer.Services;

namespace Wayfarer.Middleware;

/// <summary>
/// Adds the compiled Wayfarer version to normal HTTP responses.
/// </summary>
public sealed class AppVersionHeaderMiddleware
{
/// <summary>
/// The response header that carries the Wayfarer version.
/// </summary>
public const string HeaderName = "X-Wayfarer-Version";

private readonly RequestDelegate _next;
private readonly IAppVersionProvider _appVersionProvider;

/// <summary>
/// Creates middleware that appends the Wayfarer version header.
/// </summary>
/// <param name="next">The next middleware in the pipeline.</param>
/// <param name="appVersionProvider">The provider for the compiled application version.</param>
public AppVersionHeaderMiddleware(RequestDelegate next, IAppVersionProvider appVersionProvider)
{
_next = next;
_appVersionProvider = appVersionProvider;
}

/// <summary>
/// Registers the version header before downstream middleware starts the response.
/// </summary>
/// <param name="context">The current HTTP context.</param>
public async Task InvokeAsync(HttpContext context)
{
context.Response.OnStarting(static state =>
{
var (httpContext, version) = ((HttpContext, string))state;
httpContext.Response.Headers[HeaderName] = version;
return Task.CompletedTask;
}, (context, _appVersionProvider.Version));

await _next(context);
}
}
16 changes: 9 additions & 7 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Quartz.Impl;
using Quartz.Spi;
using Serilog;
using Wayfarer.CommandLine;
using Wayfarer.Jobs;
using Wayfarer.Middleware;
using Wayfarer.Models;
Expand All @@ -24,30 +25,29 @@
using Wayfarer.Swagger;
using Wayfarer.Util;
using IPNetwork = System.Net.IPNetwork;
// for AddQuartz(), AddQuartzHostedService()
// for UseMicrosoftDependencyInjectionJobFactory(), UsePersistentStore(), etc.
// for IJobFactory
// for UseNewtonsoftJsonSerializer()

if (AppVersionCli.TryHandle(args, new AppVersionProvider(), Console.Out, Console.Error, out var versionExitCode))
{
Environment.ExitCode = versionExitCode;
return;
}

var builder = WebApplication.CreateBuilder(args);

#region CLI Command Handling

// Handling the "reset-password" command from the CLI
if (args.Length > 0 && args[0] == "reset-password") { await HandlePasswordResetCommand(args); return; }

#endregion CLI Command Handling

#region Configuration Setup

// Configuring the application settings, such as JSON configuration files
ConfigureConfiguration(builder);

#endregion Configuration Setup

#region Serilog Logging Setup

// Setting up logging, including Serilog for file, console, and PostgreSQL logging
ConfigureLogging(builder);

#endregion Serilog Logging Setup
Expand Down Expand Up @@ -465,6 +465,7 @@ static void ConfigureServices(WebApplicationBuilder builder)
// Explicitly register IHttpContextAccessor for services that need it (e.g., TileCacheService).
// Some framework components may register it implicitly, but explicit registration is safer.
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IAppVersionProvider, AppVersionProvider>();

// Register memory cache for application services
builder.Services.AddMemoryCache();
Expand Down Expand Up @@ -700,6 +701,7 @@ static async Task ConfigureMiddleware(WebApplication app)

// CRITICAL: Add this as the FIRST middleware to process forwarded headers from nginx
app.UseForwardedHeaders();
app.UseMiddleware<AppVersionHeaderMiddleware>();

app.UseMiddleware<RequestIdLoggingMiddleware>(); // Enriches Serilog LogContext with HttpContext.TraceIdentifier
app.UseMiddleware<PerformanceMonitoringMiddleware>(); // Custom middleware for monitoring performance
Expand Down
17 changes: 17 additions & 0 deletions Services/AppVersionDisplay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Wayfarer.Services;

/// <summary>
/// Formats user-visible Wayfarer version text.
/// </summary>
public static class AppVersionDisplay
{
/// <summary>
/// Formats the shared footer version text.
/// </summary>
/// <param name="appVersionProvider">The provider for the compiled application version.</param>
/// <returns>The footer version string.</returns>
public static string FooterText(IAppVersionProvider appVersionProvider)
{
return $"Wayfarer v{appVersionProvider.Version}";
}
}
59 changes: 59 additions & 0 deletions Services/AppVersionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Reflection;

namespace Wayfarer.Services;

/// <summary>
/// Provides the compiled Wayfarer application version.
/// </summary>
public interface IAppVersionProvider
{
/// <summary>
/// Gets the application release version from assembly informational metadata.
/// </summary>
string Version { get; }
}

/// <summary>
/// Reads the Wayfarer application version from assembly informational metadata.
/// </summary>
public sealed class AppVersionProvider : IAppVersionProvider
{
private readonly Assembly _assembly;
private string? _version;

/// <summary>
/// Creates a provider for the Wayfarer application assembly.
/// </summary>
public AppVersionProvider()
: this(typeof(AppVersionProvider).Assembly)
{
}

/// <summary>
/// Creates a provider for the assembly that contains the supplied marker type.
/// </summary>
/// <param name="markerType">A type from the assembly whose metadata should be read.</param>
public AppVersionProvider(Type markerType)
: this(markerType.Assembly)
{
}

/// <summary>
/// Creates a provider for the supplied assembly.
/// </summary>
/// <param name="assembly">The assembly whose informational version should be read.</param>
public AppVersionProvider(Assembly assembly)
{
_assembly = assembly;
}

/// <inheritdoc />
public string Version => _version ??= ReadInformationalVersion(_assembly);

private static string ReadInformationalVersion(Assembly assembly)
{
return assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "0.0.0";
}
}
9 changes: 9 additions & 0 deletions Version.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<WayfarerVersion>1.4.0</WayfarerVersion>
<Version>$(WayfarerVersion)</Version>
<PackageVersion>$(WayfarerVersion)</PackageVersion>
<AssemblyInformationalVersion>$(WayfarerVersion)</AssemblyInformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
4 changes: 3 additions & 1 deletion Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@using Wayfarer.Services
@using Wayfarer.Areas.Public.Controllers
@inject Wayfarer.Parsers.IApplicationSettingsService ApplicationSettingsService
@inject IAppVersionProvider AppVersionProvider
@{
bool embed = ViewBag.IsEmbed as bool? ?? false;
}
Expand Down Expand Up @@ -143,7 +144,8 @@
<a href="https://stef-k.github.io/WayfarerMobile/" target="_blank"
class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover">Mobile</a> -
<a asp-area="" asp-controller="Home" asp-action="Privacy"
class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover">Privacy</a>
class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover">Privacy</a> -
@AppVersionDisplay.FooterText(AppVersionProvider)
</div>
</footer>

Expand Down
2 changes: 2 additions & 0 deletions Wayfarer.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<Import Project="Version.props" />

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
Expand Down
32 changes: 32 additions & 0 deletions docs/23-Versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Versioning

`Version.props` is the runtime and release version source for this slice. The root
file contains the manually edited `WayfarerVersion` value and maps the standard
MSBuild metadata directly from it:

```xml
<WayfarerVersion>1.4.0</WayfarerVersion>
<Version>$(WayfarerVersion)</Version>
<PackageVersion>$(WayfarerVersion)</PackageVersion>
<AssemblyInformationalVersion>$(WayfarerVersion)</AssemblyInformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
```

The running app reads `AssemblyInformationalVersion` from the compiled Wayfarer
assembly through `IAppVersionProvider`. Runtime surfaces such as
`dotnet run --no-launch-profile -- version`, `GET /api/version`,
`X-Wayfarer-Version`, and the shared layout footer use that provider instead of
separate constants.

Use `dotnet run --no-launch-profile -- version` when validating exact CLI
output. The app writes exactly `Wayfarer 1.4.0`; `--no-launch-profile` avoids
.NET SDK launch-profile messages so validation stays focused on app output.

## Manual bump process

To prepare a later release version, edit only the root `Version.props` file and
change `WayfarerVersion` to the target release value. Rebuild the app so the new
value is compiled into assembly metadata.

Release helper automation, changelog checks, tag validation, and GitHub release
validation are deferred to issue #324.
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@
- [Deployment](20-Deployment.md)
- [Security](21-Security.md)
- [Testing](22-Testing.md)
- [Versioning](23-Versioning.md)

55 changes: 55 additions & 0 deletions tests/Wayfarer.Tests/Versioning/AppVersionCliTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using FluentAssertions;
using Wayfarer.CommandLine;
using Wayfarer.Services;
using Xunit;

namespace Wayfarer.Tests.Versioning;

public class AppVersionCliTests
{
[Fact]
public void TryHandle_VersionCommand_WritesExactVersionLine()
{
using var output = new StringWriter();
using var error = new StringWriter();

var handled = AppVersionCli.TryHandle(
new[] { "version" },
new StubAppVersionProvider("1.4.0"),
output,
error,
out var exitCode);

handled.Should().BeTrue();
exitCode.Should().Be(0);
output.ToString().Should().Be($"Wayfarer 1.4.0{Environment.NewLine}");
error.ToString().Should().BeEmpty();
}

[Fact]
public void TryHandle_VersionCommand_DoesNotRequireWebHostOrDatabaseStartup()
{
using var output = new StringWriter();
using var error = new StringWriter();

var handled = AppVersionCli.TryHandle(
new[] { "version" },
new StubAppVersionProvider("1.4.0"),
output,
error,
out var exitCode);

handled.Should().BeTrue();
exitCode.Should().Be(0);
}

private sealed class StubAppVersionProvider : IAppVersionProvider
{
public StubAppVersionProvider(string version)
{
Version = version;
}

public string Version { get; }
}
}
Loading
Loading