Skip to content

Commit cacb810

Browse files
Merge pull request #2691 from ashwindasr/add-mr-approvers-support
Add MR approval rules automation from group.yml mr_approvers
2 parents de37e29 + 5727840 commit cacb810

File tree

4 files changed

+397
-2
lines changed

4 files changed

+397
-2
lines changed

artcommon/artcommonlib/gitlab.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,69 @@ async def set_mr_ready(self, mr_url: str):
107107
logger.info("MR is already ready (no draft prefix found)")
108108
return mr
109109

110+
def _resolve_user_ids(self, usernames: list[str]) -> list[int]:
111+
"""
112+
Resolve GitLab usernames to user IDs.
113+
114+
Arg(s):
115+
usernames: List of GitLab usernames
116+
Return Value(s):
117+
List of resolved user IDs (skips unresolved usernames with a warning)
118+
"""
119+
user_ids = []
120+
for username in usernames:
121+
users = self._client.users.list(username=username)
122+
if users:
123+
user_ids.append(users[0].id)
124+
else:
125+
logger.warning(f"Could not resolve GitLab username: {username}")
126+
return user_ids
127+
128+
async def set_mr_approval_rules(self, mr_url: str, approvers_config: dict[str, list[str]]):
129+
"""
130+
Configure MR-level approval rules based on group.yml mr_approvers config.
131+
Keeps the "ART" rule, removes all other inherited rules, and creates new
132+
rules from approvers_config.
133+
134+
Arg(s):
135+
mr_url: Full URL to the merge request
136+
approvers_config: Dict mapping approval group names to lists of GitLab usernames,
137+
e.g. {"QE": ["user1", "user2"]}
138+
"""
139+
if not mr_url or not approvers_config:
140+
return
141+
142+
if self.dry_run:
143+
for name, usernames in approvers_config.items():
144+
logger.info(f"[DRY-RUN] Would create approval rule '{name}' with users: {usernames}")
145+
return
146+
147+
mr = self.get_mr_from_url(mr_url)
148+
if not mr:
149+
logger.error(f"Could not retrieve MR from URL: {mr_url}")
150+
return
151+
152+
existing_rules = mr.approval_rules.list()
153+
154+
for rule in existing_rules:
155+
if rule.name != "ART":
156+
logger.info(f"Deleting approval rule '{rule.name}' (id={rule.id})")
157+
rule.delete()
158+
159+
for name, usernames in approvers_config.items():
160+
user_ids = self._resolve_user_ids(usernames)
161+
if not user_ids:
162+
logger.warning(f"No valid user IDs resolved for approval rule '{name}', skipping")
163+
continue
164+
mr.approval_rules.create(
165+
{
166+
"name": name,
167+
"approvals_required": 1,
168+
"user_ids": user_ids,
169+
}
170+
)
171+
logger.info(f"Created approval rule '{name}' with users: {usernames} (ids: {user_ids})")
172+
110173
async def trigger_ci_pipeline(self, mr) -> str | None:
111174
"""
112175
Trigger a GitLab Merge Request pipeline using the MR API.

ocp-build-data-validator/validator/json_schemas/assembly_group_config.schema.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,14 @@
558558
"csv_namespace": {"type": "string"},
559559
"software_lifecycle": {"type": "object"},
560560
"external_scanners": {"type": "object"},
561-
"reconciliation_prs": {"type": "object"}
561+
"reconciliation_prs": {"type": "object"},
562+
"mr_approvers": {
563+
"type": "object",
564+
"additionalProperties": {
565+
"type": "array",
566+
"items": {"type": "string"}
567+
}
568+
}
562569
},
563570
"additionalProperties": false
564571
}

pyartcd/pyartcd/pipelines/release_from_fbc.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,26 @@ async def _load_product_from_group_config(self) -> str:
249249
self.logger.info(f"Using product extracted from group name: {product}")
250250
return product
251251

252+
async def _load_mr_approvers_from_group_config(self) -> dict[str, list[str]]:
253+
"""
254+
Load the mr_approvers field from group configuration using doozer command.
255+
Returns a dict mapping approval group names to lists of GitLab usernames,
256+
e.g. {"QE": ["user1", "user2"]}. Returns empty dict if not configured.
257+
"""
258+
try:
259+
cmd = ['doozer', f'--group={self.group}', 'config:read-group', 'mr_approvers']
260+
_, output, _ = await exectools.cmd_gather_async(cmd)
261+
output = output.strip()
262+
if output and output not in ('None', 'null'):
263+
parsed = stdlib_yaml.safe_load(output)
264+
if not isinstance(parsed, dict):
265+
self.logger.warning("mr_approvers is not a dict (got %s), ignoring", type(parsed).__name__)
266+
return {}
267+
return parsed
268+
except Exception as e:
269+
self.logger.warning(f"Failed to load mr_approvers from group config: {e}")
270+
return {}
271+
252272
async def extract_fbc_labels(self, fbc_pullspec: str) -> Dict[str, Optional[str]]:
253273
"""
254274
Extract both the NVR and __doozer_key labels from the FBC image.
@@ -602,6 +622,17 @@ async def create_shipment_mr(self, shipments_by_kind: Dict[str, ShipmentConfig],
602622
mr_url = mr.web_url
603623
self.logger.info("Created Merge Request: %s", mr_url)
604624

625+
# Configure approval rules from group.yml if defined
626+
approvers_config = await self._load_mr_approvers_from_group_config()
627+
if approvers_config:
628+
if self.dry_run:
629+
self.logger.info("[DRY-RUN] Would set MR approval rules: %s", approvers_config)
630+
else:
631+
try:
632+
await self._gitlab.set_mr_approval_rules(mr_url, approvers_config)
633+
except Exception as e:
634+
self.logger.warning(f"Failed to set MR approval rules: {e}")
635+
605636
# Store the MR URL for later use
606637
self.shipment_mr_url = mr_url
607638
return mr_url

0 commit comments

Comments
 (0)