fix(publish): use gh api directly to create release with explicit tag_name#508
fix(publish): use gh api directly to create release with explicit tag_name#508
Conversation
|
Note Gemini is unable to generate a summary for this pull request due to the file types involved not being currently supported. |
There was a problem hiding this comment.
Pull request overview
This PR addresses a persistent issue where GitHub releases are created with untagged-* tags instead of the intended version tag. The previous fix (#506) attempted to resolve this by adding --target "$GITHUB_SHA" to gh release create, but the problem persisted because when a release already exists for a tag, gh release create silently falls back to creating an untagged-* draft instead of failing. This PR bypasses the gh release create wrapper entirely by calling the GitHub API directly with explicit tag_name and target_commitish fields.
Changes:
- Replace
gh release createwith directgh apiPOST call to/repos/{owner}/{repo}/releases - Set explicit
tag_nameandtarget_commitishfields to prevent fallback tountagged-*releases - Add explanatory comment about why the API is called directly
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| BODY=$(cat /tmp/release-notes.txt) | ||
| # Use the API directly so tag_name is set explicitly (gh release create | ||
| # can produce untagged-* drafts when the tag is very recently pushed) | ||
| gh api repos/${{ github.repository }}/releases \ | ||
| --method POST \ | ||
| -f tag_name="$TAG_NAME" \ | ||
| -f target_commitish="$GITHUB_SHA" \ | ||
| -f name="$TAG_NAME" \ | ||
| -f body="$BODY" \ |
There was a problem hiding this comment.
The multiline body content from CHANGELOG.md will not be properly passed to the GitHub API with this approach. When using BODY=$(cat /tmp/release-notes.txt) followed by -f body="$BODY", the shell will expand the variable with newlines, but gh api -f expects a string parameter and may not handle embedded newlines correctly.
The previous approach using --notes-file /tmp/release-notes.txt with gh release create properly handled multiline content by reading from a file. With the direct API call, you should use the @ syntax to read from a file: -F body=@/tmp/release-notes.txt instead of -f body="$BODY".
Note the difference:
-f(lowercase) sends data as form fields and expects string values-F(uppercase) can read from files when prefixed with@
This will ensure the changelog content is correctly sent as the release body.
| # Use the API directly so tag_name is set explicitly (gh release create | ||
| # can produce untagged-* drafts when the tag is very recently pushed) | ||
| gh api repos/${{ github.repository }}/releases \ | ||
| --method POST \ | ||
| -f tag_name="$TAG_NAME" \ | ||
| -f target_commitish="$GITHUB_SHA" \ | ||
| -f name="$TAG_NAME" \ | ||
| -f body="$BODY" \ | ||
| -F draft=true \ | ||
| --jq '.html_url' |
There was a problem hiding this comment.
The workflow doesn't handle the scenario where a release already exists for the tag. According to the GitHub API documentation, POSTing to /repos/{owner}/{repo}/releases with a tag_name that already has a release will result in a 422 error.
While this is better than silently creating an untagged-* draft (which was the old behavior), the workflow will fail without a clear error message. Consider adding error handling or checking if a release already exists before attempting to create one.
One approach could be to add a check before the API call:
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "Release for $TAG_NAME already exists"
gh release view "$TAG_NAME" --json htmlUrl --jq '.htmlUrl'
else
# create the release
fiThis would make the workflow more robust and provide clearer feedback when a release already exists.
| # Use the API directly so tag_name is set explicitly (gh release create | |
| # can produce untagged-* drafts when the tag is very recently pushed) | |
| gh api repos/${{ github.repository }}/releases \ | |
| --method POST \ | |
| -f tag_name="$TAG_NAME" \ | |
| -f target_commitish="$GITHUB_SHA" \ | |
| -f name="$TAG_NAME" \ | |
| -f body="$BODY" \ | |
| -F draft=true \ | |
| --jq '.html_url' | |
| # Check if a release for this tag already exists to avoid API 422 errors | |
| if gh release view "$TAG_NAME" >/dev/null 2>&1; then | |
| echo "Release for $TAG_NAME already exists:" | |
| gh release view "$TAG_NAME" --json htmlUrl --jq '.htmlUrl' | |
| else | |
| # Use the API directly so tag_name is set explicitly (gh release create | |
| # can produce untagged-* drafts when the tag is very recently pushed) | |
| gh api repos/${{ github.repository }}/releases \ | |
| --method POST \ | |
| -f tag_name="$TAG_NAME" \ | |
| -f target_commitish="$GITHUB_SHA" \ | |
| -f name="$TAG_NAME" \ | |
| -f body="$BODY" \ | |
| -F draft=true \ | |
| --jq '.html_url' | |
| fi |
| gh release create --draft "$TAG_NAME" \ | ||
| --title "$TAG_NAME" \ | ||
| --notes-file /tmp/release-notes.txt | ||
| BODY=$(cat /tmp/release-notes.txt) |
There was a problem hiding this comment.
Line 36 captures the changelog content into the BODY variable, but this variable is no longer needed if the fix for multiline content handling is applied (using -F body=@/tmp/release-notes.txt instead of -f body="$BODY"). This line can be removed to simplify the script.
…_name gh release create produces untagged-* drafts when a release for the tag already exists (e.g. created by release-plz ~60s before publish-cli runs). Instead of using the gh CLI wrapper, call the REST API directly so that tag_name is set explicitly and can never fall back to an auto-generated value. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
7c1508e to
8ecbb58
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #508 +/- ##
=======================================
Coverage 77.88% 77.88%
=======================================
Files 48 48
Lines 6660 6660
Branches 6660 6660
=======================================
Hits 5187 5187
Misses 1109 1109
Partials 364 364 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
### 🐛 Bug Fixes - **(publish)** use gh api directly to create release with explicit tag_name by [@jdx](https://github.com/jdx) in [#508](#508)
| -f name="$TAG_NAME" \ | ||
| -f body="$BODY" \ | ||
| -F draft=true \ | ||
| --jq '.html_url' |
There was a problem hiding this comment.
API POST fails with 422 when release already exists
High Severity
The PR description identifies that "something creates a GitHub release for the tag ~60 seconds before the publish-cli workflow starts." The GitHub REST API POST /repos/{owner}/{repo}/releases returns a 422 Validation Failed (already_exists) error when a release for that tag_name already exists. This means the create-release job will fail, and since build-and-publish has needs: [create-release], the entire binary upload pipeline stops — no binaries get built or uploaded at all. This is a regression from the old behavior, where gh release create at least allowed the pipeline to continue (producing an untagged-* draft).
| -f tag_name="$TAG_NAME" \ | ||
| -f target_commitish="$GITHUB_SHA" \ | ||
| -f name="$TAG_NAME" \ | ||
| -f body="$BODY" \ |
There was a problem hiding this comment.
Shell variable for body less robust than file input
Low Severity
Capturing release notes via BODY=$(cat /tmp/release-notes.txt) and passing them with -f body="$BODY" introduces unnecessary fragility — command substitution strips trailing newlines and passes potentially large content through a shell variable. The gh api command natively supports reading file content via -F body=@/tmp/release-notes.txt, which is more robust, eliminates the intermediate BODY variable, and is the idiomatic approach.


Summary
--target "$GITHUB_SHA"togh release create, but the releases are still created asuntagged-*publish-cliworkflow starts (timestamps: v2.17.1 release at 12:36:44, workflow start at 12:37:44; v2.17.2 release at 13:07:56, workflow start at 13:08:47). Whengh release create "v2.17.x"finds an existing release for that tag, it silently creates a newuntagged-*draft as a fallback instead of failinggh api repos/${{ github.repository }}/releases --method POSTdirectly with explicit-f tag_name=and-f target_commitish=fields. This bypasses thegh release createwrapper entirely and guarantees thetag_nameis set correctly regardless of pre-existing releasesTest plan
tag_name: v2.x.x(notuntagged-*) viagh api repos/jdx/usage/releases --jq '.[0]'taiki-e/upload-rust-binary-actionfinds the release and uploads binaries successfully🤖 Generated with Claude Code
Note
Low Risk
Small, workflow-only change affecting how GitHub releases are created; primary risk is misconfigured API parameters causing draft release creation to fail.
Overview
Fixes the
publish-cliworkflow’s draft release creation to avoid accidentaluntagged-*releases when a release already exists for a just-pushed tag.Replaces
gh release createwith a directgh api POST /repos/{owner}/{repo}/releases, explicitly settingtag_name,target_commitish, and the changelog-derived release body before continuing with the existing release-notes enhancement and publish steps.Written by Cursor Bugbot for commit 8ecbb58. This will update automatically on new commits. Configure here.