[IMP] runbot_merge: a PR can't be reopened if its batch is merged

In that case, ignore the reopen, close the PR, and tell the idiot to
fuck off.

Also the case where a PR is reopened while its batch is staged was
already handled, but probably not tested: it was implicit in
forcefully updating the HEAD of the PR, which triggers an unstage
since c8a06601a7.

Now that scenario is tested, which should lower the odds of breaking
it in the future.

Fixes #965
This commit is contained in:
Xavier Morel 2024-12-03 14:52:12 +01:00
parent 31f3edeea6
commit 62fbda52a8
3 changed files with 120 additions and 9 deletions

View File

@ -334,16 +334,17 @@ def handle_pr(env, event):
return 'Ignored: could not lock rows (probably being merged)' return 'Ignored: could not lock rows (probably being merged)'
if event['action'] == 'reopened' : if event['action'] == 'reopened' :
if pr_obj.state == 'merged': if pr_obj.merge_date:
feedback( if pr_obj.state == 'merged':
close=True, message = env.ref('runbot_merge.handle.pr.merged')._format(event=event)
message=env.ref('runbot_merge.handle.pr.merged')._format(event=event), else:
) message = env.ref('runbot_merge.handle.pr.mergedbatch')._format(event=event)
feedback(close=True, message=message)
elif pr_obj.closed: elif pr_obj.closed:
_logger.info('%s reopening %s', event['sender']['login'], pr_obj.display_name) _logger.info('%s reopening %s', event['sender']['login'], pr_obj.display_name)
pr_obj.write({ pr_obj.write({
'closed': False, 'closed': False,
# updating the head triggers a revalidation # updating the head triggers a revalidation, and unstages the batch
'head': pr['head']['sha'], 'head': pr['head']['sha'],
'squash': pr['commits'] == 1, 'squash': pr['commits'] == 1,
}) })

View File

@ -11,14 +11,17 @@ branch: branch (ref) name
event: complete pr event" event: complete pr event"
runbot_merge.handle.pr.merged,@{event[sender][login]} ya silly goose you can't reopen a merged PR.,"Notifies that a user tried to reopen a merged PR. runbot_merge.handle.pr.merged,@{event[sender][login]} ya silly goose you can't reopen a merged PR.,"Notifies that a user tried to reopen a merged PR.
Event: complete PR event" event: complete PR event"
runbot_merge.handle.pr.mergedbatch,"Reopening a PR in a merged batch is not allowed, create a new PR.","Notifies that a user tried to reopen a closed PR whose batch is merged.
event: complete PR event"
runbot_merge.pr.load.unmanaged,"Branch `{pr[base][ref]}` is not within my remit, imma just ignore it.","Notifies that a user tried to load a PR targeting a non-handled branch. runbot_merge.pr.load.unmanaged,"Branch `{pr[base][ref]}` is not within my remit, imma just ignore it.","Notifies that a user tried to load a PR targeting a non-handled branch.
pr: pull request (github object) pr: pull request (github object)
Repository: repository object (???)" Repository: repository object (???)"
runbot_merge.pr.load.fetched,"{ping}I didn't know about this PR and had to retrieve its information, you may have to re-approve it as I didn't see previous commands.","Notifies that we did retrieve an unknown PR (either by request or as side effect of an interaction). runbot_merge.pr.load.fetched,"{ping}I didn't know about this PR and had to retrieve its information, you may have to re-approve it as I didn't see previous commands.","Notifies that we did retrieve an unknown PR (either by request or as side effect of an interaction).
Pr: pr object we just created" pr: pr object we just created"
runbot_merge.pr.branch.disabled,"{pr.ping}the target branch {pr.target.name!r} has been disabled, you may want to close this PR.","Notifies that the target branch for this PR was deactivated. runbot_merge.pr.branch.disabled,"{pr.ping}the target branch {pr.target.name!r} has been disabled, you may want to close this PR.","Notifies that the target branch for this PR was deactivated.
pr: pull request in question" pr: pull request in question"

