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