Skip to content

Fix parallel install mutex bug and support incremental build#159

Merged
alirezanet merged 2 commits intoalirezanet:masterfrom
MattKotsenas:feature/incremental-build
Mar 11, 2026
Merged

Fix parallel install mutex bug and support incremental build#159
alirezanet merged 2 commits intoalirezanet:masterfrom
MattKotsenas:feature/incremental-build

Conversation

@MattKotsenas
Copy link
Contributor

@MattKotsenas MattKotsenas commented Mar 11, 2026

Description

Fixes #155

This PR fixes two issues that occur when the target is present in more than one project (common in monorepo scenarios).

1. Fix concurrent git config lock contention

When multiple MSBuild nodes run dotnet husky install in parallel, concurrent git rev-parse calls would intermittently fail with:

warning: unable to access '.git/config': Permission denied
fatal: unknown error occurred while reading the configuration files

The existing mutex serialized CreateResources (which writes .git/config via git config core.hooksPath), but the validation calls (git rev-parse, IsSubmodule, GetGitDirectory) ran outside the mutex. When one process held .git/config locked for writing, concurrent processes trying to read it got Permission denied.

The fix is to move all git operations inside the mutex by extracting a DoInstallAsync method that contains both validation and resource creation. The mutex now wraps the entire install as a single critical section.

Reproduced at ~10% failure rate with a 100-project monorepo test harness.

2. Make the Husky MSBuild target incremental

The Husky target ran dotnet tool restore + dotnet husky install on every build, even when nothing changed. This added seconds to every restore/build cycle, particularly painful when the target is in Directory.Build.targets (runs once per project in the solution).

The fix is to add MSBuild Inputs/Outputs to the target. .config/dotnet-tools.json is an input so that the target re-runs when tool version changes. .husky/_/install.stamp is a sentinel file created after successful install. Touch task creates the stamp; FileWrites ensures dotnet clean removes it.

Both the dotnet husky attach command and the embedded target in Husky.csproj now generate the incremental pattern. Documentation updated with the new snippet and a note about using $(MSBuildThisFileDirectory) in Directory.Build.targets.

3. Tradeoff: self-healing vs. performance

Previously, husky re-installed on every build, which meant a corrupted or manually altered install (e.g., someone running git config --unset core.hooksPath) would silently self-heal on the next build. With the stamp file, this self-healing is lost as the stamp says "done" and husky won't re-run until the inputs change or the stamp is removed.

I believe this is the right tradeoff:

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

@what-the-diff
Copy link

what-the-diff bot commented Mar 11, 2026

PR Summary

  • Husky Target Definition Enhanced
    Updates were made to the Husky target in a few documents to better support the incremental build process through including additional attributes.

  • Refactored Installation Process
    The process of installation in the InstallCommand.cs file has been simplified. A new method was created for this purpose, and redundant checks have been removed, resulting in better clarity and structure.

  • Improved Parallelism Management
    A new lock mechanism has been introduced to manage multiple installations at once. This ensure commands do not overlap when multiple installations are done simultaneously.

  • Testing Process Upgraded
    The tests have been updated in order to confirm the functioning of the new attributes in the Husky target and check the parallel executions. Also, they assure that the Git commands are executed rightly without interference.

  • Documentation Upgrades
    The documentation has been refined to explain better the usage of new attributes and file handling for Husky configurations. Additional tips are also provided for managing multi-project solutions.

Move all git operations (rev-parse, IsSubmodule, GetGitDirectory) inside
the mutex in InstallCommand so reads don't conflict with writes to
.git/config. On first run with parallel MSBuild nodes, concurrent
'git rev-parse' calls would fail with 'Permission denied' because
another process held .git/config locked for 'git config core.hooksPath'.

The mutex now wraps validation + resource creation as a single critical
section via the extracted DoInstallAsync method.

