Claude Cursor GitHub Copilot Skill

nuget-trusted-publishing

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

LLM Mart · 0 points · 20 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dotnet-skills-plugins_dotnet-advanced_skills_nuget-trusted-publishing-98f8485.zip · 8 KB
Part of dotnet/skills — 119 skills

Install

skills CLI npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-advanced/skills/nuget-trusted-publishing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
Git git clone https://github.com/dotnet/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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.

  1. 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)
  2. 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
  3. Find existing publish workflows in .github/workflows/ — look for dotnet nuget push, nuget push, or dotnet pack.

  4. Check version consistency — for MCP servers, verify .csproj <Version> matches both server.json version fields (root version and packages[].version). Flag any mismatch.

  5. 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."

  1. dotnet pack -c Release -o ./artifacts — verify .nupkg is created
  2. For tools/MCP servers: install from ./artifacts, run --help, uninstall
  3. 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.

  1. 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.

  2. 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.

  3. 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 —

  1. 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
    
  2. 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
    
  3. Replace the API key in the push step:

    --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate
    
  4. 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

Files (skills)
  • references
    • package-types.md 8.2 KB
      # NuGet Package Type Reference
      
      Structural requirements for each NuGet package type. The agent uses this to validate a repo's packaging setup before configuring trusted publishing.
      
      ## Detection Logic
      
      Inspect `.csproj` files (and `Directory.Build.props` if present) for these MSBuild properties:
      
      ```
      1. Has <PackageType>Template</PackageType>?           → Template package
      2. Has <PackageType>McpServer</PackageType>?           → MCP server (also a dotnet tool)
      3. Has <PackAsTool>true</PackAsTool>?                  → Dotnet tool
      4. Has <IsPackable>true</IsPackable> or no OutputType? → NuGet library
      5. Has <OutputType>Exe</OutputType> + <IsPackable>true</IsPackable>? → Application package
      6. Has <OutputType>Exe</OutputType> without PackAsTool or IsPackable? → Not packable by default (ask user)
      ```
      
      Check in order — MCP servers have `PackAsTool` too, so `PackageType` must be checked first.
      
      ## NuGet Library
      
      The most common case. A class library consumed via `PackageReference`.
      
      ### Required Properties
      
      | Property | Example | Notes |
      |----------|---------|-------|
      | `PackageId` | `Contoso.Utilities` | Defaults to `AssemblyName` if omitted |
      | `Version` | `0.1.0` | Start with 0.x for initial development |
      
      ### Recommended Properties
      
      | Property | Purpose |
      |----------|---------|
      | `Authors` | Package author(s) |
      | `Description` | Shown on nuget.org |
      | `PackageTags` | Discoverability |
      | `PackageReadmeFile` | README displayed on nuget.org |
      | `PackageLicenseExpression` | SPDX license identifier |
      | `RepositoryUrl` | Link back to source |
      | `PublishRepositoryUrl` | Enables source-link integration |
      
      ### Including README in Package
      
      `PackageReadmeFile` alone isn't enough — you must also include the file in the package:
      
      ```xml
      <PropertyGroup>
        <PackageReadmeFile>README.md</PackageReadmeFile>
      </PropertyGroup>
      <ItemGroup>
        <None Include="README.md" Pack="true" PackagePath="/" />
        <!-- If README is in a parent dir (common for src/ layouts): -->
        <!-- <None Include="../../README.md" Pack="true" PackagePath="/" /> -->
      </ItemGroup>
      ```
      
      ### Minimal .csproj
      
      ```xml
      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <TargetFramework>net9.0</TargetFramework>
          <PackageId>Contoso.Utilities</PackageId>
          <Version>0.1.0</Version>
          <Authors>Contoso</Authors>
          <Description>Utility library for Contoso apps</Description>
          <PackageReadmeFile>README.md</PackageReadmeFile>
        </PropertyGroup>
        <ItemGroup>
          <None Include="README.md" Pack="true" PackagePath="/" />
        </ItemGroup>
      </Project>
      ```
      
      ### Pack Command
      
      ```bash
      dotnet pack -c Release
      ```
      
      ## Dotnet Tool
      
      A console app distributed as a global or local tool via `dotnet tool install`.
      
      ### Required Properties
      
      | Property | Example | Notes |
      |----------|---------|-------|
      | `OutputType` | `Exe` | Must be an executable |
      | `PackAsTool` | `true` | Marks this as a tool package |
      | `PackageId` | `contoso-cli` | Tool package identifier |
      | `Version` | `0.1.0` | Package version (or set in Directory.Build.props) |
      
      ### Recommended Properties
      
      | Property | Example | Notes |
      |----------|---------|-------|
      | `ToolCommandName` | `contoso` | Command users type; defaults to assembly name |
      | `PackageOutputPath` | `./nupkg` | Where .nupkg is written |
      | `PackageReadmeFile` | `README.md` | Shown on nuget.org |
      
      ### Minimal .csproj
      
      ```xml
      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <OutputType>Exe</OutputType>
          <TargetFramework>net9.0</TargetFramework>
          <PackAsTool>true</PackAsTool>
          <ToolCommandName>contoso</ToolCommandName>
          <PackageId>contoso-cli</PackageId>
          <PackageReadmeFile>README.md</PackageReadmeFile>
        </PropertyGroup>
        <ItemGroup>
          <None Include="README.md" Pack="true" PackagePath="/" />
        </ItemGroup>
      </Project>
      ```
      
      ### Pack Command
      
      ```bash
      dotnet pack -c Release
      ```
      
      ## MCP Server
      
      A dotnet tool that implements the Model Context Protocol. Distributed the same way as a dotnet tool but with additional metadata for MCP client discovery.
      
      ### Naming Convention
      
      Follow the established pattern for MCP server packages:
      - **PackageId**: `{github-username}.{domain}.mcp` (e.g., `lewing.helix.mcp`)
      - **server.json `name`**: `io.github.{username}/{packageid}` (e.g., `io.github.lewing/lewing.helix.mcp`)
      
      ### Required Properties
      
      Everything from Dotnet Tool, plus:
      
      | Property | Example | Notes |
      |----------|---------|-------|
      | `PackageType` | `McpServer` | NuGet recognizes this as an MCP server |
      
      ### Recommended Properties
      
      | Property | Example | Notes |
      |----------|---------|-------|
      | `McpServerJsonTemplateFile` | `.mcp/server.json` | MCP client discovery metadata |
      
      ### Required Files
      
      | File | Purpose |
      |------|---------|
      | `.mcp/server.json` | MCP server descriptor for client discovery |
      
      The `.mcp/server.json` must be included in the package:
      
      ```xml
      <ItemGroup>
        <None Include=".mcp/server.json" Pack="true" PackagePath="/.mcp/" />
      </ItemGroup>
      ```
      
      ### Minimal server.json
      
      ```json
      {
        "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
        "name": "io.github.contoso/contoso.services.mcp",
        "description": "MCP server for Contoso services",
        "version": "0.1.0",
        "packages": [
          {
            "registryType": "nuget",
            "registryBaseUrl": "https://api.nuget.org",
            "identifier": "contoso.services.mcp",
            "version": "0.1.0",
            "transport": { "type": "stdio" }
          }
        ]
      }
      ```
      
      > ⚠️ **Version sync**: The `version` fields in `server.json` MUST match the `<Version>` in your `.csproj`. Update both when bumping versions.
      
      > ⚠️ **nuget.org MCP Server tab**: nuget.org auto-generates MCP install config from your `server.json`. If your tool requires a subcommand (e.g., `my-tool mcp`), the generated config may omit it. Ensure your tool defaults to MCP server mode when invoked with no arguments, or uses a `--yes` flag for non-interactive acceptance.
      
      ### Minimal .csproj
      
      ```xml
      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <OutputType>Exe</OutputType>
          <TargetFramework>net9.0</TargetFramework>
          <PackAsTool>true</PackAsTool>
          <PackageType>McpServer</PackageType>
          <PackageId>contoso.services.mcp</PackageId>
          <ToolCommandName>contoso-mcp</ToolCommandName>
          <McpServerJsonTemplateFile>.mcp/server.json</McpServerJsonTemplateFile>
          <PackageReadmeFile>README.md</PackageReadmeFile>
        </PropertyGroup>
        <ItemGroup>
          <None Include=".mcp/server.json" Pack="true" PackagePath="/.mcp/" />
          <None Include="README.md" Pack="true" PackagePath="/" />
        </ItemGroup>
      </Project>
      ```
      
      ## Template Package
      
      A package containing `dotnet new` templates.
      
      ### Required Properties
      
      | Property | Example | Notes |
      |----------|---------|-------|
      | `PackageType` | `Template` | NuGet recognizes this as a template package |
      | `PackageId` | `Contoso.Templates` | Template package identifier |
      
      ### Required Files
      
      | File | Purpose |
      |------|---------|
      | `content/*/.template.config/template.json` | Template definition — one per template |
      
      The `template.json` must include at minimum: `identity`, `name`, `shortName`, `tags.type` (`item` or `project`).
      
      ### Minimal .csproj
      
      ```xml
      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <PackageType>Template</PackageType>
          <PackageId>Contoso.Templates</PackageId>
          <Version>0.1.0</Version>
          <Description>Contoso project templates</Description>
      
          <!-- Template packages should not build code -->
          <IncludeContentInPack>true</IncludeContentInPack>
          <IncludeBuildOutput>false</IncludeBuildOutput>
          <ContentTargetFolders>content</ContentTargetFolders>
          <NoDefaultExcludes>true</NoDefaultExcludes>
        </PropertyGroup>
        <ItemGroup>
          <Content Include="content/**" />
        </ItemGroup>
      </Project>
      ```
      
      ## Common Gotchas
      
      - **`IsPackable` defaults**: Class libraries default to `true`, console apps to `false`. Console apps can still be published as NuGet packages by setting `<IsPackable>true</IsPackable>` — they just won't be installable via `dotnet tool install` unless `PackAsTool` is also set.
      - **`Directory.Build.props`**: Package metadata may be set at the repo root — always check there too.
      - **Multi-project repos**: A repo may contain multiple packable projects of different types. Each needs its own trusted publishing workflow or a matrix build.
      - **`GeneratePackageOnBuild`**: If `true`, `dotnet build` also produces the `.nupkg`. The workflow should use `dotnet pack` explicitly for clarity.
      
    • publish-workflow.md 3.8 KB
      # Publish Workflow Template
      
      Complete tag-triggered GitHub Actions workflow for publishing NuGet packages with trusted publishing. Copy and adapt to your repo.
      
      ## Template
      
      ```yaml
      name: Publish to NuGet
      
      on:
        push:
          tags:
            - 'v*'  # Triggers on version tags: v1.0.0, v1.2.3-preview.1, etc.
      
      jobs:
        publish:
          runs-on: ubuntu-latest
          environment: release  # Uses release environment for secret scoping + protection rules
          permissions:
            id-token: write     # Required for OIDC token (NuGet trusted publishing)
            contents: read
      
          steps:
            - uses: actions/checkout@v4
      
            - name: Setup .NET
              uses: actions/setup-dotnet@v4
              with:
                dotnet-version: '9.0.x'  # Adjust to your target framework
      
            - name: Extract version from tag
              id: version
              run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
      
            - name: Validate version matches project
              run: |
                PROJECT_VERSION=$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' path/to/YourProject.csproj)
                if [ "$PROJECT_VERSION" != "${{ steps.version.outputs.VERSION }}" ]; then
                  echo "::error::Tag version (${{ steps.version.outputs.VERSION }}) doesn't match project version ($PROJECT_VERSION)"
                  exit 1
                fi
      
            - name: Pack
              run: dotnet pack path/to/YourProject.csproj -c Release -o ./artifacts
      
            - name: NuGet login (OIDC)
              id: login
              uses: NuGet/login@v1
              with:
                user: ${{ secrets.NUGET_USER }}  # nuget.org profile name (NOT email)
      
            - name: Push to NuGet
              run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
      ```
      
      ## Customization Points
      
      | Item | What to change |
      |------|---------------|
      | `dotnet-version` | Match your `TargetFramework` |
      | `path/to/YourProject.csproj` | Path to your packable project |
      | Version extraction `sed` | Adjust for `Directory.Build.props` or .NET 10 file-based apps (`#:property Version=`) |
      | `--skip-duplicate` | Keeps push idempotent — safe for re-runs and matrix builds |
      
      ## Release Process
      
      Once the workflow is committed, the publish process is:
      
      ```bash
      # 1. Bump version in .csproj (and server.json for MCP servers)
      # 2. Commit
      git add -A && git commit -m "Bump version to 0.1.0"
      # 3. Tag and push
      git tag v0.1.0
      git push origin main --tags
      # 4. Workflow runs automatically, publishes to nuget.org
      ```
      
      ## Optional: GitHub Release Step
      
      Add after the push step if you want GitHub Releases with the `.nupkg` attached. Note: this requires changing `contents: read` to `contents: write` in the job permissions.
      
      ```yaml
            - name: Create GitHub Release
              uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
              with:
                files: ./artifacts/*.nupkg
                generate_release_notes: true
      ```
      
      > ⚠️ **Consider omitting this step entirely.** Creating GitHub Releases separately (manually or via `gh release create` in a different workflow) avoids 422 `already_exists` conflicts and keeps the publish workflow focused on NuGet. If any release step fails, it blocks NuGet publishing too.
      
      > ⚠️ **Don't use `ncipollo/release-action` AND `gh release create` for the same tag** — this causes HTTP 422 `already_exists` errors.
      
      > ⚠️ **`gh run rerun` replays the original YAML** from the tag commit, not from `main`. If the workflow fails due to a release conflict, delete the conflicting release and re-run — don't delete the tag and re-tag (NuGet package IDs are permanent).
      
      ## CI vs Publish Separation
      
      Keep your CI workflow (build + test on PR/push) separate from the publish workflow (tag-triggered). This gives you:
      - CI runs on every PR without publishing
      - Publish only runs on deliberate version tags
      - Different permission scopes (CI doesn't need `id-token: write`)
      
  • SKILL.md 9.1 KB
    ---
    name: nuget-trusted-publishing
    description: >
      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.
    license: MIT
    ---
    
    # NuGet Trusted Publishing Setup
    
    Set 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.
    
    ## 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.
    
    1. **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)
    
    2. **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 |
    
    3. **Find existing publish workflows** in `.github/workflows/` — look for `dotnet nuget push`, `nuget push`, or `dotnet pack`.
    
    4. **Check version consistency** — for MCP servers, verify `.csproj` `<Version>` matches both `server.json` version fields (root `version` and `packages[].version`). Flag any mismatch.
    
    5. **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](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."
    
    1. `dotnet pack -c Release -o ./artifacts` — verify `.nupkg` is created
    2. For tools/MCP servers: install from `./artifacts`, run `--help`, uninstall
    3. 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.
    
    1. 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.
    
    2. Guide the user to create the trusted publishing policy:
       > Go to [**nuget.org/account/trustedpublishing**](https://www.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.
    
    3. 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](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 —
    
    1. **Add OIDC permission and environment** to the publishing job:
       ```yaml
       jobs:
         publish:
           environment: release
           permissions:
             id-token: write     # Required — without this, NuGet/login fails with 403
             contents: read      # Explicit — setting permissions overrides defaults
       ```
    
    2. **Add the NuGet login step** before push:
       ```yaml
       - name: NuGet login (OIDC)
         id: login
         uses: NuGet/login@v1
         with:
           user: ${{ secrets.NUGET_USER }}  # nuget.org profile name, NOT email
       ```
    
    3. **Replace the API key** in the push step:
       ```yaml
       --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate
       ```
    
    4. **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](references/package-types.md) — detection logic, required properties, minimal .csproj examples
    - **Publish workflow template**: [references/publish-workflow.md](references/publish-workflow.md) — complete tag-triggered workflow ready to adapt
    - **Microsoft docs**: [NuGet Trusted Publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related