# ADR: Technology versions

Status: accepted, 2026-10-03. Every NuGet and npm package this repository names, the .NET SDK and runtime, the SQL build tool and Node.js: the version in use, the newest stable version on the day it was read, and the reason when the two differ.

## In plain words

This page lists the libraries, the runtime and the build tools the site is built from, which version each uses, and the newest stable version available when the list was checked. Where the site is behind, the reason is written beside it. A test reads this table against the project files, so the table cannot say one version while the code uses another.

What that is worth: a developer and the organization can see from one page what the site runs on, what was current on 3 October 2026, and why anything was held back. The Latest stable column is only as fresh as the last read.

## How the list was read

Every "latest stable" below was read live on 2026-10-03 from nuget.org, the npm registry, Docker Hub, the Node.js release schedule and Microsoft's .NET release metadata, through the runner on the build machine and never from memory. Preview, beta and release-candidate versions are left out of "latest stable". `TechnologyVersionsTests` holds the "In use" column to the project files: every package reference in `api/` and every dependency in `package.json` has a row, each row's version is the one the project uses, and a row that is behind carries a reason.

## .NET packages

| Package | In use | Latest stable | Why behind |
| --- | --- | --- | --- |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.12 | 10.0.12 | |
| Microsoft.AspNetCore.OpenApi | 10.0.12 | 10.0.12 | |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.12 | 10.0.12 | |
| Microsoft.EntityFrameworkCore.Design | 10.0.12 | 10.0.12 | |
| Microsoft.EntityFrameworkCore.SqlServer | 10.0.12 | 10.0.12 | |
| Microsoft.EntityFrameworkCore.Sqlite | 10.0.12 | 10.0.12 | |
| Microsoft.AspNetCore.Mvc.Testing | 10.0.12 | 10.0.12 | |
| Scalar.AspNetCore | 2.17.13 | 2.17.13 | |
| Microsoft.Azure.Cosmos | 3.63.2 | 3.63.2 | |
| Newtonsoft.Json | 13.0.4 | 13.0.4 | |
| Azure.Identity | 1.17.1 | 1.21.0 | Pinned. 1.21.0 broke the managed-identity token for SQL Server in the container, and PackagePinTests holds the pin (ADR: A second store on Cosmos DB, and what it costs). |
| Azure.Communication.Email | 1.1.0 | 1.1.0 | |
| Azure.Monitor.OpenTelemetry.AspNetCore | 1.6.0 | 1.6.0 | |
| Microsoft.NET.Test.Sdk | 18.10.1 | 18.10.1 | |
| coverlet.collector | 6.0.4 | 10.1.0 | Measured both on the same code: 10.1.0 counts branches differently and reported 66.0% where 6.0.4 reported 67.4%, which would move CI's coverage floor with no change to a test. Run by hand on 3 October, CI read 67.7% of branches on 6.0.4, under its 68% floor; the floor is now 90% of lines and 75% of branches with generated code left out (ADR: Counting what the tests cover, the addendum on generated code). Moving to 10.1.0 is its own change, with the floor set again from a measured run. |
| xunit | 2.9.3 | 2.9.3 | The last v2 release. xUnit v3 is 4.0.1 and changes the test platform (ADR: Staying on .NET 10, and on xUnit v2 for now). |
| xunit.runner.visualstudio | 3.1.5 | 4.0.0 | 4.0.0 is the runner for xUnit v3; 3.1.5 is the newest for v2. |
| NetArchTest.eNhancedEdition | 1.4.5 | 1.4.5 | |
| System.Security.Cryptography.Pkcs | 10.0.13 | 10.0.13 | |

## Build tools and images

| Package | In use | Latest stable | Why behind |
| --- | --- | --- | --- |
| .NET SDK and runtime | 10.0 (10.0.12) | 10.0.12 | 11.0 is a release candidate (ADR: Staying on .NET 10, and on xUnit v2 for now). |
| Microsoft.Build.Sql | 2.3.0 | 2.3.0 | |
| node (Docker build stage and CI) | 24 | 26 (Current); 24 is the newest LTS | The build stays on a long-term support line; 26 becomes LTS on 28 October 2026. |

## Front-end packages

