Coverage for scripts/init_scaffold.py: 100%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 06:18 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 06:18 +0000
1#!/usr/bin/env python3
2"""Write a rhiza-managed repo's one non-synced file: `.rhiza/template.yml`.
4Bundled with this plugin so `/rhiza:init` can point a repo at a template without
5the `rhiza` CLI. That pointer is the only file `/init` writes itself — everything
6else is another step's job:
8 project skeleton the skeleton procedure (scripts/init_skeleton.py)
9 Makefile, CI, docs the template sync, via `/rhiza:update`
10 license the license procedure (scripts/set_license.py)
11 Python version the python-version procedure (scripts/set_python_version.py)
12 README / mkdocs.yml `/rhiza:docs`
13 first module + test the user's own
15The file is created **only if absent** — an existing `template.yml` is left
16untouched (bumping one is `/rhiza:update`'s job).
18Usage:
19 uv run --python 3.12 --no-project python \
20 scripts/init_scaffold.py [TARGET] \
21 [--host github|gitlab] [--template-host github|gitlab] \
22 [--language python|go] \
23 [--template-repo owner/repo] [--ref TAG] [--json]
24"""
26from __future__ import annotations
28import argparse
29import json
30import sys
31from pathlib import Path
32from typing import Any
34DEFAULT_TEMPLATE_REPO = {"python": "jebel-quant/rhiza", "go": "jebel-quant/rhiza-go"}
37def profile_for_host(host: str) -> str:
38 """Return the sync profile matching the git hosting platform."""
39 return "gitlab-project" if host == "gitlab" else "github-project"
42def render_template_yml(
43 repo: str, ref: str, host: str, language: str, template_host: str | None = None
44) -> str:
45 """Render `.rhiza/template.yml` (mirrors template.yml.jinja2).
47 Two independent facts, which must not be conflated:
49 * *host* — where **this repo** lives. It selects the ``profiles:`` entry, so a
50 GitLab repo gets ``gitlab-project`` and its CI, not GitHub's.
51 * *template_host* — where the **template** lives. It becomes ``template-host:``,
52 which `sync.py` turns into the clone URL.
54 They are usually different: a GitLab-hosted project following ``jebel-quant/rhiza``
55 is on GitLab, but the template is on GitHub. Deriving one from the other emitted
56 ``template-host: gitlab`` for every GitLab repo, so the first sync tried to clone
57 the template from gitlab.com and died with "could not read Username". Default
58 *template_host* to GitHub — where the rhiza templates are — not to *host*.
59 """
60 lines = [f'repository: "{repo}"', f'ref: "{ref}"']
61 if (template_host or "github") == "gitlab":
62 lines.append("template-host: gitlab")
63 if language != "python":
64 lines.append(f"language: {language}")
65 lines += ["", "profiles:", f" - {profile_for_host(host)}", ""]
66 return "\n".join(lines)
69def scaffold(
70 target: Path,
71 *,
72 host: str,
73 language: str,
74 template_repo: str,
75 ref: str,
76 template_host: str | None = None,
77) -> dict[str, Any]:
78 """Write `.rhiza/template.yml` if absent; return a summary dict."""
79 path = target / ".rhiza" / "template.yml"
80 created: list[str] = []
81 skipped: list[str] = []
83 if path.exists():
84 skipped.append(".rhiza/template.yml")
85 else:
86 path.parent.mkdir(parents=True, exist_ok=True)
87 path.write_text(render_template_yml(template_repo, ref, host, language, template_host))
88 created.append(".rhiza/template.yml")
90 return {
91 "target": str(target),
92 "language": language,
93 "template_repository": template_repo,
94 "ref": ref,
95 "profile": profile_for_host(host),
96 "template_host": template_host or "github",
97 "created": created,
98 "skipped": skipped,
99 }
102def main(argv: list[str] | None = None) -> int:
103 """Entry point: parse args, write the pointer, and return an exit code."""
104 parser = argparse.ArgumentParser(
105 description="Write a rhiza-managed repo's .rhiza/template.yml pointer.",
106 )
107 parser.add_argument(
108 "target", nargs="?", default=".", help="Repository root (default: current directory)."
109 )
110 parser.add_argument(
111 "--host",
112 choices=("github", "gitlab"),
113 default="github",
114 help="Where THIS repo lives; selects the profile (and its CI).",
115 )
116 parser.add_argument(
117 "--template-host",
118 choices=("github", "gitlab"),
119 default="github",
120 help="Where the TEMPLATE lives; sets the clone URL. Usually github, even for "
121 "a GitLab-hosted repo, because the rhiza templates are on GitHub.",
122 )
123 parser.add_argument(
124 "--language", choices=("python", "go"), default="python", help="Project language."
125 )
126 parser.add_argument(
127 "--template-repo", help="Template repository owner/repo (default: by language)."
128 )
129 parser.add_argument("--ref", default="main", help="Template branch/tag (default: main).")
130 parser.add_argument(
131 "--json", dest="json_output", action="store_true", help="Emit the summary as JSON."
132 )
133 args = parser.parse_args(argv)
135 summary = scaffold(
136 Path(args.target).resolve(),
137 host=args.host,
138 language=args.language,
139 template_repo=args.template_repo or DEFAULT_TEMPLATE_REPO[args.language],
140 ref=args.ref,
141 template_host=args.template_host,
142 )
144 if args.json_output:
145 print(json.dumps(summary, indent=2))
146 else:
147 for path in summary["created"]:
148 print(f"created {path}")
149 for path in summary["skipped"]:
150 print(f"skipped {path} (already exists)", file=sys.stderr)
151 return 0
154if __name__ == "__main__":
155 raise SystemExit(main())