1 id template help
11 runbot_merge.command.approve.failure runbot_merge.command.access.no @{user} you may want to rebuild or fix this PR as it has failed CI. I'm sorry, @{user}. I'm afraid I can't do that. Responds to r+ of PR with failed CI. user: github login of comment sender pr: pr object to which the command was sent Responds to command by a user who has no rights at all. user: github login of comment sender pr: pr object to which the command was sent
12 runbot_merge.command.unapprove.p0 runbot_merge.command.approve.failure Skipchecks removed due to r-. @{user} you may want to rebuild or fix this PR as it has failed CI. Responds to r- of pr in skipchecks. user: github login of comment sender pr: pr object to which the command was sent Responds to r+ of PR with failed CI. user: github login of comment sender pr: pr object to which the command was sent
13 runbot_merge.command.method runbot_merge.command.unapprove.p0 Merge method set to {new_method}. Skipchecks removed due to r-. Responds to the setting of the merge method. new_method: ... pr: pr object to which the command was sent user: github login of the comment sender Responds to r- of pr in skipchecks. user: github login of comment sender pr: pr object to which the command was sent
14 runbot_merge.failure.approved runbot_merge.command.method {pr.ping}{status!r} failed on this reviewed PR. Merge method set to {new_method}. Notification of failed status on a reviewed PR. pr: pull request in question status: failed status Responds to the setting of the merge method. new_method: ... pr: pr object to which the command was sent user: github login of the comment sender
15 runbot_merge.failure.approved {pr.ping}{status!r} failed on this reviewed PR. Notification of failed status on a reviewed PR. pr: pull request in question status: failed status
16 runbot_merge.pr.created [![Pull request status dashboard]({pr.url}.png)]({pr.url}) Initial comment on PR creation. pr: created pr
17 runbot_merge.pr.linked.not_ready {pr.ping}linked pull request(s) {siblings} not ready. Linked PRs are not staged until all of them are ready. Comment when a PR is ready (approved & validated) but it is linked to other PRs which are not. pr: pr we're looking at siblings: its siblings, as a single comma-separated list of PR links
18 runbot_merge.pr.created runbot_merge.pr.merge_method [![Pull request status dashboard]({pr.url}.png)]({pr.url}) {pr.ping}because this PR has multiple commits, I need to know how to merge it: {methods} Initial comment on PR creation. pr: created pr Comment when a PR is ready but doesn't have a merge method set pr: the pr we can't stage methods: a markdown-formatted list of valid merge methods
19 runbot_merge.pr.linked.not_ready runbot_merge.pr.staging.mismatch {pr.ping}linked pull request(s) {siblings} not ready. Linked PRs are not staged until all of them are ready. {pr.ping}we apparently missed updates to this PR and tried to stage it in a state which might not have been approved. The properties {mismatch} were not correctly synchronized and have been updated. <details><summary>differences</summary> ```diff {diff}``` </details> Note that we are unable to check the properties {unchecked}. Please check and re-approve. Comment when a PR is ready (approved & validated) but it is linked to other PRs which are not. pr: pr we're looking at siblings: its siblings, as a single comma-separated list of PR links Comment when staging was attempted but a sanity check revealed the github state and the mergebot state differ. pr: the pr we tried to stage mismatch: comma separated list of mismatched property names diff: patch-style view of the differing properties unchecked: comma-separated list of properties which can't be checked
20 runbot_merge.pr.merge_method runbot_merge.pr.staging.fail {pr.ping}because this PR has multiple commits, I need to know how to merge it: {methods} {pr.ping}staging failed: {message} Comment when a PR is ready but doesn't have a merge method set pr: the pr we can't stage methods: a markdown-formatted list of valid merge methods Comment when a PR caused a staging to fail (normally only sent if the staging has a single batch, may be sent on multiple PRs depending whether the heuristic to guess the problematic PR of a batch succeeded) pr: the pr message: staging failure information (error message, build link, etc...)
21 runbot_merge.pr.staging.mismatch runbot_merge.forwardport.updates.closed {pr.ping}we apparently missed updates to this PR and tried to stage it in a state which might not have been approved. The properties {mismatch} were not correctly synchronized and have been updated. <details><summary>differences</summary> ```diff {diff}``` </details> Note that we are unable to check the properties {unchecked}. Please check and re-approve. {pr.ping}ancestor PR {parent.display_name} has been updated but this PR is {pr.state} and can't be updated to match. You may want or need to manually update any followup PR. Comment when staging was attempted but a sanity check revealed the github state and the mergebot state differ. pr: the pr we tried to stage mismatch: comma separated list of mismatched property names diff: patch-style view of the differing properties unchecked: comma-separated list of properties which can't be checked Comment when a PR is updated and on of its followups is already merged or closed. Sent to the followup. pr: the closed or merged PR parent: the modified ancestor PR
22 runbot_merge.pr.staging.fail runbot_merge.forwardport.updates.conflict.parent {pr.ping}staging failed: {message} {pr.ping}WARNING: the latest change ({pr.head}) triggered a conflict when updating the next forward-port ({next.display_name}), and has been ignored. You will need to update this pull request differently, or fix the issue by hand on {next.display_name}. Comment when a PR caused a staging to fail (normally only sent if the staging has a single batch, may be sent on multiple PRs depending whether the heuristic to guess the problematic PR of a batch succeeded) pr: the pr message: staging failure information (error message, build link, etc...) Comment when a PR update triggers a conflict in a child. pr: updated parent PR next: child PR in conflict
23 runbot_merge.forwardport.updates.closed runbot_merge.forwardport.updates.conflict.child {pr.ping}ancestor PR {parent.display_name} has been updated but this PR is {pr.state} and can't be updated to match. You may want or need to manually update any followup PR. {pr.ping}WARNING: the update of {previous.display_name} to {previous.head} has caused a conflict in this pull request, data may have been lost.{stdout}{stderr} Comment when a PR is updated and on of its followups is already merged or closed. Sent to the followup. pr: the closed or merged PR parent: the modified ancestor PR Comment when a PR update followup is in conflict. pr: PR where update followup conflict happened previous: parent PR which triggered the followup stdout: markdown-formatted stdout of git, if any stderr: markdown-formatted stderr of git, if any
24 runbot_merge.forwardport.updates.conflict.parent runbot_merge.forwardport.update.detached {pr.ping}WARNING: the latest change ({pr.head}) triggered a conflict when updating the next forward-port ({next.display_name}), and has been ignored. You will need to update this pull request differently, or fix the issue by hand on {next.display_name}. {pr.ping}this PR was modified / updated and has become a normal PR. It must be merged directly. Comment when a PR update triggers a conflict in a child. pr: updated parent PR next: child PR in conflict Comment when a forwardport PR gets updated, documents that the PR now needs to be merged the “normal” way. pr: the pr in question
25 runbot_merge.forwardport.updates.conflict.child runbot_merge.forwardport.update.parent {pr.ping}WARNING: the update of {previous.display_name} to {previous.head} has caused a conflict in this pull request, data may have been lost.{stdout}{stderr} {pr.ping}child PR {child.display_name} was modified / updated and has become a normal PR. This PR (and any of its parents) will need to be merged independently as approvals won't cross. Comment when a PR update followup is in conflict. pr: PR where update followup conflict happened previous: parent PR which triggered the followup stdout: markdown-formatted stdout of git, if any stderr: markdown-formatted stderr of git, if any Sent to an open PR when its direct child has been detached. pr: the pr child: its detached child
26 runbot_merge.forwardport.update.detached runbot_merge.forwardport.ci.failed {pr.ping}this PR was modified / updated and has become a normal PR. It must be merged directly. {pr.ping}{ci} failed on this forward-port PR Comment when a forwardport PR gets updated, documents that the PR now needs to be merged the “normal” way. pr: the pr in question Comment when CI fails on a forward-port PR (which thus won't port any further, for now). pr: the pr in question ci: the failed status
27 runbot_merge.forwardport.update.parent runbot_merge.forwardport.failure.discrepancy {pr.ping}child PR {child.display_name} was modified / updated and has become a normal PR. This PR (and any of its parents) will need to be merged independently as approvals won't cross. {pr.ping}this pull request can not be forward-ported: next branch is {next!r} but linked pull request {linked.display_name} has a next branch {other!r}. Sent to an open PR when its direct child has been detached. pr: the pr child: its detached child Comment when we tried to forward port a PR batch, but the PRs have different next targets (unlikely to happen really). pr: the pr we tried to forward port linked: the linked PR with a different next target next: next target for the current pr other: next target for the other pr

View File

@ -3,7 +3,7 @@ without wider relevance and thus other location.
""" """
import pytest import pytest
from utils import Commit, to_pr, pr_page from utils import Commit, to_pr, pr_page, seen
def test_close_single(env, repo): def test_close_single(env, repo):
@ -199,3 +199,110 @@ Inconsistent targets:
env.run_crons() env.run_crons()
assert env['runbot_merge.stagings'].search_count([]) assert env['runbot_merge.stagings'].search_count([])
def test_reopen_pr_in_staged_batch(env, project, make_repo2, config):
"""Reopening a closed PR from a staged batch should cancel the staging
"""
repo1 = make_repo2('a')
repo2 = make_repo2('b')
with repo1:
[m1, _] = repo1.make_commits(
None,
Commit('a', tree={'a': 'a'}),
Commit('b', tree={'b': 'b'}),
ref='heads/p',
)
repo1.make_ref('heads/master', m1)
pr1 = repo1.make_pr(target='master', head='p')
with repo2:
[m2, _] = repo2.make_commits(
None,
Commit('a', tree={'a': 'a'}),
Commit('b', tree={'b': 'b'}),
ref='heads/p',
)
repo2.make_ref('heads/master', m2)
pr2 = repo2.make_pr(target='master', head='p')
pr1_id = to_pr(env, pr1)
pr2_id = to_pr(env, pr2)
batch_id = pr1_id.batch_id
assert batch_id
assert batch_id == pr2_id.batch_id
with repo1:
repo1.post_status(pr1.head, 'success')
pr1.post_comment("hansen r+", config['role_reviewer']['token'])
with repo2:
pr2.close()
env.run_crons(None)
assert pr2_id.state == 'closed'
assert batch_id.staging_ids.filtered(lambda s: s.active)
with repo2:
pr2.open()
assert pr2_id.state == 'opened'
assert not batch_id.staging_ids.filtered(lambda s: s.active)
assert batch_id.blocked
def test_reopen_pr_in_merged_batch(env, project, make_repo2, config, users):
"""If the batch is merged, the pr should just be re-closed with a message
"""
repo1 = make_repo2('a')
repo2 = make_repo2('b')
with repo1:
[m1, _] = repo1.make_commits(
None,
Commit('a', tree={'a': 'a'}),
Commit('b', tree={'b': 'b'}),
ref='heads/p',
)
repo1.make_ref('heads/master', m1)
pr1 = repo1.make_pr(target='master', head='p')
with repo2:
[m2, _] = repo2.make_commits(
None,
Commit('a', tree={'a': 'a'}),
Commit('b', tree={'b': 'b'}),
ref='heads/p',
)
repo2.make_ref('heads/master', m2)
pr2 = repo2.make_pr(target='master', head='p')
pr1_id = to_pr(env, pr1)
pr2_id = to_pr(env, pr2)
batch_id = pr1_id.batch_id
assert batch_id
assert batch_id == pr2_id.batch_id
with repo1:
repo1.post_status(pr1.head, 'success')
pr1.post_comment("hansen r+", config['role_reviewer']['token'])
with repo2:
pr2.close()
env.run_crons(None)
with repo1, repo2:
repo1.post_status('staging.master', 'success')
repo2.post_status('staging.master', 'success')
env.run_crons(None)
assert pr1_id.state == 'merged'
assert pr2_id.state == 'closed'
with repo2:
pr2.open()
env.run_crons(None)
assert pr2_id.closed
assert pr2_id.state == 'closed'
assert pr2.comments == [
seen(env, pr2, users),
(users['user'], 'Reopening a PR in a merged batch is not allowed, create a new PR.'),
]