Compare commits

..

1 Commits

Author SHA1 Message Date
cesnimda d03ca01172 feat!: migrate to .NET 10 LTS (RECOMMENDATIONS #1)
CI / backend (pull_request) Successful in 52s
CI / frontend (pull_request) Successful in 23s
CI / format (pull_request) Successful in 51s
CI / db-tests (pull_request) Successful in 52s
Security / secrets (pull_request) Successful in 4s
Security / dependencies (pull_request) Successful in 1m1s
.NET 8 security support ends 2026-11-10; .NET 10 is LTS to Nov 2028.
- TargetFramework net8.0 -> net10.0 (Directory.Build.props)
- EF Core 8.0.4 -> 10.0.9; Npgsql provider 8.0.4 -> 10.0.2; Pgvector.EFCore
  0.2.0 -> 0.3.0; ASP.NET/Extensions packages -> 10.0.9
- Dockerfile sdk/aspnet 8.0 -> 10.0; CI setup-dotnet -> 10.0.x
- Modernised deprecated APIs: X509CertificateLoader (SYSLIB0057),
  KnownIPNetworks + System.Net.IPNetwork.Parse (ASPDEPR005)
- Adapted tests to Npgsql 10's eager connection-string validation (dummy conn
  string in test factories; behaviour change from lazy 8.x)

