From 6aa201392c8a500f96e4d90e8a867d5189c01f06 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sun, 24 Jul 2022 11:25:39 +0200 Subject: [PATCH 1/3] ENH: Add release notes functionality --- github_activity/cli.py | 7 +++++++ github_activity/github_activity.py | 22 ++++++++++++++++++++++ github_activity/graphql.py | 1 + 3 files changed, 30 insertions(+) diff --git a/github_activity/cli.py b/github_activity/cli.py index 473268d..122f9a9 100644 --- a/github_activity/cli.py +++ b/github_activity/cli.py @@ -81,6 +81,12 @@ action="store_true", help="Include a list of opened items in the markdown output", ) +parser.add_argument( + "--include-release-notes", + default=False, + action="store_true", + help="Include the `# release notes` block of each PR in the output markdown.", +) parser.add_argument( "--strip-brackets", default=False, @@ -165,6 +171,7 @@ def main(): tags=tags, include_issues=bool(args.include_issues), include_opened=bool(args.include_opened), + include_release_notes=bool(args.include_release_notes), strip_brackets=bool(args.strip_brackets), branch=args.branch, ) diff --git a/github_activity/github_activity.py b/github_activity/github_activity.py index fbe6c5b..5aece80 100644 --- a/github_activity/github_activity.py +++ b/github_activity/github_activity.py @@ -300,6 +300,7 @@ def generate_activity_md( tags=None, include_issues=False, include_opened=False, + include_release_notes=False, strip_brackets=False, heading_level=1, branch=None, @@ -338,6 +339,9 @@ def generate_activity_md( Include Issues in the markdown output. Default is False. include_opened : bool Include a list of opened items in the markdown output. Default is False. + include_release_notes : bool + Search PR descriptions for a `# Release Notes` block. If found, include + the contents of this block with the changelog output for the PR. strip_brackets : bool If True, strip any text between brackets at the beginning of the issue/PR title. E.g., [MRG], [DOC], etc. @@ -537,9 +541,27 @@ def generate_activity_md( for irow, irowdata in items["data"].iterrows(): author = irowdata["author"] ititle = irowdata["title"] + description = irowdata["body"] if strip_brackets and ititle.strip().startswith("[") and "]" in ititle: ititle = ititle.split("]", 1)[-1].strip() this_md = f"- {ititle} [#{irowdata['number']}]({irowdata['url']}) ([@{author}](https://github.com/{author}))" + + # Search the description for release notes and add them if they exist + if include_release_notes: + lines = description.split("\n") + headers = [ii.startswith("#") for ii in lines] + release_notes = [ii for ii in headers if "# release notes" in ii.lower()] + if release_notes: + # Find the line of the next header to know when to stop + release_notes_line = release_notes[0] + next_header_ix = headers.index(release_notes_line) + 1 + next_header = headers[next_header_ix] + release_notes_start = lines.index(release_notes_line) + release_notes_stop = lines.index(next_header) + + # Append the release notes to our output markdown + release_notes = "\n".join(lines[release_notes_start:release_notes_stop]) + this_md += f"\n {release_notes}" items["md"].append(this_md) # Get functional GitHub references: any git reference or master@{YY-mm-dd} diff --git a/github_activity/graphql.py b/github_activity/graphql.py index 00f7af6..05a4e1a 100644 --- a/github_activity/graphql.py +++ b/github_activity/graphql.py @@ -27,6 +27,7 @@ id title url + body createdAt updatedAt closedAt From 0d1b625f5e82a37593aac0e88d30505f8ba38456 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sat, 30 Jul 2022 23:30:28 +0200 Subject: [PATCH 2/3] Add release notes functionality --- docs/index.md | 18 ++++++++++++- github_activity/github_activity.py | 35 ++++++++++++++---------- tests/test_cli.py | 16 +++++++++++ tests/test_cli/test_release_notes.md | 40 ++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 tests/test_cli/test_release_notes.md diff --git a/docs/index.md b/docs/index.md index 71a6922..5178017 100644 --- a/docs/index.md +++ b/docs/index.md @@ -57,7 +57,7 @@ You can find the [resulting markdown here](sample_notebook_activity). For repositories that use multiple branches, it may be necessary to filter PRs by a branch name. This can be done using the `--branch` parameter in the CLI. Other git references can be used as well in place of a branch name. ``` -### Splitting PRs by tags and prefixes +### Split PRs by tags and prefixes Often you wish to split your PRs into multiple categories so that they are easier to scan and parse. You may also _only_ want to keep some PRs (e.g. features, or API @@ -80,6 +80,22 @@ You can choose to *remove* some types of PRs from your changelog by passing the left-most column above. ``` +## Pull release notes from PR descriptions + +You can optionally include snippets of release notes directly from the descriptions of Pull Requests. +These will be included just underneath the bullet point text for each PR in the markdown output. + +To do so, follow these steps: + +1. In your PR description, include a markdown header that begins with `# Release notes`. + The header can be of any level, and capitalization does not matter. + All subsequent text in the PR description will be treated as the release notes, until a header of equal or lesser level is encountered. +2. Use the `--include-release-notes` flag. For example: + + ``` + github-activity --include-release-notes + ``` + ## Use a GitHub API token `github-activity` uses the GitHub API to pull information about a repository's activity. diff --git a/github_activity/github_activity.py b/github_activity/github_activity.py index 5aece80..d7ef40b 100644 --- a/github_activity/github_activity.py +++ b/github_activity/github_activity.py @@ -5,11 +5,11 @@ import shlex import subprocess import sys -import urllib from pathlib import Path from subprocess import PIPE from subprocess import run from tempfile import TemporaryDirectory +from textwrap import indent import dateutil import numpy as np @@ -176,6 +176,7 @@ def generate_all_activity_md( tags=None, include_issues=False, include_opened=False, + include_release_notes=False, strip_brackets=False, branch=None, ): @@ -207,6 +208,8 @@ def generate_all_activity_md( Include Issues in the markdown output. Default is False. include_opened : bool Include a list of opened items in the markdown output. Default is False. + include_release_notes : bool + Include the release notes for any PRs with `# Release notes` in the description. strip_brackets : bool If True, strip any text between brackets at the beginning of the issue/PR title. E.g., [MRG], [DOC], etc. @@ -548,20 +551,24 @@ def generate_activity_md( # Search the description for release notes and add them if they exist if include_release_notes: - lines = description.split("\n") - headers = [ii.startswith("#") for ii in lines] - release_notes = [ii for ii in headers if "# release notes" in ii.lower()] + release_notes = [] + in_release_notes = False + for ii in description.split("\n"): + if in_release_notes: + # If we detect a header of equal or lesser level, stop looking + if ii.startswith("#") and len(ii.split(" ")[0]) <= n_levels: + break + # Otherwise append the line and keep going + release_notes.append(ii) + elif "# release notes" in ii.lower(): + # When we detect a release notes header, + # start collecting lines underneath and define the header + # level so we know when to stop + in_release_notes = True + n_levels = len(ii.split(" ")[0]) + if release_notes: - # Find the line of the next header to know when to stop - release_notes_line = release_notes[0] - next_header_ix = headers.index(release_notes_line) + 1 - next_header = headers[next_header_ix] - release_notes_start = lines.index(release_notes_line) - release_notes_stop = lines.index(next_header) - - # Append the release notes to our output markdown - release_notes = "\n".join(lines[release_notes_start:release_notes_stop]) - this_md += f"\n {release_notes}" + this_md += "\n\n" + indent("\n".join(release_notes).strip(), " ") items["md"].append(this_md) # Get functional GitHub references: any git reference or master@{YY-mm-dd} diff --git a/tests/test_cli.py b/tests/test_cli.py index 6e191a0..15873c4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -80,3 +80,19 @@ def test_cli_all(tmpdir, file_regression): md = path_output.read_text() index = md.index("## v0.2.0") file_regression.check(md[index:], extension=".md") + + +def test_release_notes(tmpdir, file_regression): + """Release notes that are automatically pulled from PR descriptions.""" + path_tmp = Path(tmpdir) + path_output = path_tmp.joinpath("out.md") + url = "https://github.com/executablebooks/jupyter-book" + + # This release range covers PRs with + cmd = f"github-activity {url} -s v0.7.1 -u v0.7.3 --include-release-notes -o {path_output}" + + run(cmd.split(), check=True) + + md = path_output.read_text() + test_md = md[md.index("## New features added"): md.index("## Bugs fixed")] + file_regression.check(test_md, extension=".md") \ No newline at end of file diff --git a/tests/test_cli/test_release_notes.md b/tests/test_cli/test_release_notes.md new file mode 100644 index 0000000..c780531 --- /dev/null +++ b/tests/test_cli/test_release_notes.md @@ -0,0 +1,40 @@ +## New features added + +- ✨ NEW: Adding - chapter entries to _toc.yml [#817](https://github.com/executablebooks/jupyter-book/pull/817) ([@choldgraf](https://github.com/choldgraf)) + + Here's an example of the TOC structure: + + ```yaml + - file: intro + numbered: true + + - chapter: Get started + sections: + - file: start/overview + - file: start/build + + - chapter: Book pages and types + sections: + - file: content/markdown + - file: content/notebooks + + - chapter: Reference and test pages + sections: + - file: test_pages/test + sections: + - file: test_pages/layout_elements + - file: test_pages/equations + ``` + +## Enhancements made + +- 👌 IMPROVE: improving numbered sections [#826](https://github.com/executablebooks/jupyter-book/pull/826) ([@choldgraf](https://github.com/choldgraf)) +- checking for toc modification time [#772](https://github.com/executablebooks/jupyter-book/pull/772) ([@choldgraf](https://github.com/choldgraf)) + + This is an attempt at checking for the modification time of the table of contents file and forcing a re-build of all pages if this happens. We need to do this because otherwise people could change the toc, but Sphinx won't know to re-build some pages and these TOC changes won't be reflected. + + #### This heading will be included in release notes + + Here's a sub-heading +- first pass toc directive [#757](https://github.com/executablebooks/jupyter-book/pull/757) ([@choldgraf](https://github.com/choldgraf)) + From 12beddf75f345a40b40dc59884648b1927663dc7 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sat, 30 Jul 2022 23:58:17 +0200 Subject: [PATCH 3/3] Pre-commit format --- github_activity/github_activity.py | 5 ++++- tests/test_cli.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/github_activity/github_activity.py b/github_activity/github_activity.py index d7ef40b..b30fed4 100644 --- a/github_activity/github_activity.py +++ b/github_activity/github_activity.py @@ -553,6 +553,7 @@ def generate_activity_md( if include_release_notes: release_notes = [] in_release_notes = False + n_levels = None for ii in description.split("\n"): if in_release_notes: # If we detect a header of equal or lesser level, stop looking @@ -568,7 +569,9 @@ def generate_activity_md( n_levels = len(ii.split(" ")[0]) if release_notes: - this_md += "\n\n" + indent("\n".join(release_notes).strip(), " ") + this_md += "\n\n" + indent( + "\n".join(release_notes).strip(), " " + ) items["md"].append(this_md) # Get functional GitHub references: any git reference or master@{YY-mm-dd} diff --git a/tests/test_cli.py b/tests/test_cli.py index 15873c4..5c275e8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -88,11 +88,11 @@ def test_release_notes(tmpdir, file_regression): path_output = path_tmp.joinpath("out.md") url = "https://github.com/executablebooks/jupyter-book" - # This release range covers PRs with + # This release range covers PRs with cmd = f"github-activity {url} -s v0.7.1 -u v0.7.3 --include-release-notes -o {path_output}" run(cmd.split(), check=True) md = path_output.read_text() - test_md = md[md.index("## New features added"): md.index("## Bugs fixed")] - file_regression.check(test_md, extension=".md") \ No newline at end of file + test_md = md[md.index("## New features added") : md.index("## Bugs fixed")] + file_regression.check(test_md, extension=".md")