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.4license: MIT5---67# NuGet Trusted Publishing Setup89Set 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.1011## Prerequisites1213- **GitHub Actions** — this skill covers GitHub Actions setup only14- **nuget.org account** — the user needs access to create trusted publishing policies1516## When to Use This Skill1718Use this skill when:19- Setting up trusted publishing for a NuGet package20- Migrating from `secrets.NUGET_API_KEY` to OIDC-based publishing21- Asked about keyless or secure NuGet publishing22- Creating a new NuGet publish workflow from scratch23- Asked to "remove NuGet API key" or "use NuGet/login"24- Setting up publishing for a dotnet tool, MCP server, or template package25- Asked about `NuGet/login@v1` or `id-token: write`2627## Safety Rules2829> ⚠️ **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.3031> ⚠️ **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.3233## Process3435> **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.3637### Phase 1: Assess3839Inspect the repo and report findings before making any changes.40411. **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):42 - `<PackageType>Template</PackageType>` → **Template**43 - `<PackageType>McpServer</PackageType>` → **MCP server** (also a dotnet tool)44 - `<PackAsTool>true</PackAsTool>` → **Dotnet tool**45 - Class library (`IsPackable=true` or no `OutputType`) → **Library**46 - `<OutputType>Exe</OutputType>` with `<IsPackable>true</IsPackable>` → **Application package** (not a tool, but still publishable)47 - `<OutputType>Exe</OutputType>` without `PackAsTool` or `IsPackable` → Not packable by default (ask user if they intend to publish it)48492. **Validate structure** for each project's type:5051 | Type | Required |52 |------|----------|53 | All | `PackageId`, `Version` (in .csproj or Directory.Build.props) |54 | Dotnet tool | `PackAsTool` (required); `ToolCommandName` (optional but recommended — defaults to assembly name) |55 | MCP server | `PackageType=McpServer`, `.mcp/server.json` included in package |56 | Template | `PackageType=Template`, `.template.config/template.json` under content dir |57583. **Find existing publish workflows** in `.github/workflows/` — look for `dotnet nuget push`, `nuget push`, or `dotnet pack`.59604. **Check version consistency** — for MCP servers, verify `.csproj` `<Version>` matches both `server.json` version fields (root `version` and `packages[].version`). Flag any mismatch.61625. **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.6364> ❌ See [references/package-types.md](references/package-types.md) for per-type details and required properties.6566### Phase 2: Local Verification6768Pack and verify locally before touching nuget.org — publishing errors waste a permanent version number.6970> ⚠️ **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."71721. `dotnet pack -c Release -o ./artifacts` — verify `.nupkg` is created732. For tools/MCP servers: install from `./artifacts`, run `--help`, uninstall743. For libraries: inspect the `.nupkg` contents (it's a zip)7576### Phase 3: nuget.org Policy7778This phase requires the user to act on nuget.org — guide them with exact values.79801. Determine the **repo owner**, **repo name**, and the **workflow filename** that will publish.8182 > ❌ 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.83842. Guide the user to create the trusted publishing policy:85 > Go to [**nuget.org/account/trustedpublishing**](https://www.nuget.org/account/trustedpublishing) → **Add policy**86 >87 > - **Repository Owner**: `{owner}`88 > - **Repository**: `{repo}`89 > - **Workflow File**: `{filename}.yml`90 > - **Environment**: `release` *(only if the workflow uses `environment:`; leave blank otherwise)*9192 Policy ownership: the user chooses individual account or organization. Org-owned policies apply to all packages owned by that org.9394 For **private repos**: policy is "temporarily active" for 7 days — becomes permanent after the first successful publish.95963. Guide the user to create a **GitHub Environment** (recommended but optional — provides secret scoping + approval gates):97 > Repo **Settings** → **Environments** → **New environment** → `release`98 >99 > Add environment secret: **Name** = `NUGET_USER`, **Value** = nuget.org username (NOT email)100101 Optional: add **Required reviewers** for an approval gate.102103> ⚠️ 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.104105### Phase 4: Workflow Setup106107Create 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.108109**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`.110111**Migration** (existing workflow with API key): Modify in place —1121131. **Add OIDC permission and environment** to the publishing job:114 ```yaml115 jobs:116 publish:117 environment: release118 permissions:119 id-token: write # Required — without this, NuGet/login fails with 403120 contents: read # Explicit — setting permissions overrides defaults121 ```1221232. **Add the NuGet login step** before push:124 ```yaml125 - name: NuGet login (OIDC)126 id: login127 uses: NuGet/login@v1128 with:129 user: ${{ secrets.NUGET_USER }} # nuget.org profile name, NOT email130 ```1311323. **Replace the API key** in the push step:133 ```yaml134 --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate135 ```1361374. **Verify**: Ask the user to trigger a publish and confirm the package appears on nuget.org.138139> ❌ **Don't delete the old API key secret** until trusted publishing is verified. Removing it is a one-way door — wait for confirmation.140141## Troubleshooting142143| Problem | Cause | Fix |144|---------|-------|-----|145| `NuGet/login` 403 | Missing `id-token: write` | Add to job permissions |146| "no matching policy" | Workflow filename mismatch | Verify exact filename on nuget.org |147| Push unauthorized | Package not owned by policy account | Check policy owner on nuget.org |148| Token expired | Login step >1hr before push | Move `NuGet/login` closer to push |149| "temporarily active" policy | Private repo, first publish pending | Publish within 7 days |150| `already_exists` on push | Re-running same version | Add `--skip-duplicate` |151| GitHub Release 422 | Duplicate release for tag | Delete conflicting release (confirm first) |152| Re-run uses wrong YAML | `gh run rerun` replays original commit's YAML | Delete obstacle, re-run — never re-tag |153154> ⚠️ If any blocker persists after one fix attempt, **stop and ask the user**.155156## References157158- **Package type details**: [references/package-types.md](references/package-types.md) — detection logic, required properties, minimal .csproj examples159- **Publish workflow template**: [references/publish-workflow.md](references/publish-workflow.md) — complete tag-triggered workflow ready to adapt160- **Microsoft docs**: [NuGet Trusted Publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing)