Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/on_issue_comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/release_create_rc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions news/process-backports-before-rc.changed.md
Original file line number Diff line number Diff line change
@@ -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.
124 changes: 124 additions & 0 deletions tests/tools/private/release/create_rc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
49 changes: 48 additions & 1 deletion tools/private/release/create_rc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""Subcommand to tag and push the next release candidate."""

from tools.private.release.gh import GitHub
import traceback
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,
Expand All @@ -26,6 +30,44 @@ 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}")
traceback.print_exc()
exit_code = 1
Comment thread
rickeylev marked this conversation as resolved.

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}")
Comment thread
rickeylev marked this conversation as resolved.

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)

Expand Down Expand Up @@ -153,6 +195,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
Expand Down