From 5b655817e021ef0f9c02a7f70e2c478235080264 Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 14:51:24 +0200 Subject: [PATCH 01/10] Switch NuGet publish from main-push to GitHub Release trigger - 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 ``, ``, ``, and ``; keep a `0.0.0-dev` local-build placeholder Closes #34 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build.yml | 51 +++++++++++-------- .../StrongTypes.EfCore.csproj | 5 +- src/StrongTypes/StrongTypes.csproj | 5 +- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3be3c3..e63c8f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,8 @@ on: pull_request: branches: - main + release: + types: [published] permissions: contents: read @@ -48,36 +50,45 @@ jobs: **/TestResults/**/*.trx retention-days: 5 - - name: Pack - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - run: | - dotnet pack ./src/StrongTypes/StrongTypes.csproj -c $config -o out - dotnet pack ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj -c $config -o out - - - name: Upload NuGet package - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/upload-artifact@v7 - with: - name: nuget-package - path: out/*.nupkg - retention-days: 1 - publish: needs: build - if: github.ref == 'refs/heads/main' && github.event_name == 'push' + if: github.event_name == 'release' runs-on: ubuntu-latest environment: Nuget.org + env: + config: 'Release' + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + steps: - - name: Download NuGet package - uses: actions/download-artifact@v8 - with: - name: nuget-package + - name: Checkout + uses: actions/checkout@v6 - name: Setup .NET 10 uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x + - name: Parse version from tag + id: version + run: | + TAG="${GITHUB_REF_NAME}" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + + - name: Pack + env: + NOTES: ${{ github.event.release.body }} + VERSION: ${{ steps.version.outputs.version }} + run: | + dotnet pack ./src/StrongTypes/StrongTypes.csproj \ + -c $config -o out \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" + dotnet pack ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj \ + -c $config -o out \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" + - name: Publish packages - run: dotnet nuget push ./*.nupkg --skip-duplicate --source nuget.org --api-key ${{secrets.NUGET_TOKEN}} + run: dotnet nuget push ./out/*.nupkg --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} diff --git a/src/StrongTypes.EfCore/StrongTypes.EfCore.csproj b/src/StrongTypes.EfCore/StrongTypes.EfCore.csproj index dbd5ba0..cf8c05e 100644 --- a/src/StrongTypes.EfCore/StrongTypes.EfCore.csproj +++ b/src/StrongTypes.EfCore/StrongTypes.EfCore.csproj @@ -7,9 +7,7 @@ true CS1591 - 0.3.0 - 0.3.0 - 0.3.0 + 0.0.0-dev Kalicz.StrongTypes.EfCore KaliCZ Copyright © 2026 KaliCZ @@ -21,7 +19,6 @@ git https://github.com/KaliCZ/StrongTypes.git readme.md - 0.3.0: Initial release. Ships NonEmptyString and numeric-wrapper value converters. Single-call configuration via optionsBuilder.UseStrongTypes(). true true diff --git a/src/StrongTypes/StrongTypes.csproj b/src/StrongTypes/StrongTypes.csproj index ac699dd..382ff52 100644 --- a/src/StrongTypes/StrongTypes.csproj +++ b/src/StrongTypes/StrongTypes.csproj @@ -2,9 +2,7 @@ true CS1591 - 0.3.0 - 0.3.0 - 0.3.0 + 0.0.0-dev Kalicz.StrongTypes A C# library that reduces boilerplate and prevents bugs through stronger typing. Continuation of FuncSharp. KaliCZ @@ -13,7 +11,6 @@ https://github.com/KaliCZ/StrongTypes MIT false - 0.3.0: Generic numeric types: Positive<T>, NonNegative<T>, Negative<T>, NonPositive<T>. Added a full E2E capability for these types and NonEmptyString - API JSON serialization and EF Core converters in a separate Kalicz.StrongTypes.EfCore. git https://github.com/KaliCZ/StrongTypes true From ddfbdbe2e6ba0e35c2c16bb7559c2ef7339dda44 Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 15:17:48 +0200 Subject: [PATCH 02/10] Fail publish on duplicate version 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) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e63c8f0..009b973 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,4 +91,4 @@ jobs: -p:PackageReleaseNotes="$NOTES" - name: Publish packages - run: dotnet nuget push ./out/*.nupkg --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From 96cacb7e5abe60f7eaf24384d09659c702da08d7 Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 15:29:14 +0200 Subject: [PATCH 03/10] Per-package release pipelines: tag prefix selects which package to ship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/build.yml | 56 +++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 009b973..5f9d111 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,25 +70,51 @@ jobs: with: dotnet-version: 10.0.x - - name: Parse version from tag - id: version + - name: Parse tag + id: parse run: | TAG="${GITHUB_REF_NAME}" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - - - name: Pack + case "$TAG" in + core-v*) + PROJECT=src/StrongTypes/StrongTypes.csproj + PACKAGE=Kalicz.StrongTypes + VERSION="${TAG#core-v}" + ;; + efcore-v*) + PROJECT=src/StrongTypes.EfCore/StrongTypes.EfCore.csproj + PACKAGE=Kalicz.StrongTypes.EfCore + VERSION="${TAG#efcore-v}" + ;; + *) + echo "Unknown tag prefix: $TAG. Expected one of: core-v*, efcore-v*." >&2 + exit 1 + ;; + esac + echo "project=$PROJECT" >> "$GITHUB_OUTPUT" + echo "package=$PACKAGE" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # GeneratePackageOnBuild=true in the csproj files means `dotnet build` + # produces the nupkg as a side-effect; no separate `dotnet pack` step + # needed. The -p:Version cascades into referenced projects (e.g., EfCore's + # ProjectReference to core), so the resulting nuspec pins its core + # dependency to the same version as the tag. + - name: Build and pack env: NOTES: ${{ github.event.release.body }} - VERSION: ${{ steps.version.outputs.version }} + PROJECT: ${{ steps.parse.outputs.project }} + VERSION: ${{ steps.parse.outputs.version }} run: | - dotnet pack ./src/StrongTypes/StrongTypes.csproj \ - -c $config -o out \ - -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" - dotnet pack ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj \ - -c $config -o out \ + dotnet build "$PROJECT" \ + -c $config \ -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" + -p:PackageReleaseNotes="$NOTES" \ + -p:PackageOutputPath="$PWD/out" - - name: Publish packages - run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + # Push only the target package; building a dependent (e.g., EfCore) also + # emits the referenced core nupkg into out/, which we must not republish. + - name: Publish package + env: + PACKAGE: ${{ steps.parse.outputs.package }} + VERSION: ${{ steps.parse.outputs.version }} + run: dotnet nuget push "./out/${PACKAGE}.${VERSION}.nupkg" --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From ff05a46e6d4adc25521573f06600a7d9bc9d18db Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:01:14 +0200 Subject: [PATCH 04/10] Revert to lockstep releases: single tag ships all packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/build.yml | 49 ++++++++++++------------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f9d111..b7d9773 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,51 +70,32 @@ jobs: with: dotnet-version: 10.0.x - - name: Parse tag - id: parse + - name: Parse version from tag + id: version run: | TAG="${GITHUB_REF_NAME}" - case "$TAG" in - core-v*) - PROJECT=src/StrongTypes/StrongTypes.csproj - PACKAGE=Kalicz.StrongTypes - VERSION="${TAG#core-v}" - ;; - efcore-v*) - PROJECT=src/StrongTypes.EfCore/StrongTypes.EfCore.csproj - PACKAGE=Kalicz.StrongTypes.EfCore - VERSION="${TAG#efcore-v}" - ;; - *) - echo "Unknown tag prefix: $TAG. Expected one of: core-v*, efcore-v*." >&2 - exit 1 - ;; - esac - echo "project=$PROJECT" >> "$GITHUB_OUTPUT" - echo "package=$PACKAGE" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" # GeneratePackageOnBuild=true in the csproj files means `dotnet build` # produces the nupkg as a side-effect; no separate `dotnet pack` step - # needed. The -p:Version cascades into referenced projects (e.g., EfCore's - # ProjectReference to core), so the resulting nuspec pins its core - # dependency to the same version as the tag. + # needed. -p:Version cascades into referenced projects (EfCore's + # ProjectReference to core), so EfCore's nuspec pins its core + # dependency to the same version. - name: Build and pack env: NOTES: ${{ github.event.release.body }} - PROJECT: ${{ steps.parse.outputs.project }} - VERSION: ${{ steps.parse.outputs.version }} + VERSION: ${{ steps.version.outputs.version }} run: | - dotnet build "$PROJECT" \ + dotnet build ./src/StrongTypes/StrongTypes.csproj \ + -c $config \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" \ + -p:PackageOutputPath="$PWD/out" + dotnet build ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj \ -c $config \ -p:Version="$VERSION" \ -p:PackageReleaseNotes="$NOTES" \ -p:PackageOutputPath="$PWD/out" - # Push only the target package; building a dependent (e.g., EfCore) also - # emits the referenced core nupkg into out/, which we must not republish. - - name: Publish package - env: - PACKAGE: ${{ steps.parse.outputs.package }} - VERSION: ${{ steps.parse.outputs.version }} - run: dotnet nuget push "./out/${PACKAGE}.${VERSION}.nupkg" --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + - name: Publish packages + run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From cc7e9064513f0f66d6f33e04295a0a38cef0135b Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:03:11 +0200 Subject: [PATCH 05/10] Wire FsCheck package into publish pipeline + add release body template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/RELEASE_TEMPLATE.md | 32 ++++++++++++++++++++++++++++++++ .github/workflows/build.yml | 26 ++++++++++++++++---------- 2 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 .github/RELEASE_TEMPLATE.md diff --git a/.github/RELEASE_TEMPLATE.md b/.github/RELEASE_TEMPLATE.md new file mode 100644 index 0000000..008f37c --- /dev/null +++ b/.github/RELEASE_TEMPLATE.md @@ -0,0 +1,32 @@ + + +## Kalicz.StrongTypes + +- + +## Kalicz.StrongTypes.EfCore + +- + +## Kalicz.StrongTypes.FsCheck + +- + +## Breaking changes + +- + +## Upgrade notes + +- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7d9773..e46fcc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,21 +81,27 @@ jobs: # needed. -p:Version cascades into referenced projects (EfCore's # ProjectReference to core), so EfCore's nuspec pins its core # dependency to the same version. + # Loop: skip projects that don't exist yet (e.g. FsCheck until #40 lands). + # When a new publishable package is added, just add its csproj to the list. - name: Build and pack env: NOTES: ${{ github.event.release.body }} VERSION: ${{ steps.version.outputs.version }} run: | - dotnet build ./src/StrongTypes/StrongTypes.csproj \ - -c $config \ - -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" \ - -p:PackageOutputPath="$PWD/out" - dotnet build ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj \ - -c $config \ - -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" \ - -p:PackageOutputPath="$PWD/out" + for PROJECT in \ + ./src/StrongTypes/StrongTypes.csproj \ + ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj \ + ./src/StrongTypes.FsCheck/StrongTypes.FsCheck.csproj; do + if [ -f "$PROJECT" ]; then + dotnet build "$PROJECT" \ + -c $config \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" \ + -p:PackageOutputPath="$PWD/out" + else + echo "Skipping $PROJECT (not present in repo)" + fi + done - name: Publish packages run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From b3dee2cac5541c77946209ff78fbc8cadad1a815 Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:06:32 +0200 Subject: [PATCH 06/10] Simplify publish: build the solution, pick up publishable projects by 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) --- .github/workflows/build.yml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e46fcc7..8eb7bc5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,27 +81,21 @@ jobs: # needed. -p:Version cascades into referenced projects (EfCore's # ProjectReference to core), so EfCore's nuspec pins its core # dependency to the same version. - # Loop: skip projects that don't exist yet (e.g. FsCheck until #40 lands). - # When a new publishable package is added, just add its csproj to the list. + # Build the whole solution; non-publishable projects (tests, internal + # source generators, analyzers) set IsPackable=false so they produce no + # nupkg. GeneratePackageOnBuild=true on publishable csprojs means the + # build itself emits .nupkg files into PackageOutputPath. New publishable + # packages are picked up automatically by setting the same two properties. - name: Build and pack env: NOTES: ${{ github.event.release.body }} VERSION: ${{ steps.version.outputs.version }} run: | - for PROJECT in \ - ./src/StrongTypes/StrongTypes.csproj \ - ./src/StrongTypes.EfCore/StrongTypes.EfCore.csproj \ - ./src/StrongTypes.FsCheck/StrongTypes.FsCheck.csproj; do - if [ -f "$PROJECT" ]; then - dotnet build "$PROJECT" \ - -c $config \ - -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" \ - -p:PackageOutputPath="$PWD/out" - else - echo "Skipping $PROJECT (not present in repo)" - fi - done + dotnet build StrongTypes.slnx \ + -c $config \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" \ + -p:PackageOutputPath="$PWD/out" - name: Publish packages run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From 05993b6c28c292958adc412ff9be9c7a4bcec1ab Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:17:39 +0200 Subject: [PATCH 07/10] Add Kalicz.StrongTypes.FsCheck package with the arbitraries (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the shared FsCheck arbitraries (NonEmptyString, numeric wrappers, Maybe, 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) --- StrongTypes.slnx | 3 ++ .../Generators.cs | 12 ++--- .../StrongTypes.FsCheck.csproj | 34 +++++++++++++ src/StrongTypes.FsCheck/readme.md | 49 +++++++++++++++++++ src/StrongTypes.Tests/GlobalUsings.cs | 1 + .../StrongTypes.Tests.csproj | 1 + 6 files changed, 92 insertions(+), 8 deletions(-) rename src/{StrongTypes.Tests => StrongTypes.FsCheck}/Generators.cs (93%) create mode 100644 src/StrongTypes.FsCheck/StrongTypes.FsCheck.csproj create mode 100644 src/StrongTypes.FsCheck/readme.md create mode 100644 src/StrongTypes.Tests/GlobalUsings.cs diff --git a/StrongTypes.slnx b/StrongTypes.slnx index c255838..53dc544 100644 --- a/StrongTypes.slnx +++ b/StrongTypes.slnx @@ -6,6 +6,9 @@ + + + diff --git a/src/StrongTypes.Tests/Generators.cs b/src/StrongTypes.FsCheck/Generators.cs similarity index 93% rename from src/StrongTypes.Tests/Generators.cs rename to src/StrongTypes.FsCheck/Generators.cs index f28f2a4..d4a2381 100644 --- a/src/StrongTypes.Tests/Generators.cs +++ b/src/StrongTypes.FsCheck/Generators.cs @@ -1,16 +1,12 @@ -#nullable enable - -using System.Collections.Generic; using FsCheck; using FsCheck.Fluent; -namespace StrongTypes.Tests; +namespace StrongTypes.FsCheck; /// -/// Shared FsCheck arbitraries for the test project. Reference via +/// Shared FsCheck arbitraries for Kalicz.StrongTypes. Reference via /// [Properties(Arbitrary = new[] { typeof(Generators) })] -/// on a test class. Add new arbitraries here rather than creating -/// per-feature generator classes. +/// on a test class. /// public static class Generators { @@ -80,7 +76,7 @@ public static class Generators /// /// with ~20% chance of . /// FsCheck's default string generator never produces null, so - /// the None branch has to be injected explicitly via . + /// the None branch has to be injected explicitly via Gen.Frequency. /// Empty and whitespace strings are kept as valid Some values — use /// when you want the non-empty invariant. /// diff --git a/src/StrongTypes.FsCheck/StrongTypes.FsCheck.csproj b/src/StrongTypes.FsCheck/StrongTypes.FsCheck.csproj new file mode 100644 index 0000000..a2a4ad8 --- /dev/null +++ b/src/StrongTypes.FsCheck/StrongTypes.FsCheck.csproj @@ -0,0 +1,34 @@ + + + net10.0 + 14.0 + enable + enable + true + CS1591 + + 0.0.0-dev + Kalicz.StrongTypes.FsCheck + KaliCZ + Copyright © 2026 KaliCZ + FsCheck arbitraries for Kalicz.StrongTypes. Drop-in property-test support for NonEmptyString, Positive<T>, NonNegative<T>, Negative<T>, NonPositive<T>, Maybe<T>, and NonEmptyEnumerable<T> via a single [Properties(Arbitrary = new[] { typeof(Generators) })] attribute. + StrongTypes, FsCheck, PropertyTesting, Arbitrary, Generators + MIT + false + https://github.com/KaliCZ/StrongTypes + git + https://github.com/KaliCZ/StrongTypes.git + readme.md + true + true + + + + + + + + + + + diff --git a/src/StrongTypes.FsCheck/readme.md b/src/StrongTypes.FsCheck/readme.md new file mode 100644 index 0000000..6cbed8e --- /dev/null +++ b/src/StrongTypes.FsCheck/readme.md @@ -0,0 +1,49 @@ +# Kalicz.StrongTypes.FsCheck + +FsCheck arbitraries for [Kalicz.StrongTypes](https://www.nuget.org/packages/Kalicz.StrongTypes). +Lets you write property tests against code that takes or returns `NonEmptyString`, +`Positive`, `NonNegative`, `Negative`, `NonPositive`, `Maybe`, and +`NonEmptyEnumerable` without hand-rolling generators that re-derive each type's +invariants. + +## Install + +```powershell +dotnet add package Kalicz.StrongTypes.FsCheck +``` + +## Register + +Register everything with one attribute on your test class: + +```csharp +using FsCheck.Xunit; +using StrongTypes.FsCheck; + +[Properties(Arbitrary = new[] { typeof(Generators) })] +public class MyTests +{ + [Property] + public void NonEmptyString_round_trips_through_json(NonEmptyString value) + { + // value is guaranteed non-null, non-empty, non-whitespace + } + + [Property] + public void Positive_stays_positive(Positive value) + { + Assert.True(value.Value > 0); + } +} +``` + +## What ships + +- `NonEmptyString` — filtered to non-null, non-whitespace values +- `NullableNonEmptyString` — ~10% null injection +- `Positive`, `Negative`, `NonNegative`, `NonPositive` +- `Maybe`, `Maybe`, `Maybe`, `Maybe>` — + ~20% `None` injection +- `NonEmptyEnumerable` + +Version matches the core `Kalicz.StrongTypes` package you install alongside it. diff --git a/src/StrongTypes.Tests/GlobalUsings.cs b/src/StrongTypes.Tests/GlobalUsings.cs new file mode 100644 index 0000000..1456e4d --- /dev/null +++ b/src/StrongTypes.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using StrongTypes.FsCheck; diff --git a/src/StrongTypes.Tests/StrongTypes.Tests.csproj b/src/StrongTypes.Tests/StrongTypes.Tests.csproj index b917b0c..2eeaedb 100644 --- a/src/StrongTypes.Tests/StrongTypes.Tests.csproj +++ b/src/StrongTypes.Tests/StrongTypes.Tests.csproj @@ -15,5 +15,6 @@ + From eb7c215fc1d428004049c677553c4c34f649c689 Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:27:43 +0200 Subject: [PATCH 08/10] Restrict publish to Releases targeting main 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) --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8eb7bc5..ec8939a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,9 @@ jobs: publish: needs: build - if: github.event_name == 'release' + # Gate on main: a Release drafted through the UI carries the selected + # branch in target_commitish. Only releases targeted at main publish. + if: github.event_name == 'release' && github.event.release.target_commitish == 'main' runs-on: ubuntu-latest environment: Nuget.org From 7207f01151968b3d33fb784a5bb59784671473a2 Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:36:15 +0200 Subject: [PATCH 09/10] Split build.yml into ci.yml and release.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/build.yml | 103 ---------------------------------- .github/workflows/ci.yml | 49 ++++++++++++++++ .github/workflows/release.yml | 85 ++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 103 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index ec8939a..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: build - -on: - push: - branches: - - main - pull_request: - branches: - - main - release: - types: [published] - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - - env: - config: 'Release' - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --configuration $config --no-restore - - - name: Test - run: dotnet test --no-restore --no-build --configuration $config - - - name: Upload test logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: test-logs - path: | - **/TestResults/**/*.log - **/TestResults/**/*.trx - retention-days: 5 - - publish: - needs: build - # Gate on main: a Release drafted through the UI carries the selected - # branch in target_commitish. Only releases targeted at main publish. - if: github.event_name == 'release' && github.event.release.target_commitish == 'main' - runs-on: ubuntu-latest - environment: Nuget.org - - env: - config: 'Release' - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Parse version from tag - id: version - run: | - TAG="${GITHUB_REF_NAME}" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - - # GeneratePackageOnBuild=true in the csproj files means `dotnet build` - # produces the nupkg as a side-effect; no separate `dotnet pack` step - # needed. -p:Version cascades into referenced projects (EfCore's - # ProjectReference to core), so EfCore's nuspec pins its core - # dependency to the same version. - # Build the whole solution; non-publishable projects (tests, internal - # source generators, analyzers) set IsPackable=false so they produce no - # nupkg. GeneratePackageOnBuild=true on publishable csprojs means the - # build itself emits .nupkg files into PackageOutputPath. New publishable - # packages are picked up automatically by setting the same two properties. - - name: Build and pack - env: - NOTES: ${{ github.event.release.body }} - VERSION: ${{ steps.version.outputs.version }} - run: | - dotnet build StrongTypes.slnx \ - -c $config \ - -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" \ - -p:PackageOutputPath="$PWD/out" - - - name: Publish packages - run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..87a2064 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + env: + config: 'Release' + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration $config --no-restore + + - name: Test + run: dotnet test --no-restore --no-build --configuration $config + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-logs + path: | + **/TestResults/**/*.log + **/TestResults/**/*.trx + retention-days: 5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1f9d5d8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: release + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + # Re-verifies the release commit before any package leaves the box. The tag + # ruleset already requires a green `build` check from ci.yml on the commit, + # but we re-test here so a stale/cached check can't slip by unnoticed. + test: + runs-on: ubuntu-latest + + env: + config: 'Release' + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration $config --no-restore + + - name: Test + run: dotnet test --no-restore --no-build --configuration $config + + publish: + needs: test + # Gate on main: a Release drafted through the UI carries the selected + # branch in target_commitish. Only releases targeted at main publish. + if: github.event.release.target_commitish == 'main' + runs-on: ubuntu-latest + environment: Nuget.org + + env: + config: 'Release' + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Parse version from tag + id: version + run: | + TAG="${GITHUB_REF_NAME}" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + + # Build the whole solution; non-publishable projects (tests, internal + # source generators, analyzers) set IsPackable=false so they produce no + # nupkg. GeneratePackageOnBuild=true on publishable csprojs means the + # build itself emits .nupkg files into PackageOutputPath. New publishable + # packages are picked up automatically by setting the same two properties. + - name: Build and pack + env: + NOTES: ${{ github.event.release.body }} + VERSION: ${{ steps.version.outputs.version }} + run: | + dotnet build StrongTypes.slnx \ + -c $config \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" \ + -p:PackageOutputPath="$PWD/out" + + - name: Publish packages + run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} From f25c7df9ce39952a0d59135f0c188c8b4f2409fb Mon Sep 17 00:00:00 2001 From: KaliCZ Date: Tue, 21 Apr 2026 16:36:59 +0200 Subject: [PATCH 10/10] Revert "Split build.yml into ci.yml and release.yml" This reverts commit 7207f01151968b3d33fb784a5bb59784671473a2. --- .github/workflows/build.yml | 103 ++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 49 ---------------- .github/workflows/release.yml | 85 ---------------------------- 3 files changed, 103 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ec8939a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,103 @@ +name: build + +on: + push: + branches: + - main + pull_request: + branches: + - main + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + env: + config: 'Release' + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration $config --no-restore + + - name: Test + run: dotnet test --no-restore --no-build --configuration $config + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-logs + path: | + **/TestResults/**/*.log + **/TestResults/**/*.trx + retention-days: 5 + + publish: + needs: build + # Gate on main: a Release drafted through the UI carries the selected + # branch in target_commitish. Only releases targeted at main publish. + if: github.event_name == 'release' && github.event.release.target_commitish == 'main' + runs-on: ubuntu-latest + environment: Nuget.org + + env: + config: 'Release' + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Parse version from tag + id: version + run: | + TAG="${GITHUB_REF_NAME}" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + + # GeneratePackageOnBuild=true in the csproj files means `dotnet build` + # produces the nupkg as a side-effect; no separate `dotnet pack` step + # needed. -p:Version cascades into referenced projects (EfCore's + # ProjectReference to core), so EfCore's nuspec pins its core + # dependency to the same version. + # Build the whole solution; non-publishable projects (tests, internal + # source generators, analyzers) set IsPackable=false so they produce no + # nupkg. GeneratePackageOnBuild=true on publishable csprojs means the + # build itself emits .nupkg files into PackageOutputPath. New publishable + # packages are picked up automatically by setting the same two properties. + - name: Build and pack + env: + NOTES: ${{ github.event.release.body }} + VERSION: ${{ steps.version.outputs.version }} + run: | + dotnet build StrongTypes.slnx \ + -c $config \ + -p:Version="$VERSION" \ + -p:PackageReleaseNotes="$NOTES" \ + -p:PackageOutputPath="$PWD/out" + + - name: Publish packages + run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 87a2064..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: ci - -on: - push: - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - - env: - config: 'Release' - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --configuration $config --no-restore - - - name: Test - run: dotnet test --no-restore --no-build --configuration $config - - - name: Upload test logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: test-logs - path: | - **/TestResults/**/*.log - **/TestResults/**/*.trx - retention-days: 5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1f9d5d8..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: release - -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - # Re-verifies the release commit before any package leaves the box. The tag - # ruleset already requires a green `build` check from ci.yml on the commit, - # but we re-test here so a stale/cached check can't slip by unnoticed. - test: - runs-on: ubuntu-latest - - env: - config: 'Release' - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --configuration $config --no-restore - - - name: Test - run: dotnet test --no-restore --no-build --configuration $config - - publish: - needs: test - # Gate on main: a Release drafted through the UI carries the selected - # branch in target_commitish. Only releases targeted at main publish. - if: github.event.release.target_commitish == 'main' - runs-on: ubuntu-latest - environment: Nuget.org - - env: - config: 'Release' - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Parse version from tag - id: version - run: | - TAG="${GITHUB_REF_NAME}" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - - # Build the whole solution; non-publishable projects (tests, internal - # source generators, analyzers) set IsPackable=false so they produce no - # nupkg. GeneratePackageOnBuild=true on publishable csprojs means the - # build itself emits .nupkg files into PackageOutputPath. New publishable - # packages are picked up automatically by setting the same two properties. - - name: Build and pack - env: - NOTES: ${{ github.event.release.body }} - VERSION: ${{ steps.version.outputs.version }} - run: | - dotnet build StrongTypes.slnx \ - -c $config \ - -p:Version="$VERSION" \ - -p:PackageReleaseNotes="$NOTES" \ - -p:PackageOutputPath="$PWD/out" - - - name: Publish packages - run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }}