From c483a48f397c4f0984d1ebbab650ab1e2d63e745 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:32:48 +0000 Subject: [PATCH 1/8] Initial plan From 62df16f1bd70eb3c9adb87a2bd4ce381e95149d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:40:17 +0000 Subject: [PATCH 2/8] feat: support nuget.org Trusted Publishing via GitHub Actions OIDC - Wire up OIDC token exchange in PublishNuget.cs as primary publish path - Add catch-all Exception handler and use Warning for non-fatal OIDC errors - Add id-token: write permission to _publish.yml publish job - Remove 1Password NuGet creds step and NUGET_API_KEY env var - Document Trusted Publishing in CONTRIBUTING.md Co-authored-by: arturcic <1760506+arturcic@users.noreply.github.com> --- .github/workflows/_publish.yml | 12 ++++-------- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ build/publish/Tasks/PublishNuget.cs | 25 +++++++++++++++++++------ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/.github/workflows/_publish.yml b/.github/workflows/_publish.yml index f6cad14a2b..ee3422c941 100644 --- a/.github/workflows/_publish.yml +++ b/.github/workflows/_publish.yml @@ -13,6 +13,10 @@ jobs: publish: name: ${{ matrix.taskName }} runs-on: windows-2025-vs2026 + permissions: + id-token: write + packages: write + contents: read strategy: fail-fast: false matrix: @@ -33,13 +37,6 @@ jobs: name: nuget path: ${{ github.workspace }}/artifacts/packages/nuget - - name: Load NuGet credentials - id: nuget-creds - if: inputs.publish_packages - uses: gittools/cicd/nuget-creds@v1 - with: - op_service_account_token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - - name: Load Chocolatey credentials id: choco-creds if: inputs.publish_packages @@ -53,5 +50,4 @@ jobs: run: dotnet run/publish.dll --target=Publish${{ matrix.taskName }} env: GITHUB_TOKEN: ${{ github.token }} - NUGET_API_KEY: ${{ steps.nuget-creds.outputs.nuget_api_key }} CHOCOLATEY_API_KEY: ${{ steps.choco-creds.outputs.choco_api_key }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31ecfb3096..5e6a3d32e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,6 +142,31 @@ We use Cake for our build and deployment process. The way the release process is and other distribution channels. 9. The issues and pull requests will get updated with message specifying in which release it was included. +### NuGet Trusted Publishing + +NuGet packages are published to nuget.org using [Trusted Publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing), +which replaces long-lived API keys with short-lived, identity-based tokens issued by GitHub Actions OIDC. + +**How it works:** + +1. The publish workflow requests a GitHub OIDC token scoped to `https://www.nuget.org`. +2. That token is exchanged with the nuget.org token service for a short-lived API key. +3. Packages are pushed using that short-lived key — no long-lived secret is stored or rotated. + +**One-time setup (already done for this repository):** + +The `GitTools` GitHub organisation and the `GitVersion` repository must be registered as a trusted publisher on +nuget.org for every GitVersion package. This is configured in the nuget.org package settings under +*Publishing* → *Trusted Publishers*. + +**Verification and troubleshooting:** + +- If the OIDC token exchange fails the workflow falls back to a static `NUGET_API_KEY` environment variable + (not set in normal CI runs). Check the "Publishing to nuget.org" log group for error details. +- The publish job requires `id-token: write` permission, which is declared in `.github/workflows/_publish.yml`. +- If a package fails to publish with a permissions error, verify that nuget.org Trusted Publishing is configured + for that package and that the repository/environment names match. + ## Code Style In order to apply the code style defined by by the `.editorconfig` file you can use [`dotnet-format`](https://github.com/dotnet/format). diff --git a/build/publish/Tasks/PublishNuget.cs b/build/publish/Tasks/PublishNuget.cs index f084a3a476..86899211e9 100644 --- a/build/publish/Tasks/PublishNuget.cs +++ b/build/publish/Tasks/PublishNuget.cs @@ -43,7 +43,17 @@ public override async Task RunAsync(BuildContext context) if (context.IsTaggedRelease || context.IsTaggedPreRelease) { context.StartGroup("Publishing to Nuget.org"); - var apiKey = context.Credentials?.Nuget?.ApiKey; + + // Prefer Trusted Publishing via OIDC token exchange (no long-lived API key required) + var apiKey = await GetNugetApiKey(context); + + // Fall back to a static API key when OIDC is not available + if (string.IsNullOrEmpty(apiKey)) + { + context.Information("OIDC token exchange unavailable; falling back to static NuGet API key."); + apiKey = context.Credentials?.Nuget?.ApiKey; + } + if (string.IsNullOrEmpty(apiKey)) { throw new InvalidOperationException("Could not resolve NuGet org API key."); @@ -52,8 +62,6 @@ public override async Task RunAsync(BuildContext context) PublishToNugetRepo(context, apiKey, Constants.NugetOrgUrl); context.EndGroup(); } - - await Task.CompletedTask; } private static void PublishToNugetRepo(BuildContext context, string apiKey, string apiUrl) @@ -85,17 +93,22 @@ private static void PublishToNugetRepo(BuildContext context, string apiKey, stri } catch (HttpRequestException ex) { - context.Error($"Network error while retrieving NuGet API key: {ex.Message}"); + context.Warning($"Network error while retrieving NuGet API key via OIDC: {ex.Message}"); return null; } catch (InvalidOperationException ex) { - context.Error($"Invalid operation while retrieving NuGet API key: {ex.Message}"); + context.Warning($"OIDC not available for NuGet API key retrieval: {ex.Message}"); return null; } catch (JsonException ex) { - context.Error($"JSON parsing error while retrieving NuGet API key: {ex.Message}"); + context.Warning($"JSON parsing error while retrieving NuGet API key via OIDC: {ex.Message}"); + return null; + } + catch (Exception ex) + { + context.Warning($"Unexpected error while retrieving NuGet API key via OIDC: {ex.Message}"); return null; } } From 22980a290825f0276b8fbb4a1a77d2e16adb0a38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:54:03 +0000 Subject: [PATCH 3/8] fix: restore NuGet creds step and NUGET_API_KEY env var for fallback Without NUGET_API_KEY in the env, the fallback from OIDC to static API key in PublishNuget.cs was dead code. Restore the nuget-creds step and the env var so the fallback path is functional. Co-authored-by: arturcic <1760506+arturcic@users.noreply.github.com> --- .github/workflows/_publish.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/_publish.yml b/.github/workflows/_publish.yml index ee3422c941..b94e46b468 100644 --- a/.github/workflows/_publish.yml +++ b/.github/workflows/_publish.yml @@ -37,6 +37,13 @@ jobs: name: nuget path: ${{ github.workspace }}/artifacts/packages/nuget + - name: Load NuGet credentials + id: nuget-creds + if: inputs.publish_packages + uses: gittools/cicd/nuget-creds@v1 + with: + op_service_account_token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + - name: Load Chocolatey credentials id: choco-creds if: inputs.publish_packages @@ -50,4 +57,5 @@ jobs: run: dotnet run/publish.dll --target=Publish${{ matrix.taskName }} env: GITHUB_TOKEN: ${{ github.token }} + NUGET_API_KEY: ${{ steps.nuget-creds.outputs.nuget_api_key }} CHOCOLATEY_API_KEY: ${{ steps.choco-creds.outputs.choco_api_key }} From 74227d4819694fa8dcfcf698073fc788c786a1be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:56:52 +0000 Subject: [PATCH 4/8] docs: expand nuget.org Trusted Publishing setup instructions in CONTRIBUTING.md Add step-by-step setup guide covering: - Enumerated list of the three packages that need configuration - Numbered steps to navigate nuget.org package settings - Lookup table with exact field values (owner, repo, workflow file, environment) - Warning note on exact-match OIDC claim requirement - Updated troubleshooting to mention 1Password fallback explicitly Co-authored-by: arturcic <1760506+arturcic@users.noreply.github.com> --- CONTRIBUTING.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e6a3d32e6..55bdfd9a1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,19 +153,44 @@ which replaces long-lived API keys with short-lived, identity-based tokens issue 2. That token is exchanged with the nuget.org token service for a short-lived API key. 3. Packages are pushed using that short-lived key — no long-lived secret is stored or rotated. -**One-time setup (already done for this repository):** +**One-time setup (per package on nuget.org):** -The `GitTools` GitHub organisation and the `GitVersion` repository must be registered as a trusted publisher on -nuget.org for every GitVersion package. This is configured in the nuget.org package settings under -*Publishing* → *Trusted Publishers*. +Each GitVersion package on nuget.org must have the `GitTools/GitVersion` repository registered as a Trusted +Publisher. Repeat the following steps for every package listed below: + +- [`GitVersion.Tool`](https://www.nuget.org/packages/GitVersion.Tool) +- [`GitVersion.MsBuild`](https://www.nuget.org/packages/GitVersion.MsBuild) +- [`GitVersion.Core`](https://www.nuget.org/packages/GitVersion.Core) + +Steps to register a Trusted Publisher for a package: + +1. Sign in to [nuget.org](https://www.nuget.org) as a package owner. +2. Navigate to the package page and click **Manage package**. +3. Open the **Settings** tab and scroll to the **Trusted Publishers** section. +4. Click **Add trusted publisher** and fill in the following fields: + + | Field | Value | + |---|---| + | **Publisher type** | GitHub Actions | + | **Owner** | `GitTools` | + | **Repository** | `GitVersion` | + | **Workflow file name** | `ci.yml` | + | **Environment** | *(leave blank)* | + +5. Click **Add** to save the trusted publisher entry. + +> **Note:** nuget.org will only issue a short-lived key to a workflow run whose OIDC claims match *all* of the +> registered fields exactly. A mismatch on any field (for example a different workflow file name) will cause the +> token exchange to fail and the publish step will fall back to the static `NUGET_API_KEY`. **Verification and troubleshooting:** - If the OIDC token exchange fails the workflow falls back to a static `NUGET_API_KEY` environment variable - (not set in normal CI runs). Check the "Publishing to nuget.org" log group for error details. + loaded from 1Password via the `gittools/cicd/nuget-creds@v1` action. Check the "Publishing to nuget.org" log + group for error details. - The publish job requires `id-token: write` permission, which is declared in `.github/workflows/_publish.yml`. - If a package fails to publish with a permissions error, verify that nuget.org Trusted Publishing is configured - for that package and that the repository/environment names match. + for that package and that the owner, repository, and workflow file name match exactly. ## Code Style From a1bc95e1c79f6e2b3998d83e92f4ee7180bee95f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:06:24 +0000 Subject: [PATCH 5/8] docs: correct nuget.org Trusted Publishing setup - one-time per repo, not per package Co-authored-by: arturcic <1760506+arturcic@users.noreply.github.com> --- CONTRIBUTING.md | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55bdfd9a1c..6800c9d272 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,21 +153,16 @@ which replaces long-lived API keys with short-lived, identity-based tokens issue 2. That token is exchanged with the nuget.org token service for a short-lived API key. 3. Packages are pushed using that short-lived key — no long-lived secret is stored or rotated. -**One-time setup (per package on nuget.org):** +**One-time setup on nuget.org:** -Each GitVersion package on nuget.org must have the `GitTools/GitVersion` repository registered as a Trusted -Publisher. Repeat the following steps for every package listed below: - -- [`GitVersion.Tool`](https://www.nuget.org/packages/GitVersion.Tool) -- [`GitVersion.MsBuild`](https://www.nuget.org/packages/GitVersion.MsBuild) -- [`GitVersion.Core`](https://www.nuget.org/packages/GitVersion.Core) - -Steps to register a Trusted Publisher for a package: +Trusted Publishing is configured once for the repository and workflow — not per package. A single trusted +publisher entry covers every package pushed by the same workflow run. 1. Sign in to [nuget.org](https://www.nuget.org) as a package owner. -2. Navigate to the package page and click **Manage package**. -3. Open the **Settings** tab and scroll to the **Trusted Publishers** section. -4. Click **Add trusted publisher** and fill in the following fields: +2. Go to **Account settings** → **Trusted Publishers** (or navigate to any of the + [GitVersion packages](https://www.nuget.org/profiles/GitTools) and open **Manage package** → **Settings** → + **Trusted Publishers**). +3. Click **Add trusted publisher** and fill in the following fields: | Field | Value | |---|---| @@ -177,11 +172,11 @@ Steps to register a Trusted Publisher for a package: | **Workflow file name** | `ci.yml` | | **Environment** | *(leave blank)* | -5. Click **Add** to save the trusted publisher entry. +4. Click **Add** to save the entry. -> **Note:** nuget.org will only issue a short-lived key to a workflow run whose OIDC claims match *all* of the -> registered fields exactly. A mismatch on any field (for example a different workflow file name) will cause the -> token exchange to fail and the publish step will fall back to the static `NUGET_API_KEY`. +> **Note:** nuget.org will only issue a short-lived key when the OIDC claims from the workflow run match *all* +> registered fields exactly. A mismatch on any field (e.g. wrong workflow file name) will cause the token +> exchange to fail and the publish step will fall back to the static `NUGET_API_KEY`. **Verification and troubleshooting:** @@ -190,7 +185,7 @@ Steps to register a Trusted Publisher for a package: group for error details. - The publish job requires `id-token: write` permission, which is declared in `.github/workflows/_publish.yml`. - If a package fails to publish with a permissions error, verify that nuget.org Trusted Publishing is configured - for that package and that the owner, repository, and workflow file name match exactly. + and that the owner, repository, and workflow file name match exactly. ## Code Style From 494021c28114d22de84543f70b2f167f3db73677 Mon Sep 17 00:00:00 2001 From: Artur Date: Wed, 18 Mar 2026 20:20:14 +0100 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6800c9d272..c3df7f8d36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,7 +181,7 @@ publisher entry covers every package pushed by the same workflow run. **Verification and troubleshooting:** - If the OIDC token exchange fails the workflow falls back to a static `NUGET_API_KEY` environment variable - loaded from 1Password via the `gittools/cicd/nuget-creds@v1` action. Check the "Publishing to nuget.org" log + loaded from 1Password via the `gittools/cicd/nuget-creds@v1` action. Check the "Publishing to Nuget.org" log group for error details. - The publish job requires `id-token: write` permission, which is declared in `.github/workflows/_publish.yml`. - If a package fails to publish with a permissions error, verify that nuget.org Trusted Publishing is configured From 12f1b78dc9d4ebb72f89caa2eaaf5a74a5928831 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:26:05 +0000 Subject: [PATCH 7/8] fix: format CONTRIBUTING.md table and add missing System.Text using directive Co-authored-by: arturcic <1760506+arturcic@users.noreply.github.com> --- CONTRIBUTING.md | 14 +++++++------- build/publish/Tasks/PublishNuget.cs | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3df7f8d36..4debf219d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -164,13 +164,13 @@ publisher entry covers every package pushed by the same workflow run. **Trusted Publishers**). 3. Click **Add trusted publisher** and fill in the following fields: - | Field | Value | - |---|---| - | **Publisher type** | GitHub Actions | - | **Owner** | `GitTools` | - | **Repository** | `GitVersion` | - | **Workflow file name** | `ci.yml` | - | **Environment** | *(leave blank)* | + | Field | Value | + |------------------------|-----------------| + | **Publisher type** | GitHub Actions | + | **Owner** | `GitTools` | + | **Repository** | `GitVersion` | + | **Workflow file name** | `ci.yml` | + | **Environment** | *(leave blank)* | 4. Click **Add** to save the entry. diff --git a/build/publish/Tasks/PublishNuget.cs b/build/publish/Tasks/PublishNuget.cs index 86899211e9..55d2b86402 100644 --- a/build/publish/Tasks/PublishNuget.cs +++ b/build/publish/Tasks/PublishNuget.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using System.Text; using System.Text.Json; using Cake.Common.Tools.DotNet.NuGet.Push; using Common.Utilities; From b933173c09fb269152dda333d3b53e4b24279c33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:41:34 +0000 Subject: [PATCH 8/8] fix: remove unnecessary System.Text using directive from PublishNuget.cs Co-authored-by: arturcic <1760506+arturcic@users.noreply.github.com> --- build/publish/Tasks/PublishNuget.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/build/publish/Tasks/PublishNuget.cs b/build/publish/Tasks/PublishNuget.cs index 55d2b86402..86899211e9 100644 --- a/build/publish/Tasks/PublishNuget.cs +++ b/build/publish/Tasks/PublishNuget.cs @@ -1,5 +1,4 @@ using System.Net.Http.Headers; -using System.Text; using System.Text.Json; using Cake.Common.Tools.DotNet.NuGet.Push; using Common.Utilities;