diff --git a/.github/workflows/gen-release-pr.yml b/.github/workflows/gen-release-pr.yml
index 98254369..63d68721 100644
--- a/.github/workflows/gen-release-pr.yml
+++ b/.github/workflows/gen-release-pr.yml
@@ -90,6 +90,11 @@ jobs:
PATCH_COUNT=$((PATCH_COUNT + 1))
PATCH_PRS="$PATCH_PRS\n- $PR_TITLE (#$PR_NUM)"
fi
+
+ # Collect data for rich PR body
+ PR_AUTHOR=$(gh api repos/$OWNER/$REPO/pulls/$PR_NUM --jq '.user.login' 2>/dev/null || echo "unknown")
+ LABELS_JSON=$(echo "$LABELS" | jq -R -s 'split("\n") | map(select(length > 0))')
+ echo "{\"number\":$PR_NUM,\"title\":$(echo "$PR_TITLE" | jq -Rs .),\"author\":\"$PR_AUTHOR\",\"labels\":$LABELS_JSON}" >> pr-data.jsonl
fi
done <<< "$COMMITS"
@@ -110,6 +115,7 @@ jobs:
echo "Final release type: $RELEASE_TYPE"
echo "release-type=$RELEASE_TYPE" >> $GITHUB_OUTPUT
+ echo "latest-tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "labeled-pr-list=$LABELED_PR_LIST" >> $GITHUB_OUTPUT
- name: Compute new version
@@ -166,6 +172,92 @@ jobs:
echo "Re-generated documentation"
git status
+ - name: Generate PR body
+ if: steps.version-update.outputs.new-version != ''
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OWNER: ${{ github.repository_owner }}
+ REPO: ${{ github.event.repository.name }}
+ run: |
+ VERSION="${{ steps.version-update.outputs.new-version }}"
+ SHA="${{ steps.doc-gen.outputs.plugin-sha }}"
+ LATEST_TAG="${{ steps.unreleased.outputs.latest-tag }}"
+
+ # Generate release notes preview using shared script
+ ./scripts/generate-release-notes.sh \
+ "$LATEST_TAG" "$VERSION" "$SHA" \
+ --file /tmp/release-notes.txt
+
+ # Read PR data
+ ALL_PRS=$(jq -s '.' pr-data.jsonl)
+
+ # Categorize PRs for the body list (just #XYZ, no title)
+ MAJOR_PRS=$(echo "$ALL_PRS" | jq -r '.[] | select(.labels | index("major")) | .author |= gsub("^app/"; "") | "- #\(.number) â @\(.author)"')
+ MINOR_PRS=$(echo "$ALL_PRS" | jq -r '.[] | select(.labels | index("minor")) | .author |= gsub("^app/"; "") | "- #\(.number) â @\(.author)"')
+ PATCH_PRS=$(echo "$ALL_PRS" | jq -r '.[] | select(.labels | index("patch")) | .author |= gsub("^app/"; "") | "- #\(.number) â @\(.author)"')
+ UNLABELED_PRS=$(echo "$ALL_PRS" | jq -r '.[] | select((.labels | index("major") | not) and (.labels | index("minor") | not) and (.labels | index("patch") | not) and (.labels | index("release") | not)) | .author |= gsub("^app/"; "") | "- #\(.number) â @\(.author)"')
+
+ CONTRIBUTORS=$(echo "$ALL_PRS" | jq -r '[.[].author] | unique | map(gsub("^app/"; "")) | .[] | "@\(.)"' | paste -sd ', ')
+
+ # Build the PR body header
+ cat > pr-body.md <<- BODY
+ ## Automated Release Prep
+
+ **Release Type:** ${{ steps.unreleased.outputs.release-type }}
+ **New Version:** ${VERSION}
+
+ ---
+
+ ## đ What's included in this release
+
+ ### đĻ Merged PRs (since \`${LATEST_TAG}\`)
+ BODY
+
+ # Add categorized PRs (just #XYZ format)
+ if [ -n "$MAJOR_PRS" ]; then
+ echo -e "\n**đ¨ Major Changes**\n$MAJOR_PRS" >> pr-body.md
+ fi
+ if [ -n "$MINOR_PRS" ]; then
+ echo -e "\n**⨠New Features**\n$MINOR_PRS" >> pr-body.md
+ fi
+ if [ -n "$PATCH_PRS" ]; then
+ echo -e "\n**đ Bug Fixes**\n$PATCH_PRS" >> pr-body.md
+ fi
+ if [ -n "$UNLABELED_PRS" ]; then
+ echo -e "\n**đ Other / Infrastructure**\n$UNLABELED_PRS" >> pr-body.md
+ fi
+
+ echo -e "\n\n**Contributors:** ${CONTRIBUTORS}" >> pr-body.md
+
+ # Add collapsible release notes preview
+ cat >> pr-body.md <<- PREVIEW
+
+ ---
+
+ ## đŽ Release Notes Preview
+
+
+ Click to see what the release notes will look like
+
+ \`\`\`\`
+ PREVIEW
+
+ cat /tmp/release-notes.txt >> pr-body.md
+
+ cat >> pr-body.md <<- PREVIEW
+ \`\`\`\`
+
+
+
+ ---
+
+ > **âšī¸** This PR was created automatically. It updates documentation with the release version and sha.
+ > When merged, the Release workflow will create the GitHub release with the accumulated release notes.
+ PREVIEW
+
+ echo "Generated PR body:"
+ cat pr-body.md
+
- name: Create release PR
if: steps.version-update.outputs.new-version != ''
uses: peter-evans/create-pull-request@v5
@@ -173,15 +265,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: prepare release ${{ steps.version-update.outputs.new-version }}"
title: "Release Prep ${{ steps.version-update.outputs.new-version }}"
- body: |
- ## Automated Release Prep
-
- **Release Type:** ${{ steps.unreleased.outputs.release-type }}
- **New Version:** ${{ steps.version-update.outputs.new-version }}
-
- This PR was created automatically. It updates documentation with the release version and sha.
-
- When merged, the Release workflow will create the GitHub release with the accumulated release notes.
+ body-path: pr-body.md
branch: release-prep
base: ${{ github.ref_name }}
labels: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aeb27f36..8aad5cdc 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -70,51 +70,18 @@ jobs:
id: notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OWNER: ${{ github.repository_owner }}
+ REPO: ${{ github.event.repository.name }}
run: |
- OWNER="${{ github.repository_owner }}"
- REPO="${{ github.event.repository.name }}"
VERSION=${{ steps.extract-version.outputs.version }}
-
PREVIOUS_TAG=$(git tag --sort=-v:refname | head -1)
- if [ -z "$PREVIOUS_TAG" ]; then
- COMMITS=$(git log --oneline --grep="Merge pull request" --grep="#[0-9]" -E)
- else
- COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --oneline --grep="Merge pull request" --grep="#[0-9]" -E)
- fi
-
NOTES_FILE=$(mktemp)
echo "notes-file=$NOTES_FILE" >> $GITHUB_OUTPUT
- cat > "$NOTES_FILE" <<- ENDOFNOTE
- ## Release ${VERSION}
-
- Release sha256 is \`${SHA256_HASH}\`
-
- ## Configuration example
- \`\`\`yaml
- version: '2'
- plugins:
- - name: csharp
- wasm:
- url: https://github.com/${{ github.repository }}/releases/download/${VERSION}/sqlc-gen-csharp.wasm
- sha256: ${SHA256_HASH}
- \`\`\`
-
- ## Changelog
- ENDOFNOTE
-
- while IFS= read -r commit; do
- PR_NUM=$(echo "$commit" | grep -oE '#[0-9]+' | tr -d '#' || echo "")
- if [ -n "$PR_NUM" ]; then
- PR_TITLE=$(gh api repos/$OWNER/$REPO/pulls/$PR_NUM --jq '.title' 2>/dev/null || echo "PR #$PR_NUM")
- echo "- $PR_TITLE (#$PR_NUM)" >> "$NOTES_FILE"
- fi
- done <<< "$COMMITS"
-
- echo "" >> "$NOTES_FILE"
- echo "## Contributors" >> "$NOTES_FILE"
- echo "* @SockworkOrange" >> "$NOTES_FILE"
+ ./scripts/generate-release-notes.sh \
+ "$PREVIOUS_TAG" "$VERSION" "$SHA256_HASH" \
+ --file "$NOTES_FILE"
- name: Create release and upload asset
env:
diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh
new file mode 100755
index 00000000..1d890cf7
--- /dev/null
+++ b/scripts/generate-release-notes.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+# generate-release-notes.sh
+#
+# Shared release notes generator for sqlc-gen-csharp.
+# Only includes PRs with major/minor/patch labels (excludes skip-release).
+#
+# Usage:
+# OWNER=... REPO=... GH_TOKEN=... ./scripts/generate-release-notes.sh \
+# [--file ]
+
+set -euo pipefail
+
+: "${OWNER:?}" "${REPO:?}" "${GH_TOKEN:?}"
+
+PREVIOUS_TAG="${1:?Usage: generate-release-notes.sh [--file ]}"
+NEW_VERSION="${2:?}"
+SHA256_HASH="${3:?}"
+OUTPUT_FILE=""
+shift 3
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --file) OUTPUT_FILE="$2"; shift 2 ;;
+ *) echo "Unknown option: $1" >&2; exit 1 ;;
+ esac
+done
+
+# Get commits between tags (match PR numbers like #123)
+if [ -z "$PREVIOUS_TAG" ]; then
+ COMMITS=$(git log --oneline --grep="#[0-9]" -E 2>/dev/null || echo "")
+else
+ COMMITS=$(git log "${PREVIOUS_TAG}..HEAD" --oneline --grep="#[0-9]" -E 2>/dev/null || echo "")
+fi
+
+CHANGELOG_ITEMS=()
+CONTRIBUTORS=()
+
+while IFS= read -r commit; do
+ [ -z "$commit" ] && continue
+ PR_NUM=$(echo "$commit" | grep -oE '#[0-9]+' | tr -d '#' || echo "")
+ [ -z "$PR_NUM" ] && continue
+
+ PR_JSON=$(gh api "repos/$OWNER/$REPO/pulls/$PR_NUM" \
+ --jq '{title: .title, author: .user.login, labels: [.labels[].name]}' 2>/dev/null || echo '{}')
+ PR_TITLE=$(echo "$PR_JSON" | jq -r '.title // empty')
+ PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.author // empty')
+ [ -z "$PR_TITLE" ] && continue
+
+ HAS_LABEL=false
+ SKIP_RELEASE=false
+ while IFS= read -r label; do
+ [ -z "$label" ] && continue
+ case "$label" in
+ major|minor|patch) HAS_LABEL=true ;;
+ skip-release) SKIP_RELEASE=true ;;
+ esac
+ done <<< "$(echo "$PR_JSON" | jq -r '.labels[] // empty')"
+
+ if $HAS_LABEL && ! $SKIP_RELEASE; then
+ CHANGELOG_ITEMS+=("- $PR_TITLE (#$PR_NUM)")
+ CLEAN_AUTHOR="${PR_AUTHOR#app/}"
+ FOUND=false
+ for c in "${CONTRIBUTORS[@]}"; do
+ [ "$c" = "$CLEAN_AUTHOR" ] && FOUND=true && break
+ done
+ $FOUND || CONTRIBUTORS+=("$CLEAN_AUTHOR")
+ fi
+done <<< "$COMMITS"
+
+# Write output
+if [ -n "$OUTPUT_FILE" ]; then
+ exec > "$OUTPUT_FILE"
+fi
+
+echo "## Release ${NEW_VERSION}"
+echo ""
+echo "Release sha256 is \`${SHA256_HASH}\`"
+echo ""
+echo "## Configuration example"
+echo '```yaml'
+echo "version: '2'"
+echo "plugins:"
+echo "- name: csharp"
+echo " wasm:"
+echo " url: https://github.com/${OWNER}/${REPO}/releases/download/${NEW_VERSION}/sqlc-gen-csharp.wasm"
+echo " sha256: ${SHA256_HASH}"
+echo '```'
+echo ""
+echo "## Changelog"
+for item in "${CHANGELOG_ITEMS[@]}"; do
+ echo "$item"
+done
+echo ""
+echo "## Contributors"
+for author in "${CONTRIBUTORS[@]}"; do
+ echo "* @${author}"
+done