diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..a4388bf
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,53 @@
+# Publishes the dvx.cli NuGet package when a version tag (e.g. v1.11.0) is pushed.
+# Requires a repository secret named NUGET_API_KEY.
+
+name: Publish to NuGet
+
+on:
+ push:
+ tags: [ "v*" ]
+
+jobs:
+ publish:
+ runs-on: windows-latest
+
+ permissions:
+ contents: read
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.0.x
+
+ # Fail fast if the pushed tag and the project version have drifted apart.
+ - name: Verify tag matches project version
+ shell: pwsh
+ run: |
+ $tag = $env:GITHUB_REF_NAME -replace '^v', ''
+ [xml]$csproj = Get-Content src/dvx/dvx.csproj
+ $version = $csproj.Project.PropertyGroup.Version | Where-Object { $_ } | Select-Object -First 1
+ Write-Host "Tag version: $tag"
+ Write-Host "Project version: $version"
+ if ($tag -ne $version) {
+ Write-Error "Tag $env:GITHUB_REF_NAME does not match $version in src/dvx/dvx.csproj"
+ exit 1
+ }
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Test
+ run: dotnet test --verbosity normal
+
+ - name: Pack
+ run: dotnet pack src/dvx/dvx.csproj -c Release -o out
+
+ - name: Push to NuGet
+ run: >
+ dotnet nuget push out/*.nupkg
+ --api-key ${{ secrets.NUGET_API_KEY }}
+ --source https://api.nuget.org/v3/index.json
+ --skip-duplicate
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..57be918
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,61 @@
+# Contributing
+
+Thanks for your interest in dvx. This guide covers how to build, test, and ship changes.
+
+## Prerequisites
+
+- [.NET 9 SDK](https://dotnet.microsoft.com/download)
+- A Dataverse environment if you want to test commands end to end.
+
+## Project layout
+
+- `src/dvx` — the CLI tool (`dvx.cli`), targets `net9.0`
+- `src/dvx.PluginAttributes` — the `[PluginStep]` attributes package, multi-targets `net462;net471;net9.0`
+- `src/dvx.Tests` — xUnit test project
+
+## Build and test
+
+```powershell
+dotnet build
+dotnet test
+```
+
+Run a single test by name:
+
+```powershell
+dotnet test --filter "FullyQualifiedName~PluginDiscoveryTests"
+```
+
+Please make sure `dotnet test` passes before opening a pull request — CI runs it on every PR.
+
+## Installing your local build
+
+```powershell
+dotnet tool uninstall -g dvx.cli;
+dotnet build -c Release;
+dotnet tool install -g dvx.cli --source src\dvx\bin\Release --no-cache
+```
+
+## Pull requests
+
+- Branch off `main` and target `main`.
+- Keep changes focused; one concern per PR.
+- Add or update tests for behavior changes.
+- CI (`.NET Quality Gate`) runs build, tests, and Sonar analysis — it must pass before merge.
+
+## Releasing
+
+Releases are published to NuGet by CI when a version tag is pushed. Maintainers only.
+
+1. Bump `` in `src/dvx/dvx.csproj` and update ``.
+2. Commit to `main`.
+3. Tag and push:
+
+ ```powershell
+ git tag v1.12.0
+ git push origin v1.12.0
+ ```
+
+4. The `Publish to NuGet` workflow builds, tests, packs, and pushes the package.
+
+The workflow fails if the tag does not match `` in `src/dvx/dvx.csproj`, so bump the version before tagging.
diff --git a/README.md b/README.md
index 81ba1a0..ce74b99 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ Commands are grouped by artifact type:
| Requirement | Notes |
|---|---|
-| [.NET 8 SDK](https://dotnet.microsoft.com/download) | Runtime for dvx itself |
+| [.NET 9 SDK](https://dotnet.microsoft.com/download) | Runtime for dvx itself |
| Dataverse service principal | ClientId + ClientSecret with the **Dynamics CRM System Administrator** or other role with privileges allowing plugin / web-resource deployment. For local development you can instead sign in through the browser — see [Interactive login](#interactive-login-local-development) |