Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate releases (CHANGELOG.md updating) #3179

Merged
merged 2 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/label_check.yml
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
4 changes: 3 additions & 1 deletion releaser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# script to release any of the mamba packages
# Script to release any of the mamba packages
# Please refer to `update_changelog.py` for more info about the release process

import copy
import datetime
import re
Expand Down
161 changes: 161 additions & 0 deletions update_changelog.py
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()
Loading