Fix GitHub release creation silently dropping tag_name under Native AOT - #32
Merged
Merged
Conversation
Octokit's SimpleJsonSerializer serializes request bodies via raw, unannotated reflection (no [DynamicallyAccessedMembers] anywhere in Octokit/SimpleJson.cs). Under Native AOT trimming, the linker strips getter metadata for any property whose accessors have mixed visibility. NewRelease.TagName is "public get; private set;" - the only such property among every request model this tool sends - so in the AOT-published tool tag_name silently vanished from the outgoing JSON while every other field (all plain public get/set) still serialized fine. GitHub then rejected the request with 422 "tag_name" wasn't supplied, even though the call site clearly set it. Confirmed by reproducing with a minimal repro: trimming Octokit throws a NullReferenceException resolving TagName's getter once its metadata is stripped; the fully AOT-compiled build degrades more gracefully and just omits the field instead of crashing. Bypasses Octokit only for the Create call - the one place a mixed-visibility property model gets serialized - via a direct HttpClient POST using a source-generated System.Text.Json context (fully AOT-safe by construction). Verified end-to-end against a local HTTP listener from a real native-AOT linux-x64 build: tag_name is now present in the request body. Everything else (GetLatest/Get, ReleaseUpdate, labels) keeps using Octokit since those models are all plain public get/set and aren't affected. Co-authored-by: Cursor <cursoragent@cursor.com>
This was referenced Sep 3, 2026
Mpdreamz
added a commit
that referenced
this pull request
Sep 3, 2026
#33) The previous fix (#32) assumed the AOT-unsafe Octokit serialization bug was specific to NewRelease.TagName's mixed get/private-set visibility. It isn't: the very next release run hit the identical failure mode on NewLabel.Name - a perfectly ordinary public get/set property on a completely unrelated model - creating version labels: Octokit.ApiValidationException: "name" wasn't supplied There's no reliable per-property rule to work around here: Octokit's SimpleJsonSerializer has zero AOT/trimming annotations, so which members the Native AOT compiler's reflection analysis keeps invocable is effectively unpredictable from the source alone. Given that, the only robust fix is to stop depending on Octokit's request serialization entirely for every write call this tool makes, not just release creation. Extends GitHubReleaseClient with UpdateRelease and CreateLabel alongside the existing CreateRelease, all backed by source-generated System.Text.Json (AOT-safe by construction). Read-only Octokit calls (GetLatest/Get, label/branch lookups) are untouched - those only need deserialization, which hasn't shown this failure mode. Verified both request shapes end-to-end against a local HTTP listener from a real native-AOT linux-x64 build: SERVER RECEIVED: {"tag_name":"0.14.2-aot-test","body":"hello"} SERVER RECEIVED: {"name":"v1.0.0","color":"e3e3e3"} Co-authored-by: Cursor <cursoragent@cursor.com>
Mpdreamz
added a commit
that referenced
this pull request
Sep 3, 2026
…#34) The 'does this release already exist' pre-check (client.Repository.Release.Get, deserializing Octokit's own Release model via the same unannotated reflection covered in #32/#33) is just as unreliable under Native AOT as the write side was. In the very next real release after #33, it silently failed, so CreateRelease ran unconditionally against a tag that already had a release and got: Octokit.ApiValidationException: {"code":"already_exists","field":"tag_name"} Rather than keep chasing which specific Octokit call breaks next, CreateOrUpdateRelease is now a self-contained upsert: always POST the release, and only on a 422 already_exists response resolve the existing release's id (via a minimal, source-generated-only GET - not Octokit's Release model) and PATCH it. No separate existence check, so nothing to be unreliable about. Verified the full create -> 422 already_exists -> GET id -> PATCH sequence end-to-end against a local HTTP listener from a real native-AOT linux-x64 build. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Creating a GitHub release (
create-release) from the AOT-publishedrelease-notestool has been failing on GitHub's side with:even though the call site (
new NewRelease(config.Version)) clearly sets it, andconfig.Versionis confirmed non-empty.Root cause
Octokit.Internal.SimpleJsonSerializerserializes request bodies via raw, completely unannotated reflection (type.GetRuntimeProperties(), no[DynamicallyAccessedMembers]anywhere inOctokit/SimpleJson.csor its reflection helpers). Under Native AOT trimming, the linker can't statically prove which reflected members are needed, so it strips metadata for properties it can't safely classify — in practice, this hits any property with mixed accessor visibility.NewRelease.TagNameispublic string TagName { get; private set; }— the only property, across every request model this tool sends (NewRelease,ReleaseUpdate,NewLabel,LabelUpdate), with a non-public accessor. Every other property involved is plain{ get; set; }and survives trimming fine.I confirmed the mechanism directly: publishing a minimal repro with IL trimming enabled and calling
SimpleJsonSerializer.Serialize(new NewRelease(...))throws:because
GetGetterMethodInfo(propertyInfo)returnsnullforTagNameonce its getter metadata is trimmed. The fully AOT-compiled build (as opposed to plain IL trimming) degrades more gracefully and just silently omits the field from the JSON instead of crashing — which is exactly what GitHub's 422 response showed.This is unrelated to the Octokit version —
14.0.0is already latest on NuGet, and the bug is structural (noJsonSerializerContext/source-gen support in Octokit's request serialization at all).Fix
Bypass Octokit only for the
Createcall — the one place this tool serializes a model with a mixed-visibility property.GitHubReleaseClient.CreateReleasePOSTs directly viaHttpClient, using a source-generatedSystem.Text.Json.JsonSerializerContext(fully AOT-safe by construction, no reflection at all).Everything else keeps using Octokit's
GitHubClientunchanged (GetLatest/Getfor checking existing releases,Repository.Release.EditwithReleaseUpdate, and the labeling calls) — those request/response models are all plain public get/set, so they aren't affected by this trimming behavior.Verification
Built a standalone repro of the exact
JsonContent.Create(..., JsonSerializerContext)pattern used here, published it fully native-AOT forlinux-x64in a cleanmcr.microsoft.com/dotnet/sdk:10.0container, and posted it against a localHttpListener:confirming
tag_namenow survives AOT compilation end-to-end.Also included
Rebased onto current
master, which already carries the.any-packagegenerateApiChangesfix from a previous PR (turned out cherry-picking it here was a no-op — it's already there).Made with Cursor