Skip to content

Commit 8aca5d4

Browse files
committed
ci(publish): auto-create GitHub Release on tag push
The publish workflow already runs on tag push and ships to npm via OIDC. Extend it so the same trigger creates / updates the corresponding GitHub Release with notes pulled from CHANGELOG.md's matching `## [VERSION]` section. This removes the manual `gh release create ...` step from the release workflow and keeps GitHub Releases in sync with what's on npm without separate ceremony. Implementation: - permissions.contents: read → write (needed to create the release). - New step resolves the tag from github.ref / github.event.inputs. - New step extracts the CHANGELOG section between `## [VERSION]` and the next `## ` heading using awk with string-comparison (`index($0, marker) == 1`) so brackets in the marker don't get interpreted as a regex char class. Falls back to the tag's annotation if the section can't be found. - New step is idempotent: gh release edit if exists, gh release create otherwise. --latest is set so the latest tag pushed becomes the displayed Latest release on the repo's home page. Verified locally that the extractor pulls the [0.2.2] section verbatim. The next tag push (e.g. 0.2.3) will exercise the workflow end-to-end.
1 parent bfa050d commit 8aca5d4

1 file changed

Lines changed: 54 additions & 1 deletion

File tree

.github/workflows/publish.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ on:
2323
required: false
2424

2525
permissions:
26-
contents: read
26+
contents: write # required to create the GitHub Release on tag push
2727
id-token: write # required for OIDC + provenance attestation
2828
actions: read # required to query other workflow runs
2929

@@ -117,3 +117,56 @@ jobs:
117117
118118
- name: Publish to npm with provenance via OIDC
119119
run: npm publish --provenance --access public
120+
121+
- name: Resolve release tag
122+
id: tag
123+
run: |
124+
ref="${{ github.event.inputs.tag || github.ref_name }}"
125+
# github.ref on a tag push is "refs/tags/vX.Y.Z"; ref_name is "vX.Y.Z"
126+
if [[ "$ref" == refs/tags/* ]]; then ref="${ref#refs/tags/}"; fi
127+
echo "name=$ref" >> "$GITHUB_OUTPUT"
128+
echo "Release tag: $ref"
129+
130+
- name: Extract release notes from CHANGELOG
131+
id: notes
132+
run: |
133+
tag="${{ steps.tag.outputs.name }}"
134+
version="${tag#v}"
135+
marker="## [$version]"
136+
# Pull the section starting at "## [VERSION]" up to the next "## "
137+
# heading. String-comparison match (not regex) so brackets in the
138+
# version don't get interpreted as a char-class.
139+
awk -v marker="$marker" '
140+
index($0, marker) == 1 { in_sec = 1; print; next }
141+
in_sec && /^## / { exit }
142+
in_sec { print }
143+
' CHANGELOG.md > /tmp/release-notes.md
144+
if [ ! -s /tmp/release-notes.md ]; then
145+
echo "Could not extract notes for $version from CHANGELOG.md; falling back to tag annotation."
146+
git tag -l --format='%(contents)' "$tag" > /tmp/release-notes.md
147+
fi
148+
echo "notes-path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
149+
echo "--- release notes preview ---"
150+
head -40 /tmp/release-notes.md
151+
152+
- name: Create / update GitHub Release for the tag
153+
env:
154+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
155+
GH_REPO: ${{ github.repository }}
156+
run: |
157+
tag="${{ steps.tag.outputs.name }}"
158+
# Idempotent: create if missing, edit if exists. Mark Latest only
159+
# when the tag is on the default branch's tip lineage (gh handles
160+
# this automatically via --latest).
161+
if gh release view "$tag" --json tagName --jq .tagName 2>/dev/null; then
162+
gh release edit "$tag" \
163+
--title "$tag" \
164+
--notes-file "${{ steps.notes.outputs.notes-path }}" \
165+
--latest
166+
else
167+
gh release create "$tag" \
168+
--title "$tag" \
169+
--notes-file "${{ steps.notes.outputs.notes-path }}" \
170+
--latest
171+
fi
172+
echo "✓ GitHub Release ready for $tag"

0 commit comments

Comments
 (0)