Compare commits
13
Commits
79cb646dd3
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8ddc99ef | ||
|
|
2b332da0a2 | ||
|
|
3d85e6f29c | ||
|
|
44ce6c04fa | ||
|
|
048f29f21e | ||
|
|
68034cd2f5 | ||
|
|
81d518f2c8 | ||
|
|
2785443794 | ||
|
|
d7e948df33 | ||
|
|
454be36bec | ||
|
|
e0550cb462 | ||
|
|
1f663c8e51 | ||
|
|
7ff97419e3 |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# newproject
|
# newproject
|
||||||
|
|
||||||
Composes a project from the templates in this organization: `infra` at the root, then `backend-python`, `frontend-svelte` and `caddy` on top, each with its own answers file in `.copier/`.
|
Composes a project from the templates in this organization: `infra` at the root, then `backend-python`, `frontend-svelte`, `blank` and `caddy` on top, each with its own answers file in `.copier/`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
source ~/projects/templates/newproject/shell.sh
|
source ~/projects/templates/newproject/shell.sh
|
||||||
@@ -9,12 +9,41 @@ updateproject # every part
|
|||||||
updateproject backend # one part
|
updateproject backend # one part
|
||||||
```
|
```
|
||||||
|
|
||||||
`newproject` asks for the parts, the backend language, the frontend framework and how the frontend is served, then hands over to each template's own questions. Every prompt has a flag for non-interactive use:
|
`newproject` asks for the description, the parts, the backend language, the frontend framework, how the frontend is served and the MCP servers for `.mcp.json`, then hands over to each template's own questions, and finally offers to create the Gitea repo. Every prompt has a flag for non-interactive use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
newproject ~/projects/my-thing --parts backend,frontend,caddy --backend python --frontend svelte --serve bun --defaults
|
newproject ~/projects/my-thing --parts backend,frontend,caddy --backend python --frontend svelte --serve bun --mcp context7,svelte --defaults
|
||||||
```
|
```
|
||||||
|
|
||||||
One part alone is rendered at the root of the project; two parts live in `backend/` and `frontend/`. Templates are cloned from `https://git.kotikot.com/templates` unless `TEMPLATES_DIR` points at a directory with local checkouts; `TEMPLATES_REF=HEAD` renders their working trees instead of the latest tag.
|
`other` as the backend or the frontend renders the `blank` template: the part's compose files, Makefile and `.env.example` with no code, for a stack that has no template of its own, next to any other part.
|
||||||
|
|
||||||
|
One part alone is rendered at the root of the project; two parts live in `backend/` and `frontend/`. After rendering, `make env` creates `.env` and every `docker-compose.override.yml` from their examples. `updateproject` needs a clean working tree and commits each part's update separately, refreshing `uv.lock` and `bun.lock` first, so the next part starts from a clean tree again. When copier leaves conflicts it stops before committing and lists the files: resolve, commit, run it again. Templates are cloned from `https://git.kotikot.com/templates` unless `TEMPLATES_DIR` points at a directory with local checkouts; `TEMPLATES_REF=HEAD` renders their working trees instead of the latest tag.
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
|
||||||
|
```sh
|
||||||
|
giteaproject # asks for the organization and the name
|
||||||
|
giteaproject ms-agents/my-thing
|
||||||
|
```
|
||||||
|
|
||||||
|
Run in the project directory. It creates the repo if it does not exist (private, `main` as the default branch, the description from `.copier/infra.yml`), adds it as `origin` over ssh, and when the repo is still empty and there are local commits, pushes `main` and `dev` and switches to `dev`. An existing `origin` that points elsewhere is left alone. `newproject` asks for the organization at the end, or takes `--repo org/name`; without a terminal and without `--repo` it creates nothing.
|
||||||
|
|
||||||
|
The organization defaults to `org` from the config, else to the name of the parent directory when it matches an organization. `~/.config/templates/config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
gitea_url = "https://git.kotikot.com"
|
||||||
|
remote = "ssh"
|
||||||
|
private = true
|
||||||
|
org = "ms-agents"
|
||||||
|
```
|
||||||
|
|
||||||
|
The token comes from `GITEA_TOKEN` or `~/.config/gitea/token`.
|
||||||
|
|
||||||
|
The same command prepares the repo for Renovate: the `renovate`, `wait-3d` and `deps-code` labels (Renovate fails on a label that does not exist), write access for the organization's `renovate` team, and, for an existing repo without one, a `renovate.jsonc` committed on the server that extends the shared preset in `templates/renovate`. Projects rendered by `newproject` carry that file from the `infra` template already.
|
||||||
|
|
||||||
|
**Extending the preset does not subscribe the repo to anything.** The bot walks
|
||||||
|
an explicit list, so the repo also has to be added to `repositories` in
|
||||||
|
`projects/personal/renovate/renovate-config.js` of `infra/komodo`. Miss that step
|
||||||
|
and the config sits there doing nothing, silently.
|
||||||
|
|
||||||
Requires `uv`; `bun`, `docker` and `pre-commit` are used by the generated projects.
|
Requires `uv`; `bun`, `docker` and `pre-commit` are used by the generated projects.
|
||||||
@@ -6,3 +6,4 @@ fi
|
|||||||
|
|
||||||
newproject() { uv run --script "$TEMPLATES_HOME/templates.py" new "$@"; }
|
newproject() { uv run --script "$TEMPLATES_HOME/templates.py" new "$@"; }
|
||||||
updateproject() { uv run --script "$TEMPLATES_HOME/templates.py" update "$@"; }
|
updateproject() { uv run --script "$TEMPLATES_HOME/templates.py" update "$@"; }
|
||||||
|
giteaproject() { uv run --script "$TEMPLATES_HOME/templates.py" gitea "$@"; }
|
||||||
+250
-10
@@ -14,9 +14,11 @@ from copier import run_copy, run_update
|
|||||||
|
|
||||||
SOURCE = os.environ.get("TEMPLATES_DIR", "https://git.kotikot.com/templates")
|
SOURCE = os.environ.get("TEMPLATES_DIR", "https://git.kotikot.com/templates")
|
||||||
REF = os.environ.get("TEMPLATES_REF")
|
REF = os.environ.get("TEMPLATES_REF")
|
||||||
BACKENDS = {"python": "backend-python"}
|
BLANK = "blank"
|
||||||
FRONTENDS = {"svelte": "frontend-svelte"}
|
BACKENDS = {"python": "backend-python", "other": BLANK}
|
||||||
|
FRONTENDS = {"svelte": "frontend-svelte", "other": BLANK}
|
||||||
SERVE = ["bun", "static", "api"]
|
SERVE = ["bun", "static", "api"]
|
||||||
|
MCP = ["context7", "svelte"]
|
||||||
|
|
||||||
|
|
||||||
def template(name: str) -> str:
|
def template(name: str) -> str:
|
||||||
@@ -58,6 +60,7 @@ def new(args: argparse.Namespace) -> None:
|
|||||||
dest = Path(args.dest).resolve()
|
dest = Path(args.dest).resolve()
|
||||||
name = args.name or ask_text("Project name", dest.name)
|
name = args.name or ask_text("Project name", dest.name)
|
||||||
slug = args.slug or ask_text("Slug", slugify(name))
|
slug = args.slug or ask_text("Slug", slugify(name))
|
||||||
|
description = args.description if args.description is not None else ask_text("Description", name)
|
||||||
parts = args.parts.split(",") if args.parts else ask_checkbox("Parts", ["backend", "frontend", "caddy"], ["backend", "frontend", "caddy"])
|
parts = args.parts.split(",") if args.parts else ask_checkbox("Parts", ["backend", "frontend", "caddy"], ["backend", "frontend", "caddy"])
|
||||||
backend = frontend = None
|
backend = frontend = None
|
||||||
if "backend" in parts:
|
if "backend" in parts:
|
||||||
@@ -65,29 +68,70 @@ def new(args: argparse.Namespace) -> None:
|
|||||||
if "frontend" in parts:
|
if "frontend" in parts:
|
||||||
frontend = args.frontend or ask_select("Frontend", list(FRONTENDS), "svelte")
|
frontend = args.frontend or ask_select("Frontend", list(FRONTENDS), "svelte")
|
||||||
serve = "bun"
|
serve = "bun"
|
||||||
if frontend:
|
if frontend and FRONTENDS[frontend] != BLANK:
|
||||||
options = SERVE if backend else SERVE[:-1]
|
options = SERVE if backend else SERVE[:-1]
|
||||||
serve = args.serve or ask_select("Frontend served by", options, "bun")
|
serve = args.serve or ask_select("Frontend served by", options, "bun")
|
||||||
|
mcp_default = ["svelte"] if frontend and FRONTENDS[frontend] == "frontend-svelte" else []
|
||||||
|
mcp = args.mcp.split(",") if args.mcp is not None else ask_checkbox("MCP servers", MCP, mcp_default)
|
||||||
|
mcp = [m for m in mcp if m]
|
||||||
stack = [p for p in ("backend", "frontend") if p in parts]
|
stack = [p for p in ("backend", "frontend") if p in parts]
|
||||||
single = len(stack) == 1
|
single = len(stack) == 1
|
||||||
routes = [r for r, ok in (("api", backend), ("frontend", frontend and serve != "api")) if ok]
|
routes = [r for r, ok in (("api", backend), ("frontend", frontend and serve != "api")) if ok]
|
||||||
|
|
||||||
def copy(name: str, data: dict) -> None:
|
def copy(name: str, data: dict, answers_file: str | None = None) -> None:
|
||||||
run_copy(template(name), dest, data=data, unsafe=True, defaults=args.defaults, vcs_ref=REF)
|
run_copy(template(name), dest, data=data, answers_file=answers_file, unsafe=True,
|
||||||
|
defaults=args.defaults, vcs_ref=REF)
|
||||||
|
|
||||||
|
def copy_part(part: str, name: str, data: dict) -> None:
|
||||||
|
if name == BLANK:
|
||||||
|
copy(name, {**identity, "layout": layout, "part": part}, f".copier/{part}.yml")
|
||||||
|
else:
|
||||||
|
copy(name, {**identity, "layout": layout, **data})
|
||||||
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
copy("infra", {"layout": "single" if single else "parts", "parts": [] if single else stack})
|
|
||||||
layout = "root" if single else "part"
|
|
||||||
identity = {"project_name": name, "project_slug": slug}
|
identity = {"project_name": name, "project_slug": slug}
|
||||||
|
copy("infra", {"project_name": name, "project_description": description, "layout": "single" if single else "parts",
|
||||||
|
"parts": [] if single else stack, "mcp_servers": mcp})
|
||||||
|
layout = "root" if single else "part"
|
||||||
if backend:
|
if backend:
|
||||||
copy(BACKENDS[backend], {**identity, "layout": layout, "serve_spa": serve == "api",
|
copy_part("backend", BACKENDS[backend], {"project_description": description, "serve_spa": serve == "api",
|
||||||
"author_name": git_config("user.name"), "author_email": git_config("user.email")})
|
"author_name": git_config("user.name"),
|
||||||
|
"author_email": git_config("user.email")})
|
||||||
if frontend:
|
if frontend:
|
||||||
copy(FRONTENDS[frontend], {**identity, "layout": layout, "serve": serve})
|
copy_part("frontend", FRONTENDS[frontend], {"serve": serve})
|
||||||
if "caddy" in parts:
|
if "caddy" in parts:
|
||||||
copy("caddy", {"project_slug": slug, "routes": routes})
|
copy("caddy", {"project_slug": slug, "routes": routes})
|
||||||
if (dest / "Makefile").is_file():
|
if (dest / "Makefile").is_file():
|
||||||
subprocess.run(["make", "env"], cwd=dest, check=True)
|
subprocess.run(["make", "env"], cwd=dest, check=True)
|
||||||
|
if args.repo or (args.repo is None and interactive()):
|
||||||
|
call = gitea_api()
|
||||||
|
repo = choose_repo(dest, args.repo, call)
|
||||||
|
if repo:
|
||||||
|
setup_repo(dest, repo, call)
|
||||||
|
|
||||||
|
|
||||||
|
def git_dirty() -> bool:
|
||||||
|
result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, check=False)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def update_message(answers: Path) -> str:
|
||||||
|
"""Conventional commit for a template update, with the version it moved to."""
|
||||||
|
ref = ""
|
||||||
|
for line in answers.read_text(encoding="utf-8").splitlines():
|
||||||
|
if line.startswith("_commit:"):
|
||||||
|
ref = line.split(":", 1)[1].strip().strip("'\"")
|
||||||
|
break
|
||||||
|
scope = answers.stem
|
||||||
|
return f"chore({scope}): update template to {ref}" if ref else f"chore({scope}): update template"
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_locks(part: str) -> None:
|
||||||
|
root = Path(part) if Path(part, "Makefile").is_file() else Path()
|
||||||
|
if (root / "uv.lock").is_file():
|
||||||
|
subprocess.run(["uv", "lock", "-q"], cwd=root, check=True)
|
||||||
|
if (root / "bun.lock").is_file():
|
||||||
|
subprocess.run(["bun", "install", "--silent"], cwd=root, check=True)
|
||||||
|
|
||||||
|
|
||||||
def update(args: argparse.Namespace) -> None:
|
def update(args: argparse.Namespace) -> None:
|
||||||
@@ -96,9 +140,199 @@ def update(args: argparse.Namespace) -> None:
|
|||||||
answers = [Path(".copier") / f"{p}.yml" for p in args.parts]
|
answers = [Path(".copier") / f"{p}.yml" for p in args.parts]
|
||||||
if not answers:
|
if not answers:
|
||||||
sys.exit("no .copier/*.yml here")
|
sys.exit("no .copier/*.yml here")
|
||||||
|
if git_dirty():
|
||||||
|
sys.exit("working tree is dirty, commit or stash first")
|
||||||
for file in answers:
|
for file in answers:
|
||||||
run_update(".", answers_file=str(file), unsafe=True, overwrite=True,
|
run_update(".", answers_file=str(file), unsafe=True, overwrite=True,
|
||||||
skip_answered=not args.ask, defaults=args.defaults, vcs_ref=REF)
|
skip_answered=not args.ask, defaults=args.defaults, vcs_ref=REF)
|
||||||
|
conflicts = subprocess.run(["git", "diff", "--name-only", "--diff-filter=U"],
|
||||||
|
capture_output=True, text=True, check=False).stdout.split()
|
||||||
|
if conflicts:
|
||||||
|
sys.exit(f"{file.stem}: resolve conflicts, then commit and rerun:\n " + "\n ".join(conflicts))
|
||||||
|
refresh_locks(file.stem)
|
||||||
|
if git_dirty():
|
||||||
|
subprocess.run(["git", "add", "-A"], check=True)
|
||||||
|
subprocess.run(["git", "commit", "-qm", update_message(file)], check=True)
|
||||||
|
print(f"committed {file.stem} update")
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_FILE = Path.home() / ".config/templates/config.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def config() -> dict:
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
return tomllib.loads(CONFIG_FILE.read_text(encoding="utf-8")) if CONFIG_FILE.is_file() else {}
|
||||||
|
|
||||||
|
|
||||||
|
GITEA = os.environ.get("GITEA_URL", config().get("gitea_url", "https://git.kotikot.com")).rstrip("/")
|
||||||
|
RENOVATE_FILE = "renovate.jsonc"
|
||||||
|
RENOVATE_STUB = """{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["local>templates/renovate"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
RENOVATE_LABELS = [
|
||||||
|
("renovate", "1f6feb", "Pull request from the Renovate bot"),
|
||||||
|
("wait-3d", "d29922", "Waits three days: the registry has no release timestamp, the age gate merges it"),
|
||||||
|
("deps-code", "7d8590", "Code dependencies: pull requests are collected, nothing merges itself"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def gitea_token() -> str:
|
||||||
|
token = os.environ.get("GITEA_TOKEN", "")
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
path = Path.home() / ".config/gitea/token"
|
||||||
|
if path.exists():
|
||||||
|
return path.read_text(encoding="utf-8").strip()
|
||||||
|
sys.exit("no Gitea token: set GITEA_TOKEN or write ~/.config/gitea/token")
|
||||||
|
|
||||||
|
|
||||||
|
def gitea_api():
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
token = gitea_token()
|
||||||
|
|
||||||
|
def call(method: str, path: str, body: dict | None = None):
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{GITEA}/api/v1{path}",
|
||||||
|
data=json.dumps(body).encode() if body is not None else None,
|
||||||
|
method=method,
|
||||||
|
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
raw = response.read()
|
||||||
|
return json.loads(raw) if raw else None
|
||||||
|
|
||||||
|
return call
|
||||||
|
|
||||||
|
|
||||||
|
def git(*args: str, cwd: Path) -> str:
|
||||||
|
result = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, check=False)
|
||||||
|
return result.stdout.strip() if result.returncode == 0 else ""
|
||||||
|
|
||||||
|
|
||||||
|
def project_description(cwd: Path) -> str:
|
||||||
|
answers = cwd / ".copier/infra.yml"
|
||||||
|
if not answers.is_file():
|
||||||
|
return ""
|
||||||
|
for line in answers.read_text(encoding="utf-8").splitlines():
|
||||||
|
if line.startswith("project_description:"):
|
||||||
|
return line.split(":", 1)[1].strip().strip("'\"")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def choose_repo(cwd: Path, repo: str | None, call) -> str | None:
|
||||||
|
if repo:
|
||||||
|
return repo
|
||||||
|
orgs = sorted(o["username"] for o in call("GET", "/user/orgs?limit=100") or [])
|
||||||
|
default = config().get("org") or (cwd.parent.name if cwd.parent.name in orgs else None)
|
||||||
|
if not interactive():
|
||||||
|
return f"{default}/{cwd.name}" if default else None
|
||||||
|
skip = "(no repo)"
|
||||||
|
org = questionary.select("Gitea organization", choices=[skip, *orgs], default=default or skip).unsafe_ask()
|
||||||
|
if org == skip:
|
||||||
|
return None
|
||||||
|
return f"{org}/{ask_text('Repo name', cwd.name)}"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_repo(cwd: Path, repo: str, call) -> None:
|
||||||
|
import base64
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
org, name = repo.split("/", 1)
|
||||||
|
try:
|
||||||
|
info = call("GET", f"/repos/{repo}")
|
||||||
|
print(f"repo {repo}: already there")
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
if error.code != 404:
|
||||||
|
sys.exit(f"repo {repo}: {error.code} {error.read()[:200]!r}")
|
||||||
|
info = call("POST", f"/orgs/{org}/repos", {
|
||||||
|
"name": name,
|
||||||
|
"description": project_description(cwd),
|
||||||
|
"private": config().get("private", True),
|
||||||
|
"default_branch": "main",
|
||||||
|
})
|
||||||
|
print(f"repo {repo}: created")
|
||||||
|
|
||||||
|
url = info["clone_url"] if config().get("remote") == "https" else info["ssh_url"]
|
||||||
|
if git("rev-parse", "--git-dir", cwd=cwd):
|
||||||
|
current = git("remote", "get-url", "origin", cwd=cwd)
|
||||||
|
if not current:
|
||||||
|
git("remote", "add", "origin", url, cwd=cwd)
|
||||||
|
print(f"origin: {url}")
|
||||||
|
elif current != url:
|
||||||
|
print(f"origin stays {current}, the repo is {url}")
|
||||||
|
if info.get("empty", True) and git("rev-parse", "--verify", "-q", "HEAD", cwd=cwd):
|
||||||
|
if not git("rev-parse", "--verify", "-q", "refs/heads/main", cwd=cwd):
|
||||||
|
git("branch", "-M", "main", cwd=cwd)
|
||||||
|
if not git("rev-parse", "--verify", "-q", "refs/heads/dev", cwd=cwd):
|
||||||
|
git("branch", "dev", "main", cwd=cwd)
|
||||||
|
subprocess.run(["git", "push", "-q", "-u", "origin", "main", "dev"], cwd=cwd, check=True)
|
||||||
|
git("switch", "-q", "dev", cwd=cwd)
|
||||||
|
print("pushed main and dev, on dev now")
|
||||||
|
elif info.get("empty", True):
|
||||||
|
print("nothing committed yet: commit, then run giteaproject again to push main and dev")
|
||||||
|
|
||||||
|
existing = {label["name"] for label in call("GET", f"/repos/{repo}/labels?limit=100") or []}
|
||||||
|
for label, color, description in RENOVATE_LABELS:
|
||||||
|
if label in existing:
|
||||||
|
continue
|
||||||
|
call("POST", f"/repos/{repo}/labels", {"name": label, "color": color, "description": description})
|
||||||
|
print(f"label {label}: created")
|
||||||
|
|
||||||
|
remote_empty = call("GET", f"/repos/{repo}").get("empty", True)
|
||||||
|
if not remote_empty and not (cwd / RENOVATE_FILE).is_file():
|
||||||
|
try:
|
||||||
|
call("GET", f"/repos/{repo}/contents/{RENOVATE_FILE}")
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
if error.code != 404:
|
||||||
|
sys.exit(f"{RENOVATE_FILE}: {error.code} {error.read()[:200]!r}")
|
||||||
|
call("POST", f"/repos/{repo}/contents/{RENOVATE_FILE}", {
|
||||||
|
"content": base64.b64encode(RENOVATE_STUB.encode()).decode(),
|
||||||
|
"message": "chore(renovate): extend the shared preset",
|
||||||
|
})
|
||||||
|
print(f"{RENOVATE_FILE}: committed on the server, pull it")
|
||||||
|
|
||||||
|
teams = call("GET", f"/orgs/{org}/teams?limit=50") or []
|
||||||
|
team = next((t for t in teams if t["name"] == "renovate"), None)
|
||||||
|
if team is None:
|
||||||
|
team = call("POST", f"/orgs/{org}/teams", {
|
||||||
|
"name": "renovate",
|
||||||
|
"description": "Renovate bot: write on listed repos only, not the whole org",
|
||||||
|
"includes_all_repositories": False,
|
||||||
|
"permission": "none",
|
||||||
|
"units": ["repo.code", "repo.issues", "repo.pulls"],
|
||||||
|
"units_map": {"repo.code": "write", "repo.issues": "write", "repo.pulls": "write"},
|
||||||
|
"can_create_org_repo": False,
|
||||||
|
})
|
||||||
|
print(f"team renovate in {org}: created")
|
||||||
|
members = {m["login"] for m in call("GET", f"/teams/{team['id']}/members?limit=50") or []}
|
||||||
|
if "renovate" not in members:
|
||||||
|
call("PUT", f"/teams/{team['id']}/members/renovate")
|
||||||
|
print("bot renovate: added to the team")
|
||||||
|
repos = {r["name"] for r in call("GET", f"/teams/{team['id']}/repos?limit=50") or []}
|
||||||
|
if name not in repos:
|
||||||
|
call("PUT", f"/teams/{team['id']}/repos/{org}/{name}")
|
||||||
|
print(f"team renovate: access to {name}")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"\nStill manual: add {repo} to `repositories` in\n"
|
||||||
|
"projects/personal/renovate/renovate-config.js of infra/komodo,\n"
|
||||||
|
"the bot only visits repos named in that list."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gitea(args: argparse.Namespace) -> None:
|
||||||
|
"""Create the Gitea repo if needed, point origin at it and prepare it for Renovate."""
|
||||||
|
cwd = Path.cwd()
|
||||||
|
call = gitea_api()
|
||||||
|
repo = choose_repo(cwd, args.repo, call)
|
||||||
|
if repo is None:
|
||||||
|
sys.exit("no repo chosen")
|
||||||
|
setup_repo(cwd, repo, call)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -108,10 +342,13 @@ def main() -> None:
|
|||||||
n.add_argument("dest")
|
n.add_argument("dest")
|
||||||
n.add_argument("--name")
|
n.add_argument("--name")
|
||||||
n.add_argument("--slug")
|
n.add_argument("--slug")
|
||||||
|
n.add_argument("--description")
|
||||||
n.add_argument("--parts", help="comma-separated: backend,frontend,caddy")
|
n.add_argument("--parts", help="comma-separated: backend,frontend,caddy")
|
||||||
n.add_argument("--backend", choices=list(BACKENDS))
|
n.add_argument("--backend", choices=list(BACKENDS))
|
||||||
n.add_argument("--frontend", choices=list(FRONTENDS))
|
n.add_argument("--frontend", choices=list(FRONTENDS))
|
||||||
n.add_argument("--serve", choices=SERVE)
|
n.add_argument("--serve", choices=SERVE)
|
||||||
|
n.add_argument("--mcp", help="comma-separated: context7,svelte; empty for none")
|
||||||
|
n.add_argument("--repo", help="org/name on Gitea to create and use as origin; empty for none")
|
||||||
n.add_argument("--defaults", action="store_true", help="accept template defaults without asking")
|
n.add_argument("--defaults", action="store_true", help="accept template defaults without asking")
|
||||||
n.set_defaults(func=new)
|
n.set_defaults(func=new)
|
||||||
u = sub.add_parser("update", help="Update every part from its template")
|
u = sub.add_parser("update", help="Update every part from its template")
|
||||||
@@ -119,6 +356,9 @@ def main() -> None:
|
|||||||
u.add_argument("--ask", action="store_true", help="ask every question again")
|
u.add_argument("--ask", action="store_true", help="ask every question again")
|
||||||
u.add_argument("--defaults", action="store_true")
|
u.add_argument("--defaults", action="store_true")
|
||||||
u.set_defaults(func=update)
|
u.set_defaults(func=update)
|
||||||
|
g = sub.add_parser("gitea", help="Create the Gitea repo, set origin, prepare it for Renovate")
|
||||||
|
g.add_argument("repo", nargs="?", help="org/name on Gitea, asked when omitted")
|
||||||
|
g.set_defaults(func=gitea)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user