diff --git a/.github/ISSUE_TEMPLATE/patch_release_request.md b/.github/ISSUE_TEMPLATE/patch_release_request.md deleted file mode 100644 index 7b38ec40df..0000000000 --- a/.github/ISSUE_TEMPLATE/patch_release_request.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: "🏗️ Patch release or backport" -about: Request a patch release or backport of a fix to a release. -title: 'Patch release: MAJOR.MINOR.PATCH' -labels: 'type: process' ---- - - - -**What version of `rules_python` do you want to patch?** - - -**What pull requests do you want to backport?** - -Please provide a list of pull request numbers. - -- # -- # diff --git a/.github/ISSUE_TEMPLATE/release_tracking_template.md b/.github/ISSUE_TEMPLATE/release_tracking_template.md index efa78a9b63..33438c7c8b 100644 --- a/.github/ISSUE_TEMPLATE/release_tracking_template.md +++ b/.github/ISSUE_TEMPLATE/release_tracking_template.md @@ -1,10 +1,12 @@ --- -name: Release Tracking Issue -about: Checklist for tracking a new release of rules_python. +name: Release or Backport Tracking Issue +about: Checklist for tracking a new release, patch release, or backport of rules_python. title: 'Release ' labels: ['type: release'] --- # Release tasks + - [ ] Prepare Release | status=awaiting-preparation - [ ] Create Release branch - [ ] Tag RC0 @@ -12,11 +14,13 @@ labels: ['type: release'] ## Backports -To request a backport, comment `/backport` on the PR, comment `/backport ` -on this issue, or add it to the checklist below. See +To request or track a backport, comment `/backport` on the PR, comment +`/backport ` on this issue, or add it to the checklist below. See [RELEASING.md: How to add backports](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#how-to-add-backports) for details. +- [ ] #PR_NUMBER + --- To manually control the release flow, see the diff --git a/docs/devguide.md b/docs/devguide.md index 86a9d5b542..b886e81527 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -120,17 +120,25 @@ we prepare for releases. (creating-backport-prs)= ## Creating Backport PRs +:::{note} +Prefer filing a backport issue using the [release or backport tracking +template][backport-issue], because it triggers automation to perform all +necessary backporting steps. +::: + The steps to create a backport PR are: -1. Create an issue for the patch release; use the [patch release - template][patch-release-issue]. -2. Create a fork of `rules_python`. -3. Checkout the `release/X.Y` branch. -4. Use `git cherry-pick -x` to cherry pick the desired fixes. -5. Update the release's `CHANGELOG.md` file: - * Add a Major.Minor.Patch section if one doesn't exist - * Copy the changelog text from `main` to the release's changelog. -6. Send a PR with the backport's changes. +1. Create a fork of `rules_python`. +2. Checkout the `release/X.Y` branch. +3. Use `git cherry-pick -x` to cherry pick the desired fixes. +4. Update the release's `CHANGELOG.md` file using the release tool's + `process-news` command: + ```shell + bazel run //tools/private/release -- process-news + ``` + This merges the PR's news entries into `CHANGELOG.md`, deletes the news + files, and updates any `VERSION_NEXT_*` markers. +5. Send a PR with the backport's changes. * The title should be `backport: PR#N to Major.Minor` * The body must preserve the original PR's number, commit hash, description, and authorship. @@ -145,7 +153,12 @@ The steps to create a backport PR are: ``` * If the PR contains multiple backport commits, separate each's description with `-----`. -7. Send a PR to update the `main` branch's `CHANGELOG.md` to reflect the - changes done in the patched release. +6. Send a PR to update the `main` branch's `CHANGELOG.md` to reflect the + changes done in the patched release: + * Checkout the `main` branch. + * Run the `process-news` command as before: + ```shell + bazel run //tools/private/release -- process-news + ``` -[patch-release-issue]: https://github.com/bazelbuild/rules_python/issues/new?template=patch_release.md +[backport-issue]: https://github.com/bazel-contrib/rules_python/issues/new?template=release_tracking_template.md diff --git a/docs/support.md b/docs/support.md index 088466d1bb..194cd35d7e 100644 --- a/docs/support.md +++ b/docs/support.md @@ -24,11 +24,13 @@ Backports can be done to older releases, but only if newer releases also have the fix backported. For example, if the current release is 1.5, in order to patch 1.4, version 1.5 must be patched first. -Backports can be requested by [creating an issue with the patch release -template][patch-release-issue] or by sending a pull request performing the backport. -See the dev guide for [how to create a backport PR](creating-backport-prs). +Backports can be requested by [creating an issue with the release or backport +tracking template][backport-issue] or by sending a pull request performing the +backport. Creating an issue is preferred because it triggers automation to +perform all necessary backporting steps. See the dev guide for +[how to create a backport PR](creating-backport-prs). -[patch-release-issue]: https://github.com/bazelbuild/rules_python/issues/new?template=patch_release_request.md +[backport-issue]: https://github.com/bazel-contrib/rules_python/issues/new?template=release_tracking_template.md ## Supported Bazel Versions diff --git a/tests/tools/private/release/release_issue_test.py b/tests/tools/private/release/release_issue_test.py index b95601c541..13a543e3b1 100644 --- a/tests/tools/private/release/release_issue_test.py +++ b/tests/tools/private/release/release_issue_test.py @@ -3,6 +3,7 @@ add_sync_changelog_task_to_body, format_metadata_line, load_release_tracking_template, + parse_backports, parse_checklist_state, parse_metadata_line, ) @@ -97,6 +98,41 @@ def test_add_backports_to_body(): assert updated_body.strip() == expected_body.strip() +def test_parse_backports_ignores_placeholders(): + body = """ +## Backports +- [ ] #PR_NUMBER +- [ ] # +- [ ] #123 | status=done +""" + items = parse_backports(body) + assert len(items) == 1 + assert items[0].pr_ref == "#123" + + +def test_add_backports_to_body_removes_placeholders(): + body = """ +## Checklist +- [ ] Tag Final + +## Backports +- [ ] #PR_NUMBER +- [ ] # +- [ ] #123 | status=done +""" + items = [{"ref": "124"}] + updated_body = add_backports_to_body(body, items) + expected_body = """ +## Checklist +- [ ] Tag Final + +## Backports +- [ ] #123 | status=done +- [ ] #124 +""" + assert updated_body.strip() == expected_body.strip() + + def test_add_sync_changelog_task_to_body(): body = """ ## Checklist diff --git a/tools/private/release/release_issue.py b/tools/private/release/release_issue.py index 7a3716fab4..6bfa0f58e0 100644 --- a/tools/private/release/release_issue.py +++ b/tools/private/release/release_issue.py @@ -296,9 +296,13 @@ def parse_backports(body): for line in lines: parsed = parse_metadata_line(line) if parsed: + name = parsed["name"].strip() + # Ignore empty or placeholder checklist items (e.g. '#PR_NUMBER') + if not re.match(r"^#?\d+$", name): + continue items.append( BackportTask( - pr_ref=parsed["name"], + pr_ref=name, checked=parsed["checked"], status=parsed["metadata"].get("status", "pending"), rc=parsed["metadata"].get("rc"), @@ -326,6 +330,15 @@ def add_backports_to_body(body: str, items: list[dict[str, Any]]) -> str: section_content = match.group(2) + # Filter out empty or placeholder checklist items (e.g. "- [ ] #PR_NUMBER") + cleaned_lines = [] + for line in section_content.splitlines(): + parsed = parse_metadata_line(line) + if parsed and not re.match(r"^#?\d+$", parsed["name"].strip()): + continue + cleaned_lines.append(line) + section_content = "\n".join(cleaned_lines) + # Parse existing backports to avoid duplicates existing_items = parse_backports(body) existing_refs = {item.pr_ref for item in existing_items} @@ -348,7 +361,10 @@ def add_backports_to_body(body: str, items: list[dict[str, Any]]) -> str: ) if not new_lines: - return body + section_content_clean = section_content.rstrip("\n") + updated_section = section_content_clean + "\n\n" + start, end = match.span(2) + return body[:start] + updated_section + body[end:] # Append new lines to the section content. section_content_clean = section_content.rstrip("\n")