diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml
new file mode 100644
index 0000000..6601963
--- /dev/null
+++ b/.github/workflows/publish-nuget.yml
@@ -0,0 +1,111 @@
+name: Publish NuGet packages
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+ environment: release
+ permissions:
+ contents: read
+ id-token: write
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Resolve package version
+ id: version
+ shell: bash
+ run: |
+ set -euo pipefail
+ version="${GITHUB_REF_NAME#v}"
+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ ]]; then
+ echo "Tag '$GITHUB_REF_NAME' does not produce a valid package version."
+ exit 1
+ fi
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+
+ - name: Restore
+ working-directory: MagicSettings
+ run: dotnet restore MagicSettings.sln
+
+ - name: Build
+ working-directory: MagicSettings
+ run: dotnet build MagicSettings.sln --configuration Release --no-restore
+
+ - name: Test
+ working-directory: MagicSettings
+ run: dotnet test MagicSettings.sln --configuration Release --no-build --logger "console;verbosity=normal"
+
+ - name: Pack
+ working-directory: MagicSettings
+ run: >-
+ dotnet pack MagicSettings.sln
+ --configuration Release
+ --no-build
+ --output ../artifacts/packages
+ -p:PackageVersion=${{ steps.version.outputs.version }}
+
+ - name: Validate package output
+ shell: bash
+ run: |
+ set -euo pipefail
+ version="${{ steps.version.outputs.version }}"
+ package_dir="artifacts/packages"
+
+ test -f "$package_dir/MagicSettings.$version.nupkg"
+ test -f "$package_dir/MagicSettings.Server.$version.nupkg"
+
+ if find "$package_dir" -maxdepth 1 -name 'MagicSettings.Share.*.nupkg' | grep -q .; then
+ echo "MagicSettings.Share must not be published as a standalone package."
+ exit 1
+ fi
+
+ package_count=$(find "$package_dir" -maxdepth 1 -name '*.nupkg' | wc -l)
+ if [[ "$package_count" -ne 2 ]]; then
+ echo "Expected exactly two .nupkg files, found $package_count."
+ find "$package_dir" -maxdepth 1 -type f -print
+ exit 1
+ fi
+
+ for package in \
+ "$package_dir/MagicSettings.$version.nupkg" \
+ "$package_dir/MagicSettings.Server.$version.nupkg"; do
+ if ! unzip -l "$package" | grep -q 'lib/net10.0/MagicSettings.Share.dll'; then
+ echo "$package does not contain lib/net10.0/MagicSettings.Share.dll."
+ exit 1
+ fi
+ done
+
+ - name: Upload package artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-packages-${{ steps.version.outputs.version }}
+ path: artifacts/packages/*.*nupkg
+ if-no-files-found: error
+
+ - name: NuGet login
+ id: login
+ uses: NuGet/login@v1
+ with:
+ user: ${{ secrets.NUGET_USER }}
+
+ - name: Publish packages
+ shell: bash
+ run: >-
+ dotnet nuget push "artifacts/packages/*.nupkg"
+ --api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
+ --source https://api.nuget.org/v3/index.json
+ --skip-duplicate
diff --git a/MagicSettings/Directory.Build.props b/MagicSettings/Directory.Build.props
index 4bd9c1a..4f01e4b 100644
--- a/MagicSettings/Directory.Build.props
+++ b/MagicSettings/Directory.Build.props
@@ -8,5 +8,20 @@
true
$(NoWarn);1591
true
+
+ magiccodingman
+ magiccodingman
+ https://github.com/magiccodingman/MagicSettings
+ git
+ Apache-2.0
+ README.md
+ true
+ true
+ snupkg
+ true
+
+
+
+
diff --git a/MagicSettings/Directory.Build.targets b/MagicSettings/Directory.Build.targets
new file mode 100644
index 0000000..3274810
--- /dev/null
+++ b/MagicSettings/Directory.Build.targets
@@ -0,0 +1,18 @@
+
+
+ $(TargetsForTfmSpecificBuildOutput);IncludeMagicSettingsShareInPackage
+
+
+
+
+ <_MagicSettingsShareAssembly Include="@(ReferenceCopyLocalPaths)"
+ Condition="'%(ReferenceCopyLocalPaths.Filename)' == 'MagicSettings.Share' and '%(ReferenceCopyLocalPaths.Extension)' == '.dll'" />
+
+
+
+
+
+
diff --git a/MagicSettings/MagicSettings.Server/MagicSettings.Server.csproj b/MagicSettings/MagicSettings.Server/MagicSettings.Server.csproj
index a46af5e..5a6f3a8 100644
--- a/MagicSettings/MagicSettings.Server/MagicSettings.Server.csproj
+++ b/MagicSettings/MagicSettings.Server/MagicSettings.Server.csproj
@@ -1,10 +1,13 @@
+ true
MagicSettings.Server
+ MagicSettings Server
Storage-agnostic server helpers for MagicSettings enrollment, synchronization, authorization, and proof verification.
+ configuration;settings;server;dotnet;control-plane
-
+
diff --git a/MagicSettings/MagicSettings.Share/MagicSettings.Share.csproj b/MagicSettings/MagicSettings.Share/MagicSettings.Share.csproj
index ecaa002..7ed9612 100644
--- a/MagicSettings/MagicSettings.Share/MagicSettings.Share.csproj
+++ b/MagicSettings/MagicSettings.Share/MagicSettings.Share.csproj
@@ -1,5 +1,6 @@
+ false
MagicSettings.Share
Shared protocol, schema, and security contracts for MagicSettings clients and control-plane implementations.
diff --git a/MagicSettings/MagicSettings/MagicSettings.csproj b/MagicSettings/MagicSettings/MagicSettings.csproj
index 336d442..7c2f62c 100644
--- a/MagicSettings/MagicSettings/MagicSettings.csproj
+++ b/MagicSettings/MagicSettings/MagicSettings.csproj
@@ -1,10 +1,13 @@
+ true
MagicSettings
+ MagicSettings
Strongly typed configuration with generation, reconciliation, migrations, live overrides, and client-owned control-plane integration.
+ configuration;settings;appsettings;dotnet;control-plane
-
+