Skip to content

Commit f1db067

Browse files
authored
chore(release): process backports as part of creating RC (#3896)
Automatically process pending backports when `/create-rc` is called. If any backport fails to process (e.g. due to cherry-pick conflicts), the RC creation will abort. Additionally, support `--triggering-comment` option to react with a thumbs-down emoji on the triggering comment if the command fails. - Call `ProcessBackports` in `CreateRc` before proceeding with RC tagging. - Refactor `CreateRc.run` to handle exceptions and reactions. - Update GitHub Actions workflows to pass `comment_id` to `release_create_rc` workflow. - Add unit tests for the new integration and reaction behavior.
1 parent 1d99f70 commit f1db067

6 files changed

Lines changed: 193 additions & 4 deletions

File tree

.github/workflows/on_issue_comment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ jobs:
7373
uses: ./.github/workflows/release_create_rc.yaml
7474
with:
7575
issue: ${{ needs.parse_comment.outputs.issue_number }}
76+
comment_id: "${{ github.event.comment.id }}"
7677
secrets: inherit
7778

7879
call_prepare:

.github/workflows/release_create_rc.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ on:
77
description: 'The Release Tracking Issue Number (e.g., 142)'
88
required: true
99
type: string
10+
comment_id:
11+
description: 'The ID of the comment that triggered this run (optional)'
12+
required: false
13+
type: string
1014
workflow_call:
1115
inputs:
1216
issue:
1317
description: 'The Release Tracking Issue Number (e.g., 142)'
1418
required: true
1519
type: string
20+
comment_id:
21+
description: 'The ID of the comment that triggered this run (optional)'
22+
required: false
23+
type: string
1624

1725
permissions:
1826
contents: write
@@ -44,9 +52,14 @@ jobs:
4452
env:
4553
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4654
ISSUE: ${{ inputs.issue }}
55+
COMMENT_ID: ${{ inputs.comment_id }}
4756
run: |
57+
ARGS=()
58+
if [ -n "$COMMENT_ID" ]; then
59+
ARGS+=("--triggering-comment=$COMMENT_ID")
60+
fi
4861
bazel run //tools/private/release -- \
49-
create-rc --issue "$ISSUE" --remote origin
62+
create-rc --issue "$ISSUE" --remote origin "${ARGS[@]}"
5063
5164
call_release:
5265
needs: tag_rc

RELEASING.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ Release Tracking Issue and automated workflows triggered by comments or issue ed
3333
3. **Add Backports (if needed)**: If there are backports, add them following
3434
the [How to add backports](#how-to-add-backports) steps.
3535

36-
4. **Create an RC**: Comment `/create-rc` on the tracking issue. All pending
37-
backports must be successfully processed before creating the RC.
36+
4. **Create an RC**: Comment `/create-rc` on the tracking issue. This will
37+
automatically process pending backports before creating the RC. If any
38+
backport fails, the RC creation will abort.
3839

3940
5. **Iterate**: Repeat steps 3 and 4 until backports and RCs are no longer
4041
needed.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Changed `/create-rc` command to automatically process pending backports before
2+
creating the RC, and react with a thumbs-down emoji on failure if triggered by
3+
a comment.

tests/tools/private/release/create_rc_test.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,130 @@ def test_create_rc_auto_add_task(self):
253253
call2_args[1],
254254
)
255255

256+
@patch("tools.private.release.create_rc.ProcessBackports")
257+
def test_create_rc_calls_process_backports(self, mock_pb_class):
258+
# Arrange
259+
mock_pb = mock_pb_class.return_value
260+
mock_pb.run.return_value = 0
261+
262+
args = MagicMock(issue=123, remote="my-remote")
263+
self.mock_gh.get_issue_title.return_value = "Release 2.0.0"
264+
self.mock_gh.get_issue_body.return_value = """
265+
## Checklist
266+
- [x] Prepare Release | status=done pr=#122 commit=abcdef12
267+
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
268+
- [ ] Tag RC0 | status=pending
269+
"""
270+
self.mock_git.get_remote_tags.return_value = []
271+
self.mock_git.get_commit_sha.return_value = "1234567890"
272+
273+
# Act
274+
result = CreateRc(args, self.mock_git, self.mock_gh).run()
275+
276+
# Assert
277+
self.assertEqual(result, 0)
278+
mock_pb_class.assert_called_once()
279+
called_args = mock_pb_class.call_args[0][0]
280+
self.assertEqual(called_args.issue, 123)
281+
self.assertEqual(called_args.remote, "my-remote")
282+
self.assertFalse(called_args.dry_run)
283+
self.assertIsNone(called_args.add)
284+
self.assertIsNone(called_args.triggering_comment)
285+
mock_pb.run.assert_called_once()
286+
287+
@patch("tools.private.release.create_rc.ProcessBackports")
288+
def test_create_rc_aborts_on_process_backports_failure(self, mock_pb_class):
289+
# Arrange
290+
mock_pb = mock_pb_class.return_value
291+
mock_pb.run.return_value = 1
292+
293+
args = MagicMock(issue=123, remote="my-remote")
294+
295+
# Act
296+
result = CreateRc(args, self.mock_git, self.mock_gh).run()
297+
298+
# Assert
299+
self.assertEqual(result, 1)
300+
mock_pb_class.assert_called_once()
301+
mock_pb.run.assert_called_once()
302+
self.mock_gh.get_issue_body.assert_not_called()
303+
self.mock_git.tag.assert_not_called()
304+
305+
@patch("tools.private.release.create_rc.ProcessBackports")
306+
def test_create_rc_failure_reacts_to_comment(self, mock_pb_class):
307+
# Arrange
308+
mock_pb = mock_pb_class.return_value
309+
mock_pb.run.return_value = 1 # Simulate failure
310+
311+
args = MagicMock(issue=123, remote="my-remote", triggering_comment=456)
312+
313+
# Act
314+
result = CreateRc(args, self.mock_git, self.mock_gh).run()
315+
316+
# Assert
317+
self.assertEqual(result, 1)
318+
self.mock_gh.add_comment_reaction.assert_called_once_with(456, "-1")
319+
320+
@patch("tools.private.release.create_rc.ProcessBackports")
321+
def test_create_rc_failure_no_comment_no_reaction(self, mock_pb_class):
322+
# Arrange
323+
mock_pb = mock_pb_class.return_value
324+
mock_pb.run.return_value = 1 # Simulate failure
325+
326+
args = MagicMock(issue=123, remote="my-remote", triggering_comment=None)
327+
328+
# Act
329+
result = CreateRc(args, self.mock_git, self.mock_gh).run()
330+
331+
# Assert
332+
self.assertEqual(result, 1)
333+
self.mock_gh.add_comment_reaction.assert_not_called()
334+
335+
@patch("tools.private.release.create_rc.ProcessBackports")
336+
def test_create_rc_success_with_comment_no_reaction(self, mock_pb_class):
337+
# Arrange
338+
mock_pb = mock_pb_class.return_value
339+
mock_pb.run.return_value = 0
340+
341+
args = MagicMock(issue=123, remote="my-remote", triggering_comment=456)
342+
self.mock_gh.get_issue_title.return_value = "Release 2.0.0"
343+
self.mock_gh.get_issue_body.return_value = """
344+
## Checklist
345+
- [x] Prepare Release | status=done pr=#122 commit=abcdef12
346+
- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12
347+
- [ ] Tag RC0 | status=pending
348+
"""
349+
self.mock_git.get_remote_tags.return_value = []
350+
self.mock_git.get_commit_sha.return_value = "1234567890"
351+
352+
# Act
353+
result = CreateRc(args, self.mock_git, self.mock_gh).run()
354+
355+
# Assert
356+
self.assertEqual(result, 0)
357+
self.mock_gh.add_comment_reaction.assert_not_called()
358+
359+
@patch("tools.private.release.create_rc.ProcessBackports")
360+
def test_create_rc_precondition_failure_reacts_to_comment(self, mock_pb_class):
361+
# Arrange
362+
mock_pb = mock_pb_class.return_value
363+
mock_pb.run.return_value = 0 # Backports succeed
364+
365+
args = MagicMock(issue=123, remote="my-remote", triggering_comment=456)
366+
self.mock_gh.get_issue_body.return_value = """
367+
## Checklist
368+
- [ ] Prepare Release | status=pending
369+
- [ ] Create Release branch | status=pending
370+
- [ ] Tag RC0 | status=pending
371+
"""
372+
373+
# Act
374+
result = CreateRc(args, self.mock_git, self.mock_gh).run()
375+
376+
# Assert
377+
self.assertEqual(result, 1)
378+
self.mock_gh.add_comment_reaction.assert_called_once_with(456, "-1")
379+
256380

257381
if __name__ == "__main__":
258382
unittest.main()

tools/private/release/create_rc.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Subcommand to tag and push the next release candidate."""
22

3-
from tools.private.release.gh import GitHub
3+
import traceback
4+
from argparse import Namespace
5+
6+
from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub
47
from tools.private.release.git import Git
8+
from tools.private.release.process_backports import ProcessBackports
59
from tools.private.release.release_issue import (
610
RELEASE_TITLE_RE,
711
add_rc_task_to_body,
@@ -26,6 +30,44 @@ def __init__(self, args, git: Git, gh: GitHub):
2630
def run(self) -> int:
2731
"""Executes the create-rc subcommand."""
2832
args = self.args
33+
exit_code = 0
34+
try:
35+
exit_code = self._run_internal()
36+
except Exception as e:
37+
print(f"Unexpected error: {e}")
38+
traceback.print_exc()
39+
exit_code = 1
40+
41+
if exit_code != 0 and args.triggering_comment:
42+
print(f"Reacting with thumbs-down to comment {args.triggering_comment}...")
43+
try:
44+
self.gh.add_comment_reaction(
45+
args.triggering_comment, GH_REACTION_THUMBS_DOWN
46+
)
47+
except Exception as e:
48+
print(f"Failed to add reaction to comment: {e}")
49+
50+
return exit_code
51+
52+
def _run_internal(self) -> int:
53+
"""Internal implementation of create-rc."""
54+
args = self.args
55+
56+
# Try to process pending backports first
57+
print("Processing pending backports before creating RC...")
58+
backport_args = Namespace(
59+
issue=args.issue,
60+
remote=args.remote,
61+
add=None,
62+
triggering_comment=None,
63+
dry_run=False,
64+
)
65+
pb = ProcessBackports(backport_args, self.git, self.gh)
66+
backports_exit_code = pb.run()
67+
if backports_exit_code != 0:
68+
print("Error: Processing backports failed. Aborting RC creation.")
69+
return backports_exit_code
70+
2971
body = self.gh.get_issue_body(args.issue)
3072
state = parse_checklist_state(body)
3173

@@ -153,6 +195,11 @@ def add_parser(cls, subparsers):
153195
required=True,
154196
help="The git remote to push the RC tag to (required).",
155197
)
198+
parser.add_argument(
199+
"--triggering-comment",
200+
type=int,
201+
help="The ID of the comment that triggered this run (optional).",
202+
)
156203
parser.set_defaults(command=cls.run_from_args)
157204

158205
@classmethod

0 commit comments

Comments
 (0)