mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
[IMP] forwardport: outstanding FP reminders
- only remind weekly initially (not daily) - root reminders on the forward port's creation, not the source's merge date - cap reminder interval at 4 weeks (instead of doubling every time) - track reminders on forward ports, don't share between siblings - remove `forwardport_updated_before` from the testing system, it's now possible to just update `reminder_next` to a past date and see if it gets triggered (it should to nothing on sources, or on forward port in a state which does not warrant concern) Fixes #801
This commit is contained in:
parent
916f144163
commit
942570e60a
@ -391,9 +391,7 @@ class Base(models.AbstractModel):
|
|||||||
def run_crons(self):
|
def run_crons(self):
|
||||||
builtins.current_date = self.env.context.get('current_date')
|
builtins.current_date = self.env.context.get('current_date')
|
||||||
builtins.forwardport_merged_before = self.env.context.get('forwardport_merged_before')
|
builtins.forwardport_merged_before = self.env.context.get('forwardport_merged_before')
|
||||||
builtins.forwardport_updated_before = self.env.context.get('forwardport_updated_before')
|
|
||||||
self.env['ir.cron']._process_jobs(self.env.cr.dbname)
|
self.env['ir.cron']._process_jobs(self.env.cr.dbname)
|
||||||
del builtins.forwardport_updated_before
|
|
||||||
del builtins.forwardport_merged_before
|
del builtins.forwardport_merged_before
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
'name': 'forward port bot',
|
'name': 'forward port bot',
|
||||||
'version': '1.4',
|
'version': '1.5',
|
||||||
'summary': "A port which forward ports successful PRs.",
|
'summary': "A port which forward ports successful PRs.",
|
||||||
'depends': ['runbot_merge'],
|
'depends': ['runbot_merge'],
|
||||||
'data': [
|
'data': [
|
||||||
|
6
forwardport/changelog/2025-02/reminder-individual.md
Normal file
6
forwardport/changelog/2025-02/reminder-individual.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
IMP: reminder interval is now tracked by each forward port
|
||||||
|
|
||||||
|
Previously it was tracked on the source, which made sense when the notification
|
||||||
|
was on the source, but not since the reminder is per-forward-port. However this
|
||||||
|
also means brand new forward ports start at 0 instead of wherever the reminder
|
||||||
|
source PR last got from their predecessors.
|
6
forwardport/changelog/2025-02/reminder-interval.md
Normal file
6
forwardport/changelog/2025-02/reminder-interval.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
IMP: the reminder interval has been capped at ~1 month, but increased in the initial phase
|
||||||
|
|
||||||
|
Previously the reminder interval would start at 1 day for about a week, then
|
||||||
|
double every time, quickly increasing to less than yearly.
|
||||||
|
|
||||||
|
The reminder interval is now weekly for the first month, then monthly.
|
27
forwardport/migrations/17.0.1.5/pre-migrate.py
Normal file
27
forwardport/migrations/17.0.1.5/pre-migrate.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
def migrate(cr, version):
|
||||||
|
"""
|
||||||
|
before: source PR has a `reminder_backoff_factor`, specifies the
|
||||||
|
power-of-two number of days between the source's merge date and the
|
||||||
|
next reminder(s)
|
||||||
|
after: forward ports have a `reminder_next` which is the date for sending
|
||||||
|
the next reminder, needs to be at least 7 days after the forward
|
||||||
|
port's creation, if more than that just send a reminder the next
|
||||||
|
time we can (?)
|
||||||
|
|
||||||
|
We don't actually care about the source's anything (technically we could
|
||||||
|
e.g. if we just sent a reminder via the backoff factor then don't send a
|
||||||
|
new one but...)
|
||||||
|
"""
|
||||||
|
cr.execute("""
|
||||||
|
ALTER TABLE runbot_merge_pull_requests
|
||||||
|
ADD COLUMN reminder_next varchar;
|
||||||
|
|
||||||
|
UPDATE runbot_merge_pull_requests
|
||||||
|
SET reminder_next = greatest(
|
||||||
|
now(),
|
||||||
|
create_date::timestamp + interval '7 days'
|
||||||
|
)
|
||||||
|
WHERE source_id IS NOT NULL
|
||||||
|
AND state IN ('opened', 'validated', 'approved', 'ready', 'error')
|
||||||
|
AND blocked IS NOT NULL;
|
||||||
|
""")
|
@ -36,7 +36,7 @@ from odoo.addons.runbot_merge.models.stagings_create import Message
|
|||||||
|
|
||||||
Conflict = tuple[str, str, str, list[str]]
|
Conflict = tuple[str, str, str, list[str]]
|
||||||
|
|
||||||
DEFAULT_DELTA = dateutil.relativedelta.relativedelta(days=3)
|
DEFAULT_DELTA = dateutil.relativedelta.relativedelta(days=7)
|
||||||
|
|
||||||
_logger = logging.getLogger('odoo.addons.forwardport')
|
_logger = logging.getLogger('odoo.addons.forwardport')
|
||||||
|
|
||||||
@ -183,7 +183,10 @@ class PullRequests(models.Model):
|
|||||||
merge_date: datetime.datetime
|
merge_date: datetime.datetime
|
||||||
parent_id: PullRequests
|
parent_id: PullRequests
|
||||||
|
|
||||||
reminder_backoff_factor = fields.Integer(default=-4, group_operator=None)
|
reminder_next = fields.Datetime(
|
||||||
|
default=lambda self: self.env.cr.now() + datetime.timedelta(days=7),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
@ -518,41 +521,27 @@ stderr:
|
|||||||
# don't stringify so caller can still perform alterations
|
# don't stringify so caller can still perform alterations
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def _outstanding(self, cutoff: str) -> typing.ItemsView[PullRequests, list[PullRequests]]:
|
|
||||||
""" Returns "outstanding" (unmerged and unclosed) forward-ports whose
|
|
||||||
source was merged before ``cutoff`` (all of them if not provided).
|
|
||||||
|
|
||||||
:param cutoff: a datetime (ISO-8601 formatted)
|
|
||||||
:returns: an iterator of (source, forward_ports)
|
|
||||||
"""
|
|
||||||
return groupby(self.env['runbot_merge.pull_requests'].search([
|
|
||||||
# only FP PRs
|
|
||||||
('source_id', '!=', False),
|
|
||||||
# active
|
|
||||||
('state', 'not in', ['merged', 'closed']),
|
|
||||||
# not ready
|
|
||||||
('blocked', '!=', False),
|
|
||||||
('source_id.merge_date', '<', cutoff),
|
|
||||||
], order='source_id, id'), lambda p: p.source_id)
|
|
||||||
|
|
||||||
def _reminder(self):
|
def _reminder(self):
|
||||||
cutoff = getattr(builtins, 'forwardport_updated_before', None) \
|
for _, prs in groupby(self.search([
|
||||||
or fields.Datetime.to_string(datetime.datetime.now() - DEFAULT_DELTA)
|
('source_id', '!=', False),
|
||||||
cutoff_dt = fields.Datetime.from_string(cutoff)
|
('blocked', '!=', False),
|
||||||
|
('state', 'in', ['opened', 'validated', 'approved', 'ready', 'error']),
|
||||||
for source, prs in self._outstanding(cutoff):
|
('reminder_next', '<', self.env.cr.now()),
|
||||||
backoff = dateutil.relativedelta.relativedelta(days=2**source.reminder_backoff_factor)
|
], order='source_id, id'), lambda p: p.source_id):
|
||||||
if source.merge_date > (cutoff_dt - backoff):
|
# only remind on the "tip" of every chain of descendants as they
|
||||||
continue
|
# will most likely lead to their parent being validated (?)
|
||||||
source.reminder_backoff_factor += 1
|
|
||||||
|
|
||||||
# only keep the PRs which don't have an attached descendant
|
|
||||||
for pr in set(prs).difference(p.parent_id for p in prs):
|
for pr in set(prs).difference(p.parent_id for p in prs):
|
||||||
|
# reminder every 7 days for the first 4 weeks, then every 4 weeks
|
||||||
|
if (pr.reminder_next - pr.create_date) < datetime.timedelta(days=28):
|
||||||
|
pr.reminder_next += datetime.timedelta(days=7)
|
||||||
|
else:
|
||||||
|
pr.reminder_next += datetime.timedelta(days=28)
|
||||||
|
|
||||||
self.env.ref('runbot_merge.forwardport.reminder')._send(
|
self.env.ref('runbot_merge.forwardport.reminder')._send(
|
||||||
repository=pr.repository,
|
repository=pr.repository,
|
||||||
pull_request=pr.number,
|
pull_request=pr.number,
|
||||||
token_field='fp_github_token',
|
token_field='fp_github_token',
|
||||||
format_args={'pr': pr, 'source': source},
|
format_args={'pr': pr, 'source': pr.source_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,9 +120,10 @@ def test_straightforward_flow(env, config, make_repo, users):
|
|||||||
prod.post_status(pr1.head, 'success', 'legal/cla')
|
prod.post_status(pr1.head, 'success', 'legal/cla')
|
||||||
|
|
||||||
env.run_crons()
|
env.run_crons()
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
|
||||||
|
|
||||||
pr0_, pr1_, pr2 = env['runbot_merge.pull_requests'].search([], order='number')
|
pr0_, pr1_, pr2 = env['runbot_merge.pull_requests'].search([], order='number')
|
||||||
|
pr2.reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
|
|
||||||
|
|
||||||
assert pr.comments == [
|
assert pr.comments == [
|
||||||
(users['reviewer'], 'hansen r+ rebase-ff'),
|
(users['reviewer'], 'hansen r+ rebase-ff'),
|
||||||
@ -323,8 +324,10 @@ def test_empty(env, config, make_repo, users):
|
|||||||
assert project.fp_github_name == users['other']
|
assert project.fp_github_name == users['other']
|
||||||
|
|
||||||
# check reminder
|
# check reminder
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
fail_id.reminder_next = datetime.now() - timedelta(days=1)
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
env.run_crons('forwardport.reminder')
|
||||||
|
fail_id.reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
|
|
||||||
awaiting = (
|
awaiting = (
|
||||||
users['other'],
|
users['other'],
|
||||||
@ -368,7 +371,8 @@ More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port
|
|||||||
# check that this stops if we close the PR
|
# check that this stops if we close the PR
|
||||||
with prod:
|
with prod:
|
||||||
fail_pr.close()
|
fail_pr.close()
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
fail_id.reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
assert pr1.comments == [
|
assert pr1.comments == [
|
||||||
(users['reviewer'], 'hansen r+'),
|
(users['reviewer'], 'hansen r+'),
|
||||||
seen(env, pr1, users),
|
seen(env, pr1, users),
|
||||||
@ -381,13 +385,15 @@ More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port
|
|||||||
with prod:
|
with prod:
|
||||||
prod.post_status(fail_id.head, 'success')
|
prod.post_status(fail_id.head, 'success')
|
||||||
assert fail_id.state == 'validated'
|
assert fail_id.state == 'validated'
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
fail_id.reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
assert fail_pr.comments[2:] == [awaiting]*3, "check that message triggers again"
|
assert fail_pr.comments[2:] == [awaiting]*3, "check that message triggers again"
|
||||||
|
|
||||||
with prod:
|
with prod:
|
||||||
fail_pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
fail_pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||||
assert fail_id.state == 'ready'
|
assert fail_id.state == 'ready'
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
fail_id.reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
assert fail_pr.comments[2:] == [
|
assert fail_pr.comments[2:] == [
|
||||||
awaiting,
|
awaiting,
|
||||||
awaiting,
|
awaiting,
|
||||||
|
@ -1209,7 +1209,8 @@ def test_reminder_detached(env, config, make_repo, users):
|
|||||||
pr_c = prod.get_pr(pr_c_id.number)
|
pr_c = prod.get_pr(pr_c_id.number)
|
||||||
|
|
||||||
# region sanity check
|
# region sanity check
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
(pr_a_id | pr_b_id | pr_c_id).reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
|
|
||||||
assert pr_b.comments == [
|
assert pr_b.comments == [
|
||||||
seen(env, pr_b, users),
|
seen(env, pr_b, users),
|
||||||
@ -1243,7 +1244,8 @@ More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port
|
|||||||
|
|
||||||
# region check detached
|
# region check detached
|
||||||
pr_c_id.write({'parent_id': False, 'detach_reason': 'because'})
|
pr_c_id.write({'parent_id': False, 'detach_reason': 'because'})
|
||||||
env.run_crons('forwardport.reminder', context={'forwardport_updated_before': FAKE_PREV_WEEK})
|
(pr_a_id | pr_b_id | pr_c_id).reminder_next = datetime.now() - timedelta(days=1)
|
||||||
|
env.run_crons('forwardport.reminder')
|
||||||
|
|
||||||
assert pr_b.comments[2:] == [
|
assert pr_b.comments[2:] == [
|
||||||
(users['user'], "@%s @%s child PR %s 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." % (
|
(users['user'], "@%s @%s child PR %s 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." % (
|
||||||
|
Loading…
Reference in New Issue
Block a user