CLI (Rust) GitHub Action (Docker) that scans .NET projects in a directory and updates the <dependencies> section in matching *.nuspec files according to PackageReference versions from the related .csproj (project name = <id> in nuspec metadata). Optionally updates package.json version and scoped npm dependencies.
CI/CD: pipeline diagram · distribution
Usage
Pin a release tag (recommended):
- uses: actions/checkout@v4
- uses: denis-peshkov/update-nuspec-action@v1
with:
dir: src/MyPackage
dir is relative to /github/workspace (repo root after checkout). An absolute path (starting with /) is used as-is.
Dry-run (report only, no file writes):
- uses: denis-peshkov/update-nuspec-action@v1
with:
dir: src/MyPackage
dryRun: true
Checklist (consumer workflow)
jobs:
update-nuspec:
runs-on: ubuntu-latest # linux/amd64; see Requirements
steps:
- uses: actions/checkout@v4
- uses: denis-peshkov/update-nuspec-action@v1
with:
dir: src/MyPackage # explicit folder — not "." unless you want the whole repo
dryRun: false # true = preview in logs, no writes
env:
CONSOLE_ANSI_COLOR: false # omit or true for colored log (default in image: true)
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
dir |
No | . |
Root folder to scan recursively for .csproj / .nuspec pairs and (when packageVersion is set) package.json, relative to /github/workspace. Prefer a package path (src/MyPackage); . scans the entire checkout including nested folders (tests, other packages). |
dryRun |
No | false |
true — full report in the log, no file changes ([DRY RUN]). |
packageVersion |
No | (empty) | SemVer for package.json version. Azure DevOps: $(GitVersion_SemVer) after gitversion/execute. Env fallback: PACKAGE_VERSION, GitVersion_SemVer. |
dependencyScope |
No | (empty) | npm package name prefix to set to ^packageVersion. Skipped when empty. |
imageTag |
No | (from @ref) |
GHCR tag override (2.0.117, latest, 2.0.118-preview). Default: @v2.0.117 → 2.0.117, @v2 → 2, @master → latest. Use for preview branch refs. |
Outputs
| Output | Description |
|---|---|
packageVersion |
Echo of the packageVersion input when it was provided. |
Behavior
- Recursively looks for
*.nuspecunderdir(all subfolders). - Loads
{id}.csprojfrom the same folder as each.nuspec, where{id}is<metadata><id>. - Flat nuspec — top-level
<dependency id="..." version="..." />under<dependencies>. Package list is taken forTargetFramework, or the first TFM fromTargetFrameworks. - Grouped nuspec —
<group targetFramework="net8.0">(and other TFMs). Each group is synced only with packages that apply to that TFM in the csproj:PropertyGroup Condition="'$(TargetFramework)' == 'net6.0'"(and similar) for version properties;PackageReferencewithVersion="$(PropertyName)"resolved per TFM;ConditiononPackageReference/ItemGroup, includingor(for example'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' or '$(TargetFramework)' == 'net8.0').
- Updates versions, adds packages from the csproj, removes dependencies that are not in the csproj for that TFM / flat list.
- Saved dependency order:
Cross.*, then*Boilerplate*, then*.Api.Contract*, then the rest (A–Z). - Console report: grouped nuspec — one block per
<group targetFramework="...">; flat nuspec — single block. Categories: deleted, updated, added, not changed. PrivateAssets="All"references (for example SourceLink) are not written to nuspec.- Exits with code
0if no.nuspecfiles are found (prints*.nuspec files not found!). - Prints an error if
dirdoes not exist (Path '…' is not valid!). - Dry-run — GitHub Action input
dryRun: true, or CLI flags--dry-run/-d/--demo(or positionaltrue): full report, no file save ([DRY RUN]in the log). package.json(optional) — whenpackageVersionis set: updates"version"in everypackage.jsonunderdir(skipsnode_modules). WhendependencyScopeis also set, aligns matching npm dependencies to^packageVersion.
Example multi-TFM project: UpdateNuspecTool.Tests/TestData/Cross.Messaging.csproj + Cross.Messaging.nuspec.
Example output
Colored log when CONSOLE_ANSI_COLOR=true (default in the action image). In dry-run mode (dryRun: true / --dry-run) the same report is printed, categories are shown in gray, and files are not saved ([DRY RUN] in the log).
1. Sync nuspec dependencies
For each .nuspec the tool prints a categorized diff against the sibling {id}.csproj:
| Category | Color | Meaning |
|---|---|---|
| Deleted references | red | In nuspec, not in csproj for this TFM |
| Updated references | yellow | Same package id, version changed (old -> new) |
| Added references | green | In csproj, missing from nuspec |
| Not changed references | gray | Same id and version |
Cross.Identity (config.nuspec + Cross.Identity.csproj):
- uses: denis-peshkov/update-nuspec-action@v1
with:
dir: Cross.Identity

