sum-cli 3.0.0__py3-none-any.whl → 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sum/cli.py +3 -3
- sum/commands/__init__.py +2 -2
- sum/commands/init.py +97 -15
- sum/commands/promote.py +29 -10
- sum/commands/setup.py +143 -0
- sum/setup/git_ops.py +69 -50
- sum/setup/orchestrator.py +2 -2
- sum/setup/scaffold.py +9 -12
- sum/setup/site_orchestrator.py +29 -4
- sum/site_config.py +129 -0
- sum/system_config.py +0 -37
- sum/utils/environment.py +2 -2
- sum/utils/validation.py +2 -2
- sum_cli-3.1.0.dist-info/METADATA +230 -0
- {sum_cli-3.0.0.dist-info → sum_cli-3.1.0.dist-info}/RECORD +19 -19
- sum/commands/run.py +0 -96
- sum/docs/USER_GUIDE.md +0 -663
- sum_cli-3.0.0.dist-info/METADATA +0 -127
- {sum_cli-3.0.0.dist-info → sum_cli-3.1.0.dist-info}/WHEEL +0 -0
- {sum_cli-3.0.0.dist-info → sum_cli-3.1.0.dist-info}/entry_points.txt +0 -0
- {sum_cli-3.0.0.dist-info → sum_cli-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {sum_cli-3.0.0.dist-info → sum_cli-3.1.0.dist-info}/top_level.txt +0 -0
sum/cli.py
CHANGED
|
@@ -9,7 +9,7 @@ from sum.commands.backup import backup
|
|
|
9
9
|
from sum.commands.check import check
|
|
10
10
|
from sum.commands.init import init
|
|
11
11
|
from sum.commands.promote import promote
|
|
12
|
-
from sum.commands.
|
|
12
|
+
from sum.commands.setup import setup
|
|
13
13
|
from sum.commands.themes import themes
|
|
14
14
|
from sum.commands.update import update
|
|
15
15
|
|
|
@@ -26,14 +26,14 @@ def _get_version() -> str:
|
|
|
26
26
|
version=_get_version(), prog_name="sum-platform", message="%(prog)s %(version)s"
|
|
27
27
|
)
|
|
28
28
|
def cli() -> None:
|
|
29
|
-
"""SUM Platform CLI
|
|
29
|
+
"""SUM Platform CLI - Deploy and manage client sites."""
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
cli.add_command(backup)
|
|
33
33
|
cli.add_command(check)
|
|
34
34
|
cli.add_command(init)
|
|
35
35
|
cli.add_command(promote)
|
|
36
|
-
cli.add_command(
|
|
36
|
+
cli.add_command(setup)
|
|
37
37
|
cli.add_command(themes)
|
|
38
38
|
cli.add_command(update)
|
|
39
39
|
|
sum/commands/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from sum.commands.check import check
|
|
6
6
|
from sum.commands.init import init
|
|
7
|
-
from sum.commands.
|
|
7
|
+
from sum.commands.setup import setup
|
|
8
8
|
from sum.commands.themes import themes
|
|
9
9
|
|
|
10
|
-
__all__ = ["check", "init", "
|
|
10
|
+
__all__ = ["check", "init", "setup", "themes"]
|
sum/commands/init.py
CHANGED
|
@@ -7,7 +7,7 @@ Creates a working site on staging at /srv/sum/<name>/ with:
|
|
|
7
7
|
- External venv
|
|
8
8
|
- Systemd service
|
|
9
9
|
- Caddy configuration
|
|
10
|
-
- Git repository (optional)
|
|
10
|
+
- Git repository (optional, specify provider with --git-provider)
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
@@ -17,6 +17,7 @@ from types import ModuleType
|
|
|
17
17
|
from sum.exceptions import SetupError
|
|
18
18
|
from sum.setup.infrastructure import check_infrastructure
|
|
19
19
|
from sum.setup.site_orchestrator import SiteOrchestrator, SiteSetupConfig
|
|
20
|
+
from sum.site_config import GitConfig
|
|
20
21
|
from sum.system_config import ConfigurationError, get_system_config
|
|
21
22
|
from sum.utils.output import OutputFormatter
|
|
22
23
|
from sum.utils.project import validate_project_name
|
|
@@ -37,6 +38,11 @@ def run_init(
|
|
|
37
38
|
profile: str = "starter",
|
|
38
39
|
content_path: str | None = None,
|
|
39
40
|
no_git: bool = False,
|
|
41
|
+
git_provider: str | None = None,
|
|
42
|
+
git_org: str | None = None,
|
|
43
|
+
gitea_url: str | None = None,
|
|
44
|
+
gitea_ssh_port: int = 22,
|
|
45
|
+
gitea_token_env: str = "GITEA_TOKEN",
|
|
40
46
|
skip_systemd: bool = False,
|
|
41
47
|
skip_caddy: bool = False,
|
|
42
48
|
superuser_username: str = "admin",
|
|
@@ -51,7 +57,12 @@ def run_init(
|
|
|
51
57
|
theme: Theme slug to use.
|
|
52
58
|
profile: Content profile name to seed.
|
|
53
59
|
content_path: Optional path to custom content directory.
|
|
54
|
-
no_git: Skip
|
|
60
|
+
no_git: Skip git repository creation.
|
|
61
|
+
git_provider: Git provider ("github" or "gitea").
|
|
62
|
+
git_org: Git organization/namespace.
|
|
63
|
+
gitea_url: Gitea instance URL (required if git_provider=gitea).
|
|
64
|
+
gitea_ssh_port: SSH port for Gitea.
|
|
65
|
+
gitea_token_env: Env var name for Gitea API token.
|
|
55
66
|
skip_systemd: Skip systemd service installation.
|
|
56
67
|
skip_caddy: Skip Caddy configuration.
|
|
57
68
|
superuser_username: Username for Django superuser.
|
|
@@ -96,11 +107,32 @@ def run_init(
|
|
|
96
107
|
OutputFormatter.error(f"Site already exists at {site_dir}")
|
|
97
108
|
return 1
|
|
98
109
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
# Build git config from flags
|
|
111
|
+
git_config: GitConfig | None = None
|
|
112
|
+
if not no_git:
|
|
113
|
+
# Validate required git flags
|
|
114
|
+
if not git_provider:
|
|
115
|
+
OutputFormatter.error(
|
|
116
|
+
"Git provider required. Use --git-provider github or --git-provider gitea.\n"
|
|
117
|
+
"Or use --no-git to skip git setup."
|
|
118
|
+
)
|
|
119
|
+
return 1
|
|
120
|
+
if not git_org:
|
|
121
|
+
OutputFormatter.error(
|
|
122
|
+
"Git organization required. Use --git-org <org>.\n"
|
|
123
|
+
"Or use --no-git to skip git setup."
|
|
124
|
+
)
|
|
125
|
+
return 1
|
|
126
|
+
if git_provider == "gitea" and not gitea_url:
|
|
127
|
+
OutputFormatter.error("Gitea URL required. Use --gitea-url <url>.")
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
git_config = GitConfig(
|
|
131
|
+
provider=git_provider,
|
|
132
|
+
org=git_org,
|
|
133
|
+
url=gitea_url,
|
|
134
|
+
ssh_port=gitea_ssh_port,
|
|
135
|
+
token_env=gitea_token_env,
|
|
104
136
|
)
|
|
105
137
|
|
|
106
138
|
# Build setup config
|
|
@@ -110,9 +142,9 @@ def run_init(
|
|
|
110
142
|
seed_profile=profile,
|
|
111
143
|
content_path=content_path,
|
|
112
144
|
superuser_username=superuser_username,
|
|
113
|
-
skip_git=no_git,
|
|
114
145
|
skip_systemd=skip_systemd,
|
|
115
146
|
skip_caddy=skip_caddy,
|
|
147
|
+
git_config=git_config,
|
|
116
148
|
)
|
|
117
149
|
|
|
118
150
|
# Run setup
|
|
@@ -153,6 +185,11 @@ def _init_command(
|
|
|
153
185
|
profile: str,
|
|
154
186
|
content_path: str | None,
|
|
155
187
|
no_git: bool,
|
|
188
|
+
git_provider: str | None,
|
|
189
|
+
git_org: str | None,
|
|
190
|
+
gitea_url: str | None,
|
|
191
|
+
gitea_ssh_port: int,
|
|
192
|
+
gitea_token_env: str,
|
|
156
193
|
skip_systemd: bool,
|
|
157
194
|
skip_caddy: bool,
|
|
158
195
|
superuser_username: str,
|
|
@@ -164,6 +201,11 @@ def _init_command(
|
|
|
164
201
|
profile=profile,
|
|
165
202
|
content_path=content_path,
|
|
166
203
|
no_git=no_git,
|
|
204
|
+
git_provider=git_provider,
|
|
205
|
+
git_org=git_org,
|
|
206
|
+
gitea_url=gitea_url,
|
|
207
|
+
gitea_ssh_port=gitea_ssh_port,
|
|
208
|
+
gitea_token_env=gitea_token_env,
|
|
167
209
|
skip_systemd=skip_systemd,
|
|
168
210
|
skip_caddy=skip_caddy,
|
|
169
211
|
superuser_username=superuser_username,
|
|
@@ -202,7 +244,36 @@ else:
|
|
|
202
244
|
@click.option(
|
|
203
245
|
"--no-git",
|
|
204
246
|
is_flag=True,
|
|
205
|
-
help="Skip
|
|
247
|
+
help="Skip git repository creation (local git init only).",
|
|
248
|
+
)
|
|
249
|
+
@click.option(
|
|
250
|
+
"--git-provider",
|
|
251
|
+
type=click.Choice(["github", "gitea"]),
|
|
252
|
+
default=None,
|
|
253
|
+
help="Git provider: github or gitea. Required unless --no-git.",
|
|
254
|
+
)
|
|
255
|
+
@click.option(
|
|
256
|
+
"--git-org",
|
|
257
|
+
default=None,
|
|
258
|
+
help="Git organization/namespace. Required unless --no-git.",
|
|
259
|
+
)
|
|
260
|
+
@click.option(
|
|
261
|
+
"--gitea-url",
|
|
262
|
+
default=None,
|
|
263
|
+
help="Gitea instance URL. Required if --git-provider=gitea.",
|
|
264
|
+
)
|
|
265
|
+
@click.option(
|
|
266
|
+
"--gitea-ssh-port",
|
|
267
|
+
type=int,
|
|
268
|
+
default=22,
|
|
269
|
+
show_default=True,
|
|
270
|
+
help="SSH port for Gitea.",
|
|
271
|
+
)
|
|
272
|
+
@click.option(
|
|
273
|
+
"--gitea-token-env",
|
|
274
|
+
default="GITEA_TOKEN",
|
|
275
|
+
show_default=True,
|
|
276
|
+
help="Environment variable for Gitea API token.",
|
|
206
277
|
)
|
|
207
278
|
@click.option(
|
|
208
279
|
"--skip-systemd",
|
|
@@ -227,6 +298,11 @@ else:
|
|
|
227
298
|
profile: str,
|
|
228
299
|
content_path: str | None,
|
|
229
300
|
no_git: bool,
|
|
301
|
+
git_provider: str | None,
|
|
302
|
+
git_org: str | None,
|
|
303
|
+
gitea_url: str | None,
|
|
304
|
+
gitea_ssh_port: int,
|
|
305
|
+
gitea_token_env: str,
|
|
230
306
|
skip_systemd: bool,
|
|
231
307
|
skip_caddy: bool,
|
|
232
308
|
superuser_username: str,
|
|
@@ -241,15 +317,16 @@ else:
|
|
|
241
317
|
- External virtualenv
|
|
242
318
|
- Systemd service
|
|
243
319
|
- Caddy reverse proxy
|
|
244
|
-
-
|
|
245
|
-
|
|
246
|
-
The site will be accessible at https://<SITE_NAME>.lintel.site
|
|
320
|
+
- Git repository (optional, specify provider with --git-provider)
|
|
247
321
|
|
|
248
322
|
\b
|
|
249
323
|
Examples:
|
|
250
|
-
sudo sum-platform init acme
|
|
251
|
-
sudo sum-platform init acme --
|
|
252
|
-
sudo sum-platform init acme --
|
|
324
|
+
sudo sum-platform init acme --no-git
|
|
325
|
+
sudo sum-platform init acme --git-provider github --git-org acme-corp
|
|
326
|
+
sudo sum-platform init acme --git-provider gitea --git-org clients \\
|
|
327
|
+
--gitea-url https://git.agency.com
|
|
328
|
+
sudo sum-platform init acme --git-provider gitea --git-org clients \\
|
|
329
|
+
--gitea-url https://git.agency.com --gitea-ssh-port 2222
|
|
253
330
|
"""
|
|
254
331
|
_init_command(
|
|
255
332
|
site_name,
|
|
@@ -257,6 +334,11 @@ else:
|
|
|
257
334
|
profile=profile,
|
|
258
335
|
content_path=content_path,
|
|
259
336
|
no_git=no_git,
|
|
337
|
+
git_provider=git_provider,
|
|
338
|
+
git_org=git_org,
|
|
339
|
+
gitea_url=gitea_url,
|
|
340
|
+
gitea_ssh_port=gitea_ssh_port,
|
|
341
|
+
gitea_token_env=gitea_token_env,
|
|
260
342
|
skip_systemd=skip_systemd,
|
|
261
343
|
skip_caddy=skip_caddy,
|
|
262
344
|
superuser_username=superuser_username,
|
sum/commands/promote.py
CHANGED
|
@@ -19,8 +19,9 @@ from pathlib import Path
|
|
|
19
19
|
from types import ModuleType
|
|
20
20
|
|
|
21
21
|
from sum.exceptions import SetupError
|
|
22
|
-
from sum.setup.git_ops import
|
|
22
|
+
from sum.setup.git_ops import get_git_provider_from_config
|
|
23
23
|
from sum.setup.infrastructure import generate_password, generate_secret_key
|
|
24
|
+
from sum.site_config import GitConfig, SiteConfig, SiteConfigError
|
|
24
25
|
from sum.system_config import ConfigurationError, SystemConfig, get_system_config
|
|
25
26
|
from sum.utils.output import OutputFormatter
|
|
26
27
|
|
|
@@ -206,7 +207,9 @@ WAGTAILADMIN_BASE_URL=https://{domain}
|
|
|
206
207
|
return credentials
|
|
207
208
|
|
|
208
209
|
|
|
209
|
-
def _clone_repo_on_prod(
|
|
210
|
+
def _clone_repo_on_prod(
|
|
211
|
+
site_slug: str, config: SystemConfig, git_config: GitConfig
|
|
212
|
+
) -> None:
|
|
210
213
|
"""Clone the site repository on production.
|
|
211
214
|
|
|
212
215
|
Uses HTTPS with token to avoid SSH key requirements on production.
|
|
@@ -214,8 +217,8 @@ def _clone_repo_on_prod(site_slug: str, config: SystemConfig) -> None:
|
|
|
214
217
|
"""
|
|
215
218
|
ssh_host = config.production.ssh_host
|
|
216
219
|
site_dir = config.get_site_dir(site_slug, target="prod")
|
|
217
|
-
provider =
|
|
218
|
-
org =
|
|
220
|
+
provider = get_git_provider_from_config(git_config)
|
|
221
|
+
org = git_config.org
|
|
219
222
|
|
|
220
223
|
q_app_dir = shlex.quote(f"{site_dir}/app")
|
|
221
224
|
q_site_env = shlex.quote(f"{site_dir}/.env")
|
|
@@ -226,7 +229,7 @@ def _clone_repo_on_prod(site_slug: str, config: SystemConfig) -> None:
|
|
|
226
229
|
|
|
227
230
|
# Try to get token for HTTPS clone (avoids SSH key setup on prod)
|
|
228
231
|
repo_url = ssh_clone_url # Default to SSH
|
|
229
|
-
if
|
|
232
|
+
if git_config.provider == "github":
|
|
230
233
|
# Try GitHub CLI token
|
|
231
234
|
try:
|
|
232
235
|
result = subprocess.run(
|
|
@@ -240,14 +243,14 @@ def _clone_repo_on_prod(site_slug: str, config: SystemConfig) -> None:
|
|
|
240
243
|
repo_url = f"https://{gh_token}@github.com/{org}/{site_slug}.git"
|
|
241
244
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
242
245
|
pass
|
|
243
|
-
elif
|
|
246
|
+
elif git_config.provider == "gitea":
|
|
244
247
|
# Try Gitea token from environment
|
|
245
248
|
import os
|
|
246
249
|
from urllib.parse import urlparse
|
|
247
250
|
|
|
248
|
-
gitea_token = os.environ.get(
|
|
249
|
-
if gitea_token and
|
|
250
|
-
parsed = urlparse(
|
|
251
|
+
gitea_token = os.environ.get(git_config.token_env)
|
|
252
|
+
if gitea_token and git_config.url:
|
|
253
|
+
parsed = urlparse(git_config.url)
|
|
251
254
|
repo_url = f"https://{gitea_token}@{parsed.netloc}/{org}/{site_slug}.git"
|
|
252
255
|
|
|
253
256
|
cmd = f"git clone {shlex.quote(repo_url)} {q_app_dir}"
|
|
@@ -558,6 +561,22 @@ def run_promote(
|
|
|
558
561
|
OutputFormatter.error(f"Staging site not found: {staging_dir}")
|
|
559
562
|
return 1
|
|
560
563
|
|
|
564
|
+
# Load site config to get git settings
|
|
565
|
+
try:
|
|
566
|
+
site_config = SiteConfig.load(staging_dir)
|
|
567
|
+
except SiteConfigError as exc:
|
|
568
|
+
OutputFormatter.error(str(exc))
|
|
569
|
+
return 1
|
|
570
|
+
|
|
571
|
+
if site_config.git is None:
|
|
572
|
+
OutputFormatter.error(
|
|
573
|
+
f"Site '{site_name}' was created with --no-git. "
|
|
574
|
+
"Cannot promote without git repository."
|
|
575
|
+
)
|
|
576
|
+
return 1
|
|
577
|
+
|
|
578
|
+
git_config = site_config.git
|
|
579
|
+
|
|
561
580
|
OutputFormatter.header(f"Promoting {site_name} to production")
|
|
562
581
|
print(f" Domain: {domain}")
|
|
563
582
|
print()
|
|
@@ -601,7 +620,7 @@ def run_promote(
|
|
|
601
620
|
OutputFormatter.progress(
|
|
602
621
|
current_step, total_steps, "Cloning repository on production", "⏳"
|
|
603
622
|
)
|
|
604
|
-
_clone_repo_on_prod(site_name, config)
|
|
623
|
+
_clone_repo_on_prod(site_name, config, git_config)
|
|
605
624
|
OutputFormatter.progress(current_step, total_steps, "Repository cloned", "✅")
|
|
606
625
|
|
|
607
626
|
# Step 5: Setup venv on production
|
sum/commands/setup.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Setup command for initializing /etc/sum/config.yml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from sum.system_config import DEFAULT_CONFIG_PATH
|
|
11
|
+
from sum.utils.output import OutputFormatter
|
|
12
|
+
|
|
13
|
+
click_module: ModuleType | None
|
|
14
|
+
try:
|
|
15
|
+
import click as click_module
|
|
16
|
+
except ImportError: # pragma: no cover
|
|
17
|
+
click_module = None
|
|
18
|
+
|
|
19
|
+
click: ModuleType | None = click_module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _missing_click(*_args: object, **_kwargs: object) -> None:
|
|
23
|
+
raise RuntimeError("click is required to use the setup command")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if click is None:
|
|
27
|
+
setup = _missing_click
|
|
28
|
+
else:
|
|
29
|
+
|
|
30
|
+
@click.command()
|
|
31
|
+
def setup() -> None:
|
|
32
|
+
"""Initialize system configuration interactively.
|
|
33
|
+
|
|
34
|
+
Creates /etc/sum/config.yml with infrastructure settings.
|
|
35
|
+
Requires sudo.
|
|
36
|
+
|
|
37
|
+
\b
|
|
38
|
+
The configuration file stores:
|
|
39
|
+
- Agency information
|
|
40
|
+
- Staging server settings
|
|
41
|
+
- Production server settings
|
|
42
|
+
- Infrastructure template paths
|
|
43
|
+
- Default values
|
|
44
|
+
|
|
45
|
+
\b
|
|
46
|
+
Git settings are NOT stored here - they are per-site,
|
|
47
|
+
specified when running 'sum-platform init'.
|
|
48
|
+
"""
|
|
49
|
+
if os.geteuid() != 0:
|
|
50
|
+
raise click.ClickException(
|
|
51
|
+
"Setup requires root privileges. Run with: sudo sum-platform setup"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if DEFAULT_CONFIG_PATH.exists():
|
|
55
|
+
if not click.confirm(f"{DEFAULT_CONFIG_PATH} exists. Overwrite?"):
|
|
56
|
+
raise SystemExit(0)
|
|
57
|
+
|
|
58
|
+
click.echo("SUM Platform Setup")
|
|
59
|
+
click.echo("=" * 40)
|
|
60
|
+
click.echo()
|
|
61
|
+
|
|
62
|
+
# Agency
|
|
63
|
+
click.echo("Agency Information:")
|
|
64
|
+
agency_name = click.prompt(" Agency name", type=str)
|
|
65
|
+
|
|
66
|
+
click.echo()
|
|
67
|
+
|
|
68
|
+
# Staging
|
|
69
|
+
click.echo("Staging Server Configuration:")
|
|
70
|
+
staging_server = click.prompt(" Hostname", type=str)
|
|
71
|
+
staging_domain_pattern = click.prompt(
|
|
72
|
+
" Domain pattern (use {slug} placeholder)",
|
|
73
|
+
default="{slug}." + staging_server,
|
|
74
|
+
)
|
|
75
|
+
staging_base_dir = click.prompt(" Base directory", default="/srv/sum")
|
|
76
|
+
|
|
77
|
+
click.echo()
|
|
78
|
+
|
|
79
|
+
# Production
|
|
80
|
+
click.echo("Production Server Configuration:")
|
|
81
|
+
prod_server = click.prompt(" Hostname", type=str)
|
|
82
|
+
prod_ssh_host = click.prompt(" SSH host (IP or hostname)", default=prod_server)
|
|
83
|
+
prod_base_dir = click.prompt(" Base directory", default="/srv/sum")
|
|
84
|
+
|
|
85
|
+
click.echo()
|
|
86
|
+
|
|
87
|
+
# Templates
|
|
88
|
+
click.echo("Infrastructure Templates:")
|
|
89
|
+
templates_dir = click.prompt(" Templates directory", type=str)
|
|
90
|
+
systemd_template = click.prompt(
|
|
91
|
+
" Systemd template (relative path)",
|
|
92
|
+
default="systemd/sum-site-gunicorn.service.template",
|
|
93
|
+
)
|
|
94
|
+
caddy_template = click.prompt(
|
|
95
|
+
" Caddy template (relative path)",
|
|
96
|
+
default="caddy/Caddyfile.template",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
click.echo()
|
|
100
|
+
|
|
101
|
+
# Defaults
|
|
102
|
+
click.echo("Defaults:")
|
|
103
|
+
default_theme = click.prompt(" Default theme", default="theme_a")
|
|
104
|
+
seed_profile = click.prompt(" Seed profile", default="starter")
|
|
105
|
+
deploy_user = click.prompt(" Deploy user", default="deploy")
|
|
106
|
+
postgres_port = click.prompt(" Postgres port", default=5432, type=int)
|
|
107
|
+
|
|
108
|
+
# Build config
|
|
109
|
+
config = {
|
|
110
|
+
"agency": {
|
|
111
|
+
"name": agency_name,
|
|
112
|
+
},
|
|
113
|
+
"staging": {
|
|
114
|
+
"server": staging_server,
|
|
115
|
+
"domain_pattern": staging_domain_pattern,
|
|
116
|
+
"base_dir": staging_base_dir,
|
|
117
|
+
},
|
|
118
|
+
"production": {
|
|
119
|
+
"server": prod_server,
|
|
120
|
+
"ssh_host": prod_ssh_host,
|
|
121
|
+
"base_dir": prod_base_dir,
|
|
122
|
+
},
|
|
123
|
+
"templates": {
|
|
124
|
+
"dir": templates_dir,
|
|
125
|
+
"systemd": systemd_template,
|
|
126
|
+
"caddy": caddy_template,
|
|
127
|
+
},
|
|
128
|
+
"defaults": {
|
|
129
|
+
"theme": default_theme,
|
|
130
|
+
"seed_profile": seed_profile,
|
|
131
|
+
"deploy_user": deploy_user,
|
|
132
|
+
"postgres_port": postgres_port,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Write config
|
|
137
|
+
config_path = Path(DEFAULT_CONFIG_PATH)
|
|
138
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
with open(config_path, "w") as f:
|
|
140
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
141
|
+
|
|
142
|
+
click.echo()
|
|
143
|
+
OutputFormatter.success(f"Configuration saved to {DEFAULT_CONFIG_PATH}")
|
sum/setup/git_ops.py
CHANGED
|
@@ -13,7 +13,7 @@ from abc import ABC, abstractmethod
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from sum.exceptions import SetupError
|
|
16
|
-
from sum.
|
|
16
|
+
from sum.site_config import GitConfig
|
|
17
17
|
from sum.utils.output import OutputFormatter
|
|
18
18
|
|
|
19
19
|
# =============================================================================
|
|
@@ -65,8 +65,8 @@ class GitProvider(ABC):
|
|
|
65
65
|
class GitHubProvider(GitProvider):
|
|
66
66
|
"""GitHub provider using gh CLI."""
|
|
67
67
|
|
|
68
|
-
def __init__(self,
|
|
69
|
-
self.org =
|
|
68
|
+
def __init__(self, org: str) -> None:
|
|
69
|
+
self.org = org
|
|
70
70
|
|
|
71
71
|
@property
|
|
72
72
|
def name(self) -> str:
|
|
@@ -175,10 +175,17 @@ class GitHubProvider(GitProvider):
|
|
|
175
175
|
class GiteaProvider(GitProvider):
|
|
176
176
|
"""Gitea provider using REST API."""
|
|
177
177
|
|
|
178
|
-
def __init__(
|
|
179
|
-
self
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
org: str,
|
|
181
|
+
base_url: str,
|
|
182
|
+
ssh_port: int = 22,
|
|
183
|
+
token_env: str = "GITEA_TOKEN",
|
|
184
|
+
) -> None:
|
|
185
|
+
self.org = org
|
|
186
|
+
self.base_url = base_url.rstrip("/")
|
|
187
|
+
self.ssh_port = ssh_port
|
|
188
|
+
self.token_env = token_env
|
|
182
189
|
|
|
183
190
|
@property
|
|
184
191
|
def name(self) -> str:
|
|
@@ -198,13 +205,22 @@ class GiteaProvider(GitProvider):
|
|
|
198
205
|
return f"{self.base_url}/{org}/{repo_name}"
|
|
199
206
|
|
|
200
207
|
def get_clone_url(self, org: str, repo_name: str) -> str:
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
"""Get SSH clone URL for a repository.
|
|
209
|
+
|
|
210
|
+
Uses ssh:// URL format when custom port is specified,
|
|
211
|
+
otherwise uses standard git@host:path format.
|
|
212
|
+
"""
|
|
203
213
|
from urllib.parse import urlparse
|
|
204
214
|
|
|
205
215
|
parsed = urlparse(self.base_url)
|
|
206
216
|
host = parsed.netloc
|
|
207
|
-
|
|
217
|
+
|
|
218
|
+
if self.ssh_port != 22:
|
|
219
|
+
# Non-standard port requires ssh:// URL format
|
|
220
|
+
return f"ssh://git@{host}:{self.ssh_port}/{org}/{repo_name}.git"
|
|
221
|
+
else:
|
|
222
|
+
# Standard port 22 can use short format
|
|
223
|
+
return f"git@{host}:{org}/{repo_name}.git"
|
|
208
224
|
|
|
209
225
|
def create_repo(self, app_dir: Path, repo_name: str) -> str:
|
|
210
226
|
"""Create a private Gitea repository and push the code."""
|
|
@@ -338,21 +354,49 @@ class GiteaProvider(GitProvider):
|
|
|
338
354
|
# =============================================================================
|
|
339
355
|
|
|
340
356
|
|
|
341
|
-
def get_git_provider(
|
|
342
|
-
|
|
357
|
+
def get_git_provider(
|
|
358
|
+
provider: str,
|
|
359
|
+
org: str,
|
|
360
|
+
gitea_url: str | None = None,
|
|
361
|
+
gitea_ssh_port: int = 22,
|
|
362
|
+
gitea_token_env: str = "GITEA_TOKEN",
|
|
363
|
+
) -> GitProvider:
|
|
364
|
+
"""Create a git provider instance.
|
|
343
365
|
|
|
344
366
|
Args:
|
|
345
|
-
|
|
367
|
+
provider: "github" or "gitea"
|
|
368
|
+
org: Organization/namespace
|
|
369
|
+
gitea_url: Gitea instance URL (required if provider=gitea)
|
|
370
|
+
gitea_ssh_port: SSH port for Gitea
|
|
371
|
+
gitea_token_env: Env var name for Gitea API token
|
|
346
372
|
|
|
347
373
|
Returns:
|
|
348
|
-
GitProvider instance for the
|
|
374
|
+
GitProvider instance for the specified provider.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
ValueError: If provider is gitea but gitea_url not provided.
|
|
349
378
|
"""
|
|
350
|
-
if
|
|
351
|
-
|
|
379
|
+
if provider == "gitea":
|
|
380
|
+
if not gitea_url:
|
|
381
|
+
raise ValueError("gitea_url required when provider is 'gitea'")
|
|
382
|
+
return GiteaProvider(
|
|
383
|
+
org=org,
|
|
384
|
+
base_url=gitea_url,
|
|
385
|
+
ssh_port=gitea_ssh_port,
|
|
386
|
+
token_env=gitea_token_env,
|
|
387
|
+
)
|
|
388
|
+
return GitHubProvider(org=org)
|
|
352
389
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
390
|
+
|
|
391
|
+
def get_git_provider_from_config(git_config: GitConfig) -> GitProvider:
|
|
392
|
+
"""Create a git provider from a GitConfig object."""
|
|
393
|
+
return get_git_provider(
|
|
394
|
+
provider=git_config.provider,
|
|
395
|
+
org=git_config.org,
|
|
396
|
+
gitea_url=git_config.url,
|
|
397
|
+
gitea_ssh_port=git_config.ssh_port,
|
|
398
|
+
gitea_token_env=git_config.token_env,
|
|
399
|
+
)
|
|
356
400
|
|
|
357
401
|
|
|
358
402
|
# =============================================================================
|
|
@@ -401,14 +445,14 @@ def create_initial_commit(
|
|
|
401
445
|
def setup_git_for_site(
|
|
402
446
|
app_dir: Path,
|
|
403
447
|
site_slug: str,
|
|
404
|
-
|
|
448
|
+
git_config: GitConfig | None,
|
|
405
449
|
) -> str | None:
|
|
406
450
|
"""Set up git repository for a new site.
|
|
407
451
|
|
|
408
452
|
Args:
|
|
409
453
|
app_dir: Path to the app directory.
|
|
410
454
|
site_slug: The site slug (used as repo name).
|
|
411
|
-
|
|
455
|
+
git_config: Git configuration, or None to skip remote.
|
|
412
456
|
|
|
413
457
|
Returns:
|
|
414
458
|
Remote repo URL if created, None if skipped.
|
|
@@ -419,45 +463,20 @@ def setup_git_for_site(
|
|
|
419
463
|
# Create initial commit
|
|
420
464
|
create_initial_commit(app_dir)
|
|
421
465
|
|
|
422
|
-
if
|
|
466
|
+
if git_config is None:
|
|
423
467
|
OutputFormatter.info("Skipping remote repository creation (--no-git)")
|
|
424
468
|
return None
|
|
425
469
|
|
|
426
|
-
# Get the
|
|
427
|
-
provider =
|
|
470
|
+
# Get the provider from config
|
|
471
|
+
provider = get_git_provider_from_config(git_config)
|
|
428
472
|
|
|
429
473
|
# Check if provider is available
|
|
430
474
|
if not provider.is_available():
|
|
431
475
|
OutputFormatter.warning(
|
|
432
|
-
f"{provider.name} is not available
|
|
433
|
-
"Skipping repository creation."
|
|
476
|
+
f"{provider.name} is not available. " "Skipping repository creation."
|
|
434
477
|
)
|
|
435
478
|
return None
|
|
436
479
|
|
|
437
480
|
# Create remote repo
|
|
438
481
|
repo_url = provider.create_repo(app_dir, site_slug)
|
|
439
482
|
return repo_url
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
# =============================================================================
|
|
443
|
-
# Backward Compatibility
|
|
444
|
-
# =============================================================================
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
# Keep old function names for backward compatibility
|
|
448
|
-
def check_gh_cli() -> bool:
|
|
449
|
-
"""Check if gh CLI is available and authenticated.
|
|
450
|
-
|
|
451
|
-
Deprecated: Use get_git_provider().is_available() instead.
|
|
452
|
-
"""
|
|
453
|
-
provider = GitHubProvider(get_system_config().agency)
|
|
454
|
-
return provider.is_available()
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
def create_github_repo(app_dir: Path, repo_name: str) -> str:
|
|
458
|
-
"""Create a private GitHub repository and push the code.
|
|
459
|
-
|
|
460
|
-
Deprecated: Use get_git_provider().create_repo() instead.
|
|
461
|
-
"""
|
|
462
|
-
provider = GitHubProvider(get_system_config().agency)
|
|
463
|
-
return provider.create_repo(app_dir, repo_name)
|
sum/setup/orchestrator.py
CHANGED
|
@@ -6,7 +6,7 @@ import shutil
|
|
|
6
6
|
import subprocess
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from enum import
|
|
9
|
+
from enum import StrEnum
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from sum.config import SetupConfig
|
|
@@ -33,7 +33,7 @@ class SetupResult:
|
|
|
33
33
|
url: str = "http://127.0.0.1:8000/"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class SetupStep(
|
|
36
|
+
class SetupStep(StrEnum):
|
|
37
37
|
"""Ordered, typed identifiers for setup steps."""
|
|
38
38
|
|
|
39
39
|
SCAFFOLD = "Scaffolding structure"
|