-
Notifications
You must be signed in to change notification settings - Fork 363
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Automate releases (
CHANGELOG.md
updating) (#3179)
* Add script to automate updating CHANGELOG.md for releasing * Add PRs label check workflow
- Loading branch information
Showing
3 changed files
with
207 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
name: Check release label | ||
|
||
on: | ||
pull_request: | ||
types: | ||
- synchronize | ||
- opened | ||
- reopened | ||
- labeled | ||
- unlabeled | ||
|
||
jobs: | ||
label_check: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v2 | ||
|
||
- name: Check labels | ||
run: | | ||
NUMBER_OF_LABELS=$(jq '.pull_request.labels | length' "$GITHUB_EVENT_PATH") | ||
if [ $NUMBER_OF_LABELS -eq 0 ]; then | ||
echo "PR has no labels. Please add at least one label of release type." | ||
exit 1 | ||
fi | ||
RELEASE_LABELS=("release::enhancements" "release::bug_fixes" "release::ci_docs") | ||
PR_LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH") | ||
NB_RELEASE_LABELS=0 | ||
for LABEL in $PR_LABELS; do | ||
if [[ " ${RELEASE_LABELS[@]} " =~ " ${LABEL} " ]]; then | ||
NB_RELEASE_LABELS=$((NB_RELEASE_LABELS+1)) | ||
fi | ||
done | ||
if [ $NB_RELEASE_LABELS -eq 0 ]; then | ||
echo "PR has no release labels. Please add a label of release type." | ||
exit 1 | ||
elif [ $NB_RELEASE_LABELS -gt 1 ]; then | ||
echo "PR has multiple release labels. Please remove all but one label." | ||
exit 1 | ||
fi |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# Script to update `CHANGELOG.md` in order to release any of the mamba packages | ||
|
||
# Steps: | ||
|
||
# 1. Run this script to update the root `CHANGELOG.md` file by giving the date of | ||
# the last release as input (cf. last date shown at the top of the file for reference) | ||
# or any other starting date that may be relevant for the release, | ||
# and the release version to be made. | ||
|
||
# 2. If you are happy with the changes, run `releaser.py` to update the versions and | ||
# corresponding nested `CHANGELOG.md` files. | ||
|
||
# N.B If the release is to be a pre-release (alpha,...), the versions should not be updated in `.py` and `.h` files. | ||
# Only the `CHANGELOG.md` files should be modified. | ||
# If otherwise, please revert the corresponding files if modified by the script. | ||
|
||
# 3. Follow the steps described in the `releaser.py` output. | ||
|
||
from datetime import date | ||
|
||
import json | ||
import re | ||
import subprocess | ||
|
||
|
||
def validate_date(date_str): | ||
try: | ||
date.fromisoformat(date_str) | ||
except ValueError: | ||
raise ValueError("Incorrect date format, should be YYYY-MM-DD") | ||
|
||
|
||
def subprocess_run(*args: str, **kwargs) -> str: | ||
"""Execute a command in a subprocess while properly capturing stderr in exceptions.""" | ||
try: | ||
p = subprocess.run( | ||
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, **kwargs | ||
) | ||
except subprocess.CalledProcessError as e: | ||
print(f"Command {args} failed with stderr: {e.stderr.decode()}") | ||
print(f"Command {args} failed with stdout: {e.stdout.decode()}") | ||
raise e | ||
return p.stdout | ||
|
||
|
||
def append_to_file(ctgr_name, prs, out_file): | ||
out_file.write("\n{}:\n\n".format(ctgr_name)) | ||
for pr in prs: | ||
# Author | ||
pr_author_cmd = "gh pr view {} --json author".format(pr) | ||
author_login = dict(json.loads(subprocess_run(*pr_author_cmd.split()).decode("utf-8")))[ | ||
"author" | ||
]["login"] | ||
# Title | ||
pr_title_cmd = "gh pr view {} --json title".format(pr) | ||
title = dict(json.loads(subprocess_run(*pr_title_cmd.split()).decode("utf-8")))["title"] | ||
# URL | ||
pr_url_cmd = "gh pr view {} --json url".format(pr) | ||
url = dict(json.loads(subprocess_run(*pr_url_cmd.split()).decode("utf-8")))["url"] | ||
# Files | ||
pr_files_cmd = "gh pr view {} --json files".format(pr) | ||
files = dict(json.loads(subprocess_run(*pr_files_cmd.split()).decode("utf-8")))["files"] | ||
ref_mamba_pkgs = ["libmamba/", "libmambapy/", "micromamba/"] | ||
concerned_pkgs = set() | ||
for f in files: | ||
for ref_pkg in ref_mamba_pkgs: | ||
if f["path"].startswith(ref_pkg): | ||
concerned_pkgs.add(ref_pkg) | ||
|
||
if (sorted(ref_mamba_pkgs) == sorted(concerned_pkgs)) or (len(concerned_pkgs) == 0): | ||
concerned_pkgs = ["all/"] | ||
# Write in file | ||
out_file.write( | ||
"- [{}] {} by @{} in {}\n".format( | ||
(", ".join([pkg[:-1] for pkg in concerned_pkgs])), title, author_login, url | ||
) | ||
) | ||
|
||
|
||
def main(): | ||
commits_starting_date = input( | ||
"Enter the starting date of commits to be included in the release in the format YYYY-MM-DD: " | ||
) | ||
validate_date(commits_starting_date) | ||
release_version = input("Enter the version to be released: ") | ||
|
||
# Get commits to include in the release | ||
log_cmd = "git log --since=" + commits_starting_date | ||
commits = subprocess_run(*log_cmd.split()).decode("utf-8") | ||
|
||
# Create the regular expression pattern | ||
opening_char = "(#" | ||
closing_char = ")" | ||
pattern = re.compile(re.escape(opening_char) + "(.*?)" + re.escape(closing_char)) | ||
|
||
# Get the PRs numbers | ||
prs_nbrs = re.findall(pattern, commits) | ||
|
||
# Make three lists to categorize PRs: "Enhancements", "Bug fixes" and "CI fixes and doc" | ||
enhancements_prs = [] # release::enhancements | ||
bug_fixes_prs = [] # release::bug_fixes | ||
ci_docs_prs = [] # release::ci_docs | ||
|
||
for pr in prs_nbrs: | ||
# Get labels | ||
pr_labels_cmd = "gh pr view {} --json labels".format(pr) | ||
labels = dict(json.loads(subprocess_run(*pr_labels_cmd.split()).decode("utf-8")))["labels"] | ||
nb_rls_lbls_types = 0 | ||
label = "" | ||
for lab in labels: | ||
if lab["name"].startswith("release::"): | ||
nb_rls_lbls_types = nb_rls_lbls_types + 1 | ||
label = lab["name"] | ||
|
||
# Only one release label should be set | ||
if nb_rls_lbls_types == 0: | ||
raise ValueError("No release label is set for PR #{}".format(pr)) | ||
elif nb_rls_lbls_types > 1: | ||
raise ValueError( | ||
"Only one release label should be set. PR #{} has {} labels.".format( | ||
pr, nb_rls_lbls_types | ||
) | ||
) | ||
|
||
# Dispatch PRs with their corresponding label | ||
if label == "release::enhancements": | ||
enhancements_prs.append(pr) | ||
elif label == "release::bug_fixes": | ||
bug_fixes_prs.append(pr) | ||
elif label == "release::ci_docs": | ||
ci_docs_prs.append(pr) | ||
else: | ||
raise ValueError("Unknown release label {} for PR #{}".format(label, pr)) | ||
|
||
with open("CHANGELOG.md", "r+") as changelog_file: | ||
# Make sure we're appending at the beginning of the file | ||
content_to_restore = changelog_file.read() | ||
changelog_file.seek(0) | ||
|
||
# Append new info | ||
# Release date and version | ||
changelog_file.write("{}\n".format(date.today().strftime("%Y.%m.%d"))) | ||
changelog_file.write("==========\n") | ||
changelog_file.write( | ||
"\nReleases: libmamba {0}, libmambapy {0}, micromamba {0}\n".format(release_version) | ||
) | ||
# PRs info | ||
append_to_file("Enhancements", enhancements_prs, changelog_file) | ||
append_to_file("Bug fixes", bug_fixes_prs, changelog_file) | ||
append_to_file("CI fixes and doc", ci_docs_prs, changelog_file) | ||
|
||
# Write back old content of CHANGELOG file | ||
changelog_file.write("\n" + content_to_restore) | ||
|
||
print( | ||
"'CHANGELOG.md' was successfully updated.\nPlease run 'releaser.py' if you agree with the changes applied." | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |