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.
@@ -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
- return ' '.join((value or '').lower().split())
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 the first item whose `key` is ~threshold similar to `value`, else None."""
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
- if is_similar(value, item.get(key, ''), threshold):
81
+ other = item.get(key, '')
82
+ if target and target == _normalize_title(other):
69
83
  return item
70
- return None
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
- results = _gitlab_api_call(
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}/milestones?per_page=100',
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 within this milestone ('None' = the no-milestone bucket)."""
132
- from urllib.parse import quote
133
- endpoint = f'/projects/{project_id}/issues?per_page=100'
134
- endpoint += f'&milestone={quote(milestone, safe="") if milestone else "None"}'
135
- results = _gitlab_api_call(api_host, endpoint, token)
136
- return find_similar(title, results)
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(
@@ -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 . --tag={container_registry_path}')
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
- version = Version(0, 0, 1, 1)
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.run(f'git rev-parse -q --verify "refs/tags/{version}"', warn=True, hide=True).ok:
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)
@@ -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=qohNLpIaodGmm9-FOOPL697Q3QZrrQkcK63AKhJcKZY,12683
7
- itw_python_builder/tasks.py,sha256=rnhUmJOH6mvoSOIN8-O7LhAEAinmIboaq27dbbMZ4h0,33043
8
- itw_python_builder/utils.py,sha256=exgIMPN5lsxMfW2XeRdqbF9vzresyAEEQ3evMwYx5K0,19766
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.13.dist-info/licenses/LICENSE,sha256=WZ75-earCxnov77gsH4vckNf7Ag8RPst-7y9dPaZTCU,1063
16
- itw_python_builder-0.2.13.dist-info/METADATA,sha256=bu_Z8CtKfcR_wKJesNmUAmezonLffgHy7VOMt1YnRtc,4928
17
- itw_python_builder-0.2.13.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
18
- itw_python_builder-0.2.13.dist-info/entry_points.txt,sha256=Rx2zlbaCxl954YX8kF4hey3Y25fxsaxiEPXjBPw3ky0,52
19
- itw_python_builder-0.2.13.dist-info/top_level.txt,sha256=9djpWUrRa0vuSfdVrHc4-3gvAHbBvzh_lbyzgTHtkJA,19
20
- itw_python_builder-0.2.13.dist-info/RECORD,,
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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