suite-py 1.47.2__py3-none-any.whl → 1.48.0__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.
- suite_py/__version__.py +1 -1
- suite_py/cli.py +24 -2
- suite_py/commands/release.py +136 -47
- suite_py/lib/handler/pre_commit_handler.py +10 -3
- {suite_py-1.47.2.dist-info → suite_py-1.48.0.dist-info}/METADATA +1 -1
- {suite_py-1.47.2.dist-info → suite_py-1.48.0.dist-info}/RECORD +8 -8
- {suite_py-1.47.2.dist-info → suite_py-1.48.0.dist-info}/WHEEL +0 -0
- {suite_py-1.47.2.dist-info → suite_py-1.48.0.dist-info}/entry_points.txt +0 -0
suite_py/__version__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.48.0"
|
suite_py/cli.py
CHANGED
|
@@ -326,12 +326,34 @@ def release():
|
|
|
326
326
|
@release.command(
|
|
327
327
|
"create", help="Create a github release (and deploy it if GHA are used)"
|
|
328
328
|
)
|
|
329
|
+
@click.argument(
|
|
330
|
+
"commit",
|
|
331
|
+
required=False,
|
|
332
|
+
type=str,
|
|
333
|
+
metavar="[COMMIT]",
|
|
334
|
+
)
|
|
335
|
+
@click.option(
|
|
336
|
+
"--interactive",
|
|
337
|
+
"-i",
|
|
338
|
+
is_flag=True,
|
|
339
|
+
help="Interactively choose which unreleased commit to release (ignored if COMMIT is provided)",
|
|
340
|
+
)
|
|
329
341
|
@click.pass_obj
|
|
330
342
|
@catch_exceptions
|
|
331
|
-
def cli_release_create(obj):
|
|
343
|
+
def cli_release_create(obj, commit, interactive): # type: ignore[override]
|
|
344
|
+
"""Create a release.
|
|
345
|
+
|
|
346
|
+
Optionally pass a COMMIT (full or short SHA) to create the release at that
|
|
347
|
+
specific commit instead of the latest commit (HEAD). Example:
|
|
348
|
+
|
|
349
|
+
suite-py release create 644a699
|
|
350
|
+
|
|
351
|
+
If you don't provide a COMMIT you can pass --interactive / -i to select
|
|
352
|
+
one among the unreleased commits (those after the last tag).
|
|
353
|
+
"""
|
|
332
354
|
from suite_py.commands.release import Release
|
|
333
355
|
|
|
334
|
-
obj.call(Release, action="create").run()
|
|
356
|
+
obj.call(Release, action="create", commit=commit, interactive=interactive).run()
|
|
335
357
|
|
|
336
358
|
|
|
337
359
|
@main.command("status", help="Current status of a project")
|
suite_py/commands/release.py
CHANGED
|
@@ -16,11 +16,24 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
|
16
16
|
class Release:
|
|
17
17
|
# pylint: disable=too-many-instance-attributes
|
|
18
18
|
# pylint: disable=too-many-positional-arguments
|
|
19
|
-
def __init__(
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
action,
|
|
22
|
+
project,
|
|
23
|
+
captainhook,
|
|
24
|
+
config,
|
|
25
|
+
tokens,
|
|
26
|
+
commit=None,
|
|
27
|
+
interactive=False,
|
|
28
|
+
):
|
|
20
29
|
self._action = action
|
|
21
30
|
self._project = project
|
|
22
31
|
self._config = config
|
|
23
32
|
self._tokens = tokens
|
|
33
|
+
# Optional commit (sha or short sha) to release instead of HEAD
|
|
34
|
+
self._commit = commit
|
|
35
|
+
# Whether to prompt the user to choose commit among unreleased ones
|
|
36
|
+
self._interactive = interactive
|
|
24
37
|
self._changelog_handler = ChangelogHandler()
|
|
25
38
|
self._youtrack = YoutrackHandler(config, tokens)
|
|
26
39
|
self._captainhook = captainhook
|
|
@@ -52,62 +65,138 @@ class Release:
|
|
|
52
65
|
|
|
53
66
|
def _create(self):
|
|
54
67
|
latest = self._version.get_latest_version()
|
|
68
|
+
commits, new_version, message = self._gather_commits_and_version(latest)
|
|
69
|
+
message = self._augment_message_with_changelog(new_version, message)
|
|
70
|
+
message = common.ask_for_release_description(message)
|
|
71
|
+
sha = self._resolve_target_sha(commits)
|
|
72
|
+
self._create_release(new_version, message, sha)
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
_check_migrations_deploy(commits)
|
|
62
|
-
|
|
63
|
-
message = "\n".join(
|
|
64
|
-
[
|
|
65
|
-
"* "
|
|
66
|
-
+ c.commit.message.splitlines()[0]
|
|
67
|
-
+ " by "
|
|
68
|
-
+ c.commit.author.name
|
|
69
|
-
for c in commits
|
|
70
|
-
]
|
|
71
|
-
)
|
|
74
|
+
# ---- helpers (extracted to reduce complexity of _create) ----
|
|
75
|
+
def _gather_commits_and_version(self, latest):
|
|
76
|
+
"""Return (commits, new_version, base_message)."""
|
|
77
|
+
if latest == "":
|
|
78
|
+
return self._first_release_flow()
|
|
72
79
|
|
|
73
|
-
|
|
80
|
+
logger.info(f"The current release is {latest}")
|
|
81
|
+
commits = self._github.get_commits_since_release(self._repo, latest)
|
|
82
|
+
# If user did not pass a target commit, ask interactively which commit to release.
|
|
83
|
+
# Only perform interactive selection if user explicitly requested it
|
|
84
|
+
if self._interactive:
|
|
85
|
+
self._select_commit_if_needed(commits)
|
|
86
|
+
commits = self._maybe_trim_commits(commits)
|
|
87
|
+
_check_migrations_deploy(commits)
|
|
88
|
+
message = self._build_commits_message(commits)
|
|
89
|
+
logger.info(f"\nCommits list:\n{message}\n")
|
|
90
|
+
if not prompt_utils.ask_confirm("Do you want to continue?"):
|
|
91
|
+
sys.exit() # Preserve previous early return semantics
|
|
92
|
+
new_version = self._version.select_new_version(latest, allow_prerelease=True)
|
|
93
|
+
return commits, new_version, message
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
def _first_release_flow(self):
|
|
96
|
+
logger.warning(f"No tags found, I'm about to push the tag {DEFAULT_VERSION}")
|
|
97
|
+
if not prompt_utils.ask_confirm(
|
|
98
|
+
"Are you sure you want to continue?", default=False
|
|
99
|
+
):
|
|
100
|
+
sys.exit()
|
|
101
|
+
return [], DEFAULT_VERSION, f"First release with tag {DEFAULT_VERSION}"
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
def _maybe_trim_commits(self, commits):
|
|
104
|
+
if not self._commit:
|
|
105
|
+
return commits
|
|
106
|
+
try:
|
|
107
|
+
resolved_commit = self._repo.get_commit(self._commit)
|
|
108
|
+
target_sha = resolved_commit.sha
|
|
109
|
+
except Exception: # pragma: no cover
|
|
110
|
+
logger.error(
|
|
111
|
+
f"The provided commit '{self._commit}' was not found in the repository."
|
|
80
112
|
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
sys.exit(-1)
|
|
114
|
+
target_index = None
|
|
115
|
+
for idx, c in enumerate(commits):
|
|
116
|
+
if (
|
|
117
|
+
c.sha == target_sha
|
|
118
|
+
or getattr(c, "commit", None)
|
|
119
|
+
and c.commit.sha == target_sha
|
|
120
|
+
):
|
|
121
|
+
target_index = idx
|
|
122
|
+
break
|
|
123
|
+
if target_index is None:
|
|
124
|
+
logger.error(
|
|
125
|
+
"The specified commit is not part of the unreleased commits (i.e., not after the latest release)."
|
|
86
126
|
)
|
|
127
|
+
sys.exit(-1)
|
|
128
|
+
return commits[target_index:]
|
|
129
|
+
|
|
130
|
+
def _select_commit_if_needed(self, commits):
|
|
131
|
+
"""Interactively ask the user which commit to release if none was provided.
|
|
132
|
+
|
|
133
|
+
Presents the unreleased commits (newest first) allowing the user to pick a
|
|
134
|
+
target commit. The chosen commit's SHA (full) is stored in `self._commit` so
|
|
135
|
+
that downstream logic (_maybe_trim_commits / _resolve_target_sha) works
|
|
136
|
+
unchanged.
|
|
137
|
+
"""
|
|
138
|
+
if self._commit or not commits:
|
|
139
|
+
return
|
|
140
|
+
if len(commits) == 1:
|
|
141
|
+
# Only one unreleased commit; implicitly choose it.
|
|
142
|
+
self._commit = commits[0].sha
|
|
143
|
+
return
|
|
144
|
+
choices = [
|
|
145
|
+
{
|
|
146
|
+
"name": f"{c.sha[:8]} | {c.commit.message.splitlines()[0]}",
|
|
147
|
+
"value": c.sha,
|
|
148
|
+
}
|
|
149
|
+
for c in commits
|
|
150
|
+
]
|
|
151
|
+
selected = prompt_utils.ask_choices(
|
|
152
|
+
"Select commit to release (newest first):", choices
|
|
153
|
+
)
|
|
154
|
+
self._commit = selected
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _build_commits_message(commits):
|
|
158
|
+
return "\n".join(
|
|
159
|
+
[
|
|
160
|
+
"* " + c.commit.message.splitlines()[0] + " by " + c.commit.author.name
|
|
161
|
+
for c in commits
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _augment_message_with_changelog(self, new_version, message):
|
|
166
|
+
if not self._changelog_handler.changelog_exists():
|
|
167
|
+
return message
|
|
168
|
+
latest_tag, latest_entry = self._changelog_handler.get_latest_entry_with_tag()
|
|
169
|
+
if latest_tag != new_version:
|
|
87
170
|
if not prompt_utils.ask_confirm(
|
|
88
|
-
"
|
|
171
|
+
"You didn't update your changelog, are you sure you want to proceed?"
|
|
89
172
|
):
|
|
90
173
|
sys.exit()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if self._changelog_handler.changelog_exists():
|
|
95
|
-
(
|
|
96
|
-
latest_tag,
|
|
97
|
-
latest_entry,
|
|
98
|
-
) = self._changelog_handler.get_latest_entry_with_tag()
|
|
99
|
-
|
|
100
|
-
if latest_tag != new_version:
|
|
101
|
-
if not prompt_utils.ask_confirm(
|
|
102
|
-
"You didn't update your changelog, are you sure you want to proceed?"
|
|
103
|
-
):
|
|
104
|
-
sys.exit()
|
|
105
|
-
else:
|
|
106
|
-
message = f"{latest_entry}\n\n# Commits\n\n{message}"
|
|
174
|
+
return message
|
|
175
|
+
return f"{latest_entry}\n\n# Commits\n\n{message}"
|
|
107
176
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
177
|
+
def _resolve_target_sha(self, commits):
|
|
178
|
+
if not self._commit:
|
|
179
|
+
return commits[0].commit.sha if commits else ""
|
|
180
|
+
try:
|
|
181
|
+
sha = self._repo.get_commit(self._commit).sha
|
|
182
|
+
except Exception: # pragma: no cover
|
|
183
|
+
logger.error(
|
|
184
|
+
f"Unable to resolve commit '{self._commit}' during release creation."
|
|
185
|
+
)
|
|
186
|
+
sys.exit(-1)
|
|
187
|
+
self._validate_target_commit_not_tagged(sha)
|
|
188
|
+
return sha
|
|
189
|
+
|
|
190
|
+
def _validate_target_commit_not_tagged(self, sha):
|
|
191
|
+
try:
|
|
192
|
+
for t in self._repo.get_tags(): # network call
|
|
193
|
+
if t.commit.sha == sha:
|
|
194
|
+
logger.error(
|
|
195
|
+
f"The commit {sha[:7]} is already tagged with {t.name}. Aborting."
|
|
196
|
+
)
|
|
197
|
+
sys.exit(-1)
|
|
198
|
+
except Exception: # pragma: no cover
|
|
199
|
+
logger.warning("Could not verify if commit already tagged; continuing.")
|
|
111
200
|
|
|
112
201
|
def _create_release(self, new_version, message, commit):
|
|
113
202
|
new_release = self._repo.create_git_release(
|
|
@@ -51,10 +51,17 @@ to disable it globally
|
|
|
51
51
|
* is there a `.git/hooks/pre-commit` shell script that contains keyword "gitleaks"
|
|
52
52
|
* is there a `.husky/pre-commit` shell script that contains keyword "security-hooks" (primait/security-hooks repo)
|
|
53
53
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
checks = [
|
|
55
|
+
(os.path.join(self._git.hooks_path(), "pre-commit"), "gitleaks"),
|
|
56
|
+
(
|
|
57
|
+
os.path.join(self._git.get_path(), ".husky", "pre-commit"),
|
|
58
|
+
"security-hooks",
|
|
59
|
+
),
|
|
60
|
+
]
|
|
56
61
|
|
|
57
|
-
return any(
|
|
62
|
+
return any(
|
|
63
|
+
self._script_contains_keyword(path, keyword) for path, keyword in checks
|
|
64
|
+
)
|
|
58
65
|
|
|
59
66
|
def _is_pre_commit_py_hook_setup(self):
|
|
60
67
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
suite_py/__init__.py,sha256=REmi3D0X2G1ZWnYpKs8Ffm3NIj-Hw6dMuvz2b9NW344,142
|
|
2
|
-
suite_py/__version__.py,sha256=
|
|
3
|
-
suite_py/cli.py,sha256=
|
|
2
|
+
suite_py/__version__.py,sha256=UX2PhCeqcjeKRhvfM8Al3foajhShvJlc9WnR475e-GY,49
|
|
3
|
+
suite_py/cli.py,sha256=8h0vLtTgqcYtsE5Q_idTwm9KP8Gmr7DJIsWCSxv5ziw,11861
|
|
4
4
|
suite_py/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
suite_py/commands/ask_review.py,sha256=yN__Ac-fiZBPShjRDhyCCQZGfVlQE16KozoJk4UtiNw,3788
|
|
6
6
|
suite_py/commands/bump.py,sha256=oFZU1hPfD11ujFC5G7wFyQOf2alY3xp2SO1h1ldjf3s,5406
|
|
@@ -13,7 +13,7 @@ suite_py/commands/login.py,sha256=A59e1HsbN7Ocv2L_2H0Eb7MZK7AzLkLb72QxBthnIqU,25
|
|
|
13
13
|
suite_py/commands/merge_pr.py,sha256=fXIE8mT9MjvvpqE-uVdXGBVFGhn0eQzcBxNr-N8SyAY,5171
|
|
14
14
|
suite_py/commands/open_pr.py,sha256=djaF2OsYbQo0YLTEbNRTYYFzsNT0sl7VRxqtxSX1PKc,7150
|
|
15
15
|
suite_py/commands/project_lock.py,sha256=b7OkGysue_Sl13VIT7B5CTBppCvrB_Q6iC0IJRBSHp8,1909
|
|
16
|
-
suite_py/commands/release.py,sha256=
|
|
16
|
+
suite_py/commands/release.py,sha256=aWKF4vCNywCScWKqdcTWf8hR1EV_8KJcguDqSaC8jqs,11659
|
|
17
17
|
suite_py/commands/set_token.py,sha256=fehIqKjKhE-BJGFhgkPTo3Ntr0MvpgLd6EC5yjKuRs8,1508
|
|
18
18
|
suite_py/commands/status.py,sha256=0JUK53_d1-U3WNS742JD2QTiGmCGZONo3jJx8WR7q70,1122
|
|
19
19
|
suite_py/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -27,7 +27,7 @@ suite_py/lib/handler/git_handler.py,sha256=8UVttqo65CTg5LCvOBTDXb_sNxLKUrpevCGio
|
|
|
27
27
|
suite_py/lib/handler/github_handler.py,sha256=xnBATLOTnOLpiYE29WwUrtDr7hxusfId9a1KbfK1OyA,2952
|
|
28
28
|
suite_py/lib/handler/metrics_handler.py,sha256=-Tp62pFIiYsBkDga0nQG3lWU-gxH68wEjIIIJeU1jHk,3159
|
|
29
29
|
suite_py/lib/handler/okta_handler.py,sha256=UiRcBDmFkMFi9H7Me1QaruC8yPI5fFbnLGzOf3kfxG0,2805
|
|
30
|
-
suite_py/lib/handler/pre_commit_handler.py,sha256=
|
|
30
|
+
suite_py/lib/handler/pre_commit_handler.py,sha256=D2C3b07naOf7wq-OOrrlwWsk5Rk5Zu49-_sNTevDG8I,4300
|
|
31
31
|
suite_py/lib/handler/prompt_utils.py,sha256=vgk1O7h-iYEAZv1sXtMh8xIgH1djI398rzxRIgZWZcg,2474
|
|
32
32
|
suite_py/lib/handler/version_handler.py,sha256=DXTx4yCAbFVC6CdMqPJ-LiN5YM-dT2zklG8POyKTP5A,6774
|
|
33
33
|
suite_py/lib/handler/youtrack_handler.py,sha256=_CwzeFhj7kMYb61CN8IYF0IE55Zrj6vZT38SUZLA5B8,10192
|
|
@@ -40,7 +40,7 @@ suite_py/lib/requests/session.py,sha256=P32H3cWnCWunu91WIj2iDM5U3HzaBglg60VN_C9J
|
|
|
40
40
|
suite_py/lib/symbol.py,sha256=z3QYBuNIwD3qQ3zF-cLOomIr_-C3bO_u5UIDAHMiyTo,60
|
|
41
41
|
suite_py/lib/tokens.py,sha256=4DbsHDFLIxs40t3mRw_ZyhmejZQ0Bht7iAL8dTCTQd4,5458
|
|
42
42
|
suite_py/templates/login.html,sha256=fJLls2SB84oZTSrxTdA5q1PqfvIHcCD4fhVWfyco7Ig,861
|
|
43
|
-
suite_py-1.
|
|
44
|
-
suite_py-1.
|
|
45
|
-
suite_py-1.
|
|
46
|
-
suite_py-1.
|
|
43
|
+
suite_py-1.48.0.dist-info/METADATA,sha256=iX7YnmoO1zUuKbOHahovO6jjEEOvi8g-c-b_D0suc3Y,1250
|
|
44
|
+
suite_py-1.48.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
45
|
+
suite_py-1.48.0.dist-info/entry_points.txt,sha256=dVKLC-9Infy-dHJT_MkK6LcDjOgBCJ8lfPkURJhBjxE,46
|
|
46
|
+
suite_py-1.48.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|