Skip to content

fix(release): cut the git tag unconditionally when updating an existing release - #551

Merged
joshua-temple merged 1 commit into
mainfrom
fix/update-unconditional-git-tag
Jul 10, 2026
Merged

joshua-temple merged 1 commit into
mainfrom
fix/update-unconditional-git-tag

Conversation

@joshua-temple

Copy link
Copy Markdown
Collaborator

Problem

Manager.update() only cuts the git tag on the create path (when no release object exists). If a release object already exists for the version (for example a draft release), it PATCHes the release and returns without ever calling createGitTag. So the git tag is only ever guaranteed on the create branch. If a tagless draft release for that version pre-exists, the git tag is permanently missing even though the state write (an unconditional 10-attempt CAS loop) still advances the leaf.

Because the bug itself produces tagless draft releases, they accumulate and keep starving the tag on subsequent runs. Surfaced live under a concurrent shared-path wave: the draft release web-0.2.0-rc.0 existed but the git tag web-0.2.0-rc.0 did not, while the orchestrate run concluded success and the state leaf advanced. The state write is a guaranteed CAS loop; tag creation was conditional and unretried, and that asymmetry is the defect.

Fix

internal/release/release.go: in update(), cut the git tag unconditionally when CreateTag is set, on the pre-existing-release (PATCH) branch as well as the create branch. createGitTag already treats a 422 "already exists" as success, so re-cutting a present tag is a no-op. Tag-only mode is untouched (it returns via create() and never PATCHes), and the PATCH payload semantics are unchanged.

Verification

Regression test TestManager_Update_CutsGitTag (red before, green after): update against a pre-existing draft with no git tag now cuts the tag; the no-release path still cuts via create(); a second update with the tag present is an idempotent no-op. go build ./..., go test ./... (2711 pass), go test ./... -race, golangci-lint run ./... all clean; no emitted-workflow or golden change.

Caught by the v1-readiness fleet's shared-path concurrency wave.

update() only materialized the git tag on the create path. When a release object already existed (a draft matched by tag or target SHA) it PATCHed and returned without cutting the tag, so under a concurrent shared-path wave a pre-existing tagless draft advanced the state leaf while the git tag stayed absent. The state write is an unconditional CAS loop; tag creation was conditional and unretried, and that asymmetry left the tag permanently missing.

Make the tag cut unconditional and idempotent on both update branches, mirroring create(). createGitTag already treats a 422 as success, so a convergence rerun is a no-op. Tag-only mode is unchanged. Adds TestManager_Update_CutsGitTag covering the existing-draft, no-release, and already-present-tag cases.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
@joshua-temple
joshua-temple merged commit 0b49ccc into main Jul 10, 2026
20 checks passed
@joshua-temple
joshua-temple deleted the fix/update-unconditional-git-tag branch July 10, 2026 20:00
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