diff --git a/README.md b/README.md index ebdd2e9..968c995 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ GitHub App), optional gitleaks scanning, and generated `make`/`task`/`just` - `copier__enable_secret_scanning`: include gitleaks CI - `copier__task_runner`: `make`, `task`, or `just` - `copier__repo_url`: optional remote URL to configure as `origin` during generation +- `copier__create_repo`: create provider repo automatically when missing (`gh` for GitHub, `glab` for GitLab) +- `copier__repo_visibility`: `private` or `public` for automatic repo creation ## Quick Start diff --git a/copier.yml b/copier.yml index 10ac0b8..1b5ca27 100644 --- a/copier.yml +++ b/copier.yml @@ -56,6 +56,21 @@ copier__repo_url: default: "" help: "Optional git remote URL for the generated project (e.g. git@github.com:org/repo.git)." +copier__create_repo: + type: bool + default: true + help: "Create the selected provider repository automatically if it does not exist (gh for GitHub, glab for GitLab)." + when: "{{ copier__repo_url | trim != '' }}" + +copier__repo_visibility: + type: str + default: private + choices: + - private + - public + help: "Visibility to use when creating the repository." + when: "{{ copier__create_repo }}" + copier__version: type: str default: "0.1.0" diff --git a/template/tasks.py b/template/tasks.py index bce7908..9eee064 100644 --- a/template/tasks.py +++ b/template/tasks.py @@ -1,12 +1,16 @@ import os import pathlib +import re import shlex +import shutil import subprocess CI_PROVIDER = "{{ copier__ci_provider }}" SEMANTIC_RELEASE = {{ "True" if copier__enable_semantic_release else "False" }} SECRET_SCANNING = {{ "True" if copier__enable_secret_scanning else "False" }} TASK_RUNNER = "{{ copier__task_runner }}" +CREATE_REPO = {{ "True" if copier__create_repo else "False" }} +REPO_VISIBILITY = "{{ copier__repo_visibility }}" ROOT = pathlib.Path(".") TERMINATOR = "\x1b[0m" @@ -76,8 +80,124 @@ def configure_git_remote() -> None: ) +def parse_repo_url(repo_url: str) -> tuple[str, str] | None: + patterns = [ + r"^git@(?P[^:]+):(?P[^ ]+?)(?:\.git)?$", + r"^https?://(?P[^/]+)/(?P[^ ]+?)(?:\.git)?/?$", + ] + for pattern in patterns: + match = re.match(pattern, repo_url) + if match: + host = match.group("host") + path = match.group("path").strip("/") + if path.count("/") >= 1: + return (host, path) + return None + + +def maybe_create_repo() -> None: + if not CREATE_REPO: + return + + repo_url = "{{ copier__repo_url }}".strip() + if not repo_url: + print(WARNING + "Repo creation requested but no repo_url was provided." + TERMINATOR) + return + + parsed = parse_repo_url(repo_url) + if not parsed: + print( + WARNING + + f"Repo URL is not in a supported SSH/HTTPS format ({repo_url}). Skipping repo creation." + + TERMINATOR + ) + return + host, repo_name = parsed + + if CI_PROVIDER == "github": + if not shutil.which("gh"): + print(WARNING + "gh CLI is not installed. Skipping repo creation." + TERMINATOR) + return + + repo_exists = subprocess.run( + ["gh", "repo", "view", repo_name, "--hostname", host], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if repo_exists.returncode == 0: + print(INFO + f"GitHub repository {repo_name} already exists." + TERMINATOR) + return + + print( + INFO + + f"Creating GitHub repository {repo_name} ({REPO_VISIBILITY})..." + + TERMINATOR + ) + create_repo = subprocess.run( + ["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}", "--hostname", host], + capture_output=True, + text=True, + ) + if create_repo.returncode != 0: + error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error" + print( + WARNING + + f"Failed to create GitHub repository {repo_name}: {error}" + + TERMINATOR + ) + return + print(SUCCESS + f"GitHub repository {repo_name} created." + TERMINATOR) + return + + if CI_PROVIDER == "gitlab": + if not shutil.which("glab"): + print(WARNING + "glab CLI is not installed. Skipping repo creation." + TERMINATOR) + return + + glab_env = os.environ.copy() + glab_env["GITLAB_HOST"] = host + repo_exists = subprocess.run( + ["glab", "repo", "view", repo_name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=glab_env, + ) + if repo_exists.returncode == 0: + print(INFO + f"GitLab repository {repo_name} already exists." + TERMINATOR) + return + + print( + INFO + + f"Creating GitLab repository {repo_name} ({REPO_VISIBILITY})..." + + TERMINATOR + ) + create_repo = subprocess.run( + ["glab", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], + capture_output=True, + text=True, + env=glab_env, + ) + if create_repo.returncode != 0: + error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error" + print( + WARNING + + f"Failed to create GitLab repository {repo_name}: {error}" + + TERMINATOR + ) + return + print(SUCCESS + f"GitLab repository {repo_name} created." + TERMINATOR) + return + + print( + WARNING + + f"Repo creation not implemented for provider '{CI_PROVIDER}'. Skipping." + + TERMINATOR + ) + + def main() -> None: init_git_repo() + maybe_create_repo() configure_git_remote() run_init_script()