diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 08f1903..3186e03 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,6 +3,11 @@ updates:
# Central package management means all NuGet versions live in
# Directory.Packages.props — Dependabot updates that file automatically,
# no per-project configuration needed.
+ # NOTE: Dependabot updates Directory.Packages.props but cannot rewrite packages.lock.json --
+ # the CentralTransitive entries have to be resolved by NuGet, not pattern-matched. Every bump
+ # here therefore arrives with stale lock files and fails `dotnet restore --locked-mode` (NU1004)
+ # until they are regenerated. Run the "Refresh lock files" workflow against the bump's branch;
+ # .github/workflows/refresh-lock-files.yml explains why that is a button and not automatic.
- package-ecosystem: "nuget"
directory: "/"
schedule:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8d544fd..f608c24 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,8 +29,16 @@ jobs:
8.0.x
10.0.x
+ # Named, so the failure can say what to do about it. NU1004 on a Dependabot branch means
+ # the lock files were not regenerated, which is a chore rather than a defect -- and the
+ # error NuGet prints for it does not mention the fix.
- name: Restore
- run: dotnet restore --locked-mode
+ run: |
+ set -euo pipefail
+ if ! dotnet restore --locked-mode; then
+ echo "::error title=Lock files are out of step::dotnet restore --locked-mode failed. If this is a dependency bump, Dependabot cannot rewrite packages.lock.json under central package management. Run the 'Refresh lock files' workflow against ${{ github.head_ref || github.ref_name }}, then re-run these checks."
+ exit 1
+ fi
- name: Build
run: dotnet build --configuration Release --no-restore /p:ContinuousIntegrationBuild=true
diff --git a/.github/workflows/refresh-lock-files.yml b/.github/workflows/refresh-lock-files.yml
new file mode 100644
index 0000000..d375e5c
--- /dev/null
+++ b/.github/workflows/refresh-lock-files.yml
@@ -0,0 +1,81 @@
+name: Refresh lock files
+
+# Dependabot cannot do this itself.
+#
+# Under central package management it edits Directory.Packages.props, but it does not rewrite
+# packages.lock.json -- and it cannot, because the CentralTransitive entries have to be resolved by
+# NuGet, not pattern-matched. So every NuGet bump arrives with lock files that still name the old
+# versions, and `dotnet restore --locked-mode` fails before anything builds:
+#
+# error NU1004: Mistmatch between the requestedVersion of a lock file dependency marked as
+# CentralTransitive and the version specified in the central package management file.
+#
+# Run this against the bump's branch and it fixes itself.
+#
+# Deliberately `workflow_dispatch` and not an automatic push on every Dependabot PR. A push made
+# with GITHUB_TOKEN does not re-trigger workflows, so an automatic version would commit the fix and
+# leave the required checks pinned to the superseded commit -- one stall traded for another. Making
+# that work needs a PAT or a GitHub App key, and this repository stores no such credential on
+# purpose. A human runs this, then re-runs the checks.
+
+on:
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: The branch to refresh, e.g. dependabot/nuget/microsoft-extensions-abc123
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+concurrency:
+ group: refresh-lock-files-${{ inputs.branch }}
+ cancel-in-progress: false
+
+jobs:
+ refresh:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ # The one job in this repository that writes. Scoped here rather than at the top of the file
+ # so nothing else in this workflow inherits it.
+ contents: write
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ ref: ${{ inputs.branch }}
+ persist-credentials: true
+
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ # --force-evaluate is the whole point: it re-resolves rather than asserting the lock is right.
+ - run: dotnet restore --force-evaluate
+
+ # Prove the result before committing it. If locked-mode cannot restore what force-evaluate
+ # just wrote, something is wrong that a commit would only bury.
+ - run: dotnet restore --locked-mode
+ - run: dotnet build --no-restore -c Release
+
+ - name: Commit the refreshed lock files
+ run: |
+ set -euo pipefail
+ # Named paths, never `git add -A`: this job can write, so it says exactly what it writes.
+ if [ -z "$(git status --porcelain -- '*packages.lock.json')" ]; then
+ echo "Lock files already match the manifests. Nothing to commit."
+ exit 0
+ fi
+ git config user.name 'github-actions[bot]'
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+ git add -- '*packages.lock.json'
+ git commit -m 'Refresh the lock files for this bump
+
+ Regenerated with `dotnet restore --force-evaluate`, then verified with
+ `--locked-mode` and a Release build. Dependabot cannot write these under
+ central package management; see .github/workflows/refresh-lock-files.yml.'
+ git push origin HEAD:'${{ inputs.branch }}'
+ echo "Pushed. Re-run the pull request's checks -- a GITHUB_TOKEN push does not."
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 067ffd4..baaa5ee 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -73,6 +73,18 @@
+
+
diff --git a/tests/Healthie.Tests.Unit/Healthie.Tests.Unit.csproj b/tests/Healthie.Tests.Unit/Healthie.Tests.Unit.csproj
index 49b2c01..6176a90 100644
--- a/tests/Healthie.Tests.Unit/Healthie.Tests.Unit.csproj
+++ b/tests/Healthie.Tests.Unit/Healthie.Tests.Unit.csproj
@@ -21,6 +21,7 @@
+
diff --git a/tests/Healthie.Tests.Unit/packages.lock.json b/tests/Healthie.Tests.Unit/packages.lock.json
index 2a86fff..de1ac6c 100644
--- a/tests/Healthie.Tests.Unit/packages.lock.json
+++ b/tests/Healthie.Tests.Unit/packages.lock.json
@@ -31,6 +31,15 @@
"resolved": "13.0.4",
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
},
+ "SSH.NET": {
+ "type": "Direct",
+ "requested": "[2026.0.0, )",
+ "resolved": "2026.0.0",
+ "contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
+ "dependencies": {
+ "BouncyCastle.Cryptography": "2.7.0"
+ }
+ },
"Testcontainers.CosmosDb": {
"type": "Direct",
"requested": "[4.13.0, )",
@@ -116,8 +125,8 @@
},
"BouncyCastle.Cryptography": {
"type": "Transitive",
- "resolved": "2.6.2",
- "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
+ "resolved": "2.7.0",
+ "contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
},
"Docker.DotNet.Enhanced": {
"type": "Transitive",
@@ -399,14 +408,6 @@
"SQLitePCLRaw.core": "2.1.12"
}
},
- "SSH.NET": {
- "type": "Transitive",
- "resolved": "2025.1.0",
- "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
- "dependencies": {
- "BouncyCastle.Cryptography": "2.6.2"
- }
- },
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.1.0",
@@ -831,6 +832,15 @@
"resolved": "13.0.4",
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
},
+ "SSH.NET": {
+ "type": "Direct",
+ "requested": "[2026.0.0, )",
+ "resolved": "2026.0.0",
+ "contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
+ "dependencies": {
+ "BouncyCastle.Cryptography": "2.7.0"
+ }
+ },
"Testcontainers.CosmosDb": {
"type": "Direct",
"requested": "[4.13.0, )",
@@ -916,8 +926,8 @@
},
"BouncyCastle.Cryptography": {
"type": "Transitive",
- "resolved": "2.6.2",
- "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
+ "resolved": "2.7.0",
+ "contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
},
"Docker.DotNet.Enhanced": {
"type": "Transitive",
@@ -1312,14 +1322,6 @@
"SQLitePCLRaw.core": "2.1.12"
}
},
- "SSH.NET": {
- "type": "Transitive",
- "resolved": "2025.1.0",
- "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
- "dependencies": {
- "BouncyCastle.Cryptography": "2.6.2"
- }
- },
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.1.0",