@@ -317,38 +317,64 @@ jobs:
317317 fi
318318 npm_metadata="Published to npm as \`@bitkyc08/opencodex@${RELEASE_VERSION}\` with dist-tag \`${NPM_DIST_TAG}\`."
319319
320- if [ -z "$existing_tag_sha" ]; then
321- git tag "$release_tag" "$GITHUB_SHA"
322- git push origin "refs/tags/${release_tag}"
323- fi
324-
325320 # Preview builds must be marked prerelease so GitHub "latest" keeps pointing at the
326321 # stable channel (matching npm dist-tags); see issue #64.
327322 prerelease_flag=""
328323 if [[ "$RELEASE_VERSION" == *-preview.* ]]; then
329324 prerelease_flag="--prerelease"
330325 fi
331326
332- if gh release view "$release_tag" >/dev/null 2>&1; then
333- notes_file="$(mktemp)"
327+ # Build notes before tagging: if generate-notes fails after a tag push, preflight
328+ # blocks retries because the tag already exists. API uses target_commitish, so the
329+ # tag need not exist yet. Preflight already rejects an existing GitHub Release for
330+ # non-dry runs, so this step only creates.
331+ notes_file="$(mktemp)"
332+ pr_notes=""
333+ if [ -n "$previous_tag" ]; then
334334 generate_notes_api=(
335335 "repos/${GITHUB_REPOSITORY}/releases/generate-notes"
336336 -f "tag_name=${release_tag}"
337337 -f "target_commitish=${GITHUB_SHA}"
338+ -f "previous_tag_name=${previous_tag}"
338339 )
339- if [ -n "$previous_tag" ]; then
340- generate_notes_api+=(-f "previous_tag_name=${previous_tag}")
341- fi
342- {
343- printf '%s\n\n' "$npm_metadata"
344- gh api "${generate_notes_api[@]}" --jq '.body'
345- } > "$notes_file"
346- gh release edit "$release_tag" --title "$release_tag" --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag}
340+ # Fail closed: missing PR categories is a broken release note, not a soft skip.
341+ pr_notes="$(gh api "${generate_notes_api[@]}" --jq '.body')"
342+ # Drop generate-notes' trailing compare link; we re-append it after the commit list.
343+ pr_notes="$(printf '%s\n' "$pr_notes" | sed '/^\*\*Full Changelog\*\*:/d')"
344+ else
345+ # First release on this channel: never call generate-notes without previous_tag_name.
346+ # GitHub would baseline the newest repo tag, which may belong to the other channel.
347+ echo "::notice::No previous channel tag; skipping generate-notes (commits-only notes)"
348+ fi
349+
350+ if [ -n "$previous_tag" ]; then
351+ commit_range="${previous_tag}..${GITHUB_SHA}"
352+ commits="$(git log --pretty=format:'- %s (%h)' "$commit_range")"
347353 else
348- generate_notes_args=(--generate-notes)
354+ # First release on a channel has no previous tag; cap history so the notes
355+ # body stays within GitHub release size limits.
356+ commits="$(git log --pretty=format:'- %s (%h)' --max-count=100 "$GITHUB_SHA")"
357+ fi
358+
359+ {
360+ printf '%s\n\n' "$npm_metadata"
361+ if [ -n "$(printf '%s' "$pr_notes" | tr -d '[:space:]')" ]; then
362+ printf '%s\n\n' "$pr_notes"
363+ fi
364+ if [ -n "$commits" ]; then
365+ printf '## Commits\n\n'
366+ printf '%s\n\n' "$commits"
367+ fi
349368 if [ -n "$previous_tag" ]; then
350- generate_notes_args+=(--notes-start-tag "$previous_tag")
369+ printf '**Full Changelog**: https://github.com/%s/compare/%s...%s\n' \
370+ "$GITHUB_REPOSITORY" "$previous_tag" "$release_tag"
351371 fi
352- gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \
353- --notes "$npm_metadata" "${generate_notes_args[@]}" ${prerelease_flag:+$prerelease_flag}
372+ } > "$notes_file"
373+
374+ if [ -z "$existing_tag_sha" ]; then
375+ git tag "$release_tag" "$GITHUB_SHA"
376+ git push origin "refs/tags/${release_tag}"
354377 fi
378+
379+ gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \
380+ --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag}
0 commit comments