NuGet Trusted Publishing Setup
Set up NuGet trusted publishing on a GitHub Actions repo. Replaces long-lived API keys with OIDC-based short-lived tokens — no secrets to rotate or leak.
Prerequisites
- GitHub Actions — this skill covers GitHub Actions setup only
- nuget.org account — the user needs access to create trusted publishing policies
When to Use This Skill
Use this skill when:
- Setting up trusted publishing for a NuGet package
- Migrating from
secrets.NUGET_API_KEY to OIDC-based publishing
- Asked about keyless or secure NuGet publishing
- Creating a new NuGet publish workflow from scratch
- Asked to "remove NuGet API key" or "use NuGet/login"
- Setting up publishing for a dotnet tool, MCP server, or template package
- Asked about
NuGet/login@v1 or id-token: write
Safety Rules
⚠️ Bail-out rule: If any phase fails after one fix attempt on an infrastructure/auth issue, stop and ask the user. Don't loop on environment problems.
⚠️ Never delete or overwrite without confirmation: Removing API key secrets, deleting tags/releases, removing workflow steps, or changing package IDs. NuGet package IDs are permanent — mistakes can't be undone.
Process
Fast-path for greenfield repos: When the user has a simple setup (one packable project, no existing publish workflow), don't gate on multi-turn assessment. Combine phases: create the workflow immediately, include nuget.org policy guidance, local pack recommendation, and filename-matching warning all in one response. The full phased process below is for complex or migration scenarios.
Phase 1: Assess
Inspect the repo and report findings before making any changes.
Find and classify packable projects — check .csproj files and Directory.Build.props (package metadata is often set repo-wide). Classify in this order (earlier matches win):
<PackageType>Template</PackageType> → Template
<PackageType>McpServer</PackageType> → MCP server (also a dotnet tool)
<PackAsTool>true</PackAsTool> → Dotnet tool
- Class library (
IsPackable=true or no OutputType) → Library
<OutputType>Exe</OutputType> with <IsPackable>true</IsPackable> → Application package (not a tool, but still publishable)
<OutputType>Exe</OutputType> without PackAsTool or IsPackable → Not packable by default (ask user if they intend to publish it)
Validate structure for each project's type:
| Type |
Required |
| All |
PackageId, Version (in .csproj or Directory.Build.props) |
| Dotnet tool |
PackAsTool (required); ToolCommandName (optional but recommended — defaults to assembly name) |
| MCP server |
PackageType=McpServer, .mcp/server.json included in package |
| Template |
PackageType=Template, .template.config/template.json under content dir |
Find existing publish workflows in .github/workflows/ — look for dotnet nuget push, nuget push, or dotnet pack.
Check version consistency — for MCP servers, verify .csproj <Version> matches both server.json version fields (root version and packages[].version). Flag any mismatch.
Report findings to the user: classification, missing properties, version mismatches, existing workflows. For multi-project repos, note whether one workflow or separate workflows per package are needed. Offer to fix gaps — use ask_user before modifying project files.
❌ See references/package-types.md for per-type details and required properties.
Phase 2: Local Verification
Pack and verify locally before touching nuget.org — publishing errors waste a permanent version number.
⚠️ Always mention this step, even if you defer running it. Tell the user: "Before your first publish, run dotnet pack -c Release -o ./artifacts to verify the .nupkg is created correctly."
dotnet pack -c Release -o ./artifacts — verify .nupkg is created
- For tools/MCP servers: install from
./artifacts, run --help, uninstall
- For libraries: inspect the
.nupkg contents (it's a zip)
Phase 3: nuget.org Policy
This phase requires the user to act on nuget.org — guide them with exact values.
Determine the repo owner, repo name, and the workflow filename that will publish.
❌ The policy requires the exact workflow filename (e.g., publish.yml or publish.yaml) — just the filename, no path prefix. Matching is case-insensitive. Don't use the workflow name: field.
Guide the user to create the trusted publishing policy:
Go to nuget.org/account/trustedpublishing → Add policy
- Repository Owner:
{owner}
- Repository:
{repo}
- Workflow File:
{filename}.yml
- Environment:
release (only if the workflow uses environment:; leave blank otherwise)
Policy ownership: the user chooses individual account or organization. Org-owned policies apply to all packages owned by that org.
For private repos: policy is "temporarily active" for 7 days — becomes permanent after the first successful publish.
Guide the user to create a GitHub Environment (recommended but optional — provides secret scoping + approval gates):
Repo Settings → Environments → New environment → release
Add environment secret: Name = NUGET_USER, Value = nuget.org username (NOT email)
Optional: add Required reviewers for an approval gate.
⚠️ Wait for the user to confirm they've created the policy before asking them to remove old API keys/secrets or before attempting to run/publish with the workflow. Drafting or showing the workflow file itself is OK before confirmation.
Phase 4: Workflow Setup
Create or modify the publish workflow. The workflow must always be created or shown in your response — you may draft/show it even if the nuget.org policy is not yet confirmed, but do not guide the user to actually run/publish or remove old secrets until after confirmation.
Greenfield: Create publish.yml from the template in references/publish-workflow.md. Adapt .NET version, project path, and environment name. Ensure your output explicitly mentions id-token: write and NuGet/login@v1.
Migration (existing workflow with API key): Modify in place —
Add OIDC permission and environment to the publishing job:
jobs:
publish:
environment: release
permissions:
id-token: write # Required — without this, NuGet/login fails with 403
contents: read # Explicit — setting permissions overrides defaults
Add the NuGet login step before push:
- name: NuGet login (OIDC)
id: login
uses: NuGet/login@v1
with:
user: ${{ secrets.NUGET_USER }} # nuget.org profile name, NOT email
Replace the API key in the push step:
--api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate
Verify: Ask the user to trigger a publish and confirm the package appears on nuget.org.
❌ Don't delete the old API key secret until trusted publishing is verified. Removing it is a one-way door — wait for confirmation.
Troubleshooting
| Problem |
Cause |
Fix |
NuGet/login 403 |
Missing id-token: write |
Add to job permissions |
| "no matching policy" |
Workflow filename mismatch |
Verify exact filename on nuget.org |
| Push unauthorized |
Package not owned by policy account |
Check policy owner on nuget.org |
| Token expired |
Login step >1hr before push |
Move NuGet/login closer to push |
| "temporarily active" policy |
Private repo, first publish pending |
Publish within 7 days |
already_exists on push |
Re-running same version |
Add --skip-duplicate |
| GitHub Release 422 |
Duplicate release for tag |
Delete conflicting release (confirm first) |
| Re-run uses wrong YAML |
gh run rerun replays original commit's YAML |
Delete obstacle, re-run — never re-tag |
⚠️ If any blocker persists after one fix attempt, stop and ask the user.
References
- Package type details: references/package-types.md — detection logic, required properties, minimal .csproj examples
- Publish workflow template: references/publish-workflow.md — complete tag-triggered workflow ready to adapt
- Microsoft docs: NuGet Trusted Publishing
1---2name: nuget-trusted-publishing3description: Set up NuGet trusted publishing (OIDC) on a GitHub Actions repo — replaces long-lived API keys with short-lived tokens. USE FOR: trusted publishing, NuGet OIDC, keyless NuGet publish, migrate from NuGet API key, NuGet/login, secure NuGet publishing. DO NOT USE FOR: publishing to private feeds or Azure Artifacts (OIDC is nuget.org only). INVOKES: shell (powershell or bash), edit, create, ask_user for guided repo setup.4---5
6# NuGet Trusted Publishing Setup
7
8Set up [NuGet trusted publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing) on a GitHub Actions repo. Replaces long-lived API keys with OIDC-based short-lived tokens — no secrets to rotate or leak.
9
10## Prerequisites
11
12- **GitHub Actions** — this skill covers GitHub Actions setup only
13- **nuget.org account** — the user needs access to create trusted publishing policies
14
15## When to Use This Skill
16
17Use this skill when:
18- Setting up trusted publishing for a NuGet package
19- Migrating from `secrets.NUGET_API_KEY` to OIDC-based publishing
20- Asked about keyless or secure NuGet publishing
21- Creating a new NuGet publish workflow from scratch
22- Asked to "remove NuGet API key" or "use NuGet/login"
23- Setting up publishing for a dotnet tool, MCP server, or template package
24- Asked about `NuGet/login@v1` or `id-token: write`
25
26## Safety Rules
27
28> ⚠️ **Bail-out rule**: If any phase fails after one fix attempt on an infrastructure/auth issue, stop and ask the user. Don't loop on environment problems.
29
30> ⚠️ **Never delete or overwrite without confirmation**: Removing API key secrets, deleting tags/releases, removing workflow steps, or changing package IDs. NuGet package IDs are permanent — mistakes can't be undone.
31
32## Process
33
34> **Fast-path for greenfield repos**: When the user has a simple setup (one packable project, no existing publish workflow), don't gate on multi-turn assessment. Combine phases: create the workflow immediately, include nuget.org policy guidance, local pack recommendation, and filename-matching warning all in one response. The full phased process below is for complex or migration scenarios.
35
36### Phase 1: Assess
37
38Inspect the repo and report findings before making any changes.
39
401. **Find and classify packable projects** — check `.csproj` files **and `Directory.Build.props`** (package metadata is often set repo-wide). Classify in this order (earlier matches win):
41 - `<PackageType>Template</PackageType>` → **Template**
42 - `<PackageType>McpServer</PackageType>` → **MCP server** (also a dotnet tool)
43 - `<PackAsTool>true</PackAsTool>` → **Dotnet tool**
44 - Class library (`IsPackable=true` or no `OutputType`) → **Library**
45 - `<OutputType>Exe</OutputType>` with `<IsPackable>true</IsPackable>` → **Application package** (not a tool, but still publishable)
46 - `<OutputType>Exe</OutputType>` without `PackAsTool` or `IsPackable` → Not packable by default (ask user if they intend to publish it)
47
482. **Validate structure** for each project's type:
49
50 | Type | Required |
51 |------|----------|
52 | All | `PackageId`, `Version` (in .csproj or Directory.Build.props) |
53 | Dotnet tool | `PackAsTool` (required); `ToolCommandName` (optional but recommended — defaults to assembly name) |
54 | MCP server | `PackageType=McpServer`, `.mcp/server.json` included in package |
55 | Template | `PackageType=Template`, `.template.config/template.json` under content dir |
56
573. **Find existing publish workflows** in `.github/workflows/` — look for `dotnet nuget push`, `nuget push`, or `dotnet pack`.
58
594. **Check version consistency** — for MCP servers, verify `.csproj` `<Version>` matches both `server.json` version fields (root `version` and `packages[].version`). Flag any mismatch.
60
615. **Report findings** to the user: classification, missing properties, version mismatches, existing workflows. For multi-project repos, note whether one workflow or separate workflows per package are needed. Offer to fix gaps — use `ask_user` before modifying project files.
62
63> ❌ See [references/package-types.md](references/package-types.md) for per-type details and required properties.
64
65### Phase 2: Local Verification
66
67Pack and verify locally before touching nuget.org — publishing errors waste a permanent version number.
68
69> ⚠️ **Always mention this step**, even if you defer running it. Tell the user: "Before your first publish, run `dotnet pack -c Release -o ./artifacts` to verify the .nupkg is created correctly."
70
711. `dotnet pack -c Release -o ./artifacts` — verify `.nupkg` is created
722. For tools/MCP servers: install from `./artifacts`, run `--help`, uninstall
733. For libraries: inspect the `.nupkg` contents (it's a zip)
74
75### Phase 3: nuget.org Policy
76
77This phase requires the user to act on nuget.org — guide them with exact values.
78
791. Determine the **repo owner**, **repo name**, and the **workflow filename** that will publish.
80
81 > ❌ The policy requires the **exact workflow filename** (e.g., `publish.yml` or `publish.yaml`) — just the filename, no path prefix. Matching is case-insensitive. Don't use the workflow `name:` field.
82
832. Guide the user to create the trusted publishing policy:
84 > Go to [**nuget.org/account/trustedpublishing**](https://www.nuget.org/account/trustedpublishing) → **Add policy**
85 >
86 > - **Repository Owner**: `{owner}`
87 > - **Repository**: `{repo}`
88 > - **Workflow File**: `{filename}.yml`
89 > - **Environment**: `release` *(only if the workflow uses `environment:`; leave blank otherwise)*
90
91 Policy ownership: the user chooses individual account or organization. Org-owned policies apply to all packages owned by that org.
92
93 For **private repos**: policy is "temporarily active" for 7 days — becomes permanent after the first successful publish.
94
953. Guide the user to create a **GitHub Environment** (recommended but optional — provides secret scoping + approval gates):
96 > Repo **Settings** → **Environments** → **New environment** → `release`
97 >
98 > Add environment secret: **Name** = `NUGET_USER`, **Value** = nuget.org username (NOT email)
99
100 Optional: add **Required reviewers** for an approval gate.
101
102> ⚠️ Wait for the user to confirm they've created the policy **before asking them to remove old API keys/secrets or before attempting to run/publish with the workflow**. Drafting or showing the workflow file itself is OK before confirmation.
103
104### Phase 4: Workflow Setup
105
106Create or modify the publish workflow. **The workflow must always be created or shown in your response** — you may draft/show it even if the nuget.org policy is not yet confirmed, but do not guide the user to actually run/publish or remove old secrets until after confirmation.
107
108**Greenfield**: Create `publish.yml` from the template in [references/publish-workflow.md](references/publish-workflow.md). Adapt .NET version, project path, and environment name. Ensure your output explicitly mentions `id-token: write` and `NuGet/login@v1`.
109
110**Migration** (existing workflow with API key): Modify in place —
111
1121. **Add OIDC permission and environment** to the publishing job:
113 ```yaml
114 jobs:
115 publish:
116 environment: release
117 permissions:
118 id-token: write # Required — without this, NuGet/login fails with 403
119 contents: read # Explicit — setting permissions overrides defaults
120 ```
121
1222. **Add the NuGet login step** before push:
123 ```yaml
124 - name: NuGet login (OIDC)
125 id: login
126 uses: NuGet/login@v1
127 with:
128 user: ${{ secrets.NUGET_USER }} # nuget.org profile name, NOT email
129 ```
130
1313. **Replace the API key** in the push step:
132 ```yaml
133 --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate
134 ```
135
1364. **Verify**: Ask the user to trigger a publish and confirm the package appears on nuget.org.
137
138> ❌ **Don't delete the old API key secret** until trusted publishing is verified. Removing it is a one-way door — wait for confirmation.
139
140## Troubleshooting
141
142| Problem | Cause | Fix |
143|---------|-------|-----|
144| `NuGet/login` 403 | Missing `id-token: write` | Add to job permissions |
145| "no matching policy" | Workflow filename mismatch | Verify exact filename on nuget.org |
146| Push unauthorized | Package not owned by policy account | Check policy owner on nuget.org |
147| Token expired | Login step >1hr before push | Move `NuGet/login` closer to push |
148| "temporarily active" policy | Private repo, first publish pending | Publish within 7 days |
149| `already_exists` on push | Re-running same version | Add `--skip-duplicate` |
150| GitHub Release 422 | Duplicate release for tag | Delete conflicting release (confirm first) |
151| Re-run uses wrong YAML | `gh run rerun` replays original commit's YAML | Delete obstacle, re-run — never re-tag |
152
153> ⚠️ If any blocker persists after one fix attempt, **stop and ask the user**.
154
155## References
156
157- **Package type details**: [references/package-types.md](references/package-types.md) — detection logic, required properties, minimal .csproj examples
158- **Publish workflow template**: [references/publish-workflow.md](references/publish-workflow.md) — complete tag-triggered workflow ready to adapt
159- **Microsoft docs**: [NuGet Trusted Publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing)