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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ GitHub App), optional gitleaks scanning, and generated `make`/`task`/`just`

## Copier Options

- `copier__ci_provider`: `github` or `gitlab`
- `copier__ci_provider`: `github` or `gitlab` (only asked when repo setup is disabled; otherwise inferred from `copier__repo_provider`)
- `copier__enable_semantic_release`: include release automation
- `copier__github_semantic_release_auth`: `github_token` or `github_app` (GitHub + semantic-release only)
- `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__configure_repo`: enable repository remote setup
- `copier__repo_provider`: `github` or `gitlab`
- `copier__repo_org`: organization/group name
- `copier__repo_name`: repository name
- `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

Expand Down
41 changes: 34 additions & 7 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,51 @@ copier__email:
default: "{{ copier__author_name.lower().replace(' ', '.') }}@example.com"
help: "Author email."

copier__configure_repo:
type: bool
default: true
help: "Configure a git remote for the generated project."

copier__repo_provider:
type: str
default: github
choices:
- github
- gitlab
help: "Repository provider."
when: "{{ copier__configure_repo }}"

copier__repo_org:
type: str
default: "getscaf"
help: "Repository organization/group."
when: "{{ copier__configure_repo }}"

copier__repo_name:
type: str
default: "{{ copier__project_dash }}"
help: "Repository name."
when: "{{ copier__configure_repo }}"

copier__repo_url:
type: str
default: ""
help: "Optional git remote URL for the generated project (e.g. git@github.com:org/repo.git)."
default: "{{ ('git@github.com:' + copier__repo_org + '/' + copier__repo_name + '.git') if (copier__configure_repo and copier__repo_provider == 'github') else (('git@gitlab.com:' + copier__repo_org + '/' + copier__repo_name + '.git') if copier__configure_repo else '') }}"
when: false

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 != '' }}"
help: "Create the repository automatically if it does not exist (gh for GitHub, glab for GitLab)."
when: "{{ copier__configure_repo }}"

copier__repo_visibility:
type: str
default: private
default: public
choices:
- private
- public
help: "Visibility to use when creating the repository."
when: "{{ copier__create_repo }}"
when: "{{ copier__configure_repo and copier__create_repo }}"

copier__version:
type: str
Expand All @@ -78,11 +104,12 @@ copier__version:

copier__ci_provider:
type: str
default: github
default: "{{ copier__repo_provider if copier__configure_repo else 'github' }}"
help: "CI provider to scaffold."
choices:
- github
- gitlab
when: "{{ not copier__configure_repo }}"

copier__enable_semantic_release:
type: bool
Expand Down
58 changes: 23 additions & 35 deletions template/tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import pathlib
import re
import shlex
import shutil
import subprocess
Expand All @@ -9,6 +8,11 @@
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 }}"
CONFIGURE_REPO = {{ "True" if copier__configure_repo else "False" }}
REPO_PROVIDER = "{{ copier__repo_provider }}"
REPO_ORG = "{{ copier__repo_org }}"
REPO_NAME = "{{ copier__repo_name }}"
REPO_URL = "{{ copier__repo_url }}"
CREATE_REPO = {{ "True" if copier__create_repo else "False" }}
REPO_VISIBILITY = "{{ copier__repo_visibility }}"

Expand Down Expand Up @@ -53,7 +57,9 @@ def init_git_repo() -> None:


def configure_git_remote() -> None:
repo_url = "{{ copier__repo_url }}"
if not CONFIGURE_REPO:
return
repo_url = REPO_URL.strip()
if repo_url:
print(INFO + f"repo_url: {repo_url}" + TERMINATOR)
existing_origin = subprocess.run(
Expand All @@ -80,49 +86,30 @@ 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)
if not CONFIGURE_REPO:
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
)
if not REPO_ORG.strip() or not REPO_NAME.strip():
print(WARNING + "Repo org/name not set. Skipping repo creation." + TERMINATOR)
return
host, repo_name = parsed
repo_name = f"{REPO_ORG.strip()}/{REPO_NAME.strip()}"

if CI_PROVIDER == "github":
if REPO_PROVIDER == "github":
if not shutil.which("gh"):
print(WARNING + "gh CLI is not installed. Skipping repo creation." + TERMINATOR)
return

gh_env = os.environ.copy()
gh_env["GH_HOST"] = "github.com"

repo_exists = subprocess.run(
["gh", "repo", "view", repo_name, "--hostname", host],
["gh", "repo", "view", repo_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=gh_env,
)
if repo_exists.returncode == 0:
print(INFO + f"GitHub repository {repo_name} already exists." + TERMINATOR)
Expand All @@ -134,9 +121,10 @@ def maybe_create_repo() -> None:
+ TERMINATOR
)
create_repo = subprocess.run(
["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}", "--hostname", host],
["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"],
capture_output=True,
text=True,
env=gh_env,
)
if create_repo.returncode != 0:
error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error"
Expand All @@ -149,13 +137,13 @@ def maybe_create_repo() -> None:
print(SUCCESS + f"GitHub repository {repo_name} created." + TERMINATOR)
return

if CI_PROVIDER == "gitlab":
if REPO_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
glab_env["GITLAB_HOST"] = "gitlab.com"
repo_exists = subprocess.run(
["glab", "repo", "view", repo_name],
stdout=subprocess.DEVNULL,
Expand Down Expand Up @@ -190,7 +178,7 @@ def maybe_create_repo() -> None:

print(
WARNING
+ f"Repo creation not implemented for provider '{CI_PROVIDER}'. Skipping."
+ f"Repo creation not implemented for provider '{REPO_PROVIDER}'. Skipping."
+ TERMINATOR
)

Expand Down