Add unit test that detects git call interleaving using separate mocks
per install instance and deterministic SemaphoreSlim coordination.
Add MSBuild incremental build support to the Husky target so it skips
re-running when nothing has changed. Uses a stamp file pattern:

- Inputs: .config/dotnet-tools.json (triggers on tool version changes)
- Outputs: .husky/_/install.stamp (marker created after success)
- Touch task creates the stamp after dotnet tool restore + install
- FileWrites ensures dotnet clean removes the stamp

Changes:
- AttachCommand.cs: Add Inputs/Outputs/Touch/FileWrites to GetTarget()
- Husky.csproj: Same incremental pattern on the embedded target
- AttachCommandTests.cs: Verify new target structure and path handling
- automate.md: Updated snippets, added incrementality and
  Directory.Build.targets documentation

Fixes: alirezanet#155
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses monorepo/multi-project build issues by (1) preventing parallel Husky installs from interleaving git operations and (2) making the Husky MSBuild integration incremental via an input/output stamp file.

Changes:

  • Wrap git validation + install work under the install mutex to avoid .git/config lock contention in parallel builds.
  • Add MSBuild Inputs/Outputs with a sentinel .husky/_/install.stamp to avoid running Husky install on every build.
  • Update attach generation, tests, and docs to reflect the incremental target pattern.

Reviewed changes

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

Show a summary per file
File Description
src/Husky/Cli/InstallCommand.cs Refactors install flow so git operations are performed under the mutex when parallelism is enabled.
src/Husky/Husky.csproj Makes the embedded Husky target incremental using Inputs/Outputs + a stamp file.
src/Husky/Cli/AttachCommand.cs Updates generated MSBuild target to include Inputs/Outputs, Touch, and FileWrites for the stamp.
docs/guide/automate.md Updates the manual snippet and guidance to the new incremental build pattern.
tests/HuskyTest/Cli/InstallCommandTests.cs Adds a concurrency regression test for git call interleaving.
tests/HuskyTest/Cli/AttachCommandTests.cs Extends assertions to validate incremental target attributes/elements.

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

@alirezanet
Copy link
Owner

Hi @MattKotsenas, thanks for the PR
I haven't reviewed this PR myself yet, but could you please check the Copilot suggestions first? That way, I'll know your take on each potential issue during my review.

@MattKotsenas
Copy link
Contributor Author

Hi @alirezanet! Thanks for the quick response!

In this particular case, Copilot is incorrect. It's assuming that FileWrites is a normal item group that's evaluated as part of clean. However, the way it actually works then the target runs during build, FileWrites items get written to the file obj/**/{project}.FileListAbsolute.txt on disk. When dotnet clean runs later, it reads that persisted list and deletes everything in it. It does not re-evaluate the target. So, it doesn't matter that the target doesn't execute during clean, the stamp file path was already recorded during the build that created it.

Here's a blog post to read more if you're interested: https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/february/msbuild-best-practices-for-creating-reliable-builds-part-1

Let me know if you have any questions. Thanks!

Copy link
Owner

@alirezanet alirezanet left a comment

Choose a reason for hiding this comment

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

LGTM 🎉

@alirezanet alirezanet merged commit a51fed5 into alirezanet:master Mar 11, 2026
5 checks passed
@MattKotsenas MattKotsenas deleted the feature/incremental-build branch March 11, 2026 18:35
@MattKotsenas
Copy link
Contributor Author

Thanks @alirezanet, I apologize because I know it can be annoying, but do you know when the next release would be with this fix? Depending on the timing I'll either wait for this release to fix our build, or try to find a short term workaround. Any guidance is greatly appreciated. Thanks!

@alirezanet
Copy link
Owner

Thanks @alirezanet, I apologize because I know it can be annoying, but do you know when the next release would be with this fix? Depending on the timing I'll either wait for this release to fix our build, or try to find a short term workaround. Any guidance is greatly appreciated. Thanks!

I'm working on a couple of other small issues, if I manage to fix them, I'll release the next version in 1-2 days max, maybe even tonight! 🤞

