Skip to content

Commit fab351f

Browse files
committed
[IMP] odoo_repository: sync OCA repositories
Do not maintain the list of OCA repositories manually anymore, but sync it automatically with https://github.com/OCA/repo-maintainer-conf. All repositories having a default branch set to `master` or `main` will be skipped (such repositories are not hosting Odoo modules). Others like OpenUpgrade or OCB will be skipped too.
1 parent 481b3bc commit fab351f

21 files changed

Lines changed: 670 additions & 219 deletions

odoo_repository/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from . import models
2+
from . import components
23
from . import controllers

odoo_repository/__manifest__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Copyright 2023 Camptocamp SA
2+
# Copyright 2026 Sébastien Alix
23
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
34
{
45
"name": "Odoo Repositories Data",
56
"summary": "Base module to host data collected from Odoo repositories.",
6-
"version": "18.0.1.0.0",
7+
"version": "18.0.1.1.0",
78
"category": "Tools",
89
"author": "Camptocamp, Odoo Community Association (OCA)",
910
"website": "https://github.com/OCA/module-composition-analysis",
@@ -15,9 +16,10 @@
1516
"data/odoo_repository_org.xml",
1617
"data/odoo_repository_addons_path.xml",
1718
"data/odoo_repository.xml",
18-
"data/odoo.repository.csv",
1919
"data/odoo_branch.xml",
2020
"data/queue_job.xml",
21+
"data/odoo_mca_backend.xml",
22+
"data/ir_config_parameter.xml",
2123
"views/menu.xml",
2224
"views/authentication_token.xml",
2325
"views/ssh_key.xml",
@@ -45,11 +47,14 @@
4547
"base_time_window",
4648
# OCA/queue
4749
"queue_job",
50+
# OCA/connector
51+
"component",
4852
],
4953
"external_dependencies": {
5054
"python": [
5155
"gitpython",
5256
"odoo-addons-parser",
57+
"pyyaml",
5358
# TODO to publish
5459
# "odoo-repository-scanner"
5560
],
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import oca_repository_synchronizer
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Copyright 2026 Sébastien Alix
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
import logging
5+
6+
import requests
7+
import yaml
8+
9+
from odoo import Command
10+
11+
from odoo.addons.component.core import Component
12+
from odoo.addons.queue_job.exception import RetryableJobError
13+
14+
from ..utils import github
15+
16+
_logger = logging.getLogger(__name__)
17+
18+
19+
class OCARepositorySynchronizer(Component):
20+
"""Component for synchronizing OCA repositories from GitHub configuration.
21+
22+
This component is syncing the list of OCA repositories in local database
23+
from https://github.com/OCA/repo-maintainer-conf repository.
24+
It creates new repositories, updates existing ones, and archive those
25+
no longer declared in the OCA configuration.
26+
"""
27+
28+
_name = "oca.repository.synchronizer"
29+
_collection = "odoo.mca.backend"
30+
_usage = "oca.repository.synchronizer"
31+
32+
github_conf_api_url = "repos/OCA/repo-maintainer-conf/contents/conf/repo"
33+
34+
def _get_oca_repo_blacklist(self):
35+
"""Get OCA repository blacklist from configuration parameter.
36+
37+
Returns:
38+
list: List of repository names to skip during synchronization
39+
"""
40+
param_value = (
41+
self.env["ir.config_parameter"]
42+
.sudo()
43+
.get_param("odoo_repository.oca_repo_blacklist", default="")
44+
)
45+
return [repo.strip() for repo in param_value.split(",") if repo.strip()]
46+
47+
def run(self):
48+
"""Execute OCA repository synchronization.
49+
50+
Fetches repository configurations from OCA/repo-maintainer-conf,
51+
parses the YAML files, and synchronizes the local repository database by:
52+
- Creating new repositories that exist in OCA config but not locally
53+
- Updating existing repositories with any changes
54+
- Archiving repositories that no longer exist in OCA config
55+
"""
56+
try:
57+
# Fetch YAML configurations from GitHub
58+
repo_configs = self._fetch_oca_repo_configurations()
59+
# Parse and extract repository data
60+
current_repos = self._parse_oca_repo_configurations(repo_configs)
61+
# Sync repositories (create/update/archive)
62+
stats = self._sync_oca_repositories(current_repos)
63+
_logger.info("OCA repository sync completed: %s", stats)
64+
return stats
65+
except Exception as e:
66+
raise RetryableJobError("Failed to fetch OCA repositories") from e
67+
68+
def _fetch_oca_repo_configurations(self):
69+
"""Fetch OCA repository configurations from GitHub.
70+
71+
Returns:
72+
dict: Repository configurations keyed by filename
73+
"""
74+
try:
75+
# Get list of files in conf/repo directory
76+
files = github.request(self.work.env, self.github_conf_api_url)
77+
repo_configs = {}
78+
for file_info in files:
79+
if file_info["name"].endswith(".yml"):
80+
# Fetch the actual YAML content
81+
download_url = file_info["download_url"]
82+
response = requests.get(download_url, timeout=30)
83+
response.raise_for_status()
84+
repo_configs[file_info["name"]] = response.text
85+
return repo_configs
86+
except Exception as e:
87+
_logger.error("Failed to fetch OCA repo configurations: %s", str(e))
88+
raise
89+
90+
def _parse_oca_repo_configurations(self, repo_configs: dict):
91+
"""Parse YAML configurations and extract OCA repository data.
92+
93+
Args:
94+
repo_configs: Dict of filename -> YAML content
95+
96+
Returns:
97+
dict: Repository data keyed by repository name, only including
98+
repositories with non-empty branches
99+
"""
100+
current_repos = {}
101+
for filename, yaml_content in repo_configs.items():
102+
try:
103+
# Parse YAML content
104+
data = yaml.safe_load(yaml_content)
105+
if not data:
106+
continue
107+
# Process each repository in the YAML file
108+
for repo_name, repo_config in data.items():
109+
# Get blacklist from backend configuration
110+
blacklist = self._get_oca_repo_blacklist()
111+
if repo_name in blacklist:
112+
_logger.info("Skipping import of OCA repository %s", repo_name)
113+
continue
114+
# Skip repositories with default_branch = 'master' or 'main'
115+
# These are not hosting Odoo modules
116+
default_branch = repo_config.get("default_branch", "")
117+
if default_branch in ["master", "main"]:
118+
_logger.info(
119+
"Skipping OCA repository %s "
120+
"(default_branch=%s, not hosting Odoo modules)",
121+
repo_name,
122+
default_branch,
123+
)
124+
continue
125+
# Only include repositories with branches.
126+
# Others repositories are probably not hosting Odoo modules.
127+
branches = repo_config.get("branches", [])
128+
if not branches:
129+
continue
130+
# Construct repository URLs
131+
repo_url = f"https://github.com/OCA/{repo_name}" # noqa: E231
132+
current_repos[repo_name] = {
133+
"name": repo_name,
134+
"repo_url": repo_url,
135+
"clone_url": repo_url,
136+
"repo_type": "github",
137+
}
138+
except yaml.YAMLError as e:
139+
_logger.warning("Failed to parse YAML file %s: %s", filename, str(e))
140+
continue
141+
except Exception as e:
142+
_logger.warning("Error processing file %s: %s", filename, str(e))
143+
continue
144+
return current_repos
145+
146+
def _sync_oca_repositories(self, current_repos):
147+
"""Synchronize OCA repositories with current configuration.
148+
149+
Args:
150+
current_repos: Dict of repository data from OCA configuration
151+
152+
Returns:
153+
dict: Statistics about created/updated/archived repositories
154+
"""
155+
stats = {"created": 0, "updated": 0, "archived": 0}
156+
env = self.work.env
157+
# Get OCA organization
158+
oca_org = env.ref(
159+
"odoo_repository.odoo_repository_org_oca", raise_if_not_found=False
160+
)
161+
if not oca_org:
162+
_logger.warning("OCA organization not found in database")
163+
return stats
164+
# Get existing OCA repositories
165+
odoo_repository = env["odoo.repository"]
166+
existing_repos = odoo_repository.with_context(active_test=False).search(
167+
[("org_id", "=", oca_org.id)]
168+
)
169+
# Process current repositories
170+
for repo_name, repo_data in current_repos.items():
171+
# Check if repository already exists
172+
repo = existing_repos.filtered(
173+
lambda r, repo_name=repo_name: r.name == repo_name
174+
)
175+
if repo:
176+
# Update existing repository
177+
update_vals = self._prepare_oca_repository_update_vals(repo_data)
178+
# Only update if values have changed
179+
if any(repo[field] != update_vals[field] for field in update_vals):
180+
repo.write(update_vals)
181+
stats["updated"] += 1
182+
else:
183+
# Create new repository
184+
create_vals = self._prepare_oca_repository_create_vals(repo_data)
185+
odoo_repository.create(create_vals)
186+
stats["created"] += 1
187+
# Archive repositories that no longer exist in configuration
188+
for repo in existing_repos:
189+
if repo.name not in current_repos and repo.active:
190+
repo.active = repo.to_scan = False
191+
stats["archived"] += 1
192+
return stats
193+
194+
def _prepare_oca_repository_update_vals(self, repo_data):
195+
return {
196+
"repo_url": repo_data["repo_url"],
197+
"clone_url": repo_data["clone_url"],
198+
"repo_type": repo_data["repo_type"],
199+
"active": True,
200+
}
201+
202+
def _prepare_oca_repository_create_vals(self, repo_data):
203+
oca_org = self.work.env.ref("odoo_repository.odoo_repository_org_oca")
204+
addons_path_community = self.work.env.ref(
205+
"odoo_repository.odoo_repository_addons_path_community"
206+
)
207+
return {
208+
"org_id": oca_org.id,
209+
"name": repo_data["name"],
210+
"repo_url": repo_data["repo_url"],
211+
"clone_url": repo_data["clone_url"],
212+
"repo_type": repo_data["repo_type"],
213+
"active": True,
214+
"sequence": 200,
215+
"to_scan": True, # Enable scanning by default
216+
"addons_path_ids": [Command.link(addons_path_community.id)],
217+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!-- Copyright 2026 Sébastien Alix
3+
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
4+
<odoo noupdate="1">
5+
<record id="config_parameter_oca_repo_blacklist" model="ir.config_parameter">
6+
<field name="key">odoo_repository.oca_repo_blacklist</field>
7+
<field name="value">OCB,OpenUpgrade</field>
8+
</record>
9+
</odoo>

odoo_repository/data/ir_cron.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<!-- Copyright 2023 Camptocamp SA
3+
Copyright 2026 Sébastien Alix
34
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
45
<odoo noupdate="1">
56
<record model="ir.cron" id="cron_scanner">
@@ -29,4 +30,18 @@
2930
<field name="state">code</field>
3031
<field name="code">model.cron_fetch_data()</field>
3132
</record>
33+
34+
<record model="ir.cron" id="cron_sync_oca_repositories">
35+
<field name='name'>Odoo Repositories - Sync OCA repositories</field>
36+
<field name='interval_number'>1</field>
37+
<field name='interval_type'>weeks</field>
38+
<field name="active" eval="False" />
39+
<field
40+
name="nextcall"
41+
eval="(datetime.now() + timedelta(days=7)).strftime('%Y-%m-%d 00:00:00')"
42+
/>
43+
<field name="model_id" ref="odoo_repository.model_odoo_repository" />
44+
<field name="state">code</field>
45+
<field name="code">model.cron_sync_oca_repositories()</field>
46+
</record>
3247
</odoo>

0 commit comments

Comments
 (0)