From 06b7b038ad9319501149678460d5eee90ad3b061 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 3 Jul 2026 15:33:37 +0000 Subject: [PATCH 1/3] Modify create-rc to process backports and support triggering comment - Call ProcessBackports in CreateRc before creating RC. - Add --triggering-comment option to create-rc to react with thumbs-down on failure. - Pass triggering comment from GHA workflows. - Update tests and documentation. --- .github/workflows/on_issue_comment.yaml | 1 + .github/workflows/release_create_rc.yaml | 15 ++- RELEASING.md | 5 +- tests/tools/private/release/create_rc_test.py | 124 ++++++++++++++++++ tools/private/release/create_rc.py | 47 ++++++- 5 files changed, 188 insertions(+), 4 deletions(-) diff --git a/.github/workflows/on_issue_comment.yaml b/.github/workflows/on_issue_comment.yaml index 02af5272e6..14248d0d7a 100644 --- a/.github/workflows/on_issue_comment.yaml +++ b/.github/workflows/on_issue_comment.yaml @@ -73,6 +73,7 @@ jobs: uses: ./.github/workflows/release_create_rc.yaml with: issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" secrets: inherit call_prepare: diff --git a/.github/workflows/release_create_rc.yaml b/.github/workflows/release_create_rc.yaml index 2c48dd8476..a96a04a0ec 100644 --- a/.github/workflows/release_create_rc.yaml +++ b/.github/workflows/release_create_rc.yaml @@ -7,12 +7,20 @@ on: description: 'The Release Tracking Issue Number (e.g., 142)' required: true type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string workflow_call: inputs: issue: description: 'The Release Tracking Issue Number (e.g., 142)' required: true type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string permissions: contents: write @@ -44,9 +52,14 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE: ${{ inputs.issue }} + COMMENT_ID: ${{ inputs.comment_id }} run: | + ARGS=() + if [ -n "$COMMENT_ID" ]; then + ARGS+=("--triggering-comment=$COMMENT_ID") + fi bazel run //tools/private/release -- \ - create-rc --issue "$ISSUE" --remote origin + create-rc --issue "$ISSUE" --remote origin "${ARGS[@]}" call_release: needs: tag_rc diff --git a/RELEASING.md b/RELEASING.md index 528450f611..7846cef883 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -33,8 +33,9 @@ Release Tracking Issue and automated workflows triggered by comments or issue ed 3. **Add Backports (if needed)**: If there are backports, add them following the [How to add backports](#how-to-add-backports) steps. -4. **Create an RC**: Comment `/create-rc` on the tracking issue. All pending - backports must be successfully processed before creating the RC. +4. **Create an RC**: Comment `/create-rc` on the tracking issue. This will + automatically process pending backports before creating the RC. If any + backport fails, the RC creation will abort. 5. **Iterate**: Repeat steps 3 and 4 until backports and RCs are no longer needed. diff --git a/tests/tools/private/release/create_rc_test.py b/tests/tools/private/release/create_rc_test.py index 0937dfbac6..06cdb77345 100644 --- a/tests/tools/private/release/create_rc_test.py +++ b/tests/tools/private/release/create_rc_test.py @@ -253,6 +253,130 @@ def test_create_rc_auto_add_task(self): call2_args[1], ) + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_calls_process_backports(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 + + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + mock_pb_class.assert_called_once() + called_args = mock_pb_class.call_args[0][0] + self.assertEqual(called_args.issue, 123) + self.assertEqual(called_args.remote, "my-remote") + self.assertFalse(called_args.dry_run) + self.assertIsNone(called_args.add) + self.assertIsNone(called_args.triggering_comment) + mock_pb.run.assert_called_once() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_aborts_on_process_backports_failure(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 + + args = MagicMock(issue=123, remote="my-remote") + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + mock_pb_class.assert_called_once() + mock_pb.run.assert_called_once() + self.mock_gh.get_issue_body.assert_not_called() + self.mock_git.tag.assert_not_called() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_failure_reacts_to_comment(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 # Simulate failure + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=456) + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.add_comment_reaction.assert_called_once_with(456, "-1") + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_failure_no_comment_no_reaction(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 # Simulate failure + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=None) + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.add_comment_reaction.assert_not_called() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_success_with_comment_no_reaction(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=456) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_gh.add_comment_reaction.assert_not_called() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_precondition_failure_reacts_to_comment(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 # Backports succeed + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=456) + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release | status=pending +- [ ] Create Release branch | status=pending +- [ ] Tag RC0 | status=pending +""" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.add_comment_reaction.assert_called_once_with(456, "-1") + if __name__ == "__main__": unittest.main() diff --git a/tools/private/release/create_rc.py b/tools/private/release/create_rc.py index abc91a8273..b5b77e762f 100644 --- a/tools/private/release/create_rc.py +++ b/tools/private/release/create_rc.py @@ -1,7 +1,10 @@ """Subcommand to tag and push the next release candidate.""" -from tools.private.release.gh import GitHub +from argparse import Namespace + +from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub from tools.private.release.git import Git +from tools.private.release.process_backports import ProcessBackports from tools.private.release.release_issue import ( RELEASE_TITLE_RE, add_rc_task_to_body, @@ -26,6 +29,43 @@ def __init__(self, args, git: Git, gh: GitHub): def run(self) -> int: """Executes the create-rc subcommand.""" args = self.args + exit_code = 0 + try: + exit_code = self._run_internal() + except Exception as e: + print(f"Unexpected error: {e}") + exit_code = 1 + + if exit_code != 0 and args.triggering_comment: + print(f"Reacting with thumbs-down to comment {args.triggering_comment}...") + try: + self.gh.add_comment_reaction( + args.triggering_comment, GH_REACTION_THUMBS_DOWN + ) + except Exception as e: + print(f"Failed to add reaction to comment: {e}") + + return exit_code + + def _run_internal(self) -> int: + """Internal implementation of create-rc.""" + args = self.args + + # Try to process pending backports first + print("Processing pending backports before creating RC...") + backport_args = Namespace( + issue=args.issue, + remote=args.remote, + add=None, + triggering_comment=None, + dry_run=False, + ) + pb = ProcessBackports(backport_args, self.git, self.gh) + backports_exit_code = pb.run() + if backports_exit_code != 0: + print("Error: Processing backports failed. Aborting RC creation.") + return backports_exit_code + body = self.gh.get_issue_body(args.issue) state = parse_checklist_state(body) @@ -153,6 +193,11 @@ def add_parser(cls, subparsers): required=True, help="The git remote to push the RC tag to (required).", ) + parser.add_argument( + "--triggering-comment", + type=int, + help="The ID of the comment that triggered this run (optional).", + ) parser.set_defaults(command=cls.run_from_args) @classmethod From 71ce2215f30193546f27179ab340ee2f17ebd6ed Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 3 Jul 2026 15:33:49 +0000 Subject: [PATCH 2/3] Add news entry for create-rc changes --- news/process-backports-before-rc.changed.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/process-backports-before-rc.changed.md diff --git a/news/process-backports-before-rc.changed.md b/news/process-backports-before-rc.changed.md new file mode 100644 index 0000000000..5591b700ce --- /dev/null +++ b/news/process-backports-before-rc.changed.md @@ -0,0 +1,3 @@ +Changed `/create-rc` command to automatically process pending backports before +creating the RC, and react with a thumbs-down emoji on failure if triggered by +a comment. From 7637beeb35554eb1f4427ee8cae7d2226d2f5001 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 3 Jul 2026 15:38:16 +0000 Subject: [PATCH 3/3] Print traceback on unexpected exception in create-rc --- tools/private/release/create_rc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/private/release/create_rc.py b/tools/private/release/create_rc.py index b5b77e762f..0916377925 100644 --- a/tools/private/release/create_rc.py +++ b/tools/private/release/create_rc.py @@ -1,5 +1,6 @@ """Subcommand to tag and push the next release candidate.""" +import traceback from argparse import Namespace from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub @@ -34,6 +35,7 @@ def run(self) -> int: exit_code = self._run_internal() except Exception as e: print(f"Unexpected error: {e}") + traceback.print_exc() exit_code = 1 if exit_code != 0 and args.triggering_comment: