Skip to content

Make release creation an idempotent upsert instead of Get-then-Create - #34

Merged
Mpdreamz merged 1 commit into
masterfrom
fix/idempotent-release-upsert
Sep 3, 2026
Merged

Mpdreamz merged 1 commit into
masterfrom
fix/idempotent-release-upsert

Conversation

@Mpdreamz

@Mpdreamz Mpdreamz commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #32/#33 (Octokit's request serialization dropping fields under Native AOT). This one's on the read side.

What happened

Both assembly-rewriter and nupkg-validator got past release creation and label creation successfully this time (thanks to #32/#33), but then a concurrent CI run (a master push race, see context below) meant the release for that tag had already been created by the time the tag-triggered run got to this step. The pre-check that's supposed to catch that (client.Repository.Release.Get, deserializing Octokit's own Release model via the same unannotated reflection covered in #32/#33) silently failed too, so CreateRelease ran unconditionally and got:

Octokit.ApiValidationException: {"message":"Validation Failed","errors":[{"resource":"Release","code":"already_exists","field":"tag_name"}],"status":"422"}

Why not just fix the Get call

Given #32 and #33 already showed two unrelated models breaking two different (and non-obvious) ways under Octokit's zero-annotation reflection serializer, I don't trust patching around individual failures here to be the last one. The more robust shape is to stop needing a separate, fallible existence check at all.

Fix

GitHubReleaseClient.CreateOrUpdateRelease is now a self-contained upsert: always POST the release first. Only on a 422 already_exists does it resolve the existing release's id — via a minimal, source-generated-only GET returning just { id }, not Octokit's Release model — and PATCH it. ReleaseNotesRunner.CreateRelease no longer calls ReleaseExists at all before writing.

Verification

Simulated the full sequence (POST → 422 already_exists → GET id → PATCH) against a local HttpListener from a real native-AOT linux-x64 build:

POST releases RECEIVED: {"tag_name":"0.14.3","body":"hello body"}
GET /repos/o/r/releases/tags/0.14.3
Resolved release id: 424242
PATCH releases/id RECEIVED: {"body":"hello body"}
DONE

Made with Cursor

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>
@Mpdreamz
Mpdreamz merged commit 62d95eb into master Sep 3, 2026
2 checks passed
@Mpdreamz
Mpdreamz deleted the fix/idempotent-release-upsert branch September 3, 2026 11:22
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.

1 participant