outputs:
release-notes:
description: 'The complete generated release notes including frontend changes'
- value: ${{ steps.finalize.outputs.release-notes }}
+ value: ${{ steps.generate.outputs.release-notes }}
runs:
using: 'composite'
steps:
run: |
pip install PyGithub PyYAML
- - name: Generate base release notes
+ - name: Generate complete release notes
id: generate
shell: bash
env:
run: |
chmod +x ${{ github.action_path }}/generate_notes.py
python3 ${{ github.action_path }}/generate_notes.py
-
- - name: Extract and merge frontend changes
- id: frontend
- shell: bash
- env:
- GITHUB_TOKEN: ${{ inputs.github-token }}
- VERSION: ${{ inputs.version }}
- PREVIOUS_TAG: ${{ inputs.previous-tag }}
- BRANCH: ${{ inputs.branch }}
- BASE_NOTES: ${{ steps.generate.outputs.release-notes }}
- SERVER_CONTRIBUTORS: ${{ steps.generate.outputs.contributors }}
- run: |
- # Save base notes to file from environment variable
- echo "$BASE_NOTES" > /tmp/base_notes.md
-
- # Create temp files for frontend changes
- FRONTEND_FILE=$(mktemp)
- CONTRIBUTORS_FILE=$(mktemp)
-
- # Start with server contributors
- echo "$SERVER_CONTRIBUTORS" | tr ',' '\n' > "$CONTRIBUTORS_FILE"
-
- # Find frontend update PRs
- echo "📦 Looking for frontend update PRs..."
-
- if [ -z "$PREVIOUS_TAG" ]; then
- echo "No previous tag, searching recent merged PRs"
- gh pr list --state merged --limit 100 --json number,title,body --jq '.[] | select(.title | test("^⬆️ Update music-assistant-frontend to [0-9]")) | {number: .number, title: .title, body: .body}' > /tmp/frontend_prs.json
- else
- echo "Searching between $PREVIOUS_TAG and $BRANCH"
- git log "$PREVIOUS_TAG..$BRANCH" --oneline --merges | grep -oP '#\K[0-9]+' > /tmp/pr_numbers.txt || echo ""
-
- > /tmp/frontend_prs.json
- while read -r PR_NUM; do
- if [ -n "$PR_NUM" ]; then
- PR_DATA=$(gh pr view "$PR_NUM" --json number,title,body 2>/dev/null || echo "")
- if [ -n "$PR_DATA" ]; then
- if echo "$PR_DATA" | jq -e '.title | test("^⬆️ Update music-assistant-frontend to [0-9]")' > /dev/null 2>&1; then
- echo "$PR_DATA" >> /tmp/frontend_prs.json
- fi
- fi
- fi
- done < /tmp/pr_numbers.txt
- fi
-
- # Process frontend PRs
- if [ -s /tmp/frontend_prs.json ]; then
- echo "## 🎨 Frontend Changes" > "$FRONTEND_FILE"
- echo "" >> "$FRONTEND_FILE"
-
- while IFS= read -r pr_json; do
- if [ -n "$pr_json" ]; then
- BODY=$(echo "$pr_json" | jq -r '.body')
-
- # Extract bullet points, excluding headers and dependabot lines
- echo "$BODY" | grep -E '^[[:space:]]*[•-]' | \
- grep -v '🙇' | \
- grep -viE '^[[:space:]]*[•-][[:space:]]*Chore\(deps' | \
- head -20 >> "$FRONTEND_FILE" || true
-
- # Extract contributors
- echo "$BODY" | grep -oP '@[a-zA-Z0-9_-]+' >> "$CONTRIBUTORS_FILE" || true
- fi
- done < /tmp/frontend_prs.json
-
- # Check if we found changes
- if [ $(wc -l < "$FRONTEND_FILE") -gt 3 ]; then
- echo "✅ Found frontend changes"
-
- # Merge contributors (deduplicate and format)
- # Contributors already have @ from grep, so don't add it again
- MERGED_CONTRIBUTORS=$(sort -u "$CONTRIBUTORS_FILE" | paste -sd ", " -)
-
- # Find contributors section in base notes
- CONTRIB_LINE=$(grep -n "## :bow: Thanks to our contributors" /tmp/base_notes.md | head -1 | cut -d: -f1 || echo "")
-
- if [ -n "$CONTRIB_LINE" ]; then
- # Split notes at contributors section
- head -n $((CONTRIB_LINE - 1)) /tmp/base_notes.md > /tmp/before_contrib.md
-
- # Build final notes
- {
- cat /tmp/before_contrib.md
- echo ""
- cat "$FRONTEND_FILE"
- echo ""
- echo "## :bow: Thanks to our contributors"
- echo ""
- echo "Special thanks to the following contributors who helped with this release:"
- echo ""
- echo "$MERGED_CONTRIBUTORS"
- } > /tmp/final_notes.md
- else
- # No contributors section, just append
- {
- cat /tmp/base_notes.md
- echo ""
- cat "$FRONTEND_FILE"
- } > /tmp/final_notes.md
- fi
-
- echo "has_frontend_changes=true" >> $GITHUB_OUTPUT
- else
- echo "ℹ️ No frontend changes found"
- cp /tmp/base_notes.md /tmp/final_notes.md
- echo "has_frontend_changes=false" >> $GITHUB_OUTPUT
- fi
- else
- echo "ℹ️ No frontend PRs found"
- cp /tmp/base_notes.md /tmp/final_notes.md
- echo "has_frontend_changes=false" >> $GITHUB_OUTPUT
- fi
-
- # Cleanup
- rm -f "$FRONTEND_FILE" "$CONTRIBUTORS_FILE" /tmp/frontend_prs.json /tmp/pr_numbers.txt /tmp/before_contrib.md
-
- - name: Output final notes
- id: finalize
- shell: bash
- run: |
- # Read the final notes and output them
- FINAL_NOTES=$(cat /tmp/final_notes.md)
-
- # Use multiline output format
- cat >> $GITHUB_OUTPUT << 'EOF'
- release-notes<<NOTES_EOF
- EOF
- cat /tmp/final_notes.md >> $GITHUB_OUTPUT
- cat >> $GITHUB_OUTPUT << 'EOF'
- NOTES_EOF
- EOF
-
- # Cleanup
- rm -f /tmp/base_notes.md /tmp/final_notes.md
-
- echo "✅ Release notes generation complete"
return result.replace("$URL", pr.html_url)
-def generate_release_notes(config, categories, uncategorized, contributors, previous_tag):
+def extract_frontend_changes(prs):
+ """Extract frontend changes from frontend update PRs.
+
+ Returns tuple of (frontend_changes_list, frontend_contributors_set)
+ """
+ frontend_changes = []
+ frontend_contributors = set()
+
+ # Pattern to match frontend update PRs
+ frontend_pr_pattern = re.compile(r"^⬆️ Update music-assistant-frontend to \d")
+
+ for pr in prs:
+ if not frontend_pr_pattern.match(pr.title):
+ continue
+
+ print(f"Processing frontend PR #{pr.number}: {pr.title}") # noqa: T201
+
+ if not pr.body:
+ continue
+
+ # Extract bullet points from PR body, excluding headers and dependabot lines
+ for body_line in pr.body.split("\n"):
+ stripped_line = body_line.strip()
+ # Check if it's a bullet point
+ if stripped_line.startswith(("- ", "* ", "• ")):
+ # Skip thank you lines and dependency updates
+ if "🙇" in stripped_line:
+ continue
+ if re.match(r"^[•\-\*]\s*Chore\(deps", stripped_line, re.IGNORECASE):
+ continue
+
+ # Add the change
+ frontend_changes.append(stripped_line)
+
+ # Extract contributors mentioned in this line
+ contributors_in_line = re.findall(r"@([a-zA-Z0-9_-]+)", stripped_line)
+ frontend_contributors.update(contributors_in_line)
+
+ # Limit to 20 changes per PR
+ if len(frontend_changes) >= 20:
+ break
+
+ return frontend_changes, frontend_contributors
+
+
+def generate_release_notes( # noqa: PLR0915
+ config, categories, uncategorized, contributors, previous_tag, frontend_changes=None
+):
"""Generate the formatted release notes."""
lines = []
lines.append(f"_Changes since [{previous_tag}]({repo_url}/releases/tag/{previous_tag})_")
lines.append("")
- # Add categorized PRs
+ # Add categorized PRs - first pass: categories without "after-other" flag
category_configs = config.get("categories", [])
+ deferred_categories = []
+
for cat_config in category_configs:
+ # Defer categories marked with after-other
+ if cat_config.get("after-other", False):
+ deferred_categories.append(cat_config)
+ continue
+
cat_title = cat_config.get("title", "Other")
if cat_title not in categories or not categories[cat_title]:
continue
lines.append("")
+ # Add frontend changes if any (before "Other Changes")
+ if frontend_changes and len(frontend_changes) > 0:
+ lines.append("### 🎨 Frontend Changes")
+ lines.append("")
+ for change in frontend_changes:
+ lines.append(change)
+ lines.append("")
+
# Add uncategorized PRs if any
if uncategorized:
lines.append("### Other Changes")
lines.append(format_change_line(pr, config))
lines.append("")
+ # Add deferred categories (after "Other Changes")
+ for cat_config in deferred_categories:
+ cat_title = cat_config.get("title", "Other")
+ if cat_title not in categories or not categories[cat_title]:
+ continue
+
+ prs = categories[cat_title]
+ lines.append(f"### {cat_title}")
+ lines.append("")
+
+ # Check if category should be collapsed
+ collapse_after = cat_config.get("collapse-after")
+ if collapse_after and len(prs) > collapse_after:
+ lines.append("<details>")
+ lines.append(f"<summary>{len(prs)} changes</summary>")
+ lines.append("")
+
+ for pr in prs:
+ lines.append(format_change_line(pr, config))
+
+ if collapse_after and len(prs) > collapse_after:
+ lines.append("")
+ lines.append("</details>")
+
+ lines.append("")
+
# Add contributors section using template
if contributors:
template = config.get("template", "")
categories, uncategorized = categorize_prs(prs, config)
print(f"Categorized into {len(categories)} categories, {len(uncategorized)} uncategorized") # noqa: T201
- # Get contributors
+ # Extract frontend changes and contributors
+ frontend_changes_list, frontend_contributors_set = extract_frontend_changes(prs)
+ print( # noqa: T201
+ f"Found {len(frontend_changes_list)} frontend changes "
+ f"from {len(frontend_contributors_set)} contributors"
+ )
+
+ # Get server contributors
contributors_list = get_contributors(prs, config)
- print(f"Found {len(contributors_list)} contributors") # noqa: T201
+
+ # Merge frontend contributors with server contributors
+ all_contributors = set(contributors_list) | frontend_contributors_set
+ contributors_list = sorted(all_contributors)
+ print( # noqa: T201
+ f"Total {len(contributors_list)} unique contributors (server + frontend)"
+ )
# Generate formatted notes
notes = generate_release_notes(
- config, categories, uncategorized, contributors_list, previous_tag
+ config,
+ categories,
+ uncategorized,
+ contributors_list,
+ previous_tag,
+ frontend_changes_list,
)
# Output to GitHub Actions
- 'bugfix'
- title: '🧰 Maintenance and dependency bumps'
+ after-other: true # Show this section after "Other Changes"
collapse-after: 3
labels:
- 'ci'
channel: ${{ inputs.channel }}
github-token: ${{ secrets.GITHUB_TOKEN }}
+ - name: Format release title
+ id: format_title
+ run: |
+ VERSION="${{ inputs.version }}"
+ CHANNEL="${{ inputs.channel }}"
+
+ if [[ "$CHANNEL" == "nightly" ]]; then
+ # Extract base version and date from X.Y.Z.devYYYYMMDD
+ if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.dev([0-9]+)$ ]]; then
+ BASE="${BASH_REMATCH[1]}"
+ DATE="${BASH_REMATCH[2]}"
+ TITLE="$BASE Nightly $DATE"
+ else
+ TITLE="$VERSION Nightly"
+ fi
+ elif [[ "$CHANNEL" == "beta" ]]; then
+ # Extract base version and beta number from X.Y.Z.bN
+ if [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.b([0-9]+)$ ]]; then
+ BASE="${BASH_REMATCH[1]}"
+ BETA_NUM="${BASH_REMATCH[2]}"
+ TITLE="$BASE Beta $BETA_NUM"
+ else
+ TITLE="$VERSION Beta"
+ fi
+ else
+ # Stable release - just use the version
+ TITLE="$VERSION"
+ fi
+
+ echo "title=$TITLE" >> $GITHUB_OUTPUT
+ echo "Release title: $TITLE"
+
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.version }}
- name: ${{ inputs.version }}
+ name: ${{ steps.format_title.outputs.title }}
body: ${{ steps.generate_notes.outputs.release-notes }}
prerelease: ${{ needs.validate-and-build.outputs.is_prerelease }}
target_commitish: ${{ needs.validate-and-build.outputs.branch }}