From eb9c0f04c5a7c977bc580957347388c209a09aea Mon Sep 17 00:00:00 2001 From: "Dylan Kiss (dyki)" Date: Thu, 11 Jun 2026 17:14:17 +0200 Subject: [PATCH] [IMP] runbot_merge: add callback on patch application We introduce a mechanism of calling a callback URL with the status of a patch when it either succeeded or failed to apply. The translations sync server uses this to know when to unlock the relevant components in Weblate. --- runbot_merge/controllers/misc.py | 13 +++++---- runbot_merge/data/merge_cron.xml | 9 ++++++ runbot_merge/models/patcher.py | 48 +++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/runbot_merge/controllers/misc.py b/runbot_merge/controllers/misc.py index bbd27c1c0..8f18973c5 100644 --- a/runbot_merge/controllers/misc.py +++ b/runbot_merge/controllers/misc.py @@ -6,9 +6,9 @@ class MergebotController(Controller): @from_role('tx', signed=True) @route('/i18n/merge_commit', type='json', auth='public') - def merge_commit(self, commit_hash, repository, branch, project="RD"): + def merge_commit(self, commit_hash, repository, branch, project="RD", callback_url=None): """Merge a specific commit hash in a repository - + The commit_hash must be known by mergebot (in the git network) Used for translation synchronisation from transifex """ @@ -25,9 +25,12 @@ def merge_commit(self, commit_hash, repository, branch, project="RD"): if not target: return {"error": f"Target branch {project}:{branch} not found"} - patch = request.env["runbot_merge.patch"].sudo().create({ + vals = { "repository": repository_id.id, "target": target.id, - "commit": commit_hash - }) + "commit": commit_hash, + } + if callback_url: + vals["callback_url"] = callback_url + patch = request.env["runbot_merge.patch"].sudo().create(vals) return {"patch": patch.id} diff --git a/runbot_merge/data/merge_cron.xml b/runbot_merge/data/merge_cron.xml index d0b6ba910..37135fb77 100644 --- a/runbot_merge/data/merge_cron.xml +++ b/runbot_merge/data/merge_cron.xml @@ -21,6 +21,15 @@ 40 + + Fire pending patch callbacks + + code + model._cron_fire_pending_callbacks() + 6 + hours + -1 + Warn on linked PRs where only one is ready diff --git a/runbot_merge/models/patcher.py b/runbot_merge/models/patcher.py index 699a4cf41..0573e19f1 100644 --- a/runbot_merge/models/patcher.py +++ b/runbot_merge/models/patcher.py @@ -21,6 +21,7 @@ from typing import Union from markupsafe import Markup +import requests from odoo import models, fields, api from odoo.exceptions import ValidationError @@ -144,6 +145,16 @@ class Patch(models.Model): repository = fields.Many2one('runbot_merge.repository', required=True, tracking=True) target = fields.Many2one('runbot_merge.branch', required=True, tracking=True) commit = fields.Char(size=40, string="commit to cherry-pick, must be in-network", tracking=True) + callback_url = fields.Char( + help="If set, a POST request is made to this URL after the patch was either applied or failed.", + tracking=True, + ) + callback_retries_left = fields.Integer( + help="Number of times the callback can still be retried.", + default=5, + tracking=True, + ) + success = fields.Boolean(help="Indicates whether the patch was successfully applied.", default=False, tracking=True) patch = fields.Text(string="unified diff to apply", tracking=True) format = fields.Selection([ @@ -215,7 +226,11 @@ def _auto_init(self): super()._auto_init() self.env.cr.execute(""" CREATE INDEX IF NOT EXISTS runbot_merge_patch_active - ON runbot_merge_patch (target) WHERE active + ON runbot_merge_patch (target) WHERE active; + CREATE INDEX IF NOT EXISTS runbot_merge_patch_callback + ON runbot_merge_patch (callback_url, active) + WHERE callback_url IS NOT NULL + AND active IS FALSE; """) @api.model_create_multi @@ -354,7 +369,9 @@ def _apply_patches(self, target: Branch) -> bool: ) else: info['target_head'] = c + patch.success = True + self.env.ref('runbot_merge.ir_cron_fire_patch_callbacks')._trigger() return True def _apply_commit(self, r: git.Repo, parent: str) -> str: @@ -441,3 +458,32 @@ def reader(_r, f): author=p.author, committer=p.committer, ).stdout.strip() + + @api.model + def _cron_fire_pending_callbacks(self) -> None: + pending_patches = self.search([('active', '=', False), ('callback_url', '!=', False)]) + if not pending_patches: + return + + session = requests.Session() + for patch in pending_patches: + if patch.callback_retries_left <= 0: + patch.callback_url = False + _logger.warning("Patch callback to %s failed too many times, giving up.", patch.callback_url) + self.env.cr.commit() + continue + + try: + res = session.post( + patch.callback_url, + params={'success': patch.success}, + timeout=10, + ) + res.raise_for_status() + patch.callback_url = False + _logger.info("Patch callback to %s succeeded.", patch.callback_url) + except requests.RequestException: + _logger.warning("Patch callback to %s failed.", patch.callback_url) + finally: + patch.callback_retries_left -= 1 + self.env.cr.commit()