Publishing to nuget.org

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

Publishing is one command, but a permanent one: a version pushed to nuget.org can be unlisted and deprecated, never replaced or (in practice) deleted. This page covers the push itself, what nuget.org checks afterwards and how to rehearse the whole round trip first. The round trip was run against the BaGetter 1.6.5 feed on localhost:5555 with .NET SDK 10.0.402 (NuGet 7.9.0); every nuget.org command is "not run here", with placeholder keys. Getting a key is on nuget.org Accounts, Organizations and API Keys; pushing from CI without a stored key on Trusted Publishing.

Pushing

Three ways to get a package onto nuget.org:

  • The web portal — Upload on nuget.org, select the .nupkg, review the metadata (with a README preview) and Submit. Good for a first, manual push.

  • dotnet nuget push — the CLI form, for scripts and CI:

    dotnet nuget push artifacts/Ledgerline.1.0.1.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
  • nuget push (nuget.exe 4.1.0+), with a key saved by nuget setApiKey.

The dotnet nuget push options:

Option Meaning

-s, --source

Server URL or a source name from nuget.config; a folder path copies the file.

-k, --api-key

The key; NUGET_API_KEY since NuGet 7.6.

--skip-duplicate

Treat 409 Conflict (version exists) as success — for re-running a release job.

-ss, --symbol-source; -sk, --symbol-api-key

Where .snupkg files go; by default the source’s symbol endpoint.

-n, --no-symbols

Don’t push symbols even if present.

-t, --timeout

Seconds per push; 300 by default (0 also means the default).

--configfile

Use only this nuget.config (.NET 9 SDK+).

--interactive, --no-service-endpoint

Allow credential prompts; don’t append api/v2/package to the URL.

Wildcards push many packages at once: dotnet nuget push "artifacts/*.nupkg" --skip-duplicate.

The round trip, rehearsed on BaGetter

The Ledgerline repository’s nuget.config lists the BaGetter feed as ledgerline-bagetter (HTTP, so with allowInsecureConnections="true", acceptable only on localhost). Pack and push:

dotnet pack src/Ledgerline -o artifacts/rel -p:Version=1.0.1 -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
dotnet nuget push artifacts/rel/Ledgerline.1.0.1.nupkg -s ledgerline-bagetter -k "$BAGETTER_API_KEY"
Pushing Ledgerline.1.0.1.nupkg to 'http://localhost:5555/api/v2/package'...
  PUT http://localhost:5555/api/v2/package/
  Created http://localhost:5555/api/v2/package/ 86ms
Your package was pushed.
Pushing Ledgerline.1.0.1.snupkg to 'http://localhost:5555/api/v2/symbol'...
  PUT http://localhost:5555/api/v2/symbol/
  Created http://localhost:5555/api/v2/symbol/ 60ms
Your package was pushed.

A consumer, Ledgerline.Report, referencing Ledgerline 1.0.1 with source mapping to the same feed — the first restore failed:

error NU1102: Unable to find package Ledgerline with version (>= 1.0.1)
error NU1102:   - Found 1 version(s) in ledgerline-bagetter [ Nearest version: 1.0.0 ]
error NU1102:   - Versions from nuget.org were not considered

The feed already listed 1.0.1 (curl .../v3/package/ledgerline/index.json returned {"versions":["1.0.0","1.0.1"]}); the machine’s HTTP cache still held the version list from an earlier restore, valid for 30 minutes. A restore with --no-http-cache found it and the program printed the assembly version, 1.0.1.0. In CI that tests a just-published package, use --no-http-cache.

Pushing the same version again, then with --skip-duplicate:

Pushing Ledgerline.1.0.1.nupkg to 'http://localhost:5555/api/v2/package'...
  PUT http://localhost:5555/api/v2/package/
  Conflict http://localhost:5555/api/v2/package/ 63ms
To skip already published packages, use the option --skip-duplicate
error: Response status code does not indicate success: 409 (Conflict).

