itw-python-builder 0.1.26__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.26 → 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.26 → itw_python_builder-0.1.27}/itw_python_builder/tasks.py +47 -227
- 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.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/PKG-INFO +1 -1
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/SOURCES.txt +5 -1
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/pyproject.toml +2 -2
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/LICENSE +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/README.md +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder/.pylintrc +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder/__init__.py +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder/cli.py +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder/version.py +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/dependency_links.txt +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/entry_points.txt +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/requires.txt +0 -0
- {itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/top_level.txt +0 -0
- {itw_python_builder-0.1.26 → 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,193 +1,27 @@
|
|
|
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
|
-
if detect_project_type() == 'frontend':
|
|
92
|
-
env_dir = os.path.join(os.getcwd(), 'src', 'environments')
|
|
93
|
-
candidates = []
|
|
94
|
-
if ctx is not None:
|
|
95
|
-
branch = get_current_branch(ctx)
|
|
96
|
-
candidates.append(os.path.join(env_dir, f'environment.{branch}.ts'))
|
|
97
|
-
candidates.append(os.path.join(env_dir, 'environment.ts'))
|
|
98
|
-
for path in candidates:
|
|
99
|
-
if os.path.exists(path):
|
|
100
|
-
for key, value in _parse_angular_env_file(path).items():
|
|
101
|
-
os.environ.setdefault(key, value)
|
|
102
|
-
return
|
|
103
|
-
from decouple import Config, RepositoryEnv
|
|
104
|
-
env_path = os.path.join(os.getcwd(), '.env')
|
|
105
|
-
if not os.path.exists(env_path):
|
|
106
|
-
return
|
|
107
|
-
config = Config(RepositoryEnv(env_path))
|
|
108
|
-
for key, value in config.repository.data.items():
|
|
109
|
-
os.environ.setdefault(key, value)
|
|
110
|
-
|
|
111
|
-
def get_current_branch(ctx: Context) -> str:
|
|
112
|
-
branch_result = ctx.run('git rev-parse --abbrev-ref HEAD')
|
|
113
|
-
current_branch = branch_result.stdout.splitlines()[0]
|
|
114
|
-
return current_branch
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def is_branch_production(branch: str) -> bool:
|
|
118
|
-
if branch == 'master':
|
|
119
|
-
return True
|
|
120
|
-
else:
|
|
121
|
-
return False
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def check_branch(ctx: Context) -> bool:
|
|
125
|
-
current_branch = get_current_branch(ctx)
|
|
126
|
-
production = is_branch_production(current_branch)
|
|
127
|
-
changed_result = ctx.run('git diff --quiet HEAD $REF -- $DIR || echo changed')
|
|
128
|
-
changed = changed_result.stdout.splitlines() # empty list if not changed
|
|
129
|
-
if len(changed) > 0:
|
|
130
|
-
raise RuntimeError(f'You have uncommitted changes in branch {current_branch}.')
|
|
131
|
-
if production and current_branch != 'master':
|
|
132
|
-
raise RuntimeError('You must be in master branch to release in production.')
|
|
133
|
-
elif not production and current_branch != 'staging':
|
|
134
|
-
raise RuntimeError(f'Cannot tag in branch {current_branch}.')
|
|
135
|
-
else:
|
|
136
|
-
return production
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def get_latest_tag(ctx: Context) -> Version:
|
|
140
|
-
result = ctx.run('git describe --tags $(git rev-list --tags --max-count=1)')
|
|
141
|
-
latest_tag = result.stdout.splitlines()[0]
|
|
142
|
-
return Version.parse(latest_tag)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _update_package_json_version(version: Version) -> None:
|
|
146
|
-
"""Update the top-level `version` in package.json (preserves key order)."""
|
|
147
|
-
import json
|
|
148
|
-
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
149
|
-
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
150
|
-
data = json.load(fp)
|
|
151
|
-
data['version'] = version.bare_semver()
|
|
152
|
-
with open(pkg_path, 'w', encoding='utf-8') as fp:
|
|
153
|
-
json.dump(data, fp, indent=2)
|
|
154
|
-
fp.write('\n')
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def read_version(ctx: Context) -> Version:
|
|
158
|
-
"""Read current version. Backend → VERSION file. Frontend → latest git tag."""
|
|
159
|
-
if detect_project_type() == 'frontend':
|
|
160
|
-
return get_latest_tag(ctx)
|
|
161
|
-
return Version.read_from_file()
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def save_version(version: Version) -> None:
|
|
165
|
-
"""Persist version. Backend → VERSION file. Frontend → package.json (bare semver)."""
|
|
166
|
-
if detect_project_type() == 'frontend':
|
|
167
|
-
_update_package_json_version(version)
|
|
168
|
-
return
|
|
169
|
-
version.save_to_file()
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def generate_image_path(ctx: Context, branch: str) -> str:
|
|
173
|
-
result = ctx.run('git remote get-url origin')
|
|
174
|
-
result = result.stdout.splitlines()[0]
|
|
175
|
-
container_registry = result.split(':')[-1].replace('.git', '')
|
|
176
|
-
container_registry_path = f'gitreg.it-works.io:443/{container_registry}:{branch}-latest'
|
|
177
|
-
return container_registry_path
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def get_gitlab_username(ctx: Context) -> str:
|
|
181
|
-
"""Resolve the GitLab username from git config user.email (local part before '@')."""
|
|
182
|
-
result = ctx.run('git config user.email', hide=True, warn=True)
|
|
183
|
-
email = result.stdout.strip() if result.ok else ''
|
|
184
|
-
if not email or '@' not in email:
|
|
185
|
-
raise RuntimeError(
|
|
186
|
-
'Could not determine GitLab username from git config user.email. '
|
|
187
|
-
'Set it with: git config --global user.email "you@email.com" '
|
|
188
|
-
'or pass --username=... explicitly.'
|
|
189
|
-
)
|
|
190
|
-
return email.split('@')[0]
|
|
191
25
|
|
|
192
26
|
|
|
193
27
|
@task
|
|
@@ -210,31 +44,17 @@ def login(ctx: Context, username=None):
|
|
|
210
44
|
print(f'[itw] GitLab token captured for {username} (will be used for package upload).')
|
|
211
45
|
|
|
212
46
|
|
|
213
|
-
def
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
url = result.stdout.strip()
|
|
225
|
-
if url.endswith('.git'):
|
|
226
|
-
url = url[:-4]
|
|
227
|
-
if url.startswith('git@'):
|
|
228
|
-
host, path = url.split('@', 1)[1].split(':', 1)
|
|
229
|
-
elif '://' in url:
|
|
230
|
-
host, path = url.split('://', 1)[1].split('/', 1)
|
|
231
|
-
else:
|
|
232
|
-
raise RuntimeError(f'Cannot parse GitLab remote URL: {url}')
|
|
233
|
-
return host, path
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def build_frontend(ctx: Context, branch: str) -> None:
|
|
237
|
-
"""Build the Angular app. Master branch → production config; otherwise staging."""
|
|
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
|
|
238
58
|
config = 'production' if branch == 'master' else 'staging'
|
|
239
59
|
ctx.run(f'npx ng build --configuration={config}')
|
|
240
60
|
|
|
@@ -256,7 +76,7 @@ def upload_dist(ctx: Context, version) -> None:
|
|
|
256
76
|
url = f'https://{host}/api/v4/projects/{encoded}/packages/generic/frontend/{version}/dist.tar.gz'
|
|
257
77
|
print(f'[itw] Uploading dist.tar.gz → {url}')
|
|
258
78
|
ctx.run(
|
|
259
|
-
'curl --fail --silent --show-error '
|
|
79
|
+
'curl --fail --silent --show-error --insecure '
|
|
260
80
|
'--header "PRIVATE-TOKEN: $GITLAB_TOKEN" '
|
|
261
81
|
'--upload-file dist.tar.gz '
|
|
262
82
|
f'"{url}"'
|
|
@@ -297,7 +117,7 @@ def pushimage(ctx: Context):
|
|
|
297
117
|
ctx.run(f'podman push {container_registry_path} --tls-verify=false --compress --compression-level=9')
|
|
298
118
|
|
|
299
119
|
|
|
300
|
-
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:
|
|
301
121
|
project_type = detect_project_type()
|
|
302
122
|
if not skip_pipeline:
|
|
303
123
|
test(ctx)
|
|
@@ -308,7 +128,7 @@ def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False)
|
|
|
308
128
|
pushimage(ctx)
|
|
309
129
|
else:
|
|
310
130
|
branch = get_current_branch(ctx)
|
|
311
|
-
build_frontend(ctx, branch)
|
|
131
|
+
build_frontend(ctx, branch, ssr=ssr)
|
|
312
132
|
package_dist(ctx)
|
|
313
133
|
upload_dist(ctx, version)
|
|
314
134
|
changelog(ctx, version)
|
|
@@ -325,8 +145,8 @@ def taginit(ctx: Context) -> None:
|
|
|
325
145
|
|
|
326
146
|
|
|
327
147
|
@task
|
|
328
|
-
def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
|
|
329
|
-
"""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."""
|
|
330
150
|
check_branch(ctx)
|
|
331
151
|
login(ctx)
|
|
332
152
|
project_type = detect_project_type()
|
|
@@ -334,56 +154,56 @@ def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
|
|
|
334
154
|
version.increment_release_candidate()
|
|
335
155
|
if project_type == 'backend':
|
|
336
156
|
lint(ctx, pylintrc=pylintrc)
|
|
337
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
157
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
338
158
|
save_version(version)
|
|
339
159
|
commit_version(ctx, version)
|
|
340
160
|
push_version(ctx)
|
|
341
161
|
|
|
342
162
|
|
|
343
163
|
@task
|
|
344
|
-
def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
|
|
345
|
-
"""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."""
|
|
346
166
|
production = check_branch(ctx)
|
|
347
167
|
version = read_version(ctx)
|
|
348
168
|
version.increment_patch(release=production)
|
|
349
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
169
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
350
170
|
save_version(version)
|
|
351
171
|
commit_version(ctx, version)
|
|
352
172
|
push_version(ctx)
|
|
353
173
|
|
|
354
174
|
|
|
355
175
|
@task
|
|
356
|
-
def incrementminor(ctx: Context, skip_pipeline=False) -> None:
|
|
357
|
-
"""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."""
|
|
358
178
|
production = check_branch(ctx)
|
|
359
179
|
version = read_version(ctx)
|
|
360
180
|
version.increment_minor(release=production)
|
|
361
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
181
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
362
182
|
save_version(version)
|
|
363
183
|
commit_version(ctx, version)
|
|
364
184
|
push_version(ctx)
|
|
365
185
|
|
|
366
186
|
|
|
367
187
|
@task
|
|
368
|
-
def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
|
|
369
|
-
"""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."""
|
|
370
190
|
production = check_branch(ctx)
|
|
371
191
|
version = read_version(ctx)
|
|
372
192
|
version.increment_major(release=production)
|
|
373
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
193
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
374
194
|
save_version(version)
|
|
375
195
|
commit_version(ctx, version)
|
|
376
196
|
push_version(ctx)
|
|
377
197
|
|
|
378
198
|
|
|
379
199
|
@task
|
|
380
|
-
def release(ctx: Context, skip_pipeline=False) -> None:
|
|
381
|
-
"""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."""
|
|
382
202
|
_ = check_branch(ctx)
|
|
383
203
|
login(ctx)
|
|
384
204
|
version = read_version(ctx)
|
|
385
205
|
version.reset_release_candidate(release=True)
|
|
386
|
-
tag_build_push(ctx, version, skip_pipeline)
|
|
206
|
+
tag_build_push(ctx, version, skip_pipeline, ssr=ssr)
|
|
387
207
|
save_version(version)
|
|
388
208
|
commit_version(ctx, version)
|
|
389
209
|
push_version(ctx)
|
|
@@ -460,11 +280,11 @@ def analyze(ctx: Context):
|
|
|
460
280
|
|
|
461
281
|
|
|
462
282
|
@task
|
|
463
|
-
def buildlocal(ctx: Context):
|
|
464
|
-
"""Build locally (Docker image for backend, ng build for frontend)"""
|
|
283
|
+
def buildlocal(ctx: Context, ssr=False):
|
|
284
|
+
"""Build locally (Docker image for backend, ng build for frontend). Use --ssr for SSR build."""
|
|
465
285
|
if detect_project_type() == 'frontend':
|
|
466
286
|
branch = get_current_branch(ctx)
|
|
467
|
-
build_frontend(ctx, branch)
|
|
287
|
+
build_frontend(ctx, branch, ssr=ssr)
|
|
468
288
|
print('✓ Built frontend (dist/)')
|
|
469
289
|
return
|
|
470
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.26 → 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.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/requires.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.26 → itw_python_builder-0.1.27}/itw_python_builder.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|