Task: gitlab.star-fork-create-milestone-issues
Goal: Star the 2019-nCov repo and fork it/them. Create a "practice of collective code review" milestone starting on 10-31-2025 and ending 12-31-2025. Then create three issues in the forked repo(s) with the titles, "review Georgie's code", "review Mona's code" and "review Lewis's code" and assign them to the milestone.
Task configuration:
repo: the 2019-nCov
original_namespace: yljou
namespace: byteblaze
project_name: 2019-nCov
event: practice of collective code review
search_keywords: collective code review
text_start_date: 10-31-2025
start_date: 2025-10-31
text_end_date: 12-31-2025
end_date: 2025-12-31
issue_title_1: review Georgie's code
issue_title_2: review Mona's code
issue_title_3: review Lewis's code

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 validate(messages: List[Dict[str, str]], config: Dict[str, Any]) -> int:

    if not messages:
        return 0

    base_url = os.getenv("GITLAB_BASE_URL", "").rstrip("/")

    # -- Star repos --
    # Check that the repo is starred. Use project_name for matching since
    # config["repo"] may contain descriptive text like "the 2019-nCov"
    if "project_name" not in config:
        raise ValueError("Target must contain 'project_name'")

    starred_url = base_url + "/api/v4/projects?starred=true&per_page=100"
    response = requests.get(url=starred_url, headers=_get_headers())
    if response.status_code != 200:
        return 0
    result = response.json()

    repo_names = [repo["name"].strip().lower() for repo in result]
    if config["project_name"].strip().lower() not in repo_names:
        return 0

    # -- Fork repos --
    if (
        "original_namespace" not in config
        or "namespace" not in config
    ):
        raise ValueError(
            "Target must contain 'original_namespace' and 'namespace'"
        )

    project_id = quote(
        f"{config['original_namespace']}/{config['project_name']}", safe=""
    ).replace(".", "%2E")
    forks_url = base_url + "/api/v4/projects/" + project_id + "/forks"
    response = requests.get(url=forks_url, headers=_get_headers())
    if response.status_code != 200:
        return 0
    result = response.json()
    found = False
    for repo in result:
        repo_ns = repo.get("namespace", {})
        ns_path = repo_ns.get("path", "") if isinstance(repo_ns, dict) else ""
        if (
            ns_path.lower() == config["namespace"].lower()
            and repo["name"].lower() == config["project_name"].lower()
        ):
            found = True
            break
    if not found:
        return 0

    # -- Create Milestone --
    if (
        "search_keywords" not in config
        or "start_date" not in config
        or "end_date" not in config
    ):
        raise ValueError(
            "Target must contain 'search_keywords', 'start_date', and 'end_date'"
        )

    # Search for the milestone in the forked repo (under the user's namespace)
    forked_project_id = quote(
        f"{config['namespace']}/{config['project_name']}", safe=""
    ).replace(".", "%2E")
    milestones_url = (
        base_url
        + "/api/v4/projects/"
        + forked_project_id
        + "/milestones"
        + "?search="
        + quote(config["search_keywords"], safe="")
    )
    response = requests.get(url=milestones_url, headers=_get_headers())
    if response.status_code != 200:
        return 0
    result = response.json()
    if len(result) == 0:
        return 0
    milestone = result[0]

    if config.get("start_date"):
        if milestone["start_date"] != config["start_date"]:
            return 0

    if config.get("end_date"):
        if milestone["due_date"] != config["end_date"]:
            return 0

    # -- Create Issues --
    issues_url = base_url + "/api/v4/projects/" + forked_project_id + "/issues"
    response = requests.get(url=issues_url, headers=_get_headers())
    response.raise_for_status()
    result = response.json()
    if len(result) == 0:
        return 0

    for issue_title in [
        config["issue_title_1"],
        config["issue_title_2"],
        config["issue_title_3"],
    ]:
        found = False
        for issue in result:
            if issue["title"] == issue_title:
                found = True
                break
        if not found:
            return 0

    return 1