2. Update package.json (built npm package)
With packageVersion and dependencyScope — updates version and scoped npm dependencies (e.g. client/dist/ui/package.json). After GitVersion execute:
- uses: gittools/actions/gitversion/[email protected]
id: gitversion
- uses: denis-peshkov/update-nuspec-action@v1
with:
dir: client/dist/my-app
packageVersion: ${{ env.GitVersion_SemVer }}
dependencyScope: '@guru/' # optional; empty = skip dependency alignment
Requirements
This action is a Docker container action (runs.using: docker in action.yml). GitHub runs it only on Linux runners; the image is linux/amd64 with a linux-x64 tool binary.
- Runner:
ubuntu-latest(recommended) or any linux/amd64 self-hosted host with Docker. windows-latest/macos-latest: not supported — container actions do not run on Windows or macOS hosted runners. Use a separate job onubuntu-latest(other jobs in the workflow may still use Windows).- Self-hosted ARM runners: not supported as-is — use
ubuntu-latest, or a self-hosted amd64 Linux agent, or dedicate one job toruns-on: ubuntu-latest. - Colored log output: enabled by default in the image (
CONSOLE_ANSI_COLOR=true). Override withenv: CONSOLE_ANSI_COLOR: falseon the step if needed.
On Windows: use the Rust CLI (cargo build --release in update-nuspec/, see CLI (local)) or the Azure DevOps extension (UpdateNuspec@1 on windows-latest).
Mixed workflow example (build on Windows, nuspec sync on Linux):
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# …
sync-nuspec:
runs-on: ubuntu-latest
steps:
- uses: denis-peshkov/update-nuspec-action@v1
with:
dir: src/MyPackage
Versioning (this repository)
GitVersion (GitVersion.yml) on push:
| Branch | SemVer (example) | Git tags | GHCR image | GitHub Release | Chocolatey | Homebrew | ADO extension |
|---|---|---|---|---|---|---|---|
master |
1.2.3 (stable) |
v1.2.3, v1.2, v1 |
:1.2.3, :1.2, :1, :latest |
Release (binaries + VSIX) | push (stable) | core PR / bump | Marketplace public |
release/*, hotfix/* |
1.3.0-preview.4 |
— | :1.3.0-preview.4 only |
— | push (prerelease) | preview tap (homebrew-preview-tap) |
VSIX in CI artifacts |
Preview branches publish GHCR, Chocolatey, and the Homebrew preview tap (branch homebrew-preview-tap, no git tags). Git tags and GitHub Release run on master only (moving @v tags and :latest stay on master). Pipeline details: docs/ci-cd.md.
The action is a composite wrapper: at runtime it resolves the GHCR tag from the action ref (@v2.0.117 → :2.0.117, @v2.0 → :2.0, @v2 → :2, @master → :latest) or from optional input imageTag.
uses: denis-peshkov/[email protected] # pulls ghcr.io/.../update-nuspec:2.0.117
uses: denis-peshkov/update-nuspec-action@v2 # pulls :2
uses: denis-peshkov/[email protected] # pulls :2.0
# preview from a branch ref — set imageTag explicitly:
uses: denis-peshkov/update-nuspec-action@release/my-feature
with:
imageTag: '2.0.118-preview'
dir: src/MyPackage
ADO Marketplace publish uses secret AZDO_MARKETPLACE_PAT (scope Marketplace (Publish)), publisher peshkov.
Azure DevOps extension
The same tool is available as pipeline task UpdateNuspec@1 on Visual Studio Marketplace. Usage, examples, inputs, and preview install: distribution/azure-devops-extension/marketplace/overview.md.
Development
Repository layout
| Path | Role |
|---|---|
update-nuspec/ |
Rust CLI and library (update-nuspec binary) |
UpdateNuspecTool/ |
Legacy .NET CLI (parity reference) |
UpdateNuspecTool.Tests/ |
NUnit tests; fixtures in TestData/ (.nuspec, .csproj, package.json) |
UpdateNuspecTool.slnx |
Solution (.NET projects + repo file links) |
GitVersion.yml |
SemVer for CI (version job) |
action.yml |
Public composite action: GHCR tag from @ref / imageTag, docker run |
distribution/github-action/ |
GHCR image + action helper: Dockerfile, entrypoint.sh, resolve-action-image-tag.sh |
distribution/ |
Other channels: homebrew-core/, homebrew-preview/, chocolatey/, azure-devops-extension/ |
distribution/azure-devops-extension/marketplace/ |
ADO Marketplace listing: overview.md, license.md, screenshots |
distribution/azure-devops-extension/task/ |
Pipeline task UpdateNuspec@1 (TypeScript + bundled binaries) |
.github/workflows/ci.yml |
CI orchestrator — docs/ci-cd.md |
.github/actions/version/ |
GitVersion → version, major, minor, prerelease |
.github/actions/build-release-binary/ |
Matrix cargo build --release; ado-binary-* / release-binary-* artifacts |
.github/actions/test/ |
Rust + .NET tests, SonarCloud |
.github/actions/push-tags/ |
Push git tags v{version}, v{X.Y}, v{X} (master only) |
.github/actions/build-github-action/ |
Docker image build + smoke tests |
.github/actions/publish-github-action/ |
GHCR push (github-action-image artifact) |
.github/actions/publish-ado-extension/ |
VSIX build + ADO Marketplace (master only) |
.github/actions/publish-chocolatey/ |
Chocolatey .nupkg pack + push |
.github/actions/publish-homebrew/ |
homebrew-core formula PR / bump (master only) |
.github/actions/publish-homebrew-tap/ |
Preview tap branch homebrew-preview-tap (release/*, hotfix/*) |
.github/actions/publish-release/ |
GitHub Release assets (master only) |
.github/scripts/ |
CI shell scripts — Scripts below |
docs/ |
ci-cd.md (pipeline), distribution.md (registries + ADO), examples/ |
update-nuspec-icon.png |
Project / marketplace icon |
LICENSE |
MIT |
Tests
cd update-nuspec && cargo test
dotnet restore UpdateNuspecTool.Tests/UpdateNuspecTool.Tests.csproj
dotnet test UpdateNuspecTool.Tests/UpdateNuspecTool.Tests.csproj --configuration Release --no-restore
Fixtures: UpdateNuspecTool.Tests/TestData/ (MyPackage.nuspec, Cross.Messaging.nuspec, package.json, …). In CI the same tests run in the test job — see docs/ci-cd.md.
CLI (local)
Package managers (after acceptance in the respective registries; binaries also on GitHub Releases):
brew install update-nuspec
Preview (from release/* / hotfix/*, branch homebrew-preview-tap, no git tag):
brew tap denis-peshkov/update-nuspec https://github.com/denis-peshkov/update-nuspec-action --branch homebrew-preview-tap
brew install update-nuspec-preview
choco install update-nuspec
First Homebrew submission: docs/distribution.md.
Options: --help / -h, --version / -v, --dry-run / -d / --demo (or positional true), --package-version / -pv, --dependency-scope / -ds.
cd update-nuspec
cargo run --bin update-nuspec -- --help
cargo run --bin update-nuspec -- --version
cargo run --bin update-nuspec -- ../UpdateNuspecTool.Tests/TestData --dry-run
cargo run --bin update-nuspec -- ../client/dist/my-app --package-version 1.2.3 --dependency-scope @guru/
Release build:
cd update-nuspec
cargo build --release --bin update-nuspec
./target/release/update-nuspec ../UpdateNuspecTool.Tests/TestData --dry-run
Windows (x64) — CI builds with x86_64-pc-windows-msvc (same binary for Release, Chocolatey, and ADO), or build natively on Windows:
cd update-nuspec
cargo build --release --bin update-nuspec
.\target\release\update-nuspec.exe ..\UpdateNuspecTool.Tests\TestData
Linux (x64) — used in the action Docker image and ubuntu-latest:
cd update-nuspec
cargo build --release --bin update-nuspec
./target/release/update-nuspec ../UpdateNuspecTool.Tests/TestData
| Platform | Binary |
|---|---|
| Linux x64 | update-nuspec |
| Windows x64 | update-nuspec.exe |
| macOS (local) | update-nuspec (cargo build --release) |
Docker image
The action pulls a prebuilt image from GHCR at runtime. CI builds it from the static musl binary and pushes tags X.Y.Z, X.Y, X, and latest on master. The composite action.yml maps your @ref (or imageTag input) to the GHCR tag.
Build locally (stage the binary first; Dockerfile only copies it):
cd update-nuspec && cargo build --release --target x86_64-unknown-linux-musl --bin update-nuspec && cd ..
mkdir -p distribution/github-action/docker
cp update-nuspec/target/x86_64-unknown-linux-musl/release/update-nuspec distribution/github-action/docker/update-nuspec
docker build --platform linux/amd64 -t update-nuspec-action:local distribution/github-action
docker run --rm --platform linux/amd64 \
-v "$PWD:/github/workspace" \
update-nuspec-action:local UpdateNuspecTool.Tests/TestData true
First release only: make the GHCR package public (
github.com/users/denis-peshkov/packages/container/update-nuspec/settings) souses: …@v1can pull it without auth.
On Apple Silicon hosts, use --platform linux/amd64 so the image matches GitHub-hosted runners.
CI (GitHub Actions)
Workflow: .github/workflows/ci.yml.
Диаграмма pipeline (jobs, артефакты, needs, события, секреты): docs/ci-cd.md.
Публикация в registries: docs/distribution.md.
License
MIT — see LICENSE.