From 759228a8d2f88f32d62eef260904da500e4949fc Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Mon, 6 Jul 2026 17:26:24 +0300 Subject: [PATCH 01/12] Consume prebuilt native binaries from a fetched archive, drop NativeBinaries package Replace the LibGit2Sharp.NativeBinaries.UiPath PackageReference with a directly-consumed native payload: natives.lock.json pins the archive (tag + url + sha256) published by the libgit2sharp.nativebinaries build workflow; fetch.natives.ps1 downloads, SHA256-verifies, and extracts it to native/ (git-ignored). Targets/NativeBinaries.props (imported via the root Directory.Build.props) sets libgit2_filename/libgit2_hash and stages the multi-RID runtimes into every project's output and into the LibGit2Sharp.UiPath package. CI runs fetch.natives.ps1 before build/test. This collapses the two-package layout (managed + NativeBinaries) into a single LibGit2Sharp.UiPath nupkg that bundles all six RIDs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 13 ++++++ .gitignore | 3 ++ Directory.Build.props | 4 ++ LibGit2Sharp/LibGit2Sharp.csproj | 3 +- Targets/NativeBinaries.props | 51 +++++++++++++++++++++ fetch.natives.ps1 | 79 ++++++++++++++++++++++++++++++++ natives.lock.json | 8 ++++ 7 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 Targets/NativeBinaries.props create mode 100644 fetch.natives.ps1 create mode 100644 natives.lock.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f44285812..09d97010b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,11 @@ jobs: uses: actions/setup-dotnet@v3.0.3 with: dotnet-version: 7.0.x + - name: Fetch native binaries + # Downloads + SHA256-verifies the prebuilt native payload pinned in natives.lock.json into + # native/ (replaces the old NativeBinaries.UiPath PackageReference). pwsh ships on all runners. + shell: pwsh + run: ./fetch.natives.ps1 - name: Build run: dotnet build LibGit2Sharp.sln --configuration Release - name: Upload packages @@ -53,6 +58,9 @@ jobs: dotnet-version: | 7.0.x 6.0.x + - name: Fetch native binaries + shell: pwsh + run: ./fetch.natives.ps1 - name: Run ${{ matrix.tfm }} tests run: dotnet test LibGit2Sharp.sln --configuration Release --framework ${{ matrix.tfm }} --logger "GitHubActions" /p:ExtraDefine=LEAKS_IDENTIFYING test-linux: @@ -80,6 +88,11 @@ jobs: uses: actions/checkout@v3.5.0 with: fetch-depth: 0 + - name: Fetch native binaries + # Fetch on the host (pwsh is preinstalled on ubuntu runners): native/ lives under $PWD, which + # is mounted into the test container as /app, so the dockerized dotnet test sees it. + shell: pwsh + run: ./fetch.natives.ps1 - name: Setup QEMU if: matrix.arch == 'arm64' run: docker run --rm --privileged multiarch/qemu-user-static:register --reset diff --git a/.gitignore b/.gitignore index 2f75ccc1d..ef28b1d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ _ReSharper*/ _NCrunch_LibGit2Sharp/ packages/ worktree.playlist + +# Prebuilt native binaries fetched by fetch.natives.ps1 (pinned in natives.lock.json) +/native/ diff --git a/Directory.Build.props b/Directory.Build.props index 72eda8864..10a527853 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,4 +12,8 @@ true + + + diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 4aaf939d4..865481adb 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -34,7 +34,8 @@ - + diff --git a/Targets/NativeBinaries.props b/Targets/NativeBinaries.props new file mode 100644 index 000000000..8c18ac33d --- /dev/null +++ b/Targets/NativeBinaries.props @@ -0,0 +1,51 @@ + + + + + $(MSBuildThisFileDirectory)..\native\ + + libgit2 + $(MSBuildThisFileFullPath) + + + + + $([System.IO.File]::ReadAllText('$(NativesDir)libgit2\libgit2_hash.txt').Trim()) + + + + + + runtimes\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + true + runtimes\%(RecursiveDir) + false + + + + + + + lib\win32\x64\%(Filename)%(Extension) + PreserveNewest + false + false + + + + + + + + + diff --git a/fetch.natives.ps1 b/fetch.natives.ps1 new file mode 100644 index 000000000..0a6ae6873 --- /dev/null +++ b/fetch.natives.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Downloads the prebuilt LibGit2Sharp native payload (all RIDs) pinned in natives.lock.json, + verifies its SHA256, and extracts it to native/. + + This replaces the old LibGit2Sharp.NativeBinaries.UiPath PackageReference: the managed build + consumes these binaries directly. Verification is mandatory - if the lockfile SHA256 is empty or + does not match the download, this fails hard. +.PARAMETER Force + Re-download and re-extract even if native/ already exists. +#> + +Param( + [switch]$Force +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +# Expand-Archive/native calls below drive on their own results; don't let a non-zero native exit +# auto-throw first (PowerShell 7.4+ defaults this to $true). Harmless no-op on Windows PowerShell 5.1. +$PSNativeCommandUseErrorActionPreference = $false + +$projectDirectory = Split-Path $MyInvocation.MyCommand.Path +$lockPath = Join-Path $projectDirectory 'natives.lock.json' +$nativeDirectory = Join-Path $projectDirectory 'native' +$cacheDirectory = Join-Path $nativeDirectory '_cache' + +# Proxy-aware download, mirroring fetch.deps.ps1 in the nativebinaries repo. +function Invoke-Download($url, $outFile) { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $params = @{ Uri = $url; OutFile = $outFile; UseBasicParsing = $true } + $proxy = [System.Net.WebRequest]::GetSystemWebProxy() + if ($proxy) { + $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials + $proxyUri = $proxy.GetProxy([uri]$url) + # GetProxy returns an empty value (or the url itself) for a direct connection; only route + # through a proxy when it names a genuinely different endpoint. + if ($proxyUri -and "$proxyUri" -ne "$url") { + $params.Proxy = "$proxyUri" + $params.ProxyUseDefaultCredentials = $true + } + } + Write-Host "-> Downloading $url" + Invoke-WebRequest @params +} + +$lock = Get-Content $lockPath -Raw | ConvertFrom-Json +$expectedSha = "$($lock.sha256)".Trim().ToLower() +if (-not $expectedSha) { + throw "natives.lock.json has no sha256. Run the nativebinaries 'build' workflow (publish=true), " + + "then populate tag/url/sha256 here. Refusing to fetch without hash verification." +} + +if ((Test-Path $nativeDirectory) -and -not $Force) { + $marker = Join-Path $nativeDirectory '.fetched-sha256' + if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim().ToLower() -eq $expectedSha)) { + Write-Host "==> Native payload already present for sha256 $expectedSha (use -Force to refresh). Skipping." + return $nativeDirectory + } +} + +New-Item -ItemType Directory -Path $cacheDirectory -Force | Out-Null +$archive = Join-Path $cacheDirectory $lock.filename +Invoke-Download $lock.url $archive + +$actualSha = (Get-FileHash -Algorithm SHA256 -Path $archive).Hash.ToLower() +if ($actualSha -ne $expectedSha) { + Remove-Item $archive -Force + throw "SHA256 mismatch for '$($lock.filename)'.`n expected: $expectedSha`n actual: $actualSha`nAborting." +} +Write-Host "==> SHA256 verified: $actualSha" + +# Wipe the payload (but keep the download cache) so stale RIDs never linger. +Get-ChildItem $nativeDirectory -Force -Exclude '_cache' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force +Expand-Archive -Path $archive -DestinationPath $nativeDirectory -Force +Set-Content -Path (Join-Path $nativeDirectory '.fetched-sha256') -Value $expectedSha -NoNewline +Write-Host "==> Extracted native payload to '$nativeDirectory'" + +return $nativeDirectory diff --git a/natives.lock.json b/natives.lock.json new file mode 100644 index 000000000..9e45de8e3 --- /dev/null +++ b/natives.lock.json @@ -0,0 +1,8 @@ +{ + "$comment": "Pins the prebuilt LibGit2Sharp native payload (all RIDs) produced by the UiPath/libgit2sharp.nativebinaries 'build' workflow. fetch.natives.ps1 downloads this archive and fails hard unless its SHA256 matches. To bump: run that workflow with publish=true, then copy the new tag/url/sha256 (printed by the workflow and in the release's .sha256 sidecar) here.", + "repo": "UiPath/libgit2sharp.nativebinaries", + "tag": "natives-1.9.1-v5.21", + "filename": "natives-1.9.1-v5.21.zip", + "url": "https://github.com/UiPath/libgit2sharp.nativebinaries/releases/download/natives-1.9.1-v5.21/natives-1.9.1-v5.21.zip", + "sha256": "1a40ac67ac14b1e099b4d3a0823019e164fcde9211e3c40c95c6b355267f7440" +} From e244e21e3f2a22a3506b5e9fe1ce4c0e92fd0278 Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Mon, 6 Jul 2026 17:34:15 +0300 Subject: [PATCH 02/12] Target net8.0 (cross-platform) instead of net6.0-windows The library does its OS checks at runtime and ships native binaries for all six RIDs, so the Windows-only TFM was an unnecessary restriction. Move the library, the test project, and the native-load probe apps to net8.0. The net472-only code paths (desktop/**, NativeLibraryLoadTestApp wiring, #if NETFRAMEWORK) stay dormant under the non-framework branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj | 2 +- LibGit2Sharp/LibGit2Sharp.csproj | 2 +- .../x64/NativeLibraryLoadTestApp.x64.csproj | 2 +- .../x86/NativeLibraryLoadTestApp.x86.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 89308b934..c90aaa8e9 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -1,7 +1,7 @@  - net6.0-windows + net8.0 latest diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 865481adb..41923db0a 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -1,7 +1,7 @@  - net6.0-windows + net8.0 true LibGit2Sharp brings all the might and speed of libgit2, a native Git implementation, to the managed world of .NET LibGit2Sharp contributors diff --git a/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj b/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj index 20255e289..c9682009f 100644 --- a/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj +++ b/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj @@ -2,7 +2,7 @@ Exe - net6.0-windows + net8.0 x64 diff --git a/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj b/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj index c44ee8dab..841181bc1 100644 --- a/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj +++ b/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj @@ -2,7 +2,7 @@ Exe - net6.0-windows + net8.0 x86 From fecb92c853e69b78b3b8f320aff46244513127d1 Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Mon, 6 Jul 2026 17:40:41 +0300 Subject: [PATCH 03/12] Align CI matrix to net8.0 cross-platform Bump the build/test SDK to 8.0.x and run tests on one native runner per OS (windows-latest, ubuntu-latest, macos-14). Drop the dockerized multi-distro test-linux job: it pinned .NET 6/7 SDK images and covered alpine/musl, which the glibc-linked linux natives we ship can't run. The win-arm64/linux-arm64/osx-x64 natives ship in the package but are not runtime-tested here (no arm Windows/Linux runners, scarce Intel mac). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 60 ++++++++-------------------------------- 1 file changed, 11 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09d97010b..ed30d8fca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Install .NET SDK uses: actions/setup-dotnet@v3.0.3 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Fetch native binaries # Downloads + SHA256-verifies the prebuilt native payload pinned in natives.lock.json into # native/ (replaces the old NativeBinaries.UiPath PackageReference). pwsh ships on all runners. @@ -36,16 +36,18 @@ jobs: path: bin/Packages/ retention-days: 7 test: - name: Test / ${{ matrix.os }} / ${{ matrix.arch }} / ${{ matrix.tfm }} + name: Test / ${{ matrix.os }} / ${{ matrix.tfm }} runs-on: ${{ matrix.os }} strategy: matrix: - arch: [ amd64 ] - os: [ windows-2019, macos-11 ] - tfm: [ net472, net6.0, net7.0 ] - exclude: - - os: macos-11 - tfm: net472 + # Native runners, one per OS. macos-14 is Apple Silicon, so it exercises the osx-arm64 + # native; windows-latest -> win-x64, ubuntu-latest -> linux-x64. The win-arm64/linux-arm64 + # and osx-x64 natives ship in the package but are not runtime-tested here (would need arm + # Windows/Linux runners and a scarce Intel mac). The old dockerized multi-distro matrix was + # dropped: it pinned .NET 6/7 SDK images and included alpine/musl, which our glibc-linked + # linux natives can't run anyway. + os: [ windows-latest, ubuntu-latest, macos-14 ] + tfm: [ net8.0 ] fail-fast: false steps: - name: Checkout @@ -55,50 +57,10 @@ jobs: - name: Install .NET SDK uses: actions/setup-dotnet@v3.0.3 with: - dotnet-version: | - 7.0.x - 6.0.x + dotnet-version: 8.0.x - name: Fetch native binaries shell: pwsh run: ./fetch.natives.ps1 - name: Run ${{ matrix.tfm }} tests run: dotnet test LibGit2Sharp.sln --configuration Release --framework ${{ matrix.tfm }} --logger "GitHubActions" /p:ExtraDefine=LEAKS_IDENTIFYING - test-linux: - name: Test / ${{ matrix.distro }} / ${{ matrix.arch }} / ${{ matrix.tfm }} - runs-on: ubuntu-22.04 - strategy: - matrix: - arch: [ amd64 ] - # arch: [ amd64, arm64 ] - distro: [ alpine.3.13, alpine.3.14, alpine.3.15, alpine.3.16, alpine.3.17, centos.7, centos.stream.8, debian.10, debian.11, fedora.36, ubuntu.18.04, ubuntu.20.04, ubuntu.22.04 ] - sdk: [ '6.0', '7.0' ] - exclude: - - distro: alpine.3.13 - sdk: '7.0' - - distro: alpine.3.14 - sdk: '7.0' - include: - - sdk: '6.0' - tfm: net6.0 - - sdk: '7.0' - tfm: net7.0 - fail-fast: false - steps: - - name: Checkout - uses: actions/checkout@v3.5.0 - with: - fetch-depth: 0 - - name: Fetch native binaries - # Fetch on the host (pwsh is preinstalled on ubuntu runners): native/ lives under $PWD, which - # is mounted into the test container as /app, so the dockerized dotnet test sees it. - shell: pwsh - run: ./fetch.natives.ps1 - - name: Setup QEMU - if: matrix.arch == 'arm64' - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset - - name: Run ${{ matrix.tfm }} tests - run: | - git_command="git config --global --add safe.directory /app" - test_command="dotnet test LibGit2Sharp.sln --configuration Release -p:TargetFrameworks=${{ matrix.tfm }} --logger "GitHubActions" -p:ExtraDefine=LEAKS_IDENTIFYING" - docker run -t --rm --platform linux/${{ matrix.arch }} -v "$PWD:/app" gittools/build-images:${{ matrix.distro }}-sdk-${{ matrix.sdk }} sh -c "$git_command && $test_command" From aa7c77aa46bac3a443320fa640a27244caaa6bdc Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Mon, 6 Jul 2026 17:45:56 +0300 Subject: [PATCH 04/12] Bump CI actions to v4 actions/upload-artifact v3 is deprecated and auto-fails runs; move checkout, setup-dotnet and upload-artifact to v4 (matching the nativebinaries workflows). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed30d8fca..645444538 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install .NET SDK - uses: actions/setup-dotnet@v3.0.3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Fetch native binaries @@ -30,7 +30,7 @@ jobs: - name: Build run: dotnet build LibGit2Sharp.sln --configuration Release - name: Upload packages - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4 with: name: NuGet packages path: bin/Packages/ @@ -51,11 +51,11 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install .NET SDK - uses: actions/setup-dotnet@v3.0.3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Fetch native binaries From 5b7c86702013ea2ffa7343ea3d56ae2468789909 Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Mon, 6 Jul 2026 17:59:59 +0300 Subject: [PATCH 05/12] Resolve the native library on macOS; skip network cert test in CI The DllImport resolver only probed runtimes//native/lib*.so on Linux, so on macOS the library was never found (the fork was previously Windows-only and never exercised macOS resolution) and every test failed with DllNotFoundException. Extend the probe to macOS (.dylib). Also exclude CanInspectCertificateOnClone from CI: it is an upstream network-integration test asserting GitHub's hardcoded SSH host-key MD5, which is unstable on CI runners and unrelated to native packaging. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 5 ++++- LibGit2Sharp/Core/NativeMethods.cs | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 645444538..43aece4ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,5 +62,8 @@ jobs: shell: pwsh run: ./fetch.natives.ps1 - name: Run ${{ matrix.tfm }} tests - run: dotnet test LibGit2Sharp.sln --configuration Release --framework ${{ matrix.tfm }} --logger "GitHubActions" /p:ExtraDefine=LEAKS_IDENTIFYING + # CanInspectCertificateOnClone is an upstream network-integration test that asserts GitHub's + # hardcoded SSH host-key MD5 fingerprint; it is unstable on CI (external host key, network) + # and does not exercise our native packaging, so it is excluded here. + run: dotnet test LibGit2Sharp.sln --configuration Release --framework ${{ matrix.tfm }} --logger "GitHubActions" /p:ExtraDefine=LEAKS_IDENTIFYING --filter "FullyQualifiedName!~CanInspectCertificateOnClone" diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index c452c7bab..c65f471d6 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -117,12 +117,14 @@ private static IntPtr ResolveDll(string libraryName, Assembly assembly, DllImpor return handle; } - // We carry a number of .so files for Linux which are linked against various - // libc/OpenSSL libraries. Try them out. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + // We carry the native library per-RID under 'runtimes//native/'. On Linux/macOS + // the default resolver above won't find it (it is not registered as a deps.json + // native asset), so probe those locations explicitly. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - // The libraries are located at 'runtimes//native/lib{libraryName}.so' - // The ends with the processor architecture. e.g. fedora-x64. + // The libraries are located at 'runtimes//native/lib{libraryName}.{so|dylib}' + // The ends with the processor architecture. e.g. linux-x64, osx-arm64. + string libraryExtension = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" : ".so"; string assemblyDirectory = Path.GetDirectoryName(typeof(NativeMethods).Assembly.Location); string processorArchitecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); string runtimesDirectory = Path.Combine(assemblyDirectory, "runtimes"); @@ -131,7 +133,7 @@ private static IntPtr ResolveDll(string libraryName, Assembly assembly, DllImpor { foreach (var runtimeFolder in Directory.GetDirectories(runtimesDirectory, $"*-{processorArchitecture}")) { - string libPath = Path.Combine(runtimeFolder, "native", $"lib{libraryName}.so"); + string libPath = Path.Combine(runtimeFolder, "native", $"lib{libraryName}{libraryExtension}"); if (NativeLibrary.TryLoad(libPath, out handle)) { From 3639efe6b5fc190e116862db0f7697a73b44595f Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Mon, 6 Jul 2026 18:05:30 +0300 Subject: [PATCH 06/12] Fix doubled runtimes path in packed nupkg PackagePath pointed at a folder (runtimes//native/), so NuGet re-appended each file's recursive path, packing natives at runtimes//native//native/. A consumer's loader probes runtimes//native/ and would not find them. Set PackagePath to the full target path including filename (mirroring Link), so NuGet places each file exactly once. Build output (via Link) was already correct, which is why the test runs passed. Co-Authored-By: Claude Opus 4.8 (1M context) --- Targets/NativeBinaries.props | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Targets/NativeBinaries.props b/Targets/NativeBinaries.props index 8c18ac33d..4d0a2d965 100644 --- a/Targets/NativeBinaries.props +++ b/Targets/NativeBinaries.props @@ -27,7 +27,9 @@ runtimes\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest true - runtimes\%(RecursiveDir) + + runtimes\%(RecursiveDir)%(Filename)%(Extension) false From 7b7fece44af33e810ece3ba4ad6540c41667c9cb Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Tue, 7 Jul 2026 10:33:30 +0300 Subject: [PATCH 07/12] Add release-automation skill and single-number version scheme Introduce the release-libgit2-natives skill (.claude/skills/): an interactive, cross-repo runbook plus the Update-Lockfile.ps1 atom for rolling new SHA256 pins through deps.lock.json and natives.lock.json, each hop gated by a human-reviewed PR. Re-include the skill directory in .gitignore (its name was caught by the [Rr]elease*/ build-output pattern). Collapse the package version to a single auto-incrementing X.Y.Z-v (e.g. 1.9.1-v22) in the AdjustVersions target: the -vN base tag is a permanent counting anchor, the MinVer height is the version, so releases need no manual version tagging. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/release-libgit2-natives/SKILL.md | 200 ++++++++++++++++++ .../Update-Lockfile.ps1 | 130 ++++++++++++ .gitignore | 4 + LibGit2Sharp/LibGit2Sharp.csproj | 10 +- 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/release-libgit2-natives/SKILL.md create mode 100644 .claude/skills/release-libgit2-natives/Update-Lockfile.ps1 diff --git a/.claude/skills/release-libgit2-natives/SKILL.md b/.claude/skills/release-libgit2-natives/SKILL.md new file mode 100644 index 000000000..9d8d6ca9b --- /dev/null +++ b/.claude/skills/release-libgit2-natives/SKILL.md @@ -0,0 +1,200 @@ +--- +name: release-libgit2-natives +description: >- + Runbook for cutting a new LibGit2Sharp.UiPath native release: bump the OpenSSL/libssh2 submodules, + rebuild the prebuilt deps, rebuild the multi-RID native archive, and roll the new SHA256 pins + through the two lockfiles up to the final NuGet publish. Use when asked to bump OpenSSL or libssh2, + refresh the native binaries, produce a new natives archive, or ship a new LibGit2Sharp.UiPath + version. Driven interactively — pause for human review at each hash-pin gate. +--- + +# Releasing LibGit2Sharp.UiPath native binaries + +This skill drives the cross-repo release chain by hand, **interactively**. There is deliberately no +CI auto-PR: every hop that moves a SHA256 pin ends at a **human review gate** — you show the diff, +confirm with the user, then commit. Hash-pinning is a security boundary; a freshly-built SHA must be +reviewed before the next stage is allowed to consume it. Never auto-merge, never skip a gate. + +## The two repos + +| Role | Path | GitHub | Default branch | +|------|------|--------|----------------| +| Managed library (this repo) | `.` | `UiPath/libgit2sharp` | `develop` | +| Native binaries (sibling) | `../libgit2sharp.nativebinaries` | `UiPath/libgit2sharp.nativebinaries` | `develop` | + +The native-binaries repo is a **sibling directory** next to this one. All paths below are relative to +this repo's root (the managed `libgit2sharp`). The atom script is at +`./.claude/skills/release-libgit2-natives/Update-Lockfile.ps1`. + +> **Branch note:** `develop` is the default/integration branch for **both** repos. Dispatch workflows +> against `develop` (`--ref develop`), or against a feature branch (`--ref `) to validate the +> whole chain before merging. + +## The chain (what produces what) + +``` +bump openssl/libssh2 submodule + deps/vcpkg.json (sibling repo) + │ + ▼ build-deps.yml (manual) + Release deps-openssl-_libssh2- → 6× deps-.{zip,tar.gz} + .sha256 + │ + ▼ GATE A: Update-Lockfile.ps1 → deps.lock.json (sibling repo) — review + branch + commit + PR + │ + ▼ build.yml (publish=true) + Release natives- → natives-.zip + .sha256 + │ + ▼ GATE B: Update-Lockfile.ps1 → natives.lock.json (this repo) — review + branch + commit + PR + │ + ▼ fetch.natives.ps1 + CI (ci.yml) build & test all 3 platforms + │ + ▼ (the very last step) publish LibGit2Sharp.UiPath nupkg to the uipath-internal feed +``` + +`build-deps.yml` runs **rarely** — only when the submodule versions change. Most releases that are +*just* a libgit2 rebuild start at `build.yml` and reuse the existing deps release. + +--- + +## Step 0 — Bump the deps (only when changing OpenSSL/libssh2) + +Skip this whole step if you are not changing the dep versions. The usual trigger is a new **OpenSSL** +patch (occasionally **libssh2**), often a CVE fix. Three things move in **lockstep**, or +`build.deps.ps1` throws on its version assertion: + +1. **The submodule tag** (the authoritative pin), in `../libgit2sharp.nativebinaries`: + ```bash + cd ../libgit2sharp.nativebinaries + git -C openssl fetch --tags --quiet + git -C openssl checkout openssl- # e.g. openssl-3.6.4 (libssh2 uses tag libssh2-) + git add openssl + ``` +2. **The `overrides` version** in `deps/vcpkg.json` — must equal the submodule version exactly. +3. **`builtin-baseline`** in `deps/vcpkg.json` — a vcpkg commit whose versions database actually + contains that OpenSSL/libssh2 version. **This is the real friction, not the SHA copying:** if + vcpkg's registry doesn't yet ship the exact patch at the current baseline, bump `builtin-baseline` + to a newer vcpkg commit that does (check `microsoft/vcpkg` history / `versions/o-/openssl.json`). + `build.deps.ps1` asserts `submodule == override == what-vcpkg-built` and fails loudly otherwise. + +Commit these on a branch in the sibling repo. **Gate:** show the user the submodule + vcpkg.json diff +and confirm before pushing. + +## Step 1 — Rebuild the deps (`build-deps.yml`) + +```bash +gh workflow run build-deps.yml --repo UiPath/libgit2sharp.nativebinaries --ref +# grab the run id, then: +gh run list --repo UiPath/libgit2sharp.nativebinaries --workflow build-deps.yml --limit 1 --json databaseId +gh run watch --repo UiPath/libgit2sharp.nativebinaries --exit-status +``` + +It fans out over 6 platforms and publishes Release **`deps-openssl-_libssh2-`** (the tag is +derived from the built versions, so it's predictable from `deps/vcpkg.json`). Each archive gets a +bare-hash `.sha256` sidecar. + +## Gate A — Roll the deps pins into `deps.lock.json` + +```bash +TAG=deps-openssl-_libssh2- +gh release download "$TAG" --repo UiPath/libgit2sharp.nativebinaries --pattern '*.sha256' --dir "$TMPDIR/depssha" +pwsh ./.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 \ + -LockfilePath ../libgit2sharp.nativebinaries/deps.lock.json -Tag "$TAG" -ShaDir "$TMPDIR/depssha" +``` + +**Human gate:** `git -C ../libgit2sharp.nativebinaries diff deps.lock.json`, show it, confirm, then in +the sibling repo **create a new branch, commit, push, and open a PR** (`gh pr create`) against +`develop`. Do **not** merge yourself and do **not** proceed until the user has reviewed and merged it — +the libgit2 build is about to trust these hashes. + +## Step 2 — Rebuild the native archive (`build.yml`, `publish=true`) + +```bash +gh workflow run build.yml --repo UiPath/libgit2sharp.nativebinaries --ref -f publish=true +gh run list --repo UiPath/libgit2sharp.nativebinaries --workflow build.yml --limit 1 --json databaseId +gh run watch --repo UiPath/libgit2sharp.nativebinaries --exit-status +``` + +It fetches + SHA256-verifies the deps (no OpenSSL/libssh2 compile), builds libgit2 for all 6 RIDs, +assembles `natives-.zip`, and — because `publish=true` (or on `develop`) — publishes Release +**`natives-`**. `` comes from **MinVer** on the nativebinaries repo, collapsed to a +single auto number `X.Y.Z-v` (base tag `1.9.1-v5` + height 21 → `1.9.1-v21`; see Reference). +Find the exact tag: + +```bash +gh release list --repo UiPath/libgit2sharp.nativebinaries --limit 5 # newest natives-* is yours +``` + +## Gate B — Roll the natives pin into `natives.lock.json` (this repo) + +```bash +NTAG=natives- +gh release download "$NTAG" --repo UiPath/libgit2sharp.nativebinaries --pattern '*.sha256' --dir "$TMPDIR/natsha" +pwsh ./.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 \ + -LockfilePath ./natives.lock.json -Tag "$NTAG" -ShaDir "$TMPDIR/natsha" +# prove it fetches + verifies before trusting the pin: +pwsh ./fetch.natives.ps1 -Force +``` + +`fetch.natives.ps1` fails hard on a SHA mismatch, so a green fetch confirms the pin. **Human gate:** +show the `natives.lock.json` diff, confirm, then **create a new branch, commit, push, and open a PR** +(`gh pr create`) against `develop`. Do **not** merge yourself — the PR's `ci.yml` run exercises +windows/ubuntu/macos, and the user reviews and merges. + +## Step 3 — Ship the managed package (the very last step) + +**Versioning — nothing to do.** The package version is `X.Y.Z-v` (e.g. `1.9.1-v22`), a single +auto-incrementing number. `` is MinVer's commit count since the base tag, surfaced as `v` +by the managed repo's `AdjustVersions` target and the nativebinaries `build.yml` "Resolve version" +step (both collapse MinVer's `.` into one `v`). Every develop commit yields the +next `v` — no manual version tagging. + +**Iron rule:** the existing `X.Y.Z-vN` tag (e.g. `1.9.1-v5`) is a **permanent counting anchor**. Do +**not** cut another `-vN` tag on the same `X.Y.Z` line — the height (hence `v`) would reset and the +package version would regress, which NuGet forbids (versions must rise). Cut a new base tag **only** +when the upstream base changes, once (e.g. `git tag 1.10.0-v0`), to start a fresh line. Same scheme +and anchor tag apply to the nativebinaries repo. + +**Publishing to the uipath-internal feed — NOT YET IMPLEMENTED.** This is the one remaining piece of +the pipeline. The intended design mirrors `build.yml`'s gating: on **`develop`** the managed CI +publishes the `LibGit2Sharp.UiPath` nupkg to the feed **automatically**; on a **PR** it publishes only +when a flag is active (otherwise it just produces the nupkg artifact). Until that job is built, +`ci.yml` produces the nupkg but does not push it anywhere. + +So a normal release needs no manual version or publish step: once the Gate B PR merges to `develop`, +CI builds `LibGit2Sharp.UiPath` at `X.Y.Z-v` and (once the publish job lands) ships it to the +feed. Only if the upstream base changed: `git tag X.Y.Z-v0` once, first. + +--- + +## Reference + +**Lockfile shapes** (both carry a `repo` field the atom reads to rebuild URLs): +- `deps.lock.json` (sibling repo): `{ repo, tag, platforms: { : { filename, url, sha256 } } }`, + 6 RIDs. Windows = `.zip` (dynamic DLLs); posix = `.tar.gz` (static `.a`, symlinks preserved). +- `natives.lock.json` (this repo): `{ repo, tag, filename, url, sha256 }`, one archive; `filename` is + always `.zip`. + +**MinVer** — both repos use **bare numeric base tags** (no `v` prefix) with +`--default-pre-release-identifiers preview.0`. MinVer emits `X.Y.Z-.` (e.g. +`1.9.1-v5.22`); both repos then **collapse it to `X.Y.Z-v`** (e.g. `1.9.1-v22`) — a single +monotonic auto number. The base tag (`1.9.1-v5`) is a permanent anchor; never re-tag the line (Step +3's iron rule). New upstream base → one fresh `X.Y.Z-v0` tag. + +**Release gating** — `build.yml`'s publish step is gated on `inputs.publish || github.ref == +refs/heads/develop`. PR/branch runs without `publish=true` produce only a workflow artifact (no stray +release). Always pass `-f publish=true` when you actually want the durable natives Release. + +**`build-deps.yml`** is manual (`workflow_dispatch`) only — never on push — so a rebuild can't +silently republish archives with fresh, unpinned SHA256s. + +**Sidecar formats differ** (the atom handles both): deps sidecars are a bare hash; the natives +sidecar is `sha filename` (sha256sum format). `Update-Lockfile.ps1` takes the first token either way. + +**Testing off a branch** — `gh workflow run … --ref ` dispatches the workflow as it exists on +that branch, so you can validate the whole chain before merging to `develop`. + +**Gotchas** +- The deps rebuild is gated on the version existing in vcpkg's registry at the pinned baseline — + budget time for a `builtin-baseline` bump, not just a submodule checkout. +- The libgit2 submodule is private; `build.yml` clones it via the `LIBGIT2_DEPLOY_KEY` secret + (read-only deploy key). Nothing to do locally unless you're building libgit2 yourself. +- `gh run watch` needs the run id; logs are only fully available once the whole run completes. diff --git a/.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 b/.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 new file mode 100644 index 000000000..4cb5f0fbb --- /dev/null +++ b/.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 @@ -0,0 +1,130 @@ +<# +.SYNOPSIS + Rewrites a LibGit2Sharp dependency lockfile (deps.lock.json or natives.lock.json) to point at a + new GitHub Release: sets the tag, recomputes each asset URL, and writes the SHA256(s). + + This is the single shared "atom" of the release chain. The release-libgit2-natives skill calls it + at both hand-off points (deps -> deps.lock.json, natives -> natives.lock.json). It never talks to + the network or to git; you hand it a tag and the SHA256 sidecar(s) the workflow already produced, + and it edits one JSON file. Review the diff and commit yourself. + +.DESCRIPTION + Two lockfile shapes are handled, auto-detected from the JSON: + + * multi-platform (deps.lock.json, in the sibling libgit2sharp.nativebinaries repo): has a + top-level "platforms" object. Each platform keeps its existing "filename" + (deps-.zip / .tar.gz), and its "url" + "sha256" are refreshed. A SHA256 must be + supplied for every platform present in the file (via -ShaDir), or it throws. + + * flat (natives.lock.json, in this repo): has a top-level "filename". The filename is derived + as ".zip" (the natives archive is always named after its tag), and "url" + "sha256" are + refreshed. + + The base repo ("UiPath/...") is read from the lockfile's own "repo" field, so URLs stay correct + without being passed in. SHA256 values come either from -Sha256 (single, flat lockfiles only) or + from -ShaDir, a directory of ".sha256" sidecars as published on the Release / uploaded + as workflow artifacts. Sidecars may be a bare hash (deps) or "sha filename" sha256sum format + (natives); the first whitespace-delimited token is taken as the hash. + +.PARAMETER LockfilePath + Path to the lockfile to rewrite (deps.lock.json or natives.lock.json). + +.PARAMETER Tag + The GitHub Release tag the assets live under (e.g. 'deps-openssl-3.6.3_libssh2-1.11.1' or + 'natives-1.9.1-v5.21'). + +.PARAMETER ShaDir + Directory containing '.sha256' sidecar files. Get them with: + gh release download --repo --pattern '*.sha256' --dir + Required for multi-platform lockfiles; optional for flat ones (use -Sha256 instead). + +.PARAMETER Sha256 + A single SHA256 hash, for flat (natives) lockfiles only. Convenient when you already have the hash + from the workflow output. Ignored for multi-platform lockfiles. + +.EXAMPLE + # Deps hand-off (6 platforms): download the sidecars, then rewrite the sibling repo's lockfile. + gh release download deps-openssl-3.6.3_libssh2-1.11.1 --repo UiPath/libgit2sharp.nativebinaries ` + --pattern '*.sha256' --dir $env:TEMP/depssha + ./.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 ` + -LockfilePath ../libgit2sharp.nativebinaries/deps.lock.json ` + -Tag deps-openssl-3.6.3_libssh2-1.11.1 -ShaDir $env:TEMP/depssha + +.EXAMPLE + # Natives hand-off (single archive): pass the hash straight through to this repo's lockfile. + ./.claude/skills/release-libgit2-natives/Update-Lockfile.ps1 -LockfilePath ./natives.lock.json ` + -Tag natives-1.9.1-v5.22 -Sha256 1a40ac67ac14b1e099b4d3a0823019e164fcde9211e3c40c95c6b355267f7440 +#> + +[CmdletBinding()] +Param( + [Parameter(Mandatory)][string]$LockfilePath, + [Parameter(Mandatory)][string]$Tag, + [string]$ShaDir = '', + [string]$Sha256 = '' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path $LockfilePath)) { throw "Lockfile not found: $LockfilePath" } + +$lock = Get-Content $LockfilePath -Raw | ConvertFrom-Json +$repo = "$($lock.repo)".Trim() +if (-not $repo) { throw "Lockfile '$LockfilePath' has no 'repo' field; cannot build asset URLs." } + +function Get-ShaFor($filename) { + # A single explicit hash always wins (flat lockfiles). Otherwise read the sidecar. + if ($Sha256) { return $Sha256.Trim().ToLower() } + if (-not $ShaDir) { + throw "No -Sha256 and no -ShaDir given; cannot resolve the SHA256 for '$filename'." + } + $sidecar = Join-Path $ShaDir "$filename.sha256" + if (-not (Test-Path $sidecar)) { + throw "SHA256 sidecar not found for '$filename' (looked for '$sidecar'). " + + "Did 'gh release download --pattern *.sha256' fetch it?" + } + # Sidecars are either a bare hash (deps) or 'sha filename' (natives); take the first token. + $raw = (Get-Content $sidecar -Raw).Trim() + $hash = ($raw -split '\s+')[0].ToLower() + if ($hash -notmatch '^[0-9a-f]{64}$') { + throw "Sidecar '$sidecar' did not contain a 64-char SHA256 (got '$hash')." + } + return $hash +} + +function New-AssetUrl($filename) { + return "https://github.com/$repo/releases/download/$Tag/$filename" +} + +$lock.tag = $Tag + +if ($lock.PSObject.Properties.Name -contains 'platforms') { + # --- multi-platform (deps.lock.json) ------------------------------------------------------ + if (-not $ShaDir) { throw "Multi-platform lockfile needs -ShaDir (one .sha256 per platform)." } + foreach ($p in $lock.platforms.PSObject.Properties) { + $entry = $p.Value + $filename = "$($entry.filename)".Trim() + if (-not $filename) { throw "Platform '$($p.Name)' has no 'filename' in the lockfile." } + $entry.url = New-AssetUrl $filename + $entry.sha256 = Get-ShaFor $filename + Write-Host " $($p.Name): $($entry.sha256)" + } +} +elseif ($lock.PSObject.Properties.Name -contains 'filename') { + # --- flat (natives.lock.json) ------------------------------------------------------------- + # The natives archive is always named after its tag. + $filename = "$Tag.zip" + $lock.filename = $filename + $lock.url = New-AssetUrl $filename + $lock.sha256 = Get-ShaFor $filename + Write-Host " ${filename}: $($lock.sha256)" +} +else { + throw "Unrecognised lockfile shape (no 'platforms' and no 'filename'): $LockfilePath" +} + +# Depth covers the nested platforms object; pwsh 7 pretty-prints and does not escape '/'. The first +# rewrite may reformat the file once; subsequent runs touch only tag/url/sha256 lines. +$lock | ConvertTo-Json -Depth 10 | Set-Content -Path $LockfilePath -Encoding utf8 +Write-Host "==> Updated $LockfilePath -> tag $Tag" diff --git a/.gitignore b/.gitignore index ef28b1d0b..b7af7ab39 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ worktree.playlist # Prebuilt native binaries fetched by fetch.natives.ps1 (pinned in natives.lock.json) /native/ + +# The release-libgit2-natives Claude skill: its directory name is caught by the [Rr]elease*/ +# build-output pattern above, so re-include it explicitly to keep the skill tracked. +!.claude/skills/release-libgit2-natives/ diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 41923db0a..0a9debb65 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -52,8 +52,16 @@ $(MinVerMajor).$(MinVerMinor).0.0 + + <_VersionHeight>0 + <_VersionHeight Condition="$(MinVerPreRelease.Contains('.'))">$(MinVerPreRelease.Substring($([MSBuild]::Add($(MinVerPreRelease.LastIndexOf('.')), 1)))) $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) - $(PackageVersion)-$(MinVerPreRelease) + $(PackageVersion)-v$(_VersionHeight) From 331ec69148d506cb26a0196e4f382c4f7707d46f Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Tue, 7 Jul 2026 10:48:02 +0300 Subject: [PATCH 08/12] Rework skill Step 3: interactive az/nuget publish to the internal feed Publishing LibGit2Sharp.UiPath to the UiPath-Internal Azure DevOps feed is done interactively from the release skill (no CI publish job), matching the PR-gated, human-driven shape of the rest of the chain. Recommended auth is a short-lived Azure DevOps token minted via `az account get-access-token` (no stored PAT); the Azure Artifacts credential provider is the fallback. Also correct the gates to review + branch + commit + PR, drop the stale feature-branch names, and fix version-scheme examples. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/release-libgit2-natives/SKILL.md | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/.claude/skills/release-libgit2-natives/SKILL.md b/.claude/skills/release-libgit2-natives/SKILL.md index 9d8d6ca9b..bf2875d6f 100644 --- a/.claude/skills/release-libgit2-natives/SKILL.md +++ b/.claude/skills/release-libgit2-natives/SKILL.md @@ -153,15 +153,31 @@ package version would regress, which NuGet forbids (versions must rise). Cut a n when the upstream base changes, once (e.g. `git tag 1.10.0-v0`), to start a fresh line. Same scheme and anchor tag apply to the nativebinaries repo. -**Publishing to the uipath-internal feed — NOT YET IMPLEMENTED.** This is the one remaining piece of -the pipeline. The intended design mirrors `build.yml`'s gating: on **`develop`** the managed CI -publishes the `LibGit2Sharp.UiPath` nupkg to the feed **automatically**; on a **PR** it publishes only -when a flag is active (otherwise it just produces the nupkg artifact). Until that job is built, -`ci.yml` produces the nupkg but does not push it anywhere. - -So a normal release needs no manual version or publish step: once the Gate B PR merges to `develop`, -CI builds `LibGit2Sharp.UiPath` at `X.Y.Z-v` and (once the publish job lands) ships it to the -feed. Only if the upstream base changed: `git tag X.Y.Z-v0` once, first. +**Publishing to the uipath-internal feed — done interactively from here** (like the gates: there is +no CI publish job). Once the Gate B PR is merged to `develop` and CI is green, get the +`LibGit2Sharp.UiPath` nupkg — download the managed CI's **NuGet packages** artifact, or build locally +(`dotnet build -c Release` emits it under `bin/Packages/`, via `GeneratePackageOnBuild`). + +**Recommended — mint a short-lived Azure DevOps token via the Azure CLI** (no stored PAT, reuses your +`az login` SSO): + +```bash +FEED=https://pkgs.dev.azure.com/uipath/Public.Feeds/_packaging/UiPath-Internal/nuget/v3/index.json +# 499b84ac-... is the well-known Azure DevOps resource id +TOKEN=$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv) +dotnet nuget add source "$FEED" --name uipath-internal 2>/dev/null || true +NuGetPackageSourceCredentials_uipath-internal="Username=az;Password=$TOKEN" \ + dotnet nuget push ".nupkg" --source uipath-internal --api-key az --skip-duplicate +``` + +**Fallback** — if the Azure Artifacts credential provider is already configured on the machine, just: +`nuget push -src "$FEED" -ApiKey AzureDevops -SkipDuplicate`. + +`--api-key`/`-ApiKey` is a required-but-ignored dummy (auth is the token / credential provider, not the +key). `--skip-duplicate` keeps it idempotent — and matters here: the feed may already hold a +manually-published `1.9.1-v5`, so the emitted `X.Y.Z-v` must **exceed** the highest version +already on the feed (see the version-collision caveat). Human-run step — confirm the exact version with +the user before pushing. Only if the upstream base changed: `git tag X.Y.Z-v0` once, first. --- From 2e9927d446af5b085ed569a3bba439d3b34fcf14 Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Tue, 7 Jul 2026 10:59:47 +0300 Subject: [PATCH 09/12] Retarget managed lib to net6.0 via NetCoreVersion property Drive the .NET target from a single NetCoreVersion property in Directory.Build.props (consumed as net$(NetCoreVersion) by the library, tests, and both NativeLibraryLoadTestApps) and set it to net6.0, so the LibGit2Sharp.UiPath package stays consumable by net6.0/7.0/8.0 apps instead of requiring net8.0. Bumping the one property retargets all projects. CI mirrors the target by hand (a workflow matrix can't read an MSBuild property): matrix tfm is net6.0, and setup-dotnet now installs the 6.0.x runtime alongside 8.0.x so the tests can run on net6.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 12 +++++++++--- Directory.Build.props | 4 ++++ LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj | 2 +- LibGit2Sharp/LibGit2Sharp.csproj | 2 +- .../x64/NativeLibraryLoadTestApp.x64.csproj | 2 +- .../x86/NativeLibraryLoadTestApp.x86.csproj | 2 +- 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43aece4ad..15731597f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,9 @@ jobs: - name: Install .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: | + 6.0.x + 8.0.x - name: Fetch native binaries # Downloads + SHA256-verifies the prebuilt native payload pinned in natives.lock.json into # native/ (replaces the old NativeBinaries.UiPath PackageReference). pwsh ships on all runners. @@ -47,7 +49,9 @@ jobs: # dropped: it pinned .NET 6/7 SDK images and included alpine/musl, which our glibc-linked # linux natives can't run anyway. os: [ windows-latest, ubuntu-latest, macos-14 ] - tfm: [ net8.0 ] + # Mirror NetCoreVersion in Directory.Build.props (net6.0). A workflow matrix can't read an + # MSBuild property, so keep the two in sync by hand when bumping the target framework. + tfm: [ net6.0 ] fail-fast: false steps: - name: Checkout @@ -57,7 +61,9 @@ jobs: - name: Install .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: | + 6.0.x + 8.0.x - name: Fetch native binaries shell: pwsh run: ./fetch.natives.ps1 diff --git a/Directory.Build.props b/Directory.Build.props index 10a527853..050ed7769 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,10 @@ + + 6.0 true $(MSBuildThisFileDirectory)bin\$(MSBuildProjectName)\$(Configuration)\ $(MSBuildThisFileDirectory)obj\$(MSBuildProjectName)\ diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index c90aaa8e9..601217ff5 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net$(NetCoreVersion) latest diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 0a9debb65..7ee9e0064 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -1,7 +1,7 @@  - net8.0 + net$(NetCoreVersion) true LibGit2Sharp brings all the might and speed of libgit2, a native Git implementation, to the managed world of .NET LibGit2Sharp contributors diff --git a/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj b/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj index c9682009f..a59cef4c9 100644 --- a/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj +++ b/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net$(NetCoreVersion) x64 diff --git a/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj b/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj index 841181bc1..bbab0a08f 100644 --- a/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj +++ b/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net$(NetCoreVersion) x86 From fe981184d948a99ddd23a0cac2c9ab13c1e4e8a5 Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Tue, 7 Jul 2026 11:47:11 +0300 Subject: [PATCH 10/12] Version as v, not v Resolve PackageVersion as the base tag's epoch plus the MinVer height (e.g. tag 1.9.1-v5 + height 9 -> 1.9.1-v14), matching the nativebinaries repo. The number now continues from the tag instead of restarting at the height; on the tag itself the height is 0, so the number equals the tag. Co-Authored-By: Claude Opus 4.8 (1M context) --- LibGit2Sharp/LibGit2Sharp.csproj | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 7ee9e0064..5c931d6ae 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -52,16 +52,18 @@ $(MinVerMajor).$(MinVerMinor).0.0 - - <_VersionHeight>0 - <_VersionHeight Condition="$(MinVerPreRelease.Contains('.'))">$(MinVerPreRelease.Substring($([MSBuild]::Add($(MinVerPreRelease.LastIndexOf('.')), 1)))) + + <_PreNoV>$(MinVerPreRelease.TrimStart('v')) + <_Epoch Condition="$(_PreNoV.Contains('.'))">$(_PreNoV.Substring(0, $(_PreNoV.IndexOf('.')))) + <_Epoch Condition="!$(_PreNoV.Contains('.'))">$(_PreNoV) + <_Height>0 + <_Height Condition="$(_PreNoV.Contains('.'))">$(_PreNoV.Substring($([MSBuild]::Add($(_PreNoV.IndexOf('.')), 1)))) $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) - $(PackageVersion)-v$(_VersionHeight) + $(PackageVersion)-v$([MSBuild]::Add($(_Epoch), $(_Height))) From 0fdcca5a9794e726aa98b347a8b5ce7877ba9e5b Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Tue, 7 Jul 2026 11:54:04 +0300 Subject: [PATCH 11/12] Pin native binaries to natives-1.9.1-v7 Point natives.lock.json at the natives-1.9.1-v7 release (epoch+height versioning) published from libgit2sharp.nativebinaries develop. SHA256 verified via fetch.natives.ps1. Co-Authored-By: Claude Opus 4.8 (1M context) --- natives.lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/natives.lock.json b/natives.lock.json index 9e45de8e3..ad289940b 100644 --- a/natives.lock.json +++ b/natives.lock.json @@ -1,8 +1,8 @@ { "$comment": "Pins the prebuilt LibGit2Sharp native payload (all RIDs) produced by the UiPath/libgit2sharp.nativebinaries 'build' workflow. fetch.natives.ps1 downloads this archive and fails hard unless its SHA256 matches. To bump: run that workflow with publish=true, then copy the new tag/url/sha256 (printed by the workflow and in the release's .sha256 sidecar) here.", "repo": "UiPath/libgit2sharp.nativebinaries", - "tag": "natives-1.9.1-v5.21", - "filename": "natives-1.9.1-v5.21.zip", - "url": "https://github.com/UiPath/libgit2sharp.nativebinaries/releases/download/natives-1.9.1-v5.21/natives-1.9.1-v5.21.zip", - "sha256": "1a40ac67ac14b1e099b4d3a0823019e164fcde9211e3c40c95c6b355267f7440" + "tag": "natives-1.9.1-v7", + "filename": "natives-1.9.1-v7.zip", + "url": "https://github.com/UiPath/libgit2sharp.nativebinaries/releases/download/natives-1.9.1-v7/natives-1.9.1-v7.zip", + "sha256": "deb162eda061d6695e785f7759c02bf98d19fa39eccdd9ea77bddfb22260f7dd" } From 0e24c428499b7efe2b4384d802874fe541c8a407 Mon Sep 17 00:00:00 2001 From: Andrei Balint Date: Tue, 7 Jul 2026 11:57:14 +0300 Subject: [PATCH 12/12] docs: skill version scheme is v Update SKILL.md to describe the version as N = base tag epoch + MinVer height (e.g. 1.9.1-v5 + height 1 -> v6), replacing the earlier height-only wording. Note that re-tagging at the current v is seamless, and record why height-only produced v1 after a merge (MinVer height is the shortest graph distance to the tag). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/release-libgit2-natives/SKILL.md | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/.claude/skills/release-libgit2-natives/SKILL.md b/.claude/skills/release-libgit2-natives/SKILL.md index bf2875d6f..bad2ac7b6 100644 --- a/.claude/skills/release-libgit2-natives/SKILL.md +++ b/.claude/skills/release-libgit2-natives/SKILL.md @@ -115,8 +115,8 @@ gh run watch --repo UiPath/libgit2sharp.nativebinaries --exit-status It fetches + SHA256-verifies the deps (no OpenSSL/libssh2 compile), builds libgit2 for all 6 RIDs, assembles `natives-.zip`, and — because `publish=true` (or on `develop`) — publishes Release -**`natives-`**. `` comes from **MinVer** on the nativebinaries repo, collapsed to a -single auto number `X.Y.Z-v` (base tag `1.9.1-v5` + height 21 → `1.9.1-v21`; see Reference). +**`natives-`**. `` comes from **MinVer** on the nativebinaries repo, reformatted to a +single auto number `X.Y.Z-v` (base tag `1.9.1-v5` + height 1 → `1.9.1-v6`; see Reference). Find the exact tag: ```bash @@ -141,17 +141,17 @@ windows/ubuntu/macos, and the user reviews and merges. ## Step 3 — Ship the managed package (the very last step) -**Versioning — nothing to do.** The package version is `X.Y.Z-v` (e.g. `1.9.1-v22`), a single -auto-incrementing number. `` is MinVer's commit count since the base tag, surfaced as `v` -by the managed repo's `AdjustVersions` target and the nativebinaries `build.yml` "Resolve version" -step (both collapse MinVer's `.` into one `v`). Every develop commit yields the -next `v` — no manual version tagging. +**Versioning — nothing to do.** The package version is `X.Y.Z-v` (e.g. `1.9.1-v6`), a single auto +number where **`N = the base tag's epoch + the MinVer height`**. The base tag `1.9.1-v5` sets the epoch +(5) and each commit past it increments N (v6, v7, …), computed by the managed repo's `AdjustVersions` +target and the nativebinaries `build.yml` "Resolve version" step. Every develop commit yields the next +`v` — no manual version tagging. -**Iron rule:** the existing `X.Y.Z-vN` tag (e.g. `1.9.1-v5`) is a **permanent counting anchor**. Do -**not** cut another `-vN` tag on the same `X.Y.Z` line — the height (hence `v`) would reset and the -package version would regress, which NuGet forbids (versions must rise). Cut a new base tag **only** -when the upstream base changes, once (e.g. `git tag 1.10.0-v0`), to start a fresh line. Same scheme -and anchor tag apply to the nativebinaries repo. +**Re-tagging is seamless** (unlike a height-only scheme). Because `N = epoch + height`, on the tag +commit itself height is 0, so `v` equals the tag — cutting a new tag at the **current** `v` (e.g. +`git tag 1.9.1-v20` when the version already reads `v20`) just re-bases the epoch with no jump, and the +number keeps climbing. Only pitfall: never tag **below** the current `v`, or the version goes +backwards (NuGet forbids). New upstream base → `git tag X.Y.Z-v0`. Same scheme in the nativebinaries repo. **Publishing to the uipath-internal feed — done interactively from here** (like the gates: there is no CI publish job). Once the Gate B PR is merged to `develop` and CI is green, get the @@ -175,7 +175,7 @@ NuGetPackageSourceCredentials_uipath-internal="Username=az;Password=$TOKEN" \ `--api-key`/`-ApiKey` is a required-but-ignored dummy (auth is the token / credential provider, not the key). `--skip-duplicate` keeps it idempotent — and matters here: the feed may already hold a -manually-published `1.9.1-v5`, so the emitted `X.Y.Z-v` must **exceed** the highest version +manually-published `1.9.1-v5`, so the emitted `X.Y.Z-v` must **exceed** the highest version already on the feed (see the version-collision caveat). Human-run step — confirm the exact version with the user before pushing. Only if the upstream base changed: `git tag X.Y.Z-v0` once, first. @@ -189,11 +189,12 @@ the user before pushing. Only if the upstream base changed: `git tag X.Y.Z-v0` o - `natives.lock.json` (this repo): `{ repo, tag, filename, url, sha256 }`, one archive; `filename` is always `.zip`. -**MinVer** — both repos use **bare numeric base tags** (no `v` prefix) with -`--default-pre-release-identifiers preview.0`. MinVer emits `X.Y.Z-.` (e.g. -`1.9.1-v5.22`); both repos then **collapse it to `X.Y.Z-v`** (e.g. `1.9.1-v22`) — a single -monotonic auto number. The base tag (`1.9.1-v5`) is a permanent anchor; never re-tag the line (Step -3's iron rule). New upstream base → one fresh `X.Y.Z-v0` tag. +**MinVer** — both repos use `X.Y.Z-vN` base tags with `--default-pre-release-identifiers preview.0`. +MinVer emits `X.Y.Z-v.` (e.g. `1.9.1-v5.1`); both repos reformat to +**`X.Y.Z-v`** (e.g. `1.9.1-v6`) — a single auto number that continues from the tag. +MinVer's height is the *shortest graph distance* to the tag, so it stays small after a merge (the base +tag often lands on the merge commit's first parent — that's why a naive height-only scheme wrongly +produced `v1`); epoch+height keeps climbing regardless. New upstream base → `git tag X.Y.Z-v0`. **Release gating** — `build.yml`'s publish step is gated on `inputs.publish || github.ref == refs/heads/develop`. PR/branch runs without `publish=true` produce only a workflow artifact (no stray