From ed3acdc0a6101a0b4e490386cad78d6bc8485526 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 29 Nov 2023 15:57:37 +0100 Subject: [PATCH 01/18] Add script to automatically update changelog --- dev/update-changelog.py | 308 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 309 insertions(+) create mode 100644 dev/update-changelog.py diff --git a/dev/update-changelog.py b/dev/update-changelog.py new file mode 100644 index 000000000000..704d29af2fa9 --- /dev/null +++ b/dev/update-changelog.py @@ -0,0 +1,308 @@ +import re +from sys import argv +from github import Github + +# Constants +REPO_NAME = "adap/flower" # Replace with your GitHub repo +CHANGELOG_FILE = "doc/source/ref-changelog.md" +CHANGELOG_SECTION_HEADER = "## Changelog entry" + + +def get_latest_tag(g): + repo = g.get_repo(REPO_NAME) + tags = repo.get_tags() + return tags[0] if tags.totalCount > 0 else None + + +def get_pull_requests_since_tag(g, tag): + repo = g.get_repo(REPO_NAME) + commits = set( + [commit.sha for commit in repo.compare(tag.commit.sha, "main").commits] + ) + prs = set() + for pr in repo.get_pulls( + state="closed", sort="created", direction="desc", base="main" + ): + if pr.merge_commit_sha in commits: + prs.add(pr) + if len(prs) == len(commits): + break + return prs + + +def format_pr_reference(title, number, url): + return f"- **{title}** ([#{number}]({url}))" + + +def extract_changelog_entry(pr): + # Extract the changelog entry + entry_match = re.search( + f"{CHANGELOG_SECTION_HEADER}(.+?)(?=##|$)", pr.body, re.DOTALL + ) + if not entry_match: + return None, "general" + + entry_text = entry_match.group(1).strip() + + # Remove markdown comments + entry_text = re.sub(r"", "", entry_text, flags=re.DOTALL).strip() + + if "" in entry_text: + return entry_text, "general" + if "" in entry_text: + return entry_text, "skip" + if "" in entry_text: + return entry_text, "baselines" + if "" in entry_text: + return entry_text, "examples" + if "" in entry_text: + return entry_text, "sdk" + if "" in entry_text: + return entry_text, "simulations" + + return entry_text, None + + +def update_changelog(prs): + with open(CHANGELOG_FILE, "r+") as file: + content = file.read() + unreleased_index = content.find("## Unreleased") + + if unreleased_index == -1: + print("Unreleased header not found in the changelog.") + return + + # Find the end of the Unreleased section + next_header_index = content.find("##", unreleased_index + 1) + if next_header_index == -1: + next_header_index = len(content) + + for pr in prs: + pr_entry_text, token = extract_changelog_entry(pr) + + # Check if the PR number is already in the changelog + if token == "skip" or f"#{pr.number}]" in content: + continue + + pr_reference = format_pr_reference(pr.title, pr.number, pr.html_url) + + if token == "general": + general_index = content.find( + "General improvements", unreleased_index, next_header_index + ) + if general_index != -1: + # Find the closing parenthesis before the newline + newline_index = content.find("\n", general_index) + closing_parenthesis_index = content.rfind( + ")", unreleased_index, newline_index + ) + updated_entry = f", [{pr.number}]({pr.html_url})" + content = ( + content[:closing_parenthesis_index] + + updated_entry + + content[closing_parenthesis_index:] + ) + else: + # Create a new 'General improvements' section + new_section = f"\n- **General improvements** ([#{pr.number}]({pr.html_url}))\n" + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + new_section + content[insert_index:] + ) + + # Update next_header_index if necessary + next_header_index = content.find("##", unreleased_index + 1) + if next_header_index == -1: + next_header_index = len(content) + + continue + + if token == "baselines": + general_index = content.find( + "General updates to Flower Baselines", + unreleased_index, + next_header_index, + ) + if general_index != -1: + # Find the closing parenthesis before the newline + newline_index = content.find("\n", general_index) + closing_parenthesis_index = content.rfind( + ")", unreleased_index, newline_index + ) + updated_entry = f", [{pr.number}]({pr.html_url})" + content = ( + content[:closing_parenthesis_index] + + updated_entry + + content[closing_parenthesis_index:] + ) + else: + # Create a new 'General improvements' section + new_section = f"\n- **General updates to Flower Baselines** ([#{pr.number}]({pr.html_url}))\n" + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + new_section + content[insert_index:] + ) + + # Update next_header_index if necessary + next_header_index = content.find("##", unreleased_index + 1) + if next_header_index == -1: + next_header_index = len(content) + + continue + + if token == "examples": + general_index = content.find( + "General updates to Flower Examples", + unreleased_index, + next_header_index, + ) + if general_index != -1: + # Find the closing parenthesis before the newline + newline_index = content.find("\n", general_index) + closing_parenthesis_index = content.rfind( + ")", unreleased_index, newline_index + ) + updated_entry = f", [{pr.number}]({pr.html_url})" + content = ( + content[:closing_parenthesis_index] + + updated_entry + + content[closing_parenthesis_index:] + ) + else: + # Create a new 'General improvements' section + new_section = f"\n- **General updates to Flower Examples** ([#{pr.number}]({pr.html_url}))\n" + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + new_section + content[insert_index:] + ) + + # Update next_header_index if necessary + next_header_index = content.find("##", unreleased_index + 1) + if next_header_index == -1: + next_header_index = len(content) + + continue + + if token == "sdk": + general_index = content.find( + "General updates to Flower SDKs", + unreleased_index, + next_header_index, + ) + if general_index != -1: + # Find the closing parenthesis before the newline + newline_index = content.find("\n", general_index) + closing_parenthesis_index = content.rfind( + ")", unreleased_index, newline_index + ) + updated_entry = f", [{pr.number}]({pr.html_url})" + content = ( + content[:closing_parenthesis_index] + + updated_entry + + content[closing_parenthesis_index:] + ) + else: + # Create a new 'General improvements' section + new_section = f"\n- **General updates to Flower SDKs** ([#{pr.number}]({pr.html_url}))\n" + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + new_section + content[insert_index:] + ) + + # Update next_header_index if necessary + next_header_index = content.find("##", unreleased_index + 1) + if next_header_index == -1: + next_header_index = len(content) + + continue + + if token == "simulations": + general_index = content.find( + "General updates to Flower Simulations", + unreleased_index, + next_header_index, + ) + if general_index != -1: + # Find the closing parenthesis before the newline + newline_index = content.find("\n", general_index) + closing_parenthesis_index = content.rfind( + ")", unreleased_index, newline_index + ) + updated_entry = f", [{pr.number}]({pr.html_url})" + content = ( + content[:closing_parenthesis_index] + + updated_entry + + content[closing_parenthesis_index:] + ) + else: + # Create a new 'General improvements' section + new_section = f"\n- **General updates to Flower Simulations** ([#{pr.number}]({pr.html_url}))\n" + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + new_section + content[insert_index:] + ) + + # Update next_header_index if necessary + next_header_index = content.find("##", unreleased_index + 1) + if next_header_index == -1: + next_header_index = len(content) + + continue + + # If entry text length is greater than 0, check for existing entry + if pr_entry_text: + existing_entry_start = content.find(pr_entry_text) + if existing_entry_start != -1: + # Find the end of the PR reference line + pr_ref_end = content.rfind("\n", 0, existing_entry_start) + updated_entry = ( + f"{content[pr_ref_end]}\n, [{pr.number}]({pr.html_url})" + ) + content = ( + content[:pr_ref_end] + + updated_entry + + content[existing_entry_start:] + ) + else: + # Insert new entry + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + + pr_reference + + "\n " + + pr_entry_text + + "\n" + + content[insert_index:] + ) + else: + # Append PR reference for PRs with no entry text + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + + "\n" + + pr_reference + + "\n" + + content[insert_index:] + ) + + file.seek(0) + file.write(content) + file.truncate() + + print("Changelog updated.") + + +def main(g): + latest_tag = get_latest_tag(g) + if not latest_tag: + print("No tags found in the repository.") + return + + prs = get_pull_requests_since_tag(g, latest_tag) + update_changelog(prs) + + +if __name__ == "__main__": + # Initialize GitHub Client + g = Github(argv[1]) + main(g) diff --git a/pyproject.toml b/pyproject.toml index 2349d554a409..adadba711787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ twine = "==4.0.2" pyroma = "==4.2" check-wheel-contents = "==0.4.0" GitPython = "==3.1.32" +PyGithub = "==2.1.1" licensecheck = "==2023.5.1" [tool.isort] From 814cc7a369efc360171759fa749f04bf0689152d Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 29 Nov 2023 16:00:22 +0100 Subject: [PATCH 02/18] Reformat --- dev/update-changelog.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dev/update-changelog.py b/dev/update-changelog.py index 704d29af2fa9..156083c5a0ac 100644 --- a/dev/update-changelog.py +++ b/dev/update-changelog.py @@ -2,8 +2,7 @@ from sys import argv from github import Github -# Constants -REPO_NAME = "adap/flower" # Replace with your GitHub repo +REPO_NAME = "adap/flower" CHANGELOG_FILE = "doc/source/ref-changelog.md" CHANGELOG_SECTION_HEADER = "## Changelog entry" @@ -136,7 +135,7 @@ def update_changelog(prs): + content[closing_parenthesis_index:] ) else: - # Create a new 'General improvements' section + # Create a new 'General Baselines' section new_section = f"\n- **General updates to Flower Baselines** ([#{pr.number}]({pr.html_url}))\n" insert_index = content.find("\n", unreleased_index) + 1 content = ( @@ -169,7 +168,7 @@ def update_changelog(prs): + content[closing_parenthesis_index:] ) else: - # Create a new 'General improvements' section + # Create a new 'General Examples' section new_section = f"\n- **General updates to Flower Examples** ([#{pr.number}]({pr.html_url}))\n" insert_index = content.find("\n", unreleased_index) + 1 content = ( @@ -202,7 +201,7 @@ def update_changelog(prs): + content[closing_parenthesis_index:] ) else: - # Create a new 'General improvements' section + # Create a new 'General SDKs' section new_section = f"\n- **General updates to Flower SDKs** ([#{pr.number}]({pr.html_url}))\n" insert_index = content.find("\n", unreleased_index) + 1 content = ( @@ -235,7 +234,7 @@ def update_changelog(prs): + content[closing_parenthesis_index:] ) else: - # Create a new 'General improvements' section + # Create a new 'General Simulations' section new_section = f"\n- **General updates to Flower Simulations** ([#{pr.number}]({pr.html_url}))\n" insert_index = content.find("\n", unreleased_index) + 1 content = ( @@ -303,6 +302,6 @@ def main(g): if __name__ == "__main__": - # Initialize GitHub Client + # Initialize GitHub Client with provided token (as argument) g = Github(argv[1]) main(g) From 4a77c4955d3ba813661aa8eca4d4d57d28994240 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 29 Nov 2023 16:12:07 +0100 Subject: [PATCH 03/18] Update default PR template --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8d73ed618919..53a7dfaf86b1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -41,6 +41,10 @@ Example: The variable `rnd` was renamed to `server_round` to improve readability - [ ] Make CI checks pass - [ ] Ping maintainers on [Slack](https://flower.dev/join-slack/) (channel `#contributions`) +## Changelog entry + + + ### Any other comments? ", "", entry_text, flags=re.DOTALL).strip() - - if "" in entry_text: - return entry_text, "general" - if "" in entry_text: - return entry_text, "skip" - if "" in entry_text: - return entry_text, "baselines" - if "" in entry_text: - return entry_text, "examples" - if "" in entry_text: - return entry_text, "sdk" - if "" in entry_text: - return entry_text, "simulations" - - return entry_text, None - - -def update_changelog(prs): - with open(CHANGELOG_FILE, "r+") as file: - content = file.read() - unreleased_index = content.find("## Unreleased") - - if unreleased_index == -1: - print("Unreleased header not found in the changelog.") - return - - # Find the end of the Unreleased section - next_header_index = content.find("##", unreleased_index + 1) - if next_header_index == -1: - next_header_index = len(content) - - for pr in prs: - pr_entry_text, token = extract_changelog_entry(pr) - - # Check if the PR number is already in the changelog - if token == "skip" or f"#{pr.number}]" in content: - continue - - pr_reference = format_pr_reference(pr.title, pr.number, pr.html_url) - - if token == "general": - general_index = content.find( - "General improvements", unreleased_index, next_header_index - ) - if general_index != -1: - # Find the closing parenthesis before the newline - newline_index = content.find("\n", general_index) - closing_parenthesis_index = content.rfind( - ")", unreleased_index, newline_index - ) - updated_entry = f", [{pr.number}]({pr.html_url})" - content = ( - content[:closing_parenthesis_index] - + updated_entry - + content[closing_parenthesis_index:] - ) - else: - # Create a new 'General improvements' section - new_section = f"\n- **General improvements** ([#{pr.number}]({pr.html_url}))\n" - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] + new_section + content[insert_index:] - ) - - # Update next_header_index if necessary - next_header_index = content.find("##", unreleased_index + 1) - if next_header_index == -1: - next_header_index = len(content) - - continue - - if token == "baselines": - general_index = content.find( - "General updates to Flower Baselines", - unreleased_index, - next_header_index, - ) - if general_index != -1: - # Find the closing parenthesis before the newline - newline_index = content.find("\n", general_index) - closing_parenthesis_index = content.rfind( - ")", unreleased_index, newline_index - ) - updated_entry = f", [{pr.number}]({pr.html_url})" - content = ( - content[:closing_parenthesis_index] - + updated_entry - + content[closing_parenthesis_index:] - ) - else: - # Create a new 'General Baselines' section - new_section = f"\n- **General updates to Flower Baselines** ([#{pr.number}]({pr.html_url}))\n" - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] + new_section + content[insert_index:] - ) - - # Update next_header_index if necessary - next_header_index = content.find("##", unreleased_index + 1) - if next_header_index == -1: - next_header_index = len(content) - - continue - - if token == "examples": - general_index = content.find( - "General updates to Flower Examples", - unreleased_index, - next_header_index, - ) - if general_index != -1: - # Find the closing parenthesis before the newline - newline_index = content.find("\n", general_index) - closing_parenthesis_index = content.rfind( - ")", unreleased_index, newline_index - ) - updated_entry = f", [{pr.number}]({pr.html_url})" - content = ( - content[:closing_parenthesis_index] - + updated_entry - + content[closing_parenthesis_index:] - ) - else: - # Create a new 'General Examples' section - new_section = f"\n- **General updates to Flower Examples** ([#{pr.number}]({pr.html_url}))\n" - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] + new_section + content[insert_index:] - ) - - # Update next_header_index if necessary - next_header_index = content.find("##", unreleased_index + 1) - if next_header_index == -1: - next_header_index = len(content) - - continue - - if token == "sdk": - general_index = content.find( - "General updates to Flower SDKs", - unreleased_index, - next_header_index, - ) - if general_index != -1: - # Find the closing parenthesis before the newline - newline_index = content.find("\n", general_index) - closing_parenthesis_index = content.rfind( - ")", unreleased_index, newline_index - ) - updated_entry = f", [{pr.number}]({pr.html_url})" - content = ( - content[:closing_parenthesis_index] - + updated_entry - + content[closing_parenthesis_index:] - ) - else: - # Create a new 'General SDKs' section - new_section = f"\n- **General updates to Flower SDKs** ([#{pr.number}]({pr.html_url}))\n" - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] + new_section + content[insert_index:] - ) - - # Update next_header_index if necessary - next_header_index = content.find("##", unreleased_index + 1) - if next_header_index == -1: - next_header_index = len(content) - - continue - - if token == "simulations": - general_index = content.find( - "General updates to Flower Simulations", - unreleased_index, - next_header_index, - ) - if general_index != -1: - # Find the closing parenthesis before the newline - newline_index = content.find("\n", general_index) - closing_parenthesis_index = content.rfind( - ")", unreleased_index, newline_index - ) - updated_entry = f", [{pr.number}]({pr.html_url})" - content = ( - content[:closing_parenthesis_index] - + updated_entry - + content[closing_parenthesis_index:] - ) - else: - # Create a new 'General Simulations' section - new_section = f"\n- **General updates to Flower Simulations** ([#{pr.number}]({pr.html_url}))\n" - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] + new_section + content[insert_index:] - ) - - # Update next_header_index if necessary - next_header_index = content.find("##", unreleased_index + 1) - if next_header_index == -1: - next_header_index = len(content) - - continue - - # If entry text length is greater than 0, check for existing entry - if pr_entry_text: - existing_entry_start = content.find(pr_entry_text) - if existing_entry_start != -1: - # Find the end of the PR reference line - pr_ref_end = content.rfind("\n", 0, existing_entry_start) - updated_entry = ( - f"{content[pr_ref_end]}\n, [{pr.number}]({pr.html_url})" - ) - content = ( - content[:pr_ref_end] - + updated_entry - + content[existing_entry_start:] - ) - else: - # Insert new entry - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] - + pr_reference - + "\n " - + pr_entry_text - + "\n" - + content[insert_index:] - ) - else: - # Append PR reference for PRs with no entry text - insert_index = content.find("\n", unreleased_index) + 1 - content = ( - content[:insert_index] - + "\n" - + pr_reference - + "\n" - + content[insert_index:] - ) - - file.seek(0) - file.write(content) - file.truncate() - - print("Changelog updated.") - - -def main(g): - latest_tag = get_latest_tag(g) - if not latest_tag: - print("No tags found in the repository.") - return - - prs = get_pull_requests_since_tag(g, latest_tag) - update_changelog(prs) - - -if __name__ == "__main__": - # Initialize GitHub Client with provided token (as argument) - g = Github(argv[1]) - main(g) diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py new file mode 100644 index 000000000000..e56abc71af7f --- /dev/null +++ b/src/py/flwr_tool/update_changelog.py @@ -0,0 +1,213 @@ +# mypy: ignore-errors +# Copyright 2023 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""This module is used to update the changelog.""" + + +import re +from sys import argv + +from github import Github + +REPO_NAME = "adap/flower" +CHANGELOG_FILE = "doc/source/ref-changelog.md" +CHANGELOG_SECTION_HEADER = "## Changelog entry" + + +def _get_latest_tag(gh_api): + """Retrieve the latest tag from the GitHub repository.""" + repo = gh_api.get_repo(REPO_NAME) + tags = repo.get_tags() + return tags[0] if tags.totalCount > 0 else None + + +def _get_pull_requests_since_tag(gh_api, tag): + """Get a list of pull requests merged into the main branch since a given tag.""" + repo = gh_api.get_repo(REPO_NAME) + commits = set( + [commit.sha for commit in repo.compare(tag.commit.sha, "main").commits] + ) + prs = set() + for pr in repo.get_pulls( + state="closed", sort="created", direction="desc", base="main" + ): + if pr.merge_commit_sha in commits: + prs.add(pr) + if len(prs) == len(commits): + break + return prs + + +def _format_pr_reference(title, number, url): + """Format a pull request reference as a markdown list item.""" + return f"- **{title}** ([#{number}]({url}))" + + +def _extract_changelog_entry(pr): + """Extract the changelog entry from a pull request's body.""" + entry_match = re.search( + f"{CHANGELOG_SECTION_HEADER}(.+?)(?=##|$)", pr.body, re.DOTALL + ) + if not entry_match: + return None, "general" + + entry_text = entry_match.group(1).strip() + + # Remove markdown comments + entry_text = re.sub(r"", "", entry_text, flags=re.DOTALL).strip() + + if "" in entry_text: + return entry_text, "general" + if "" in entry_text: + return entry_text, "skip" + if "" in entry_text: + return entry_text, "baselines" + if "" in entry_text: + return entry_text, "examples" + if "" in entry_text: + return entry_text, "sdk" + if "" in entry_text: + return entry_text, "simulations" + + return entry_text, None + + +def _update_changelog(prs): + """Update the changelog file with entries from provided pull requests.""" + with open(CHANGELOG_FILE, "r+") as file: + content = file.read() + unreleased_index = content.find("## Unreleased") + + if unreleased_index == -1: + print("Unreleased header not found in the changelog.") + return + + # Find the end of the Unreleased section + next_header_index = content.find("##", unreleased_index + 1) + next_header_index = ( + next_header_index if next_header_index != -1 else len(content) + ) + + for pr in prs: + pr_entry_text, category = _extract_changelog_entry(pr) + + # Skip if PR should be skipped or already in changelog + if category == "skip" or f"#{pr.number}]" in content: + continue + + pr_reference = _format_pr_reference(pr.title, pr.number, pr.html_url) + + # Process based on category + if category in ["general", "baselines", "examples", "sdk", "simulations"]: + entry_title = _get_category_title(category) + content = _update_entry( + content, + entry_title, + pr, + unreleased_index, + next_header_index, + ) + + elif pr_entry_text: + content = _insert_new_entry( + content, pr, pr_reference, pr_entry_text, unreleased_index + ) + + else: + content = _insert_entry_no_desc(content, pr_reference, unreleased_index) + + next_header_index = content.find("##", unreleased_index + 1) + next_header_index = ( + next_header_index if next_header_index != -1 else len(content) + ) + + # Finalize content update + file.seek(0) + file.write(content) + file.truncate() + + print("Changelog updated.") + + +def _get_category_title(category): + """Get the title of a changelog section based on its category.""" + headers = { + "general": "General improvements", + "baselines": "General updates to Flower Baselines", + "examples": "General updates to Flower Examples", + "sdk": "General updates to Flower SDKs", + "simulations": "General updates to Flower Simulations", + } + return headers.get(category, "") + + +def _update_entry(content, category_title, pr, unreleased_index, next_header_index): + """Update a specific section in the changelog content.""" + section_index = content.find(category_title, unreleased_index, next_header_index) + if section_index != -1: + newline_index = content.find("\n", section_index) + closing_parenthesis_index = content.rfind(")", unreleased_index, newline_index) + updated_entry = f", [{pr.number}]({pr.html_url})" + content = ( + content[:closing_parenthesis_index] + + updated_entry + + content[closing_parenthesis_index:] + ) + else: + new_section = f"\n- **{category_title}** ([#{pr.number}]({pr.html_url}))\n" + insert_index = content.find("\n", unreleased_index) + 1 + content = content[:insert_index] + new_section + content[insert_index:] + return content + + +def _insert_new_entry(content, pr, pr_reference, pr_entry_text, unreleased_index): + """Insert a new entry into the changelog.""" + existing_entry_start = content.find(pr_entry_text) + if existing_entry_start != -1: + pr_ref_end = content.rfind("\n", 0, existing_entry_start) + updated_entry = f"{content[pr_ref_end]}\n, [{pr.number}]({pr.html_url})" + content = content[:pr_ref_end] + updated_entry + content[existing_entry_start:] + else: + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + + pr_reference + + "\n " + + pr_entry_text + + "\n" + + content[insert_index:] + ) + return content + + +def _insert_entry_no_desc(content, pr_reference, unreleased_index): + """Insert a changelog entry for a pull request with no specific description.""" + insert_index = content.find("\n", unreleased_index) + 1 + content = ( + content[:insert_index] + "\n" + pr_reference + "\n" + content[insert_index:] + ) + return content + + +if __name__ == "__main__": + # Initialize GitHub Client with provided token (as argument) + gh_api = Github(argv[1]) + latest_tag = _get_latest_tag(gh_api) + if not latest_tag: + print("No tags found in the repository.") + exit(1) + + prs = _get_pull_requests_since_tag(gh_api, latest_tag) + _update_changelog(prs) From 60c26ebed4644e6cd88e8031ec25b6d227c1c37d Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 29 Nov 2023 19:41:29 +0100 Subject: [PATCH 07/18] Fix linting errors --- src/py/flwr_tool/update_changelog.py | 53 +++++++++++++++++----------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py index e56abc71af7f..353a2a9dc6e4 100644 --- a/src/py/flwr_tool/update_changelog.py +++ b/src/py/flwr_tool/update_changelog.py @@ -36,15 +36,13 @@ def _get_latest_tag(gh_api): def _get_pull_requests_since_tag(gh_api, tag): """Get a list of pull requests merged into the main branch since a given tag.""" repo = gh_api.get_repo(REPO_NAME) - commits = set( - [commit.sha for commit in repo.compare(tag.commit.sha, "main").commits] - ) + commits = {commit.sha for commit in repo.compare(tag.commit.sha, "main").commits} prs = set() - for pr in repo.get_pulls( + for pr_info in repo.get_pulls( state="closed", sort="created", direction="desc", base="main" ): - if pr.merge_commit_sha in commits: - prs.add(pr) + if pr_info.merge_commit_sha in commits: + prs.add(pr_info) if len(prs) == len(commits): break return prs @@ -55,10 +53,10 @@ def _format_pr_reference(title, number, url): return f"- **{title}** ([#{number}]({url}))" -def _extract_changelog_entry(pr): +def _extract_changelog_entry(pr_info): """Extract the changelog entry from a pull request's body.""" entry_match = re.search( - f"{CHANGELOG_SECTION_HEADER}(.+?)(?=##|$)", pr.body, re.DOTALL + f"{CHANGELOG_SECTION_HEADER}(.+?)(?=##|$)", pr_info.body, re.DOTALL ) if not entry_match: return None, "general" @@ -100,14 +98,16 @@ def _update_changelog(prs): next_header_index if next_header_index != -1 else len(content) ) - for pr in prs: - pr_entry_text, category = _extract_changelog_entry(pr) + for pr_info in prs: + pr_entry_text, category = _extract_changelog_entry(pr_info) # Skip if PR should be skipped or already in changelog - if category == "skip" or f"#{pr.number}]" in content: + if category == "skip" or f"#{pr_info.number}]" in content: continue - pr_reference = _format_pr_reference(pr.title, pr.number, pr.html_url) + pr_reference = _format_pr_reference( + pr_info.title, pr_info.number, pr_info.html_url + ) # Process based on category if category in ["general", "baselines", "examples", "sdk", "simulations"]: @@ -115,14 +115,14 @@ def _update_changelog(prs): content = _update_entry( content, entry_title, - pr, + pr_info, unreleased_index, next_header_index, ) elif pr_entry_text: content = _insert_new_entry( - content, pr, pr_reference, pr_entry_text, unreleased_index + content, pr_info, pr_reference, pr_entry_text, unreleased_index ) else: @@ -153,31 +153,37 @@ def _get_category_title(category): return headers.get(category, "") -def _update_entry(content, category_title, pr, unreleased_index, next_header_index): +def _update_entry( + content, category_title, pr_info, unreleased_index, next_header_index +): """Update a specific section in the changelog content.""" section_index = content.find(category_title, unreleased_index, next_header_index) if section_index != -1: newline_index = content.find("\n", section_index) closing_parenthesis_index = content.rfind(")", unreleased_index, newline_index) - updated_entry = f", [{pr.number}]({pr.html_url})" + updated_entry = f", [{pr_info.number}]({pr_info.html_url})" content = ( content[:closing_parenthesis_index] + updated_entry + content[closing_parenthesis_index:] ) else: - new_section = f"\n- **{category_title}** ([#{pr.number}]({pr.html_url}))\n" + new_section = ( + f"\n- **{category_title}** ([#{pr_info.number}]({pr_info.html_url}))\n" + ) insert_index = content.find("\n", unreleased_index) + 1 content = content[:insert_index] + new_section + content[insert_index:] return content -def _insert_new_entry(content, pr, pr_reference, pr_entry_text, unreleased_index): +def _insert_new_entry(content, pr_info, pr_reference, pr_entry_text, unreleased_index): """Insert a new entry into the changelog.""" existing_entry_start = content.find(pr_entry_text) if existing_entry_start != -1: pr_ref_end = content.rfind("\n", 0, existing_entry_start) - updated_entry = f"{content[pr_ref_end]}\n, [{pr.number}]({pr.html_url})" + updated_entry = ( + f"{content[pr_ref_end]}\n, [{pr_info.number}]({pr_info.html_url})" + ) content = content[:pr_ref_end] + updated_entry + content[existing_entry_start:] else: insert_index = content.find("\n", unreleased_index) + 1 @@ -201,13 +207,18 @@ def _insert_entry_no_desc(content, pr_reference, unreleased_index): return content -if __name__ == "__main__": +def main(): + """Updaet changelog using the descriptions of PRs since the latest tag.""" # Initialize GitHub Client with provided token (as argument) gh_api = Github(argv[1]) latest_tag = _get_latest_tag(gh_api) if not latest_tag: print("No tags found in the repository.") - exit(1) + return prs = _get_pull_requests_since_tag(gh_api, latest_tag) _update_changelog(prs) + + +if __name__ == "__main__": + main() From 36ddfb425db6b4aff470796d0242410a98ff287b Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 29 Nov 2023 20:40:38 +0100 Subject: [PATCH 08/18] Fix errors --- src/py/flwr_tool/update_changelog.py | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py index 353a2a9dc6e4..f326e52027a2 100644 --- a/src/py/flwr_tool/update_changelog.py +++ b/src/py/flwr_tool/update_changelog.py @@ -66,25 +66,26 @@ def _extract_changelog_entry(pr_info): # Remove markdown comments entry_text = re.sub(r"", "", entry_text, flags=re.DOTALL).strip() - if "" in entry_text: - return entry_text, "general" - if "" in entry_text: - return entry_text, "skip" - if "" in entry_text: - return entry_text, "baselines" - if "" in entry_text: - return entry_text, "examples" - if "" in entry_text: - return entry_text, "sdk" - if "" in entry_text: - return entry_text, "simulations" - - return entry_text, None + token_markers = { + "general": "", + "skip": "", + "baselines": "", + "examples": "", + "sdk": "", + "simulations": "", + } + + # Find the token based on the presence of its marker in entry_text + token = next( + (token for token, marker in token_markers.items() if marker in entry_text), None + ) + + return entry_text, token def _update_changelog(prs): """Update the changelog file with entries from provided pull requests.""" - with open(CHANGELOG_FILE, "r+") as file: + with open(CHANGELOG_FILE, "r+", encoding="utf-8") as file: content = file.read() unreleased_index = content.find("## Unreleased") From 5adf83cbf24d15518968d8c218b999a47e52ddab Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 15:19:29 +0100 Subject: [PATCH 09/18] Update src/py/flwr_tool/update_changelog.py Co-authored-by: Robert Steiner --- src/py/flwr_tool/update_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py index f326e52027a2..31feb1057620 100644 --- a/src/py/flwr_tool/update_changelog.py +++ b/src/py/flwr_tool/update_changelog.py @@ -209,7 +209,7 @@ def _insert_entry_no_desc(content, pr_reference, unreleased_index): def main(): - """Updaet changelog using the descriptions of PRs since the latest tag.""" + """Update changelog using the descriptions of PRs since the latest tag.""" # Initialize GitHub Client with provided token (as argument) gh_api = Github(argv[1]) latest_tag = _get_latest_tag(gh_api) From 4e1bdd9fc7fb64d0b48e1a8ad6f02c6258654025 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 15:28:04 +0100 Subject: [PATCH 10/18] Add description to PR template --- .github/PULL_REQUEST_TEMPLATE.md | 19 +++++++++++++++++-- src/py/flwr_tool/update_changelog.py | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 53a7dfaf86b1..479f88c1bbd5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -37,11 +37,26 @@ Example: The variable `rnd` was renamed to `server_round` to improve readability - [ ] Implement proposed change - [ ] Write tests - [ ] Update [documentation](https://flower.dev/docs/writing-documentation.html) -- [ ] Update [changelog](https://github.com/adap/flower/blob/main/doc/source/changelog.rst) +- [ ] Update the changelog entry below - [ ] Make CI checks pass - [ ] Ping maintainers on [Slack](https://flower.dev/join-slack/) (channel `#contributions`) -## Changelog entry + + +### Changelog entry diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py index 31feb1057620..47792ce5d5f5 100644 --- a/src/py/flwr_tool/update_changelog.py +++ b/src/py/flwr_tool/update_changelog.py @@ -23,7 +23,7 @@ REPO_NAME = "adap/flower" CHANGELOG_FILE = "doc/source/ref-changelog.md" -CHANGELOG_SECTION_HEADER = "## Changelog entry" +CHANGELOG_SECTION_HEADER = "### Changelog entry" def _get_latest_tag(gh_api): From a344bd5cc179c1f732e20c1b639a9cd96707b62a Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 15:35:02 +0100 Subject: [PATCH 11/18] Use walrus operator --- src/py/flwr_tool/update_changelog.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py index 47792ce5d5f5..8b0f9c866f58 100644 --- a/src/py/flwr_tool/update_changelog.py +++ b/src/py/flwr_tool/update_changelog.py @@ -158,8 +158,11 @@ def _update_entry( content, category_title, pr_info, unreleased_index, next_header_index ): """Update a specific section in the changelog content.""" - section_index = content.find(category_title, unreleased_index, next_header_index) - if section_index != -1: + if ( + section_index := content.find( + category_title, unreleased_index, next_header_index + ) + ) != -1: newline_index = content.find("\n", section_index) closing_parenthesis_index = content.rfind(")", unreleased_index, newline_index) updated_entry = f", [{pr_info.number}]({pr_info.html_url})" @@ -179,8 +182,7 @@ def _update_entry( def _insert_new_entry(content, pr_info, pr_reference, pr_entry_text, unreleased_index): """Insert a new entry into the changelog.""" - existing_entry_start = content.find(pr_entry_text) - if existing_entry_start != -1: + if (existing_entry_start := content.find(pr_entry_text)) != -1: pr_ref_end = content.rfind("\n", 0, existing_entry_start) updated_entry = ( f"{content[pr_ref_end]}\n, [{pr_info.number}]({pr_info.html_url})" From c6edc9ef635a3991dbd9d4f94bf78c75d42606ad Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 15:48:20 +0100 Subject: [PATCH 12/18] Handle empty PR description --- src/py/flwr_tool/update_changelog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/py/flwr_tool/update_changelog.py b/src/py/flwr_tool/update_changelog.py index 8b0f9c866f58..bbd5c7f3dc7b 100644 --- a/src/py/flwr_tool/update_changelog.py +++ b/src/py/flwr_tool/update_changelog.py @@ -55,6 +55,9 @@ def _format_pr_reference(title, number, url): def _extract_changelog_entry(pr_info): """Extract the changelog entry from a pull request's body.""" + if not pr_info.body: + return None, "general" + entry_match = re.search( f"{CHANGELOG_SECTION_HEADER}(.+?)(?=##|$)", pr_info.body, re.DOTALL ) From 7b9e87a61992924e9cf8bfd1442b21e6a2b88d93 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 16:43:05 +0100 Subject: [PATCH 13/18] Added some info in the docs --- doc/source/contributor-tutorial-contribute-on-github.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/contributor-tutorial-contribute-on-github.rst b/doc/source/contributor-tutorial-contribute-on-github.rst index 9aeb8229b412..4422ce692e93 100644 --- a/doc/source/contributor-tutorial-contribute-on-github.rst +++ b/doc/source/contributor-tutorial-contribute-on-github.rst @@ -195,6 +195,10 @@ Creating and merging a pull request (PR) The input box in the middle is there for you to describe what your PR does and to link it to existing issues. We have placed comments (that won't be rendered once the PR is opened) to guide you through the process. + It is important to follow the instructions described in comments. For instance, in order to not break how our changelog system works, + you should read the information above the `Changelog entry` section carefully. Essentially, in this section, + you should put the description of your changes that will be added to the changelog alongside your PR title. + At the bottom you will find the button to open the PR. This will notify reviewers that a new PR has been opened and that they should look over it to merge or to request changes. From a76ea22f68157f1d112070b88633ed1234cfc851 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 19:06:59 +0100 Subject: [PATCH 14/18] Add appendix --- ...tributor-tutorial-contribute-on-github.rst | 98 ++++++++++++++++--- 1 file changed, 85 insertions(+), 13 deletions(-) diff --git a/doc/source/contributor-tutorial-contribute-on-github.rst b/doc/source/contributor-tutorial-contribute-on-github.rst index 4422ce692e93..a94680ab526b 100644 --- a/doc/source/contributor-tutorial-contribute-on-github.rst +++ b/doc/source/contributor-tutorial-contribute-on-github.rst @@ -46,7 +46,7 @@ Setting up the repository $ git clone - This will create a `flower/` (or the name of your fork if you renamed it) folder in the current working directory. + This will create a ``flower/`` (or the name of your fork if you renamed it) folder in the current working directory. 4. **Add origin** You can then go into the repository folder: @@ -180,9 +180,9 @@ Creating and merging a pull request (PR) .. image:: _static/compare_and_pr.png - Otherwise you can always find this option in the `Branches` page. + Otherwise you can always find this option in the ``Branches`` page. - Once you click the `Compare & pull request` button, you should see something similar to this: + Once you click the ``Compare & pull request`` button, you should see something similar to this: .. image:: _static/creating_pr.png @@ -196,8 +196,8 @@ Creating and merging a pull request (PR) We have placed comments (that won't be rendered once the PR is opened) to guide you through the process. It is important to follow the instructions described in comments. For instance, in order to not break how our changelog system works, - you should read the information above the `Changelog entry` section carefully. Essentially, in this section, - you should put the description of your changes that will be added to the changelog alongside your PR title. + you should read the information above the ``Changelog entry`` section carefully. + You can also checkout some examples and details in the `appendix <#changelog-entry>`_ of this page. At the bottom you will find the button to open the PR. This will notify reviewers that a new PR has been opened and that they should look over it to merge or to request changes. @@ -276,8 +276,8 @@ Solution This is a tiny change, but it’ll allow us to test your end-to-end setup. After cloning and setting up the Flower repo, here’s what you should do: -- Find the source file in `doc/source` -- Make the change in the `.rst` file (beware, the dashes under the title should be the same length as the title itself) +- Find the source file in ``doc/source`` +- Make the change in the ``.rst`` file (beware, the dashes under the title should be the same length as the title itself) - Build the docs and check the result: ``_ Rename file @@ -288,18 +288,18 @@ If we just change the file, then we break all existing links to it - it is **ver Here’s how to change the file name: -- Change the file name to `save-progress.rst` -- Add a redirect rule to `doc/source/conf.py` +- Change the file name to ``save-progress.rst`` +- Add a redirect rule to ``doc/source/conf.py`` -This will cause a redirect from `saving-progress.html` to `save-progress.html`, old links will continue to work. +This will cause a redirect from ``saving-progress.html`` to ``save-progress.html``, old links will continue to work. Apply changes in the index file ::::::::::::::::::::::::::::::: -For the lateral navigation bar to work properly, it is very important to update the `index.rst` file as well. +For the lateral navigation bar to work properly, it is very important to update the ``index.rst`` file as well. This is where we define the whole arborescence of the navbar. -- Find and modify the file name in `index.rst` +- Find and modify the file name in ``index.rst`` Open PR ::::::: @@ -335,7 +335,7 @@ Here are a few positive examples which provide helpful information without repea * Update docs banner to mention Flower Summit 2023 * Remove unnecessary XGBoost dependency * Remove redundant attributes in strategies subclassing FedAvg -* Add CI job to deploy the staging system when the `main` branch changes +* Add CI job to deploy the staging system when the ``main`` branch changes * Add new amazing library which will be used to improve the simulation engine @@ -345,3 +345,75 @@ Next steps Once you have made your first PR, and want to contribute more, be sure to check out the following : - `Good first contributions `_, where you should particularly look into the :code:`baselines` contributions. + + +Appendix +-------- + +.. _changelog-entry: + +Changelog entry +::::::::::::::: + +When opening a new PR, inside its description, there should be a ``Changelog entry`` header. + +As per the comment above this section:: + + Inside the following 'Changelog entry' section, + you should put the description of your changes that will be added to the changelog alongside your PR title. + + If the section is completely empty (without any token), + the changelog will just contain the title of the PR for the changelog entry, without any description. + If the 'Changelog entry' section is removed entirely, + it will categorize the PR as "General improvement" and add it to the changelog accordingly. + If the section contains some text other than tokens, it will use it to add a description to the change. + If the section contains one of the following tokens it will ignore any other text and put the PR under the corresponding section of the changelog: + + is for classifying a PR as a general improvement. + is to not add the PR to the changelog + is to add a general baselines change to the PR + is to add a general examples change to the PR + is to add a general sdk change to the PR + is to add a general simulations change to the PR + + Note that only one token should be used. + +Its content must have a specific format. We will break down what each possibility does: + +- If the ``### Changelog entry`` section is removed, the following text will be added to the changelog:: + + - **General improvements** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +- If the ``### Changelog entry`` section contains nothing but exists, the following text will be added to the changelog:: + + - **PR TITLE** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +- If the ``### Changelog entry`` section contains a description (and no token), the following text will be added to the changelog:: + + - **PR TITLE** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + + DESCRIPTION FROM THE CHANGELOG ENTRY + +- If the ``### Changelog entry`` section contains ````, nothing will change in the changelog. + +- If the ``### Changelog entry`` section contains ````, the following text will be added to the changelog:: + + - **General improvements** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +- If the ``### Changelog entry`` section contains ````, the following text will be added to the changelog:: + + - **General updates to Flower Baselines** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +- If the ``### Changelog entry`` section contains ````, the following text will be added to the changelog:: + + - **General updates to Flower Examples** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +- If the ``### Changelog entry`` section contains ````, the following text will be added to the changelog:: + + - **General updates to Flower SDKs** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +- If the ``### Changelog entry`` section contains ````, the following text will be added to the changelog:: + + - **General updates to Flower Simulations** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) + +Note that only one token must be provided, otherwise, only the first action (in the order listed above), will be performed. \ No newline at end of file From 52baf66afa662183d138c8068bd580dfa6a74496 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 19:29:56 +0100 Subject: [PATCH 15/18] Corret syntax --- doc/source/contributor-tutorial-contribute-on-github.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/source/contributor-tutorial-contribute-on-github.rst b/doc/source/contributor-tutorial-contribute-on-github.rst index a94680ab526b..03fc582b86b6 100644 --- a/doc/source/contributor-tutorial-contribute-on-github.rst +++ b/doc/source/contributor-tutorial-contribute-on-github.rst @@ -197,7 +197,7 @@ Creating and merging a pull request (PR) It is important to follow the instructions described in comments. For instance, in order to not break how our changelog system works, you should read the information above the ``Changelog entry`` section carefully. - You can also checkout some examples and details in the `appendix <#changelog-entry>`_ of this page. + You can also checkout some examples and details in the appendix :ref:`below `. At the bottom you will find the button to open the PR. This will notify reviewers that a new PR has been opened and that they should look over it to merge or to request changes. @@ -350,10 +350,8 @@ Once you have made your first PR, and want to contribute more, be sure to check Appendix -------- -.. _changelog-entry: - Changelog entry -::::::::::::::: +*************** When opening a new PR, inside its description, there should be a ``Changelog entry`` header. @@ -416,4 +414,4 @@ Its content must have a specific format. We will break down what each possibilit - **General updates to Flower Simulations** ([#PR_NUMBER](https://github.com/adap/flower/pull/PR_NUMBER)) -Note that only one token must be provided, otherwise, only the first action (in the order listed above), will be performed. \ No newline at end of file +Note that only one token must be provided, otherwise, only the first action (in the order listed above), will be performed. From 808103ae5ddadc607c7339d689e018d0c9f0336e Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 19:34:44 +0100 Subject: [PATCH 16/18] Fix link --- doc/source/contributor-tutorial-contribute-on-github.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributor-tutorial-contribute-on-github.rst b/doc/source/contributor-tutorial-contribute-on-github.rst index 03fc582b86b6..10e90b1dfa6f 100644 --- a/doc/source/contributor-tutorial-contribute-on-github.rst +++ b/doc/source/contributor-tutorial-contribute-on-github.rst @@ -197,7 +197,7 @@ Creating and merging a pull request (PR) It is important to follow the instructions described in comments. For instance, in order to not break how our changelog system works, you should read the information above the ``Changelog entry`` section carefully. - You can also checkout some examples and details in the appendix :ref:`below `. + You can also checkout some examples and details in the appendix :ref:`below`. At the bottom you will find the button to open the PR. This will notify reviewers that a new PR has been opened and that they should look over it to merge or to request changes. From 5f294aabaa04be4a77e8e09ad550251778302890 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 19:40:07 +0100 Subject: [PATCH 17/18] Test other link --- doc/source/contributor-tutorial-contribute-on-github.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/contributor-tutorial-contribute-on-github.rst b/doc/source/contributor-tutorial-contribute-on-github.rst index 10e90b1dfa6f..351c0e58360a 100644 --- a/doc/source/contributor-tutorial-contribute-on-github.rst +++ b/doc/source/contributor-tutorial-contribute-on-github.rst @@ -197,7 +197,7 @@ Creating and merging a pull request (PR) It is important to follow the instructions described in comments. For instance, in order to not break how our changelog system works, you should read the information above the ``Changelog entry`` section carefully. - You can also checkout some examples and details in the appendix :ref:`below`. + You can also checkout some examples and details in the appendix :ref:`below`. At the bottom you will find the button to open the PR. This will notify reviewers that a new PR has been opened and that they should look over it to merge or to request changes. @@ -350,6 +350,8 @@ Once you have made your first PR, and want to contribute more, be sure to check Appendix -------- +.. _below: + Changelog entry *************** From 4bfbb8b5abab8d01408fd49fa83464a71f2aa1cf Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 6 Dec 2023 19:51:02 +0100 Subject: [PATCH 18/18] Good reference --- doc/source/contributor-tutorial-contribute-on-github.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/contributor-tutorial-contribute-on-github.rst b/doc/source/contributor-tutorial-contribute-on-github.rst index 351c0e58360a..aca7b0e68968 100644 --- a/doc/source/contributor-tutorial-contribute-on-github.rst +++ b/doc/source/contributor-tutorial-contribute-on-github.rst @@ -197,7 +197,7 @@ Creating and merging a pull request (PR) It is important to follow the instructions described in comments. For instance, in order to not break how our changelog system works, you should read the information above the ``Changelog entry`` section carefully. - You can also checkout some examples and details in the appendix :ref:`below`. + You can also checkout some examples and details in the :ref:`changelogentry` appendix. At the bottom you will find the button to open the PR. This will notify reviewers that a new PR has been opened and that they should look over it to merge or to request changes. @@ -350,7 +350,7 @@ Once you have made your first PR, and want to contribute more, be sure to check Appendix -------- -.. _below: +.. _changelogentry: Changelog entry ***************