Pin the dependency graph with lock files - #43
Merged
Conversation
Restore resolved whatever the feed offered at the time. Central package management fixes the direct versions, but the transitive graph underneath them was free to move between one restore and the next, which is what the scorecard was pointing at with nine "nugetCommand not pinned by hash" findings. One property does it: RestorePackagesWithLockFile writes the resolved graph to a packages.lock.json beside each project, and every restore that runs in CI or in a sample image passes the locked-mode switch, so a graph that drifts from the lock fails the build instead of quietly resolving to something else. Twenty-five lock files, committed. Verified in both directions rather than assumed: a clean restore in locked mode passes, and moving a single version in Directory.Packages.props without regenerating the locks fails it with NU1004. A control that only ever passes is not a control. The Aspire AppHost opts out, and that is the honest answer rather than a workaround. The Aspire SDK adds a runtime-specific Aspire.Dashboard.Sdk package, so its resolved graph depends on the operating system doing the restore: a lock generated on Windows names win-x64, and a Linux restore then asks for linux-x64 and fails as inconsistent. A lock file cannot describe a graph that changes with where it was produced. This is a sample orchestrator, not anything shipped. The reason is recorded in the csproj. That failure was found by CI and then reproduced locally in the same Linux SDK image CI uses, which is also where the fix was confirmed, rather than pushing a guess and watching. The sample Dockerfiles restore inside the image against the manifests alone, so they copy the lock file next to the csproj as well. Both images were built to prove it, and the WebApi one was run: it serves /healthie and logs the startup warning about unauthenticated mutating endpoints. Changing a version now also means regenerating the locks, or restore fails. The comment in Directory.Build.props says so.
ivanvyd
force-pushed
the
chore/nuget-lock-files
branch
from
July 30, 2026 02:47
470f004 to
88fdfdb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last of the Scorecard findings — the nine
nugetCommand not pinned by hashalerts.Central package management already fixes the direct versions. The transitive graph underneath them was free to move between one restore and the next, which is what those alerts point at.
One property, no workaround
That writes the resolved graph to a
packages.lock.jsonbeside each project — 26 of them, committed. Every restore in CI, inpublish.yml, and in the two sample Dockerfiles now passes--locked-mode.The rest of the hand-written diff is five one-word changes. No csproj gymnastics: I briefly added some and then removed them, because they turned out to be fixing a problem I had caused myself (below).
Verified in both directions
A control that only ever passes isn't a control.
Directory.Packages.props, locks not regeneratedNU1004The Dockerfiles needed the lock file too
They restore against the manifests alone to keep the layer cacheable, copying only
Directory.*.propsand the csproj. With locked mode there's nothing to check against, so the build fails. Both now copypackages.lock.jsonnext to the csproj.Both images were built to prove it, and the WebApi one was run:
That is also the first time the startup warning from #42 has been seen firing in a real container rather than a unit test.
A mistake worth recording
My first attempt failed with
Invalid framework identifier '', and I misdiagnosed it as the test projects'<TargetFramework />/<TargetFrameworks />clearing. I restructuredDirectory.Build.propsand both csproj files to work around it.The actual cause: I had written
--locked-modeinside an XML comment inDirectory.Build.props, so the whole file failed to load and no properties were set. I have a memory entry about--in XML comments from earlier in this project and walked into it again.All the restructuring is reverted.
<TargetFramework />works fine with lock files.Cost, stated plainly
Changing a version in
Directory.Packages.propsnow also means regenerating the locks or restore fails. Dependabot handles this itself; a human does it with a force-evaluate restore. The comment inDirectory.Build.propssays so.473 tests green on net8.0 and net10.0, 0 errors / 0 warnings.