From efc1ad820bb4d7ad2270d61d535c8bc59ad9e2a0 Mon Sep 17 00:00:00 2001 From: "liang.he@intel.com" Date: Fri, 18 Jul 2025 01:36:15 +0000 Subject: [PATCH 1/2] Add script to generate release notes from merged PRs --- .github/scripts/generate_release_notes.py | 202 ++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 .github/scripts/generate_release_notes.py diff --git a/.github/scripts/generate_release_notes.py b/.github/scripts/generate_release_notes.py new file mode 100644 index 000000000..af1bb3911 --- /dev/null +++ b/.github/scripts/generate_release_notes.py @@ -0,0 +1,202 @@ +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# get the last release tag from git, and use it to find all merged PRs since +# that tag. extract their titles, labels and PR numbers and classify them into +# break changes, new # features, enhancements, bug fixes, and others based on +# their labels. +# +# The release version is generated based on the last release tag. The tag +# should be in the format of "WAMR-major.minor.patch", where major, minor, +# and patch are numbers. If there is new feature in merged PRs, the minor +# version should be increased by 1, and the patch version should be reset to 0. +# If there is no new feature, the patch version should be increased by 1. +# +# new content should be inserted into the beginning of the RELEASE_NOTES.md file. +# in a form like: +# +# ``` markdown +# ## WAMR-major.minor.patch +# +# ### Breaking Changes +# +# ### New Features +# +# ### Bug Fixes +# +# ### Enhancements +# +# ### Others +# ``` +# The path of RELEASE_NOTES.md is passed in as an command line argument. + +import json +import os +import subprocess +import sys + + +def run_cmd(cmd): + result = subprocess.run( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + if result.returncode != 0: + print(f"Error running command: {cmd}\n{result.stderr}") + sys.exit(1) + return result.stdout.strip() + + +def get_last_release_tag(): + tags = run_cmd("git tag --sort=-creatordate").splitlines() + for tag in tags: + if tag.startswith("WAMR-"): + return tag + return None + + +def get_merged_prs_since(tag): + # Get commits since the last release tag + log_cmd = f'git log {tag}..HEAD --pretty=format:"%s"' + logs = run_cmd(log_cmd).splitlines() + + print(f"Found {len(logs)} merge commits since last tag '{tag}'.") + + pr_numbers = [] + for line in logs: + # assume the commit message ends with "(#PR_NUMBER)" + if not line.endswith(")"): + continue + + # Extract PR number + parts = line.split("(#") + if len(parts) < 2: + continue + + # PR_NUMBER) -> PR_NUMBER + pr_num = parts[1][:-1] + pr_numbers.append(pr_num) + return pr_numbers + + +def get_pr_info(pr_number): + # Use GitHub CLI to get PR info + pr_json = run_cmd(f"gh pr view {pr_number} --json title,labels,url") + pr_data = json.loads(pr_json) + title = pr_data.get("title", "") + labels = [label["name"] for label in pr_data.get("labels", [])] + url = pr_data.get("url", "") + return title, labels, url + + +def classify_pr(title, labels, url): + entry = f"- {title} (#{url.split('/')[-1]})" + if "breaking-change" in labels: + return "Breaking Changes", entry + elif "new feature" in labels: + return "New Features", entry + elif "enhancement" in labels: + return "Enhancements", entry + elif "bug-fix" in labels: + return "Bug Fixes", entry + else: + return "Others", entry + + +def generate_release_notes(pr_numbers): + sections = { + "Breaking Changes": [], + "New Features": [], + "Bug Fixes": [], + "Enhancements": [], + "Others": [], + } + for pr_num in pr_numbers: + title, labels, url = get_pr_info(pr_num) + section, entry = classify_pr(title, labels, url) + sections[section].append(entry) + return sections + + +def generate_version_string(last_tag, sections): + last_tag_parts = last_tag.split("-")[-1] + major, minor, patch = map(int, last_tag_parts.split(".")) + + if sections["New Features"]: + minor += 1 + patch = 0 + else: + patch += 1 + + return f"WAMR-{major}.{minor}.{patch}" + + +def format_release_notes(version, sections): + notes = [f"## {version}\n"] + for section in [ + "Breaking Changes", + "New Features", + "Bug Fixes", + "Enhancements", + "Others", + ]: + notes.append(f"### {section}\n") + if sections[section]: + notes.extend(sections[section]) + else: + notes.append("") + notes.append("") + return "\n".join(notes) + + +def insert_release_notes(notes, RELEASE_NOTES_FILE): + with open(RELEASE_NOTES_FILE, "r", encoding="utf-8") as f: + old_content = f.read() + with open(RELEASE_NOTES_FILE, "w", encoding="utf-8") as f: + f.write(notes + old_content) + + +def set_action_output(name, value): + """Set the output for GitHub Actions.""" + if not os.getenv("GITHUB_OUTPUT"): + return + + print(f"{name}={value}") + + +def main(RELEASE_NOTES_FILE): + last_tag = get_last_release_tag() + if not last_tag: + print("No release tag found.") + sys.exit(1) + + print(f"Last release tag: {last_tag}") + + pr_numbers = get_merged_prs_since(last_tag) + if not pr_numbers: + print("No merged PRs since last release.") + sys.exit(0) + + print(f"Found {len(pr_numbers)} merged PRs since last release.") + print(f"PR numbers: {', '.join(pr_numbers)}") + + sections = generate_release_notes(pr_numbers) + + next_version = generate_version_string(last_tag, sections) + print(f"Next version will be: {next_version}") + + notes = format_release_notes(next_version, sections) + insert_release_notes(notes, RELEASE_NOTES_FILE) + print(f"Release notes for {next_version} generated and inserted.") + + set_action_output("next_version", next_version) + + +if __name__ == "__main__": + if len(sys.argv) > 1: + RELEASE_NOTES_FILE = sys.argv[1] + else: + RELEASE_NOTES_FILE = os.path.join( + os.path.dirname(__file__), "../../RELEASE_NOTES.md" + ) + + main(RELEASE_NOTES_FILE) From 63741787466f0fd8b53758ca9422665a6995b164 Mon Sep 17 00:00:00 2001 From: "liang.he@intel.com" Date: Fri, 18 Jul 2025 02:06:19 +0000 Subject: [PATCH 2/2] Add workflow to prepare for the next release include - updating version files - generating release notes --- .github/scripts/generate_release_notes.py | 33 ++++++------ .github/workflows/prepare_release.yml | 66 +++++++++++++++++++++++ 2 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/prepare_release.yml diff --git a/.github/scripts/generate_release_notes.py b/.github/scripts/generate_release_notes.py index af1bb3911..8e20f07b8 100644 --- a/.github/scripts/generate_release_notes.py +++ b/.github/scripts/generate_release_notes.py @@ -2,9 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # get the last release tag from git, and use it to find all merged PRs since -# that tag. extract their titles, labels and PR numbers and classify them into -# break changes, new # features, enhancements, bug fixes, and others based on -# their labels. +# that tag. Since their titles might not following the same format, we use +# gh cli to search merged PR by commit SHA. +# +# Once having those PRs' information, extract their titles, labels and PR +# numbers and classify them into break changes, new features, enhancements, +# bug fixes, and others based on their labels. # # The release version is generated based on the last release tag. The tag # should be in the format of "WAMR-major.minor.patch", where major, minor, @@ -56,25 +59,25 @@ def get_last_release_tag(): def get_merged_prs_since(tag): # Get commits since the last release tag - log_cmd = f'git log {tag}..HEAD --pretty=format:"%s"' + log_cmd = f'git log {tag}..HEAD --pretty=format:"%s %H"' logs = run_cmd(log_cmd).splitlines() print(f"Found {len(logs)} merge commits since last tag '{tag}'.") pr_numbers = [] for line in logs: - # assume the commit message ends with "(#PR_NUMBER)" - if not line.endswith(")"): - continue + _, sha = line.rsplit(" ", 1) + # Use SHA to find merged PRs + pr_cmd = f"gh pr list --search {sha} --state merged --json number,title" + pr_json = run_cmd(pr_cmd) + pr_data = json.loads(pr_json) - # Extract PR number - parts = line.split("(#") - if len(parts) < 2: - continue + for pr in pr_data: + pr_number = pr.get("number") + print(f"Found PR #{pr_number} {pr['title']}") + if pr_number and pr_number not in pr_numbers: + pr_numbers.append(f"{pr_number}") - # PR_NUMBER) -> PR_NUMBER - pr_num = parts[1][:-1] - pr_numbers.append(pr_num) return pr_numbers @@ -145,7 +148,7 @@ def format_release_notes(version, sections): else: notes.append("") notes.append("") - return "\n".join(notes) + return "\n".join(notes) + "\n" def insert_release_notes(notes, RELEASE_NOTES_FILE): diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 000000000..ed86f7c2e --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,66 @@ +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# It includes: +# - add new content into the RELEASE_NOTES.md +# - update the version in build-scripts/version.cmake and core/version.h +# +# The new content is added to the beginning of the RELEASE_NOTES.md file. +# It includes all merged PR titles since the last release(tag). +# Based on every PR's label, it will be categorized into different sections. +# +# The version number is updated to the next version. +# Based on new content in the RELEASE_NOTES.md, it will be determined +# 1. if there is breaking change or new features, the next version will be a minor version +# 2. if there is no breaking change and new features, the next version will be a patch version +# +# At the end, file a PR to update the files. + +name: preparation for a release. + +on: + workflow_dispatch: + +# Cancel any in-flight jobs for the same PR/branch so there's only one active +# at a time +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + prepare_release: + permissions: + contents: write # update files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: prepare the release note + id: generate_release_notes + run: | + python3 ./.github/scripts/generate_release_notes.py ./RELEASE_NOTES.md" + + - name: extract next version from previous step's output + id: extract_version + run: | + echo "next_version=${{ steps.generate_release_notes.outputs.next_version }}" >> $GITHUB_ENV + + - name: update version files + run: | + python3 ./.github/scripts/update_version_files.py ${{ env.next_version }} + + - name: file a PR + id: file_pr + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "Prepare for the next release" + body: | + This PR prepares for the next release. + It updates the version files and adds new content to the RELEASE_NOTES.md. + commit-message: "prepare for the next release" + branch: prepare-release-${{ github.run_id }} + paths: | + RELEASE_NOTES.md + build-scripts/version.cmake + core/version.h