honestly releasing this library is a bit risky because it might block a lot of people if I miss any small potential issues. if you could help me later to test it properly I would do it faster 😊. (I mean after release)

@MattKotsenas
Copy link
Contributor Author

Sounds good! Feel free to ping me when you have a release or a pre-release and I'm happy to test it

@alirezanet
Copy link
Owner

Sounds good! Feel free to ping me when you have a release or a pre-release and I'm happy to test it

Just release the v0.9.0
https://github.com/alirezanet/Husky.Net/releases/tag/v0.9.0

github-actions bot pushed a commit to ptr727/LanguageTags that referenced this pull request Mar 13, 2026
Updated [husky](https://github.com/alirezanet/husky.net) from 0.8.0 to
0.9.0.

<details>
<summary>Release notes</summary>

_Sourced from [husky's
releases](https://github.com/alirezanet/husky.net/releases)._

## 0.9.0

## What's Changed
* remove net6.0/net7.0 since out of support by @​WeihanLi in
alirezanet/Husky.Net#135
* fix: update regex pattern to allow breaking commits by @​joaoopereira
in alirezanet/Husky.Net#153
* Fix parallel install mutex bug and support incremental build by
@​MattKotsenas in alirezanet/Husky.Net#159
* fix: handle file paths with spaces in git commands in
alirezanet/Husky.Net#156
* feat: add `staged` property to custom variables for re-staging support
in alirezanet/Husky.Net#163
* feat: support variables in include/exclude glob patterns in
alirezanet/Husky.Net#161
* fix: use AfterTargets="Restore" to support NuGet credential helpers
for private feeds in alirezanet/Husky.Net#162

## New Contributors
* @​joaoopereira made their first contribution in
alirezanet/Husky.Net#153
* @​MattKotsenas made their first contribution in
alirezanet/Husky.Net#159

**Full Changelog**:
alirezanet/Husky.Net@v0.8.0...v0.9.0

Commits viewable in [compare
view](alirezanet/Husky.Net@v0.8.0...v0.9.0).
</details>

Updated
[Microsoft.Extensions.Logging.Abstractions](https://github.com/dotnet/dotnet)
from 10.0.4 to 10.0.5.

<details>
<summary>Release notes</summary>

_Sourced from [Microsoft.Extensions.Logging.Abstractions's
releases](https://github.com/dotnet/dotnet/releases)._

No release notes found for this version range.

Commits viewable in [compare
view](https://github.com/dotnet/dotnet/commits).
</details>

Updated [Microsoft.SourceLink.GitHub](https://github.com/dotnet/dotnet)
from 10.0.200 to 10.0.201.

<details>
<summary>Release notes</summary>

_Sourced from [Microsoft.SourceLink.GitHub's
releases](https://github.com/dotnet/dotnet/releases)._

## 10.0.201

You can build .NET 10.0 from the repository by cloning the release tag
`v10.0.201` and following the build instructions in the [main
README.md](https://github.com/dotnet/dotnet/blob/v10.0.201/README.md#building).

Alternatively, you can build from the sources attached to this release
directly.
More information on this process can be found in the [dotnet/dotnet
repository](https://github.com/dotnet/dotnet/blob/v10.0.201/README.md#building-from-released-sources).

Attached are PGP signatures for the GitHub generated tarball and
zipball. You can find the public key at https://dot.net/release-key-2023

Commits viewable in [compare
view](dotnet/dotnet@v10.0.200...v10.0.201).
</details>

Updated [System.CommandLine](https://github.com/dotnet/dotnet) from
2.0.4 to 2.0.5.

<details>
<summary>Release notes</summary>

_Sourced from [System.CommandLine's
releases](https://github.com/dotnet/dotnet/releases)._

No release notes found for this version range.

Commits viewable in [compare
view](https://github.com/dotnet/dotnet/commits).
</details>

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.

Guide for automating installation causes unnecessary work to be done when applied to multiple projects

3 participants