| Package | In use | Latest stable | Why behind |
| --- | --- | --- | --- |
| react | 19.3.0 | 19.3.0 | |
| react-dom | 19.3.0 | 19.3.0 | |
| marked | 18.0.14 | 18.0.14 | |
| highlight.js | 11.12.0 | 11.12.0 | |
| vite | 8.3.2 | 8.3.2 | |
| @vitejs/plugin-react | 6.1.1 | 6.1.1 | |
| typescript | 7.0.2 | 7.0.2 | |
| vitest | 5.0.3 | 5.0.3 | |
| @playwright/test | 1.63.0 | 1.63.0 | |
| @axe-core/playwright | 4.13.0 | 4.13.0 | |
| @types/react | 19.3.0 | 19.3.0 | |
| @types/react-dom | 19.3.0 | 19.3.0 | |
| oxlint | 1.86.0 | 1.86.0 | |
| prettier | 3.9.9 | 3.9.9 | |
| sharp | 0.35.5 | 0.35.5 | |
| concurrently | 10.0.5 | 10.0.5 | |

## C# 14, where it landed

The language moved with .NET 10. Each feature was used only where it removes code or says the intent more plainly:

- **Extension members.** `StandingRules` in `api/TheYard.Domain/StandingRules.cs` gives `Vehicle` a `RaisedTo` method and a `Reserve` property, the one copy of the rule the buyer's bids, the room's bids and the room's price to beat all use; it replaced three hand-written copies. `UtcBuckets` in `api/TheYard.Application/UtcBuckets.cs` gives `DateTimeOffset` the `UtcDay`, `UtcHour` and `UtcMinute` properties and replaced five helper methods spread across four classes.
- **Null-conditional assignment.** `cosmos?.Logger = ...` in `api/TheYard.Api/Composition/Startup.cs` replaced a four-line null check.
- **The `field` keyword.** Not used. No property here wraps a backing field that `field` would remove, and adding one to use the keyword would be the opposite of the point.

The test that holds the table above to the project files, read from this build:

