diff --git a/.github/scripts/auto-backport.py b/.github/scripts/auto-backport.py index 41ef71efde..6628cadaaa 100755 --- a/.github/scripts/auto-backport.py +++ b/.github/scripts/auto-backport.py @@ -29,10 +29,11 @@ def parse_args(): parser.add_argument('--commits', default=None, type=str, help='Range of promoted commits.') parser.add_argument('--pull-request', type=int, help='Pull request number to be backported') parser.add_argument('--head-commit', type=str, required=is_pull_request(), help='The HEAD of target branch after the pull request specified by --pull-request is merged') + parser.add_argument('--github-event', type=str, help='Get GitHub event type') return parser.parse_args() -def create_pull_request(repo, new_branch_name, base_branch_name, pr, backport_pr_title, commits, is_draft=False): +def create_pull_request(repo, new_branch_name, base_branch_name, pr, backport_pr_title, commits, is_draft, is_collaborator): pr_body = f'{pr.body}\n\n' for commit in commits: pr_body += f'- (cherry picked from commit {commit})\n\n' @@ -46,7 +47,8 @@ def create_pull_request(repo, new_branch_name, base_branch_name, pr, backport_pr draft=is_draft ) logging.info(f"Pull request created: {backport_pr.html_url}") - backport_pr.add_to_assignees(pr.user) + if is_collaborator: + backport_pr.add_to_assignees(pr.user) if is_draft: backport_pr.add_to_labels("conflicts") pr_comment = f"@{pr.user.login} - This PR was marked as draft because it has conflicts\n" @@ -92,18 +94,7 @@ def get_pr_commits(repo, pr, stable_branch, start_commit=None): return commits -def create_pr_comment_and_remove_label(pr, comment_body): - labels = pr.get_labels() - pattern = re.compile(r"backport/\d+\.\d+$") - for label in labels: - if pattern.match(label.name): - print(f"Removing label: {label.name}") - comment_body += f'- {label.name}\n' - pr.remove_from_labels(label) - pr.create_issue_comment(comment_body) - - -def backport(repo, pr, version, commits, backport_base_branch): +def backport(repo, pr, version, commits, backport_base_branch, is_collaborator): new_branch_name = f'backport/{pr.number}/to-{version}' backport_pr_title = f'[Backport {version}] {pr.title}' repo_url = f'https://scylladbbot:{github_token}@github.com/{repo.full_name}.git' @@ -123,7 +114,7 @@ def backport(repo, pr, version, commits, backport_base_branch): repo_local.git.cherry_pick('--continue') repo_local.git.push(fork_repo, new_branch_name, force=True) create_pull_request(repo, new_branch_name, backport_base_branch, pr, backport_pr_title, commits, - is_draft=is_draft) + is_draft, is_collaborator) except GitCommandError as e: logging.warning(f"GitCommandError: {e}") @@ -140,10 +131,6 @@ def with_github_keyword_prefix(repo, pr): break if not match: print(f'No valid close reference for {pr.number}') - comment = f':warning: @{pr.user.login} PR body does not contain a Fixes reference to an issue ' - comment += ' and can not be backported\n\n' - comment += 'The following labels were removed:\n' - create_pr_comment_and_remove_label(pr, comment) return False else: return True @@ -168,6 +155,7 @@ def main(): scylladbbot_repo = g.get_repo(fork_repo_name) closed_prs = [] start_commit = None + is_collaborator = True if args.commits: start_commit, end_commit = args.commits.split('..') @@ -192,21 +180,33 @@ def main(): if not backport_labels: print(f'no backport label: {pr.number}') continue - if args.commits and not with_github_keyword_prefix(repo, pr): + if not with_github_keyword_prefix(repo, pr) and args.github_event != 'unlabeled': + comment = f''':warning: @{pr.user.login} PR body or PR commits do not contain a Fixes reference to an issue and can not be backported + please update PR body with a valid ref to an issue. Then remove `scylladbbot/backport_error` label to re-trigger the backport process + ''' + pr.create_issue_comment(comment) + pr.add_to_labels("scylladbbot/backport_error") continue if not repo.private and not scylladbbot_repo.has_in_collaborators(pr.user.login): logging.info(f"Sending an invite to {pr.user.login} to become a collaborator to {scylladbbot_repo.full_name} ") scylladbbot_repo.add_to_collaborators(pr.user.login) - comment = f':warning: @{pr.user.login} you have been added as collaborator to scylladbbot fork ' - comment += f'Please check your inbox and approve the invitation, once it is done, please add the backport labels again\n' - create_pr_comment_and_remove_label(pr, comment) - continue + comment = f''':warning: @{pr.user.login} you have been added as collaborator to scylladbbot fork + Please check your inbox and approve the invitation, otherwise you will not be able to edit PR branch when needed + ''' + # When a pull request is pending for backport but its author is not yet a collaborator of "scylladbbot", + # we attach a "scylladbbot/backport_error" label to the PR. + # This prevents the workflow from proceeding with the backport process + # until the author has been granted proper permissions + # the author should remove the label manually to re-trigger the backport workflow. + pr.add_to_labels("scylladbbot/backport_error") + pr.create_issue_comment(comment) + is_collaborator = False commits = get_pr_commits(repo, pr, stable_branch, start_commit) logging.info(f"Found PR #{pr.number} with commit {commits} and the following labels: {backport_labels}") for backport_label in backport_labels: version = backport_label.replace('backport/', '') backport_base_branch = backport_label.replace('backport/', backport_branch) - backport(repo, pr, version, commits, backport_base_branch) + backport(repo, pr, version, commits, backport_base_branch, is_collaborator) if __name__ == "__main__": diff --git a/.github/workflows/add-label-when-promoted.yaml b/.github/workflows/add-label-when-promoted.yaml index 7afc1de3aa..7b7d3c1107 100644 --- a/.github/workflows/add-label-when-promoted.yaml +++ b/.github/workflows/add-label-when-promoted.yaml @@ -7,7 +7,7 @@ on: - branch-*.* - enterprise pull_request_target: - types: [labeled] + types: [labeled, unlabeled] branches: [master, next, enterprise] jobs: @@ -53,19 +53,28 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.AUTO_BACKPORT_TOKEN }} run: python .github/scripts/auto-backport.py --repo ${{ github.repository }} --base-branch ${{ github.ref }} --commits ${{ github.event.before }}..${{ github.sha }} - - name: Check if label starts with 'backport/' and contains digits + - name: Check if a valid backport label exists and no backport_error id: check_label run: | - label_name="${{ github.event.label.name }}" - if [[ "$label_name" =~ ^backport/[0-9]+\.[0-9]+$ ]]; then - echo "Label matches backport/X.X pattern." - echo "backport_label=true" >> $GITHUB_OUTPUT + labels_json='${{ toJson(github.event.pull_request.labels) }}' + echo "Checking labels: $(echo "$labels_json" | jq -r '.[].name')" + + # Check if a valid backport label exists + if echo "$labels_json" | jq -e 'any(.[] | .name; test("backport/[0-9]+\\.[0-9]+$"))' > /dev/null; then + # Ensure scylladbbot/backport_error is NOT present + if ! echo "$labels_json" | jq -e '.[] | select(.name == "scylladbbot/backport_error")' > /dev/null; then + echo "A matching backport label was found and no backport_error label exists." + echo "ready_for_backport=true" >> "$GITHUB_OUTPUT" + exit 0 + else + echo "The label 'scylladbbot/backport_error' is present, invalidating backport." + fi else - echo "Label does not match the required pattern." - echo "backport_label=false" >> $GITHUB_OUTPUT + echo "No matching backport label found." fi - - name: Run auto-backport.py when label was added - if: ${{ github.event_name == 'pull_request_target' && steps.check_label.outputs.backport_label == 'true' && github.event.pull_request.state == 'closed' }} + echo "ready_for_backport=false" >> "$GITHUB_OUTPUT" + - name: Run auto-backport.py when PR is closed + if: ${{ github.event_name == 'pull_request_target' && steps.check_label.outputs.ready_for_backport == 'true' && github.event.pull_request.state == 'closed' }} env: GITHUB_TOKEN: ${{ secrets.AUTO_BACKPORT_TOKEN }} - run: python .github/scripts/auto-backport.py --repo ${{ github.repository }} --base-branch ${{ github.ref }} --pull-request ${{ github.event.pull_request.number }} --head-commit ${{ github.event.pull_request.base.sha }} + run: python .github/scripts/auto-backport.py --repo ${{ github.repository }} --base-branch ${{ github.ref }} --pull-request ${{ github.event.pull_request.number }} --head-commit ${{ github.event.pull_request.base.sha }} --github-event ${{ github.event.action }}