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",