diff --git a/evergreen/evergreen.yml b/evergreen/evergreen.yml index 4a7f30c7c4b..7b7c5d25110 100644 --- a/evergreen/evergreen.yml +++ b/evergreen/evergreen.yml @@ -300,6 +300,17 @@ functions: content_type: text/markdown display_name: ssdlc_compliance_report.md + generate-release-notes: + - command: shell.exec + params: + working_dir: "mongo-csharp-driver" + shell: "bash" + env: + GITHUB_APIKEY: ${github_apikey} + script: | + ${PREPARE_SHELL} + ./evergreen/generate-release-notes.sh ${PACKAGE_VERSION} ${triggered_by_git_tag} + bootstrap-mongohoused: - command: shell.exec params: @@ -1581,6 +1592,10 @@ tasks: - func: download-and-promote-augmented-sbom-to-s3-bucket - func: generate-ssdlc-report + - name: generate-release-notes + commands: + - func: generate-release-notes + - name: validate-apidocs commands: - func: install-dotnet @@ -2525,7 +2540,6 @@ buildvariants: depends_on: - name: build-packages variant: ".build-packages" - ## add dependency onto packages smoke test once it implemented - matrix_name: test-SK matrix_spec: @@ -2549,7 +2563,6 @@ buildvariants: depends_on: - name: build-packages variant: ".build-packages" - ## add dependency onto packages smoke test once it implemented - matrix_name: push-packages-myget matrix_spec: @@ -2561,7 +2574,18 @@ buildvariants: depends_on: - name: build-packages variant: ".build-packages" - ## add dependency onto packages smoke test once it implemented + +- matrix_name: generate-release-notes + matrix_spec: + os: "ubuntu-2004" + display_name: "Generate release notes" + tags: ["release-tag"] + tasks: + - name: generate-release-notes + git_tag_only: true + depends_on: + - name: push-packages-nuget + variant: ".push-packages" - matrix_name: ssdlc-reports matrix_spec: diff --git a/evergreen/generate-release-notes.sh b/evergreen/generate-release-notes.sh new file mode 100644 index 00000000000..53fafdea865 --- /dev/null +++ b/evergreen/generate-release-notes.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -o errexit # Exit the script with error if any of the commands fail + +version=$1 +version_tag=$2 +previous_commit_sha=$(git rev-list ${version_tag} --skip=1 --max-count=1) +previous_tag=$(git describe ${previous_commit_sha} --tags --abbrev=0) + +if [[ "$version" == *.0 ]]; then + template_file="./evergreen/release-notes.yml" +else + template_file="./evergreen/patch-notes.yml" +fi + +python ./evergreen/release-notes.py ${version} mongodb/mongo-csharp-driver ${version_tag} ${previous_tag} ${template_file} diff --git a/evergreen/patch-notes.yml b/evergreen/patch-notes.yml new file mode 100644 index 00000000000..e62301b79a3 --- /dev/null +++ b/evergreen/patch-notes.yml @@ -0,0 +1,16 @@ +title: ".NET Driver Version ${version} Release Notes" + +autoformat: + - match: '(CSHARP-\d+)' + replace: '[\1](https://jira.mongodb.org/browse/\1)' + +ignore: + labels: + - chore + +sections: + - title: 'This is a patch release that contains fixes and stability improvements:' + labels: "*" + + - The full list of issues resolved in this release is available at [CSHARP JIRA project](https://jira.mongodb.org/issues/?jql=project%20%3D%20CSHARP%20AND%20fixVersion%20%3D%20${version}%20ORDER%20BY%20key%20ASC). + - Documentation on the .NET driver can be found [here](https://www.mongodb.com/docs/drivers/csharp/${docs_version}/). diff --git a/evergreen/release-notes.py b/evergreen/release-notes.py new file mode 100644 index 00000000000..5d5d5367a10 --- /dev/null +++ b/evergreen/release-notes.py @@ -0,0 +1,187 @@ +import argparse +import yaml +import requests +import re +import os + +parser = argparse.ArgumentParser() +parser.add_argument('version') +parser.add_argument('repo') +parser.add_argument('version_tag') +parser.add_argument('previous_tag') +parser.add_argument('template_file') + +options = parser.parse_args() + +options.docs_version = options.version_tag[:options.version_tag.rfind('.')] +options.github_api_base_url = 'https://api.github.com/repos/' +options.github_api_key = os.environ.get("GITHUB_APIKEY") +options.github_headers = { + "Authorization": "Bearer {api_key}".format(api_key=options.github_api_key), + "X-GitHub-Api-Version": "2022-11-28", + "Accept": "application/vnd.github+json" +} + +print("Preparing release notes for: tag {version_tag}, previous tag {previous_tag}".format(version_tag = options.version_tag, previous_tag = options.previous_tag)) + +def load_config(opts): + print("Loading template...") + with open(opts.template_file, 'r') as stream: + try: + opts.template = yaml.safe_load(stream) + for section in opts.template["sections"]: + if type(section) is dict: + section["items"] = [] + except yaml.YAMLError as e: + print('Cannot load template file:', e) + + +def mapPullRequest(pullRequest, opts): + title = pullRequest["title"] + for regex in opts.template["autoformat"]: + title = re.sub(regex["match"], regex["replace"], title) + + return { + "title": title, + "labels": list(map(lambda l: l["name"], pullRequest["labels"])) + } + + +def is_in_section(pullrequest, section): + if section is None: + return False + if type(section) is str: + return False + if "exclude-labels" in section: + for lbl in section["exclude-labels"]: + if lbl in pullrequest["labels"]: + return False + + if "labels" in section: + if section["labels"] == "*": + return True + for lbl in section["labels"]: + if lbl in pullrequest["labels"]: + return True + return False + + return True + + +def load_pull_requests(opts): + print("Loading changeset...") + page = 0 + total_pages = 1 + page_size = 100 + commits_url = "{github_api_base_url}{repo}/compare/{previous_tag}...{version_tag}".format( + github_api_base_url=opts.github_api_base_url, + repo=opts.repo, + previous_tag=opts.previous_tag, + version_tag=opts.version_tag) + ignore_section = opts.template["ignore"] + + while total_pages > page: + response = requests.get(commits_url, params={'per_page': page_size, 'page': page}, headers=opts.github_headers) + response.raise_for_status() + commits = response.json() + total_pages = commits["total_commits"] / page_size + + for commit in commits["commits"]: + pullrequests_url = "{github_api_base_url}{repo}/commits/{commit_sha}/pulls".format( + github_api_base_url=opts.github_api_base_url, + repo=opts.repo, + commit_sha=commit["sha"]) + pullrequests = requests.get(pullrequests_url, headers=opts.github_headers).json() + for pullrequest in pullrequests: + mapped = mapPullRequest(pullrequest, opts) + if is_in_section(mapped, ignore_section): + break + + for section in opts.template["sections"]: + if is_in_section(mapped, section): + if mapped in section["items"]: + break # PR was already added to the section + section["items"].append(mapped) + break # adding PR to the section, skip evaluating next sections + else: + opts.template["unclassified"].append(mapped) + page = page + 1 + + +def get_field(source, path): + elements = path.split('.') + for elem in elements: + source = getattr(source, elem) + return source + + +def apply_template(template, parameters): + return re.sub(r'\$\{([\w.]+)}', lambda m: get_field(parameters, m.group(1)), template) + + +def process_section(section): + if type(section) is str: + return apply_template(section, options) + if len(section["items"]) == 0: + return "" + + content = "" + title = section.get("title", "") + if title != "": + content = apply_template(title, options) + '\n' + + for pullrequest in section["items"]: + content = content + '\n - ' + pullrequest["title"] + + return content + + +def publish_release_notes(opts, title, content): + print("Publishing release notes...") + url = '{github_api_base_url}{repo}/releases/tags/{tag}'.format(github_api_base_url=opts.github_api_base_url, repo=opts.repo, tag=opts.version_tag) + response = requests.get(url, headers=opts.github_headers) + response.raise_for_status() + if response.status_code != 404: + raise SystemExit("Release with the tag already exists") + + post_data = { + "tag_name": opts.version_tag, + "name": title, + "body": content, + "draft": True, + "generate_release_notes": False, + "make_latest": "false" + } + response = requests.post(url, json=post_data, headers=opts.github_headers) + response.raise_for_status() + + +load_config(options) +options.template["unclassified"] = [] +load_pull_requests(options) + +print("Processing title...") +release_title = apply_template(options.template["title"], options) +print("Title: {title}".format(title=release_title)) + +print("Processing content...") +release_content = "" +for section in options.template["sections"]: + section_content = process_section(section) + if section_content != "": + release_content += "\n\n" + section_content + +if len(options.template["unclassified"]) > 0: + release_content += "\n\n================================" + release_content += "\n\n!!!UNCLASSIFIED PULL REQUESTS!!!" + for pr in options.template["unclassified"]: + release_content += "\n" + pr["title"] + release_content += "\n\n================================" + +print("----------") +print(release_content) +print("----------") + +publish_release_notes(options, release_title, release_content) + +print("Done.") diff --git a/evergreen/release-notes.yml b/evergreen/release-notes.yml new file mode 100644 index 00000000000..b8dfb0158a3 --- /dev/null +++ b/evergreen/release-notes.yml @@ -0,0 +1,28 @@ +title: ".NET Driver Version ${version} Release Notes" + +autoformat: + - match: '(CSHARP-\d+)' + replace: '[\1](https://jira.mongodb.org/browse/\1)' + +ignore: + labels: + - chore + +sections: + - This is the general availability release for the ${version} version of the driver. + + - title: "### The main new features in ${version} include:" + labels: + - feature + - title: "### Improvements:" + labels: + - improvement + - title: "### Fixes:" + labels: + - bug + - title: "### Maintenance:" + labels: + - maintenance + + - The full list of issues resolved in this release is available at [CSHARP JIRA project](https://jira.mongodb.org/issues/?jql=project%20%3D%20CSHARP%20AND%20fixVersion%20%3D%20${version}%20ORDER%20BY%20key%20ASC). + - Documentation on the .NET driver can be found [here](https://www.mongodb.com/docs/drivers/csharp/${docs_version}/).