itw-python-builder 0.1.25__tar.gz → 0.1.27__tar.gz
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.
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/PKG-INFO +1 -1
- itw_python_builder-0.1.27/itw_python_builder/ssr_tasks.py +161 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder/tasks.py +92 -217
- itw_python_builder-0.1.27/itw_python_builder/templates/server.sitemap.snippet.ts +12 -0
- itw_python_builder-0.1.27/itw_python_builder/templates/sitemap.routes.ts +35 -0
- itw_python_builder-0.1.27/itw_python_builder/utils.py +225 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/PKG-INFO +1 -1
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/SOURCES.txt +5 -1
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/pyproject.toml +2 -2
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/LICENSE +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/README.md +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder/.pylintrc +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder/__init__.py +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder/cli.py +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder/version.py +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/dependency_links.txt +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/entry_points.txt +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/requires.txt +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/top_level.txt +0 -0
- {itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/setup.cfg +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""SSR scaffolding tasks. One-time `itw ssr-init` for Angular SSR + ngx-seo-helper.
|
|
2
|
+
|
|
3
|
+
The build/version tasks (`incrementrc`, `release`, etc.) live in tasks.py and
|
|
4
|
+
take an `ssr=False` kwarg. With --ssr they invoke:
|
|
5
|
+
- `npm run build:ssr` on master
|
|
6
|
+
- `npm run build:ssr:staging` on any other branch
|
|
7
|
+
|
|
8
|
+
These two scripts are a contract the project owner defines in package.json.
|
|
9
|
+
ssr-init intentionally does NOT write them, because the right command depends
|
|
10
|
+
on which Angular SSR setup the project ended up with:
|
|
11
|
+
|
|
12
|
+
legacy browser builder → ng build --configuration=X && ng run <name>:server:X
|
|
13
|
+
modern application builder (Angular 17+, fresh `ng add @angular/ssr`)
|
|
14
|
+
→ ng build --configuration=X
|
|
15
|
+
"""
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from invoke import task, Context
|
|
21
|
+
|
|
22
|
+
from itw_python_builder.utils import detect_project_type, _read_package_name, is_ssr_project
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TEMPLATES_DIR = Path(__file__).parent / 'templates'
|
|
26
|
+
|
|
27
|
+
SITEMAP_IMPORT_LINE = "import {sitemapEntries} from './src/sitemap.routes';"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_template(name: str) -> str:
|
|
31
|
+
"""Read a template file, stripping the leading `// @ts-nocheck` editor hint."""
|
|
32
|
+
content = (TEMPLATES_DIR / name).read_text(encoding='utf-8')
|
|
33
|
+
lines = content.splitlines(keepends=True)
|
|
34
|
+
if lines and lines[0].lstrip().startswith('// @ts-nocheck'):
|
|
35
|
+
lines = lines[1:]
|
|
36
|
+
if lines and lines[0].strip() == '':
|
|
37
|
+
lines = lines[1:]
|
|
38
|
+
return ''.join(lines)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_ng_add_ssr(ctx: Context) -> None:
|
|
42
|
+
print('[itw] Running `ng add @angular/ssr` (this may take a minute)...')
|
|
43
|
+
ctx.run('npx ng add @angular/ssr --skip-confirmation --server-routing=false')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _install_ngx_seo_helper(ctx: Context) -> None:
|
|
47
|
+
print('[itw] Installing ngx-seo-helper...')
|
|
48
|
+
ctx.run('npm install ngx-seo-helper')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _write_sitemap_template() -> None:
|
|
52
|
+
path = os.path.join(os.getcwd(), 'src', 'sitemap.routes.ts')
|
|
53
|
+
if os.path.exists(path):
|
|
54
|
+
print(f'[itw] Skipping sitemap template — {path} already exists.')
|
|
55
|
+
return
|
|
56
|
+
with open(path, 'w', encoding='utf-8') as fp:
|
|
57
|
+
fp.write(_load_template('sitemap.routes.ts'))
|
|
58
|
+
print('[itw] Wrote src/sitemap.routes.ts (empty tree — fill in your routes).')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _patch_server_ts() -> None:
|
|
62
|
+
"""Inject sitemap import + /sitemap.xml handler into server.ts (idempotent)."""
|
|
63
|
+
path = os.path.join(os.getcwd(), 'server.ts')
|
|
64
|
+
if not os.path.exists(path):
|
|
65
|
+
raise RuntimeError('server.ts missing after `ng add @angular/ssr` — cannot patch.')
|
|
66
|
+
|
|
67
|
+
with open(path, 'r', encoding='utf-8') as fp:
|
|
68
|
+
content = fp.read()
|
|
69
|
+
|
|
70
|
+
if 'sitemap.routes' in content:
|
|
71
|
+
print('[itw] server.ts already references sitemap.routes — skipping patch.')
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
lines = content.splitlines(keepends=True)
|
|
75
|
+
last_import_idx = -1
|
|
76
|
+
for i, line in enumerate(lines):
|
|
77
|
+
if line.startswith('import '):
|
|
78
|
+
last_import_idx = i
|
|
79
|
+
if last_import_idx == -1:
|
|
80
|
+
raise RuntimeError('server.ts has no imports — unexpected shape, aborting patch.')
|
|
81
|
+
lines.insert(last_import_idx + 1, SITEMAP_IMPORT_LINE + '\n')
|
|
82
|
+
content = ''.join(lines)
|
|
83
|
+
|
|
84
|
+
match = re.search(r"(server\.set\('views',\s*distFolder\);\s*\n)", content)
|
|
85
|
+
if not match:
|
|
86
|
+
match = re.search(r"(const\s+commonEngine\s*=\s*new\s+CommonEngine\(\);\s*\n)", content)
|
|
87
|
+
if not match:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
'Could not find an injection point in server.ts (looked for `server.set(\'views\', ...)` '
|
|
90
|
+
'or `new CommonEngine()`). Patch sitemap handler manually.'
|
|
91
|
+
)
|
|
92
|
+
insert_at = match.end()
|
|
93
|
+
content = content[:insert_at] + _load_template('server.sitemap.snippet.ts') + content[insert_at:]
|
|
94
|
+
|
|
95
|
+
with open(path, 'w', encoding='utf-8') as fp:
|
|
96
|
+
fp.write(content)
|
|
97
|
+
print('[itw] Patched server.ts with /sitemap.xml handler.')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _print_next_steps(project_name: str) -> None:
|
|
101
|
+
print()
|
|
102
|
+
print('=' * 70)
|
|
103
|
+
print('[itw] SSR scaffolding complete.')
|
|
104
|
+
print('=' * 70)
|
|
105
|
+
print('Next steps you must do manually:')
|
|
106
|
+
print()
|
|
107
|
+
print(' 1. Add `NgxSeoModule` to your AppModule imports.')
|
|
108
|
+
print(' 2. Fill in src/sitemap.routes.ts with your route tree.')
|
|
109
|
+
print(' 3. Add `staging` (and optionally `development`) configurations to')
|
|
110
|
+
print(' angular.json under projects.' + project_name + '.architect.build.configurations.')
|
|
111
|
+
print(' ng add @angular/ssr only creates the `production` configuration.')
|
|
112
|
+
print()
|
|
113
|
+
print(' 4. Add these two npm scripts to package.json — the deploy pipeline')
|
|
114
|
+
print(' calls them by name:')
|
|
115
|
+
print()
|
|
116
|
+
print(' "build:ssr": "<build command for production>"')
|
|
117
|
+
print(' "build:ssr:staging": "<build command for staging>"')
|
|
118
|
+
print()
|
|
119
|
+
print(' The exact command depends on which builder angular.json uses:')
|
|
120
|
+
print()
|
|
121
|
+
print(' • Modern application builder (default for fresh ng add @angular/ssr):')
|
|
122
|
+
print(' "build:ssr": "ng build --configuration=production"')
|
|
123
|
+
print(' "build:ssr:staging": "ng build --configuration=staging"')
|
|
124
|
+
print()
|
|
125
|
+
print(' • Legacy browser builder (older projects with a separate server target):')
|
|
126
|
+
print(f' "build:ssr": "ng build --configuration=production && ng run {project_name}:server:production"')
|
|
127
|
+
print(f' "build:ssr:staging": "ng build --configuration=staging && ng run {project_name}:server:staging"')
|
|
128
|
+
print()
|
|
129
|
+
print(' 5. Verify locally: npm run build:ssr && npm run serve:ssr')
|
|
130
|
+
print(' 6. Commit the new files.')
|
|
131
|
+
print(' 7. Deploy with: itw incrementrc --ssr (or incrementpatch/release --ssr)')
|
|
132
|
+
print('=' * 70)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@task(name='ssr-init')
|
|
136
|
+
def ssr_init(ctx: Context, force: bool = False) -> None:
|
|
137
|
+
"""One-time SSR scaffold: ng add @angular/ssr, ngx-seo-helper, sitemap stub, server.ts patch.
|
|
138
|
+
|
|
139
|
+
Does NOT write package.json scripts — those depend on which builder Angular
|
|
140
|
+
chose. The post-run message tells you the two scripts the pipeline expects.
|
|
141
|
+
"""
|
|
142
|
+
if detect_project_type() != 'frontend':
|
|
143
|
+
raise RuntimeError('ssr-init only runs on frontend (Angular) projects.')
|
|
144
|
+
|
|
145
|
+
if is_ssr_project() and not force:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
'SSR scaffolding already present (server.ts + tsconfig.server.json exist). '
|
|
148
|
+
'Re-run with --force to re-apply the sitemap stub and server.ts patch.'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
name = _read_package_name()
|
|
152
|
+
print(f'[itw] Scaffolding SSR for project: {name}')
|
|
153
|
+
|
|
154
|
+
if not is_ssr_project():
|
|
155
|
+
_run_ng_add_ssr(ctx)
|
|
156
|
+
|
|
157
|
+
_install_ngx_seo_helper(ctx)
|
|
158
|
+
_write_sitemap_template()
|
|
159
|
+
_patch_server_ts()
|
|
160
|
+
|
|
161
|
+
_print_next_steps(name)
|
|
@@ -1,216 +1,87 @@
|
|
|
1
1
|
from invoke import task, Context
|
|
2
2
|
from itw_python_builder.version import Version
|
|
3
|
+
from itw_python_builder.utils import (
|
|
4
|
+
detect_project_type,
|
|
5
|
+
detect_and_activate_venv,
|
|
6
|
+
load_env,
|
|
7
|
+
get_current_branch,
|
|
8
|
+
check_branch,
|
|
9
|
+
read_version,
|
|
10
|
+
save_version,
|
|
11
|
+
generate_image_path,
|
|
12
|
+
get_gitlab_username,
|
|
13
|
+
_parse_gitlab_remote,
|
|
14
|
+
is_ssr_project,
|
|
15
|
+
)
|
|
16
|
+
from itw_python_builder.ssr_tasks import * # noqa: F401,F403 — exposes ssr-init
|
|
17
|
+
|
|
3
18
|
import getpass
|
|
4
19
|
import io
|
|
5
20
|
import os
|
|
6
|
-
import re
|
|
7
21
|
from datetime import datetime
|
|
8
22
|
from pathlib import Path
|
|
9
23
|
|
|
10
24
|
PYLINTRC = Path(__file__).parent / ".pylintrc"
|
|
11
|
-
_venv_activated = False
|
|
12
|
-
|
|
13
|
-
def detect_project_type() -> str:
|
|
14
|
-
"""Detect whether the current directory is a 'frontend' or 'backend' project."""
|
|
15
|
-
cwd = os.getcwd()
|
|
16
|
-
has_package_json = os.path.isfile(os.path.join(cwd, 'package.json'))
|
|
17
|
-
has_manage_py = os.path.isfile(os.path.join(cwd, 'manage.py'))
|
|
18
|
-
|
|
19
|
-
if has_package_json and has_manage_py:
|
|
20
|
-
raise RuntimeError(
|
|
21
|
-
'Ambiguous project: both package.json and manage.py found in current directory.'
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
if has_package_json:
|
|
25
|
-
if not os.path.isdir(os.path.join(cwd, 'node_modules')):
|
|
26
|
-
raise RuntimeError(
|
|
27
|
-
'Found package.json but no node_modules/ directory.\n'
|
|
28
|
-
'Install dependencies with: npm install'
|
|
29
|
-
)
|
|
30
|
-
return 'frontend'
|
|
31
|
-
|
|
32
|
-
if has_manage_py:
|
|
33
|
-
has_venv = (
|
|
34
|
-
os.path.isdir(os.path.join(cwd, '.venv'))
|
|
35
|
-
or os.path.isdir(os.path.join(cwd, 'venv'))
|
|
36
|
-
)
|
|
37
|
-
if not has_venv:
|
|
38
|
-
raise RuntimeError(
|
|
39
|
-
'Found manage.py but no virtual environment (venv or .venv) in current directory.\n'
|
|
40
|
-
'Create one with: python -m venv .venv'
|
|
41
|
-
)
|
|
42
|
-
return 'backend'
|
|
43
|
-
|
|
44
|
-
raise RuntimeError(
|
|
45
|
-
'Could not detect project type: no package.json or manage.py in current directory.'
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def detect_and_activate_venv():
|
|
50
|
-
"""Detect venv in current directory and prepend to PATH. Runs only once."""
|
|
51
|
-
global _venv_activated
|
|
52
|
-
if _venv_activated:
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
cwd = os.getcwd()
|
|
56
|
-
found = []
|
|
57
|
-
for venv_name in ['.venv', 'venv']:
|
|
58
|
-
venv_path = os.path.join(cwd, venv_name)
|
|
59
|
-
if os.path.isdir(venv_path):
|
|
60
|
-
bin_dir = os.path.join(venv_path, 'bin')
|
|
61
|
-
if os.path.isdir(bin_dir):
|
|
62
|
-
found.append(venv_path)
|
|
63
|
-
|
|
64
|
-
if len(found) == 0:
|
|
65
|
-
raise RuntimeError(
|
|
66
|
-
'No virtual environment found in current directory.\n'
|
|
67
|
-
'Create one with: python -m venv .venv'
|
|
68
|
-
)
|
|
69
|
-
if len(found) > 1:
|
|
70
|
-
raise RuntimeError(
|
|
71
|
-
f'Multiple virtual environments found: {", ".join(found)}\n'
|
|
72
|
-
'Remove one and keep only .venv or venv.'
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
venv_path = found[0]
|
|
76
|
-
bin_dir = os.path.join(venv_path, 'bin')
|
|
77
|
-
os.environ['PATH'] = bin_dir + os.pathsep + os.environ.get('PATH', '')
|
|
78
|
-
os.environ['VIRTUAL_ENV'] = venv_path
|
|
79
|
-
print(f"[itw] Using venv: {venv_path}")
|
|
80
|
-
_venv_activated = True
|
|
81
|
-
|
|
82
|
-
def _parse_angular_env_file(path: str) -> dict:
|
|
83
|
-
"""Extract string-valued keys from an Angular environment.ts object literal."""
|
|
84
|
-
with open(path, 'r', encoding='utf-8') as fp:
|
|
85
|
-
content = fp.read()
|
|
86
|
-
pattern = re.compile(r"""^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*['"]([^'"]*)['"]""", re.MULTILINE)
|
|
87
|
-
return dict(pattern.findall(content))
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def load_env(ctx: Context = None):
|
|
91
|
-
"""Load env vars. Backend → .env in cwd. Frontend → src/environments/environment[.<branch>].ts."""
|
|
92
|
-
if detect_project_type() == 'frontend':
|
|
93
|
-
env_dir = os.path.join(os.getcwd(), 'src', 'environments')
|
|
94
|
-
candidates = []
|
|
95
|
-
if ctx is not None:
|
|
96
|
-
branch = get_current_branch(ctx)
|
|
97
|
-
candidates.append(os.path.join(env_dir, f'environment.{branch}.ts'))
|
|
98
|
-
candidates.append(os.path.join(env_dir, 'environment.ts'))
|
|
99
|
-
env_path = next((p for p in candidates if os.path.exists(p)), None)
|
|
100
|
-
if not env_path:
|
|
101
|
-
return
|
|
102
|
-
for key, value in _parse_angular_env_file(env_path).items():
|
|
103
|
-
os.environ.setdefault(key, value)
|
|
104
|
-
return
|
|
105
|
-
from decouple import Config, RepositoryEnv
|
|
106
|
-
env_path = os.path.join(os.getcwd(), '.env')
|
|
107
|
-
if not os.path.exists(env_path):
|
|
108
|
-
return
|
|
109
|
-
config = Config(RepositoryEnv(env_path))
|
|
110
|
-
for key, value in config.repository.data.items():
|
|
111
|
-
os.environ.setdefault(key, value)
|
|
112
|
-
|
|
113
|
-
def get_current_branch(ctx: Context) -> str:
|
|
114
|
-
branch_result = ctx.run('git rev-parse --abbrev-ref HEAD')
|
|
115
|
-
current_branch = branch_result.stdout.splitlines()[0]
|
|
116
|
-
return current_branch
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def is_branch_production(branch: str) -> bool:
|
|
120
|
-
if branch == 'master':
|
|
121
|
-
return True
|
|
122
|
-
else:
|
|
123
|
-
return False
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def check_branch(ctx: Context) -> bool:
|
|
127
|
-
current_branch = get_current_branch(ctx)
|
|
128
|
-
production = is_branch_production(current_branch)
|
|
129
|
-
changed_result = ctx.run('git diff --quiet HEAD $REF -- $DIR || echo changed')
|
|
130
|
-
changed = changed_result.stdout.splitlines() # empty list if not changed
|
|
131
|
-
if len(changed) > 0:
|
|
132
|
-
raise RuntimeError(f'You have uncommitted changes in branch {current_branch}.')
|
|
133
|
-
if production and current_branch != 'master':
|
|
134
|
-
raise RuntimeError('You must be in master branch to release in production.')
|
|
135
|
-
elif not production and current_branch != 'staging':
|
|
136
|
-
raise RuntimeError(f'Cannot tag in branch {current_branch}.')
|
|
137
|
-
else:
|
|
138
|
-
return production
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def get_latest_tag(ctx: Context) -> Version:
|
|
142
|
-
result = ctx.run('git describe --tags $(git rev-list --tags --max-count=1)')
|
|
143
|
-
latest_tag = result.stdout.splitlines()[0]
|
|
144
|
-
return Version.parse(latest_tag)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _update_package_json_version(version: Version) -> None:
|
|
148
|
-
"""Rewrite only the `"version": "..."` line in package.json, preserving formatting."""
|
|
149
|
-
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
150
|
-
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
151
|
-
content = fp.read()
|
|
152
|
-
new_content, n = re.subn(
|
|
153
|
-
r'(^\s*"version"\s*:\s*")[^"]*(")',
|
|
154
|
-
lambda m: f'{m.group(1)}{version.bare_semver()}{m.group(2)}',
|
|
155
|
-
content,
|
|
156
|
-
count=1,
|
|
157
|
-
flags=re.MULTILINE,
|
|
158
|
-
)
|
|
159
|
-
if n == 0:
|
|
160
|
-
raise RuntimeError(f'Could not find "version" field in {pkg_path}')
|
|
161
|
-
with open(pkg_path, 'w', encoding='utf-8') as fp:
|
|
162
|
-
fp.write(new_content)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def read_version(ctx: Context) -> Version:
|
|
166
|
-
"""Read current version. Backend → VERSION file. Frontend → latest git tag."""
|
|
167
|
-
if detect_project_type() == 'frontend':
|
|
168
|
-
return get_latest_tag(ctx)
|
|
169
|
-
return Version.read_from_file()
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def save_version(version: Version) -> None:
|
|
173
|
-
"""Persist version. Backend → VERSION file. Frontend → package.json (bare semver)."""
|
|
174
|
-
if detect_project_type() == 'frontend':
|
|
175
|
-
_update_package_json_version(version)
|
|
176
|
-
return
|
|
177
|
-
version.save_to_file()
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def generate_image_path(ctx: Context, branch: str) -> str:
|
|
181
|
-
result = ctx.run('git remote get-url origin')
|
|
182
|
-
result = result.stdout.splitlines()[0]
|
|
183
|
-
container_registry = result.split(':')[-1].replace('.git', '')
|
|
184
|
-
container_registry_path = f'gitreg.it-works.io:443/{container_registry}:{branch}-latest'
|
|
185
|
-
return container_registry_path
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def get_gitlab_username(ctx: Context) -> str:
|
|
189
|
-
"""Resolve the GitLab username from git config user.email (local part before '@')."""
|
|
190
|
-
result = ctx.run('git config user.email', hide=True, warn=True)
|
|
191
|
-
email = result.stdout.strip() if result.ok else ''
|
|
192
|
-
if not email or '@' not in email:
|
|
193
|
-
raise RuntimeError(
|
|
194
|
-
'Could not determine GitLab username from git config user.email. '
|
|
195
|
-
'Set it with: git config --global user.email "you@email.com" '
|
|
196
|
-
'or pass --username=... explicitly.'
|
|
197
|
-
)
|
|
198
|
-
return email.split('@')[0]
|
|
199
25
|
|
|
200
26
|
|
|
201
27
|
@task
|
|
202
28
|
def login(ctx: Context, username=None):
|
|
203
|
-
"""
|
|
29
|
+
"""Capture GitLab token. Backend also logs into the container registry."""
|
|
204
30
|
if not username:
|
|
205
31
|
username = get_gitlab_username(ctx)
|
|
206
|
-
|
|
207
|
-
token = getpass.getpass(f'Enter GitLab token (password) for {username}: ')
|
|
32
|
+
token = getpass.getpass(f'Enter GitLab token for {username}: ')
|
|
208
33
|
if not token:
|
|
209
34
|
raise RuntimeError('No token entered; aborting login.')
|
|
35
|
+
os.environ['GITLAB_TOKEN'] = token
|
|
36
|
+
os.environ['GITLAB_USERNAME'] = username
|
|
37
|
+
if detect_project_type() == 'backend':
|
|
38
|
+
print(f'Logging in to gitreg.it-works.io:443 as {username}')
|
|
39
|
+
ctx.run(
|
|
40
|
+
f'podman login -u {username} --password-stdin gitreg.it-works.io:443 --tls-verify=false',
|
|
41
|
+
in_stream=io.StringIO(token)
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
print(f'[itw] GitLab token captured for {username} (will be used for package upload).')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_frontend(ctx: Context, branch: str, ssr: bool = False) -> None:
|
|
48
|
+
"""Build the Angular app. With ssr=True uses the SSR npm scripts."""
|
|
49
|
+
if ssr:
|
|
50
|
+
if not is_ssr_project():
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
'Passed --ssr but project is not SSR-scaffolded '
|
|
53
|
+
'(missing server.ts / tsconfig.server.json). Run `itw ssr-init` first.'
|
|
54
|
+
)
|
|
55
|
+
script = 'build:ssr' if branch == 'master' else 'build:ssr:staging'
|
|
56
|
+
ctx.run(f'npm run {script}')
|
|
57
|
+
return
|
|
58
|
+
config = 'production' if branch == 'master' else 'staging'
|
|
59
|
+
ctx.run(f'npx ng build --configuration={config}')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def package_dist(ctx: Context) -> None:
|
|
63
|
+
"""Tar the dist/ output directory into dist.tar.gz."""
|
|
64
|
+
if not os.path.isdir(os.path.join(os.getcwd(), 'dist')):
|
|
65
|
+
raise RuntimeError('No dist/ directory found after build — did `ng build` succeed?')
|
|
66
|
+
ctx.run('tar -czf dist.tar.gz -C dist .')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def upload_dist(ctx: Context, version) -> None:
|
|
70
|
+
"""Upload dist.tar.gz to this project's GitLab Generic Package Registry under <version>/."""
|
|
71
|
+
from urllib.parse import quote
|
|
72
|
+
if not os.environ.get('GITLAB_TOKEN'):
|
|
73
|
+
raise RuntimeError('GITLAB_TOKEN not set; run `itw login` first.')
|
|
74
|
+
host, path = _parse_gitlab_remote(ctx)
|
|
75
|
+
encoded = quote(path, safe='')
|
|
76
|
+
url = f'https://{host}/api/v4/projects/{encoded}/packages/generic/frontend/{version}/dist.tar.gz'
|
|
77
|
+
print(f'[itw] Uploading dist.tar.gz → {url}')
|
|
210
78
|
ctx.run(
|
|
211
|
-
|
|
212
|
-
|
|
79
|
+
'curl --fail --silent --show-error --insecure '
|
|
80
|
+
'--header "PRIVATE-TOKEN: $GITLAB_TOKEN" '
|
|
81
|
+
'--upload-file dist.tar.gz '
|
|
82
|
+
f'"{url}"'
|
|
213
83
|
)
|
|
84
|
+
print('[itw] Upload complete.')
|
|
214
85
|
|
|
215
86
|
|
|
216
87
|
def commit_version(ctx: Context, version: Version) -> None:
|
|
@@ -246,7 +117,7 @@ def pushimage(ctx: Context):
|
|
|
246
117
|
ctx.run(f'podman push {container_registry_path} --tls-verify=false --compress --compression-level=9')
|
|
247
118
|
|
|
248
119
|
|
|
249
|
-
def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False) -> None:
|
|
120
|
+
def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False, ssr: bool = False) -> None:
|
|
250
121
|
project_type = detect_project_type()
|
|
251
122
|
if not skip_pipeline:
|
|
252
123
|
test(ctx)
|
|
@@ -255,6 +126,11 @@ def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False)
|
|
|
255
126
|
if project_type == 'backend':
|
|
256
127
|
buildimage(ctx)
|
|
257
128
|
pushimage(ctx)
|
|
129
|
+
else:
|
|
130
|
+
branch = get_current_branch(ctx)
|
|
131
|
+
build_frontend(ctx, branch, ssr=ssr)
|
|
132
|
+
package_dist(ctx)
|
|
133
|
+
upload_dist(ctx, version)
|
|
258
134
|
changelog(ctx, version)
|
|
259
135
|
push(ctx)
|
|
260
136
|
|
|
@@ -269,67 +145,65 @@ def taginit(ctx: Context) -> None:
|
|
|
269
145
|
|
|
270
146
|
|
|
271
147
|
@task
|
|
272
|
-
def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
|
|
273
|
-
"""Increment release candidate version and deploy"""
|
|
148
|
+
def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None, ssr=False) -> None:
|
|
149
|
+
"""Increment release candidate version and deploy. Use --ssr for Angular SSR builds."""
|
|
274
150
|
check_branch(ctx)
|
|
151
|
+
login(ctx)
|
|
275
152
|
project_type = detect_project_type()
|
|
276
|
-
if project_type == 'backend':
|
|
277
|
-
login(ctx)
|
|
278
153
|
version = read_version(ctx)
|
|
279
154
|
version.increment_release_candidate()
|
|
280
155
|
if project_type == 'backend':
|
|
281
156
|
lint(ctx, pylintrc=pylintrc)
|
|
282
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
157
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
283
158
|
save_version(version)
|
|
284
159
|
commit_version(ctx, version)
|
|
285
160
|
push_version(ctx)
|
|
286
161
|
|
|
287
162
|
|
|
288
163
|
@task
|
|
289
|
-
def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
|
|
290
|
-
"""Increment patch version, build, and deploy"""
|
|
164
|
+
def incrementpatch(ctx: Context, skip_pipeline=False, ssr=False) -> None:
|
|
165
|
+
"""Increment patch version, build, and deploy. Use --ssr for Angular SSR builds."""
|
|
291
166
|
production = check_branch(ctx)
|
|
292
167
|
version = read_version(ctx)
|
|
293
168
|
version.increment_patch(release=production)
|
|
294
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
169
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
295
170
|
save_version(version)
|
|
296
171
|
commit_version(ctx, version)
|
|
297
172
|
push_version(ctx)
|
|
298
173
|
|
|
299
174
|
|
|
300
175
|
@task
|
|
301
|
-
def incrementminor(ctx: Context, skip_pipeline=False) -> None:
|
|
302
|
-
"""Increment minor version, build, and deploy"""
|
|
176
|
+
def incrementminor(ctx: Context, skip_pipeline=False, ssr=False) -> None:
|
|
177
|
+
"""Increment minor version, build, and deploy. Use --ssr for Angular SSR builds."""
|
|
303
178
|
production = check_branch(ctx)
|
|
304
179
|
version = read_version(ctx)
|
|
305
180
|
version.increment_minor(release=production)
|
|
306
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
181
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
307
182
|
save_version(version)
|
|
308
183
|
commit_version(ctx, version)
|
|
309
184
|
push_version(ctx)
|
|
310
185
|
|
|
311
186
|
|
|
312
187
|
@task
|
|
313
|
-
def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
|
|
314
|
-
"""Increment major version, build, and deploy"""
|
|
188
|
+
def incrementmajor(ctx: Context, skip_pipeline=False, ssr=False) -> None:
|
|
189
|
+
"""Increment major version, build, and deploy. Use --ssr for Angular SSR builds."""
|
|
315
190
|
production = check_branch(ctx)
|
|
316
191
|
version = read_version(ctx)
|
|
317
192
|
version.increment_major(release=production)
|
|
318
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
193
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
319
194
|
save_version(version)
|
|
320
195
|
commit_version(ctx, version)
|
|
321
196
|
push_version(ctx)
|
|
322
197
|
|
|
323
198
|
|
|
324
199
|
@task
|
|
325
|
-
def release(ctx: Context, skip_pipeline=False) -> None:
|
|
326
|
-
"""Promote release candidate to stable release and deploy"""
|
|
200
|
+
def release(ctx: Context, skip_pipeline=False, ssr=False) -> None:
|
|
201
|
+
"""Promote release candidate to stable release and deploy. Use --ssr for Angular SSR builds."""
|
|
327
202
|
_ = check_branch(ctx)
|
|
328
|
-
|
|
329
|
-
login(ctx)
|
|
203
|
+
login(ctx)
|
|
330
204
|
version = read_version(ctx)
|
|
331
205
|
version.reset_release_candidate(release=True)
|
|
332
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
206
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
333
207
|
save_version(version)
|
|
334
208
|
commit_version(ctx, version)
|
|
335
209
|
push_version(ctx)
|
|
@@ -406,10 +280,11 @@ def analyze(ctx: Context):
|
|
|
406
280
|
|
|
407
281
|
|
|
408
282
|
@task
|
|
409
|
-
def buildlocal(ctx: Context):
|
|
410
|
-
"""Build locally (Docker image for backend,
|
|
283
|
+
def buildlocal(ctx: Context, ssr=False):
|
|
284
|
+
"""Build locally (Docker image for backend, ng build for frontend). Use --ssr for SSR build."""
|
|
411
285
|
if detect_project_type() == 'frontend':
|
|
412
|
-
|
|
286
|
+
branch = get_current_branch(ctx)
|
|
287
|
+
build_frontend(ctx, branch, ssr=ssr)
|
|
413
288
|
print('✓ Built frontend (dist/)')
|
|
414
289
|
return
|
|
415
290
|
current_branch = get_current_branch(ctx)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @ts-nocheck — template fragment, injected into a file where express is imported
|
|
2
|
+
|
|
3
|
+
const sitemapHandler = (req: express.Request, res: express.Response) => {
|
|
4
|
+
const origin = `https://${req.get('host')}`;
|
|
5
|
+
const now = new Date().toISOString();
|
|
6
|
+
const urls = sitemapEntries
|
|
7
|
+
.map(e => `<url><loc>${origin}${e.path}</loc><lastmod>${now}</lastmod><changefreq>${e.changefreq}</changefreq><priority>${e.priority}</priority></url>`)
|
|
8
|
+
.join('');
|
|
9
|
+
res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
server.get('/sitemap.xml', sitemapHandler);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @ts-nocheck — template file, types resolved in the target project
|
|
2
|
+
import {Routes} from '@angular/router';
|
|
3
|
+
|
|
4
|
+
// Add your real routes here. Each entry can carry sitemap metadata via
|
|
5
|
+
// data: { sitemap: { priority: 1, changefreq: 'daily' } }
|
|
6
|
+
// Children are walked recursively. Routes with ':' params are skipped.
|
|
7
|
+
type SitemapMeta = { priority?: number; changefreq?: string };
|
|
8
|
+
type SitemapEntry = { path: string; priority: number; changefreq: string };
|
|
9
|
+
|
|
10
|
+
const join = (a: string, b: string) => `/${[a,b].filter(Boolean).join('/')}`.replaceAll(/\/+/g,'/');
|
|
11
|
+
|
|
12
|
+
const flatten = (
|
|
13
|
+
base: string,
|
|
14
|
+
routes: Routes,
|
|
15
|
+
inherited: SitemapMeta = {}
|
|
16
|
+
): SitemapEntry[] =>
|
|
17
|
+
routes.flatMap(r => {
|
|
18
|
+
const merged: SitemapMeta = { changefreq: 'daily', priority: 0.8, ...inherited, ...r.data?.['sitemap'] };
|
|
19
|
+
const full = join(base, r.path || '');
|
|
20
|
+
if (r.children?.length) return flatten(full, r.children, merged);
|
|
21
|
+
return [{ path: full || '/', priority: merged.priority!, changefreq: merged.changefreq! }];
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const tree: Routes = [
|
|
25
|
+
// { path: '', data: { sitemap: { priority: 1 } } },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const sitemapEntries: SitemapEntry[] = Array.from(
|
|
29
|
+
new Map(
|
|
30
|
+
flatten('', tree)
|
|
31
|
+
.filter(e => !e.path.includes(':'))
|
|
32
|
+
.sort((a,b) => a.path.localeCompare(b.path))
|
|
33
|
+
.map(e => [e.path, e])
|
|
34
|
+
).values()
|
|
35
|
+
);
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Shared helpers used by tasks.py and ssr_tasks.py.
|
|
2
|
+
|
|
3
|
+
Only non-@task functions live here. CLI tasks stay in tasks.py / ssr_tasks.py.
|
|
4
|
+
"""
|
|
5
|
+
import getpass
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from invoke import Context
|
|
13
|
+
|
|
14
|
+
from itw_python_builder.version import Version
|
|
15
|
+
|
|
16
|
+
_venv_activated = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def detect_project_type() -> str:
|
|
20
|
+
"""Detect whether the current directory is a 'frontend' or 'backend' project."""
|
|
21
|
+
cwd = os.getcwd()
|
|
22
|
+
has_package_json = os.path.isfile(os.path.join(cwd, 'package.json'))
|
|
23
|
+
has_manage_py = os.path.isfile(os.path.join(cwd, 'manage.py'))
|
|
24
|
+
|
|
25
|
+
if has_package_json and has_manage_py:
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
'Ambiguous project: both package.json and manage.py found in current directory.'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if has_package_json:
|
|
31
|
+
if not os.path.isdir(os.path.join(cwd, 'node_modules')):
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
'Found package.json but no node_modules/ directory.\n'
|
|
34
|
+
'Install dependencies with: npm install'
|
|
35
|
+
)
|
|
36
|
+
return 'frontend'
|
|
37
|
+
|
|
38
|
+
if has_manage_py:
|
|
39
|
+
has_venv = (
|
|
40
|
+
os.path.isdir(os.path.join(cwd, '.venv'))
|
|
41
|
+
or os.path.isdir(os.path.join(cwd, 'venv'))
|
|
42
|
+
)
|
|
43
|
+
if not has_venv:
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
'Found manage.py but no virtual environment (venv or .venv) in current directory.\n'
|
|
46
|
+
'Create one with: python -m venv .venv'
|
|
47
|
+
)
|
|
48
|
+
return 'backend'
|
|
49
|
+
|
|
50
|
+
raise RuntimeError(
|
|
51
|
+
'Could not detect project type: no package.json or manage.py in current directory.'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def detect_and_activate_venv():
|
|
56
|
+
"""Detect venv in current directory and prepend to PATH. Runs only once."""
|
|
57
|
+
global _venv_activated
|
|
58
|
+
if _venv_activated:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
cwd = os.getcwd()
|
|
62
|
+
found = []
|
|
63
|
+
for venv_name in ['.venv', 'venv']:
|
|
64
|
+
venv_path = os.path.join(cwd, venv_name)
|
|
65
|
+
if os.path.isdir(venv_path):
|
|
66
|
+
bin_dir = os.path.join(venv_path, 'bin')
|
|
67
|
+
if os.path.isdir(bin_dir):
|
|
68
|
+
found.append(venv_path)
|
|
69
|
+
|
|
70
|
+
if len(found) == 0:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
'No virtual environment found in current directory.\n'
|
|
73
|
+
'Create one with: python -m venv .venv'
|
|
74
|
+
)
|
|
75
|
+
if len(found) > 1:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
f'Multiple virtual environments found: {", ".join(found)}\n'
|
|
78
|
+
'Remove one and keep only .venv or venv.'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
venv_path = found[0]
|
|
82
|
+
bin_dir = os.path.join(venv_path, 'bin')
|
|
83
|
+
os.environ['PATH'] = bin_dir + os.pathsep + os.environ.get('PATH', '')
|
|
84
|
+
os.environ['VIRTUAL_ENV'] = venv_path
|
|
85
|
+
print(f"[itw] Using venv: {venv_path}")
|
|
86
|
+
_venv_activated = True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_angular_env_file(path: str) -> dict:
|
|
90
|
+
"""Extract string-valued keys from an Angular environment.ts object literal."""
|
|
91
|
+
with open(path, 'r', encoding='utf-8') as fp:
|
|
92
|
+
content = fp.read()
|
|
93
|
+
pattern = re.compile(r"""^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*['"]([^'"]*)['"]""", re.MULTILINE)
|
|
94
|
+
return dict(pattern.findall(content))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def load_env(ctx: Context = None):
|
|
98
|
+
if detect_project_type() == 'frontend':
|
|
99
|
+
env_dir = os.path.join(os.getcwd(), 'src', 'environments')
|
|
100
|
+
candidates = []
|
|
101
|
+
if ctx is not None:
|
|
102
|
+
branch = get_current_branch(ctx)
|
|
103
|
+
candidates.append(os.path.join(env_dir, f'environment.{branch}.ts'))
|
|
104
|
+
candidates.append(os.path.join(env_dir, 'environment.ts'))
|
|
105
|
+
for path in candidates:
|
|
106
|
+
if os.path.exists(path):
|
|
107
|
+
for key, value in _parse_angular_env_file(path).items():
|
|
108
|
+
os.environ.setdefault(key, value)
|
|
109
|
+
return
|
|
110
|
+
from decouple import Config, RepositoryEnv
|
|
111
|
+
env_path = os.path.join(os.getcwd(), '.env')
|
|
112
|
+
if not os.path.exists(env_path):
|
|
113
|
+
return
|
|
114
|
+
config = Config(RepositoryEnv(env_path))
|
|
115
|
+
for key, value in config.repository.data.items():
|
|
116
|
+
os.environ.setdefault(key, value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_current_branch(ctx: Context) -> str:
|
|
120
|
+
branch_result = ctx.run('git rev-parse --abbrev-ref HEAD')
|
|
121
|
+
current_branch = branch_result.stdout.splitlines()[0]
|
|
122
|
+
return current_branch
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_branch_production(branch: str) -> bool:
|
|
126
|
+
return branch == 'master'
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_branch(ctx: Context) -> bool:
|
|
130
|
+
current_branch = get_current_branch(ctx)
|
|
131
|
+
production = is_branch_production(current_branch)
|
|
132
|
+
changed_result = ctx.run('git diff --quiet HEAD $REF -- $DIR || echo changed')
|
|
133
|
+
changed = changed_result.stdout.splitlines()
|
|
134
|
+
if len(changed) > 0:
|
|
135
|
+
raise RuntimeError(f'You have uncommitted changes in branch {current_branch}.')
|
|
136
|
+
if production and current_branch != 'master':
|
|
137
|
+
raise RuntimeError('You must be in master branch to release in production.')
|
|
138
|
+
elif not production and current_branch != 'staging':
|
|
139
|
+
raise RuntimeError(f'Cannot tag in branch {current_branch}.')
|
|
140
|
+
else:
|
|
141
|
+
return production
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_latest_tag(ctx: Context) -> Version:
|
|
145
|
+
result = ctx.run('git describe --tags $(git rev-list --tags --max-count=1)')
|
|
146
|
+
latest_tag = result.stdout.splitlines()[0]
|
|
147
|
+
return Version.parse(latest_tag)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _update_package_json_version(version: Version) -> None:
|
|
151
|
+
"""Update the top-level `version` in package.json (preserves key order)."""
|
|
152
|
+
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
153
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
154
|
+
data = json.load(fp)
|
|
155
|
+
data['version'] = version.bare_semver()
|
|
156
|
+
with open(pkg_path, 'w', encoding='utf-8') as fp:
|
|
157
|
+
json.dump(data, fp, indent=2)
|
|
158
|
+
fp.write('\n')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def read_version(ctx: Context) -> Version:
|
|
162
|
+
"""Read current version. Backend → VERSION file. Frontend → latest git tag."""
|
|
163
|
+
if detect_project_type() == 'frontend':
|
|
164
|
+
return get_latest_tag(ctx)
|
|
165
|
+
return Version.read_from_file()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def save_version(version: Version) -> None:
|
|
169
|
+
"""Persist version. Backend → VERSION file. Frontend → package.json (bare semver)."""
|
|
170
|
+
if detect_project_type() == 'frontend':
|
|
171
|
+
_update_package_json_version(version)
|
|
172
|
+
return
|
|
173
|
+
version.save_to_file()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def generate_image_path(ctx: Context, branch: str) -> str:
|
|
177
|
+
result = ctx.run('git remote get-url origin')
|
|
178
|
+
result = result.stdout.splitlines()[0]
|
|
179
|
+
container_registry = result.split(':')[-1].replace('.git', '')
|
|
180
|
+
container_registry_path = f'gitreg.it-works.io:443/{container_registry}:{branch}-latest'
|
|
181
|
+
return container_registry_path
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_gitlab_username(ctx: Context) -> str:
|
|
185
|
+
"""Resolve the GitLab username from git config user.email (local part before '@')."""
|
|
186
|
+
result = ctx.run('git config user.email', hide=True, warn=True)
|
|
187
|
+
email = result.stdout.strip() if result.ok else ''
|
|
188
|
+
if not email or '@' not in email:
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
'Could not determine GitLab username from git config user.email. '
|
|
191
|
+
'Set it with: git config --global user.email "you@email.com" '
|
|
192
|
+
'or pass --username=... explicitly.'
|
|
193
|
+
)
|
|
194
|
+
return email.split('@')[0]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _read_package_name() -> str:
|
|
198
|
+
"""Read 'name' field from package.json in cwd."""
|
|
199
|
+
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
200
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
201
|
+
return json.load(fp)['name']
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _parse_gitlab_remote(ctx: Context) -> tuple:
|
|
205
|
+
"""Return (host, project_path) parsed from `git remote get-url origin`."""
|
|
206
|
+
result = ctx.run('git remote get-url origin', hide=True)
|
|
207
|
+
url = result.stdout.strip()
|
|
208
|
+
if url.endswith('.git'):
|
|
209
|
+
url = url[:-4]
|
|
210
|
+
if url.startswith('git@'):
|
|
211
|
+
host, path = url.split('@', 1)[1].split(':', 1)
|
|
212
|
+
elif '://' in url:
|
|
213
|
+
host, path = url.split('://', 1)[1].split('/', 1)
|
|
214
|
+
else:
|
|
215
|
+
raise RuntimeError(f'Cannot parse GitLab remote URL: {url}')
|
|
216
|
+
return host, path
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def is_ssr_project() -> bool:
|
|
220
|
+
"""Return True if the current frontend project already has SSR scaffolding."""
|
|
221
|
+
cwd = os.getcwd()
|
|
222
|
+
return (
|
|
223
|
+
os.path.isfile(os.path.join(cwd, 'server.ts'))
|
|
224
|
+
and os.path.isfile(os.path.join(cwd, 'tsconfig.server.json'))
|
|
225
|
+
)
|
{itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/SOURCES.txt
RENAMED
|
@@ -4,11 +4,15 @@ pyproject.toml
|
|
|
4
4
|
itw_python_builder/.pylintrc
|
|
5
5
|
itw_python_builder/__init__.py
|
|
6
6
|
itw_python_builder/cli.py
|
|
7
|
+
itw_python_builder/ssr_tasks.py
|
|
7
8
|
itw_python_builder/tasks.py
|
|
9
|
+
itw_python_builder/utils.py
|
|
8
10
|
itw_python_builder/version.py
|
|
9
11
|
itw_python_builder.egg-info/PKG-INFO
|
|
10
12
|
itw_python_builder.egg-info/SOURCES.txt
|
|
11
13
|
itw_python_builder.egg-info/dependency_links.txt
|
|
12
14
|
itw_python_builder.egg-info/entry_points.txt
|
|
13
15
|
itw_python_builder.egg-info/requires.txt
|
|
14
|
-
itw_python_builder.egg-info/top_level.txt
|
|
16
|
+
itw_python_builder.egg-info/top_level.txt
|
|
17
|
+
itw_python_builder/templates/server.sitemap.snippet.ts
|
|
18
|
+
itw_python_builder/templates/sitemap.routes.ts
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "itw_python_builder"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.27"
|
|
8
8
|
description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -40,7 +40,7 @@ Issues = "https://git.it-works.io/"
|
|
|
40
40
|
include-package-data = true
|
|
41
41
|
|
|
42
42
|
[tool.setuptools.package-data]
|
|
43
|
-
itw_python_builder = [".pylintrc"]
|
|
43
|
+
itw_python_builder = [".pylintrc", "templates/*.ts"]
|
|
44
44
|
|
|
45
45
|
[project.scripts]
|
|
46
46
|
itw = "itw_python_builder.cli:main"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/requires.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.25 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|