From 200cffcfd8b545d0744ce97d22548bf99c387564 Mon Sep 17 00:00:00 2001 From: Denis Afonso Date: Wed, 14 Oct 2026 16:42:56 +0100 Subject: [PATCH] fix(scripts/release): promote stable on immutable releases (#30745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `./scripts/release_promote_stable.sh v2.37.4` failed today with: ``` edit release failed: PATCH https://api.github.com/repos/coder/coder/releases/403906567: 404 Not Found ``` Releases on this repo are immutable (`gh release view v2.37.4 --json isImmutable` → `true`). `promoteVersionToStable` cloned the entire `RepositoryRelease` into `EditRelease`, so the PATCH carried `tag_name`, `target_commitish` and the assets. GitHub rejects that on an immutable release even when the values are unchanged, and answers 404 rather than 422. The same edit with only `body`, `prerelease` and `make_latest` succeeds (that's how v2.37.4 was promoted by hand). ## Change - Build a minimal `RepositoryRelease{Body, Prerelease, MakeLatest}` for the edit. - Drop the `Draft: false` assignment (published releases are never drafts, and `draft` is not an editable field on immutable releases). - Dry-run now logs the body diff instead of a diff of the whole object. - Remove the now-unused `cloneRelease` helper. Verified with `go test ./scripts/release/` and a `promote --dry-run` against v2.36.7. Every release published since immutable releases were enabled is affected, so without this the command cannot promote anything. Related: the release-process review notes that this step is also missing from the runbook and should become a workflow job on every `.0` mainline release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- scripts/release/main.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/scripts/release/main.go b/scripts/release/main.go index 1be7232c58..e25a82212a 100644 --- a/scripts/release/main.go +++ b/scripts/release/main.go @@ -236,33 +236,31 @@ func (r *releaseCommand) promoteVersionToStable(ctx context.Context, inv *serpen logger.Info(ctx, "promoting selected version to stable") - // Update the release to latest. - updatedNewStable := cloneRelease(newStable) - + // Update the release to latest. Only send the fields we change: + // releases on this repo are immutable, and GitHub rejects an edit + // that carries the tag, target commitish or assets of an immutable + // release (it answers 404 Not Found), even when those values are + // unchanged. Body, prerelease and make_latest remain editable. updatedBody := removeMainlineBlurb(newStable.GetBody()) updatedBody = addStableSince(time.Now().UTC(), updatedBody) - updatedNewStable.Body = github.String(updatedBody) - updatedNewStable.MakeLatest = github.String("true") - updatedNewStable.Prerelease = github.Bool(false) - updatedNewStable.Draft = github.Bool(false) + edit := &github.RepositoryRelease{ + Body: github.String(updatedBody), + MakeLatest: github.String("true"), + Prerelease: github.Bool(false), + } if !r.dryRun { - _, _, err = client.Repositories.EditRelease(ctx, owner, repo, newStable.GetID(), updatedNewStable) + _, _, err = client.Repositories.EditRelease(ctx, owner, repo, newStable.GetID(), edit) if err != nil { return xerrors.Errorf("edit release failed: %w", err) } logger.Info(ctx, "selected version promoted to stable", slog.F("url", newStable.GetHTMLURL())) } else { - logger.Info(ctx, "dry-run: release not updated", slog.F("uncommitted_changes", cmp.Diff(newStable, updatedNewStable))) + logger.Info(ctx, "dry-run: release not updated", slog.F("body_diff", cmp.Diff(newStable.GetBody(), updatedBody))) } return nil } -func cloneRelease(r *github.RepositoryRelease) *github.RepositoryRelease { - rr := *r - return &rr -} - // addStableSince adds a stable since note to the release body. // // Example: