Lib.Release
10.4.0
dotnet tool install --global Lib.Release --version 10.4.0
dotnet new tool-manifest
dotnet tool install --local Lib.Release --version 10.4.0
#tool dotnet:?package=Lib.Release&version=10.4.0
nuke :add-package Lib.Release --version 10.4.0
Lib.Release
A .NET global tool that packs one or more projects in a target repository and publishes them to
nuget.org. What to release is declared in a lib.release.json manifest that lives in the
target repo. The tool only publishes a project when its declared version differs from the latest version already on
nuget.org, so re-running it is safe and idempotent.
This README is written to help you (human or agent) author a lib.release.json for a repo you want to release —
see The release manifest. The rest documents the CLI and the release pipeline.
- Package id:
Lib.Release— Command:lib.release— Target framework:net10.0 - nuget.org: https://www.nuget.org/packages/lib.release
- Source: https://github.com/rzmoz/lib.release
Install
dotnet tool install --global Lib.Release
Requires the dotnet CLI on PATH (build/pack/push), and git on PATH when the target is a git repository
(for the working-tree check). A non-git target skips the working-tree check and does not need git. The tool has no
other runtime dependencies.
Usage
lib.release <Lib> [--ApiKey <key>] [--SkipTests] [-v|-d]
| Argument / Option | Required | Description |
|---|---|---|
<Lib> |
yes | The target repo. Either a bare name (resolved under the projects dir — env LIB_RELEASE_PROJECTS_DIR, default c:\projects) or a fully rooted path. Must exist. |
--ApiKey <key> |
yes* | nuget.org API key with push permission. Falls back to the NUGET_API_KEY environment variable; an explicit --ApiKey overrides the env var. |
--SkipTests |
no | Skip the test step. Default false. |
-v, --verbose |
no | Verbose logging (trace level). |
-d, --debug |
no | Debug logging. |
Options are case-insensitive. \* Required as either the flag or the NUGET_API_KEY env var — the run exits with
InvalidArguments if neither is set.
Setup commands (no API key needed)
These modes are self-contained and non-interactive — an agent with only the installed tool (no source access) can discover and bootstrap everything from them:
| Command | What it does |
|---|---|
lib.release --help |
Usage, the getting-started sequence, and the manifest essentials. |
lib.release --check [dir] |
Pre-flight (read-only): reports whether the repo is releasable and lists what would be released / tested / skipped. Exits Incompatible (13) if nothing is releasable. Run this before authoring a manifest. Default dir: current. |
lib.release --init <dir> |
Scaffolds lib.release.json into <dir>. Greedy: includes every releasable project and every test project found. --exclude <names> (comma/space list) leaves projects out; --force overwrites an existing file. |
lib.release --migrate <dir> |
Converts a legacy (name-based) lib.release.json to the path-based format in place, preserving versions. Writes nothing unless every entry resolves to a real .csproj. |
lib.release --schema |
Prints the JSON Schema for lib.release.json to stdout (e.g. lib.release --schema > lib.release.schema.json). Every field self-describes. |
Typical agent flow in a target repo: --check (is it compatible?) → --init (write the manifest) → review
versions → release.
The release manifest: lib.release.json
lib.release.json is the only thing you author to make a repo releasable. It lists the projects to publish and the
test projects to run. The tool reads the manifest in the target repo — not its own.
Where it goes
Put it anywhere in the repo; the tool searches recursively under <Lib> and uses the first match, so keep exactly
one. The repository root is the conventional location.
Schema
A JSON Schema ships with the tool's source at
lib.release.schema.json. Reference it from
your manifest for editor validation and inline field docs — the tool ignores the $schema key:
{
"$schema": "https://raw.githubusercontent.com/rzmoz/lib.release/main/lib.release.schema.json",
"releases": [ { "project": "src/MyLib/MyLib.csproj", "version": "1.0.0", "prerelease": "" } ],
"tests": [ "test/MyLib.Tests/MyLib.Tests.csproj" ]
}
Projects are referenced by their .csproj path relative to the repo root — there is no file-naming or
folder-layout convention.
Migrating from the old format: earlier manifests identified projects by
name(resolved by a<repo>/<name>/<name>.csprojconvention). That format is no longer supported — the tool detects it and exitsStaleManifest(14), and--checkflags it. Runlib.release --migrate <dir>to convert it in place, preserving versions (it maps eachnameto its.csprojpath), then commit. (To rebuild from scratch instead, uselib.release --init <dir> --force.)
| Field | Type | Required | Description |
|---|---|---|---|
releases |
array | yes | One entry per project to pack and publish. |
releases[].project |
string | yes | Path to the project's .csproj, relative to the repo root (forward slashes), e.g. src/Foo/Foo.csproj. The nuget package id is read from the csproj (<PackageId> → <AssemblyName> → file name). |
releases[].version |
string | yes | Major.Minor.Patch (e.g. 1.4.0). Bump it to publish a new version. |
releases[].prerelease |
string | no | SemVer 2.0 pre-release label without the leading - (e.g. beta, rc.1). "" or omitted = stable. |
tests |
array of string | no | Paths (relative to the repo root) of test-project .csproj files to run before packing. Omit or [] for none. |
Unknown keys are ignored by the tool, but the schema rejects them so authoring mistakes surface in your editor.
Generating a manifest
The fastest path is lib.release --init <dir>, which applies the rules below greedily and writes the file (preview
first with lib.release --check <dir>). To produce or review one by hand, work through these steps — they are exactly
what --init automates:
- Find the packable projects. Look at every
*.csprojin the repo (any folder, any depth) and keep only those that produce a NuGet package:- Include class libraries with packaging metadata, and tools with
<PackAsTool>true</PackAsTool>. - Exclude test projects (
*.Tests, or those with<IsPackable>false</IsPackable>), executable/sample apps (<OutputType>Exe</OutputType>console projects you don't ship), and anything else not meant for nuget.org.
- Include class libraries with packaging metadata, and tools with
- Emit one
releasesentry per packable project. Setprojectto the csproj path relative to the repo root (e.g.src/Foo/Foo.csproj). No naming convention — the package id is taken from the csproj. - Set the version. Use the project's intended next
Major.Minor.Patch. Setprereleaseonly for pre-release builds. (The committed.csprojversion can stay a placeholder like0.0.0; the tool injects the manifest version at release time — see pipeline.) - List test projects in
tests[]by csproj path. - Add the
$schemaline so editors validate the file. - Save it at the repo root.
Then verify, before committing:
- every
releases[].projectpath points to an existing.csproj; - each release project carries packable metadata and ships its README (see packable csproj);
- every
tests[]path points to an existing.csproj; - every
versionisMajor.Minor.Patch.
Examples
Multi-package library repo — DotNet.Basics
Releases three libraries and runs one test project. Note that the repo also contains a DotNet.Basics.Cli.Console
sample app — it is deliberately not in releases because it isn't a published package.
{
"$schema": "https://raw.githubusercontent.com/rzmoz/lib.release/main/lib.release.schema.json",
"releases": [
{ "project": "DotNet.Basics/DotNet.Basics.csproj", "version": "12.12.3", "prerelease": "" },
{ "project": "DotNet.Basics.Pipelines/DotNet.Basics.Pipelines.csproj", "version": "12.11.1", "prerelease": "" },
{ "project": "DotNet.Basics.Cli/DotNet.Basics.Cli.csproj", "version": "12.15.1", "prerelease": "" }
],
"tests": [
"DotNet.Basics.Tests/DotNet.Basics.Tests.csproj"
]
}
Single-package tool repo — this repo (Lib.Release)
One PackAsTool package, releasing itself, with one test project:
{
"$schema": "./lib.release.schema.json",
"releases": [
{ "project": "Lib.Release/Lib.Release.csproj", "version": "10.3.0", "prerelease": "" }
],
"tests": [
"Lib.Release.Tests/Lib.Release.Tests.csproj"
]
}
Project requirements
Referencing projects
A release entry's project is just the .csproj path relative to the repo root (forward slashes), e.g.
src/Foo/Foo.csproj. Any layout works — flat, src/, nested, whatever. The package id is whatever the csproj
produces: <PackageId> if set, else <AssemblyName>, else the file name. That package id is what the tool looks up on
nuget.org (case-insensitively) to decide whether the version already exists.
Packable csproj metadata
Each release project should carry standard NuGet packaging metadata so it produces a valid package. The version
fields can stay as placeholders (e.g. 0.0.0) — the tool overwrites them transiently at release time. Example from
DotNet.Basics:
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Description>...</Description>
<Authors>rzmoz</Authors>
<RepositoryUrl>https://github.com/rzmoz/DotNet.Basics</RepositoryUrl>
<Version>0.0.0</Version>
<AssemblyVersion>0.0.0</AssemblyVersion>
<FileVersion>0.0.0</FileVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="**/*.tmp" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<None Include="README.md" Pack="true" .../>together with<PackageReadmeFile>README.md</PackageReadmeFile>ships the per-project README into the package (required by nuget.org for a README to render).<None Remove="**/*.tmp" />keeps the transient*.csproj.tmpbackups the tool creates out of the package.
Test projects
Each entry in tests[] is a .csproj path (relative to the repo root). The tool runs each in release
configuration and fails the release if any test fails.
Clean git working tree
If the target is a git repo, all changes must be committed before releasing — a dirty working tree aborts the run. A non-git target skips this check.
What it does (pipeline)
The tool runs these steps in order against the target repo. The first step to fail stops the run and the tool exits with the matching code from the exit-codes table — it reports the reason and never dumps a stack trace for a known failure.
| # | Step | Behavior | Failure code |
|---|---|---|---|
| 1 | Init for release | If <Lib> is a git repo, asserts the working tree is clean (a non-git directory skips this check and proceeds); loads lib.release.json (first match found recursively under <Lib>); then resolves each releases[].project path to an existing csproj. |
DirtyWorkingTree, GitNotAvailable, ReleaseManifestNotFound, ProjectFileNotFound |
| 2 | Init versions | For each declared release, queries nuget.org for the latest published version. If the declared version+prerelease equals what is already published, that release is dropped. |
NoReleaseCandidates (when nothing is left to release) |
| 3 | Apply version | For each release, backs up the csproj at the project path and patches <Version> (SemVer 2.0) plus <AssemblyVersion> and <FileVersion> (Major.Minor.Patch) into the first <PropertyGroup>, creating the nodes if absent. (The path is resolved and existence-checked in step 1.) |
ProjectFileFormatNotRecognized |
| 4 | Run tests | Unless --SkipTests, runs dotnet test "<project>" -c release for each tests[] path. |
TestProjectNotResolved, TestsFailed |
| 5 | Pack nugets | Cleans each project's output dir, then runs dotnet pack "<csproj>" -c release --force -o <projDir>/bin/.nuget. |
PackFailed |
| 6 | Push nugets | Runs dotnet nuget push <pkg>.nupkg --api-key <key> --source https://api.nuget.org/v3/index.json --skip-duplicate for each produced package. --skip-duplicate makes re-pushing an existing version succeed, which is what keeps the tool idempotent. |
PushFailed |
Important: the csproj version edits from step 3 are transient. When the run finishes (success or failure), every
patched .csproj is restored from its backup. The release version is sourced from lib.release.json, applied only for
the duration of the pack/push, then reverted — your committed csproj files keep their original (placeholder) version.
Exit codes
Every code is in the 0–255 range. 0 is success; anything else is a distinct failure reason.
| Code | Name | Meaning |
|---|---|---|
| 0 | Success |
All requested packages released (or nothing needed releasing on a re-run). |
| 1 | UnexpectedError |
An unhandled error, or an exit code the host produced outside 0–255. |
| 2 | InvalidArguments |
Missing/invalid CLI args (e.g. no <Lib>, or no API key). |
| 3 | DirtyWorkingTree |
Uncommitted changes in the target git repo. |
| 4 | ReleaseManifestNotFound |
No lib.release.json found under <Lib>. |
| 5 | NoReleaseCandidates |
Everything declared is already published. Logged as a warning. |
| 6 | ProjectFileNotFound |
A releases[].project path does not point to an existing .csproj. |
| 7 | ProjectFileFormatNotRecognized |
The csproj has no <Project>/<PropertyGroup>. |
| 8 | TestProjectNotResolved |
A tests[] path does not point to an existing .csproj. |
| 9 | TestsFailed |
dotnet test returned non-zero. |
| 10 | PackFailed |
dotnet pack returned non-zero. |
| 11 | PushFailed |
dotnet nuget push returned non-zero. |
| 12 | GitNotAvailable |
<Lib> is a git repo but git was not found on PATH. |
| 13 | Incompatible |
--check/--init found no packable .csproj anywhere in the repo. |
| 14 | StaleManifest |
The lib.release.json uses the old name-based format. Regenerate with --init --force. |
Release workflow
- Bump
version(and/orprerelease) inlib.release.jsonfor the projects you want to publish. - Commit the change (if the repo is under git, the working tree must be clean).
- Run
lib.release <repo> --ApiKey <key>.
Only projects whose manifest version differs from what is already on nuget.org are packed and pushed; everything else is skipped. If no project needs releasing, the run exits early.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.4.0 | 133 | 5/27/2026 |
| 10.3.0 | 106 | 5/27/2026 |
| 10.2.0 | 115 | 5/27/2026 |
| 10.1.31 | 112 | 5/8/2026 |
| 10.1.30 | 103 | 5/8/2026 |
| 10.1.23 | 99 | 5/6/2026 |
| 10.1.22 | 105 | 5/6/2026 |
| 10.1.21 | 91 | 5/6/2026 |
| 10.1.20 | 110 | 5/6/2026 |
| 10.1.12 | 87 | 5/6/2026 |
| 10.1.11 | 89 | 5/6/2026 |
| 10.1.10 | 93 | 5/6/2026 |
| 10.1.9 | 101 | 5/4/2026 |
| 10.1.8 | 112 | 5/4/2026 |
| 10.1.7 | 92 | 5/4/2026 |
| 10.1.6 | 92 | 5/4/2026 |
| 10.1.5 | 95 | 5/4/2026 |
| 10.1.4 | 98 | 5/4/2026 |
| 10.1.3 | 104 | 5/4/2026 |
| 10.1.2 | 93 | 5/4/2026 |