Skip to content

Pin the dependency graph with lock files - #43

Merged
ivanvyd merged 1 commit into
mainfrom
chore/nuget-lock-files
Jul 30, 2026
Merged

Pin the dependency graph with lock files#43
ivanvyd merged 1 commit into
mainfrom
chore/nuget-lock-files

Conversation

@ivanvyd

@ivanvyd ivanvyd commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes the last of the Scorecard findings — the nine nugetCommand not pinned by hash alerts.

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

<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>

That writes the resolved graph to a packages.lock.json beside each project — 26 of them, committed. Every restore in CI, in publish.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.

result
clean restore, locked mode passes
one version moved in Directory.Packages.props, locks not regenerated fails, NU1004

The Dockerfiles needed the lock file too

They restore against the manifests alone to keep the layer cacheable, copying only Directory.*.props and the csproj. With locked mode there's nothing to check against, so the build fails. Both now copy packages.lock.json next to the csproj.

Both images were built to prove it, and the WebApi one was run:

GET /healthie -> HTTP 200
warn: Healthie.Api.Diagnostics.UnauthenticatedSurfaceWarning[4001]
      Healthie: 6 endpoint(s) that can change a pulse checker are reachable without
      authenticating -- healthie/{checkerName}/interval, .../reset, .../start, ...

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 restructured Directory.Build.props and both csproj files to work around it.

The actual cause: I had written --locked-mode inside an XML comment in Directory.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.props now also means regenerating the locks or restore fails. Dependabot handles this itself; a human does it with a force-evaluate restore. The comment in Directory.Build.props says so.

473 tests green on net8.0 and net10.0, 0 errors / 0 warnings.

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
ivanvyd force-pushed the chore/nuget-lock-files branch from 470f004 to 88fdfdb Compare July 30, 2026 02:47
@ivanvyd
ivanvyd merged commit 370f3b6 into main Jul 30, 2026
7 of 8 checks passed
@ivanvyd
ivanvyd deleted the chore/nuget-lock-files branch July 30, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant