Skip to content

Commit 1480a4c

Browse files
committed
[FIX] oca_github_bot: use authenticated GitHub API to check maintainers in other branches
1 parent eb48c96 commit 1480a4c

2 files changed

Lines changed: 113 additions & 15 deletions

File tree

src/oca_github_bot/manifest.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -258,34 +258,95 @@ def user_can_push(gh, org, repo, username, addons_dir, target_branch):
258258
if result:
259259
return True
260260

261-
other_branches = config.MAINTAINER_CHECK_ODOO_RELEASES
261+
other_branches = list(config.MAINTAINER_CHECK_ODOO_RELEASES)
262262
if target_branch in other_branches:
263263
other_branches.remove(target_branch)
264264

265265
return is_maintainer_other_branches(
266-
org, repo, username, modified_addons, other_branches
266+
org, repo, username, modified_addons, other_branches, gh=gh
267267
)
268268

269269

270-
def is_maintainer_other_branches(org, repo, username, modified_addons, other_branches):
270+
def _get_manifest_from_api(gh_repo, addon, branch, manifest_file):
271+
"""Read an addon manifest from a GitHub branch using the authenticated API.
272+
273+
Returns the parsed manifest dict, or None if the file does not exist or
274+
cannot be read.
275+
"""
276+
path = f"{addon}/{manifest_file}"
277+
try:
278+
file_contents = gh_repo.file_contents(path, ref=branch)
279+
except Exception as e:
280+
_logger.debug("Could not read %s@%s via GitHub API: %s", path, branch, e)
281+
return None
282+
if file_contents is None:
283+
return None
284+
try:
285+
return parse_manifest(file_contents.content)
286+
except Exception as e:
287+
_logger.warning(
288+
"Failed to parse manifest %s@%s from GitHub API: %s", path, branch, e
289+
)
290+
return None
291+
292+
293+
def _get_manifest_from_raw(org, repo, addon, branch, manifest_file):
294+
"""Fallback to read an addon manifest from github raw URLs.
295+
296+
Returns the parsed manifest dict, or None if the file does not exist or
297+
cannot be read.
298+
"""
299+
url = f"https://github.com/{org}/{repo}/raw/{branch}/{addon}/{manifest_file}"
300+
_logger.debug("Looking for maintainers in %s", url)
301+
try:
302+
r = requests.get(
303+
url, allow_redirects=True, headers={"Cache-Control": "no-cache"}
304+
)
305+
except requests.RequestException as e:
306+
_logger.warning("Failed to fetch %s: %s", url, e)
307+
return None
308+
if not r.ok:
309+
return None
310+
try:
311+
return parse_manifest(r.content)
312+
except Exception as e:
313+
_logger.warning("Failed to parse manifest from %s: %s", url, e)
314+
return None
315+
316+
317+
def is_maintainer_other_branches(
318+
org, repo, username, modified_addons, other_branches, gh=None
319+
):
320+
"""Check if username is maintainer of modified_addons in any configured branch.
321+
322+
When an authenticated GitHub session is available, the GitHub contents API
323+
is used instead of unauthenticated raw.githubusercontent.com requests. This
324+
avoids rate-limiting and transient network errors that could spuriously deny
325+
maintainer privileges during migrations.
326+
"""
327+
gh_repo = None
328+
if gh is not None:
329+
try:
330+
gh_repo = gh.repository(org, repo)
331+
except Exception as e:
332+
_logger.warning("Could not get repository %s/%s from API: %s", org, repo, e)
333+
271334
for addon in modified_addons:
272335
is_maintainer = False
273336
for branch in other_branches:
274337
manifest_file = (
275338
"__openerp__.py" if float(branch) < 10.0 else "__manifest__.py"
276339
)
277-
url = (
278-
f"https://github.com/{org}/{repo}/raw/{branch}/{addon}/{manifest_file}"
279-
)
280-
_logger.debug("Looking for maintainers in %s", url)
281-
r = requests.get(
282-
url, allow_redirects=True, headers={"Cache-Control": "no-cache"}
283-
)
284-
if r.ok:
285-
manifest = parse_manifest(r.content)
286-
if username in manifest.get("maintainers", []):
287-
is_maintainer = True
288-
break
340+
manifest = None
341+
if gh_repo is not None:
342+
manifest = _get_manifest_from_api(gh_repo, addon, branch, manifest_file)
343+
if manifest is None:
344+
manifest = _get_manifest_from_raw(
345+
org, repo, addon, branch, manifest_file
346+
)
347+
if manifest and username in manifest.get("maintainers", []):
348+
is_maintainer = True
349+
break
289350

290351
if not is_maintainer:
291352
return False

tests/test_manifest.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,40 @@ def test_is_maintainer_other_branches():
249249
assert not is_maintainer_other_branches(
250250
"OCA", "mis-builder", "fpdoo", {"mis_builder"}, ["12.0"]
251251
)
252+
253+
254+
def test_is_maintainer_other_branches_with_gh(mocker):
255+
"""When an authenticated GitHub session is provided, the API is used.
256+
257+
The raw URL fallback must not be hit if the API returns a valid manifest.
258+
"""
259+
gh_mock = mocker.MagicMock()
260+
file_contents_mock = mocker.MagicMock()
261+
file_contents_mock.content = b"{'name': 'addon1', 'maintainers': ['u1']}"
262+
gh_repo_mock = mocker.MagicMock()
263+
gh_repo_mock.file_contents.return_value = file_contents_mock
264+
gh_mock.repository.return_value = gh_repo_mock
265+
266+
assert is_maintainer_other_branches(
267+
"OCA", "repo", "u1", {"addon1"}, ["15.0"], gh=gh_mock
268+
)
269+
gh_repo_mock.file_contents.assert_called_once_with(
270+
"addon1/__manifest__.py", ref="15.0"
271+
)
272+
273+
274+
def test_is_maintainer_other_branches_api_fallback_to_raw(mocker):
275+
"""If the GitHub API returns no manifest, the raw URL fallback is used."""
276+
gh_mock = mocker.MagicMock()
277+
gh_repo_mock = mocker.MagicMock()
278+
gh_repo_mock.file_contents.return_value = None
279+
gh_mock.repository.return_value = gh_repo_mock
280+
raw_mock = mocker.patch(
281+
"oca_github_bot.manifest._get_manifest_from_raw",
282+
return_value={"name": "addon1", "maintainers": ["u1"]},
283+
)
284+
285+
assert is_maintainer_other_branches(
286+
"OCA", "repo", "u1", {"addon1"}, ["15.0"], gh=gh_mock
287+
)
288+
raw_mock.assert_called_once_with("OCA", "repo", "addon1", "15.0", "__manifest__.py")

0 commit comments

Comments
 (0)