From fd85511de142289b5bc1dc1fedef56531afca3a8 Mon Sep 17 00:00:00 2001
From: Chrison Simtian <chris.simon@myfoodbag.co.nz>
Date: Mon, 18 May 2026 19:06:31 +1200
Subject: [PATCH 1/2] feat(release): Nerdbank.GitVersioning + GitHub Actions
 release pipeline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Versioning moves from GitFlow-driven GitVersion to trunk-based
Nerdbank.GitVersioning. Adds the missing release pipeline so merges
to main can publish to nuget.org.

Versioning
- version.json — Nerdbank config. version "10.2", publicReleaseRefSpec
  scoped to main (stable versions only on main, prereleases elsewhere),
  semVer 2 nuget output, assembly version pinned to major.minor for
  binding stability.
- Directory.Packages.props — Nerdbank.GitVersioning 3.7.115.
- source/Directory.Build.props — PackageReference with PrivateAssets="All"
  so the package isn't a transitive dep of consumers.
- GitVersion still installed for the moment because Build.cs's
  MajorMinorPatchVersion property still reads from it; that's a
  follow-up cleanup.

Release pipeline
- .github/workflows/release.yml — hand-written (not auto-generated).
  Triggered on push to main, fetch-depth: 0 for Nerdbank, runs
  Test + Pack + Publish via ./build.cmd. Reads NUGET_API_KEY secret
  (must be added to repo secrets before the first release fires).
- Why hand-written: lets us name the GitHub secret NUGET_API_KEY
  (conventional screaming-snake-case) while keeping the NUKE
  parameter NuGetApiKey (idiomatic C#). The [GitHubActions]
  attribute generator would force the two to share a name.

Build.cs simplification
- Drop FeedzNuGetSource / FeedzNuGetApiKey / IsPublicRelease (no
  alpha feed in trunk-based; main is the only release target).
- Rename PublicNuGetApiKey → NuGetApiKey.
- IPublish.NuGetSource / NuGetApiKey now unconditional.
- Pack.SetVersion(9999.0.0) only applies for Terminal (local) builds;
  CI lets Nerdbank's MSBuild-injected version stand.
- Test.OnlyWhenStatic guard removed (was excluding alpha-deployment).
- DeletePackages now only handles the Terminal case.

Workflow constant
- Build.CI.GitHubActions.cs — AlphaDeployment → ReleaseWorkflow.
- Publish.Requires now checks Workflow == ReleaseWorkflow.

Docs
- CLAUDE.md — versioning + release-pipeline sections rewritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .github/workflows/release.yml   | 35 +++++++++++++++++++++++++++++
 CLAUDE.md                       |  4 +++-
 Directory.Packages.props        |  1 +
 build/Build.CI.GitHubActions.cs | 27 +++++++++++-----------
 build/Build.SignPackages.cs     |  2 +-
 build/Build.cs                  | 40 ++++++++++-----------------------
 source/Directory.Build.props    |  1 +
 version.json                    | 18 +++++++++++++++
 8 files changed, 84 insertions(+), 44 deletions(-)
 create mode 100644 .github/workflows/release.yml
 create mode 100644 version.json

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..9333e0032
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,35 @@
+# Hand-written (not auto-generated) — see Build.CI.GitHubActions.cs for the
+# rationale (lets us bridge GitHub's NUGET_API_KEY secret to NUKE's
+# NuGetApiKey parameter, which would otherwise have to share a name).
+
+name: release
+
+on:
+  push:
+    branches:
+      - main
+
+permissions:
+  contents: write     # for ICreateGitHubRelease (tag + GitHub release)
+  packages: write     # in case we later push to GitHub Packages too
+
+jobs:
+  release:
+    name: release
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          fetch-depth: 0   # Nerdbank.GitVersioning needs full history
+      - name: 'Cache: .nuke/temp, ~/.nuget/packages'
+        uses: actions/cache@v4
+        with:
+          path: |
+            .nuke/temp
+            ~/.nuget/packages
+          key: ${{ runner.os }}-release-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props', 'version.json') }}
+      - name: 'Run: Test, Pack, Publish'
+        run: ./build.cmd Test Pack Publish
+        env:
+          NuGetApiKey: ${{ secrets.NUGET_API_KEY }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CLAUDE.md b/CLAUDE.md
index 86e9a339f..bf309e1fc 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -60,7 +60,9 @@ CI providers in use: **GitHub Actions only** (the other providers were dropped 
 
 Validation workflows (`ubuntu-latest`, `windows-latest`, `macos-latest`) run on every push to non-main branches and every PR targeting `main`, with `paths-ignore` for `docs/**`, `images/**`, `**/*.md`.
 
-Release pipeline is being introduced in a follow-up PR alongside Nerdbank.GitVersioning. GitVersion is still in use until that lands.
+**Versioning:** [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) — configured in `version.json` at the repo root. Major+minor is hand-bumped; patch comes from git-height. `main` is the public-release ref (stable versions); everything else gets prerelease tags. GitVersion is still installed as a transitional helper for `MajorMinorPatchVersion` in `Build.cs`; full removal is a follow-up.
+
+**Release pipeline:** `.github/workflows/release.yml` — triggered on push to `main`, runs `./build.cmd Test Pack Publish`. Uses the repo secret `NUGET_API_KEY` (you must set this in repo settings before the first release fires).
 
 Alpha packages flow to the Feedz feed (`https://f.feedz.io/nuke/alpha/nuget`) from `develop` via the GitHub Actions `AlphaDeployment` workflow. Public NuGet publishes happen from `master`/`release/*` via AppVeyor.
 
diff --git a/Directory.Packages.props b/Directory.Packages.props
index da745fd64..bb310e3a8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,6 +14,7 @@
     <PackageVersion Include="JetBrains.Annotations" Version="2025.2.2" />
     <PackageVersion Include="Microsoft.ApplicationInsights" Version="2.23.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="10.0.0" />
+    <PackageVersion Include="Nerdbank.GitVersioning" Version="3.7.115" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
     <PackageVersion Include="NJsonSchema" Version="11.5.2" />
     <PackageVersion Include="NJsonSchema.NewtonsoftJson" Version="11.5.2" />
diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs
index d719f3234..47b7d2b4b 100644
--- a/build/Build.CI.GitHubActions.cs
+++ b/build/Build.CI.GitHubActions.cs
@@ -5,13 +5,6 @@
 using Nuke.Common.CI.GitHubActions;
 using Nuke.Components;
 
-[GitHubActions(
-    "windows-latest",
-    GitHubActionsImage.WindowsLatest,
-    FetchDepth = 0,
-    OnPushBranches = new[] { MainBranch },
-    InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) },
-    PublishArtifacts = false)]
 // macOS and Windows runs are reserved for main-branch validation (post-merge
 // and release pipelines). PRs and feature-branch pushes get Linux-only for
 // fast, cheap feedback.
@@ -22,6 +15,13 @@
     OnPushBranches = new[] { MainBranch },
     InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) },
     PublishArtifacts = false)]
