2018-06-13 17:35:22 +07:00
|
|
|
import hashlib
|
|
|
|
import hmac
|
2018-03-14 16:37:46 +07:00
|
|
|
import logging
|
|
|
|
import json
|
2024-10-07 13:07:59 +07:00
|
|
|
from datetime import datetime, timedelta
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2023-06-15 13:22:38 +07:00
|
|
|
import sentry_sdk
|
2018-06-13 17:35:22 +07:00
|
|
|
import werkzeug.exceptions
|
|
|
|
|
2018-03-14 16:37:46 +07:00
|
|
|
from odoo.http import Controller, request, route
|
|
|
|
|
2018-06-18 15:08:48 +07:00
|
|
|
from . import dashboard
|
2019-12-10 00:14:17 +07:00
|
|
|
from . import reviewer_provisioning
|
2019-11-18 19:57:32 +07:00
|
|
|
from .. import utils, github
|
2018-06-18 15:08:48 +07:00
|
|
|
|
2018-03-14 16:37:46 +07:00
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
class MergebotController(Controller):
|
[ADD] runbot_merge: staging query endpoints
`/runbot_merge/stagings`
========================
This endpoint is a reverse lookup from any number of commits to a
(number of) staging(s):
- it takes a list of commit hashes as either the `commits` or the
`heads` keyword parameter
- it then returns the stagings which have *all* these commits as
respectively commits or heads, if providing all commits for a
project the result should always be unique (if any)
- `commits` are the merged commits, aka the stuff which ends up in the
actual branches
- `heads` are the staging heads, aka the commits at the tip of the
`staging.$name` branches, those may be the same as the corresponding
commit, or might be deduplicator commits which get discarded on
success
`/runbot_merge/stagings/:id`
============================
Returns a list of all PRs in the staging, grouped by batch (aka PRs
which have the same label and must be merged together).
For each PR, the `repository` name, `number`, and `name` in the form
`$repository#$number` get returned.
`/runbot_merge/stagings/:id1/:id2`
==================================
Returns a list of all the *successfully merged* stagings between `id1`
and `id2`, from oldest to most recent. Individual records have the
form:
- `staging` is the id of the staging
- `prs` is the contents of the previous endpoint (a list of PRs
grouped by batch)
`id1` *must* be lower than `id2`.
By default, this endpoint is inclusive on both ends, the
`include_from` and / or `include_to` parameters can be passed with the
`False` value to exclude the corresponding bound from the result.
Related to #768
2023-08-11 16:13:34 +07:00
|
|
|
@route('/runbot_merge/stagings', auth='none', type='json')
|
|
|
|
def stagings_for_commits(self, commits=None, heads=None):
|
|
|
|
Stagings = request.env(user=1)['runbot_merge.stagings'].sudo()
|
|
|
|
if commits:
|
|
|
|
stagings = Stagings.for_commits(*commits)
|
|
|
|
elif heads:
|
|
|
|
stagings = Stagings.for_heads(*heads)
|
|
|
|
else:
|
|
|
|
raise ValueError('Must receive one of "commits" or "heads" kwarg')
|
|
|
|
|
|
|
|
return stagings.ids
|
|
|
|
|
|
|
|
@route('/runbot_merge/stagings/<int:staging>', auth='none', type='json')
|
|
|
|
def prs_for_staging(self, staging):
|
|
|
|
staging = request.env(user=1)['runbot_merge.stagings'].browse(staging)
|
|
|
|
return [
|
|
|
|
batch.prs.mapped(lambda p: {
|
|
|
|
'name': p.display_name,
|
|
|
|
'repository': p.repository.name,
|
|
|
|
'number': p.number,
|
|
|
|
})
|
|
|
|
for batch in staging.sudo().batch_ids
|
|
|
|
]
|
|
|
|
|
|
|
|
@route('/runbot_merge/stagings/<int:from_staging>/<int:to_staging>', auth='none', type='json')
|
2024-02-12 16:18:25 +07:00
|
|
|
def prs_for_stagings(self, from_staging, to_staging, include_from=True, include_to=True):
|
[ADD] runbot_merge: staging query endpoints
`/runbot_merge/stagings`
========================
This endpoint is a reverse lookup from any number of commits to a
(number of) staging(s):
- it takes a list of commit hashes as either the `commits` or the
`heads` keyword parameter
- it then returns the stagings which have *all* these commits as
respectively commits or heads, if providing all commits for a
project the result should always be unique (if any)
- `commits` are the merged commits, aka the stuff which ends up in the
actual branches
- `heads` are the staging heads, aka the commits at the tip of the
`staging.$name` branches, those may be the same as the corresponding
commit, or might be deduplicator commits which get discarded on
success
`/runbot_merge/stagings/:id`
============================
Returns a list of all PRs in the staging, grouped by batch (aka PRs
which have the same label and must be merged together).
For each PR, the `repository` name, `number`, and `name` in the form
`$repository#$number` get returned.
`/runbot_merge/stagings/:id1/:id2`
==================================
Returns a list of all the *successfully merged* stagings between `id1`
and `id2`, from oldest to most recent. Individual records have the
form:
- `staging` is the id of the staging
- `prs` is the contents of the previous endpoint (a list of PRs
grouped by batch)
`id1` *must* be lower than `id2`.
By default, this endpoint is inclusive on both ends, the
`include_from` and / or `include_to` parameters can be passed with the
`False` value to exclude the corresponding bound from the result.
Related to #768
2023-08-11 16:13:34 +07:00
|
|
|
Stagings = request.env(user=1, context={"active_test": False})['runbot_merge.stagings']
|
|
|
|
from_staging = Stagings.browse(from_staging)
|
|
|
|
to_staging = Stagings.browse(to_staging)
|
|
|
|
if from_staging.target != to_staging.target:
|
|
|
|
raise ValueError(f"Stagings must have the same target branch, found {from_staging.target.name} and {to_staging.target.name}")
|
|
|
|
if from_staging.id >= to_staging.id:
|
|
|
|
raise ValueError("first staging must be older than second staging")
|
|
|
|
|
|
|
|
stagings = Stagings.search([
|
|
|
|
('target', '=', to_staging.target.id),
|
|
|
|
('state', '=', 'success'),
|
|
|
|
('id', '>=' if include_from else '>', from_staging.id),
|
|
|
|
('id', '<=' if include_to else '<', to_staging.id),
|
|
|
|
], order="id asc")
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
'staging': staging.id,
|
|
|
|
'prs': [
|
|
|
|
batch.prs.mapped(lambda p: {
|
|
|
|
'name': p.display_name,
|
|
|
|
'repository': p.repository.name,
|
|
|
|
'number': p.number,
|
|
|
|
})
|
|
|
|
for batch in staging.batch_ids
|
|
|
|
]
|
|
|
|
}
|
|
|
|
for staging in stagings
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2018-03-14 16:37:46 +07:00
|
|
|
@route('/runbot_merge/hooks', auth='none', type='json', csrf=False, methods=['POST'])
|
|
|
|
def index(self):
|
2018-06-13 17:35:22 +07:00
|
|
|
req = request.httprequest
|
|
|
|
event = req.headers['X-Github-Event']
|
2023-06-15 13:22:38 +07:00
|
|
|
with sentry_sdk.configure_scope() as scope:
|
|
|
|
if scope.transaction:
|
|
|
|
# only in 1.8.0 (or at least 1.7.2
|
|
|
|
if hasattr(scope, 'set_transaction_name'):
|
|
|
|
scope.set_transaction_name(f"webhook {event}")
|
|
|
|
else: # but our servers use 1.4.3
|
|
|
|
scope.transaction = f"webhook {event}"
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2019-11-18 19:57:32 +07:00
|
|
|
github._gh.info(self._format(req))
|
|
|
|
|
2024-08-07 19:14:27 +07:00
|
|
|
data = request.get_json_data()
|
|
|
|
repo = data.get('repository', {}).get('full_name')
|
2018-06-21 14:55:14 +07:00
|
|
|
env = request.env(user=1)
|
|
|
|
|
[ADD] *: per-repository webhook secret
Currently webhook secrets are configured per *project* which is an
issue both because different repositories may have different
administrators and thus creates safety concerns, and because multiple
repositories can feed into different projects (e.g. on mergebot,
odoo-dev/odoo is both an ancillary repository to the main RD project,
and the main repository to the minor / legacy master-wowl
project). This means it can be necessary to have multiple projects
share the same secret as well, this then mandates the secret for more
repositories per (1).
This is a pain in the ass, so just detach secrets from projects and
link them *only* to repositories, it's cleaner and easier to manage
and set up progressively.
This requires a lot of changes to the tests, as they all need to
correctly configure the signaling.
For `runbot_merge` there was *some* setup sharing already via the
module-level `repo` fixtures`, those were merged into a conftest-level
fixture which could handle the signaling setup. A few tests which
unnecessarily set up repositories ad-hoc were also moved to the
fixture. But for most of the ad-hoc setup in `runbot_merge`, as well
as `forwardport` where it's all ad-hoc, events sources setup was just
appended as is. This should probably be cleaned up at one point, with
the various requirements collected and organised into a small set of
fixtures doing the job more uniformly.
Fixes #887
2024-06-06 16:07:57 +07:00
|
|
|
source = repo and env['runbot_merge.events_sources'].search([('repository', '=', repo)])
|
|
|
|
if not source:
|
|
|
|
_logger.warning(
|
|
|
|
"Ignored hook %s to unknown source repository %s",
|
|
|
|
req.headers.get("X-Github-Delivery"),
|
|
|
|
repo,
|
|
|
|
)
|
|
|
|
return werkzeug.exceptions.Forbidden()
|
|
|
|
elif secret := source.secret:
|
2024-02-26 16:11:53 +07:00
|
|
|
signature = 'sha256=' + hmac.new(secret.strip().encode(), req.get_data(), hashlib.sha256).hexdigest()
|
2023-08-30 16:43:13 +07:00
|
|
|
if not hmac.compare_digest(signature, req.headers.get('X-Hub-Signature-256', '')):
|
2024-02-12 16:19:53 +07:00
|
|
|
_logger.warning(
|
2024-02-26 16:11:53 +07:00
|
|
|
"Ignored hook %s with incorrect signature on %s: got %s expected %s, in:\n%s",
|
2024-02-12 16:19:53 +07:00
|
|
|
req.headers.get('X-Github-Delivery'),
|
2024-02-26 16:11:53 +07:00
|
|
|
repo,
|
2024-02-12 16:19:53 +07:00
|
|
|
req.headers.get('X-Hub-Signature-256'),
|
|
|
|
signature,
|
2024-02-26 16:11:53 +07:00
|
|
|
req.headers,
|
2024-02-12 16:19:53 +07:00
|
|
|
)
|
2018-06-21 14:55:14 +07:00
|
|
|
return werkzeug.exceptions.Forbidden()
|
2024-02-26 16:11:53 +07:00
|
|
|
elif req.headers.get('X-Hub-Signature-256'):
|
|
|
|
_logger.info("No secret for %s but received a signature in:\n%s", repo, req.headers)
|
|
|
|
else:
|
|
|
|
_logger.info("No secret or signature for %s", repo)
|
2018-06-21 14:55:14 +07:00
|
|
|
|
2018-03-27 21:39:29 +07:00
|
|
|
c = EVENTS.get(event)
|
2018-06-21 14:55:14 +07:00
|
|
|
if not c:
|
2019-02-28 19:50:25 +07:00
|
|
|
_logger.warning('Unknown event %s', event)
|
2018-06-21 14:55:14 +07:00
|
|
|
return 'Unknown event {}'.format(event)
|
|
|
|
|
2024-08-07 19:14:27 +07:00
|
|
|
sentry_sdk.set_context('webhook', data)
|
|
|
|
return c(env, data)
|
2018-06-21 14:55:14 +07:00
|
|
|
|
2019-11-18 19:57:32 +07:00
|
|
|
def _format(self, request):
|
2024-02-12 16:19:53 +07:00
|
|
|
return """{r.method} {r.full_path}
|
2019-11-18 19:57:32 +07:00
|
|
|
{headers}
|
2024-02-12 16:19:53 +07:00
|
|
|
|
2019-11-18 19:57:32 +07:00
|
|
|
{body}
|
2024-02-12 16:19:53 +07:00
|
|
|
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\
|
2019-11-18 19:57:32 +07:00
|
|
|
""".format(
|
|
|
|
r=request,
|
|
|
|
headers='\n'.join(
|
|
|
|
'\t%s: %s' % entry for entry in request.headers.items()
|
|
|
|
),
|
2024-02-12 16:19:53 +07:00
|
|
|
body=request.get_data(as_text=True),
|
2019-11-18 19:57:32 +07:00
|
|
|
)
|
|
|
|
|
2018-06-21 14:55:14 +07:00
|
|
|
def handle_pr(env, event):
|
2018-03-14 16:37:46 +07:00
|
|
|
if event['action'] in [
|
|
|
|
'assigned', 'unassigned', 'review_requested', 'review_request_removed',
|
|
|
|
'labeled', 'unlabeled'
|
|
|
|
]:
|
|
|
|
_logger.debug(
|
2020-01-28 21:34:29 +07:00
|
|
|
'Ignoring pull_request[%s] on %s#%s',
|
2018-03-14 16:37:46 +07:00
|
|
|
event['action'],
|
|
|
|
event['pull_request']['base']['repo']['full_name'],
|
|
|
|
event['pull_request']['number'],
|
|
|
|
)
|
2018-03-26 18:08:49 +07:00
|
|
|
return 'Ignoring'
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
pr = event['pull_request']
|
|
|
|
r = pr['base']['repo']['full_name']
|
|
|
|
b = pr['base']['ref']
|
|
|
|
|
|
|
|
repo = env['runbot_merge.repository'].search([('name', '=', r)])
|
|
|
|
if not repo:
|
|
|
|
_logger.warning("Received a PR for %s but not configured to handle that repo", r)
|
|
|
|
# sadly shit's retarded so odoo json endpoints really mean
|
|
|
|
# jsonrpc and it's LITERALLY NOT POSSIBLE TO REPLY WITH
|
|
|
|
# ACTUAL RAW HTTP RESPONSES and thus not possible to
|
|
|
|
# report actual errors to the webhooks listing thing on
|
|
|
|
# github (not that we'd be looking at them but it'd be
|
|
|
|
# useful for tests)
|
2018-03-26 18:08:49 +07:00
|
|
|
return "Not configured to handle {}".format(r)
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
# PRs to unmanaged branches are not necessarily abnormal and
|
|
|
|
# we don't care
|
2020-10-06 17:43:57 +07:00
|
|
|
branch = env['runbot_merge.branch'].with_context(active_test=False).search([
|
2018-03-14 16:37:46 +07:00
|
|
|
('name', '=', b),
|
|
|
|
('project_id', '=', repo.project_id.id),
|
|
|
|
])
|
|
|
|
|
2020-10-06 17:43:57 +07:00
|
|
|
def feedback(**info):
|
|
|
|
return env['runbot_merge.pull_requests.feedback'].create({
|
|
|
|
'repository': repo.id,
|
|
|
|
'pull_request': pr['number'],
|
|
|
|
**info,
|
|
|
|
})
|
2018-03-14 16:37:46 +07:00
|
|
|
def find(target):
|
|
|
|
return env['runbot_merge.pull_requests'].search([
|
|
|
|
('repository', '=', repo.id),
|
|
|
|
('number', '=', pr['number']),
|
2024-02-05 22:10:15 +07:00
|
|
|
# ('target', '=', target.id),
|
2018-03-14 16:37:46 +07:00
|
|
|
])
|
|
|
|
# edition difficulty: pr['base']['ref] is the *new* target, the old one
|
|
|
|
# is at event['change']['base']['ref'] (if the target changed), so edition
|
|
|
|
# handling must occur before the rest of the steps
|
|
|
|
if event['action'] == 'edited':
|
2018-06-07 19:50:11 +07:00
|
|
|
source = event['changes'].get('base', {'ref': {'from': b}})['ref']['from']
|
2020-10-06 17:43:57 +07:00
|
|
|
source_branch = env['runbot_merge.branch'].with_context(active_test=False).search([
|
2018-03-14 16:37:46 +07:00
|
|
|
('name', '=', source),
|
|
|
|
('project_id', '=', repo.project_id.id),
|
|
|
|
])
|
|
|
|
# retargeting to un-managed => delete
|
|
|
|
if not branch:
|
|
|
|
pr = find(source_branch)
|
|
|
|
pr.unlink()
|
2018-03-26 18:08:49 +07:00
|
|
|
return 'Retargeted {} to un-managed branch {}, deleted'.format(pr.id, b)
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
# retargeting from un-managed => create
|
|
|
|
if not source_branch:
|
2018-06-21 14:55:14 +07:00
|
|
|
return handle_pr(env, dict(event, action='opened'))
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2022-07-11 19:00:35 +07:00
|
|
|
pr_obj = find(source_branch)
|
2018-03-14 16:37:46 +07:00
|
|
|
updates = {}
|
|
|
|
if source_branch != branch:
|
2022-07-11 19:00:35 +07:00
|
|
|
if branch != pr_obj.target:
|
|
|
|
updates['target'] = branch.id
|
|
|
|
updates['squash'] = pr['commits'] == 1
|
|
|
|
|
2024-10-22 18:12:28 +07:00
|
|
|
if 'title' in event['changes'] or 'body' in event['changes']:
|
|
|
|
updates['message'] = utils.make_message(pr)
|
2022-07-11 19:00:35 +07:00
|
|
|
|
2023-01-20 21:16:37 +07:00
|
|
|
_logger.info("update: %s = %s (by %s)", pr_obj.display_name, updates, event['sender']['login'])
|
2018-03-14 16:37:46 +07:00
|
|
|
if updates:
|
2023-01-20 21:16:37 +07:00
|
|
|
# copy because it updates the `updates` dict internally
|
|
|
|
pr_obj.write(dict(updates))
|
|
|
|
return 'Updated {}'.format(', '.join(updates))
|
|
|
|
return "Nothing to update ({})".format(', '.join(event['changes'].keys()))
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2020-10-06 17:43:57 +07:00
|
|
|
message = None
|
|
|
|
if not branch:
|
2023-06-12 19:41:42 +07:00
|
|
|
message = env.ref('runbot_merge.handle.branch.unmanaged')._format(
|
|
|
|
repository=r,
|
|
|
|
branch=b,
|
|
|
|
event=event,
|
|
|
|
)
|
2020-10-06 17:43:57 +07:00
|
|
|
_logger.info("Ignoring event %s on PR %s#%d for un-managed branch %s",
|
|
|
|
event['action'], r, pr['number'], b)
|
|
|
|
elif not branch.active:
|
2023-06-12 19:41:42 +07:00
|
|
|
message = env.ref('runbot_merge.handle.branch.inactive')._format(
|
|
|
|
repository=r,
|
|
|
|
branch=b,
|
|
|
|
event=event,
|
|
|
|
)
|
2020-10-06 17:43:57 +07:00
|
|
|
if message and event['action'] not in ('synchronize', 'closed'):
|
|
|
|
feedback(message=message)
|
|
|
|
|
2018-03-14 16:37:46 +07:00
|
|
|
if not branch:
|
2018-03-26 18:08:49 +07:00
|
|
|
return "Not set up to care about {}:{}".format(r, b)
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2023-06-07 14:19:48 +07:00
|
|
|
headers = request.httprequest.headers if request else {}
|
2023-01-11 20:32:39 +07:00
|
|
|
_logger.info(
|
|
|
|
"%s: %s#%s (%s) (by %s, delivery %s by %s)",
|
|
|
|
event['action'],
|
|
|
|
repo.name, pr['number'],
|
|
|
|
pr['title'].strip(),
|
|
|
|
event['sender']['login'],
|
|
|
|
headers.get('X-Github-Delivery'),
|
|
|
|
headers.get('User-Agent'),
|
|
|
|
)
|
[FIX] runbot_merge: tracking message author on PullRequest events
d4fa1fd35315d330566e37f515a937f722859ef7 added tracking to changes
from *comments* (as well as a few hacks around authorship transfer),
however it missed two things:
First, it set the `change-author` during comments handling only, so
changes from the `PullRequest` hook e.g. open, synchronise, close,
edit, don't get attributed to their actual source, and instead just
fall back to uid(1). This is easy enough to fix as the `sender` is
always provided, that can be resolved to a partner which is then set
as the author of whatever changes happen.
Second, I actually missed one of the message hooks: there's both
`_message_log` and `_message_log_batch` and they don't call one
another, so both have to be overridden in order for tracking to be
consistent. In this case, specifically, the *creation* of a tracked
object goes through `_message_log_batch` (since that's a very generic
message and so works on every tracked object created during the
transaction... even though batch has a message per record anyway...)
while *updates* go through `_message_log`.
Fixes #895
2024-06-21 21:33:44 +07:00
|
|
|
sender = env['res.partner'].search([('github_login', '=', event['sender']['login'])], limit=1)
|
|
|
|
if not sender:
|
|
|
|
sender = env['res.partner'].create({'name': event['sender']['login'], 'github_login': event['sender']['login']})
|
[MERGE] bot from 16.0 to 17.0
Broken (can't run odoo at all):
- In Odoo 17.0, the `pre_init_hook` takes an env, not a cursor, update
`_check_citext`.
- Odoo 17.0 rejects `@attrs` and doesn't say where they are or how to
update them, fun, hunt down `attrs={'invisible': ...` and try to fix
them.
- Odoo 17.0 warns on non-multi creates, update them, most were very
reasonable, one very wasn't.
Test failures:
- Odoo 17.0 deprecates `name_get` and doesn't use it as a *source*
anymore, replace overrides by overrides to `_compute_display_name`.
- Multiple tracking changes:
- `_track_set_author` takes a `Partner` not an id.
- `_message_compute_author` still requires overriding in order to
handle record creation, which in standard doesn't support author
overriding.
- `mail.tracking.value.field_type` has been removed, the field type
now needs to be retrieved from the `field_id`.
- Some tracking ordering have changed and require adjusting a few
tests.
Also added a few flushes before SQL queries which are not (obviously
at least) at the start of a cron or controller, no test failure
observed but better safe than sorry (probably).
2024-08-12 18:13:03 +07:00
|
|
|
env['runbot_merge.pull_requests']._track_set_author(sender, fallback=True)
|
[FIX] runbot_merge: tracking message author on PullRequest events
d4fa1fd35315d330566e37f515a937f722859ef7 added tracking to changes
from *comments* (as well as a few hacks around authorship transfer),
however it missed two things:
First, it set the `change-author` during comments handling only, so
changes from the `PullRequest` hook e.g. open, synchronise, close,
edit, don't get attributed to their actual source, and instead just
fall back to uid(1). This is easy enough to fix as the `sender` is
always provided, that can be resolved to a partner which is then set
as the author of whatever changes happen.
Second, I actually missed one of the message hooks: there's both
`_message_log` and `_message_log_batch` and they don't call one
another, so both have to be overridden in order for tracking to be
consistent. In this case, specifically, the *creation* of a tracked
object goes through `_message_log_batch` (since that's a very generic
message and so works on every tracked object created during the
transaction... even though batch has a message per record anyway...)
while *updates* go through `_message_log`.
Fixes #895
2024-06-21 21:33:44 +07:00
|
|
|
|
2018-03-14 16:37:46 +07:00
|
|
|
if event['action'] == 'opened':
|
2022-07-04 14:44:11 +07:00
|
|
|
author_name = pr['user']['login']
|
|
|
|
author = env['res.partner'].search([('github_login', '=', author_name)], limit=1)
|
|
|
|
if not author:
|
|
|
|
env['res.partner'].create({'name': author_name, 'github_login': author_name})
|
2019-08-23 21:16:30 +07:00
|
|
|
pr_obj = env['runbot_merge.pull_requests']._from_gh(pr)
|
2018-03-26 18:08:49 +07:00
|
|
|
return "Tracking PR as {}".format(pr_obj.id)
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2024-03-12 18:17:30 +07:00
|
|
|
pr_obj = env['runbot_merge.pull_requests']._get_or_schedule(r, pr['number'], closing=event['action'] == 'closed')
|
2018-03-14 16:37:46 +07:00
|
|
|
if not pr_obj:
|
2021-01-13 21:48:39 +07:00
|
|
|
_logger.info("webhook %s on unknown PR %s#%s, scheduled fetch", event['action'], repo.name, pr['number'])
|
2018-06-21 14:55:14 +07:00
|
|
|
return "Unknown PR {}:{}, scheduling fetch".format(repo.name, pr['number'])
|
2018-03-14 16:37:46 +07:00
|
|
|
if event['action'] == 'synchronize':
|
|
|
|
if pr_obj.head == pr['head']['sha']:
|
2023-01-11 20:32:39 +07:00
|
|
|
_logger.warning(
|
|
|
|
"PR %s sync %s -> %s => same head",
|
|
|
|
pr_obj.display_name,
|
|
|
|
pr_obj.head,
|
|
|
|
pr['head']['sha'],
|
|
|
|
)
|
2018-03-26 18:08:49 +07:00
|
|
|
return 'No update to pr head'
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
if pr_obj.state in ('closed', 'merged'):
|
2023-01-11 20:32:39 +07:00
|
|
|
_logger.error("PR %s sync %s -> %s => failure (closed)", pr_obj.display_name, pr_obj.head, pr['head']['sha'])
|
2018-06-08 18:16:12 +07:00
|
|
|
return "It's my understanding that closed/merged PRs don't get sync'd"
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2019-03-01 21:52:13 +07:00
|
|
|
_logger.info(
|
2023-01-11 20:32:39 +07:00
|
|
|
"PR %s sync %s -> %s by %s => reset to 'open' and squash=%s",
|
2019-10-14 14:25:21 +07:00
|
|
|
pr_obj.display_name,
|
2023-01-11 20:32:39 +07:00
|
|
|
pr_obj.head,
|
|
|
|
pr['head']['sha'],
|
|
|
|
event['sender']['login'],
|
2019-03-01 21:52:13 +07:00
|
|
|
pr['commits'] == 1
|
|
|
|
)
|
2024-10-07 13:07:59 +07:00
|
|
|
if pr['base']['ref'] != pr_obj.target.name:
|
|
|
|
env['runbot_merge.fetch_job'].create({
|
|
|
|
'repository': pr_obj.repository.id,
|
|
|
|
'number': pr_obj.number,
|
|
|
|
'commits_at': datetime.now() + timedelta(minutes=5),
|
|
|
|
})
|
2019-03-01 21:52:13 +07:00
|
|
|
|
|
|
|
pr_obj.write({
|
[CHG] *: rewrite commands set, rework status management
This commit revisits the commands set in order to make it more
regular, and limit inconsistent command-sets, although it includes
pseudo-command aliases for common tasks now removed from the core set.
Hard Errors
===========
The previous iteration of the commands set would ignore any
non-command term in a command line. This has been changed to hard
error (and ignoring the entire thing) if any command is unknown or
invalid.
This fixes inconsistent / unexpected interpretations where a user
sends a command, then writes a novel on the same line some words of
which happen to *also* be commands, leading to merge states they did
not expect. They should now be told to fuck off.
Priority Restructuring
----------------------
The numerical priority system was pretty messy in that it confused
"staging priority" (in ways which were not entirely straightforward)
with overrides to other concerns.
This has now being split along all the axis, with separate command
subsets for:
- staging prioritisation, now separated between `default`, `priority`,
and `alone`,
- `default` means PRs are picked by an unspecified order when
creating a staging, if nothing better is available
- `priority` means PRs are picked first when staging, however if
`priority` PRs don't fill the staging the rest will be filled with
`default`, this mode did not previously exist
- `alone` means the PRs are picked first, before splits, and only
`alone` PRs can be part of the staging (which usually matches the
modename)
- `skipchecks` overrides both statuses and approval checks, for the
batch, something previously implied in `p=0`, but now
independent. Setting `skipchecks` basically makes the entire batch
`ready`.
For consistency this also sets the reviewer implicitly: since
skipchecks overrides both statuses *and approval*, whoever enables
this mode is essentially the reviewer.
- `cancel` cancels any ongoing staging when the marked PR becomes
ready again, previously this was also implied (in a more restricted
form) by setting `p=0`
FWBot removal
=============
While the "forwardport bot" still exists as an API level (to segregate
access rights between tokens) it has been removed as an interaction
point, as part of the modules merge plan. As a result,
fwbot stops responding
----------------------
Feedback messages are now always sent by the mergebot, the
forward-porting bot should not send any message or notification
anymore.
commands moved to the merge bot
-------------------------------
- `ignore`/`up to` simply changes bot
- `close` as well
- `skipci` is now a choice / flag of an `fw` command, which denotes
the forward-port policy,
- `fw=default` is the old `ci` and resets the policy to default,
that is wait for the PR to be merged to create forward ports, and
for the required statuses on each forward port to be received
before creating the next
- `fw=skipci` is the old `skipci`, it waits for the merge of the
base PR but then creates all the forward ports immediately (unless
it gets a conflict)
- `fw=skipmerge` immediately creates all the forward ports, without
even waiting for the PR to be merged
This is a completely new mode, and may be rather broken as until
now the 'bot has always assumed the source PR had been merged.
approval rework
---------------
Because of the previous section, there is no distinguishing feature
between `mergebot r+` = "merge this PR" and `forwardbot r+` = "merge
this PR and all its parent with different access rights".
As a result, the two have been merged under a single `mergebot r+`
with heuristics attempting to provide the best experience:
- if approving a non-forward port, the behavior does not change
- else, with review rights on the source, all ancestors are approved
- else, as author of the original, approves all ancestors which descend
from a merged PR
- else, approves all ancestors up to and including the oldest ancestor
to which we have review rights
Most notably, the source's author is not delegated on the source or
any of its descendants anymore. This might need to be revisited if it
provides too restrictive.
For the very specialized need of approving a forward-port *and none of
its ancestors*, `review=` can now take a comma (`,`) separated list of
pull request numbers (github numbers, not mergebot ids).
Computed State
==============
The `state` field of pull requests is now computed. Hopefully this
makes the status more consistent and predictable in the long run, and
importantly makes status management more reliable (because reference
datum get updated naturally flowing to the state).
For now however it makes things more complicated as some of the states
have to be separately signaled or updated:
- `closed` and `error` are now separate flags
- `merge_date` is pulled down from forwardport and becomes the
transition signal for ready -> merged
- `reviewed_by` becomes the transition signal for approval (might be a
good idea to rename it...)
- `status` is computed from the head's statuses and overrides, and
*that* becomes the validation state
Ideally, batch-level flags like `skipchecks` should be on, well, the
batch, and `state` should have a dependency on the batch. However
currently the batch is not a durable / permanent member of the system,
so it's a PR-level flag and a messy pile.
On notable change is that *forcing* the state to `ready` now does that
but also sets the reviewer, `skipchecks`, and overrides to ensure the
API-mediated readying does not get rolled back by e.g. the runbot
sending a status.
This is useful for a few types of automated / programmatic PRs
e.g. translation exports, where we set the state programmatically to
limit noise.
recursive dependency hack
-------------------------
Given a sequence of PRs with an override of the source, if one of the
PRs is updated its descendants should not have the override
anymore. However if the updated PR gets overridden, its descendants
should have *that* override.
This requires some unholy manipulations via an override of `modified`,
as the ORM supports recursive fields but not recursive
dependencies (on a different field).
unconditional followup scheduling
---------------------------------
Previously scheduling forward-port followup was contigent on the FW
policy, but it's not actually correct if the new PR is *immediately*
validated (which can happen now that the field is computed, if there
are no required statuses *or* all of the required statuses are
overridden by an ancestor) as nothing will trigger the state change
and thus scheduling of the fp followup.
The followup function checks all the properties of the batch to port,
so this should not result on incorrect ports. Although it's a bit more
expensive, and will lead to more spam.
Previously this would not happen because on creation of a PR the
validation task (commit -> PR) would still have to execute.
Misc Changes
============
- If a PR is marked as overriding / canceling stagings, it now does
so on retry not just when setting initially.
This was not handled at all previously, so a PR in P0 going into
error due to e.g. a non-deterministic bug would be retried and still
p=0, but a current staging would not get cancelled. Same when a PR
in p=0 goes into error because something was failed, then is updated
with a fix.
- Add tracking to a bunch of relevant PR fields.
Post-mortem analysis currently generally requires going through the
text logs to see what happened, which is annoying.
There is a nondeterminism / inconsistency in the tracking which
sometimes leads the admin user to trigger tracking before the bot
does, leading to the staging tracking being attributed to them
during tests, shove under the carpet by ignoring the user to whom
that tracking is attributed.
When multiple users update tracked fields in the same transaction
all the changes are attributed to the first one having triggered
tracking (?), I couldn't find why the admin sometimes takes over.
- added and leveraged support for enum-backed selection fields
- moved variuous fields from forwardport to runbot_merge
- fix a migration which had never worked and which never run (because
I forgot to bump the version on the module)
- remove some unnecessary intermediate de/serialisation
fixes #673, fixes #309, fixes #792, fixes #846 (probably)
2023-10-31 13:42:07 +07:00
|
|
|
'reviewed_by': False,
|
|
|
|
'error': False,
|
2019-03-01 21:52:13 +07:00
|
|
|
'head': pr['head']['sha'],
|
|
|
|
'squash': pr['commits'] == 1,
|
|
|
|
})
|
2023-01-20 21:16:37 +07:00
|
|
|
return f'Updated to {pr_obj.head}'
|
2021-08-11 16:36:35 +07:00
|
|
|
|
|
|
|
if event['action'] == 'ready_for_review':
|
|
|
|
pr_obj.draft = False
|
|
|
|
return f'Updated {pr_obj.display_name} to ready'
|
|
|
|
if event['action'] == 'converted_to_draft':
|
|
|
|
pr_obj.draft = True
|
|
|
|
return f'Updated {pr_obj.display_name} to draft'
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
# don't marked merged PRs as closed (!!!)
|
|
|
|
if event['action'] == 'closed' and pr_obj.state != 'merged':
|
2021-07-30 14:20:57 +07:00
|
|
|
oldstate = pr_obj.state
|
2019-08-23 21:16:30 +07:00
|
|
|
if pr_obj._try_closing(event['sender']['login']):
|
2021-07-30 14:20:57 +07:00
|
|
|
_logger.info(
|
|
|
|
'%s closed %s (state=%s)',
|
|
|
|
event['sender']['login'],
|
|
|
|
pr_obj.display_name,
|
|
|
|
oldstate,
|
|
|
|
)
|
2021-08-11 16:36:35 +07:00
|
|
|
return 'Closed {}'.format(pr_obj.display_name)
|
2023-06-14 18:34:22 +07:00
|
|
|
|
|
|
|
_logger.info(
|
|
|
|
'%s tried to close %s (state=%s) but locking failed',
|
|
|
|
event['sender']['login'],
|
|
|
|
pr_obj.display_name,
|
|
|
|
oldstate,
|
|
|
|
)
|
|
|
|
return 'Ignored: could not lock rows (probably being merged)'
|
2018-10-16 19:49:57 +07:00
|
|
|
|
2020-02-10 15:48:03 +07:00
|
|
|
if event['action'] == 'reopened' :
|
2024-12-03 20:52:12 +07:00
|
|
|
if pr_obj.merge_date:
|
|
|
|
if pr_obj.state == 'merged':
|
|
|
|
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)
|
[CHG] *: rewrite commands set, rework status management
This commit revisits the commands set in order to make it more
regular, and limit inconsistent command-sets, although it includes
pseudo-command aliases for common tasks now removed from the core set.
Hard Errors
===========
The previous iteration of the commands set would ignore any
non-command term in a command line. This has been changed to hard
error (and ignoring the entire thing) if any command is unknown or
invalid.
This fixes inconsistent / unexpected interpretations where a user
sends a command, then writes a novel on the same line some words of
which happen to *also* be commands, leading to merge states they did
not expect. They should now be told to fuck off.
Priority Restructuring
----------------------
The numerical priority system was pretty messy in that it confused
"staging priority" (in ways which were not entirely straightforward)
with overrides to other concerns.
This has now being split along all the axis, with separate command
subsets for:
- staging prioritisation, now separated between `default`, `priority`,
and `alone`,
- `default` means PRs are picked by an unspecified order when
creating a staging, if nothing better is available
- `priority` means PRs are picked first when staging, however if
`priority` PRs don't fill the staging the rest will be filled with
`default`, this mode did not previously exist
- `alone` means the PRs are picked first, before splits, and only
`alone` PRs can be part of the staging (which usually matches the
modename)
- `skipchecks` overrides both statuses and approval checks, for the
batch, something previously implied in `p=0`, but now
independent. Setting `skipchecks` basically makes the entire batch
`ready`.
For consistency this also sets the reviewer implicitly: since
skipchecks overrides both statuses *and approval*, whoever enables
this mode is essentially the reviewer.
- `cancel` cancels any ongoing staging when the marked PR becomes
ready again, previously this was also implied (in a more restricted
form) by setting `p=0`
FWBot removal
=============
While the "forwardport bot" still exists as an API level (to segregate
access rights between tokens) it has been removed as an interaction
point, as part of the modules merge plan. As a result,
fwbot stops responding
----------------------
Feedback messages are now always sent by the mergebot, the
forward-porting bot should not send any message or notification
anymore.
commands moved to the merge bot
-------------------------------
- `ignore`/`up to` simply changes bot
- `close` as well
- `skipci` is now a choice / flag of an `fw` command, which denotes
the forward-port policy,
- `fw=default` is the old `ci` and resets the policy to default,
that is wait for the PR to be merged to create forward ports, and
for the required statuses on each forward port to be received
before creating the next
- `fw=skipci` is the old `skipci`, it waits for the merge of the
base PR but then creates all the forward ports immediately (unless
it gets a conflict)
- `fw=skipmerge` immediately creates all the forward ports, without
even waiting for the PR to be merged
This is a completely new mode, and may be rather broken as until
now the 'bot has always assumed the source PR had been merged.
approval rework
---------------
Because of the previous section, there is no distinguishing feature
between `mergebot r+` = "merge this PR" and `forwardbot r+` = "merge
this PR and all its parent with different access rights".
As a result, the two have been merged under a single `mergebot r+`
with heuristics attempting to provide the best experience:
- if approving a non-forward port, the behavior does not change
- else, with review rights on the source, all ancestors are approved
- else, as author of the original, approves all ancestors which descend
from a merged PR
- else, approves all ancestors up to and including the oldest ancestor
to which we have review rights
Most notably, the source's author is not delegated on the source or
any of its descendants anymore. This might need to be revisited if it
provides too restrictive.
For the very specialized need of approving a forward-port *and none of
its ancestors*, `review=` can now take a comma (`,`) separated list of
pull request numbers (github numbers, not mergebot ids).
Computed State
==============
The `state` field of pull requests is now computed. Hopefully this
makes the status more consistent and predictable in the long run, and
importantly makes status management more reliable (because reference
datum get updated naturally flowing to the state).
For now however it makes things more complicated as some of the states
have to be separately signaled or updated:
- `closed` and `error` are now separate flags
- `merge_date` is pulled down from forwardport and becomes the
transition signal for ready -> merged
- `reviewed_by` becomes the transition signal for approval (might be a
good idea to rename it...)
- `status` is computed from the head's statuses and overrides, and
*that* becomes the validation state
Ideally, batch-level flags like `skipchecks` should be on, well, the
batch, and `state` should have a dependency on the batch. However
currently the batch is not a durable / permanent member of the system,
so it's a PR-level flag and a messy pile.
On notable change is that *forcing* the state to `ready` now does that
but also sets the reviewer, `skipchecks`, and overrides to ensure the
API-mediated readying does not get rolled back by e.g. the runbot
sending a status.
This is useful for a few types of automated / programmatic PRs
e.g. translation exports, where we set the state programmatically to
limit noise.
recursive dependency hack
-------------------------
Given a sequence of PRs with an override of the source, if one of the
PRs is updated its descendants should not have the override
anymore. However if the updated PR gets overridden, its descendants
should have *that* override.
This requires some unholy manipulations via an override of `modified`,
as the ORM supports recursive fields but not recursive
dependencies (on a different field).
unconditional followup scheduling
---------------------------------
Previously scheduling forward-port followup was contigent on the FW
policy, but it's not actually correct if the new PR is *immediately*
validated (which can happen now that the field is computed, if there
are no required statuses *or* all of the required statuses are
overridden by an ancestor) as nothing will trigger the state change
and thus scheduling of the fp followup.
The followup function checks all the properties of the batch to port,
so this should not result on incorrect ports. Although it's a bit more
expensive, and will lead to more spam.
Previously this would not happen because on creation of a PR the
validation task (commit -> PR) would still have to execute.
Misc Changes
============
- If a PR is marked as overriding / canceling stagings, it now does
so on retry not just when setting initially.
This was not handled at all previously, so a PR in P0 going into
error due to e.g. a non-deterministic bug would be retried and still
p=0, but a current staging would not get cancelled. Same when a PR
in p=0 goes into error because something was failed, then is updated
with a fix.
- Add tracking to a bunch of relevant PR fields.
Post-mortem analysis currently generally requires going through the
text logs to see what happened, which is annoying.
There is a nondeterminism / inconsistency in the tracking which
sometimes leads the admin user to trigger tracking before the bot
does, leading to the staging tracking being attributed to them
during tests, shove under the carpet by ignoring the user to whom
that tracking is attributed.
When multiple users update tracked fields in the same transaction
all the changes are attributed to the first one having triggered
tracking (?), I couldn't find why the admin sometimes takes over.
- added and leveraged support for enum-backed selection fields
- moved variuous fields from forwardport to runbot_merge
- fix a migration which had never worked and which never run (because
I forgot to bump the version on the module)
- remove some unnecessary intermediate de/serialisation
fixes #673, fixes #309, fixes #792, fixes #846 (probably)
2023-10-31 13:42:07 +07:00
|
|
|
elif pr_obj.closed:
|
2020-02-10 15:48:03 +07:00
|
|
|
_logger.info('%s reopening %s', event['sender']['login'], pr_obj.display_name)
|
|
|
|
pr_obj.write({
|
[CHG] *: rewrite commands set, rework status management
This commit revisits the commands set in order to make it more
regular, and limit inconsistent command-sets, although it includes
pseudo-command aliases for common tasks now removed from the core set.
Hard Errors
===========
The previous iteration of the commands set would ignore any
non-command term in a command line. This has been changed to hard
error (and ignoring the entire thing) if any command is unknown or
invalid.
This fixes inconsistent / unexpected interpretations where a user
sends a command, then writes a novel on the same line some words of
which happen to *also* be commands, leading to merge states they did
not expect. They should now be told to fuck off.
Priority Restructuring
----------------------
The numerical priority system was pretty messy in that it confused
"staging priority" (in ways which were not entirely straightforward)
with overrides to other concerns.
This has now being split along all the axis, with separate command
subsets for:
- staging prioritisation, now separated between `default`, `priority`,
and `alone`,
- `default` means PRs are picked by an unspecified order when
creating a staging, if nothing better is available
- `priority` means PRs are picked first when staging, however if
`priority` PRs don't fill the staging the rest will be filled with
`default`, this mode did not previously exist
- `alone` means the PRs are picked first, before splits, and only
`alone` PRs can be part of the staging (which usually matches the
modename)
- `skipchecks` overrides both statuses and approval checks, for the
batch, something previously implied in `p=0`, but now
independent. Setting `skipchecks` basically makes the entire batch
`ready`.
For consistency this also sets the reviewer implicitly: since
skipchecks overrides both statuses *and approval*, whoever enables
this mode is essentially the reviewer.
- `cancel` cancels any ongoing staging when the marked PR becomes
ready again, previously this was also implied (in a more restricted
form) by setting `p=0`
FWBot removal
=============
While the "forwardport bot" still exists as an API level (to segregate
access rights between tokens) it has been removed as an interaction
point, as part of the modules merge plan. As a result,
fwbot stops responding
----------------------
Feedback messages are now always sent by the mergebot, the
forward-porting bot should not send any message or notification
anymore.
commands moved to the merge bot
-------------------------------
- `ignore`/`up to` simply changes bot
- `close` as well
- `skipci` is now a choice / flag of an `fw` command, which denotes
the forward-port policy,
- `fw=default` is the old `ci` and resets the policy to default,
that is wait for the PR to be merged to create forward ports, and
for the required statuses on each forward port to be received
before creating the next
- `fw=skipci` is the old `skipci`, it waits for the merge of the
base PR but then creates all the forward ports immediately (unless
it gets a conflict)
- `fw=skipmerge` immediately creates all the forward ports, without
even waiting for the PR to be merged
This is a completely new mode, and may be rather broken as until
now the 'bot has always assumed the source PR had been merged.
approval rework
---------------
Because of the previous section, there is no distinguishing feature
between `mergebot r+` = "merge this PR" and `forwardbot r+` = "merge
this PR and all its parent with different access rights".
As a result, the two have been merged under a single `mergebot r+`
with heuristics attempting to provide the best experience:
- if approving a non-forward port, the behavior does not change
- else, with review rights on the source, all ancestors are approved
- else, as author of the original, approves all ancestors which descend
from a merged PR
- else, approves all ancestors up to and including the oldest ancestor
to which we have review rights
Most notably, the source's author is not delegated on the source or
any of its descendants anymore. This might need to be revisited if it
provides too restrictive.
For the very specialized need of approving a forward-port *and none of
its ancestors*, `review=` can now take a comma (`,`) separated list of
pull request numbers (github numbers, not mergebot ids).
Computed State
==============
The `state` field of pull requests is now computed. Hopefully this
makes the status more consistent and predictable in the long run, and
importantly makes status management more reliable (because reference
datum get updated naturally flowing to the state).
For now however it makes things more complicated as some of the states
have to be separately signaled or updated:
- `closed` and `error` are now separate flags
- `merge_date` is pulled down from forwardport and becomes the
transition signal for ready -> merged
- `reviewed_by` becomes the transition signal for approval (might be a
good idea to rename it...)
- `status` is computed from the head's statuses and overrides, and
*that* becomes the validation state
Ideally, batch-level flags like `skipchecks` should be on, well, the
batch, and `state` should have a dependency on the batch. However
currently the batch is not a durable / permanent member of the system,
so it's a PR-level flag and a messy pile.
On notable change is that *forcing* the state to `ready` now does that
but also sets the reviewer, `skipchecks`, and overrides to ensure the
API-mediated readying does not get rolled back by e.g. the runbot
sending a status.
This is useful for a few types of automated / programmatic PRs
e.g. translation exports, where we set the state programmatically to
limit noise.
recursive dependency hack
-------------------------
Given a sequence of PRs with an override of the source, if one of the
PRs is updated its descendants should not have the override
anymore. However if the updated PR gets overridden, its descendants
should have *that* override.
This requires some unholy manipulations via an override of `modified`,
as the ORM supports recursive fields but not recursive
dependencies (on a different field).
unconditional followup scheduling
---------------------------------
Previously scheduling forward-port followup was contigent on the FW
policy, but it's not actually correct if the new PR is *immediately*
validated (which can happen now that the field is computed, if there
are no required statuses *or* all of the required statuses are
overridden by an ancestor) as nothing will trigger the state change
and thus scheduling of the fp followup.
The followup function checks all the properties of the batch to port,
so this should not result on incorrect ports. Although it's a bit more
expensive, and will lead to more spam.
Previously this would not happen because on creation of a PR the
validation task (commit -> PR) would still have to execute.
Misc Changes
============
- If a PR is marked as overriding / canceling stagings, it now does
so on retry not just when setting initially.
This was not handled at all previously, so a PR in P0 going into
error due to e.g. a non-deterministic bug would be retried and still
p=0, but a current staging would not get cancelled. Same when a PR
in p=0 goes into error because something was failed, then is updated
with a fix.
- Add tracking to a bunch of relevant PR fields.
Post-mortem analysis currently generally requires going through the
text logs to see what happened, which is annoying.
There is a nondeterminism / inconsistency in the tracking which
sometimes leads the admin user to trigger tracking before the bot
does, leading to the staging tracking being attributed to them
during tests, shove under the carpet by ignoring the user to whom
that tracking is attributed.
When multiple users update tracked fields in the same transaction
all the changes are attributed to the first one having triggered
tracking (?), I couldn't find why the admin sometimes takes over.
- added and leveraged support for enum-backed selection fields
- moved variuous fields from forwardport to runbot_merge
- fix a migration which had never worked and which never run (because
I forgot to bump the version on the module)
- remove some unnecessary intermediate de/serialisation
fixes #673, fixes #309, fixes #792, fixes #846 (probably)
2023-10-31 13:42:07 +07:00
|
|
|
'closed': False,
|
2024-12-03 20:52:12 +07:00
|
|
|
# updating the head triggers a revalidation, and unstages the batch
|
2020-02-10 15:48:03 +07:00
|
|
|
'head': pr['head']['sha'],
|
2020-05-28 18:10:55 +07:00
|
|
|
'squash': pr['commits'] == 1,
|
2020-02-10 15:48:03 +07:00
|
|
|
})
|
|
|
|
|
2021-08-11 16:36:35 +07:00
|
|
|
return 'Reopened {}'.format(pr_obj.display_name)
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
_logger.info("Ignoring event %s on PR %s", event['action'], pr['number'])
|
2018-03-26 18:08:49 +07:00
|
|
|
return "Not handling {} yet".format(event['action'])
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2018-06-21 14:55:14 +07:00
|
|
|
def handle_status(env, event):
|
2018-03-14 16:37:46 +07:00
|
|
|
_logger.info(
|
2020-01-31 16:36:01 +07:00
|
|
|
'status on %(sha)s %(context)s:%(state)s (%(target_url)s) [%(description)r]',
|
2018-09-17 16:04:31 +07:00
|
|
|
event
|
2018-03-14 16:37:46 +07:00
|
|
|
)
|
2021-08-09 18:21:24 +07:00
|
|
|
status_value = json.dumps({
|
|
|
|
event['context']: {
|
|
|
|
'state': event['state'],
|
|
|
|
'target_url': event['target_url'],
|
2024-09-20 17:17:17 +07:00
|
|
|
'description': event['description'],
|
|
|
|
'updated_at': datetime.now().isoformat(timespec='seconds'),
|
2019-10-07 21:38:14 +07:00
|
|
|
}
|
2021-08-09 18:21:24 +07:00
|
|
|
})
|
|
|
|
# create status, or merge update into commit *unless* the update is already
|
|
|
|
# part of the status (dupe status)
|
|
|
|
env.cr.execute("""
|
|
|
|
INSERT INTO runbot_merge_commit AS c (sha, to_check, statuses)
|
|
|
|
VALUES (%s, true, %s)
|
|
|
|
ON CONFLICT (sha) DO UPDATE
|
|
|
|
SET to_check = true,
|
|
|
|
statuses = c.statuses::jsonb || EXCLUDED.statuses::jsonb
|
|
|
|
WHERE NOT c.statuses::jsonb @> EXCLUDED.statuses::jsonb
|
[IMP] runbot_merge: hide concurrent update errors
As far as I can tell they are properly handled:
- In `handle_status` we let the http layer retry the query, which
pretty much always succeeds.
- In `Commit.notify`, we rollback the application of the current
commit, meaning it'll be processed by the next run of the cron,
which also seems to succeed every time (that is going through the
log I pretty much never notice the same commit being serialization
failure'd twice in a row).
Which we can trigger for faster action, this last item is not
entirely necessary as statuses should generally come in fast and
especially if we have concurrency errors, but it can't hurt.
This means the only genuine issue is... sql_db logging a "bad query"
every time there's a serialization failure.
In `handle_status`, just suppress the message outright, if there's an
error other than serialization the http / dispatch layer should catch
and log it.
In `Commit._notify` things are slightly more difficult as the execute
is implicit (`flush` -> `_write` -> `execute`) so we can't pass the
flag by parameter. One option would be to set and unset
`_default_log_exception`, but it would either be a bit dodgy or it
would require using a context manager and increasing the indentation
level (or using a custom context manager).
Instead just `mute_logger` the fucking thing. It's a bit brutish and
mostly used in tests, but not just, and feels like the least bad
option here...
Closes #805
2024-10-22 18:57:58 +07:00
|
|
|
""", [event['sha'], status_value], log_exceptions=False)
|
2024-06-21 15:58:05 +07:00
|
|
|
env.ref("runbot_merge.process_updated_commits")._trigger()
|
2018-03-14 16:37:46 +07:00
|
|
|
|
|
|
|
return 'ok'
|
|
|
|
|
2018-06-21 14:55:14 +07:00
|
|
|
def handle_comment(env, event):
|
2018-03-14 16:37:46 +07:00
|
|
|
if 'pull_request' not in event['issue']:
|
|
|
|
return "issue comment, ignoring"
|
|
|
|
|
2018-06-22 15:55:44 +07:00
|
|
|
repo = event['repository']['full_name']
|
|
|
|
issue = event['issue']['number']
|
2018-09-24 15:06:58 +07:00
|
|
|
author = event['comment']['user']['login']
|
2018-06-22 15:55:44 +07:00
|
|
|
comment = event['comment']['body']
|
2024-06-25 20:05:32 +07:00
|
|
|
if len(comment) > 5000:
|
|
|
|
_logger.warning('comment(%s): %s %s#%s => ignored (%d characters)', event['comment']['html_url'], author, repo, issue, len(comment))
|
|
|
|
return "ignored: too big"
|
|
|
|
|
2021-01-14 13:42:10 +07:00
|
|
|
_logger.info('comment[%s]: %s %s#%s %r', event['action'], author, repo, issue, comment)
|
2018-11-26 16:28:27 +07:00
|
|
|
if event['action'] != 'created':
|
|
|
|
return "Ignored: action (%r) is not 'created'" % event['action']
|
2018-06-07 22:30:06 +07:00
|
|
|
|
2020-07-14 15:06:07 +07:00
|
|
|
return _handle_comment(env, repo, issue, event['comment'])
|
2018-03-14 16:37:46 +07:00
|
|
|
|
2018-06-21 14:55:14 +07:00
|
|
|
def handle_review(env, event):
|
2018-09-25 19:05:41 +07:00
|
|
|
repo = event['repository']['full_name']
|
2018-10-16 17:40:45 +07:00
|
|
|
pr = event['pull_request']['number']
|
2018-09-25 19:05:41 +07:00
|
|
|
author = event['review']['user']['login']
|
2018-10-16 17:40:45 +07:00
|
|
|
comment = event['review']['body'] or ''
|
2024-06-25 20:05:32 +07:00
|
|
|
if len(comment) > 5000:
|
|
|
|
_logger.warning('comment(%s): %s %s#%s => ignored (%d characters)', event['review']['html_url'], author, repo, pr, len(comment))
|
|
|
|
return "ignored: too big"
|
2018-11-26 16:28:27 +07:00
|
|
|
|
2021-01-14 13:42:10 +07:00
|
|
|
_logger.info('review[%s]: %s %s#%s %r', event['action'], author, repo, pr, comment)
|
2018-11-26 16:28:27 +07:00
|
|
|
if event['action'] != 'submitted':
|
|
|
|
return "Ignored: action (%r) is not 'submitted'" % event['action']
|
2018-09-25 19:05:41 +07:00
|
|
|
|
2018-10-16 17:40:45 +07:00
|
|
|
return _handle_comment(
|
2020-07-14 15:06:07 +07:00
|
|
|
env, repo, pr, event['review'],
|
2018-10-16 17:40:45 +07:00
|
|
|
target=event['pull_request']['base']['ref'])
|
2018-03-27 21:39:29 +07:00
|
|
|
|
2018-06-21 14:55:14 +07:00
|
|
|
def handle_ping(env, event):
|
2024-06-25 20:06:41 +07:00
|
|
|
_logger.info("Got ping! %s", event['zen'])
|
2018-03-14 16:37:46 +07:00
|
|
|
return "pong"
|
|
|
|
|
|
|
|
EVENTS = {
|
|
|
|
'pull_request': handle_pr,
|
|
|
|
'status': handle_status,
|
|
|
|
'issue_comment': handle_comment,
|
2018-03-27 21:39:29 +07:00
|
|
|
'pull_request_review': handle_review,
|
2018-03-14 16:37:46 +07:00
|
|
|
'ping': handle_ping,
|
|
|
|
}
|
2018-10-16 17:40:45 +07:00
|
|
|
|
2020-07-14 15:06:07 +07:00
|
|
|
def _handle_comment(env, repo, issue, comment, target=None):
|
2018-10-16 17:40:45 +07:00
|
|
|
repository = env['runbot_merge.repository'].search([('name', '=', repo)])
|
2020-07-14 15:06:07 +07:00
|
|
|
if not repository.project_id._find_commands(comment['body'] or ''):
|
2018-10-16 17:40:45 +07:00
|
|
|
return "No commands, ignoring"
|
|
|
|
|
2024-11-18 19:52:27 +07:00
|
|
|
pr = env['runbot_merge.pull_requests']._get_or_schedule(
|
|
|
|
repo, issue, target=target, commenter=comment['user']['login'],
|
|
|
|
)
|
2018-10-16 17:40:45 +07:00
|
|
|
if not pr:
|
|
|
|
return "Unknown PR, scheduling fetch"
|
|
|
|
|
2020-07-14 15:06:07 +07:00
|
|
|
partner = env['res.partner'].search([('github_login', '=', comment['user']['login'])])
|
|
|
|
return pr._parse_commands(partner, comment, comment['user']['login'])
|