Skip to main content

06. NuGet publish flow

This note documents the Mochi-as-.NET-library publish path: how mochi pkg publish --to=nuget.org lowers a Mochi package to a publishable .NET NuGet package and uploads it.

The library package shape

A .NET NuGet package intended for nuget.org distribution has the following structure:

mochi-emitted.nupkg (a ZIP archive)
[Content_Types].xml
_rels/.rels
package/
services/
metadata/
core-properties/
<guid>.psmdcp
MyMochiLib.nuspec # the package manifest
lib/
net8.0/
MyMochiLib.dll # the compiled .NET assembly
MyMochiLib.xml # XML documentation (optional)
icon.png # optional
README.md # optional

The user writes Mochi in <package>/src/*.mochi. The bridge lowers via TargetDotNetLibrary (a new MEP-53 build target) to a C# project, compiles it with dotnet build -c Release, and packages it with dotnet pack.

The emitted .csproj:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyMochiLib</AssemblyName>
<RootNamespace>MyMochiLib</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>MyMochiLib</PackageId>
<PackageVersion>1.0.0</PackageVersion>
<Authors>tamnd</Authors>
<Description>A Mochi library published as a NuGet package.</Description>
<PackageProjectUrl>https://github.com/example/mymochi</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>mochi</PackageTags>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="mochi-runtime-dotnet" Version="0.6.0" />
</ItemGroup>
</Project>

Metadata fields come from the Mochi mochi.toml [dotnet.publish] table. The package name is the Mochi package name (with @scope/name form, if present, flattened to scope.name since NuGet does not support scoped package identifiers).

src/MyMochiLib.cs is generated by lowering the Mochi package's public API. Mochi fun items become public static methods. Mochi record items become public record struct items. Mochi type T = A | B sum types become public abstract class T with nested derived types A and B. Private items become internal.

NuGet v3 API and resolution

The NuGet v3 API is a RESTful JSON API documented at https://docs.microsoft.com/en-us/nuget/api/overview. The bridge uses the following endpoints:

  • Service index (https://api.nuget.org/v3/index.json): returns a JSON document listing all v3 API endpoints. The bridge fetches this at lock time.
  • Search query service (a URL from the service index with resource type SearchQueryService/3.5.0): resolves package name + version constraint to a specific version.
  • Flat container (resource type PackageBaseAddress/3.0.0): the content-addressed .nupkg download URL, in the form <baseUrl>/<id>/<version>/<id>.<version>.nupkg.
  • Registration pages (resource type RegistrationsBaseUrl/3.6.0): package metadata, including .nuspec dependency groups, deprecation status, and vulnerability advisories.

The resolution process:

  1. Fetch the service index.
  2. For each [dotnet-dependencies] entry, query the search API with the package name and version constraint.
  3. Select the highest satisfying version (NuGet's default strategy; overridable via [dotnet.resolve] strategy = "lowest" in mochi.toml).
  4. Fetch the registration page for the resolved version to get the .nuspec dependency groups.
  5. Recurse into transitive dependencies until the full graph is resolved.
  6. Download each .nupkg to ~/.cache/mochi/dotnet-deps/<sha512-hex>/.

The .nuspec manifest

A .nuspec file is an XML manifest inside the .nupkg that declares package metadata:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>MyMochiLib</id>
<version>1.0.0</version>
<authors>tamnd</authors>
<description>A Mochi library published as a NuGet package.</description>
<projectUrl>https://github.com/example/mymochi</projectUrl>
<license type="expression">Apache-2.0</license>
<readme>README.md</readme>
<tags>mochi</tags>
<dependencies>
<group targetFramework="net8.0">
<dependency id="mochi-runtime-dotnet" version="0.6.0" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
</package>

The bridge generates this manifest from [dotnet.publish] in mochi.toml. Required fields: id, version, authors, description, license. Optional but strongly recommended: projectUrl, readme, tags.

The mochi pkg publish --to=nuget.org flow

$ mochi pkg publish --to=nuget.org
[1/6] Lowering package via TargetDotNetLibrary ... (2.1s)
[2/6] Compiling C# project ... (4.3s)
[3/6] Packing .nupkg ... (1.0s)
[4/6] Validating .nuspec metadata ... OK
[5/6] Obtaining GitHub Actions OIDC token (audience: nuget.org) ...
sub: repo:example/mymochi:ref:refs/tags/v1.0.0
[6/6] Uploading to nuget.org ... OK
Package published: https://www.nuget.org/packages/MyMochiLib/1.0.0

The --dry-run flag skips step 6:

$ mochi pkg publish --to=nuget.org --dry-run
...
[6/6] DRY RUN: would POST .nupkg to https://www.nuget.org/api/v2/package
Package: MyMochiLib 1.0.0
SHA-512: abc123...

nuget.org upload protocol

The nuget.org upload endpoint (v2-compatible, the currently supported endpoint) is:

PUT https://www.nuget.org/api/v2/package
Authorization: <token> (legacy) or X-NuGet-Protocol-Version: 4.1.0 (trusted publishing)
Content-Type: application/octet-stream
Body: raw .nupkg bytes

For trusted publishing (see 07-nuget-trusted-publishing), the Authorization header carries the OIDC token:

Authorization: Bearer <oidc-token>
X-NuGet-Protocol-Version: 4.1.0

nuget.org's server-side handler validates the OIDC token against the configured trusted publisher and processes the upload.

Metadata validation

Before upload the bridge validates:

  • package-id matches the NuGet package name regex [A-Za-z0-9_\-.]{1,64} (case-insensitive; no @scope/ form).
  • version parses as NuGet semver (SemVer 2.0.0 compliant).
  • license parses as an SPDX expression.
  • description is between 1 and 4000 characters.
  • The README.md file exists in the package root.
  • No file in the package exceeds 100 MB (nuget.org's per-file limit).
  • The total .nupkg size does not exceed 250 MB (nuget.org's package limit).

A validation failure exits before the upload step.

TargetDotNetLibrary emit path

TargetDotNetLibrary is a new MEP-53 build target. The driver's Build function gates on Driver.LibraryMode=true plus target == TargetDotNetLibrary. The emit path:

  1. Lowers the Mochi package's public surface to a C# file (src/<PackageId>.cs) containing public static methods, public record struct types, and public abstract class sum types.
  2. Generates the .csproj from [dotnet.publish].
  3. Runs dotnet build -c Release.
  4. Runs dotnet pack -c Release --no-build.
  5. Returns the path to the generated .nupkg.

The emit step preserves Mochi's module visibility model: only pub items in the Mochi package become public in the generated C#; non-pub items become internal.

Cross-references