Skip to content

ci: make the NuGet lock-file refresh a one-command chore - #86

Merged
thomas-lg merged 2 commits into
mainfrom
deps/lockfile-automation
Aug 31, 2026
Merged

thomas-lg merged 2 commits into
mainfrom
deps/lockfile-automation

Conversation

@thomas-lg

Copy link
Copy Markdown
Owner

Why

Five of the sixteen Dependabot PRs open this morning were red for the same non-reason:

error NU1004: The project references coffeetracker.infrastructure whose dependencies has
changed. The packages lock file is inconsistent with the project dependencies so restore
can't be run in locked mode.

backend/Directory.Build.props enables NuGet lock files and ci.yml restores with --locked-mode. NuGet writes one lock file per project, so a bump in Application or Infrastructure also invalidates Api and Tests, which reference them transitively — but Dependabot only refreshes the lock file of the project whose .csproj it edited. Every backend bump therefore arrives broken and needs a manual --force-evaluate pass.

Weakening --locked-mode would make the symptom go away, but that gate is deliberate supply-chain protection: a dependency bump must not land without a reviewed lock file. So this makes the chore trivial and rare instead.

What

  • dependabot.yml — all NuGet updates group into one weekly PR (patterns: ["*"]), with majors in their own group so they stay reviewable on their own merits. One refresh per week instead of one per package.
  • scripts/refresh-lockfiles.ps1 — the solution-wide --force-evaluate restore in one command. Uses a local .NET SDK when there is one, the SDK container pinned to the same digest as the Dockerfile otherwise, so it works on a host with no SDK. -Check reproduces CI's --locked-mode restore. It compares content hashes before/after and refuses to leave behind changes to anything but the lock files — a changed .csproj means the restore resolved something unintended.
  • refresh-lockfiles.yml — the same job in CI: gh workflow run refresh-lockfiles.yml -f pr=<number>.
  • README.md — a short section so the next NU1004 doesn't need re-diagnosing.

Two constraints are encoded in the workflow's header comment because they're easy to trip over: Dependabot-triggered runs get a read-only GITHUB_TOKEN and no secrets (so this can't be an automatic pull_request fix-up), and a GITHUB_TOKEN push raises no workflow events (so the last step re-triggers the required checks explicitly).

Considered and rejected

Central Package Management. It would shrink the .csproj diffs, but it does not fix NU1004 — NuGet still writes one lock file per project and Dependabot still refreshes only the one it edited. Not worth a migration on a false premise.

Verified

  • refresh-lockfiles.ps1 -Check passes on a clean tree; the default mode reports Lock files were already up to date; nothing to commit. (The first version of the guard wrongly flagged unrelated working-tree edits — caught by running it, fixed by hash-comparing only what the restore touched.)
  • Both YAML files parse.
  • The SDK-container fallback is the path actually exercised above: this machine has the dotnet host but no SDK.

Dependabot refreshes packages.lock.json only for the project whose .csproj it
edited. Api and Tests reference Application/Infrastructure transitively, so any
backend bump leaves their lock files stale and ci.yml's restore --locked-mode
fails with NU1004 -- which is why five NuGet PRs sat red for weeks.

Rather than weaken --locked-mode (it is deliberate supply-chain protection), make
the refresh trivial and rare:

- dependabot.yml groups all NuGet updates into one weekly PR (majors separately),
  so the refresh is a weekly chore instead of a per-package one.
- scripts/refresh-lockfiles.ps1 runs the solution-wide --force-evaluate restore,
  using a local SDK when present and the pinned SDK container otherwise, and
  refuses to leave behind changes to anything but the lock files.
- refresh-lockfiles.yml does the same in CI for a given PR number, for when no
  local toolchain is available.

Note: Central Package Management would not have helped -- NuGet still writes one
lock file per project and Dependabot still refreshes only what it edited.
@thomas-lg
thomas-lg merged commit 24a3f16 into main Aug 31, 2026
@thomas-lg
thomas-lg deleted the deps/lockfile-automation branch August 31, 2026 18:23
thomas-lg added a commit that referenced this pull request Sep 13, 2026
Dependabot refreshes packages.lock.json only for the project whose .csproj it
edited. Api and Tests reference Application/Infrastructure transitively, so any
backend bump leaves their lock files stale and ci.yml's restore --locked-mode
fails with NU1004 -- which is why five NuGet PRs sat red for weeks.

Rather than weaken --locked-mode (it is deliberate supply-chain protection), make
the refresh trivial and rare:

- dependabot.yml groups all NuGet updates into one weekly PR (majors separately),
  so the refresh is a weekly chore instead of a per-package one.
- scripts/refresh-lockfiles.ps1 runs the solution-wide --force-evaluate restore,
  using a local SDK when present and the pinned SDK container otherwise, and
  refuses to leave behind changes to anything but the lock files.
- refresh-lockfiles.yml does the same in CI for a given PR number, for when no
  local toolchain is available.

Note: Central Package Management would not have helped -- NuGet still writes one
lock file per project and Dependabot still refreshes only what it edited.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant