From a90d8ec0db1156c32a7b60c5b56857437445e62e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Oct 2025 15:52:03 +0200 Subject: [PATCH] Final attempt to fix the release process --- .../actions/generate-release-notes/README.md | 163 ++++++++++ .../actions/generate-release-notes/action.yml | 187 ++++++++++++ .../generate-release-notes/generate_notes.py | 285 ++++++++++++++++++ ...e-drafter.yml => release-notes-config.yml} | 16 +- .github/workflows/RELEASE_NOTES_GENERATION.md | 31 +- .github/workflows/RELEASE_WORKFLOW_GUIDE.md | 9 +- .github/workflows/release.yml | 188 ++---------- 7 files changed, 694 insertions(+), 185 deletions(-) create mode 100644 .github/actions/generate-release-notes/README.md create mode 100644 .github/actions/generate-release-notes/action.yml create mode 100755 .github/actions/generate-release-notes/generate_notes.py rename .github/{release-drafter.yml => release-notes-config.yml} (68%) diff --git a/.github/actions/generate-release-notes/README.md b/.github/actions/generate-release-notes/README.md new file mode 100644 index 00000000..9b441169 --- /dev/null +++ b/.github/actions/generate-release-notes/README.md @@ -0,0 +1,163 @@ +# Custom Release Notes Generator + +## Overview + +We've replaced Release Drafter with a custom GitHub Action that generates release notes with full control over commit ranges and PR categorization. + +## Why Custom Solution? + +Release Drafter's `filter-by-commitish` feature proved unreliable: +- Inconsistent handling of branch references (`dev` vs `refs/heads/dev`) +- No way to explicitly specify the previous release tag +- Would sometimes include entire git history instead of just changes since last release +- Caused "body too long" errors when it tried to include all commits + +## How It Works + +### 1. Custom Action: `.github/actions/generate-release-notes/` + +This is a complete, self-contained GitHub Action that handles everything: + +**Inputs:** +- `version`: The version being released +- `previous-tag`: The previous release tag to compare against (optional) +- `branch`: The branch being released from +- `channel`: Release channel (stable/beta/nightly) +- `github-token`: GitHub token for API access + +**Outputs:** +- `release-notes`: Complete release notes including server changes, frontend changes, and merged contributors + +**What it does internally:** +1. Generates base release notes from server PRs +2. Extracts frontend changes from frontend update PRs +3. Merges contributors from both server and frontend +4. Returns complete, formatted release notes ready to publish + +### 2. Python Script: `generate_notes.py` + +The script: +1. **Loads configuration** from `.github/release-notes-config.yml` +2. **Fetches PRs** between the previous tag and current branch HEAD using GitHub API +3. **Categorizes PRs** based on labels: + - ⚠ Breaking Changes + - 🚀 New Providers + - 🚀 Features and enhancements + - 🐛 Bugfixes + - 🧰 Maintenance and dependency bumps +4. **Extracts contributors** excluding bots +5. **Formats notes** using templates from config + +### 3. Workflow Integration + +The release workflow is now incredibly simple: +1. **Detects previous tag** using channel-specific patterns +2. **Calls the action** with one step - that's it! +3. **Creates GitHub release** with the complete notes from the action + +All the complexity (server PRs, frontend PRs, contributor merging) is handled inside the reusable action. + +## Benefits + +✅ **Full control** - We explicitly specify which tag to compare against +✅ **Reliable** - No mysterious "entire history" issues +✅ **Consistent** - Uses same config format as Release Drafter +✅ **Faster** - Only fetches the PRs we need +✅ **Maintainable** - Clear Python code instead of black-box action +✅ **Flexible** - Easy to customize formatting or add features + +## Configuration + +The generator reads `.github/release-notes-config.yml`: + +```yaml +change-template: '- $TITLE (by @$AUTHOR in #$NUMBER)' + +exclude-contributors: + - dependabot + - dependabot[bot] + # ... more bots + +categories: + - title: "⚠ Breaking Changes" + labels: + - 'breaking-change' + - title: "🚀 Features and enhancements" + labels: + - 'feature' + - 'enhancement' + # ... more categories + +template: | + $CHANGES + + ## :bow: Thanks to our contributors + + Special thanks to the following contributors who helped with this release: + + $CONTRIBUTORS +``` + +## Example Output + +```markdown +## 📦 Nightly Release + +_Changes since [2.7.0.dev20251022](https://github.com/music-assistant/server/releases/tag/2.7.0.dev20251022)_ + +### 🚀 Features and enhancements + +- Add new audio processor (by @contributor1 in #123) +- Improve queue management (by @contributor2 in #124) + +### 🐛 Bugfixes + +- Fix playback issue (by @contributor1 in #125) + +## 🎨 Frontend Changes + +- Add dark mode toggle +- Improve mobile layout +- Fix typo in settings + +## :bow: Thanks to our contributors + +Special thanks to the following contributors who helped with this release: + +@contributor1, @contributor2, @frontend-contributor +``` + +## Testing + +To test locally: +```bash +cd .github/actions/generate-release-notes + +# Set environment variables +export GITHUB_TOKEN="your_token" +export VERSION="2.7.0.dev20251024" +export PREVIOUS_TAG="2.7.0.dev20251023" +export BRANCH="dev" +export CHANNEL="nightly" +export GITHUB_REPOSITORY="music-assistant/server" + +# Run the script +python3 generate_notes.py +``` + +## Maintenance + +To modify release notes formatting: +1. Edit `.github/release-notes-config.yml` to change categories, labels, or templates +2. Edit `generate_notes.py` if you need to change the generation logic +3. No changes needed to the main workflow unless adding new features + +## Configuration File Format + +The configuration file (`.github/release-notes-config.yml`) uses the same format as Release Drafter used to: +- ✅ Same configuration structure +- ✅ Same output formatting +- ✅ Same contributor exclusion logic +- ✅ Same label-based categorization + +But now we have **full control** over the commit range and complete visibility into the generation process. diff --git a/.github/actions/generate-release-notes/action.yml b/.github/actions/generate-release-notes/action.yml new file mode 100644 index 00000000..cfe6f085 --- /dev/null +++ b/.github/actions/generate-release-notes/action.yml @@ -0,0 +1,187 @@ +name: 'Generate Release Notes' +description: 'Generate complete release notes with PR categorization, frontend changes, and merged contributors' +inputs: + version: + description: 'The version being released' + required: true + previous-tag: + description: 'The previous release tag to compare against' + required: false + default: '' + branch: + description: 'The branch being released from' + required: true + channel: + description: 'The release channel (stable, beta, nightly)' + required: true + github-token: + description: 'GitHub token for API access' + required: true +outputs: + release-notes: + description: 'The complete generated release notes including frontend changes' + value: ${{ steps.finalize.outputs.release-notes }} +runs: + using: 'composite' + steps: + - name: Set up Python + uses: actions/setup-python@v6.0.0 + with: + python-version: '3.12' + + - name: Install dependencies + shell: bash + run: | + pip install PyGithub PyYAML + + - name: Generate base release notes + id: generate + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + VERSION: ${{ inputs.version }} + PREVIOUS_TAG: ${{ inputs.previous-tag }} + BRANCH: ${{ inputs.branch }} + CHANNEL: ${{ inputs.channel }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + 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 }} + run: | + # Get the base release notes + BASE_NOTES="${{ steps.generate.outputs.release-notes }}" + SERVER_CONTRIBUTORS="${{ steps.generate.outputs.contributors }}" + + # Save base notes to file + cat > /tmp/base_notes.md << 'EOF' + ${{ steps.generate.outputs.release-notes }} + EOF + + # 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) + MERGED_CONTRIBUTORS=$(sort -u "$CONTRIBUTORS_FILE" | sed 's/^/@/' | 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<> $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" diff --git a/.github/actions/generate-release-notes/generate_notes.py b/.github/actions/generate-release-notes/generate_notes.py new file mode 100755 index 00000000..e83da717 --- /dev/null +++ b/.github/actions/generate-release-notes/generate_notes.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Generate release notes based on PRs between two tags. + +Reads configuration from .github/release-notes-config.yml for categorization and formatting. +""" + +import os +import re +import sys +from collections import defaultdict + +import yaml +from github import Github, GithubException + + +def load_config(): + """Load the release-notes-config.yml configuration.""" + config_path = ".github/release-notes-config.yml" + if not os.path.exists(config_path): + print(f"Error: {config_path} not found") # noqa: T201 + sys.exit(1) + + with open(config_path) as f: + return yaml.safe_load(f) + + +def get_prs_between_tags(repo, previous_tag, current_branch): + """Get all merged PRs between the previous tag and current HEAD.""" + if not previous_tag: + print("No previous tag specified, will include all PRs from branch history") # noqa: T201 + # Get the first commit on the branch + commits = list(repo.get_commits(sha=current_branch)) + # Limit to last 100 commits to avoid going too far back + commits = commits[:100] + else: + print(f"Finding PRs between {previous_tag} and {current_branch}") # noqa: T201 + comparison = repo.compare(previous_tag, current_branch) + commits = comparison.commits + print(f"Found {comparison.total_commits} commits") # noqa: T201 + + # Extract PR numbers from commit messages + pr_numbers = set() + pr_pattern = re.compile(r"#(\d+)") + merge_pattern = re.compile(r"Merge pull request #(\d+)") + + for commit in commits: + message = commit.commit.message + # First check for merge commits + merge_match = merge_pattern.search(message) + if merge_match: + pr_numbers.add(int(merge_match.group(1))) + else: + # Look for PR references in the message + for match in pr_pattern.finditer(message): + pr_numbers.add(int(match.group(1))) + + print(f"Found {len(pr_numbers)} unique PRs") # noqa: T201 + + # Fetch the actual PR objects + prs = [] + for pr_num in sorted(pr_numbers): + try: + pr = repo.get_pull(pr_num) + if pr.merged: + prs.append(pr) + except GithubException as e: + print(f"Warning: Could not fetch PR #{pr_num}: {e}") # noqa: T201 + + return prs + + +def categorize_prs(prs, config): + """Categorize PRs based on their labels using the config.""" + categories = defaultdict(list) + uncategorized = [] + + # Get category definitions from config + category_configs = config.get("categories", []) + + # Get excluded labels + exclude_labels = set(config.get("exclude-labels", [])) + include_labels = config.get("include-labels") + if include_labels: + include_labels = set(include_labels) + + for pr in prs: + # Check if PR should be excluded + pr_labels = {label.name for label in pr.labels} + + if exclude_labels and pr_labels & exclude_labels: + continue + + if include_labels and not (pr_labels & include_labels): + continue + + # Try to categorize + categorized = False + for cat_config in category_configs: + cat_title = cat_config.get("title", "Other") + cat_labels = cat_config.get("labels", []) + if isinstance(cat_labels, str): + cat_labels = [cat_labels] + + # Check if PR has any of the category labels + if pr_labels & set(cat_labels): + categories[cat_title].append(pr) + categorized = True + break + + if not categorized: + uncategorized.append(pr) + + return categories, uncategorized + + +def get_contributors(prs, config): + """Extract unique contributors from PRs.""" + excluded = set(config.get("exclude-contributors", [])) + contributors = set() + + for pr in prs: + author = pr.user.login + if author not in excluded: + contributors.add(author) + + return sorted(contributors) + + +def format_change_line(pr, config): + """Format a single PR line using the change-template from config.""" + template = config.get("change-template", "- $TITLE (by @$AUTHOR in #$NUMBER)") + + # Get title and escape characters if specified + title = pr.title + escapes = config.get("change-title-escapes", "") + if escapes: + for char in escapes: + if char in title: + title = title.replace(char, "\\" + char) + + # Replace template variables + result = template.replace("$TITLE", title) + result = result.replace("$AUTHOR", pr.user.login) + result = result.replace("$NUMBER", str(pr.number)) + return result.replace("$URL", pr.html_url) + + +def generate_release_notes(config, categories, uncategorized, contributors, previous_tag): + """Generate the formatted release notes.""" + lines = [] + + # Add header if previous tag exists + if previous_tag: + repo_url = ( + os.environ.get("GITHUB_SERVER_URL", "https://github.com") + + "/" + + os.environ["GITHUB_REPOSITORY"] + ) + channel = os.environ.get("CHANNEL", "").title() + if channel: + lines.append(f"## 📦 {channel} Release") + lines.append("") + lines.append(f"_Changes since [{previous_tag}]({repo_url}/releases/tag/{previous_tag})_") + lines.append("") + + # Add categorized PRs + category_configs = config.get("categories", []) + for cat_config in category_configs: + 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("
") + lines.append(f"{len(prs)} changes") + 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("
") + + lines.append("") + + # Add uncategorized PRs if any + if uncategorized: + lines.append("### Other Changes") + lines.append("") + for pr in uncategorized: + lines.append(format_change_line(pr, config)) + lines.append("") + + # Add contributors section using template + if contributors: + template = config.get("template", "") + if "$CONTRIBUTORS" in template or not template: + lines.append("## :bow: Thanks to our contributors") + lines.append("") + lines.append( + "Special thanks to the following contributors who helped with this release:" + ) + lines.append("") + lines.append(", ".join(f"@{c}" for c in contributors)) + + return "\n".join(lines) + + +def main(): + """Generate release notes for the target version.""" + # Get environment variables + github_token = os.environ.get("GITHUB_TOKEN") + version = os.environ.get("VERSION") + previous_tag = os.environ.get("PREVIOUS_TAG", "") + branch = os.environ.get("BRANCH") + channel = os.environ.get("CHANNEL") + repo_name = os.environ.get("GITHUB_REPOSITORY") + + if not all([github_token, version, branch, channel, repo_name]): + print("Error: Missing required environment variables") # noqa: T201 + sys.exit(1) + + print(f"Generating release notes for {version} ({channel} channel)") # noqa: T201 + print(f"Repository: {repo_name}") # noqa: T201 + print(f"Branch: {branch}") # noqa: T201 + print(f"Previous tag: {previous_tag or 'None (first release)'}") # noqa: T201 + + # Initialize GitHub API + g = Github(github_token) + repo = g.get_repo(repo_name) + + # Load configuration + config = load_config() + print(f"Loaded config with {len(config.get('categories', []))} categories") # noqa: T201 + + # Get PRs between tags + prs = get_prs_between_tags(repo, previous_tag, branch) + print(f"Processing {len(prs)} merged PRs") # noqa: T201 + + if not prs: + print("No PRs found in range") # noqa: T201 + no_changes = config.get("no-changes-template", "* No changes") + notes = no_changes + contributors_list = [] + else: + # Categorize PRs + categories, uncategorized = categorize_prs(prs, config) + print(f"Categorized into {len(categories)} categories, {len(uncategorized)} uncategorized") # noqa: T201 + + # Get contributors + contributors_list = get_contributors(prs, config) + print(f"Found {len(contributors_list)} contributors") # noqa: T201 + + # Generate formatted notes + notes = generate_release_notes( + config, categories, uncategorized, contributors_list, previous_tag + ) + + # Output to GitHub Actions + # Use multiline output format + output_file = os.environ.get("GITHUB_OUTPUT") + if output_file: + with open(output_file, "a") as f: + f.write("release-notes<> $GITHUB_OUTPUT fi - - name: Create Release with Release Drafter - id: create_release - uses: release-drafter/release-drafter@v6 + - name: Generate complete release notes + id: generate_notes + uses: ./.github/actions/generate-release-notes with: version: ${{ inputs.version }} - tag: ${{ inputs.version }} - name: ${{ inputs.version }} - prerelease: ${{ needs.validate-and-build.outputs.is_prerelease }} - commitish: ${{ needs.validate-and-build.outputs.branch }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Add channel context to release notes - if: steps.prev_version.outputs.has_prev_tag == 'true' - run: | - PREV_TAG="${{ steps.prev_version.outputs.prev_tag }}" - CHANNEL="${{ inputs.channel }}" - VERSION="${{ inputs.version }}" - REPO="${{ github.repository }}" - - # Get current release notes from Release Drafter - gh release view "$VERSION" --json body --jq .body > /tmp/current_notes.md - - # Prepend channel context - echo "## 📦 ${CHANNEL^} Release" > /tmp/new_notes.md - echo "" >> /tmp/new_notes.md - echo "_Changes since [$PREV_TAG](https://github.com/$REPO/releases/tag/$PREV_TAG)_" >> /tmp/new_notes.md - echo "" >> /tmp/new_notes.md - echo "___" >> /tmp/new_notes.md - echo "" >> /tmp/new_notes.md - cat /tmp/current_notes.md >> /tmp/new_notes.md - - # Update the release - gh release edit "$VERSION" --notes-file /tmp/new_notes.md - - echo "✅ Added channel context to release notes" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract and append frontend changes to release notes - id: update_notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "📦 Extracting frontend changes from PRs..." - - # Get the current release body - RELEASE_BODY=$(gh release view "${{ inputs.version }}" --json body --jq .body) - - # Find all frontend update PRs since the previous tag - PREV_TAG="${{ steps.prev_version.outputs.prev_tag }}" - - # Create temp files - FRONTEND_FILE=$(mktemp) - CONTRIBUTORS_FILE=$(mktemp) - - if [ -z "$PREV_TAG" ]; then - echo "No previous tag found, 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 - # Get PR numbers from merge commits since previous tag - echo "Searching for frontend PRs between $PREV_TAG and HEAD" - git log $PREV_TAG..HEAD --oneline --merges | grep -oP '#\K[0-9]+' > /tmp/pr_numbers.txt || echo "" - - # Fetch details for frontend update PRs - > /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 - - # Extract contributors from server release notes (existing) - echo "$RELEASE_BODY" | grep -oP '@[a-zA-Z0-9_-]+' | sort -u > "$CONTRIBUTORS_FILE" || true - - # Process each frontend PR and extract changes + contributors - 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 from the body, excluding: - # - Section headers (🙇) - # - Dependabot dependency lines (starting with "Chore(deps") - echo "$BODY" | grep -E '^[[:space:]]*[•-]' | \ - grep -v '🙇' | \ - grep -viE '^[[:space:]]*[•-][[:space:]]*Chore\(deps' | \ - head -20 >> "$FRONTEND_FILE" || true - - # Extract contributors from frontend PR body - echo "$BODY" | grep -oP '@[a-zA-Z0-9_-]+' >> "$CONTRIBUTORS_FILE" || true - fi - done < /tmp/frontend_prs.json - - # Check if we actually found any changes - if [ $(wc -l < "$FRONTEND_FILE") -gt 3 ]; then - echo "✅ Found frontend changes" - - # Deduplicate and sort contributors - MERGED_CONTRIBUTORS=$(sort -u "$CONTRIBUTORS_FILE" | paste -sd ", " -) - - # Split release body into parts (before and after contributors section) - echo "$RELEASE_BODY" > /tmp/original_notes.md - - # Find where the contributors section starts - CONTRIB_LINE=$(grep -n "## :bow: Thanks to our contributors" /tmp/original_notes.md | head -1 | cut -d: -f1 || echo "") - - if [ -n "$CONTRIB_LINE" ]; then - # Extract everything before contributors section - head -n $((CONTRIB_LINE - 1)) /tmp/original_notes.md > /tmp/notes_before_contrib.md - - # Build new release notes with frontend changes and merged contributors - { - cat /tmp/notes_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/updated_notes.md - else - # No contributors section found, just append frontend changes - { - cat /tmp/original_notes.md - echo "" - cat "$FRONTEND_FILE" - } > /tmp/updated_notes.md - fi - - # Update the release - gh release edit "${{ inputs.version }}" --notes-file /tmp/updated_notes.md - - echo "✅ Release notes updated with frontend changes and merged contributors" - else - echo "ℹ️ No frontend bullet points found" - fi - else - echo "ℹ️ No frontend update PRs found in this release" - fi - - # Cleanup - rm -f "$FRONTEND_FILE" "$CONTRIBUTORS_FILE" /tmp/frontend_prs.json /tmp/pr_numbers.txt /tmp/updated_notes.md /tmp/original_notes.md /tmp/notes_before_contrib.md + previous-tag: ${{ steps.prev_version.outputs.prev_tag }} + branch: ${{ needs.validate-and-build.outputs.branch }} + channel: ${{ inputs.channel }} + github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Upload artifacts to release + - name: Create GitHub Release + id: create_release uses: softprops/action-gh-release@v2 with: tag_name: ${{ inputs.version }} + name: ${{ inputs.version }} + body: ${{ steps.generate_notes.outputs.release-notes }} + prerelease: ${{ needs.validate-and-build.outputs.is_prerelease }} + target_commitish: ${{ needs.validate-and-build.outputs.branch }} files: dist/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Output release info + run: | + echo "✅ Created release ${{ inputs.version }}" + echo "Release URL: ${{ steps.create_release.outputs.url }}" + pypi-publish: name: Publish release to PyPI (stable releases only) runs-on: ubuntu-latest - needs: - - validate-and-build - - create-release - if: ${{ inputs.channel == 'stable' }} + needs: create-release + if: inputs.channel == 'stable' + permissions: + id-token: write steps: - - name: Retrieve release distributions + - name: Download distributions uses: actions/download-artifact@v5 with: name: release-dists path: dist/ - - name: Publish release to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 build-and-push-container-image: name: Build and push Music Assistant Server container to ghcr.io -- 2.34.1