From 5f5cc285f30a06c48ca0ffbc97ead412229c9bad Mon Sep 17 00:00:00 2001 From: Steven Gates Date: Tue, 8 Sep 2026 09:53:41 -0500 Subject: [PATCH] fix(tag-release): push the tag with git, then create the release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gh release create --target ` returns HTTP 403 "Resource not accessible by integration" (or "...by personal access token") for GitHub Actions tokens and fine-grained PATs even when contents: write is granted — cli/cli#9514. The same command succeeds under personal auth, which is why this only showed up once the release moved into a workflow: 1.0.49 through 1.0.51 were tagged from the owner's machine. Create and push the tag with git, then create the release against the tag that now exists, with no --target. Plain git push is not affected, and the result is identical: an annotated ref at the verified sha with a release attached. Re-running after a partial failure is now handled explicitly. The tag lands before the release, so a failure between the two used to leave a state the old "tag already exists" check would refuse forever. A tag at the SAME sha with no release is now recognized as resumable; a tag at a different sha is still a hard stop. --- scripts/tag-release.ps1 | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/scripts/tag-release.ps1 b/scripts/tag-release.ps1 index c671793b..ff84db6f 100644 --- a/scripts/tag-release.ps1 +++ b/scripts/tag-release.ps1 @@ -13,6 +13,12 @@ that sha is on main, matches the versionName being tagged, and that the AAB built from it exists locally before creating the release. + The tag is created and pushed with git, then the release is created against it. `gh release + create --target ` is NOT used: it returns HTTP 403 for Actions tokens and fine-grained + PATs even with contents: write (cli/cli#9514), while plain `git push` of the tag is unaffected. + That bug is invisible when running as a human, which is why it only surfaced once the release + moved into a workflow. + .PARAMETER Version Tag name / versionName, e.g. "1.0.49" (no "v" prefix — matches 1.0.49, the first tag cut for this repo). .PARAMETER Sha The chore/release- bump's merge commit on main. Get it right after @@ -45,9 +51,21 @@ if ($LASTEXITCODE -ne 0) { Write-Error "‘$Sha’ is not a commit in this repo. git merge-base --is-ancestor $Sha origin/main if ($LASTEXITCODE -ne 0) { Write-Error "$Sha is not an ancestor of origin/main — refusing to tag it."; exit 1 } -# 2. The tag must not already exist — no silent re-tagging. -$existing = git tag -l $Version -if ($existing) { Write-Error "Tag $Version already exists (points at $(git rev-parse $Version)). Bump the version or delete the stale tag first."; exit 1 } +# 2. An existing tag is either a re-run to finish, or a mistake. The tag is pushed before the +# release is created, so a failure between those two steps leaves the tag in place with nothing +# attached; refusing outright would make that state permanent. Same sha and no release yet means +# resume. A DIFFERENT sha is still a hard stop — that tag says something else shipped. +$tagExists = [bool](git tag -l $Version) +if ($tagExists) { + $taggedSha = (git rev-list -n 1 $Version) + if ($taggedSha -ne $Sha) { + Write-Error "Tag $Version already exists and points at $taggedSha, not $Sha. Bump the version or delete the stale tag first." + exit 1 + } + gh release view $Version *> $null + if ($LASTEXITCODE -eq 0) { Write-Error "$Version is already tagged AND released. Nothing to do."; exit 1 } + Write-Host "Tag $Version already present at $Sha with no release — creating the release for it." +} # 3. versionName at that sha must match the tag being cut — catches a wrong/stale sha. # git show returns a string[] (one element per line) in PowerShell; -match on an array filters @@ -65,9 +83,18 @@ if (-not (Test-Path $aab)) { Write-Warning "$aab not found locally — this tag should be cut AFTER building and uploading that bundle (release-signing-and-aab.md #3-5). Continuing anyway; verify you meant to." } -Write-Host "Tagging $Version at $Sha (versionCode $versionCode)..." -$ghArgs = @("release", "create", $Version, "--target", $Sha, "--title", $Title) +# 5. Push the tag ourselves, then create the release against it. See .DESCRIPTION for why this is +# not `gh release create --target`. +if (-not $tagExists) { + Write-Host "Tagging $Version at $Sha (versionCode $versionCode)..." + git tag $Version $Sha + if ($LASTEXITCODE -ne 0) { Write-Error "Could not create the tag $Version locally."; exit 1 } + git push origin "refs/tags/$Version" + if ($LASTEXITCODE -ne 0) { Write-Error "Could not push the tag $Version. The local tag remains; delete it with ``git tag -d $Version`` if you are not retrying."; exit 1 } +} + +$ghArgs = @("release", "create", $Version, "--title", $Title) if ($NotesFile) { $ghArgs += @("-F", $NotesFile) } else { $ghArgs += "--generate-notes" } gh @ghArgs -if ($LASTEXITCODE -ne 0) { Write-Error "gh release create failed."; exit 1 } +if ($LASTEXITCODE -ne 0) { Write-Error "gh release create failed. The tag $Version is pushed; re-run this script to attach the release."; exit 1 } Write-Host "Done: https://github.com/$(gh repo view --json nameWithOwner -q .nameWithOwner)/releases/tag/$Version"