From f150f47054a0aeca5b18a289684ea59052408c47 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 17 Feb 2026 17:11:25 -0600 Subject: [PATCH 1/7] Add nuget-trusted-publishing skill Knowledge-driven skill for setting up NuGet trusted publishing (OIDC) on GitHub Actions repos. Guides users through a 5-phase process: discovery, structure validation, local testing, nuget.org policy setup, and workflow creation/modification. Supports library, dotnet tool, MCP server, and template package types with per-type structural requirements and a complete tag-triggered workflow template. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- skills/nuget-trusted-publishing/SKILL.md | 199 ++++++++++++++ .../references/package-types.md | 254 ++++++++++++++++++ .../references/publish-workflow.md | 102 +++++++ 3 files changed, 555 insertions(+) create mode 100644 skills/nuget-trusted-publishing/SKILL.md create mode 100644 skills/nuget-trusted-publishing/references/package-types.md create mode 100644 skills/nuget-trusted-publishing/references/publish-workflow.md diff --git a/skills/nuget-trusted-publishing/SKILL.md b/skills/nuget-trusted-publishing/SKILL.md new file mode 100644 index 0000000000..6012a5e401 --- /dev/null +++ b/skills/nuget-trusted-publishing/SKILL.md @@ -0,0 +1,199 @@ +--- +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: powershell, edit, create, ask_user for guided repo setup. +--- + +# 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 specifically (trusted publishing also supports Azure DevOps, but that requires a different configuration flow) +- **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` + +## Process + +> ⚠️ **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. + +> ⚠️ **Safety rule**: Never delete, remove, or overwrite anything without explaining the consequences and getting user confirmation first. This includes: removing API key secrets, deleting tags/releases, removing workflow steps, or changing package IDs. NuGet package IDs are permanent — mistakes can't be undone. + +### Phase 1: Discovery + +Inspect the repo to understand what's being packaged and how. + +1. **Find packable projects**: Search for `.csproj` files. Check `IsPackable`, `PackAsTool`, `PackageType`, and `OutputType` properties. Also check `Directory.Build.props` for repo-wide settings. + +2. **Classify each project** (check in this order): + - `Template` → **Template package** + - `McpServer` → **MCP server** (also a dotnet tool) + - `true` → **Dotnet tool** + - Class library (no `OutputType` or `IsPackable=true`) → **NuGet library** + - `Exe` without `PackAsTool` → Not a NuGet package, skip + +3. **Find existing workflows**: Search `.github/workflows/*.yml` for `dotnet nuget push`, `nuget push`, or `dotnet pack` steps. + +4. **Report findings** to the user before proceeding. + +> See [references/package-types.md](references/package-types.md) for per-type structural requirements and detection details. + +### Phase 2: Structure Validation + +Verify the repo has the right MSBuild properties and supporting files for its package type. + +| Type | Check for | +|------|-----------| +| All | `PackageId`, `Version` in .csproj or Directory.Build.props | +| Dotnet tool | `PackAsTool`, `ToolCommandName` | +| MCP server | `PackageType=McpServer`, `.mcp/server.json` exists, included in package via `` | +| Template | `PackageType=Template`, `.template.config/template.json` exists under content dir | + +If anything is missing, offer to add it. Use `ask_user` to confirm before modifying project files. + +> ❌ **Don't skip `Directory.Build.props`** — package metadata is often set at the repo root, not in individual .csproj files. Missing it means reporting false negatives. + +### Phase 3: Local Pre-Publish Testing + +Before configuring nuget.org, verify the package builds and works locally. + +1. **Pack**: `dotnet pack -c Release -o ./artifacts` +2. **Verify the `.nupkg`** was created in `./artifacts/` +3. **For dotnet tools / MCP servers** — install from local and test: + ```bash + dotnet tool install -g --add-source ./artifacts {PackageId} + {ToolCommandName} --help # Verify it runs + dotnet tool uninstall -g {PackageId} + ``` +4. **For libraries** — verify the package contains expected assemblies: + ```bash + dotnet nuget locals all --list # Note the global-packages path + # Or unzip the .nupkg (it's a zip) and inspect lib/ + ``` + +> ❌ **Don't skip local testing** — discovering packaging errors after publishing wastes a version number (nuget.org IDs are permanent). + +### Phase 4: nuget.org Policy Setup + +This is a manual step — guide the user through it with exact values. + +1. Extract repo owner and name from the git remote: + ```powershell + git remote get-url origin + # Parse: https://github.com/{owner}/{repo}.git or git@github.com:{owner}/{repo}.git + ``` + +2. Identify the workflow filename (just the filename, not the path) that will do the publishing. + +3. Tell the user: + > Go to **nuget.org** → click your username → **Trusted Publishing** → **Add policy** + > + > Enter these values: + > - **Repository Owner**: `{owner}` + > - **Repository**: `{repo}` + > - **Workflow File**: `{filename}.yml` + > - **Environment**: `{env}` *(only if the workflow uses `environment:`)* + +4. Explain policy ownership: choose individual account or organization as owner. The policy applies to all packages owned by that entity. + +5. Note: for **private repos**, the policy starts as "temporarily active" for 7 days. It becomes permanent after the first successful publish. + +6. **Create a GitHub Environment** for publish secret scoping: + > Go to **repo Settings** → **Environments** → **New environment** → name it `release` + > + > Then add a secret to this environment: + > - Click **Add environment secret** + > - **Name**: `NUGET_USER` + > - **Value**: your nuget.org username (NOT email) + > + > Optional: add **Required reviewers** for an approval gate before publishing. + + Environment-scoped secrets are only available to workflows referencing that environment — preventing accidental use in CI jobs. + +> ❌ **Don't guess the workflow filename** — the policy requires the exact filename (e.g., `publish.yml`), not the workflow `name:` field. Get it wrong and OIDC validation silently fails. + +> ⚠️ Wait for the user to confirm they've created the policy before proceeding to Phase 5. + +### Phase 5: Workflow Setup + +Either modify an existing publish workflow or create a new one from scratch. + +**If no publish workflow exists** (greenfield): +- Create a new `publish.yml` using the complete template from [references/publish-workflow.md](references/publish-workflow.md) +- Adapt the template: set the correct .NET version, project path, and environment name +- The template uses tag-triggered publishing (`on: push: tags: ['v*']`) — the standard pattern + +**If a publish workflow already exists**: +- Modify it in place following the steps below + +> ❌ **Don't delete the old API key secret** until trusted publishing is verified working. Keep it as a fallback. When the user is ready to remove it, explain that it's a one-way door (they'd need to regenerate on nuget.org) and wait for confirmation. + +1. **Add OIDC permission and environment** to the publishing job: + ```yaml + jobs: + publish: + environment: release # Uses the environment with NUGET_USER secret + permissions: + id-token: write # Required for NuGet trusted publishing + ``` + + > ❌ **Forgetting `id-token: write`** is the most common mistake. Without it, the OIDC token request fails and `NuGet/login` will error with 403. + +2. **Add the NuGet login step** before the push step: + ```yaml + - name: NuGet login (OIDC) + id: login + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} # nuget.org profile name, NOT email + ``` + + > ❌ **Don't use an email address** for the `user` input — it must be the nuget.org profile/username. Recommend storing it as an environment secret for scoping (it's not truly sensitive but scoping prevents accidental use in CI jobs). + +3. **Replace the API key reference** in the push step: + ```yaml + # Before: + --api-key ${{ secrets.NUGET_API_KEY }} + + # After: + --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate + ``` + Make sure the login step has `id: login` if referencing outputs by step ID. The `--skip-duplicate` flag makes pushes idempotent — safe for re-runs. + +4. **Verify**: After pushing the workflow change, ask the user to trigger a publish and confirm the package appears on nuget.org. + +## Common Blockers + +| Problem | Cause | Action | +|---------|-------|--------| +| `NuGet/login` fails with 403 | Missing `id-token: write` permission | Add to job permissions, re-run | +| `NuGet/login` fails with "no matching policy" | Workflow filename or repo owner doesn't match policy | Verify exact filename on nuget.org (case-insensitive) | +| Push fails with unauthorized | Package ID not owned by policy account | Verify policy owner owns the package ID on nuget.org | +| Token expired | Workflow requested token too early (>1 hour before push) | Move `NuGet/login` step closer to the push step | +| Policy shows "temporarily active" | Private repo, no publish yet | Complete first publish within 7 days | +| `already_exists` on push | Re-running a publish for same version | Add `--skip-duplicate` to `dotnet nuget push` | +| GitHub Release creation 422 | Duplicate release for same tag | Explain the conflict. Recommend removing the release step from the workflow or deleting the duplicate release — but wait for confirmation before either | +| Re-run uses wrong workflow | `gh run rerun` replays the original YAML from the tag commit | Explain the situation to the user. Recommend: remove the obstacle (e.g., delete conflicting release), then re-run. Never delete and re-tag — NuGet package IDs are permanent. Wait for user confirmation before deleting anything. | + +> ⚠️ If any blocker persists after one fix attempt, **stop and ask the user** — don't loop on infrastructure issues. + +## References + +- **Package type details**: See [references/package-types.md](references/package-types.md) for detection logic, required properties, and minimal .csproj examples per package type. +- **Publish workflow template**: See [references/publish-workflow.md](references/publish-workflow.md) for a complete tag-triggered publish workflow ready to adapt. +- **Microsoft docs**: [NuGet Trusted Publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing) diff --git a/skills/nuget-trusted-publishing/references/package-types.md b/skills/nuget-trusted-publishing/references/package-types.md new file mode 100644 index 0000000000..d08a1f0940 --- /dev/null +++ b/skills/nuget-trusted-publishing/references/package-types.md @@ -0,0 +1,254 @@ +# 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 Template? → Template package +2. Has McpServer? → MCP server (also a dotnet tool) +3. Has true? → Dotnet tool +4. Has true or no OutputType? → NuGet library +5. Has Exe without PackAsTool? → Not a NuGet package (skip) +``` + +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 + + README.md + + + + + + +``` + +### Minimal .csproj + +```xml + + + net9.0 + Contoso.Utilities + 0.1.0 + Contoso + Utility library for Contoso apps + README.md + + + + + +``` + +### 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 | + +### 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 + + + Exe + net9.0 + true + contoso + contoso-cli + README.md + + + + + +``` + +### 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 + + + +``` + +### 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 `` 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 + + + Exe + net9.0 + true + McpServer + contoso.services.mcp + contoso-mcp + .mcp/server.json + README.md + + + + + + +``` + +## 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 + + + Template + Contoso.Templates + 0.1.0 + Contoso project templates + + + true + false + content + true + + + + + +``` + +## Common Gotchas + +- **`IsPackable` defaults**: Class libraries default to `true`, console apps to `false`. If a console app should be a tool, set `PackAsTool` (which implies packable). +- **`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. diff --git a/skills/nuget-trusted-publishing/references/publish-workflow.md b/skills/nuget-trusted-publishing/references/publish-workflow.md new file mode 100644 index 0000000000..d74311c9fb --- /dev/null +++ b/skills/nuget-trusted-publishing/references/publish-workflow.md @@ -0,0 +1,102 @@ +# 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:.*\(.*\).*:\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`) From f9623f0f0a6c33bb0be155aab140d2c966eea4fc Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 18 Feb 2026 16:58:16 -0600 Subject: [PATCH 2/7] improve nuget-trusted-publishing skill flow and accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge 5 phases into 4 (Discovery + Structure Validation → Assess) - Fix nuget.org URL to correct https://www.nuget.org/account/trustedpublishing - Add contents: read to migration YAML snippet (prevents checkout failures) - Add MCP server version-sync check (.csproj ↔ server.json) - Clarify environment name is convention not requirement - Restore policy owner (user vs org) guidance - Add multi-project repo note - Fix .yml/.yaml extension guidance - Add Version to dotnet tool required properties in package-types.md - Compact troubleshooting table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- skills/nuget-trusted-publishing/SKILL.md | 170 +++++++----------- .../references/package-types.md | 1 + 2 files changed, 66 insertions(+), 105 deletions(-) diff --git a/skills/nuget-trusted-publishing/SKILL.md b/skills/nuget-trusted-publishing/SKILL.md index 6012a5e401..a6ad188283 100644 --- a/skills/nuget-trusted-publishing/SKILL.md +++ b/skills/nuget-trusted-publishing/SKILL.md @@ -28,133 +28,98 @@ Use this skill when: - Setting up publishing for a dotnet tool, MCP server, or template package - Asked about `NuGet/login@v1` or `id-token: write` -## Process +## 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. -> ⚠️ **Safety rule**: Never delete, remove, or overwrite anything without explaining the consequences and getting user confirmation first. This includes: removing API key secrets, deleting tags/releases, removing workflow steps, or changing package IDs. NuGet package IDs are permanent — mistakes can't be undone. +> ⚠️ **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. -### Phase 1: Discovery +## Process -Inspect the repo to understand what's being packaged and how. +### Phase 1: Assess -1. **Find packable projects**: Search for `.csproj` files. Check `IsPackable`, `PackAsTool`, `PackageType`, and `OutputType` properties. Also check `Directory.Build.props` for repo-wide settings. +Inspect the repo and report findings before making any changes. -2. **Classify each project** (check in this order): - - `Template` → **Template package** +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): + - `Template` → **Template** - `McpServer` → **MCP server** (also a dotnet tool) - `true` → **Dotnet tool** - - Class library (no `OutputType` or `IsPackable=true`) → **NuGet library** - - `Exe` without `PackAsTool` → Not a NuGet package, skip - -3. **Find existing workflows**: Search `.github/workflows/*.yml` for `dotnet nuget push`, `nuget push`, or `dotnet pack` steps. - -4. **Report findings** to the user before proceeding. + - Class library (`IsPackable=true` or no `OutputType`) → **Library** + - `Exe` without `PackAsTool` → Skip, not a NuGet package -> See [references/package-types.md](references/package-types.md) for per-type structural requirements and detection details. +2. **Validate structure** for each project's type: -### Phase 2: Structure Validation + | Type | Required | + |------|----------| + | All | `PackageId`, `Version` (in .csproj or Directory.Build.props) | + | Dotnet tool | `PackAsTool`, `ToolCommandName` | + | MCP server | `PackageType=McpServer`, `.mcp/server.json` included in package | + | Template | `PackageType=Template`, `.template.config/template.json` under content dir | -Verify the repo has the right MSBuild properties and supporting files for its package type. +3. **Find existing publish workflows** in `.github/workflows/` — look for `dotnet nuget push`, `nuget push`, or `dotnet pack`. -| Type | Check for | -|------|-----------| -| All | `PackageId`, `Version` in .csproj or Directory.Build.props | -| Dotnet tool | `PackAsTool`, `ToolCommandName` | -| MCP server | `PackageType=McpServer`, `.mcp/server.json` exists, included in package via `` | -| Template | `PackageType=Template`, `.template.config/template.json` exists under content dir | +4. **Check version consistency** — for MCP servers, verify `.csproj` `` matches both `server.json` version fields (root `version` and `packages[].version`). Flag any mismatch. -If anything is missing, offer to add it. Use `ask_user` to confirm before modifying project files. +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. -> ❌ **Don't skip `Directory.Build.props`** — package metadata is often set at the repo root, not in individual .csproj files. Missing it means reporting false negatives. +> ❌ See [references/package-types.md](references/package-types.md) for per-type details and required properties. -### Phase 3: Local Pre-Publish Testing +### Phase 2: Local Verification -Before configuring nuget.org, verify the package builds and works locally. +Pack and verify locally before touching nuget.org — publishing errors waste a permanent version number. -1. **Pack**: `dotnet pack -c Release -o ./artifacts` -2. **Verify the `.nupkg`** was created in `./artifacts/` -3. **For dotnet tools / MCP servers** — install from local and test: - ```bash - dotnet tool install -g --add-source ./artifacts {PackageId} - {ToolCommandName} --help # Verify it runs - dotnet tool uninstall -g {PackageId} - ``` -4. **For libraries** — verify the package contains expected assemblies: - ```bash - dotnet nuget locals all --list # Note the global-packages path - # Or unzip the .nupkg (it's a zip) and inspect lib/ - ``` +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) -> ❌ **Don't skip local testing** — discovering packaging errors after publishing wastes a version number (nuget.org IDs are permanent). +### Phase 3: nuget.org Policy -### Phase 4: nuget.org Policy Setup +This phase requires the user to act on nuget.org — guide them with exact values. -This is a manual step — guide the user through it with exact values. - -1. Extract repo owner and name from the git remote: - ```powershell - git remote get-url origin - # Parse: https://github.com/{owner}/{repo}.git or git@github.com:{owner}/{repo}.git - ``` +1. Determine the **repo owner**, **repo name**, and the **workflow filename** that will publish. -2. Identify the workflow filename (just the filename, not the path) that will do the publishing. + > ❌ 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. -3. Tell the user: - > Go to **nuget.org** → click your username → **Trusted Publishing** → **Add policy** +2. Guide the user to create the trusted publishing policy: + > Go to [**nuget.org/account/trustedpublishing**](https://www.nuget.org/account/trustedpublishing) → **Add policy** > - > Enter these values: > - **Repository Owner**: `{owner}` > - **Repository**: `{repo}` > - **Workflow File**: `{filename}.yml` - > - **Environment**: `{env}` *(only if the workflow uses `environment:`)* + > - **Environment**: `release` *(only if the workflow uses `environment:`; leave blank otherwise)* -4. Explain policy ownership: choose individual account or organization as owner. The policy applies to all packages owned by that entity. + Policy ownership: the user chooses individual account or organization. Org-owned policies apply to all packages owned by that org. -5. Note: for **private repos**, the policy starts as "temporarily active" for 7 days. It becomes permanent after the first successful publish. + For **private repos**: policy is "temporarily active" for 7 days — becomes permanent after the first successful publish. -6. **Create a GitHub Environment** for publish secret scoping: - > Go to **repo Settings** → **Environments** → **New environment** → name it `release` +3. Guide the user to create a **GitHub Environment** (recommended but optional — provides secret scoping + approval gates): + > Repo **Settings** → **Environments** → **New environment** → `release` > - > Then add a secret to this environment: - > - Click **Add environment secret** - > - **Name**: `NUGET_USER` - > - **Value**: your nuget.org username (NOT email) - > - > Optional: add **Required reviewers** for an approval gate before publishing. - - Environment-scoped secrets are only available to workflows referencing that environment — preventing accidental use in CI jobs. + > Add environment secret: **Name** = `NUGET_USER`, **Value** = nuget.org username (NOT email) -> ❌ **Don't guess the workflow filename** — the policy requires the exact filename (e.g., `publish.yml`), not the workflow `name:` field. Get it wrong and OIDC validation silently fails. + Optional: add **Required reviewers** for an approval gate. -> ⚠️ Wait for the user to confirm they've created the policy before proceeding to Phase 5. +> ⚠️ Wait for the user to confirm they've created the policy before proceeding. -### Phase 5: Workflow Setup +### Phase 4: Workflow Setup -Either modify an existing publish workflow or create a new one from scratch. +Create or modify the publish workflow. -**If no publish workflow exists** (greenfield): -- Create a new `publish.yml` using the complete template from [references/publish-workflow.md](references/publish-workflow.md) -- Adapt the template: set the correct .NET version, project path, and environment name -- The template uses tag-triggered publishing (`on: push: tags: ['v*']`) — the standard pattern +**Greenfield**: Create `publish.yml` from the template in [references/publish-workflow.md](references/publish-workflow.md). Adapt .NET version, project path, and environment name. -**If a publish workflow already exists**: -- Modify it in place following the steps below - -> ❌ **Don't delete the old API key secret** until trusted publishing is verified working. Keep it as a fallback. When the user is ready to remove it, explain that it's a one-way door (they'd need to regenerate on nuget.org) and wait for confirmation. +**Migration** (existing workflow with API key): Modify in place — 1. **Add OIDC permission and environment** to the publishing job: ```yaml jobs: publish: - environment: release # Uses the environment with NUGET_USER secret + environment: release permissions: - id-token: write # Required for NuGet trusted publishing + id-token: write # Required — without this, NuGet/login fails with 403 + contents: read # Explicit — setting permissions overrides defaults ``` - > ❌ **Forgetting `id-token: write`** is the most common mistake. Without it, the OIDC token request fails and `NuGet/login` will error with 403. - -2. **Add the NuGet login step** before the push step: +2. **Add the NuGet login step** before push: ```yaml - name: NuGet login (OIDC) id: login @@ -163,37 +128,32 @@ Either modify an existing publish workflow or create a new one from scratch. user: ${{ secrets.NUGET_USER }} # nuget.org profile name, NOT email ``` - > ❌ **Don't use an email address** for the `user` input — it must be the nuget.org profile/username. Recommend storing it as an environment secret for scoping (it's not truly sensitive but scoping prevents accidental use in CI jobs). - -3. **Replace the API key reference** in the push step: +3. **Replace the API key** in the push step: ```yaml - # Before: - --api-key ${{ secrets.NUGET_API_KEY }} - - # After: --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate ``` - Make sure the login step has `id: login` if referencing outputs by step ID. The `--skip-duplicate` flag makes pushes idempotent — safe for re-runs. -4. **Verify**: After pushing the workflow change, ask the user to trigger a publish and confirm the package appears on nuget.org. +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. -## Common Blockers +## Troubleshooting -| Problem | Cause | Action | -|---------|-------|--------| -| `NuGet/login` fails with 403 | Missing `id-token: write` permission | Add to job permissions, re-run | -| `NuGet/login` fails with "no matching policy" | Workflow filename or repo owner doesn't match policy | Verify exact filename on nuget.org (case-insensitive) | -| Push fails with unauthorized | Package ID not owned by policy account | Verify policy owner owns the package ID on nuget.org | -| Token expired | Workflow requested token too early (>1 hour before push) | Move `NuGet/login` step closer to the push step | -| Policy shows "temporarily active" | Private repo, no publish yet | Complete first publish within 7 days | -| `already_exists` on push | Re-running a publish for same version | Add `--skip-duplicate` to `dotnet nuget push` | -| GitHub Release creation 422 | Duplicate release for same tag | Explain the conflict. Recommend removing the release step from the workflow or deleting the duplicate release — but wait for confirmation before either | -| Re-run uses wrong workflow | `gh run rerun` replays the original YAML from the tag commit | Explain the situation to the user. Recommend: remove the obstacle (e.g., delete conflicting release), then re-run. Never delete and re-tag — NuGet package IDs are permanent. Wait for user confirmation before deleting anything. | +| 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** — don't loop on infrastructure issues. +> ⚠️ If any blocker persists after one fix attempt, **stop and ask the user**. ## References -- **Package type details**: See [references/package-types.md](references/package-types.md) for detection logic, required properties, and minimal .csproj examples per package type. -- **Publish workflow template**: See [references/publish-workflow.md](references/publish-workflow.md) for a complete tag-triggered publish workflow ready to adapt. +- **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) diff --git a/skills/nuget-trusted-publishing/references/package-types.md b/skills/nuget-trusted-publishing/references/package-types.md index d08a1f0940..5eb4ffe05e 100644 --- a/skills/nuget-trusted-publishing/references/package-types.md +++ b/skills/nuget-trusted-publishing/references/package-types.md @@ -89,6 +89,7 @@ A console app distributed as a global or local tool via `dotnet tool install`. | `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 From 78fe11f9414681208c92824c25deb80579715dc3 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 23 Feb 2026 17:18:33 -0600 Subject: [PATCH 3/7] Move skill to src/dotnet/skills/ and add eval tests - Move nuget-trusted-publishing from skills/ to src/dotnet/skills/ - Add eval.yaml with two scenarios: greenfield setup and API key migration - Skills are auto-discovered via plugin.json, no marketplace.json change needed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills}/nuget-trusted-publishing/SKILL.md | 0 .../references/package-types.md | 0 .../references/publish-workflow.md | 0 .../tests/nuget-trusted-publishing/eval.yaml | 36 +++++++++++++++++++ 4 files changed, 36 insertions(+) rename {skills => src/dotnet/skills}/nuget-trusted-publishing/SKILL.md (100%) rename {skills => src/dotnet/skills}/nuget-trusted-publishing/references/package-types.md (100%) rename {skills => src/dotnet/skills}/nuget-trusted-publishing/references/publish-workflow.md (100%) create mode 100644 src/dotnet/tests/nuget-trusted-publishing/eval.yaml diff --git a/skills/nuget-trusted-publishing/SKILL.md b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md similarity index 100% rename from skills/nuget-trusted-publishing/SKILL.md rename to src/dotnet/skills/nuget-trusted-publishing/SKILL.md diff --git a/skills/nuget-trusted-publishing/references/package-types.md b/src/dotnet/skills/nuget-trusted-publishing/references/package-types.md similarity index 100% rename from skills/nuget-trusted-publishing/references/package-types.md rename to src/dotnet/skills/nuget-trusted-publishing/references/package-types.md diff --git a/skills/nuget-trusted-publishing/references/publish-workflow.md b/src/dotnet/skills/nuget-trusted-publishing/references/publish-workflow.md similarity index 100% rename from skills/nuget-trusted-publishing/references/publish-workflow.md rename to src/dotnet/skills/nuget-trusted-publishing/references/publish-workflow.md diff --git a/src/dotnet/tests/nuget-trusted-publishing/eval.yaml b/src/dotnet/tests/nuget-trusted-publishing/eval.yaml new file mode 100644 index 0000000000..4914d07ba7 --- /dev/null +++ b/src/dotnet/tests/nuget-trusted-publishing/eval.yaml @@ -0,0 +1,36 @@ +scenarios: + - name: "Set up trusted publishing for a new NuGet library" + prompt: | + I have a .NET class library at src/MyLib/MyLib.csproj that I want to publish + to nuget.org using trusted publishing (OIDC) instead of an API key. + The repo is hosted on GitHub at myorg/mylib. Help me set this up. + assertions: + - type: "output_contains" + value: "NuGet/login" + - type: "output_contains" + value: "id-token" + - type: "output_matches" + pattern: "(trusted.?publishing|OIDC)" + rubric: + - "Guides the user to create a trusted publishing policy on nuget.org" + - "Mentions the workflow filename must match the nuget.org policy exactly" + - "Includes id-token: write in the workflow permissions" + - "Recommends local pack verification before publishing" + timeout: 120 + - name: "Migrate existing workflow from API key to trusted publishing" + prompt: | + My current publish workflow uses secrets.NUGET_API_KEY to push packages. + I want to switch to NuGet trusted publishing (OIDC). What do I need to change? + assertions: + - type: "output_contains" + value: "NuGet/login" + - type: "output_contains" + value: "id-token" + - type: "output_matches" + pattern: "(api.?key|API.?KEY|NUGET_API_KEY)" + rubric: + - "Explains how to replace the API key with NuGet/login OIDC step" + - "Mentions adding id-token: write permission to the job" + - "Advises keeping the old API key secret until trusted publishing is verified" + - "Mentions creating a trusted publishing policy on nuget.org" + timeout: 120 From 33458a2dd4d9f435908775a18cc0fda31411f009 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 26 Feb 2026 13:01:14 -0600 Subject: [PATCH 4/7] Improve greenfield scenario: add fast-path to avoid multi-turn assessment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill's phased process was too heavyweight for simple greenfield setups — the agent would spend turns on assessment and never create the workflow. Added fast-path guidance to combine phases for single-project repos, and strengthened Phase 2/4 to ensure key content (NuGet/login, id-token, local pack verification) always appears in the first response. Eval results improved from 2.2/5 (regression) to 4.2/5 (improvement). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/dotnet/skills/nuget-trusted-publishing/SKILL.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/dotnet/skills/nuget-trusted-publishing/SKILL.md b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md index a6ad188283..92766821c3 100644 --- a/src/dotnet/skills/nuget-trusted-publishing/SKILL.md +++ b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md @@ -36,6 +36,8 @@ Use this skill when: ## 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. @@ -68,6 +70,8 @@ Inspect the repo and report findings before making any changes. 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) @@ -103,9 +107,9 @@ This phase requires the user to act on nuget.org — guide them with exact value ### Phase 4: Workflow Setup -Create or modify the publish workflow. +Create or modify the publish workflow. **The workflow must always be created or shown in your response** — don't defer this to a later turn. -**Greenfield**: Create `publish.yml` from the template in [references/publish-workflow.md](references/publish-workflow.md). Adapt .NET version, project path, and environment name. +**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 — From 4c2ca18f4e93e28ed9ad55dc5e0631540fb2bc4f Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 26 Feb 2026 13:51:29 -0600 Subject: [PATCH 5/7] Add eval scenario for natural prompt without 'trusted publishing' keyword Tests that the skill activates when the user asks about secure keyless NuGet publishing without explicitly mentioning trusted publishing or OIDC. Scores +2.6 improvement over baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/nuget-trusted-publishing/eval.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/dotnet/tests/nuget-trusted-publishing/eval.yaml b/src/dotnet/tests/nuget-trusted-publishing/eval.yaml index 4914d07ba7..8f44dd9d7c 100644 --- a/src/dotnet/tests/nuget-trusted-publishing/eval.yaml +++ b/src/dotnet/tests/nuget-trusted-publishing/eval.yaml @@ -17,6 +17,24 @@ scenarios: - "Includes id-token: write in the workflow permissions" - "Recommends local pack verification before publishing" timeout: 120 + - name: "Set up NuGet publishing without mentioning trusted publishing" + prompt: | + I have a .NET library at src/MyLib/MyLib.csproj and I want to publish it + to nuget.org securely from GitHub Actions without storing API keys. + The repo is at myorg/mylib. How do I set this up? + assertions: + - type: "output_contains" + value: "NuGet/login" + - type: "output_contains" + value: "id-token" + - type: "output_matches" + pattern: "(trusted.?publishing|OIDC)" + rubric: + - "Identifies that OIDC/trusted publishing is the right approach for keyless publishing" + - "Creates or shows a workflow with NuGet/login and id-token: write" + - "Guides the user to configure a trusted publishing policy on nuget.org" + - "Recommends local pack verification before publishing" + timeout: 120 - name: "Migrate existing workflow from API key to trusted publishing" prompt: | My current publish workflow uses secrets.NUGET_API_KEY to push packages. From e4a36e2dd6230e802dadde38412f7ae918c2d5b4 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 26 Feb 2026 15:37:57 -0600 Subject: [PATCH 6/7] Apply review feedback: fix INVOKES and ToolCommandName consistency - Change INVOKES from 'powershell' to 'bash' (workflow runs on ubuntu) - Mark ToolCommandName as optional (defaults to assembly name), matching package-types.md reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/dotnet/skills/nuget-trusted-publishing/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotnet/skills/nuget-trusted-publishing/SKILL.md b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md index 92766821c3..a86beee99d 100644 --- a/src/dotnet/skills/nuget-trusted-publishing/SKILL.md +++ b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md @@ -5,7 +5,7 @@ description: > 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: powershell, edit, create, ask_user for guided repo setup. + INVOKES: bash, edit, create, ask_user for guided repo setup. --- # NuGet Trusted Publishing Setup @@ -54,7 +54,7 @@ Inspect the repo and report findings before making any changes. | Type | Required | |------|----------| | All | `PackageId`, `Version` (in .csproj or Directory.Build.props) | - | Dotnet tool | `PackAsTool`, `ToolCommandName` | + | 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 | From 3a5726a06dac46c4ed84a0cc4722e429d2d575c7 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 26 Feb 2026 18:59:46 -0600 Subject: [PATCH 7/7] Fix Exe package classification and make INVOKES shell-agnostic - Exe projects with IsPackable=true can be published as NuGet packages (application packages), not just tools - Changed INVOKES from 'bash' to 'shell (powershell or bash)' for cross-platform compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/dotnet/skills/nuget-trusted-publishing/SKILL.md | 5 +++-- .../nuget-trusted-publishing/references/package-types.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/dotnet/skills/nuget-trusted-publishing/SKILL.md b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md index a86beee99d..ebca1abb46 100644 --- a/src/dotnet/skills/nuget-trusted-publishing/SKILL.md +++ b/src/dotnet/skills/nuget-trusted-publishing/SKILL.md @@ -5,7 +5,7 @@ description: > 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: bash, edit, create, ask_user for guided repo setup. + INVOKES: shell (powershell or bash), edit, create, ask_user for guided repo setup. --- # NuGet Trusted Publishing Setup @@ -47,7 +47,8 @@ Inspect the repo and report findings before making any changes. - `McpServer` → **MCP server** (also a dotnet tool) - `true` → **Dotnet tool** - Class library (`IsPackable=true` or no `OutputType`) → **Library** - - `Exe` without `PackAsTool` → Skip, not a NuGet package + - `Exe` with `true` → **Application package** (not a tool, but still publishable) + - `Exe` without `PackAsTool` or `IsPackable` → Not packable by default (ask user if they intend to publish it) 2. **Validate structure** for each project's type: diff --git a/src/dotnet/skills/nuget-trusted-publishing/references/package-types.md b/src/dotnet/skills/nuget-trusted-publishing/references/package-types.md index 5eb4ffe05e..4c21bd6ef3 100644 --- a/src/dotnet/skills/nuget-trusted-publishing/references/package-types.md +++ b/src/dotnet/skills/nuget-trusted-publishing/references/package-types.md @@ -11,7 +11,8 @@ Inspect `.csproj` files (and `Directory.Build.props` if present) for these MSBui 2. Has McpServer? → MCP server (also a dotnet tool) 3. Has true? → Dotnet tool 4. Has true or no OutputType? → NuGet library -5. Has Exe without PackAsTool? → Not a NuGet package (skip) +5. Has Exe + true? → Application package +6. Has Exe without PackAsTool or IsPackable? → Not packable by default (ask user) ``` Check in order — MCP servers have `PackAsTool` too, so `PackageType` must be checked first. @@ -249,7 +250,7 @@ The `template.json` must include at minimum: `identity`, `name`, `shortName`, `t ## Common Gotchas -- **`IsPackable` defaults**: Class libraries default to `true`, console apps to `false`. If a console app should be a tool, set `PackAsTool` (which implies packable). +- **`IsPackable` defaults**: Class libraries default to `true`, console apps to `false`. Console apps can still be published as NuGet packages by setting `true` — 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.