Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
120 changes: 120 additions & 0 deletions template/tasks.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -76,8 +80,124 @@ def configure_git_remote() -> None:
)


def parse_repo_url(repo_url: str) -> tuple[str, str] | None:
patterns = [
r"^git@(?P<host>[^:]+):(?P<path>[^ ]+?)(?:\.git)?$",
r"^https?://(?P<host>[^/]+)/(?P<path>[^ ]+?)(?:\.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()

Expand Down