Verified: 0 errors/0 warnings; all 54 tests green on net10; the 3 LiveDb tests
green against a real pgvector container on the new EF10/Npgsql10/Pgvector 0.3
stack; dotnet format clean; vulnerable-package scan clean; the .NET 10 API
Docker image builds successfully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:29:21 +02:00
14 changed files with 40 additions and 97 deletions
+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '10.0.x'
- name: Restore - name: Restore
run: dotnet restore InboxIntel.sln run: dotnet restore InboxIntel.sln
- name: Build - name: Build
@@ -45,7 +45,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '10.0.x'
- name: dotnet format (verify only) - name: dotnet format (verify only)
run: dotnet format InboxIntel.sln --verify-no-changes run: dotnet format InboxIntel.sln --verify-no-changes
@@ -67,7 +67,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '10.0.x'
- name: Wait for Postgres - name: Wait for Postgres
run: | run: |
for i in $(seq 1 30); do for i in $(seq 1 30); do
-35
View File
@@ -1,35 +0,0 @@
name: Renovate
# RECOMMENDATIONS #2: automated dependency-update PRs (NuGet, npm, Dockerfiles, Actions)
# that ride the existing required CI gates. Runs weekly + on demand.
#
# ONE-TIME SETUP (manual): create a Gitea personal access token with scopes
# repo (rw) + user (r) + issue (rw) + organization (r), and add it as the Actions
# secret RENOVATE_TOKEN (repo Settings -> Actions -> Secrets). Without the secret this
# workflow fails fast with a clear message. See https://docs.renovatebot.com/modules/platform/gitea/
on:
schedule:
- cron: '30 4 * * 1' # Mondays 04:30 UTC
workflow_dispatch: {}
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Require RENOVATE_TOKEN
run: |
if [ -z "${{ secrets.RENOVATE_TOKEN }}" ]; then
echo "RENOVATE_TOKEN secret is not set — see the comment at the top of this workflow." >&2
exit 1
fi
- name: Run Renovate
uses: https://github.com/renovatebot/github-action@v40.3.6
with:
token: ${{ secrets.RENOVATE_TOKEN }}
env:
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: https://git.cesnimda.uk/api/v1
RENOVATE_REPOSITORIES: cesnimda/Inboxintel
RENOVATE_ONBOARDING: "false"
RENOVATE_REQUIRE_CONFIG: optional
LOG_LEVEL: info
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '10.0.x'
- name: Restore - name: Restore
run: dotnet restore InboxIntel.sln run: dotnet restore InboxIntel.sln
- name: .NET vulnerable packages (fail on any) - name: .NET vulnerable packages (fail on any)
+1 -1
View File
@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
-27
View File
@@ -1,27 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"timezone": "Europe/Berlin",
"schedule": ["before 6am on monday"],
"labels": ["dependencies"],
"prConcurrentLimit": 5,
"commitMessagePrefix": "chore(deps):",
"packageRules": [
{
"description": "Group safe minor+patch updates into one weekly PR per ecosystem",
"matchUpdateTypes": ["minor", "patch"],
"groupName": "{{manager}} minor & patch"
},
{
"description": "Major updates stay individual PRs for careful review",
"matchUpdateTypes": ["major"],
"dependencyDashboardApproval": true
}
],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"schedule": ["at any time"]
},
"ignorePaths": ["**/node_modules/**", "**/bin/**", "**/obj/**"]
}
+2 -2
View File
@@ -1,5 +1,5 @@
# Multi-stage build for the ASP.NET Core API. # Multi-stage build for the ASP.NET Core API.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src WORKDIR /src
# Copy solution + project files first for layer-cached restore. # Copy solution + project files first for layer-cached restore.
@@ -13,7 +13,7 @@ RUN dotnet restore src/InboxIntel.Api/InboxIntel.Api.csproj
COPY src/ src/ COPY src/ src/
RUN dotnet publish src/InboxIntel.Api/InboxIntel.Api.csproj -c Release -o /app/publish /p:UseAppHost=false RUN dotnet publish src/InboxIntel.Api/InboxIntel.Api.csproj -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app WORKDIR /app
COPY --from=build /app/publish . COPY --from=build /app/publish .
+3 -3
View File
@@ -5,10 +5,10 @@
<UserSecretsId>210c6d96-c7e4-4ee9-8982-8b91424979b8</UserSecretsId> <UserSecretsId>210c6d96-c7e4-4ee9-8982-8b91424979b8</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
<!-- Required on the startup project for `dotnet ef migrations` to work. --> <!-- Required on the startup project for `dotnet ef migrations` to work. -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
+5 -6
View File
@@ -38,8 +38,8 @@ var dp = builder.Services.AddDataProtection()
var dpCertPath = builder.Configuration["DataProtection:CertificatePath"]; var dpCertPath = builder.Configuration["DataProtection:CertificatePath"];
if (!string.IsNullOrWhiteSpace(dpCertPath)) if (!string.IsNullOrWhiteSpace(dpCertPath))
{ {
dp.ProtectKeysWithCertificate(new System.Security.Cryptography.X509Certificates.X509Certificate2( dp.ProtectKeysWithCertificate(System.Security.Cryptography.X509Certificates.X509CertificateLoader
dpCertPath, builder.Configuration["DataProtection:CertificatePassword"])); .LoadPkcs12FromFile(dpCertPath, builder.Configuration["DataProtection:CertificatePassword"]));
} }
builder.Services.AddApplication(); builder.Services.AddApplication();
@@ -201,15 +201,14 @@ var forwardedOptions = new ForwardedHeadersOptions
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
ForwardLimit = app.Configuration.GetValue<int?>("ForwardedHeaders:ForwardLimit") ?? 1 ForwardLimit = app.Configuration.GetValue<int?>("ForwardedHeaders:ForwardLimit") ?? 1
}; };
forwardedOptions.KnownNetworks.Clear(); forwardedOptions.KnownIPNetworks.Clear();
forwardedOptions.KnownProxies.Clear(); forwardedOptions.KnownProxies.Clear();
var trustedNetworks = app.Configuration.GetSection("ForwardedHeaders:KnownNetworks").Get<string[]>() var trustedNetworks = app.Configuration.GetSection("ForwardedHeaders:KnownNetworks").Get<string[]>()
?? new[] { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "::1/128" }; ?? new[] { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "::1/128" };
foreach (var cidr in trustedNetworks) foreach (var cidr in trustedNetworks)
{ {
var parts = cidr.Split('/'); if (System.Net.IPNetwork.TryParse(cidr, out var network))
if (parts.Length == 2 && System.Net.IPAddress.TryParse(parts[0], out var prefix) && int.TryParse(parts[1], out var len)) forwardedOptions.KnownIPNetworks.Add(network);
forwardedOptions.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(prefix, len));
} }
app.UseForwardedHeaders(forwardedOptions); app.UseForwardedHeaders(forwardedOptions);
@@ -6,13 +6,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.2" /> <PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<!-- DbSet<> is exposed on IAppDbContext so the Application layer can query. <!-- DbSet<> is exposed on IAppDbContext so the Application layer can query.
Pinned to 8.0.4 to match the Npgsql provider's Relational dependency. --> Pinned to 8.0.4 to match the Npgsql provider's Relational dependency. -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<!-- Transitive security pins: patch known .NET 8.0.0 advisories pulled in by EF Core. --> <!-- Transitive security pins: patch known .NET 8.0.0 advisories pulled in by EF Core. -->
<PackageReference Include="System.Text.Json" Version="8.0.6" /> <PackageReference Include="System.Text.Json" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\InboxIntel.Domain\InboxIntel.Domain.csproj" /> <ProjectReference Include="..\InboxIntel.Domain\InboxIntel.Domain.csproj" />
@@ -6,7 +6,7 @@
<ItemGroup> <ItemGroup>
<!-- NpgsqlTypes.NpgsqlTsVector (FTS) and Pgvector.Vector (semantic search) are used as <!-- NpgsqlTypes.NpgsqlTsVector (FTS) and Pgvector.Vector (semantic search) are used as
column types on the Email entity — same pragmatic precedent for both. --> column types on the Email entity — same pragmatic precedent for both. -->
<PackageReference Include="Npgsql" Version="8.0.3" /> <PackageReference Include="Npgsql" Version="10.0.2" />
<PackageReference Include="Pgvector" Version="0.2.0" /> <PackageReference Include="Pgvector" Version="0.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -4,18 +4,18 @@
<AssemblyName>InboxIntel.Infrastructure</AssemblyName> <AssemblyName>InboxIntel.Infrastructure</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" /> <PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageReference Include="Google.Apis.Auth" Version="1.68.0" /> <PackageReference Include="Google.Apis.Auth" Version="1.68.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.0" /> <PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
<PackageReference Include="Polly" Version="8.4.1" /> <PackageReference Include="Polly" Version="8.4.1" />
<PackageReference Include="QuestPDF" Version="2024.7.0" /> <PackageReference Include="QuestPDF" Version="2024.7.0" />
<PackageReference Include="CsvHelper" Version="33.0.1" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
@@ -24,9 +24,9 @@
<!-- Transitive security pins: patch known .NET 8.0.0 advisories pulled in by <!-- Transitive security pins: patch known .NET 8.0.0 advisories pulled in by
EF Core / ASP.NET / DataProtection. Remove once the parent packages ship EF Core / ASP.NET / DataProtection. Remove once the parent packages ship
these versions transitively. --> these versions transitively. -->
<PackageReference Include="System.Text.Json" Version="8.0.6" /> <PackageReference Include="System.Text.Json" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="8.0.3" /> <PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\InboxIntel.Application\InboxIntel.Application.csproj" /> <ProjectReference Include="..\InboxIntel.Application\InboxIntel.Application.csproj" />
@@ -25,7 +25,7 @@ namespace InboxIntel.IntegrationTests;
/// endpoints (model validation, per-user rate limits) without a real Google login.</summary> /// endpoints (model validation, per-user rate limits) without a real Google login.</summary>
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions> public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{ {
public const string Scheme = "Test"; public new const string Scheme = "Test";
// Stable across requests so per-user rate-limit partitions accumulate correctly. // Stable across requests so per-user rate-limit partitions accumulate correctly.
public static readonly string Uid = Guid.NewGuid().ToString(); public static readonly string Uid = Guid.NewGuid().ToString();
@@ -52,6 +52,9 @@ public class AuditTestAppFactory : WebApplicationFactory<Program>
builder.ConfigureHostConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary<string, string?> builder.ConfigureHostConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary<string, string?>
{ {
["Database:AutoMigrate"] = "false", ["Database:AutoMigrate"] = "false",
// Npgsql 10 eagerly validates the connection string when the DbContext is
// resolved (8.x was lazy); these tests never connect, but the string must parse.
["ConnectionStrings:Postgres"] = "Host=localhost;Database=test;Username=test;Password=test",
["GoogleOAuth:ClientId"] = "test-client-id", ["GoogleOAuth:ClientId"] = "test-client-id",
["GoogleOAuth:ClientSecret"] = "test-client-secret", ["GoogleOAuth:ClientSecret"] = "test-client-secret",
// H-2: make the auth policy trip on the 3rd request within the window. // H-2: make the auth policy trip on the 3rd request within the window.
@@ -19,6 +19,9 @@ public class TestAppFactory : WebApplicationFactory<Program>
builder.ConfigureHostConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary<string, string?> builder.ConfigureHostConfiguration(cfg => cfg.AddInMemoryCollection(new Dictionary<string, string?>
{ {
["Database:AutoMigrate"] = "false", ["Database:AutoMigrate"] = "false",
// Npgsql 10 eagerly validates the connection string when the DbContext is
// resolved (8.x was lazy); these tests never connect, but the string must parse.
["ConnectionStrings:Postgres"] = "Host=localhost;Database=test;Username=test;Password=test",
// Dummy OAuth creds so the Google challenge produces a real 302 redirect // Dummy OAuth creds so the Google challenge produces a real 302 redirect
// (an empty ClientId can make the handler throw instead of redirecting). // (an empty ClientId can make the handler throw instead of redirecting).
["GoogleOAuth:ClientId"] = "test-client-id", ["GoogleOAuth:ClientId"] = "test-client-id",
@@ -7,8 +7,8 @@
<PackageReference Include="xunit" Version="2.9.0" /> <PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\InboxIntel.Api\InboxIntel.Api.csproj" /> <ProjectReference Include="..\..\src\InboxIntel.Api\InboxIntel.Api.csproj" />