```csharp
/// <summary>One row of the record's tables: the package, the version in use, the newest stable one, and why it is behind.</summary>
private sealed record Row(string Package, string InUse, string Latest, string Why);

private static Dictionary<string, Row> Rows()
{
    string record = File.ReadAllText(Path.Combine(Repo.Root(), "docs", "decisions", "ADR-089-technology-versions.md"));
    var rows = new Dictionary<string, Row>(StringComparer.OrdinalIgnoreCase);
    foreach (Match row in Regex.Matches(record, @"^\| ([^|]+?) \| ([^|]+?) \| ([^|]+?) \| ?([^|]*?) ?\|$", RegexOptions.Multiline))
    {
        string package = row.Groups[1].Value.Trim();
        if (package is "Package" || package.StartsWith("---", StringComparison.Ordinal))
        {
            continue;
        }
        rows[package] = new Row(package, row.Groups[2].Value.Trim(), row.Groups[3].Value.Trim(), row.Groups[4].Value.Trim());
    }
    return rows;
}

[Fact]
public void Every_dotnet_package_reference_has_a_row_with_the_version_in_use()
{
    var rows = Rows();
    Assert.NotEmpty(rows);
    var wrong = new List<string>();
    foreach (string project in Directory.EnumerateFiles(Path.Combine(Repo.Root(), "api"), "*.csproj", SearchOption.AllDirectories))
    {
        foreach (Match reference in Regex.Matches(File.ReadAllText(project), @"<PackageReference Include=""([^""]+)"" Version=""([^""]+)"""))
        {
            string package = reference.Groups[1].Value;
            string version = reference.Groups[2].Value;
            if (!rows.TryGetValue(package, out var row))
            {
                wrong.Add($"{Path.GetFileName(project)} uses {package} {version}, which the record does not list");
            }
            else if (row.InUse != version)
            {
                wrong.Add($"{Path.GetFileName(project)} uses {package} {version} and the record says {row.InUse}");
            }
        }
    }
    Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
}

[Fact]
public void Every_front_end_package_has_a_row_with_the_version_in_use()
{
    var rows = Rows();
    using var manifest = JsonDocument.Parse(File.ReadAllText(Path.Combine(Repo.Root(), "package.json")));
    var wrong = new List<string>();
    foreach (string section in new[] { "dependencies", "devDependencies" })
    {
        foreach (var dependency in manifest.RootElement.GetProperty(section).EnumerateObject())
        {
            string version = dependency.Value.GetString()!.TrimStart('^', '~');
            if (!rows.TryGetValue(dependency.Name, out var row))
            {
                wrong.Add($"package.json uses {dependency.Name} {version}, which the record does not list");
            }
            else if (row.InUse != version)
            {
                wrong.Add($"package.json uses {dependency.Name} {version} and the record says {row.InUse}");
            }
        }
    }
    Assert.True(wrong.Count == 0, string.Join(Environment.NewLine, wrong));
}

[Fact]
public void The_build_tools_in_the_record_are_the_ones_the_build_uses()
{
    var rows = Rows();
    string root = Repo.Root();
    string sqlProject = File.ReadAllText(Path.Combine(root, "api", "TheYard.Database", "TheYard.Database.sqlproj"));
    Assert.Contains($"Microsoft.Build.Sql/{rows["Microsoft.Build.Sql"].InUse}\"", sqlProject, StringComparison.Ordinal);

    string node = rows["node (Docker build stage and CI)"].InUse;
    // The image is Docker's official one read from Google's mirror of Docker Hub, which needs no account and
    // has no per-address data allowance for GitHub's shared runners to use up (ADR-002, the addenda on the mirror).
    Assert.Contains($"FROM mirror.gcr.io/library/node:{node}-alpine", File.ReadAllText(Path.Combine(root, "Dockerfile")), StringComparison.Ordinal);
    Assert.Contains($"FROM mirror.gcr.io/library/node:{node}-alpine", File.ReadAllText(Path.Combine(root, "render", "Dockerfile")), StringComparison.Ordinal);
    string ci = File.ReadAllText(Path.Combine(root, ".github", "workflows", "ci.yml"));
    Assert.DoesNotMatch(new Regex(@"node-version: (?!" + node + @"\b)\d+"), ci);
}

[Fact]
public void Every_row_that_is_behind_says_why()
{
    var unexplained = Rows().Values
        .Where(row => row.InUse != row.Latest && row.Why.Length == 0)
        .Select(row => $"{row.Package} is at {row.InUse} with {row.Latest} out and no reason given")
        .ToList();
    Assert.True(unexplained.Count == 0, string.Join(Environment.NewLine, unexplained));
}
```
*Live from [`api/TheYard.Tests/TechnologyVersionsTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/TechnologyVersionsTests.cs#L15-L108), region `versions`, read from this build at 1d6b50b.*

## Addendum, 14 October 2026: a package named only to raise its version

Two advisories published on 13 October cover System.Security.Cryptography.Pkcs 9.0.11 (GHSA-846f-f764-76mx, high, and GHSA-2fmv-rv2c-59x7, moderate). No project here names it: it arrives under SQL Server's client, Microsoft.Data.SqlClient 6.1.6, which the Entity Framework provider brings. NuGet's audit reports it as a warning, every warning is red, and the gate for 1.0.3.124 stopped at the build on it. `TheYard.Infrastructure.csproj` now names the package at 10.0.13, the patched release, so every project that reaches SQL Server's client resolves that version instead; nothing in the code calls it. The row above is held by `TechnologyVersionsTests` like every other. The reference can go once the provider brings a patched version on its own.

## Where it sits

Outside the code: the project files, package.json and the Dockerfile, which TechnologyVersionsTests reads against this table. It cost a live read of five sources and one test. A package with a reason to stay behind gets a row that says why, and a new release changes the right-hand column the next time the list is read.

## Files

- [`api/TheYard.Tests/TechnologyVersionsTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/TechnologyVersionsTests.cs): the test that reads this table against the project files.
- [`api/TheYard.Tests/PackagePinTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/PackagePinTests.cs): the Azure.Identity pin.
- [`package.json`](https://github.com/SteveStout/TheYard/blob/main/package.json): the front-end packages.
- [`Dockerfile`](https://github.com/SteveStout/TheYard/blob/main/Dockerfile): the build images.