+[GitHubActions(
+    "windows-latest",
+    GitHubActionsImage.WindowsLatest,
+    FetchDepth = 0,
+    OnPushBranches = new[] { MainBranch },
+    InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) },
+    PublishArtifacts = false)]
 [GitHubActions(
     "ubuntu-latest",
     GitHubActionsImage.UbuntuLatest,
@@ -33,11 +33,10 @@
     PublishArtifacts = false)]
 partial class Build
 {
-    // AlphaDeployment workflow removed in trunk migration. The release pipeline
-    // is reintroduced in the follow-up Nerdbank.GitVersioning PR with proper
-    // main-branch publish semantics. Code paths still referencing this constant
-    // (Test.OnlyWhenStatic, Pack.PackSettings, Publish.Requires, DeletePackages)
-    // are intentionally preserved — they evaluate to false until a workflow
-    // with this name exists again.
-    const string AlphaDeployment = "alpha-deployment";
+    // The release workflow is intentionally hand-written at
+    // .github/workflows/release.yml — that lets us name the GitHub secret
+    // NUGET_API_KEY (conventional screaming-snake-case) while keeping the
+    // Build.cs property name NuGetApiKey (idiomatic C#). The NUKE attribute
+    // generator would force the two to match.
+    const string ReleaseWorkflow = "release";
 }
diff --git a/build/Build.SignPackages.cs b/build/Build.SignPackages.cs
index 2ff0230bd..7d48c5391 100644
--- a/build/Build.SignPackages.cs
+++ b/build/Build.SignPackages.cs
@@ -18,6 +18,6 @@ partial class Build : ISignPackages
 
     public Target SignPackages => _ => _
         .Inherit<ISignPackages>()
-        .OnlyWhenStatic(() => IsPublicRelease)
+        .OnlyWhenStatic(() => GitRepository.IsOnMainBranch())
         .OnlyWhenStatic(() => EnvironmentInfo.IsWin);
 }
diff --git a/build/Build.cs b/build/Build.cs
index 4e960d311..7a90bec74 100644
--- a/build/Build.cs
+++ b/build/Build.cs
@@ -103,7 +103,6 @@ from framework in project.GetTargetFrameworks()
 
     Target ITest.Test => _ => _
         .Inherit<ITest>()
-        .OnlyWhenStatic(() => Host is not GitHubActions { Workflow: AlphaDeployment })
         .Partition(2);
 
     bool IReportCoverage.CreateCoverageHtmlReport => true;
