From 4b36292e3a416272e864548891185e12dda88bdc Mon Sep 17 00:00:00 2001 From: mgravell Date: Fri, 11 Sep 2026 14:47:14 +0100 Subject: [PATCH 1/5] Target 1.1.0, fix the release-tag pattern, and report the version in CI Three things, all in service of cutting 1.1.0 cleanly. **Land on 1.1.0.** versionHeightOffset goes -1 -> -3. The offset is a fixed shift, not a pin: height counts commits since the `version` property changed, and two more have landed since (#221, and this one), so -1 would compute 1.1.1. Editing versionHeightOffset does *not* reset the height - verified, height stays 3 - so the offset has to absorb the drift. -3 puts this commit at exactly 1.1.0, which is what main will compute once this squash-merges (one commit on top of 78f0fc7 either way). **Fix the release-tag pattern**, which has never worked. publicReleaseRefSpec requires `^refs/tags/v\d+\.\d+`, but every tag this repo has ever cut is unprefixed - 1.0.52, 1.0.48, 1.0.45 and so on - so the regex cannot match any of them. Checking out the real 1.0.52 tag and asking nbgv gives `1.0.52-g7a36975e31`, PublicRelease False: a tag-triggered build has always produced a -g suffixed version, and the packages that shipped must have come from main builds instead. Relaxed to `v?`, so both spellings work; at an unprefixed 1.1.0 tag nbgv now reports a clean 1.1.0, PublicRelease True. Same fix, same reason, as StackExchange.Redis 611e478. **Report the computed version in CI**, modelled on StackExchange.Redis's CI.yml: a step that writes the NuGetPackageVersion to the job summary and the log, placed before restore/build so it stays legible when a later step fails. Cutting a release then means reading that line off a green main build and tagging with exactly it. --- .github/workflows/dotnet.yml | 8 ++++++++ version.json | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 95dc588c..f5f05417 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -29,6 +29,14 @@ jobs: with: setAllVars: true + # deliberately before restore/build, so the version is legible even when a later step + # fails. Cutting a release means tagging with exactly what this prints + - name: Report computed version + run: | + $version = nbgv get-version --variable NuGetPackageVersion + "### Computed package version: ``$version``" >> $env:GITHUB_STEP_SUMMARY + Write-Output "Computed package version: $version" + - name: Restore dependencies run: dotnet restore Build.csproj diff --git a/version.json b/version.json index 01f9d210..d5f097f4 100644 --- a/version.json +++ b/version.json @@ -1,11 +1,11 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", "version": "1.1", - "versionHeightOffset": -1, + "versionHeightOffset": -3, "assemblyVersion": "1.0.0.0", "publicReleaseRefSpec": [ "^refs/heads/main$", - "^refs/tags/v\\d+\\.\\d+" + "^refs/tags/v?\\d+\\.\\d+" ], "nugetPackageVersion": { "semVer": 2 From 50fe7777861abafa16bc6c3046daedb799639e8d Mon Sep 17 00:00:00 2001 From: mgravell Date: Fri, 11 Sep 2026 14:51:22 +0100 Subject: [PATCH 2/5] Add release.yml: verify the tag, then publish both packages There was no release workflow at all - dotnet.yml pushes to MyGet on main, and nothing publishes to nuget.org, so every release to date must have been pushed by hand. That is also why the broken tag pattern went unnoticed. Triggered by a published GitHub Release, with workflow_dispatch as a dry run that does everything except the tag check and the push, so the pipeline can be proven without cutting a release. The guard is the point: nbgv computes the version from version.json plus commit height, so the tag name does not set it and a mistyped tag would otherwise ship a package that disagrees with its release. The step compares the two and fails loudly instead. A leading "v" is stripped, matching the v? pattern this PR also fixes. Publishes **both** packages: Dapper.AOT and Dapper.Advisor are both on nuget.org at 1.0.52, and both set GeneratePackageOnBuild for Release, so a Release build produces the pair and the collect step gathers them with the same glob dotnet.yml already uses. They are uploaded as a run artifact before the push, so a failed push does not cost the build. Auth is Trusted Publishing (OIDC) - no long-lived key in the repo. That needs setup outside this file, recorded in the header comment: a GitHub environment named "release", a NUGET_USER secret, and a trusted-publishing policy on nuget.org for *each* of the two packages. No further versionHeightOffset delta: the repo squash-merges, so this lands as one commit on main regardless of how many are on the branch, and -3 still computes 1.1.0. --- .github/workflows/release.yml | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..8f76b564 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +# Publishes Dapper.AOT and Dapper.Advisor to nuget.org when a GitHub Release is published. +# +# Auth is NuGet Trusted Publishing (OIDC): no long-lived API key is stored anywhere. +# One-time setup on nuget.org, for *each* package: username menu -> Trusted Publishing -> +# add a policy with +# Repository Owner: DapperLib / Repository: DapperAOT / Workflow File: release.yml +# Environment: release +# plus a NUGET_USER repository secret holding the nuget.org profile name to publish as, +# and a GitHub environment named "release". +# +# Versioning note: Nerdbank.GitVersioning computes the version from version.json plus commit +# height - the tag name does not set it. Read the version to tag off a green main run's step +# summary ("Report computed version" in dotnet.yml) and create the release with exactly that +# tag; the guard step below fails the run on any mismatch rather than publishing a package +# that disagrees with its release. A leading "v" is accepted and stripped, matching +# publicReleaseRefSpec in version.json. +# +# Deliberately does not run tests: the commit being released already passed CI, and this job's +# only job is to reproduce that build and push it. Runs on Windows to match dotnet.yml, since +# the solution includes a net48 leg and that is the configuration CI already proves. + +name: Release + +on: + release: + types: [published] + # dry run: everything except the tag check and the push, so the pipeline can be proven + # (and the packages inspected, via the run artifact) without cutting a release + workflow_dispatch: + +jobs: + publish: + runs-on: windows-latest + environment: release + permissions: + id-token: write # for the OIDC exchange with nuget.org + contents: read + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # depth is needed for nbgv + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + 10.0.x + + - uses: dotnet/nbgv@master + with: + setAllVars: true + + - name: Verify tag matches computed version + if: github.event_name == 'release' + run: | + $computed = nbgv get-version --variable NuGetPackageVersion + $tag = "${{ github.event.release.tag_name }}" -replace '^v', '' + if ($computed -ne $tag) { + Write-Output "::error::Tag '$tag' does not match the computed version '$computed'; retag the release commit so the two agree." + exit 1 + } + "### Publishing: ``$computed``" >> $env:GITHUB_STEP_SUMMARY + Write-Output "Publishing $computed" + + - name: Restore dependencies + run: dotnet restore Build.csproj + + - name: Purge + run: del src/Dapper.*/bin/Release/Dapper.*.nupkg + + # both src projects set GeneratePackageOnBuild for Release, so this produces + # Dapper.AOT and Dapper.Advisor without a separate pack step + - name: Build + run: dotnet build Build.csproj --no-restore -c Release + + - name: Collect packages + run: | + New-Item -ItemType Directory -Force -Path .nupkgs | Out-Null + Copy-Item src/Dapper.*/bin/Release/Dapper.*.nupkg .nupkgs/ + Get-ChildItem .nupkgs/*.nupkg | ForEach-Object { Write-Output $_.Name } + + # the packages survive as a run artifact even if the push fails + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: packages + path: .nupkgs/*.nupkg + + - name: NuGet login (OIDC to temp API key) + if: github.event_name == 'release' + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} + + - name: Push to nuget.org + if: github.event_name == 'release' + run: dotnet nuget push .nupkgs/*.nupkg --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate From 79e9a23762bfde0f97068c4c7ef6cf37762e720a Mon Sep 17 00:00:00 2001 From: mgravell Date: Fri, 11 Sep 2026 14:52:58 +0100 Subject: [PATCH 3/5] Drop the MyGet push, and give the README badges release.yml now publishes to nuget.org on a tagged release, so the MyGet push on every main build is both redundant and a second, unversioned place for packages to appear. Removed, along with the Pack and Purge steps that existed only to feed it - packaging is still exercised on every build, because both src projects set GeneratePackageOnBuild for Release, so a broken nuspec still fails CI. That also retires the MYGETAPIKEY secret; nothing references it now. Badges: there were none at all, so this adds rather than updates - build status, and current nuget.org versions for both published packages. Worth having now that a release actually lands on nuget.org by a route anyone can see. --- .github/workflows/dotnet.yml | 13 ------------- README.md | 6 +++++- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f5f05417..ef7f32c7 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -40,9 +40,6 @@ jobs: - name: Restore dependencies run: dotnet restore Build.csproj - - name: Purge - run: del src/Dapper.*/bin/Release/Dapper.*.nupkg - - name: Build run: dotnet build Build.csproj --no-restore -c Release @@ -56,13 +53,3 @@ jobs: # compared at test time - so without this, a netfx golden can be wrong and CI stays green - name: Test .NET Framework 4.8 run: dotnet test Build.csproj --no-build --verbosity normal -c Release -f net48 --filter FullyQualifiedName!~Integration - - - name: Pack - if: ${{ success() && !github.base_ref }} - run: dotnet pack src/Dapper.AOT/Dapper.AOT.csproj --no-build --verbosity normal -c Release - - - name: Push to MyGet - if: ${{ success() && !github.base_ref }} - run: dotnet nuget push src/Dapper.*/bin/Release/Dapper.*.nupkg --source https://www.myget.org/F/dapper/api/v2/package --api-key "$env:MYGETAPIKEY" - env: - MYGETAPIKEY: ${{ secrets.MYGETAPIKEY }} \ No newline at end of file diff --git a/README.md b/README.md index 2ac0e4b3..67309dc2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![Build](https://github.com/DapperLib/DapperAOT/actions/workflows/dotnet.yml/badge.svg)](https://github.com/DapperLib/DapperAOT/actions/workflows/dotnet.yml) +[![Dapper.AOT](https://img.shields.io/nuget/v/Dapper.AOT?label=Dapper.AOT)](https://www.nuget.org/packages/Dapper.AOT) +[![Dapper.Advisor](https://img.shields.io/nuget/v/Dapper.Advisor?label=Dapper.Advisor)](https://www.nuget.org/packages/Dapper.Advisor) + Let's face it: ADO.NET is a complicated API, and writing "good" ADO.NET code by hand is time consuming and error-prone. But a lot of times you also don't want the ceremony of an ORM like EF or LLBLGenPro - you just want to execute SQL! @@ -5,4 +9,4 @@ For years now, Dapper helped by providing a great low-friction way of talking to Dapper.AOT radically changes how Dapper works, generating the necessary code *during build*, and offers a range of usage guidance to improve how you use Dapper. -[Getting Started](https://aot.dapperlib.dev/gettingstarted) | [Documentation](https://aot.dapperlib.dev/) \ No newline at end of file +[Getting Started](https://aot.dapperlib.dev/gettingstarted) | [Documentation](https://aot.dapperlib.dev/) From 67b1f0511cb6d0deee84fe837d36259c09a18d7a Mon Sep 17 00:00:00 2001 From: mgravell Date: Fri, 11 Sep 2026 14:59:48 +0100 Subject: [PATCH 4/5] Fix the stale netfx golden #220 left behind The new net48 CI step caught this on its first real outing, which is exactly what it was added for: CommandDefinitionOverloads.output.netfx.txt still claimed "2 skipped silently" and carried no DAP057, because #220 was developed on Linux and only the .output.* goldens can be regenerated there - the .output.netfx.* twins need an actual net48 run. Swept the rest rather than fixing just the one that failed: comparing every .output.txt against its .output.netfx.txt, this is the *only* pair whose diagnostic ids differ, and the only one whose scorecard buckets disagree. The other scorecard differences are legitimate - netfx genuinely has fewer call-sites (15 of 15 vs 17 of 17, and so on), and DateOnly.net6 is gated off netfx entirely. The generated-code goldens are untouched: #220 added diagnostics, not code, and the handled count is 1 of 3 on both sides either way. --- .../CommandDefinitionOverloads.output.netfx.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/Dapper.AOT.Test/Interceptors/CommandDefinitionOverloads.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/CommandDefinitionOverloads.output.netfx.txt index 5ade3665..7b996834 100644 --- a/test/Dapper.AOT.Test/Interceptors/CommandDefinitionOverloads.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/CommandDefinitionOverloads.output.netfx.txt @@ -1,4 +1,10 @@ -Generator produced 1 diagnostics: +Generator produced 3 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 1 of 3 enabled call-sites (0 unsupported API, 0 refused with diagnostics, 2 skipped silently) using 1 interceptors, 1 commands and 0 readers +Dapper.AOT handled 1 of 3 enabled call-sites (0 unsupported API, 2 refused with diagnostics, 0 skipped silently) using 1 interceptors, 1 commands and 0 readers + +Info DAP057 Interceptors/CommandDefinitionOverloads.input.cs L18 C24 +'Query' passes its SQL inside a CommandDefinition, which Dapper.AOT cannot read at build time; use the overload that takes the SQL directly. This call-site is left on vanilla Dapper, which will not work under native AOT + +Info DAP057 Interceptors/CommandDefinitionOverloads.input.cs L19 C24 +'Execute' passes its SQL inside a CommandDefinition, which Dapper.AOT cannot read at build time; use the overload that takes the SQL directly. This call-site is left on vanilla Dapper, which will not work under native AOT From f28159b996525b0f29f3437dfd257820bfa4a238 Mon Sep 17 00:00:00 2001 From: mgravell Date: Fri, 11 Sep 2026 15:05:06 +0100 Subject: [PATCH 5/5] Say which ref the computed version belongs to The first real run printed `1.1.4-g90538b49e3` on this PR, which is correct and misleading at once: a PR builds refs/pull/N/merge, whose height includes every branch commit plus the merge, so it is not the number that will ship. Squash-merged onto main the same work computes 1.1.0. Since the whole point of this step is "read this, tag with it", that gap is a live mis-tag waiting to happen. On main the summary now says to tag with exactly that value; anywhere else it says, in the summary itself, that the number is not the one that would ship and to read it off a main run instead. The log line carries the ref either way. release.yml's guard would catch the resulting mismatch, but not hitting it beats being caught by it. --- .github/workflows/dotnet.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ef7f32c7..a18a17f4 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -30,12 +30,20 @@ jobs: setAllVars: true # deliberately before restore/build, so the version is legible even when a later step - # fails. Cutting a release means tagging with exactly what this prints + # fails. Cutting a release means tagging with exactly what a *main* run prints - a PR + # run builds refs/pull/N/merge, whose height includes the branch commits and the merge, + # so its number is not the one that will ship - name: Report computed version run: | $version = nbgv get-version --variable NuGetPackageVersion - "### Computed package version: ``$version``" >> $env:GITHUB_STEP_SUMMARY - Write-Output "Computed package version: $version" + if ($env:GITHUB_REF -eq 'refs/heads/main') { + "### Computed package version: ``$version``" >> $env:GITHUB_STEP_SUMMARY + "Tag the release with exactly this." >> $env:GITHUB_STEP_SUMMARY + } else { + "### Computed package version: ``$version``" >> $env:GITHUB_STEP_SUMMARY + "_This is a ``$env:GITHUB_REF`` build - **not** the version that would ship. Read the release version off a main run._" >> $env:GITHUB_STEP_SUMMARY + } + Write-Output "Computed package version: $version (ref: $env:GITHUB_REF)" - name: Restore dependencies run: dotnet restore Build.csproj