Task: gitlab.create-repo
Goal: Start a private project project_site with NodeJS template and add primer, convexegg, vinta as members
Task configuration:
account_list: primer, convexegg, vinta
project_name: project_site
project_template: NodeJS
visibility_level: private

Validation code:
from typing import Any, Dict, List
from urllib.parse import quote

import requests
import json
import os

def _get_headers():
    return {
        "Content-Type": "application/json",
        "Accept": "application/json",
        **json.loads(os.getenv("GITLAB_EXTRA_HTTP_HEADERS", "{}")),
    }

def _find_project(config):
    """Find the project, handling cases where namespace may be missing."""
    base_url = os.getenv("GITLAB_BASE_URL", "").rstrip("/")

    if "namespace" in config:
        # Use namespace/project_name to find the project
        project_id = quote(f"{config['namespace']}/{config['project_name']}", safe="")
        url = base_url + "/api/v4/projects/" + project_id
        response = requests.get(url=url, headers=_get_headers())
        if response.status_code == 200:
            return response.json()

    # Fallback: search for the project by name, owned by the current user
    url = base_url + "/api/v4/projects"
    response = requests.get(
        url=url,
        headers=_get_headers(),
        params={"search": config["project_name"], "owned": True},
    )
    if response.status_code != 200:
        return None

    for project in response.json():
        if project["name"] == config["project_name"]:
            return project

    return None

def validate(messages: List[Dict[str, str]], config: Dict[str, Any]) -> int:

    # Check whether we have the necessary information to validate
    if "project_name" not in config:
        raise ValueError("Target must contain 'project_name'")

    # check whether we have messages
    if not messages:
        raise ValueError("Messages are empty")

    result = _find_project(config)
    if result is None:
        return 0

    project_id = str(result["id"])

    if "visibility_level" in config:
        if result["visibility"] != config["visibility_level"]:
            return 0

    if "project_template" in config:
        template = config["project_template"].lower().strip()
        is_blank = template in ["blank", "empty"]

        if is_blank:
            # Blank template: repo should be empty
            if not result.get("empty_repo", True):
                return 0
        else:
            # Non-blank template: GitLab built-in template READMEs contain
            # "template project" and a link to gitlab-org/project-templates.
            # Map config template names to the keyword in the README header.
            template_readme_keywords = {
                "nodejs": "node express",
                "node": "node express",
                "android": "android",
                "html": "pages/plain html",
                "jekyll": "jekyll",
                "rails": "ruby on rails",
                "spring": "spring",
                "gatsby": "gatsby",
                "hugo": "hugo",
                "gitbook": "gitbook",
            }
            keyword = template_readme_keywords.get(template, template)

            base_url = os.getenv("GITLAB_BASE_URL", "").rstrip("/")
            readme_found = False
            for ref in [result.get("default_branch", "main"), "main", "master"]:
                url = (
                    base_url + "/api/v4/projects/" + project_id
                    + "/repository/files/README.md/raw?ref=" + quote(ref, safe="")
                )
                resp = requests.get(url=url, headers=_get_headers())
                if resp.status_code == 200:
                    readme_content = resp.text.lower()
                    if keyword in readme_content:
                        readme_found = True
                    break
            if not readme_found:
                return 0

    if "account_list" in config:
        config_names = [
            name.lower().strip() for name in config["account_list"].split(",")
        ]

        # get all members
        url = (
            os.getenv("GITLAB_BASE_URL", "").rstrip("/") + "/api/v4/projects/" + project_id + "/members?per_page=100"
        )
        response = requests.get(url=url, headers=_get_headers())
        response.raise_for_status()
        result = response.json()
        member_names = [member["name"].lower() for member in result]
        member_names += [member["username"].lower() for member in result]

        for config_name in config_names:
            # check if the config name is in the member names
            # we only check whether config name is a substring of member names
            # because we do not want to be too strict
            found = False
            for member_name in member_names:
                if config_name in member_name:
                    found = True
                    break
            if not found:
                return 0

    return 1