Skip to content

Release pipeline via GitHub Releases + StrongTypes.FsCheck package (#34, #40)#48

Merged
KaliCZ merged 10 commits into
mainfrom
tag-based-nuget-publish
Apr 21, 2026
Merged

Release pipeline via GitHub Releases + StrongTypes.FsCheck package (#34, #40)#48
KaliCZ merged 10 commits into
mainfrom
tag-based-nuget-publish

Conversation

@KaliCZ

@KaliCZ KaliCZ commented Apr 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces the old "push to main → publish" pipeline with a GitHub Release–driven flow in .github/workflows/build.yml
  • Adds the new Kalicz.StrongTypes.FsCheck package — shared FsCheck arbitraries, discovered automatically by the publish step
  • Packages ship lockstep at a single version; release notes come from the GitHub Release body

Closes #34. Closes #40.

How a release works

  1. Draft a GitHub Release → tag v0.4.0, target main
  2. Fill in release notes (start from .github/RELEASE_TEMPLATE.md if helpful)
  3. Publish → build.yml runs: build job verifies the commit (tests included), then publish job packs and pushes every publishable nupkg to nuget.org
  4. All publishable packages land on nuget.org at 0.4.0

git push origin v0.4.0 alone does nothing. Only publishing a GitHub Release triggers publish.

Workflow structure

Single build.yml with two jobs:

  • build — runs on push to main, PR to main, and release:published. Restore, build, test, upload test logs.
  • publish — runs on release:published only, needs: build. Parses the tag, passes version + release notes to dotnet build, pushes the resulting nupkgs.

needs: build guarantees tests pass on the release commit before any package leaves the box.

Safety layers stacking

  1. Tag ruleset (set up separately via Settings): only admins can create v* tags; tag creation requires a green build check from build.yml on the tagged commit.
  2. Environment restriction (set up on the Nuget.org environment): allowed refs restricted to v* tags.
  3. Workflow gate: publish job conditional on github.event.release.target_commitish == 'main' — rejects Releases drafted against any branch other than main.
  4. Fresh re-test: the build job runs on the release event itself before publish starts.
  5. Duplicate-push protection: dotnet nuget push (no --skip-duplicate) fails loudly if a version was already shipped.

Publish is convention-based, not hardcoded

The pack step is a single dotnet build StrongTypes.slnx with -p:Version, -p:PackageReleaseNotes, and -p:PackageOutputPath. Publishable projects have <PackageId> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild>; non-publishable ones set <IsPackable>false</IsPackable>. Future publishable packages join the release stream automatically just by following the convention.

Today that covers three packages: Kalicz.StrongTypes, Kalicz.StrongTypes.EfCore, Kalicz.StrongTypes.FsCheck.

Version-matching across packages

EfCore's and FsCheck's <ProjectReference> to core picks up the cascading -p:Version MSBuild property, so when CI builds them at 0.4.0, the generated nuspec writes <dependency id="Kalicz.StrongTypes" version="0.4.0" /> automatically. Verified locally.

New FsCheck package

Moves the shared arbitraries from src/StrongTypes.Tests/Generators.cs into src/StrongTypes.FsCheck/ with namespace StrongTypes.FsCheck. Consumers register everything with a single attribute:

using StrongTypes.FsCheck;

[Properties(Arbitrary = new[] { typeof(Generators) })]
public class MyTests { ... }

Runtime dep is FsCheck 3.3.2 (no xunit-specific bits). Internal test project picks up the type via a new GlobalUsings.cs so the existing 11 test files keep referencing Generators unqualified.

csproj changes (Version/Notes)

Removed from the publishable csprojs:

  • <Version>, <AssemblyVersion>, <FileVersion> (now -p:Version from the tag)
  • <PackageReleaseNotes> (now -p:PackageReleaseNotes from the Release body)

Each publishable csproj keeps <Version>0.0.0-dev</Version> as a local-build placeholder.

Test plan

  • Merge PR
  • Configure tag ruleset: target v*, restrict creations/updates/deletions (admin bypass), require status check build
  • Configure Nuget.org environment: add allowed deployment tag pattern v*
  • Cut a pre-release (v0.3.1-test.1) to validate end-to-end — all three packages should land on nuget.org at that version
  • Confirm routine push to main does not trigger publish
  • Once green, cut the real v0.4.0

🤖 Generated with Claude Code

- Workflow: build/test on main push + PR; publish only on `release: published`
- Version and release notes come from the Release: `-p:Version` from the tag
  name (stripped of leading `v`), `-p:PackageReleaseNotes` from the body
- csproj: drop hardcoded `<Version>`, `<AssemblyVersion>`, `<FileVersion>`,
  and `<PackageReleaseNotes>`; keep a `0.0.0-dev` local-build placeholder

Closes #34

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ self-assigned this Apr 21, 2026
KaliCZ and others added 2 commits April 21, 2026 15:17
Drop --skip-duplicate so re-publishing an already-shipped version surfaces
as a workflow failure instead of silently no-opping. A Release whose tag
collides with a prior one should never look green in Actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tag `core-vX.Y.Z` publishes Kalicz.StrongTypes at X.Y.Z; tag
`efcore-vX.Y.Z` publishes Kalicz.StrongTypes.EfCore at X.Y.Z. Unknown
prefixes fail the publish step with a clear message.

Build and test still run on every push/PR and on the release event
(via `needs: build`), so no release ships without a green test run.

Because EfCore's ProjectReference to core picks up the cascading
`-p:Version` MSBuild property, the EfCore nuspec pins its core
dependency to the same version as the tag. This enforces the
"dependent packages ship at the same version as the core they need"
rule without extra workflow logic — you just have to publish core
first when bumping the shared version.

Switched from `dotnet pack` to `dotnet build -p:PackageOutputPath`
because GeneratePackageOnBuild=true in the csproj files makes
`dotnet build` produce the nupkg natively. The push step pins to
./out/$PACKAGE.$VERSION.nupkg so building a dependent doesn't
accidentally republish its transitively-built core nupkg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ changed the title Tag-based NuGet publish via GitHub Releases (#34) Per-package tag-based NuGet publish via GitHub Releases (#34) Apr 21, 2026
Per-package tags added meaningful ceremony (three tag prefixes, remembering
to release core first, three Release drafts) for a benefit — independent
release notes per package — that doesn't justify the cost for a tightly
coupled package family that will almost always ship together.

Tag `vX.Y.Z` now publishes all packages at X.Y.Z with a single shared
release body. Version matching between dependents and core is still
enforced by MSBuild's `-p:Version` cascading through ProjectReferences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ changed the title Per-package tag-based NuGet publish via GitHub Releases (#34) Tag-based NuGet publish via GitHub Releases (#34) Apr 21, 2026
KaliCZ and others added 3 commits April 21, 2026 16:03
Publish job now iterates over all three publishable csproj files, skipping
any that don't exist yet. When StrongTypes.FsCheck lands (#40), the next
release automatically picks it up with no workflow change.

Added .github/RELEASE_TEMPLATE.md as a copy-paste structure for the Release
body — GitHub has no native release body template feature, so this is a
manual template contributors paste into the Release form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… convention

Replace the hardcoded list of csproj paths with a single `dotnet build
StrongTypes.slnx`. Non-publishable projects (tests, analyzers, source
generators) already set IsPackable=false, so they produce no nupkg; only
projects with GeneratePackageOnBuild=true emit packages. New publishable
packages are picked up automatically by setting those two properties.

Verified locally: solution build at version X emits exactly
Kalicz.StrongTypes.X.nupkg and Kalicz.StrongTypes.EfCore.X.nupkg, with the
latter pinning its core dependency to version X.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the shared FsCheck arbitraries (NonEmptyString, numeric wrappers,
Maybe<T>, NonEmptyEnumerable) from src/StrongTypes.Tests/Generators.cs
into a new publishable package, so downstream users can property-test
against StrongTypes types via:

  [Properties(Arbitrary = new[] { typeof(Generators) })]

Namespace: StrongTypes.FsCheck (sibling to StrongTypes.EfCore).
Runtime dep: FsCheck 3.3.2 (not the xunit-specific bits).

Test project picks up the type via a new GlobalUsings.cs so the existing
11 test files keep referencing Generators unqualified.

The publish pipeline added earlier in this PR auto-includes FsCheck the
moment the csproj lands in the solution — no workflow change needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ changed the title Tag-based NuGet publish via GitHub Releases (#34) Tag-based NuGet publish + StrongTypes.FsCheck package (#34, #40) Apr 21, 2026
KaliCZ and others added 2 commits April 21, 2026 16:27
Add `github.event.release.target_commitish == 'main'` to the publish job's
if-condition. A Release drafted via the UI captures its target branch in
this field; this gate blocks any Release that was drafted against a
feature branch or pre-existing tag from shipping to NuGet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci.yml runs on push/PR — restore, build, test. Produces the `build`
status check the tag ruleset requires.

release.yml runs on release:published — re-runs the full test suite on
the release commit, then packs and pushes. Publish stays gated on
target_commitish == 'main', still uses the Nuget.org environment, and
picks up all publishable packages via `dotnet build StrongTypes.slnx`.

Each workflow has one clear job of work; a reader can understand CI
without reading CD and vice versa. Mild duplication of the restore/build/
test block — small price for the separation of concerns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KaliCZ KaliCZ changed the title Tag-based NuGet publish + StrongTypes.FsCheck package (#34, #40) Release pipeline: CI/CD split, GitHub Releases, FsCheck package (#34, #40) Apr 21, 2026
@KaliCZ KaliCZ changed the title Release pipeline: CI/CD split, GitHub Releases, FsCheck package (#34, #40) Release pipeline via GitHub Releases + StrongTypes.FsCheck package (#34, #40) Apr 21, 2026
@KaliCZ KaliCZ enabled auto-merge (squash) April 21, 2026 14:38
@KaliCZ KaliCZ merged commit 6abb79a into main Apr 21, 2026
2 checks passed
@KaliCZ KaliCZ deleted the tag-based-nuget-publish branch April 21, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish FsCheck generators as a separate package (Kalicz.StrongTypes.FsCheck) Switch NuGet publish from main-push to tag-based GitHub Release

1 participant