itw-python-builder 0.2.12__tar.gz → 0.2.13__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.2.12 → itw_python_builder-0.2.13}/PKG-INFO +1 -1
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/task_utils.py +154 -44
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/tasks.py +54 -8
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/templates/task_template.md +9 -8
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/PKG-INFO +1 -1
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/pyproject.toml +1 -1
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/LICENSE +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/README.md +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/.pylintrc +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/__init__.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/_pyruntime/sitecustomize.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/cli.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/notify.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/ssr_tasks.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/templates/new_version_email.html +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/templates/server.sitemap.snippet.ts +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/templates/sitemap.routes.ts +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/utils.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder/version.py +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/SOURCES.txt +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/dependency_links.txt +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/entry_points.txt +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/requires.txt +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/top_level.txt +0 -0
- {itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/setup.cfg +0 -0
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from difflib import SequenceMatcher
|
|
2
3
|
|
|
3
4
|
from itw_python_builder.utils import _gitlab_api_call
|
|
4
5
|
|
|
6
|
+
SIMILARITY_THRESHOLD = 0.95
|
|
7
|
+
|
|
8
|
+
DATE_PLACEHOLDER = 'YYYY-MM-DD'
|
|
9
|
+
|
|
10
|
+
DEFAULT_LABEL_COLOR = '#428BCA'
|
|
5
11
|
|
|
6
12
|
DEFAULT_ACCEPTANCE_CRITERIA = (
|
|
7
13
|
'- [ ] U kryen testet manuale në ndërfaqe\n'
|
|
@@ -10,7 +16,6 @@ DEFAULT_ACCEPTANCE_CRITERIA = (
|
|
|
10
16
|
'- [ ] U kontrollua cilësia e kodit'
|
|
11
17
|
)
|
|
12
18
|
|
|
13
|
-
|
|
14
19
|
_RE_AC_HEADER = re.compile(r'(?im)^##\s*acceptance\s+criteria\s*$')
|
|
15
20
|
_RE_COMMENT_HEADER = re.compile(r'(?im)^##\s*comment\s*$')
|
|
16
21
|
_RE_AC_BLOCK = re.compile(
|
|
@@ -20,9 +25,9 @@ _RE_COMMENT_BLOCK = re.compile(
|
|
|
20
25
|
r'(?ims)^##\s*comment\s*$(.*?)^##\s*comment\s*$'
|
|
21
26
|
)
|
|
22
27
|
|
|
23
|
-
|
|
24
28
|
_COLOR_GREEN = '\033[92m'
|
|
25
29
|
_COLOR_RED = '\033[91m'
|
|
30
|
+
_COLOR_WHITE = '\033[97m'
|
|
26
31
|
_COLOR_RESET = '\033[0m'
|
|
27
32
|
|
|
28
33
|
|
|
@@ -34,10 +39,37 @@ def log_red(msg: str) -> None:
|
|
|
34
39
|
print(f'{_COLOR_RED}{msg}{_COLOR_RESET}')
|
|
35
40
|
|
|
36
41
|
|
|
42
|
+
def log_white(msg: str) -> None:
|
|
43
|
+
print(f'{_COLOR_WHITE}{msg}{_COLOR_RESET}')
|
|
44
|
+
|
|
45
|
+
|
|
37
46
|
def log_info(msg: str) -> None:
|
|
38
47
|
print(msg)
|
|
39
48
|
|
|
40
49
|
|
|
50
|
+
def _normalize_title(value: str) -> str:
|
|
51
|
+
return ' '.join((value or '').lower().split())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_similar(left: str, right: str, threshold: float = SIMILARITY_THRESHOLD) -> bool:
|
|
55
|
+
"""True when two titles are at least `threshold` similar (default 95%)."""
|
|
56
|
+
a = _normalize_title(left)
|
|
57
|
+
b = _normalize_title(right)
|
|
58
|
+
if not a or not b:
|
|
59
|
+
return False
|
|
60
|
+
if a == b:
|
|
61
|
+
return True
|
|
62
|
+
return SequenceMatcher(None, a, b).ratio() >= threshold
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
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."""
|
|
67
|
+
for item in items:
|
|
68
|
+
if is_similar(value, item.get(key, ''), threshold):
|
|
69
|
+
return item
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
41
73
|
def _list_group_projects(api_host: str, group_path: str, token: str) -> list:
|
|
42
74
|
from urllib.parse import quote
|
|
43
75
|
encoded = quote(group_path, safe='')
|
|
@@ -87,26 +119,30 @@ def get_project_by_path(api_host: str, project_path: str, token: str) -> dict:
|
|
|
87
119
|
|
|
88
120
|
|
|
89
121
|
def find_project_milestone(api_host: str, project_id: int, title: str, token: str):
|
|
90
|
-
from urllib.parse import quote
|
|
91
|
-
encoded = quote(title, safe='')
|
|
92
122
|
results = _gitlab_api_call(
|
|
93
123
|
api_host,
|
|
94
|
-
f'/projects/{project_id}/milestones?
|
|
124
|
+
f'/projects/{project_id}/milestones?per_page=100',
|
|
95
125
|
token,
|
|
96
126
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
return find_similar(title, results)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
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)
|
|
101
137
|
|
|
102
138
|
|
|
103
139
|
def create_project_milestone(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
api_host: str,
|
|
141
|
+
project_id: int,
|
|
142
|
+
title: str,
|
|
143
|
+
start_date: str = None,
|
|
144
|
+
due_date: str = None,
|
|
145
|
+
token: str = None,
|
|
110
146
|
) -> dict:
|
|
111
147
|
payload = {'title': title}
|
|
112
148
|
if start_date:
|
|
@@ -122,6 +158,20 @@ def create_project_milestone(
|
|
|
122
158
|
)
|
|
123
159
|
|
|
124
160
|
|
|
161
|
+
def list_project_labels(api_host: str, project_id: int, token: str) -> list:
|
|
162
|
+
return _gitlab_api_call(api_host, f'/projects/{project_id}/labels?per_page=100', token)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_project_label(api_host: str, project_id: int, name: str, token: str) -> dict:
|
|
166
|
+
return _gitlab_api_call(
|
|
167
|
+
api_host,
|
|
168
|
+
f'/projects/{project_id}/labels',
|
|
169
|
+
token,
|
|
170
|
+
method='POST',
|
|
171
|
+
payload={'name': name, 'color': DEFAULT_LABEL_COLOR},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
125
175
|
def create_issue_note(api_host: str, project_id: int, issue_iid: int, body: str, token: str) -> dict:
|
|
126
176
|
return _gitlab_api_call(
|
|
127
177
|
api_host,
|
|
@@ -132,6 +182,21 @@ def create_issue_note(api_host: str, project_id: int, issue_iid: int, body: str,
|
|
|
132
182
|
)
|
|
133
183
|
|
|
134
184
|
|
|
185
|
+
def _normalize_repo(raw: str) -> str:
|
|
186
|
+
value = (raw or '').strip()
|
|
187
|
+
if not value:
|
|
188
|
+
return ''
|
|
189
|
+
m = re.match(r'^[a-z][a-z0-9+.-]*://[^/]+/(.*)$', value, re.IGNORECASE)
|
|
190
|
+
if m:
|
|
191
|
+
value = m.group(1)
|
|
192
|
+
else:
|
|
193
|
+
m = re.match(r'^[^/\s]+@[^:/\s]+:(.*)$', value)
|
|
194
|
+
if m:
|
|
195
|
+
value = m.group(1)
|
|
196
|
+
value = re.sub(r'\.git$', '', value.strip(), flags=re.IGNORECASE)
|
|
197
|
+
return value.strip('/')
|
|
198
|
+
|
|
199
|
+
|
|
135
200
|
def _strip_milestone_wrap(raw: str) -> str:
|
|
136
201
|
raw = raw.strip()
|
|
137
202
|
if raw.startswith('%"') and raw.endswith('"'):
|
|
@@ -143,6 +208,30 @@ def _strip_milestone_wrap(raw: str) -> str:
|
|
|
143
208
|
|
|
144
209
|
_CRED_KEYS = ('glab_host', 'glab_username', 'glab_token')
|
|
145
210
|
|
|
211
|
+
_RE_MARKER_COMMENT = re.compile(r'^<!--.*-->$')
|
|
212
|
+
_RE_EMPTY_CHECKBOX = re.compile(r'^[-*]\s*\[[ xX]?\]\s*$')
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _block_has_content(text: str) -> bool:
|
|
216
|
+
"""False when a section holds only blank lines or empty checklist placeholders."""
|
|
217
|
+
for line in (text or '').splitlines():
|
|
218
|
+
stripped = line.strip()
|
|
219
|
+
if not stripped or _RE_EMPTY_CHECKBOX.match(stripped):
|
|
220
|
+
continue
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _match_directive(line: str, name: str):
|
|
226
|
+
"""None when the line is not this directive, '' when it carries no value."""
|
|
227
|
+
m = re.match(rf'^/{name}(?![\w-])\s*(.*)$', line, re.IGNORECASE)
|
|
228
|
+
if not m:
|
|
229
|
+
return None
|
|
230
|
+
value = m.group(1).strip()
|
|
231
|
+
if value.upper() == DATE_PLACEHOLDER:
|
|
232
|
+
return ''
|
|
233
|
+
return value
|
|
234
|
+
|
|
146
235
|
|
|
147
236
|
def extract_glab_credentials(file_path: str) -> dict:
|
|
148
237
|
"""Scan md file for /glab_host, /glab_username, /glab_token top-level directives."""
|
|
@@ -152,9 +241,9 @@ def extract_glab_credentials(file_path: str) -> dict:
|
|
|
152
241
|
for line in content.splitlines():
|
|
153
242
|
stripped = line.strip()
|
|
154
243
|
for key in _CRED_KEYS:
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
creds[key] =
|
|
244
|
+
value = _match_directive(stripped, key)
|
|
245
|
+
if value is not None:
|
|
246
|
+
creds[key] = value or None
|
|
158
247
|
break
|
|
159
248
|
return creds
|
|
160
249
|
|
|
@@ -162,7 +251,7 @@ def extract_glab_credentials(file_path: str) -> dict:
|
|
|
162
251
|
def strip_glab_credentials_lines(content: str) -> str:
|
|
163
252
|
"""Remove /glab_* lines so they don't leak into issue bodies."""
|
|
164
253
|
pattern = re.compile(
|
|
165
|
-
rf'(?im)
|
|
254
|
+
rf'(?im)^[ \t]*/(?:{"|".join(_CRED_KEYS)})(?![\w-])[^\n]*(?:\n|$)'
|
|
166
255
|
)
|
|
167
256
|
return pattern.sub('', content)
|
|
168
257
|
|
|
@@ -204,14 +293,16 @@ def _parse_single_issue(block: str, idx: int, file_path: str) -> dict:
|
|
|
204
293
|
if len(cm_headers) >= 2:
|
|
205
294
|
m = _RE_COMMENT_BLOCK.search(remaining)
|
|
206
295
|
if m:
|
|
207
|
-
|
|
296
|
+
if _block_has_content(m.group(1)):
|
|
297
|
+
comment_text = m.group(1).strip()
|
|
208
298
|
remaining = _RE_COMMENT_BLOCK.sub('', remaining, count=1)
|
|
209
299
|
|
|
210
300
|
acceptance_text = None
|
|
211
301
|
if len(ac_headers) >= 2:
|
|
212
302
|
m = _RE_AC_BLOCK.search(remaining)
|
|
213
303
|
if m:
|
|
214
|
-
|
|
304
|
+
if _block_has_content(m.group(1)):
|
|
305
|
+
acceptance_text = m.group(1).strip()
|
|
215
306
|
remaining = _RE_AC_BLOCK.sub('', remaining, count=1)
|
|
216
307
|
|
|
217
308
|
title = None
|
|
@@ -220,6 +311,7 @@ def _parse_single_issue(block: str, idx: int, file_path: str) -> dict:
|
|
|
220
311
|
milestone_start = None
|
|
221
312
|
milestone_end = None
|
|
222
313
|
assignee = None
|
|
314
|
+
labels = []
|
|
223
315
|
has_estimate = False
|
|
224
316
|
has_due = False
|
|
225
317
|
body_lines = []
|
|
@@ -227,35 +319,52 @@ def _parse_single_issue(block: str, idx: int, file_path: str) -> dict:
|
|
|
227
319
|
for line in remaining.splitlines():
|
|
228
320
|
stripped = line.strip()
|
|
229
321
|
|
|
230
|
-
|
|
231
|
-
if m:
|
|
232
|
-
title = m.group(1).strip()
|
|
322
|
+
if _RE_MARKER_COMMENT.match(stripped):
|
|
233
323
|
continue
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
324
|
+
|
|
325
|
+
value = _match_directive(stripped, 'title')
|
|
326
|
+
if value is not None:
|
|
327
|
+
title = value or None
|
|
237
328
|
continue
|
|
238
|
-
|
|
239
|
-
if
|
|
240
|
-
|
|
329
|
+
value = _match_directive(stripped, 'repo')
|
|
330
|
+
if value is not None:
|
|
331
|
+
repo = _normalize_repo(value) or None
|
|
241
332
|
continue
|
|
242
|
-
|
|
243
|
-
if
|
|
244
|
-
|
|
333
|
+
value = _match_directive(stripped, 'milestone-start')
|
|
334
|
+
if value is not None:
|
|
335
|
+
milestone_start = value or None
|
|
245
336
|
continue
|
|
246
|
-
|
|
247
|
-
if
|
|
248
|
-
|
|
337
|
+
value = _match_directive(stripped, 'milestone-end')
|
|
338
|
+
if value is not None:
|
|
339
|
+
milestone_end = value or None
|
|
249
340
|
continue
|
|
250
|
-
|
|
251
|
-
if
|
|
252
|
-
|
|
253
|
-
|
|
341
|
+
value = _match_directive(stripped, 'milestone')
|
|
342
|
+
if value is not None:
|
|
343
|
+
milestone = _strip_milestone_wrap(value) or None
|
|
344
|
+
continue
|
|
345
|
+
value = _match_directive(stripped, 'label')
|
|
346
|
+
if value is not None:
|
|
347
|
+
labels = [p.strip().lstrip('~').strip('"') for p in value.split(',') if p.strip()]
|
|
348
|
+
labels = [p for p in labels if p]
|
|
349
|
+
continue
|
|
350
|
+
value = _match_directive(stripped, 'assignee')
|
|
351
|
+
if value is not None:
|
|
352
|
+
if value:
|
|
353
|
+
assignee = value
|
|
354
|
+
body_lines.append(f'/assign {assignee}')
|
|
355
|
+
continue
|
|
356
|
+
value = _match_directive(stripped, 'estimate')
|
|
357
|
+
if value is not None:
|
|
358
|
+
if value:
|
|
359
|
+
has_estimate = True
|
|
360
|
+
body_lines.append(f'/estimate {value}')
|
|
361
|
+
continue
|
|
362
|
+
value = _match_directive(stripped, 'due')
|
|
363
|
+
if value is not None:
|
|
364
|
+
if value:
|
|
365
|
+
has_due = True
|
|
366
|
+
body_lines.append(f'/due {value}')
|
|
254
367
|
continue
|
|
255
|
-
if re.match(r'^/estimate\s+\S+', stripped, re.IGNORECASE):
|
|
256
|
-
has_estimate = True
|
|
257
|
-
if re.match(r'^/due\s+\S+', stripped, re.IGNORECASE):
|
|
258
|
-
has_due = True
|
|
259
368
|
|
|
260
369
|
body_lines.append(line)
|
|
261
370
|
|
|
@@ -273,6 +382,7 @@ def _parse_single_issue(block: str, idx: int, file_path: str) -> dict:
|
|
|
273
382
|
'milestone_start': milestone_start,
|
|
274
383
|
'milestone_end': milestone_end,
|
|
275
384
|
'assignee': assignee,
|
|
385
|
+
'labels': labels,
|
|
276
386
|
'has_estimate': has_estimate,
|
|
277
387
|
'has_due': has_due,
|
|
278
388
|
'has_acceptance_criteria': acceptance_text is not None,
|
|
@@ -38,11 +38,16 @@ from itw_python_builder.task_utils import (
|
|
|
38
38
|
build_issue_description,
|
|
39
39
|
get_project_by_path,
|
|
40
40
|
find_project_milestone,
|
|
41
|
+
find_project_issue,
|
|
41
42
|
create_project_milestone,
|
|
43
|
+
list_project_labels,
|
|
44
|
+
create_project_label,
|
|
42
45
|
create_issue_note,
|
|
43
46
|
log_green,
|
|
44
47
|
log_red,
|
|
45
48
|
log_info,
|
|
49
|
+
log_white,
|
|
50
|
+
find_similar,
|
|
46
51
|
)
|
|
47
52
|
from itw_python_builder.version import Version
|
|
48
53
|
|
|
@@ -201,12 +206,27 @@ def create_task(ctx: Context, file=None):
|
|
|
201
206
|
if not issue['has_due']:
|
|
202
207
|
log_red('No due date is defined for this issue. Use /due in you md file to define a due date')
|
|
203
208
|
|
|
204
|
-
milestone_id = _resolve_milestone(api_host, project['id'], issue, token)
|
|
209
|
+
milestone_id, milestone_title = _resolve_milestone(api_host, project['id'], issue, token)
|
|
210
|
+
|
|
211
|
+
duplicate = find_project_issue(
|
|
212
|
+
api_host, project['id'], issue['title'], token, milestone_title
|
|
213
|
+
)
|
|
214
|
+
if duplicate:
|
|
215
|
+
scope = f'milestone {milestone_title}' if milestone_title else 'issues without a milestone'
|
|
216
|
+
log_white(
|
|
217
|
+
f'Issue "{issue["title"]}" was not created because it already exists '
|
|
218
|
+
f'in this project for {scope}: {duplicate["web_url"]}'
|
|
219
|
+
)
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
labels = _resolve_labels(api_host, project['id'], issue, token)
|
|
205
223
|
|
|
206
224
|
description = build_issue_description(issue)
|
|
207
225
|
payload = {'title': issue['title'], 'description': description}
|
|
208
226
|
if milestone_id is not None:
|
|
209
227
|
payload['milestone_id'] = milestone_id
|
|
228
|
+
if labels:
|
|
229
|
+
payload['labels'] = ','.join(labels)
|
|
210
230
|
|
|
211
231
|
result = create_gitlab_issue(api_host, project['id'], payload, token)
|
|
212
232
|
print(f'[itw] Created: {result["web_url"]}')
|
|
@@ -264,6 +284,24 @@ def _resolve_target_project(api_host: str, project_path: str, issue: dict, token
|
|
|
264
284
|
print('Invalid choice, try again.')
|
|
265
285
|
|
|
266
286
|
|
|
287
|
+
def _resolve_labels(api_host: str, project_id: int, issue: dict, token: str) -> list:
|
|
288
|
+
if not issue['labels']:
|
|
289
|
+
return []
|
|
290
|
+
existing = list_project_labels(api_host, project_id, token)
|
|
291
|
+
resolved = []
|
|
292
|
+
for name in issue['labels']:
|
|
293
|
+
match = find_similar(name, existing, key='name')
|
|
294
|
+
if match:
|
|
295
|
+
log_white(f'Using existing label {match["name"]}')
|
|
296
|
+
resolved.append(match['name'])
|
|
297
|
+
continue
|
|
298
|
+
created = create_project_label(api_host, project_id, name, token)
|
|
299
|
+
log_green(f'Created new label {created["name"]}')
|
|
300
|
+
resolved.append(created['name'])
|
|
301
|
+
existing.append(created)
|
|
302
|
+
return resolved
|
|
303
|
+
|
|
304
|
+
|
|
267
305
|
def _resolve_milestone(api_host: str, project_id: int, issue: dict, token: str):
|
|
268
306
|
title = issue['milestone']
|
|
269
307
|
if not title:
|
|
@@ -271,12 +309,12 @@ def _resolve_milestone(api_host: str, project_id: int, issue: dict, token: str):
|
|
|
271
309
|
'There is no milestone defined for this issue. '
|
|
272
310
|
'Use /milestone in your md file to define a milestone'
|
|
273
311
|
)
|
|
274
|
-
return None
|
|
312
|
+
return None, None
|
|
275
313
|
|
|
276
314
|
existing = find_project_milestone(api_host, project_id, title, token)
|
|
277
315
|
if existing:
|
|
278
|
-
log_info(f'Using existing milestone {title}')
|
|
279
|
-
return existing['id']
|
|
316
|
+
log_info(f'Using existing milestone {existing["title"]}')
|
|
317
|
+
return existing['id'], existing['title']
|
|
280
318
|
|
|
281
319
|
start = issue['milestone_start']
|
|
282
320
|
end = issue['milestone_end']
|
|
@@ -289,7 +327,7 @@ def _resolve_milestone(api_host: str, project_id: int, issue: dict, token: str):
|
|
|
289
327
|
f'Created new milestone {title} | No start date or end date is defined for this '
|
|
290
328
|
'new milestone. Use /milestone-start and /milestone-end in your md file to define them '
|
|
291
329
|
)
|
|
292
|
-
return created['id']
|
|
330
|
+
return created['id'], created['title']
|
|
293
331
|
|
|
294
332
|
|
|
295
333
|
def build_frontend(ctx: Context, branch: str, ssr: bool = False) -> None:
|
|
@@ -743,10 +781,20 @@ def pipelinelocal(ctx: Context, settings='backend.test_settings', pylintrc=None)
|
|
|
743
781
|
print("=" * 60)
|
|
744
782
|
|
|
745
783
|
|
|
784
|
+
def _ensure_changelog() -> None:
|
|
785
|
+
if os.path.exists('CHANGELOG.md'):
|
|
786
|
+
return
|
|
787
|
+
with open('CHANGELOG.md', 'w') as f:
|
|
788
|
+
f.write('# Changelog\n')
|
|
789
|
+
log_green('Created CHANGELOG.md')
|
|
790
|
+
|
|
791
|
+
|
|
746
792
|
@task
|
|
747
793
|
def changelog(ctx: Context, version: Version = None):
|
|
748
794
|
"""Generate changelog from commits with Changelog trailer"""
|
|
749
795
|
|
|
796
|
+
_ensure_changelog()
|
|
797
|
+
|
|
750
798
|
exclude = f'--exclude={version}' if version else ''
|
|
751
799
|
last_tag_result = ctx.run(f'git describe --tags --abbrev=0 {exclude}', hide=True, warn=True)
|
|
752
800
|
if last_tag_result.ok and last_tag_result.stdout.strip():
|
|
@@ -831,9 +879,7 @@ def changelog(ctx: Context, version: Version = None):
|
|
|
831
879
|
changelog_entry += f"{commit}\n"
|
|
832
880
|
changelog_entry += "\n"
|
|
833
881
|
|
|
834
|
-
|
|
835
|
-
with open('CHANGELOG.md', 'w') as f:
|
|
836
|
-
f.write('# Changelog\n')
|
|
882
|
+
_ensure_changelog()
|
|
837
883
|
|
|
838
884
|
with open('CHANGELOG.md', 'r') as f:
|
|
839
885
|
existing = f.read()
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<!-- OPTIONAL -->
|
|
2
|
-
/glab_host
|
|
3
|
-
/glab_username
|
|
4
|
-
/glab_token
|
|
2
|
+
/glab_host
|
|
3
|
+
/glab_username
|
|
4
|
+
/glab_token
|
|
5
5
|
|
|
6
6
|
<!-- REQUIRED -->
|
|
7
|
-
/title
|
|
7
|
+
/title
|
|
8
8
|
|
|
9
9
|
<!-- OPTIONAL -->
|
|
10
|
-
/repo
|
|
11
|
-
/milestone
|
|
10
|
+
/repo
|
|
11
|
+
/milestone
|
|
12
12
|
/milestone-start YYYY-MM-DD
|
|
13
13
|
/milestone-end YYYY-MM-DD
|
|
14
|
-
/assignee
|
|
15
|
-
/
|
|
14
|
+
/assignee
|
|
15
|
+
/label
|
|
16
|
+
/estimate
|
|
16
17
|
/due YYYY-MM-DD
|
|
17
18
|
|
|
18
19
|
<!-- OPTIONAL -->
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "itw_python_builder"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.13"
|
|
8
8
|
description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/requires.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.2.12 → itw_python_builder-0.2.13}/itw_python_builder.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|