@@ -120,25 +119,24 @@ from framework in project.GetTargetFrameworks()
     IEnumerable<string> IReportIssues.InspectCodeFailOnIssues => new string[0];
     IEnumerable<string> IReportIssues.InspectCodeFailOnCategories => new string[0];
 
+    // Local Terminal runs use a placeholder version so packed nupkgs don't
+    // collide with real releases. CI runs let Nerdbank.GitVersioning inject the
+    // real version via MSBuild ($PackageVersion).
     Configure<DotNetPackSettings> IPack.PackSettings => _ => _
-        .When(Host is Terminal or GitHubActions { Workflow: AlphaDeployment }, _ => _
+        .When(Host is Terminal, _ => _
             .SetVersion(DefaultDeploymentVersion));
 
-    string PublicNuGetSource => "https://api.nuget.org/v3/index.json";
-    string FeedzNuGetSource => "https://f.feedz.io/nuke/alpha/nuget";
     string DefaultDeploymentVersion => "9999.0.0";
 
-    [Parameter] [Secret] readonly string PublicNuGetApiKey;
-    [Parameter] [Secret] readonly string FeedzNuGetApiKey;
+    [Parameter] [Secret] readonly string NuGetApiKey;
 
-    bool IsPublicRelease => GitRepository.IsOnMainBranch();
-    string IPublish.NuGetSource => IsPublicRelease ? PublicNuGetSource : FeedzNuGetSource;
-    string IPublish.NuGetApiKey => IsPublicRelease ? PublicNuGetApiKey : FeedzNuGetApiKey;
+    string IPublish.NuGetSource => "https://api.nuget.org/v3/index.json";
+    string IPublish.NuGetApiKey => NuGetApiKey;
 
     Target IPublish.Publish => _ => _
         .Inherit<IPublish>()
         .Consumes(From<IPack>().Pack)
-        .Requires(() => GitRepository.IsOnMainBranch() && Host is GitHubActions && GitHubActions.Workflow == AlphaDeployment)
+        .Requires(() => GitRepository.IsOnMainBranch() && Host is GitHubActions && GitHubActions.Workflow == ReleaseWorkflow)
         .WhenSkipped(DependencyBehavior.Execute);
 
     IEnumerable<AbsolutePath> NuGetPackageFiles
@@ -147,26 +145,12 @@ IEnumerable<AbsolutePath> NuGetPackageFiles
     Target DeletePackages => _ => _
         .DependentFor<IPublish>()
         .After<IPack>()
-        .OnlyWhenStatic(() => Host is Terminal or GitHubActions { Workflow: AlphaDeployment })
+        .OnlyWhenStatic(() => Host is Terminal)
         .Executes(() =>
         {
-            if (Host is Terminal)
-            {
-                var packagesDirectory = NuGetPackageResolver.GetPackagesDirectory(packagesConfigFile: BuildProjectFile);
-                var packageDirectories = packagesDirectory.GlobDirectories($"nuke.*/{DefaultDeploymentVersion}");
-                packageDirectories.DeleteDirectories();
-            }
-            else if (Host is GitHubActions)
-            {
-                void DeletePackage(string id, string version)
-                    => DotNet(
-                        $"nuget delete {id} {version} --source {FeedzNuGetSource} --api-key {FeedzNuGetApiKey} --non-interactive",
-                        logOutput: false);
-
-                var packageIds = NuGetPackageFiles.Select(x => new PackageArchiveReader(x).NuspecReader.GetId());
-                foreach (var packageId in packageIds)
-                    SuppressErrors(() => DeletePackage(packageId, DefaultDeploymentVersion), logWarning: false);
-            }
+            var packagesDirectory = NuGetPackageResolver.GetPackagesDirectory(packagesConfigFile: BuildProjectFile);
+            var packageDirectories = packagesDirectory.GlobDirectories($"nuke.*/{DefaultDeploymentVersion}");
+            packageDirectories.DeleteDirectories();
         });
 
     string ICreateGitHubRelease.Name => MajorMinorPatchVersion;
diff --git a/source/Directory.Build.props b/source/Directory.Build.props
index d4bbbd9d0..16e2b9c57 100644
--- a/source/Directory.Build.props
+++ b/source/Directory.Build.props
@@ -36,6 +36,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
+    <PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="All" />
   </ItemGroup>
 
   <ItemGroup Condition="$(MSBuildProjectName.EndsWith('Tests'))">
diff --git a/version.json b/version.json
new file mode 100644
index 000000000..b1b83ee48
--- /dev/null
+++ b/version.json
@@ -0,0 +1,18 @@
+{
+  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
+  "version": "10.2",
+  "publicReleaseRefSpec": [
+    "^refs/heads/main$"
+  ],
+  "nuGetPackageVersion": {
+    "semVer": 2
+  },
+  "assemblyVersion": {
+    "precision": "minor"
+  },
+  "release": {
+    "versionIncrement": "minor",
+    "firstUnstableTag": "preview",
+    "branchName": "release/v{version}"
+  }
+}

From 0c1f25ae04ebb79ad6a9edf76f3c4e90d91bdc85 Mon Sep 17 00:00:00 2001
From: Chrison Simtian <chris.simon@myfoodbag.co.nz>
Date: Mon, 18 May 2026 19:42:38 +1200
Subject: [PATCH 2/2] fix(release): publish to GitHub Packages, not nuget.org
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Per Matt's wish that the "Nuke" name not be carried over, the
successor project needs a new name before publishing to nuget.org.
Until that lands, release publishing targets GitHub Packages on this
fork — gives us a real release pipeline without claiming the upstream
identity.

- Build.cs IPublish.NuGetSource → GitHub Packages URL derived from
  GITHUB_REPOSITORY_OWNER (with ChrisonSimtian fallback for local).
- release.yml uses secrets.GITHUB_TOKEN (auto-provided, packages:write
  already granted) instead of a separate NUGET_API_KEY.
- CLAUDE.md updated: dropped stale Feedz/AppVeyor paragraph that
  slipped through the trunk-migration edit; added GitHub Packages
  rationale + name-change context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .github/workflows/release.yml | 5 ++++-
 CLAUDE.md                     | 6 ++----
 build/Build.cs                | 8 +++++++-
 3 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9333e0032..4c69cb10a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,5 +31,8 @@ jobs:
       - name: 'Run: Test, Pack, Publish'
         run: ./build.cmd Test Pack Publish
         env:
-          NuGetApiKey: ${{ secrets.NUGET_API_KEY }}
+          # Publish target is GitHub Packages on this fork, not nuget.org.
+          # GITHUB_TOKEN has packages:write per the permissions block above —
+          # no separate NuGet API key needed until the project rename.
+          NuGetApiKey: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CLAUDE.md b/CLAUDE.md
index bf309e1fc..13cb7831c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -62,11 +62,9 @@ Validation workflows (`ubuntu-latest`, `windows-latest`, `macos-latest`) run on
 
 **Versioning:** [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) — configured in `version.json` at the repo root. Major+minor is hand-bumped; patch comes from git-height. `main` is the public-release ref (stable versions); everything else gets prerelease tags. GitVersion is still installed as a transitional helper for `MajorMinorPatchVersion` in `Build.cs`; full removal is a follow-up.
 
-**Release pipeline:** `.github/workflows/release.yml` — triggered on push to `main`, runs `./build.cmd Test Pack Publish`. Uses the repo secret `NUGET_API_KEY` (you must set this in repo settings before the first release fires).
+**Release pipeline:** `.github/workflows/release.yml` — triggered on push to `main`, runs `./build.cmd Test Pack Publish`. **Publishes to GitHub Packages** (`https://nuget.pkg.github.com/<owner>/index.json`) using the auto-provided `GITHUB_TOKEN` — no separate NuGet API key is needed.
 
-Alpha packages flow to the Feedz feed (`https://f.feedz.io/nuke/alpha/nuget`) from `develop` via the GitHub Actions `AlphaDeployment` workflow. Public NuGet publishes happen from `master`/`release/*` via AppVeyor.
-
-Versioning is driven by GitVersion (`GitVersion.yml`).
+**Why not nuget.org?** Per Matt's wish, the "Nuke" name cannot be carried over to a successor project. Until the hard fork picks a new name, packages stay on this fork's GitHub Packages feed. Switching the target to nuget.org becomes a follow-up once the rename ceremony happens.
 
 ## Conventions worth respecting
 
diff --git a/build/Build.cs b/build/Build.cs
index 7a90bec74..6d33da50e 100644
--- a/build/Build.cs
+++ b/build/Build.cs
@@ -130,7 +130,13 @@ from framework in project.GetTargetFrameworks()
 
     [Parameter] [Secret] readonly string NuGetApiKey;
 
-    string IPublish.NuGetSource => "https://api.nuget.org/v3/index.json";
+    // Publishing to GitHub Packages on this fork until the post-hard-fork
+    // project rename lands. nuget.org would require the new name and can't
+    // be done under "Nuke.*" — see project_nuke_strategy memory note.
+    // Repository owner comes from GITHUB_REPOSITORY_OWNER, automatically set
+    // by GitHub Actions runners. ChrisonSimtian is the local-dev fallback.
+    string IPublish.NuGetSource =>
+        $"https://nuget.pkg.github.com/{EnvironmentInfo.GetVariable("GITHUB_REPOSITORY_OWNER") ?? "ChrisonSimtian"}/index.json";
     string IPublish.NuGetApiKey => NuGetApiKey;
 
     Target IPublish.Publish => _ => _
