|
| 1 | +# Copyright (C) 2019 Intel Corporation. All rights reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 3 | + |
| 4 | +# get the last release tag from git, and use it to find all merged PRs since |
| 5 | +# that tag. extract their titles, labels and PR numbers and classify them into |
| 6 | +# break changes, new # features, enhancements, bug fixes, and others based on |
| 7 | +# their labels. |
| 8 | +# |
| 9 | +# The release version is generated based on the last release tag. The tag |
| 10 | +# should be in the format of "WAMR-major.minor.patch", where major, minor, |
| 11 | +# and patch are numbers. If there is new feature in merged PRs, the minor |
| 12 | +# version should be increased by 1, and the patch version should be reset to 0. |
| 13 | +# If there is no new feature, the patch version should be increased by 1. |
| 14 | +# |
| 15 | +# new content should be inserted into the beginning of the RELEASE_NOTES.md file. |
| 16 | +# in a form like: |
| 17 | +# |
| 18 | +# ``` markdown |
| 19 | +# ## WAMR-major.minor.patch |
| 20 | +# |
| 21 | +# ### Breaking Changes |
| 22 | +# |
| 23 | +# ### New Features |
| 24 | +# |
| 25 | +# ### Bug Fixes |
| 26 | +# |
| 27 | +# ### Enhancements |
| 28 | +# |
| 29 | +# ### Others |
| 30 | +# ``` |
| 31 | +# The path of RELEASE_NOTES.md is passed in as an command line argument. |
| 32 | + |
| 33 | +import json |
| 34 | +import os |
| 35 | +import subprocess |
| 36 | +import sys |
| 37 | + |
| 38 | + |
| 39 | +def run_cmd(cmd): |
| 40 | + result = subprocess.run( |
| 41 | + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True |
| 42 | + ) |
| 43 | + if result.returncode != 0: |
| 44 | + print(f"Error running command: {cmd}\n{result.stderr}") |
| 45 | + sys.exit(1) |
| 46 | + return result.stdout.strip() |
| 47 | + |
| 48 | + |
| 49 | +def get_last_release_tag(): |
| 50 | + tags = run_cmd("git tag --sort=-creatordate").splitlines() |
| 51 | + for tag in tags: |
| 52 | + if tag.startswith("WAMR-"): |
| 53 | + return tag |
| 54 | + return None |
| 55 | + |
| 56 | + |
| 57 | +def get_merged_prs_since(tag): |
| 58 | + # Get commits since the last release tag |
| 59 | + log_cmd = f'git log {tag}..HEAD --pretty=format:"%s"' |
| 60 | + logs = run_cmd(log_cmd).splitlines() |
| 61 | + |
| 62 | + print(f"Found {len(logs)} merge commits since last tag '{tag}'.") |
| 63 | + |
| 64 | + pr_numbers = [] |
| 65 | + for line in logs: |
| 66 | + # assume the commit message ends with "(#PR_NUMBER)" |
| 67 | + if not line.endswith(")"): |
| 68 | + continue |
| 69 | + |
| 70 | + # Extract PR number |
| 71 | + parts = line.split("(#") |
| 72 | + if len(parts) < 2: |
| 73 | + continue |
| 74 | + |
| 75 | + # PR_NUMBER) -> PR_NUMBER |
| 76 | + pr_num = parts[1][:-1] |
| 77 | + pr_numbers.append(pr_num) |
| 78 | + return pr_numbers |
| 79 | + |
| 80 | + |
| 81 | +def get_pr_info(pr_number): |
| 82 | + # Use GitHub CLI to get PR info |
| 83 | + pr_json = run_cmd(f"gh pr view {pr_number} --json title,labels,url") |
| 84 | + pr_data = json.loads(pr_json) |
| 85 | + title = pr_data.get("title", "") |
| 86 | + labels = [label["name"] for label in pr_data.get("labels", [])] |
| 87 | + url = pr_data.get("url", "") |
| 88 | + return title, labels, url |
| 89 | + |
| 90 | + |
| 91 | +def classify_pr(title, labels, url): |
| 92 | + entry = f"- {title} (#{url.split('/')[-1]})" |
| 93 | + if "breaking-change" in labels: |
| 94 | + return "Breaking Changes", entry |
| 95 | + elif "new feature" in labels: |
| 96 | + return "New Features", entry |
| 97 | + elif "enhancement" in labels: |
| 98 | + return "Enhancements", entry |
| 99 | + elif "bug-fix" in labels: |
| 100 | + return "Bug Fixes", entry |
| 101 | + else: |
| 102 | + return "Others", entry |
| 103 | + |
| 104 | + |
| 105 | +def generate_release_notes(pr_numbers): |
| 106 | + sections = { |
| 107 | + "Breaking Changes": [], |
| 108 | + "New Features": [], |
| 109 | + "Bug Fixes": [], |
| 110 | + "Enhancements": [], |
| 111 | + "Others": [], |
| 112 | + } |
| 113 | + for pr_num in pr_numbers: |
| 114 | + title, labels, url = get_pr_info(pr_num) |
| 115 | + section, entry = classify_pr(title, labels, url) |
| 116 | + sections[section].append(entry) |
| 117 | + return sections |
| 118 | + |
| 119 | + |
| 120 | +def generate_version_string(last_tag, sections): |
| 121 | + last_tag_parts = last_tag.split("-")[-1] |
| 122 | + major, minor, patch = map(int, last_tag_parts.split(".")) |
| 123 | + |
| 124 | + if sections["New Features"]: |
| 125 | + minor += 1 |
| 126 | + patch = 0 |
| 127 | + else: |
| 128 | + patch += 1 |
| 129 | + |
| 130 | + return f"WAMR-{major}.{minor}.{patch}" |
| 131 | + |
| 132 | + |
| 133 | +def format_release_notes(version, sections): |
| 134 | + notes = [f"## {version}\n"] |
| 135 | + for section in [ |
| 136 | + "Breaking Changes", |
| 137 | + "New Features", |
| 138 | + "Bug Fixes", |
| 139 | + "Enhancements", |
| 140 | + "Others", |
| 141 | + ]: |
| 142 | + notes.append(f"### {section}\n") |
| 143 | + if sections[section]: |
| 144 | + notes.extend(sections[section]) |
| 145 | + else: |
| 146 | + notes.append("") |
| 147 | + notes.append("") |
| 148 | + return "\n".join(notes) |
| 149 | + |
| 150 | + |
| 151 | +def insert_release_notes(notes, RELEASE_NOTES_FILE): |
| 152 | + with open(RELEASE_NOTES_FILE, "r", encoding="utf-8") as f: |
| 153 | + old_content = f.read() |
| 154 | + with open(RELEASE_NOTES_FILE, "w", encoding="utf-8") as f: |
| 155 | + f.write(notes + old_content) |
| 156 | + |
| 157 | + |
| 158 | +def set_action_output(name, value): |
| 159 | + """Set the output for GitHub Actions.""" |
| 160 | + if not os.getenv("GITHUB_OUTPUT"): |
| 161 | + return |
| 162 | + |
| 163 | + print(f"{name}={value}") |
| 164 | + |
| 165 | + |
| 166 | +def main(RELEASE_NOTES_FILE): |
| 167 | + last_tag = get_last_release_tag() |
| 168 | + if not last_tag: |
| 169 | + print("No release tag found.") |
| 170 | + sys.exit(1) |
| 171 | + |
| 172 | + print(f"Last release tag: {last_tag}") |
| 173 | + |
| 174 | + pr_numbers = get_merged_prs_since(last_tag) |
| 175 | + if not pr_numbers: |
| 176 | + print("No merged PRs since last release.") |
| 177 | + sys.exit(0) |
| 178 | + |
| 179 | + print(f"Found {len(pr_numbers)} merged PRs since last release.") |
| 180 | + print(f"PR numbers: {', '.join(pr_numbers)}") |
| 181 | + |
| 182 | + sections = generate_release_notes(pr_numbers) |
| 183 | + |
| 184 | + next_version = generate_version_string(last_tag, sections) |
| 185 | + print(f"Next version will be: {next_version}") |
| 186 | + |
| 187 | + notes = format_release_notes(next_version, sections) |
| 188 | + insert_release_notes(notes, RELEASE_NOTES_FILE) |
| 189 | + print(f"Release notes for {next_version} generated and inserted.") |
| 190 | + |
| 191 | + set_action_output("next_version", next_version) |
| 192 | + |
| 193 | + |
| 194 | +if __name__ == "__main__": |
| 195 | + if len(sys.argv) > 1: |
| 196 | + RELEASE_NOTES_FILE = sys.argv[1] |
| 197 | + else: |
| 198 | + RELEASE_NOTES_FILE = os.path.join( |
| 199 | + os.path.dirname(__file__), "../../RELEASE_NOTES.md" |
| 200 | + ) |
| 201 | + |
| 202 | + main(RELEASE_NOTES_FILE) |
0 commit comments