Pushing Ledgerline.1.0.1.nupkg to 'http://localhost:5555/api/v2/package'...
  PUT http://localhost:5555/api/v2/package/
  Conflict http://localhost:5555/api/v2/package/ 62ms
Package '.../artifacts/rel/Ledgerline.1.0.1.nupkg' already exists at feed 'http://localhost:5555/api/v2/package'.

The first exited with 1, the second with 0. nuget.org also answers 409 for an existing version; the exact message text it returns is not documented, so the BaGetter output stands in for it here.

What happens after the push

sequenceDiagram participant CI as dotnet nuget push participant G as nuget.org gallery participant V as Validation participant I as Search and V3 indexes CI->>G: PUT /api/v2/package (API key) G-->>CI: 201 Created G->>V: package ID, signatures, malware scan, metadata alt validation fails V-->>G: failed, owners e-mailed, not published else validation passes V->>G: repository-signed G->>I: catalog, registrations, flat container I-->>CI: available to search and restore, usually within 15 minutes end

The push returns as soon as the gallery accepts the upload; nuget.org then validates the package asynchronously (signature requirements, malware scanning, symbol checks) and adds its repository signature before the package shows up in search and restore. That usually takes under 15 minutes; after an hour, check status.nuget.org and then contact support from the package page.

Limits:

  • Package ID — letters, digits, dots and dashes, no consecutive dots or dashes, at most 100 characters. The publishing page lets an ID start with an underscore; the NU5052 reference says letter or digit only (Package Metadata).

  • Size — about 250 MB; keep packages under 1 MB if possible and split assets into dependency packages. Larger uploads fail with error: Response status code does not indicate success: 413 (The package file exceeds the size limit. Please try again.). (quoted from the documentation).

  • Metadata is immutable. Description, README, licence and dependencies cannot be edited after upload — signed content includes the .nuspec. A fix is a new version.

nuget.org runs a separate test site, int.nugettest.org, with its own accounts and keys; its V3 endpoint is https://apiint.nugettest.org/v3/index.json. Pushing there first shows the real validation, README rendering and search listing without making anything public on nuget.org:

dotnet nuget push artifacts/Ledgerline.1.0.1.nupkg -k "$NUGETTEST_API_KEY" -s https://apiint.nugettest.org/v3/index.json

ID prefix reservation

An owner can reserve an ID prefix such as Ledgerline.*. nuget.org then refuses new packages with that prefix from other accounts, and shows a verified checkmark on the owner’s packages there and in Visual Studio. Advanced options: delegating sub-prefixes (Ledgerline.Build.*) to other owners, and public prefixes that keep the checkmark but stay open to submissions (for community projects). Packages that other owners already had under the prefix stay, without the checkmark, and can still get new versions. The Visual Studio indicator needs VS 2017 15.4+ and a single selected package source.

Apply by e-mail to account@nuget.org with the owner and the prefixes. The criteria: the prefix clearly identifies the owner; it is not a common or generic word and has at least four characters; and not reserving it would cause confusion. Packages under a reserved prefix are expected to follow the authoring best practices — consistent authors, a license element (not licenseUrl) and an embedded icon. Disputes go to support@nuget.org.

Account security

The library guidance: ✔️ DO enable two-factor authentication and ✔️ DO enable e-mail notification when a package is published, so an unexpected version is noticed. The same page (2018) still recommends MyGet for CI prereleases and says nothing about Trusted Publishing, scoped keys or signing; GitHub Packages, Azure Artifacts or a self-hosted feed (Private Feeds) take that role today.

Common mistakes

  • Publishing straight from a laptop — use a CI job, ideally with Trusted Publishing.

  • No --skip-duplicate in a re-runnable job — a retried job fails on the packages that already went out.

  • Testing a just-pushed version through the HTTP cache — NU1102 for half an hour; use --no-http-cache.

  • A 300 MB package — 413; move native or content assets into dependency packages.

  • Fixing a typo by re-uploading — impossible; publish a new version.