itw-python-builder 0.2.13__py3-none-any.whl → 0.2.15__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.
- itw_python_builder/task_utils.py +93 -15
- itw_python_builder/tasks.py +17 -5
- itw_python_builder/utils.py +45 -0
- itw_python_builder-0.2.15.dist-info/METADATA +338 -0
- {itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/RECORD +9 -9
- {itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/WHEEL +1 -1
- itw_python_builder-0.2.13.dist-info/METADATA +0 -166
- {itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/entry_points.txt +0 -0
- {itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/licenses/LICENSE +0 -0
- {itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/top_level.txt +0 -0
itw_python_builder/task_utils.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import unicodedata
|
|
2
3
|
from difflib import SequenceMatcher
|
|
4
|
+
from urllib.parse import quote
|
|
3
5
|
|
|
4
6
|
from itw_python_builder.utils import _gitlab_api_call
|
|
5
7
|
|
|
6
8
|
SIMILARITY_THRESHOLD = 0.95
|
|
9
|
+
GITLAB_PAGE_SIZE = 100
|
|
10
|
+
_RE_DIGITS = re.compile(r'\d+')
|
|
7
11
|
|
|
8
12
|
DATE_PLACEHOLDER = 'YYYY-MM-DD'
|
|
9
13
|
|
|
@@ -48,7 +52,12 @@ def log_info(msg: str) -> None:
|
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
def _normalize_title(value: str) -> str:
|
|
51
|
-
|
|
55
|
+
collapsed = ' '.join((value or '').lower().split())
|
|
56
|
+
return unicodedata.normalize('NFC', collapsed)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _digit_tokens(value: str) -> list:
|
|
60
|
+
return _RE_DIGITS.findall(value or '')
|
|
52
61
|
|
|
53
62
|
|
|
54
63
|
def is_similar(left: str, right: str, threshold: float = SIMILARITY_THRESHOLD) -> bool:
|
|
@@ -59,19 +68,64 @@ def is_similar(left: str, right: str, threshold: float = SIMILARITY_THRESHOLD) -
|
|
|
59
68
|
return False
|
|
60
69
|
if a == b:
|
|
61
70
|
return True
|
|
71
|
+
if _digit_tokens(a) != _digit_tokens(b):
|
|
72
|
+
return False
|
|
62
73
|
return SequenceMatcher(None, a, b).ratio() >= threshold
|
|
63
74
|
|
|
64
75
|
|
|
65
76
|
def find_similar(value: str, items: list, key: str = 'title', threshold: float = SIMILARITY_THRESHOLD):
|
|
66
|
-
"""Return
|
|
77
|
+
"""Return an exact normalized match if one exists, else the first ~threshold match."""
|
|
78
|
+
target = _normalize_title(value)
|
|
79
|
+
fuzzy = None
|
|
67
80
|
for item in items:
|
|
68
|
-
|
|
81
|
+
other = item.get(key, '')
|
|
82
|
+
if target and target == _normalize_title(other):
|
|
69
83
|
return item
|
|
70
|
-
|
|
84
|
+
if fuzzy is None and is_similar(value, other, threshold):
|
|
85
|
+
fuzzy = item
|
|
86
|
+
return fuzzy
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _gitlab_api_list(api_host: str, endpoint: str, token: str) -> list:
|
|
90
|
+
"""GET a GitLab collection, following pages until a short page is returned."""
|
|
91
|
+
joiner = '&' if '?' in endpoint else '?'
|
|
92
|
+
page = 1
|
|
93
|
+
collected = []
|
|
94
|
+
while True:
|
|
95
|
+
chunk = _gitlab_api_call(
|
|
96
|
+
api_host,
|
|
97
|
+
f'{endpoint}{joiner}per_page={GITLAB_PAGE_SIZE}&page={page}',
|
|
98
|
+
token,
|
|
99
|
+
)
|
|
100
|
+
if not isinstance(chunk, list):
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
f'GitLab API GET {endpoint} expected a list, got {type(chunk).__name__}'
|
|
103
|
+
)
|
|
104
|
+
collected.extend(chunk)
|
|
105
|
+
if len(chunk) < GITLAB_PAGE_SIZE:
|
|
106
|
+
break
|
|
107
|
+
page += 1
|
|
108
|
+
return collected
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _issue_milestone_title(issue: dict) -> str:
|
|
112
|
+
milestone = issue.get('milestone') or {}
|
|
113
|
+
if isinstance(milestone, dict):
|
|
114
|
+
return (milestone.get('title') or '').strip()
|
|
115
|
+
return ''
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_duplicate_issue(issue: dict, title: str, milestone: str = None) -> bool:
|
|
119
|
+
"""True when titles match and the issue is in the target milestone or has none."""
|
|
120
|
+
if not is_similar(title, issue.get('title', '')):
|
|
121
|
+
return False
|
|
122
|
+
existing = _issue_milestone_title(issue)
|
|
123
|
+
if milestone:
|
|
124
|
+
return (not existing) or is_similar(milestone, existing)
|
|
125
|
+
return not existing
|
|
71
126
|
|
|
72
127
|
|
|
73
128
|
def _list_group_projects(api_host: str, group_path: str, token: str) -> list:
|
|
74
|
-
from urllib.parse import quote
|
|
75
129
|
encoded = quote(group_path, safe='')
|
|
76
130
|
return _gitlab_api_call(
|
|
77
131
|
api_host,
|
|
@@ -113,27 +167,51 @@ def create_gitlab_issue(api_host: str, project_id: int, payload: dict, token: st
|
|
|
113
167
|
|
|
114
168
|
|
|
115
169
|
def get_project_by_path(api_host: str, project_path: str, token: str) -> dict:
|
|
116
|
-
from urllib.parse import quote
|
|
117
170
|
encoded = quote(project_path, safe='')
|
|
118
171
|
return _gitlab_api_call(api_host, f'/projects/{encoded}', token)
|
|
119
172
|
|
|
120
173
|
|
|
121
174
|
def find_project_milestone(api_host: str, project_id: int, title: str, token: str):
|
|
122
|
-
|
|
175
|
+
"""Find a project or ancestor-group milestone by title."""
|
|
176
|
+
encoded = quote(title, safe='')
|
|
177
|
+
base = f'/projects/{project_id}/milestones?include_ancestors=true'
|
|
178
|
+
exact = _gitlab_api_list(api_host, f'{base}&title={encoded}', token)
|
|
179
|
+
match = find_similar(title, exact)
|
|
180
|
+
if match:
|
|
181
|
+
return match
|
|
182
|
+
searched = _gitlab_api_list(api_host, f'{base}&search={encoded}', token)
|
|
183
|
+
return find_similar(title, searched)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _list_issues_for_milestone(api_host: str, project_id: int, milestone: str, token: str) -> list:
|
|
187
|
+
encoded = quote(milestone, safe='') if milestone else 'None'
|
|
188
|
+
return _gitlab_api_list(
|
|
123
189
|
api_host,
|
|
124
|
-
f'/projects/{project_id}/
|
|
190
|
+
f'/projects/{project_id}/issues?milestone={encoded}',
|
|
125
191
|
token,
|
|
126
192
|
)
|
|
127
|
-
return find_similar(title, results)
|
|
128
193
|
|
|
129
194
|
|
|
130
195
|
def find_project_issue(api_host: str, project_id: int, title: str, token: str, milestone: str = None):
|
|
131
|
-
"""Look for a ~matching title
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
196
|
+
"""Look for a ~matching title in this milestone, or among issues with no milestone."""
|
|
197
|
+
encoded = quote(title, safe='')
|
|
198
|
+
searched = _gitlab_api_list(
|
|
199
|
+
api_host,
|
|
200
|
+
f'/projects/{project_id}/issues?search={encoded}&in=title',
|
|
201
|
+
token,
|
|
202
|
+
)
|
|
203
|
+
for item in searched:
|
|
204
|
+
if _is_duplicate_issue(item, title, milestone):
|
|
205
|
+
return item
|
|
206
|
+
|
|
207
|
+
for item in _list_issues_for_milestone(api_host, project_id, milestone, token):
|
|
208
|
+
if _is_duplicate_issue(item, title, milestone):
|
|
209
|
+
return item
|
|
210
|
+
if milestone:
|
|
211
|
+
for item in _list_issues_for_milestone(api_host, project_id, None, token):
|
|
212
|
+
if _is_duplicate_issue(item, title, milestone):
|
|
213
|
+
return item
|
|
214
|
+
return None
|
|
137
215
|
|
|
138
216
|
|
|
139
217
|
def create_project_milestone(
|
itw_python_builder/tasks.py
CHANGED
|
@@ -29,6 +29,8 @@ from itw_python_builder.utils import (
|
|
|
29
29
|
TOKEN_CACHE_PATH,
|
|
30
30
|
maybe_override_release_version,
|
|
31
31
|
ensure_gitlab_token,
|
|
32
|
+
tag_exists,
|
|
33
|
+
latest_staging_rc,
|
|
32
34
|
)
|
|
33
35
|
from itw_python_builder.task_utils import (
|
|
34
36
|
resolve_pm_repo,
|
|
@@ -453,9 +455,11 @@ def push(ctx: Context):
|
|
|
453
455
|
|
|
454
456
|
|
|
455
457
|
def buildimage(ctx: Context):
|
|
458
|
+
token = load_cached_token()
|
|
459
|
+
username = get_gitlab_username(ctx)
|
|
456
460
|
current_branch = get_current_branch(ctx)
|
|
457
461
|
container_registry_path = generate_image_path(ctx, current_branch)
|
|
458
|
-
ctx.run(f'podman build
|
|
462
|
+
ctx.run(f'podman build --build-arg GITLAB_USERNAME={username} --build-arg GITLAB_TOKEN={token} --tag={container_registry_path} .')
|
|
459
463
|
|
|
460
464
|
|
|
461
465
|
def pushimage(ctx: Context):
|
|
@@ -499,16 +503,24 @@ def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False,
|
|
|
499
503
|
|
|
500
504
|
@task(name='tag-init')
|
|
501
505
|
def taginit(ctx: Context) -> None:
|
|
502
|
-
"""Initialize version tagging"""
|
|
503
|
-
check_branch(ctx)
|
|
504
|
-
|
|
506
|
+
"""Initialize version tagging — release candidate on staging, release on master."""
|
|
507
|
+
production = check_branch(ctx)
|
|
508
|
+
if production:
|
|
509
|
+
version = latest_staging_rc(ctx)
|
|
510
|
+
log_info(f'Latest release candidate on staging: {version}')
|
|
511
|
+
version.reset_release_candidate(release=True)
|
|
512
|
+
else:
|
|
513
|
+
version = Version(0, 0, 1, 1)
|
|
514
|
+
if tag_exists(ctx, version):
|
|
515
|
+
raise RuntimeError(f'Tag {version} already exists — nothing to initialize.')
|
|
505
516
|
tag(ctx, version)
|
|
506
517
|
save_version(version)
|
|
518
|
+
log_green(f'Initialized {version} on {get_current_branch(ctx)}')
|
|
507
519
|
|
|
508
520
|
|
|
509
521
|
def _ensure_unique_tag(ctx: Context, version: Version) -> None:
|
|
510
522
|
"""If the tag already exists, auto-increment RC (RC tags) or patch (release tags)."""
|
|
511
|
-
while ctx
|
|
523
|
+
while tag_exists(ctx, version):
|
|
512
524
|
if version.release_candidate == 0:
|
|
513
525
|
print(f"[itw] Tag {version} already exists, auto-incrementing patch...")
|
|
514
526
|
version.increment_patch(release=True)
|
itw_python_builder/utils.py
CHANGED
|
@@ -254,6 +254,51 @@ def get_latest_tag(ctx: Context) -> Version:
|
|
|
254
254
|
return Version.parse(tags[0])
|
|
255
255
|
|
|
256
256
|
|
|
257
|
+
STAGING_REFS = ('staging', 'origin/staging')
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def ref_exists(ctx: Context, ref: str) -> bool:
|
|
261
|
+
return ctx.run(f'git rev-parse -q --verify "{ref}"', warn=True, hide=True).ok
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def tag_exists(ctx: Context, version: Version) -> bool:
|
|
265
|
+
return ctx.run(f'git rev-parse -q --verify "refs/tags/{version}"', warn=True, hide=True).ok
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def list_rc_tags(ctx: Context, ref: str) -> list:
|
|
269
|
+
result = ctx.run(f'git tag --list "v.*-rc*" --merged {ref}', hide=True)
|
|
270
|
+
versions = []
|
|
271
|
+
for line in result.stdout.splitlines():
|
|
272
|
+
line = line.strip()
|
|
273
|
+
if not line:
|
|
274
|
+
continue
|
|
275
|
+
try:
|
|
276
|
+
versions.append(Version.parse(line))
|
|
277
|
+
except ValueError:
|
|
278
|
+
continue
|
|
279
|
+
return versions
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def latest_staging_rc(ctx: Context) -> Version:
|
|
283
|
+
"""Highest release candidate reachable from staging."""
|
|
284
|
+
ctx.run('git fetch origin --tags', warn=True, hide=True)
|
|
285
|
+
refs = [ref for ref in STAGING_REFS if ref_exists(ctx, ref)]
|
|
286
|
+
if not refs:
|
|
287
|
+
raise RuntimeError(
|
|
288
|
+
'No staging branch found locally or on origin, so there is no release '
|
|
289
|
+
'candidate to promote. Run `itw tag-init` on staging first.'
|
|
290
|
+
)
|
|
291
|
+
versions = []
|
|
292
|
+
for ref in refs:
|
|
293
|
+
versions.extend(list_rc_tags(ctx, ref))
|
|
294
|
+
if not versions:
|
|
295
|
+
raise RuntimeError(
|
|
296
|
+
f'No release candidate tag found on {" or ".join(refs)}. '
|
|
297
|
+
'Run `itw tag-init` on staging first.'
|
|
298
|
+
)
|
|
299
|
+
return max(versions, key=lambda v: (v.major, v.minor, v.patch, v.release_candidate))
|
|
300
|
+
|
|
301
|
+
|
|
257
302
|
def _update_package_json_version(version: Version) -> None:
|
|
258
303
|
"""Update the top-level `version` in package.json (preserves key order)."""
|
|
259
304
|
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: itw_python_builder
|
|
3
|
+
Version: 0.2.15
|
|
4
|
+
Summary: Standardized Django deployment pipeline with Docker, testing, and SonarQube integration
|
|
5
|
+
Author-email: IT-Works <contact@it-works.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://git.it-works.io/
|
|
8
|
+
Project-URL: Repository, https://git.it-works.io/
|
|
9
|
+
Project-URL: Issues, https://git.it-works.io/
|
|
10
|
+
Keywords: django,deployment,docker,ci-cd,sonarqube
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: invoke>=2.0.0
|
|
24
|
+
Requires-Dist: pylint>=3.0.0
|
|
25
|
+
Requires-Dist: pylint-django>=2.5.0
|
|
26
|
+
Requires-Dist: python-decouple>=3.8
|
|
27
|
+
Requires-Dist: requests>=2.28.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# ITW Python Builder
|
|
31
|
+
|
|
32
|
+
Standardized Django and Angular deployment pipeline with Docker, testing, SonarQube integration, automatic changelog generation, GitLab issue creation, and code quality enforcement.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Automated deployment with semantic versioning
|
|
37
|
+
- Docker/Podman-based build and push pipeline (backend)
|
|
38
|
+
- Angular build, packaging and upload to the GitLab Package Registry (frontend)
|
|
39
|
+
- Angular SSR scaffolding and SSR builds
|
|
40
|
+
- Automated testing with coverage
|
|
41
|
+
- SonarQube static code analysis
|
|
42
|
+
- Pylint linting with SonarQube integration
|
|
43
|
+
- Quality gates - deploy only when tests pass
|
|
44
|
+
- Automatic changelog generation from commit trailers
|
|
45
|
+
- Automatic `CHANGELOG.md` creation and propagation to `staging`/`develop`
|
|
46
|
+
- GitLab issue creation from a markdown file
|
|
47
|
+
- Cached GitLab authentication
|
|
48
|
+
- Support for local and production pipelines
|
|
49
|
+
- Global CLI tool
|
|
50
|
+
- Automatic venv detection per project
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
```bash
|
|
54
|
+
pip install itw-python-builder
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Install globally (outside any project venv). The `itw` command becomes available system-wide.
|
|
58
|
+
|
|
59
|
+
Check the installed version:
|
|
60
|
+
```bash
|
|
61
|
+
itw --version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
1. Navigate to your project directory (Django projects must have a `.venv` or `venv`):
|
|
67
|
+
|
|
68
|
+
2. Authenticate with GitLab:
|
|
69
|
+
```bash
|
|
70
|
+
itw login
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. Initialize versioning — run it on `staging` first, then on `master`:
|
|
74
|
+
```bash
|
|
75
|
+
git checkout staging && itw tag-init # creates v.0.0.1-rc1
|
|
76
|
+
git checkout master && itw tag-init # promotes the latest staging RC to -release
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. Run local pipeline:
|
|
80
|
+
```bash
|
|
81
|
+
itw pipelinelocal
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
5. Deploy to staging:
|
|
85
|
+
```bash
|
|
86
|
+
itw incrementrc
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
6. Deploy to production:
|
|
90
|
+
```bash
|
|
91
|
+
itw incrementpatch
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Available Commands
|
|
95
|
+
|
|
96
|
+
### Deployment Commands
|
|
97
|
+
|
|
98
|
+
- `itw incrementpatch` — Increment patch version and deploy
|
|
99
|
+
- `itw incrementminor` — Increment minor version and deploy
|
|
100
|
+
- `itw incrementmajor` — Increment major version and deploy
|
|
101
|
+
- `itw incrementrc` — Increment release candidate (staging)
|
|
102
|
+
- `itw release` — Promote RC to stable release (master)
|
|
103
|
+
|
|
104
|
+
### Authentication Commands
|
|
105
|
+
|
|
106
|
+
- `itw login` — Capture and cache a GitLab token (backend also logs into the container registry)
|
|
107
|
+
- `itw logout` — Forget the cached token and log out of the container registry
|
|
108
|
+
|
|
109
|
+
### GitLab Issue Commands
|
|
110
|
+
|
|
111
|
+
- `itw task-init` — Create a `TASK.md` template in the current directory
|
|
112
|
+
- `itw task --file=TASK.md` — Create GitLab issues from a markdown file
|
|
113
|
+
|
|
114
|
+
### Local Development Commands
|
|
115
|
+
|
|
116
|
+
- `itw pipelinelocal` — Run full local pipeline (lint → test → analyze → build)
|
|
117
|
+
- `itw lintlocal` — Run pylint with human-readable output
|
|
118
|
+
- `itw lint` — Run pylint and generate SonarQube report files
|
|
119
|
+
- `itw buildlocal` — Build Docker image locally
|
|
120
|
+
- `itw test` — Run tests with coverage
|
|
121
|
+
- `itw analyze` — Run SonarQube analysis
|
|
122
|
+
- `itw changelog` — Generate changelog manually
|
|
123
|
+
- `itw tag-init` — Initialize version tagging (see below)
|
|
124
|
+
- `itw ssr-init` — Scaffold Angular SSR (one-time, frontend only)
|
|
125
|
+
|
|
126
|
+
### Version Initialization
|
|
127
|
+
|
|
128
|
+
`itw tag-init` is branch-aware and must be run from `staging` or `master`:
|
|
129
|
+
|
|
130
|
+
- On **staging** it creates the first release candidate, `v.0.0.1-rc1`.
|
|
131
|
+
- On **master** it reads the latest `-rc` tag reachable from `staging` (local or `origin/staging`, after fetching tags) and creates that same version as a release. For example `v.0.0.2-rc10` on staging becomes `v.0.0.2-release` on master.
|
|
132
|
+
|
|
133
|
+
The `VERSION` file is written with the tag that was created. The command stops with a clear error when there is no staging branch, no release candidate to promote, or when the tag already exists.
|
|
134
|
+
|
|
135
|
+
### Skip Pipeline
|
|
136
|
+
```bash
|
|
137
|
+
itw incrementpatch --skip-pipeline
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Run a Subset of Tests
|
|
141
|
+
```bash
|
|
142
|
+
# Django: an app or a single test module
|
|
143
|
+
itw test --target=users
|
|
144
|
+
itw test --target=users.test_views
|
|
145
|
+
|
|
146
|
+
# Angular: a directory or a spec name
|
|
147
|
+
itw test --target=core
|
|
148
|
+
itw test --target=core.service
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### SSR Builds
|
|
152
|
+
|
|
153
|
+
Frontend projects scaffolded with `itw ssr-init` build through the SSR npm scripts when `--ssr` is passed:
|
|
154
|
+
```bash
|
|
155
|
+
itw incrementrc --ssr
|
|
156
|
+
itw incrementpatch --ssr
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Virtual Environment Detection
|
|
160
|
+
|
|
161
|
+
`itw` is installed globally but automatically detects the project's virtual environment (`.venv` or `venv`) in the current directory. Tasks that need Python/Django dependencies (test, lint, analyze) activate the venv automatically — no need to manually activate it.
|
|
162
|
+
|
|
163
|
+
If no venv is found, those tasks will error with a clear message. Tasks that only use git (like `changelog`, `tag-init`, `buildlocal`) work without a venv.
|
|
164
|
+
|
|
165
|
+
## Linting
|
|
166
|
+
|
|
167
|
+
The pipeline runs pylint automatically on every deployment and generates report files for SonarQube. The `.pylintrc` configuration is shipped with the package — no config files needed in your project.
|
|
168
|
+
```bash
|
|
169
|
+
# Review issues with human-readable output
|
|
170
|
+
itw lintlocal
|
|
171
|
+
|
|
172
|
+
# Generate report files for SonarQube
|
|
173
|
+
itw lint
|
|
174
|
+
|
|
175
|
+
# Use a custom pylint configuration
|
|
176
|
+
itw lint --pylintrc=.pylintrc
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Add this to your `sonar-project.properties`:
|
|
180
|
+
```properties
|
|
181
|
+
sonar.python.pylint.reportPaths=pylint-report.txt
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Changelog Generation
|
|
185
|
+
|
|
186
|
+
Changelog entries are generated automatically on every deployment based on commit messages. To categorize a commit, add a `Changelog:` trailer to the commit body:
|
|
187
|
+
```
|
|
188
|
+
add user authentication
|
|
189
|
+
|
|
190
|
+
Changelog: added
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Available categories: `added`, `fixed`, `changed`, `deprecated`, `removed`, `security`, `performance`, `tests`, `docs`, `refactor`
|
|
194
|
+
|
|
195
|
+
Commits without a trailer appear under `Other Changes`. The `CHANGELOG.md` file is committed and pushed automatically with each deployment.
|
|
196
|
+
|
|
197
|
+
If `CHANGELOG.md` does not exist in the current directory it is created automatically — including when there are no new commits since the last tag, so the deployment never fails on a missing file.
|
|
198
|
+
|
|
199
|
+
### Changelog Propagation
|
|
200
|
+
|
|
201
|
+
After a successful deployment the changelog is synced forward through a temporary git worktree:
|
|
202
|
+
|
|
203
|
+
- from `master` → `staging` and `develop`
|
|
204
|
+
- from `staging` → `develop`
|
|
205
|
+
|
|
206
|
+
Propagation is skipped when the working tree is not clean or when there is nothing new to sync.
|
|
207
|
+
|
|
208
|
+
## GitLab Issue Creation
|
|
209
|
+
|
|
210
|
+
Create GitLab issues from a markdown file instead of the web UI.
|
|
211
|
+
|
|
212
|
+
Generate the template:
|
|
213
|
+
```bash
|
|
214
|
+
itw task-init
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Then fill it in and run:
|
|
218
|
+
```bash
|
|
219
|
+
itw task --file=TASK.md
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`task-init` refuses to overwrite an existing `TASK.md`.
|
|
223
|
+
|
|
224
|
+
### Template
|
|
225
|
+
|
|
226
|
+
```markdown
|
|
227
|
+
<!-- OPTIONAL -->
|
|
228
|
+
/glab_host
|
|
229
|
+
/glab_username
|
|
230
|
+
/glab_token
|
|
231
|
+
|
|
232
|
+
<!-- REQUIRED -->
|
|
233
|
+
/title
|
|
234
|
+
|
|
235
|
+
<!-- OPTIONAL -->
|
|
236
|
+
/repo
|
|
237
|
+
/milestone
|
|
238
|
+
/milestone-start YYYY-MM-DD
|
|
239
|
+
/milestone-end YYYY-MM-DD
|
|
240
|
+
/assignee
|
|
241
|
+
/label
|
|
242
|
+
/estimate
|
|
243
|
+
/due YYYY-MM-DD
|
|
244
|
+
|
|
245
|
+
<!-- OPTIONAL -->
|
|
246
|
+
## Acceptance Criteria
|
|
247
|
+
- [ ]
|
|
248
|
+
- [ ]
|
|
249
|
+
## Acceptance Criteria
|
|
250
|
+
|
|
251
|
+
<!-- OPTIONAL -->
|
|
252
|
+
## Comment
|
|
253
|
+
|
|
254
|
+
## Comment
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Every key except `/title` is optional. Leaving a key empty is allowed — empty keys are simply not sent to GitLab. The date keys keep the `YYYY-MM-DD` format in the template and are treated as empty until you replace them.
|
|
258
|
+
|
|
259
|
+
Any text outside the directives becomes the issue description. Create several issues from one file by separating the blocks with a line containing only `===`.
|
|
260
|
+
|
|
261
|
+
### Directives
|
|
262
|
+
|
|
263
|
+
| Directive | Description |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `/glab_host` | GitLab host to authenticate against |
|
|
266
|
+
| `/glab_username` | GitLab username |
|
|
267
|
+
| `/glab_token` | GitLab personal access token |
|
|
268
|
+
| `/title` | Issue title (required) |
|
|
269
|
+
| `/repo` | Target project path |
|
|
270
|
+
| `/milestone` | Milestone title |
|
|
271
|
+
| `/milestone-start` | Milestone start date, used only when the milestone is created |
|
|
272
|
+
| `/milestone-end` | Milestone due date, used only when the milestone is created |
|
|
273
|
+
| `/assignee` | Username to assign the issue to |
|
|
274
|
+
| `/label` | Comma-separated labels |
|
|
275
|
+
| `/estimate` | Time estimate (e.g. `3h`) |
|
|
276
|
+
| `/due` | Issue due date |
|
|
277
|
+
|
|
278
|
+
### Authentication
|
|
279
|
+
|
|
280
|
+
Either provide all three of `/glab_host`, `/glab_username` and `/glab_token`, or none of them. Providing only some of them is an error.
|
|
281
|
+
|
|
282
|
+
When no credentials are given, `itw` uses the `.git` directory in the current folder to detect the GitLab host and project, and the token cached by `itw login`.
|
|
283
|
+
|
|
284
|
+
### Target Repository
|
|
285
|
+
|
|
286
|
+
With no `/repo`, the target is resolved by walking the current repository's namespace upward looking for a `_pm` project. If several are found you are prompted to pick one.
|
|
287
|
+
|
|
288
|
+
With `/repo`, leading and trailing slashes are stripped automatically, and full URLs or SSH remotes are accepted:
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
/repo /tools/itw_python_builder/ → tools/itw_python_builder
|
|
292
|
+
/repo https://git.it-works.io/tools/x.git → tools/x
|
|
293
|
+
/repo git@git.it-works.io:tools/x.git → tools/x
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Milestones, Labels and Duplicates
|
|
297
|
+
|
|
298
|
+
Milestones, labels and issue titles are matched with a shared fuzzy comparison at 95% similarity, so small differences in spacing or casing still count as a match. Digit runs must be identical (`java 38` does not match `java 37`). The threshold is a single constant (`SIMILARITY_THRESHOLD` in `task_utils.py`) if you need to tune it.
|
|
299
|
+
|
|
300
|
+
- **Milestone** — an existing project or parent-group milestone with the same title is reused; otherwise it is created (with the start/end dates when both are given).
|
|
301
|
+
- **Label** — a similar existing label is linked and logged in white; a new one is created and logged in green. An empty `/label` is never sent to GitLab.
|
|
302
|
+
- **Duplicate issues** — before creating, the target project is searched by title. A match in the same milestone, or an unassigned issue with the same title, is treated as a duplicate and skipped.
|
|
303
|
+
|
|
304
|
+
### Acceptance Criteria and Comments
|
|
305
|
+
|
|
306
|
+
The `## Acceptance Criteria` section must be opened and closed with the same header. When omitted or left with empty checkboxes, a default checklist is applied.
|
|
307
|
+
|
|
308
|
+
The `## Comment` section, opened and closed the same way, is posted as a note on the issue after it is created.
|
|
309
|
+
|
|
310
|
+
## Requirements
|
|
311
|
+
|
|
312
|
+
- Python 3.10+
|
|
313
|
+
- Podman
|
|
314
|
+
- Git
|
|
315
|
+
- SonarQube server
|
|
316
|
+
- Node.js and npm (frontend projects)
|
|
317
|
+
|
|
318
|
+
## Configuration
|
|
319
|
+
|
|
320
|
+
### Environment Variables
|
|
321
|
+
|
|
322
|
+
Add to your project `.env`:
|
|
323
|
+
```
|
|
324
|
+
SONAR_HOST_URL=https://your-sonar-server
|
|
325
|
+
SONAR_TOKEN=your-token
|
|
326
|
+
GIT_DEPTH=0
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Project Files Required
|
|
330
|
+
|
|
331
|
+
- Git repository with `develop`, `staging`, and `master` branches
|
|
332
|
+
- `VERSION` file in project root (backend) or `package.json` (frontend)
|
|
333
|
+
- `Dockerfile` (backend)
|
|
334
|
+
- `sonar-project.properties`
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
MIT
|
|
@@ -3,18 +3,18 @@ itw_python_builder/__init__.py,sha256=0HwEqonfK8w1kx80BOX80fvxSL8ov1hmUD2Zi4SU75
|
|
|
3
3
|
itw_python_builder/cli.py,sha256=UKnswr7vzLC6SnB5YgxWu8ZLbOe8WGStdUHCgkzkyqM,939
|
|
4
4
|
itw_python_builder/notify.py,sha256=Rv1RA9QdDo00bvjMMSDq-oQh2pjDmjjj97yzzhgiwpM,3011
|
|
5
5
|
itw_python_builder/ssr_tasks.py,sha256=TqkuGLH9XJqDkTPBuRnFAKJywYqvdf0lSlSMXtVPM5Y,15730
|
|
6
|
-
itw_python_builder/task_utils.py,sha256=
|
|
7
|
-
itw_python_builder/tasks.py,sha256=
|
|
8
|
-
itw_python_builder/utils.py,sha256=
|
|
6
|
+
itw_python_builder/task_utils.py,sha256=VGTjuWxcMxCOXDc2yGKrqN5Y6lyffCYN5KHbP1VY85U,15347
|
|
7
|
+
itw_python_builder/tasks.py,sha256=_I-hbfHzA7JRkleX7MkmxKi3lLzcrxTh8MIqgQzbCpQ,33617
|
|
8
|
+
itw_python_builder/utils.py,sha256=wKNJM64Vl3O3KbyTSNYu6RelhWgmlapGOUGUokap9Pk,21294
|
|
9
9
|
itw_python_builder/version.py,sha256=RCSKNU4eblCP38-PH5hT6qMDKOyQq5YGCoT2mgpfZkE,3165
|
|
10
10
|
itw_python_builder/_pyruntime/sitecustomize.py,sha256=cjTrKErMdAFI-uVuW60BNNF8X41NrH9XDrIHJlNlEtY,805
|
|
11
11
|
itw_python_builder/templates/new_version_email.html,sha256=I0RuZ8UMBavXTaSzQJQP1wNjsDY5TtxrASgeGiX0NGo,2854
|
|
12
12
|
itw_python_builder/templates/server.sitemap.snippet.ts,sha256=qjRCLiAqzDUTqsWxJLeqlr-jOluK0IE2IoN_lbSx0ec,669
|
|
13
13
|
itw_python_builder/templates/sitemap.routes.ts,sha256=yOXZ8TpHq_-QKH7y_u75XpL8paCF4AEv5z5cvWeI7yY,1323
|
|
14
14
|
itw_python_builder/templates/task_template.md,sha256=SxoaWOuQVru6xNTLmhICkDoOSuHPM2B5FiVO67i7ciM,345
|
|
15
|
-
itw_python_builder-0.2.
|
|
16
|
-
itw_python_builder-0.2.
|
|
17
|
-
itw_python_builder-0.2.
|
|
18
|
-
itw_python_builder-0.2.
|
|
19
|
-
itw_python_builder-0.2.
|
|
20
|
-
itw_python_builder-0.2.
|
|
15
|
+
itw_python_builder-0.2.15.dist-info/licenses/LICENSE,sha256=WZ75-earCxnov77gsH4vckNf7Ag8RPst-7y9dPaZTCU,1063
|
|
16
|
+
itw_python_builder-0.2.15.dist-info/METADATA,sha256=RrfIX8iXpf0yVBzkqAUVU3ZIlKdjaoqnLIJwTwzaPaw,11369
|
|
17
|
+
itw_python_builder-0.2.15.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
18
|
+
itw_python_builder-0.2.15.dist-info/entry_points.txt,sha256=Rx2zlbaCxl954YX8kF4hey3Y25fxsaxiEPXjBPw3ky0,52
|
|
19
|
+
itw_python_builder-0.2.15.dist-info/top_level.txt,sha256=9djpWUrRa0vuSfdVrHc4-3gvAHbBvzh_lbyzgTHtkJA,19
|
|
20
|
+
itw_python_builder-0.2.15.dist-info/RECORD,,
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: itw_python_builder
|
|
3
|
-
Version: 0.2.13
|
|
4
|
-
Summary: Standardized Django deployment pipeline with Docker, testing, and SonarQube integration
|
|
5
|
-
Author-email: IT-Works <contact@it-works.io>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://git.it-works.io/
|
|
8
|
-
Project-URL: Repository, https://git.it-works.io/
|
|
9
|
-
Project-URL: Issues, https://git.it-works.io/
|
|
10
|
-
Keywords: django,deployment,docker,ci-cd,sonarqube
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
-
Requires-Python: >=3.10
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
License-File: LICENSE
|
|
23
|
-
Requires-Dist: invoke>=2.0.0
|
|
24
|
-
Requires-Dist: pylint>=3.0.0
|
|
25
|
-
Requires-Dist: pylint-django>=2.5.0
|
|
26
|
-
Requires-Dist: python-decouple>=3.8
|
|
27
|
-
Requires-Dist: requests>=2.28.0
|
|
28
|
-
Dynamic: license-file
|
|
29
|
-
|
|
30
|
-
# ITW Python Builder
|
|
31
|
-
|
|
32
|
-
Standardized Django deployment pipeline with Docker, testing, SonarQube integration, automatic changelog generation, and code quality enforcement.
|
|
33
|
-
|
|
34
|
-
## Features
|
|
35
|
-
|
|
36
|
-
- Automated deployment with semantic versioning
|
|
37
|
-
- Docker/Podman-based build and push pipeline
|
|
38
|
-
- Automated testing with coverage
|
|
39
|
-
- SonarQube static code analysis
|
|
40
|
-
- Pylint linting with SonarQube integration
|
|
41
|
-
- Quality gates - deploy only when tests pass
|
|
42
|
-
- Automatic changelog generation from commit trailers
|
|
43
|
-
- Support for local and production pipelines
|
|
44
|
-
- Global CLI tool
|
|
45
|
-
- Automatic venv detection per project
|
|
46
|
-
|
|
47
|
-
## Installation
|
|
48
|
-
```bash
|
|
49
|
-
pip install itw-python-builder
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Install globally (outside any project venv). The `itw` command becomes available system-wide.
|
|
53
|
-
|
|
54
|
-
## Quick Start
|
|
55
|
-
|
|
56
|
-
1. Navigate to your Django project directory (must have a `.venv` or `venv`):
|
|
57
|
-
|
|
58
|
-
2. Initialize versioning:
|
|
59
|
-
```bash
|
|
60
|
-
itw tag-init
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
3. Run local pipeline:
|
|
64
|
-
```bash
|
|
65
|
-
itw pipelinelocal
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
4. Deploy to staging:
|
|
69
|
-
```bash
|
|
70
|
-
itw incrementrc
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
5. Deploy to production:
|
|
74
|
-
```bash
|
|
75
|
-
itw incrementpatch
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Available Commands
|
|
79
|
-
|
|
80
|
-
### Deployment Commands
|
|
81
|
-
|
|
82
|
-
- `itw incrementpatch` — Increment patch version and deploy
|
|
83
|
-
- `itw incrementminor` — Increment minor version and deploy
|
|
84
|
-
- `itw incrementmajor` — Increment major version and deploy
|
|
85
|
-
- `itw incrementrc` — Increment release candidate (staging)
|
|
86
|
-
- `itw release` — Promote RC to stable release (master)
|
|
87
|
-
|
|
88
|
-
### Local Development Commands
|
|
89
|
-
|
|
90
|
-
- `itw pipelinelocal` — Run full local pipeline (lint → test → analyze → build)
|
|
91
|
-
- `itw lintlocal` — Run pylint with human-readable output
|
|
92
|
-
- `itw lint` — Run pylint and generate SonarQube report files
|
|
93
|
-
- `itw buildlocal` — Build Docker image locally
|
|
94
|
-
- `itw testlocal` — Run tests locally
|
|
95
|
-
- `itw analyzelocal` — Run SonarQube analysis locally
|
|
96
|
-
- `itw changelog` — Generate changelog manually
|
|
97
|
-
- `itw tag-init` — Initialize version tagging
|
|
98
|
-
|
|
99
|
-
### Skip Pipeline
|
|
100
|
-
```bash
|
|
101
|
-
itw incrementpatch --skip-pipeline
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Virtual Environment Detection
|
|
105
|
-
|
|
106
|
-
`itw` is installed globally but automatically detects the project's virtual environment (`.venv` or `venv`) in the current directory. Tasks that need Python/Django dependencies (test, lint, analyze) activate the venv automatically — no need to manually activate it.
|
|
107
|
-
|
|
108
|
-
If no venv is found, those tasks will error with a clear message. Tasks that only use git (like `changelog`, `tag-init`, `buildlocal`) work without a venv.
|
|
109
|
-
|
|
110
|
-
## Linting
|
|
111
|
-
|
|
112
|
-
The pipeline runs pylint automatically on every deployment and generates report files for SonarQube. The `.pylintrc` configuration is shipped with the package — no config files needed in your project.
|
|
113
|
-
```bash
|
|
114
|
-
# Review issues with human-readable output
|
|
115
|
-
itw lintlocal
|
|
116
|
-
|
|
117
|
-
# Generate report files for SonarQube
|
|
118
|
-
itw lint
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Add this to your `sonar-project.properties`:
|
|
122
|
-
```properties
|
|
123
|
-
sonar.python.pylint.reportPaths=pylint-report.txt
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Changelog Generation
|
|
127
|
-
|
|
128
|
-
Changelog entries are generated automatically on every deployment based on commit messages. To categorize a commit, add a `Changelog:` trailer to the commit body:
|
|
129
|
-
```
|
|
130
|
-
add user authentication
|
|
131
|
-
|
|
132
|
-
Changelog: added
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Available categories: `added`, `fixed`, `changed`, `deprecated`, `removed`, `security`, `performance`, `tests`, `docs`, `refactor`
|
|
136
|
-
|
|
137
|
-
Commits without a trailer appear under `Other Changes`. The `CHANGELOG.md` file is committed and pushed automatically with each deployment.
|
|
138
|
-
|
|
139
|
-
## Requirements
|
|
140
|
-
|
|
141
|
-
- Python 3.10+
|
|
142
|
-
- Podman
|
|
143
|
-
- Git
|
|
144
|
-
- SonarQube server
|
|
145
|
-
|
|
146
|
-
## Configuration
|
|
147
|
-
|
|
148
|
-
### Environment Variables
|
|
149
|
-
|
|
150
|
-
Add to your project `.env`:
|
|
151
|
-
```
|
|
152
|
-
SONAR_HOST_URL=https://your-sonar-server
|
|
153
|
-
SONAR_TOKEN=your-token
|
|
154
|
-
GIT_DEPTH=0
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Project Files Required
|
|
158
|
-
|
|
159
|
-
- Git repository with `develop`, `staging`, and `master` branches
|
|
160
|
-
- `VERSION` file in project root
|
|
161
|
-
- `Dockerfile`
|
|
162
|
-
- `sonar-project.properties`
|
|
163
|
-
|
|
164
|
-
## License
|
|
165
|
-
|
|
166
|
-
MIT
|
{itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.2.13.dist-info → itw_python_builder-0.2.15.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|