Skip to content

Commit 5d5aaa1

Browse files
authored
Merge pull request #4884 from GitTools/copilot/support-nuget-org-trusted-publishing
feat: support nuget.org Trusted Publishing via GitHub Actions OIDC
2 parents 48638eb + b933173 commit 5d5aaa1

3 files changed

Lines changed: 68 additions & 6 deletions

File tree

.github/workflows/_publish.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ jobs:
1313
publish:
1414
name: ${{ matrix.taskName }}
1515
runs-on: windows-2025-vs2026
16+
permissions:
17+
id-token: write
18+
packages: write
19+
contents: read
1620
strategy:
1721
fail-fast: false
1822
matrix:

CONTRIBUTING.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,51 @@ We use Cake for our build and deployment process. The way the release process is
142142
and other distribution channels.
143143
9. The issues and pull requests will get updated with message specifying in which release it was included.
144144

145+
### NuGet Trusted Publishing
146+
147+
NuGet packages are published to nuget.org using [Trusted Publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing),
148+
which replaces long-lived API keys with short-lived, identity-based tokens issued by GitHub Actions OIDC.
149+
150+
**How it works:**
151+
152+
1. The publish workflow requests a GitHub OIDC token scoped to `https://www.nuget.org`.
153+
2. That token is exchanged with the nuget.org token service for a short-lived API key.
154+
3. Packages are pushed using that short-lived key — no long-lived secret is stored or rotated.
155+
156+
**One-time setup on nuget.org:**
157+
158+
Trusted Publishing is configured once for the repository and workflow — not per package. A single trusted
159+
publisher entry covers every package pushed by the same workflow run.
160+
161+
1. Sign in to [nuget.org](https://www.nuget.org) as a package owner.
162+
2. Go to **Account settings****Trusted Publishers** (or navigate to any of the
163+
[GitVersion packages](https://www.nuget.org/profiles/GitTools) and open **Manage package****Settings**
164+
**Trusted Publishers**).
165+
3. Click **Add trusted publisher** and fill in the following fields:
166+
167+
| Field | Value |
168+
|------------------------|-----------------|
169+
| **Publisher type** | GitHub Actions |
170+
| **Owner** | `GitTools` |
171+
| **Repository** | `GitVersion` |
172+
| **Workflow file name** | `ci.yml` |
173+
| **Environment** | *(leave blank)* |
174+
175+
4. Click **Add** to save the entry.
176+
177+
> **Note:** nuget.org will only issue a short-lived key when the OIDC claims from the workflow run match *all*
178+
> registered fields exactly. A mismatch on any field (e.g. wrong workflow file name) will cause the token
179+
> exchange to fail and the publish step will fall back to the static `NUGET_API_KEY`.
180+
181+
**Verification and troubleshooting:**
182+
183+
- If the OIDC token exchange fails the workflow falls back to a static `NUGET_API_KEY` environment variable
184+
loaded from 1Password via the `gittools/cicd/nuget-creds@v1` action. Check the "Publishing to Nuget.org" log
185+
group for error details.
186+
- The publish job requires `id-token: write` permission, which is declared in `.github/workflows/_publish.yml`.
187+
- If a package fails to publish with a permissions error, verify that nuget.org Trusted Publishing is configured
188+
and that the owner, repository, and workflow file name match exactly.
189+
145190
## Code Style
146191

147192
In order to apply the code style defined by by the `.editorconfig` file you can use [`dotnet-format`](https://github.com/dotnet/format).

build/publish/Tasks/PublishNuget.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,17 @@ public override async Task RunAsync(BuildContext context)
4343
if (context.IsTaggedRelease || context.IsTaggedPreRelease)
4444
{
4545
context.StartGroup("Publishing to Nuget.org");
46-
var apiKey = context.Credentials?.Nuget?.ApiKey;
46+
47+
// Prefer Trusted Publishing via OIDC token exchange (no long-lived API key required)
48+
var apiKey = await GetNugetApiKey(context);
49+
50+
// Fall back to a static API key when OIDC is not available
51+
if (string.IsNullOrEmpty(apiKey))
52+
{
53+
context.Information("OIDC token exchange unavailable; falling back to static NuGet API key.");
54+
apiKey = context.Credentials?.Nuget?.ApiKey;
55+
}
56+
4757
if (string.IsNullOrEmpty(apiKey))
4858
{
4959
throw new InvalidOperationException("Could not resolve NuGet org API key.");
@@ -52,8 +62,6 @@ public override async Task RunAsync(BuildContext context)
5262
PublishToNugetRepo(context, apiKey, Constants.NugetOrgUrl);
5363
context.EndGroup();
5464
}
55-
56-
await Task.CompletedTask;
5765
}
5866

5967
private static void PublishToNugetRepo(BuildContext context, string apiKey, string apiUrl)
@@ -85,17 +93,22 @@ private static void PublishToNugetRepo(BuildContext context, string apiKey, stri
8593
}
8694
catch (HttpRequestException ex)
8795
{
88-
context.Error($"Network error while retrieving NuGet API key: {ex.Message}");
96+
context.Warning($"Network error while retrieving NuGet API key via OIDC: {ex.Message}");
8997
return null;
9098
}
9199
catch (InvalidOperationException ex)
92100
{
93-
context.Error($"Invalid operation while retrieving NuGet API key: {ex.Message}");
101+
context.Warning($"OIDC not available for NuGet API key retrieval: {ex.Message}");
94102
return null;
95103
}
96104
catch (JsonException ex)
97105
{
98-
context.Error($"JSON parsing error while retrieving NuGet API key: {ex.Message}");
106+
context.Warning($"JSON parsing error while retrieving NuGet API key via OIDC: {ex.Message}");
107+
return null;
108+
}
109+
catch (Exception ex)
110+
{
111+
context.Warning($"Unexpected error while retrieving NuGet API key via OIDC: {ex.Message}");
99112
return null;
100113
}
101114
}

0 commit comments

Comments
 (0)