Skip to content

Replace Inputs/Outputs with stamp existence check for incremental builds - #170

Open
MattKotsenas wants to merge 3 commits into
alirezanet:masterfrom
MattKotsenas:fix/tool-manifest-path
Open

Replace Inputs/Outputs with stamp existence check for incremental builds#170
MattKotsenas wants to merge 3 commits into
alirezanet:masterfrom
MattKotsenas:fix/tool-manifest-path

Conversation

@MattKotsenas

Copy link
Copy Markdown
Contributor

Description

Follow-up to #169 (should be merged first or taken together).

.NET SDK 10.0.102 intentionally changed where dotnet new tool-manifest places the manifest file, from .config/dotnet-tools.json to the current directory (dotnet/sdk#49296). The SDK's manifest
discovery walks up the directory tree checking both dotnet-tools.json and .config/dotnet-tools.json at each level, so manifests can now live anywhere along the path to root.

The MSBuild target's Inputs attribute had a hardcoded path to .config/dotnet-tools.json. On repos created with SDK 10.0.102+, this file doesn't exist, so MSBuild can't do the timestamp comparison, and the target re-runs every build, defeating the incremental optimization.

Rather than adding manifest discovery logic to MSBuild XML, this replaces the Inputs/Outputs mechanism with a simpler !Exists(stamp) condition. The target runs once (creating the stamp), then skips on all subsequent builds. dotnet clean
removes the stamp (via FileWrites), so the next build re-installs. If you update tool versions in the manifest, run dotnet clean or dotnet tool install to pick up the change.

Alternatively, we could create another ItemGroup or Target that searches for the dotnet-tools.json file before calling our Husky target. If we want the most deterministic build possible, I'm happy to make that change instead. However, in that case I'd suggest dropping the manual install instructions and pushing everyone more forcefully towards the attach command as part of the same change.

Depends on #169.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or that my feature works
  • I have made corresponding changes to the documentation
  • I have commented my code, particularly in hard-to-understand areas
  • New and existing unit tests pass locally with my changes
  • I did test corresponding changes on Windows
  • I did test corresponding changes on Linux
  • I did test corresponding changes on Mac

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Husky’s MSBuild incremental-install behavior to avoid relying on the .config/dotnet-tools.json path (which changed in .NET SDK 10.0.102), switching to a stamp-file existence check so the restore/install target can reliably skip after first run.

Changes:

  • Replace MSBuild Inputs/Outputs incremental build logic with !Exists(install.stamp) gating.
  • Introduce/configure a HuskyRoot MSBuild property to derive all paths (stamp, working directory) consistently.
  • Update attach command generation, tests, and docs to match the new stamp + HuskyRoot approach.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/HuskyTest/Cli/AttachCommandTests.cs Updates assertions to validate HuskyRoot + !Exists(stamp) condition and remove Inputs/Outputs expectations.
src/Husky/Husky.csproj Adds HuskyRoot property and switches target gating from Inputs/Outputs to stamp existence.
src/Husky/Cli/AttachCommand.cs Generates HuskyRoot PropertyGroup and a stamp existence condition; removes prior incremental Inputs/Outputs behavior.
docs/guide/submodules.md Updates manual XML example to include HuskyRoot and stamp existence gating.
docs/guide/automate.md Updates manual attach instructions and rationale to the new HuskyRoot + stamp approach.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Husky/Cli/AttachCommand.cs Outdated
@MattKotsenas
MattKotsenas force-pushed the fix/tool-manifest-path branch from 1272f93 to 8fb4ba4 Compare March 21, 2026 04:51
@MattKotsenas

MattKotsenas commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

Ok, let me release this branch, then it'll be ready for review.

Edit: Updated now. @alirezanet, do you mind taking a look whenever you have time? If you have any questions, please let me know. Thanks!

@MattKotsenas
MattKotsenas force-pushed the fix/tool-manifest-path branch from 8fb4ba4 to f8e90d5 Compare March 21, 2026 21:12
@MattKotsenas

Copy link
Copy Markdown
Contributor Author

Hi @alirezanet, sorry for the bother, but I wanted to make sure you saw this is now updated and ready for review. If you have any questions, please let me know. Thanks!

@alirezanet

alirezanet commented Mar 26, 2026

Copy link
Copy Markdown
Owner

Hey @MattKotsenas,
I saw your update, but I’m a bit busy these days and can’t test it right now. If you’re confident there are no breaking changes and it’s been properly tested, I’m happy to trust you and merge it.

The target listed the stamp in <FileWrites>, but it lives at the repo root (.husky/_/) outside the project obj/bin dirs, so MSBuild never recorded it in FileListAbsolute.txt. dotnet clean left the stamp in place and the documented "clean re-installs" behavior never worked. Replace the dead <FileWrites> with a HuskyClean target (AfterTargets="Clean") that deletes the stamp.
LoggerEx.logger is a process-global set once at startup in production (husky runs one command per process). The unit tests mutate it per-test, and with xUnit's default cross-class parallelism InstallCommandTests' "Git hooks installed" output leaked into AttachCommandTests' console assertion, failing it ~50% of runs. Serialize the suite via xunit.runner.json, matching the integration project's wiring. No production code is affected.
@MattKotsenas

MattKotsenas commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Hi @alirezanet! I apologize for the delay; I had some family issues to attend to.

I've been validating this change and can confirm no breaking changes (to the limits of "absence of evidence is not evidence of absence" :)). In testing I discovered that Clean wasn't working correctly, so I also fixed that as part of this PR and added an integration test to validate.

<FileWrites> does not work in this scenario because it's outside the output paths (e.g. obj/ and bin/). To address that I hooked clean and use a <Delete> explicitly.

Lastly, I added the xunit config to serialize the tests, otherwise they pollute each other on the shared static Logger.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment on lines +115 to +125
// The stamp is written from a target that runs AfterTargets="Restore", so it is
// never recorded in the build's FileListAbsolute.txt and `dotnet clean` cannot
// remove it. Delete it explicitly after Clean so the next build re-installs
// (e.g. after a tool version bump).
var cleanTarget = new XElement("Target");
cleanTarget.SetAttributeValue("Name", "HuskyClean");
cleanTarget.SetAttributeValue("AfterTargets", "Clean");
var delete = new XElement("Delete");
delete.SetAttributeValue("Files", "$(HuskyRoot).husky/_/install.stamp");
cleanTarget.Add(delete);
doc.Add(cleanTarget);
Comment thread src/Husky/Husky.csproj
</ItemGroup>
</Target>
<Target Name="HuskyClean" AfterTargets="Clean">
<Delete Files="$(HuskyRoot).husky/_/install.stamp" />
Comment on lines +52 to +57
await c.BashAsync(output, "dotnet build TestProjectBase.csproj");
var firstMtime = (await c.BashAsync(output, StampMtime)).Stdout.Trim();
firstMtime.Should().NotBe("NONE", "the first build should create the stamp");

await c.BashAsync(output, "dotnet build TestProjectBase.csproj");
var secondMtime = (await c.BashAsync(output, StampMtime)).Stdout.Trim();
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.

3 participants