Repo Update #6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update Changelog | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - master | |
| workflow_dispatch: | |
| jobs: | |
| update-changelog: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.x' | |
| - name: Generate Changelog | |
| run: | | |
| python3 << 'EOF' | |
| import subprocess | |
| import sys | |
| from datetime import datetime | |
| from collections import defaultdict | |
| def run_git_log(args): | |
| try: | |
| result = subprocess.run(['git'] + args, capture_output=True, text=True, check=True) | |
| # FIX #1: Split by a null character instead of a newline to handle multi-line bodies. | |
| return result.stdout.strip().split('\x00') if result.stdout.strip() else [] | |
| except subprocess.CalledProcessError as e: | |
| print(f"Error running git: {e}") | |
| sys.exit(1) | |
| def get_commits_by_tag(): | |
| try: | |
| latest_tag = subprocess.run( | |
| ['git', 'describe', '--tags', '--abbrev=0'], | |
| capture_output=True, text=True, check=True | |
| ).stdout.strip() | |
| tag_date = subprocess.run( | |
| ['git', 'log', '-1', '--format=%ad', '--date=short', latest_tag], | |
| capture_output=True, text=True, check=True | |
| ).stdout.strip() | |
| tagged = run_git_log([ | |
| 'log', latest_tag, | |
| # FIX #2: Add the %x00 null character as a safe delimiter for commits. | |
| '--pretty=format:%s|||%b|||%an|||%ad|||%H%x00', | |
| '--date=short', | |
| '--invert-grep', | |
| '--grep=docs: update changelog', | |
| '--grep=changelog.yml', | |
| '--grep=\\[skip ci\\]' | |
| ]) | |
| unreleased = run_git_log([ | |
| 'log', f'{latest_tag}..HEAD', | |
| # FIX #3: Add the delimiter here as well. | |
| '--pretty=format:%s|||%b|||%an|||%ad|||%H%x00', | |
| '--date=short', | |
| '--invert-grep', | |
| '--grep=docs: update changelog', | |
| '--grep=changelog.yml', | |
| '--grep=\\[skip ci\\]' | |
| ]) | |
| return { | |
| 'tagged': {latest_tag: {'commits': tagged, 'date': tag_date}}, | |
| 'unreleased': unreleased | |
| } | |
| except subprocess.CalledProcessError: | |
| all_commits = run_git_log([ | |
| 'log', | |
| # FIX #4: And add the delimiter here for the fallback case. | |
| '--pretty=format:%s|||%b|||%an|||%ad|||%H%x00', | |
| '--date=short', | |
| '--invert-grep', | |
| '--grep=docs: update changelog', | |
| '--grep=changelog.yml', | |
| '--grep=\\[skip ci\\]' | |
| ]) | |
| return { | |
| 'tagged': {}, | |
| 'unreleased': all_commits | |
| } | |
| def categorize_commit(subject, body): | |
| text = (subject + ' ' + body).lower() | |
| if any(x in text for x in ['security', 'vulnerability', 'cve', 'exploit']): | |
| return 'security' | |
| if any(x in text for x in ['breaking change', 'breaking:', 'break:']): | |
| return 'breaking' | |
| if any(x in text for x in ['deprecat', 'obsolete', 'phase out']): | |
| return 'deprecated' | |
| if any(x in text for x in ['remove', 'delete', 'drop', 'eliminate']): | |
| return 'removed' | |
| # Correction: Check for 'added' keywords before 'fixed' keywords. | |
| if any(x in text for x in ['add', 'new', 'create', 'implement', 'feat', 'feature']): | |
| return 'added' | |
| if any(x in text for x in ['fix', 'resolve', 'correct', 'patch', 'bug', 'issue']): | |
| return 'fixed' | |
| return 'changed' | |
| def format_commit_entry(commit): | |
| entry = f"- **{commit['subject']}** ({commit['date']} – {commit['author']})" | |
| body = commit['body'].replace('\\n', '\n') | |
| if body.strip(): | |
| lines = [line.strip() for line in body.splitlines() if line.strip()] | |
| for line in lines: | |
| entry += f"\n {line}" | |
| return entry + "\n" | |
| def parse_commits(lines): | |
| commits = [] | |
| for line in lines: | |
| if not line: continue | |
| parts = line.split('|||') | |
| if len(parts) >= 5: | |
| subject, body, author, date, hash_id = map(str.strip, parts) | |
| commits.append({ | |
| 'subject': subject, | |
| 'body': body, | |
| 'author': author, | |
| 'date': date, | |
| 'hash': hash_id[:7] | |
| }) | |
| return commits | |
| def build_changelog(commits_by_version): | |
| sections = [ | |
| ('security', 'Security'), | |
| ('breaking', 'Breaking Changes'), | |
| ('deprecated', 'Deprecated'), | |
| ('added', 'Added'), | |
| ('changed', 'Changed'), | |
| ('fixed', 'Fixed'), | |
| ('removed', 'Removed') | |
| ] | |
| lines = [ | |
| "# Changelog", | |
| "", | |
| "All notable changes to this project will be documented in this file.", | |
| "", | |
| "The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),", | |
| "and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).", | |
| "" | |
| ] | |
| unreleased = parse_commits(commits_by_version['unreleased']) | |
| if unreleased: | |
| lines.append("## [Unreleased]") | |
| lines.append("") | |
| categorized = defaultdict(list) | |
| for commit in unreleased: | |
| cat = categorize_commit(commit['subject'], commit['body']) | |
| categorized[cat].append(commit) | |
| for key, label in sections: | |
| if categorized[key]: | |
| lines.append(f"### {label}") | |
| for commit in categorized[key]: | |
| lines.append(format_commit_entry(commit)) | |
| lines.append("") | |
| else: | |
| lines.append("## [Unreleased]\n") | |
| lines.append("_No unreleased changes._\n") | |
| for tag, info in commits_by_version['tagged'].items(): | |
| commits = parse_commits(info['commits']) | |
| lines.append(f"## [{tag}] - {info['date']}\n") | |
| categorized = defaultdict(list) | |
| for commit in commits: | |
| cat = categorize_commit(commit['subject'], commit['body']) | |
| categorized[cat].append(commit) | |
| for key, label in sections: | |
| if categorized[key]: | |
| lines.append(f"### {label}") | |
| for commit in categorized[key]: | |
| lines.append(format_commit_entry(commit)) | |
| lines.append("") | |
| return "\n".join(lines) | |
| try: | |
| commit_data = get_commits_by_tag() | |
| changelog = build_changelog(commit_data) | |
| with open("CHANGELOG.md", "w", encoding="utf-8") as f: | |
| f.write(changelog) | |
| print("Changelog generated.") | |
| except Exception as e: | |
| print(f"Error generating changelog: {e}") | |
| sys.exit(1) | |
| EOF | |
| - name: Commit Updated Changelog | |
| run: | | |
| git config --local user.email "[email protected]" | |
| git config --local user.name "GitHub Action" | |
| git add CHANGELOG.md | |
| if git diff --staged --quiet; then | |
| echo "No changes to commit" | |
| else | |
| git commit -m "docs: update changelog [skip ci]" | |
| git push | |
| fi |