Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions artcommon/artcommonlib/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,71 @@ async def set_mr_ready(self, mr_url: str):
logger.info("MR is already ready (no draft prefix found)")
return mr

def _resolve_user_ids(self, usernames: list[str]) -> list[int]:
"""
Resolve GitLab usernames to user IDs.

Arg(s):
usernames: List of GitLab usernames
Return Value(s):
List of resolved user IDs (skips unresolved usernames with a warning)
"""
user_ids = []
for username in usernames:
users = self._client.users.list(username=username)
if users:
user_ids.append(users[0].id)
else:
logger.warning(f"Could not resolve GitLab username: {username}")
return user_ids

async def set_mr_approval_rules(self, mr_url: str, approvers_config: dict[str, list[str]]):
"""
Configure MR-level approval rules based on group.yml mr_approvers config.
Keeps the "ART" rule, removes all other inherited rules, and creates new
rules from approvers_config.

Arg(s):
mr_url: Full URL to the merge request
approvers_config: Dict mapping approval group names to lists of GitLab usernames,
e.g. {"QE": ["user1", "user2"]}
"""
if not mr_url or not approvers_config:
return

mr = self.get_mr_from_url(mr_url)
if not mr:
logger.error(f"Could not retrieve MR from URL: {mr_url}")
return

existing_rules = mr.approval_rules.list()

if self.dry_run:
rules_to_delete = [r for r in existing_rules if r.name != "ART"]
logger.info(f"[DRY-RUN] Would delete {len(rules_to_delete)} non-ART approval rules")
for name, usernames in approvers_config.items():
logger.info(f"[DRY-RUN] Would create approval rule '{name}' with users: {usernames}")
return

for rule in existing_rules:
if rule.name != "ART":
logger.info(f"Deleting approval rule '{rule.name}' (id={rule.id})")
rule.delete()

for name, usernames in approvers_config.items():
user_ids = self._resolve_user_ids(usernames)
if not user_ids:
logger.warning(f"No valid user IDs resolved for approval rule '{name}', skipping")
continue
mr.approval_rules.create(
{
"name": name,
"approvals_required": 1,
"user_ids": user_ids,
}
)
logger.info(f"Created approval rule '{name}' with users: {usernames} (ids: {user_ids})")

async def trigger_ci_pipeline(self, mr) -> str | None:
"""
Trigger a GitLab Merge Request pipeline using the MR API.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,8 @@
"csv_namespace": {"type": "string"},
"software_lifecycle": {"type": "object"},
"external_scanners": {"type": "object"},
"reconciliation_prs": {"type": "object"}
"reconciliation_prs": {"type": "object"},
"mr_approvers": {"type": "object"}
},
"additionalProperties": false
}
31 changes: 31 additions & 0 deletions pyartcd/pyartcd/pipelines/release_from_fbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,26 @@ async def _load_product_from_group_config(self) -> str:
self.logger.info(f"Using product extracted from group name: {product}")
return product

async def _load_mr_approvers_from_group_config(self) -> dict:
"""
Load the mr_approvers field from group configuration using doozer command.
Returns a dict mapping approval group names to lists of GitLab usernames,
e.g. {"QE": ["user1", "user2"]}. Returns empty dict if not configured.
"""
try:
cmd = ['doozer', f'--group={self.group}', 'config:read-group', 'mr_approvers']
_, output, _ = await exectools.cmd_gather_async(cmd)
output = output.strip()
if output and output not in ('None', 'null'):
parsed = stdlib_yaml.safe_load(output)
if not isinstance(parsed, dict):
self.logger.warning("mr_approvers is not a dict (got %s), ignoring", type(parsed).__name__)
return {}
return parsed
except Exception as e:
self.logger.warning(f"Failed to load mr_approvers from group config: {e}")
return {}

async def extract_fbc_labels(self, fbc_pullspec: str) -> Dict[str, Optional[str]]:
"""
Extract both the NVR and __doozer_key labels from the FBC image.
Expand Down Expand Up @@ -602,6 +622,17 @@ async def create_shipment_mr(self, shipments_by_kind: Dict[str, ShipmentConfig],
mr_url = mr.web_url
self.logger.info("Created Merge Request: %s", mr_url)

# Configure approval rules from group.yml if defined
approvers_config = await self._load_mr_approvers_from_group_config()
if approvers_config:
if self.dry_run:
self.logger.info("[DRY-RUN] Would set MR approval rules: %s", approvers_config)
else:
try:
await self._gitlab.set_mr_approval_rules(mr_url, approvers_config)
except Exception as e:
self.logger.warning(f"Failed to set MR approval rules: {e}")

# Store the MR URL for later use
self.shipment_mr_url = mr_url
return mr_url
Expand Down
Loading