Skip to content

Commit 8a9d372

Browse files
authored
feat(template): add provider-aware repo auto-creation (#2)
1 parent 7384d67 commit 8a9d372

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ GitHub App), optional gitleaks scanning, and generated `make`/`task`/`just`
3030
- `copier__enable_secret_scanning`: include gitleaks CI
3131
- `copier__task_runner`: `make`, `task`, or `just`
3232
- `copier__repo_url`: optional remote URL to configure as `origin` during generation
33+
- `copier__create_repo`: create provider repo automatically when missing (`gh` for GitHub, `glab` for GitLab)
34+
- `copier__repo_visibility`: `private` or `public` for automatic repo creation
3335

3436
## Quick Start
3537

copier.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ copier__repo_url:
5656
default: ""
5757
help: "Optional git remote URL for the generated project (e.g. git@github.com:org/repo.git)."
5858

59+
copier__create_repo:
60+
type: bool
61+
default: true
62+
help: "Create the selected provider repository automatically if it does not exist (gh for GitHub, glab for GitLab)."
63+
when: "{{ copier__repo_url | trim != '' }}"
64+
65+
copier__repo_visibility:
66+
type: str
67+
default: private
68+
choices:
69+
- private
70+
- public
71+
help: "Visibility to use when creating the repository."
72+
when: "{{ copier__create_repo }}"
73+
5974
copier__version:
6075
type: str
6176
default: "0.1.0"

template/tasks.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import os
22
import pathlib
3+
import re
34
import shlex
5+
import shutil
46
import subprocess
57

68
CI_PROVIDER = "{{ copier__ci_provider }}"
79
SEMANTIC_RELEASE = {{ "True" if copier__enable_semantic_release else "False" }}
810
SECRET_SCANNING = {{ "True" if copier__enable_secret_scanning else "False" }}
911
TASK_RUNNER = "{{ copier__task_runner }}"
12+
CREATE_REPO = {{ "True" if copier__create_repo else "False" }}
13+
REPO_VISIBILITY = "{{ copier__repo_visibility }}"
1014

1115
ROOT = pathlib.Path(".")
1216
TERMINATOR = "\x1b[0m"
@@ -76,8 +80,124 @@ def configure_git_remote() -> None:
7680
)
7781

7882

83+
def parse_repo_url(repo_url: str) -> tuple[str, str] | None:
84+
patterns = [
85+
r"^git@(?P<host>[^:]+):(?P<path>[^ ]+?)(?:\.git)?$",
86+
r"^https?://(?P<host>[^/]+)/(?P<path>[^ ]+?)(?:\.git)?/?$",
87+
]
88+
for pattern in patterns:
89+
match = re.match(pattern, repo_url)
90+
if match:
91+
host = match.group("host")
92+
path = match.group("path").strip("/")
93+
if path.count("/") >= 1:
94+
return (host, path)
95+
return None
96+
97+
98+
def maybe_create_repo() -> None:
99+
if not CREATE_REPO:
100+
return
101+
102+
repo_url = "{{ copier__repo_url }}".strip()
103+
if not repo_url:
104+
print(WARNING + "Repo creation requested but no repo_url was provided." + TERMINATOR)
105+
return
106+
107+
parsed = parse_repo_url(repo_url)
108+
if not parsed:
109+
print(
110+
WARNING
111+
+ f"Repo URL is not in a supported SSH/HTTPS format ({repo_url}). Skipping repo creation."
112+
+ TERMINATOR
113+
)
114+
return
115+
host, repo_name = parsed
116+
117+
if CI_PROVIDER == "github":
118+
if not shutil.which("gh"):
119+
print(WARNING + "gh CLI is not installed. Skipping repo creation." + TERMINATOR)
120+
return
121+
122+
repo_exists = subprocess.run(
123+
["gh", "repo", "view", repo_name, "--hostname", host],
124+
stdout=subprocess.DEVNULL,
125+
stderr=subprocess.DEVNULL,
126+
)
127+
if repo_exists.returncode == 0:
128+
print(INFO + f"GitHub repository {repo_name} already exists." + TERMINATOR)
129+
return
130+
131+
print(
132+
INFO
133+
+ f"Creating GitHub repository {repo_name} ({REPO_VISIBILITY})..."
134+
+ TERMINATOR
135+
)
136+
create_repo = subprocess.run(
137+
["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}", "--hostname", host],
138+
capture_output=True,
139+
text=True,
140+
)
141+
if create_repo.returncode != 0:
142+
error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error"
143+
print(
144+
WARNING
145+
+ f"Failed to create GitHub repository {repo_name}: {error}"
146+
+ TERMINATOR
147+
)
148+
return
149+
print(SUCCESS + f"GitHub repository {repo_name} created." + TERMINATOR)
150+
return
151+
152+
if CI_PROVIDER == "gitlab":
153+
if not shutil.which("glab"):
154+
print(WARNING + "glab CLI is not installed. Skipping repo creation." + TERMINATOR)
155+
return
156+
157+
glab_env = os.environ.copy()
158+
glab_env["GITLAB_HOST"] = host
159+
repo_exists = subprocess.run(
160+
["glab", "repo", "view", repo_name],
161+
stdout=subprocess.DEVNULL,
162+
stderr=subprocess.DEVNULL,
163+
env=glab_env,
164+
)
165+
if repo_exists.returncode == 0:
166+
print(INFO + f"GitLab repository {repo_name} already exists." + TERMINATOR)
167+
return
168+
169+
print(
170+
INFO
171+
+ f"Creating GitLab repository {repo_name} ({REPO_VISIBILITY})..."
172+
+ TERMINATOR
173+
)
174+
create_repo = subprocess.run(
175+
["glab", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"],
176+
capture_output=True,
177+
text=True,
178+
env=glab_env,
179+
)
180+
if create_repo.returncode != 0:
181+
error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error"
182+
print(
183+
WARNING
184+
+ f"Failed to create GitLab repository {repo_name}: {error}"
185+
+ TERMINATOR
186+
)
187+
return
188+
print(SUCCESS + f"GitLab repository {repo_name} created." + TERMINATOR)
189+
return
190+
191+
print(
192+
WARNING
193+
+ f"Repo creation not implemented for provider '{CI_PROVIDER}'. Skipping."
194+
+ TERMINATOR
195+
)
196+
197+
79198
def main() -> None:
80199
init_git_repo()
200+
maybe_create_repo()
81201
configure_git_remote()
82202
run_init_script()
83203

0 commit comments

Comments
 (0)