suite-py 1.47.2__py3-none-any.whl → 1.49.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 +51 -8
- suite_py/commands/context.py +7 -3
- suite_py/commands/create_branch.py +48 -24
- suite_py/commands/release.py +248 -47
- suite_py/lib/handler/pre_commit_handler.py +10 -3
- {suite_py-1.47.2.dist-info → suite_py-1.49.0.dist-info}/METADATA +2 -2
- {suite_py-1.47.2.dist-info → suite_py-1.49.0.dist-info}/RECORD +10 -10
- {suite_py-1.47.2.dist-info → suite_py-1.49.0.dist-info}/WHEEL +0 -0
- {suite_py-1.47.2.dist-info → suite_py-1.49.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.49.0"
|
suite_py/cli.py
CHANGED
|
@@ -47,8 +47,10 @@ from suite_py.lib.config import Config
|
|
|
47
47
|
from suite_py.lib.handler import git_handler as git
|
|
48
48
|
from suite_py.lib.handler import prompt_utils
|
|
49
49
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
50
|
+
from suite_py.lib.handler.git_handler import GitHandler
|
|
50
51
|
from suite_py.lib.handler.okta_handler import Okta
|
|
51
52
|
from suite_py.lib.handler.pre_commit_handler import PreCommit
|
|
53
|
+
from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
52
54
|
from suite_py.lib.tokens import Tokens
|
|
53
55
|
|
|
54
56
|
# pylint: enable=wrong-import-position
|
|
@@ -176,17 +178,21 @@ def main(ctx, project, timeout, verbose):
|
|
|
176
178
|
if timeout:
|
|
177
179
|
config.user["captainhook_timeout"] = timeout
|
|
178
180
|
|
|
179
|
-
project = os.path.basename(project)
|
|
180
181
|
tokens = Tokens()
|
|
181
182
|
okta = Okta(config, tokens)
|
|
182
183
|
captainhook = CaptainHook(config, okta, tokens)
|
|
184
|
+
project = os.path.basename(project)
|
|
185
|
+
git_handler = GitHandler(project, config)
|
|
186
|
+
youtrack_handler = YoutrackHandler(config, tokens)
|
|
183
187
|
|
|
184
188
|
ctx.obj = Context(
|
|
189
|
+
captainhook=captainhook,
|
|
190
|
+
config=config,
|
|
191
|
+
git_handler=git_handler,
|
|
192
|
+
okta=okta,
|
|
185
193
|
project=project,
|
|
186
194
|
tokens=tokens,
|
|
187
|
-
|
|
188
|
-
config=config,
|
|
189
|
-
captainhook=captainhook,
|
|
195
|
+
youtrack_handler=youtrack_handler,
|
|
190
196
|
)
|
|
191
197
|
|
|
192
198
|
ctx.obj.call(metrics.setup)
|
|
@@ -233,12 +239,27 @@ def bump(obj: Context, project: Optional[str] = None, version: Optional[str] = N
|
|
|
233
239
|
is_flag=True,
|
|
234
240
|
help="Stash uncommitted changes before creating the branch and reapply them afterward",
|
|
235
241
|
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"--parent-branch",
|
|
244
|
+
type=click.STRING,
|
|
245
|
+
help="Parent branch to create the new branch from",
|
|
246
|
+
)
|
|
247
|
+
@click.option(
|
|
248
|
+
"--branch-name",
|
|
249
|
+
type=click.STRING,
|
|
250
|
+
help="Branch name template. Supports {card_id}, {type}, {summary} placeholders (ex. '{card_id}/{type}/{summary}')",
|
|
251
|
+
)
|
|
236
252
|
@click.pass_obj
|
|
237
253
|
@catch_exceptions
|
|
238
|
-
def cli_create_branch(obj, card, autostash):
|
|
254
|
+
def cli_create_branch(obj, card, autostash, parent_branch, branch_name):
|
|
239
255
|
from suite_py.commands.create_branch import CreateBranch
|
|
240
256
|
|
|
241
|
-
obj.call(CreateBranch
|
|
257
|
+
obj.call(CreateBranch).run(
|
|
258
|
+
card_id=card,
|
|
259
|
+
autostash=autostash,
|
|
260
|
+
parent_branch=parent_branch,
|
|
261
|
+
branch_name=branch_name,
|
|
262
|
+
)
|
|
242
263
|
|
|
243
264
|
|
|
244
265
|
@main.command("lock", help="Lock project on staging or prod")
|
|
@@ -326,12 +347,34 @@ def release():
|
|
|
326
347
|
@release.command(
|
|
327
348
|
"create", help="Create a github release (and deploy it if GHA are used)"
|
|
328
349
|
)
|
|
350
|
+
@click.argument(
|
|
351
|
+
"commit",
|
|
352
|
+
required=False,
|
|
353
|
+
type=str,
|
|
354
|
+
metavar="[COMMIT]",
|
|
355
|
+
)
|
|
356
|
+
@click.option(
|
|
357
|
+
"--interactive",
|
|
358
|
+
"-i",
|
|
359
|
+
is_flag=True,
|
|
360
|
+
help="Interactively choose which unreleased commit to release (ignored if COMMIT is provided)",
|
|
361
|
+
)
|
|
329
362
|
@click.pass_obj
|
|
330
363
|
@catch_exceptions
|
|
331
|
-
def cli_release_create(obj):
|
|
364
|
+
def cli_release_create(obj, commit, interactive): # type: ignore[override]
|
|
365
|
+
"""Create a release.
|
|
366
|
+
|
|
367
|
+
Optionally pass a COMMIT (full or short SHA) to create the release at that
|
|
368
|
+
specific commit instead of the latest commit (HEAD). Example:
|
|
369
|
+
|
|
370
|
+
suite-py release create 644a699
|
|
371
|
+
|
|
372
|
+
If you don't provide a COMMIT you can pass --interactive / -i to select
|
|
373
|
+
one among the unreleased commits (those after the last tag).
|
|
374
|
+
"""
|
|
332
375
|
from suite_py.commands.release import Release
|
|
333
376
|
|
|
334
|
-
obj.call(Release, action="create").run()
|
|
377
|
+
obj.call(Release, action="create", commit=commit, interactive=interactive).run()
|
|
335
378
|
|
|
336
379
|
|
|
337
380
|
@main.command("status", help="Current status of a project")
|
suite_py/commands/context.py
CHANGED
|
@@ -3,17 +3,21 @@ from inspect import signature
|
|
|
3
3
|
|
|
4
4
|
from suite_py.lib.config import Config
|
|
5
5
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
6
|
+
from suite_py.lib.handler.git_handler import GitHandler
|
|
6
7
|
from suite_py.lib.handler.okta_handler import Okta
|
|
8
|
+
from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
7
9
|
from suite_py.lib.tokens import Tokens
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@dataclasses.dataclass
|
|
11
13
|
class Context:
|
|
12
|
-
project: str
|
|
13
|
-
config: Config
|
|
14
14
|
captainhook: CaptainHook
|
|
15
|
-
|
|
15
|
+
config: Config
|
|
16
|
+
git_handler: GitHandler
|
|
16
17
|
okta: Okta
|
|
18
|
+
project: str
|
|
19
|
+
tokens: Tokens
|
|
20
|
+
youtrack_handler: YoutrackHandler
|
|
17
21
|
|
|
18
22
|
# Call the function to_call with kwargs, injecting fields from self as default arguments
|
|
19
23
|
def call(self, to_call, **kwargs):
|
|
@@ -11,25 +11,37 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CreateBranch:
|
|
14
|
-
def __init__(
|
|
15
|
-
self
|
|
16
|
-
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
config,
|
|
17
|
+
git_handler: GitHandler,
|
|
18
|
+
youtrack_handler: YoutrackHandler,
|
|
19
|
+
):
|
|
17
20
|
self._config = config
|
|
18
|
-
self.
|
|
19
|
-
self.
|
|
20
|
-
self._autostash = autostash
|
|
21
|
+
self._git_handler = git_handler
|
|
22
|
+
self._youtrack = youtrack_handler
|
|
21
23
|
|
|
22
24
|
@metrics.command("create-branch")
|
|
23
|
-
def run(
|
|
24
|
-
|
|
25
|
+
def run(
|
|
26
|
+
self,
|
|
27
|
+
autostash=False,
|
|
28
|
+
branch_name=None,
|
|
29
|
+
card_id=None,
|
|
30
|
+
parent_branch=None,
|
|
31
|
+
):
|
|
32
|
+
if (
|
|
33
|
+
not self._git_handler.is_detached()
|
|
34
|
+
and self._git_handler.is_dirty()
|
|
35
|
+
and not autostash
|
|
36
|
+
):
|
|
25
37
|
# Default behaviour is to pull when not detached.
|
|
26
38
|
# Can't do that with uncommitted changes.
|
|
27
39
|
logger.error("You have some uncommitted changes, I can't continue")
|
|
28
40
|
sys.exit(-1)
|
|
29
41
|
|
|
30
42
|
try:
|
|
31
|
-
if
|
|
32
|
-
issue = self._youtrack.get_issue(
|
|
43
|
+
if card_id:
|
|
44
|
+
issue = self._youtrack.get_issue(card_id)
|
|
33
45
|
else:
|
|
34
46
|
issue = self._youtrack.get_issue(self._ask_card())
|
|
35
47
|
except Exception:
|
|
@@ -38,7 +50,7 @@ class CreateBranch:
|
|
|
38
50
|
)
|
|
39
51
|
sys.exit(-1)
|
|
40
52
|
|
|
41
|
-
self._checkout_branch(issue,
|
|
53
|
+
self._checkout_branch(issue, autostash, parent_branch, branch_name)
|
|
42
54
|
|
|
43
55
|
user = self._youtrack.get_current_user()
|
|
44
56
|
self._youtrack.assign_to(issue["id"], user["login"])
|
|
@@ -95,33 +107,45 @@ class CreateBranch:
|
|
|
95
107
|
)
|
|
96
108
|
return []
|
|
97
109
|
|
|
98
|
-
def _checkout_branch(
|
|
110
|
+
def _checkout_branch(
|
|
111
|
+
self, issue, autostash=False, parent_branch=None, branch_name_template=None
|
|
112
|
+
):
|
|
99
113
|
default_parent_branch_name = self._config.user.get(
|
|
100
|
-
"default_parent_branch", self.
|
|
114
|
+
"default_parent_branch", self._git_handler.current_branch_name()
|
|
101
115
|
)
|
|
102
116
|
|
|
103
|
-
parent_branch_name = prompt_utils.ask_questions_input(
|
|
104
|
-
"
|
|
117
|
+
parent_branch_name = parent_branch or prompt_utils.ask_questions_input(
|
|
118
|
+
"Enter parent branch: ", default_parent_branch_name
|
|
105
119
|
)
|
|
106
120
|
|
|
107
121
|
full_branch_name = ""
|
|
108
122
|
branch_name = _normalize_git_ref_segment(issue["summary"])
|
|
109
123
|
branch_type = _normalize_git_ref_segment(issue["Type"])
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
if branch_name_template:
|
|
126
|
+
# Use the template and replace placeholders
|
|
127
|
+
full_branch_name = branch_name_template.format(
|
|
128
|
+
card_id=issue["idReadable"], type=branch_type, summary=branch_name
|
|
114
129
|
)
|
|
115
130
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
if not is_branch_name_valid(full_branch_name):
|
|
132
|
+
logger.error(f"Invalid branch name from template: {full_branch_name}")
|
|
133
|
+
sys.exit(-1)
|
|
134
|
+
else:
|
|
135
|
+
while True:
|
|
136
|
+
branch_name = str(
|
|
137
|
+
prompt_utils.ask_questions_input("Enter branch name: ", branch_name)
|
|
138
|
+
)
|
|
119
139
|
|
|
120
|
-
|
|
140
|
+
full_branch_name = f"{issue['idReadable']}/{branch_type}/{branch_name}"
|
|
141
|
+
if is_branch_name_valid(full_branch_name):
|
|
142
|
+
break
|
|
121
143
|
|
|
122
|
-
|
|
144
|
+
logger.error(f"Invalid branch name: {full_branch_name}. Try again?")
|
|
123
145
|
|
|
124
|
-
self.
|
|
146
|
+
self._git_handler.checkout(parent_branch_name, autostash=autostash)
|
|
147
|
+
|
|
148
|
+
self._git_handler.checkout(full_branch_name, new=True, autostash=autostash)
|
|
125
149
|
|
|
126
150
|
|
|
127
151
|
# Normalize a string into a valid segment(ie. the part of the branch name between the /)
|
suite_py/commands/release.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import re
|
|
3
3
|
import sys
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
5
|
|
|
5
6
|
from suite_py.commands import common
|
|
6
7
|
from suite_py.lib import logger, metrics
|
|
@@ -16,11 +17,24 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
|
16
17
|
class Release:
|
|
17
18
|
# pylint: disable=too-many-instance-attributes
|
|
18
19
|
# pylint: disable=too-many-positional-arguments
|
|
19
|
-
def __init__(
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
action,
|
|
23
|
+
project,
|
|
24
|
+
captainhook,
|
|
25
|
+
config,
|
|
26
|
+
tokens,
|
|
27
|
+
commit=None,
|
|
28
|
+
interactive=False,
|
|
29
|
+
):
|
|
20
30
|
self._action = action
|
|
21
31
|
self._project = project
|
|
22
32
|
self._config = config
|
|
23
33
|
self._tokens = tokens
|
|
34
|
+
# Optional commit (sha or short sha) to release instead of HEAD
|
|
35
|
+
self._commit = commit
|
|
36
|
+
# Whether to prompt the user to choose commit among unreleased ones
|
|
37
|
+
self._interactive = interactive
|
|
24
38
|
self._changelog_handler = ChangelogHandler()
|
|
25
39
|
self._youtrack = YoutrackHandler(config, tokens)
|
|
26
40
|
self._captainhook = captainhook
|
|
@@ -52,62 +66,249 @@ class Release:
|
|
|
52
66
|
|
|
53
67
|
def _create(self):
|
|
54
68
|
latest = self._version.get_latest_version()
|
|
69
|
+
commits, new_version, message = self._gather_commits_and_version(latest)
|
|
70
|
+
message = self._augment_message_with_changelog(new_version, message)
|
|
71
|
+
message = common.ask_for_release_description(message)
|
|
72
|
+
sha = self._resolve_target_sha(commits)
|
|
73
|
+
self._create_release(new_version, message, sha)
|
|
55
74
|
|
|
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
|
-
)
|
|
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 not commits:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"No commits found after the latest release tag; nothing to release."
|
|
85
|
+
)
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
# If user did not pass a target commit, ask interactively which commit to release.
|
|
88
|
+
# Only perform interactive selection if user explicitly requested it
|
|
89
|
+
if self._interactive:
|
|
90
|
+
self._select_commit_if_needed(commits)
|
|
91
|
+
commits = self._maybe_trim_commits(commits)
|
|
92
|
+
_check_migrations_deploy(commits)
|
|
93
|
+
message = self._build_commits_message(commits)
|
|
94
|
+
logger.info(f"\nCommits list:\n{message}\n")
|
|
95
|
+
if not prompt_utils.ask_confirm("Do you want to continue?"):
|
|
96
|
+
sys.exit()
|
|
97
|
+
new_version = self._version.select_new_version(latest, allow_prerelease=True)
|
|
98
|
+
return commits, new_version, message
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
def _first_release_flow(self):
|
|
101
|
+
logger.warning(f"No tags found, I'm about to push the tag {DEFAULT_VERSION}")
|
|
102
|
+
if not prompt_utils.ask_confirm(
|
|
103
|
+
"Are you sure you want to continue?", default=False
|
|
104
|
+
):
|
|
105
|
+
sys.exit()
|
|
106
|
+
return [], DEFAULT_VERSION, f"First release with tag {DEFAULT_VERSION}"
|
|
77
107
|
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
def _maybe_trim_commits(self, commits):
|
|
109
|
+
if not self._commit:
|
|
110
|
+
return commits
|
|
111
|
+
try:
|
|
112
|
+
resolved_commit = self._repo.get_commit(self._commit)
|
|
113
|
+
target_sha = resolved_commit.sha
|
|
114
|
+
except Exception:
|
|
115
|
+
logger.error(
|
|
116
|
+
f"The provided commit '{self._commit}' was not found in the repository."
|
|
80
117
|
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
sys.exit(-1)
|
|
119
|
+
target_index = None
|
|
120
|
+
for idx, c in enumerate(commits):
|
|
121
|
+
if (
|
|
122
|
+
c.sha == target_sha
|
|
123
|
+
or getattr(c, "commit", None)
|
|
124
|
+
and c.commit.sha == target_sha
|
|
125
|
+
):
|
|
126
|
+
target_index = idx
|
|
127
|
+
break
|
|
128
|
+
if target_index is None:
|
|
129
|
+
logger.error(
|
|
130
|
+
"The specified commit is not part of the unreleased commits (i.e., not after the latest release)."
|
|
86
131
|
)
|
|
132
|
+
sys.exit(-1)
|
|
133
|
+
# We want ONLY the selected commit and the older commits (towards the previous release),
|
|
134
|
+
# excluding any *newer* commits that appear before it in the list.
|
|
135
|
+
return commits[target_index:]
|
|
136
|
+
|
|
137
|
+
def _select_commit_if_needed(self, commits):
|
|
138
|
+
"""Interactively ask the user which commit to release if none was provided.
|
|
139
|
+
|
|
140
|
+
Presents the unreleased commits (newest first) allowing the user to pick a
|
|
141
|
+
target commit. The chosen commit's SHA (full) is stored in `self._commit` so
|
|
142
|
+
that downstream logic (_maybe_trim_commits / _resolve_target_sha) works
|
|
143
|
+
unchanged.
|
|
144
|
+
"""
|
|
145
|
+
if self._commit or not commits:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
choices = []
|
|
149
|
+
|
|
150
|
+
def build_choice(c, icon):
|
|
151
|
+
summary = c.commit.message.splitlines()[0]
|
|
152
|
+
return {
|
|
153
|
+
"name": f"{icon} {c.sha[:8]} | {summary}",
|
|
154
|
+
"value": c.sha,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
with ThreadPoolExecutor(max_workers=min(8, len(commits))) as executor:
|
|
158
|
+
future_map = {
|
|
159
|
+
executor.submit(self._get_commit_status_icon, c.sha): c for c in commits
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
icon_results = {}
|
|
163
|
+
for future in as_completed(future_map):
|
|
164
|
+
c = future_map[future]
|
|
165
|
+
try:
|
|
166
|
+
icon_results[c.sha] = future.result()
|
|
167
|
+
except Exception:
|
|
168
|
+
icon_results[c.sha] = "?"
|
|
169
|
+
for c in commits:
|
|
170
|
+
choices.append(build_choice(c, icon_results.get(c.sha, "?")))
|
|
171
|
+
|
|
172
|
+
subtitle = "(only one available)" if len(choices) == 1 else "(newest first):"
|
|
173
|
+
selected = prompt_utils.ask_choices(
|
|
174
|
+
f"Select commit to release {subtitle}", choices
|
|
175
|
+
)
|
|
176
|
+
self._commit = selected
|
|
177
|
+
|
|
178
|
+
def _get_commit_status_icon(self, sha):
|
|
179
|
+
"""Return an icon representing the simplified CI status for a commit.
|
|
180
|
+
|
|
181
|
+
Final exposed statuses (icons):
|
|
182
|
+
success (✅), failure (❌), in_progress (🏗️), cancelled (🚫), unknown (❓)
|
|
183
|
+
|
|
184
|
+
Everything else is folded into one of those buckets:
|
|
185
|
+
pending / queued / waiting -> in_progress
|
|
186
|
+
skipped / neutral -> unknown (unless at least one success present)
|
|
187
|
+
Preference order for determining state:
|
|
188
|
+
1. Check runs API
|
|
189
|
+
2. Combined status API
|
|
190
|
+
3. Fallback unknown
|
|
191
|
+
"""
|
|
192
|
+
check_runs = self._safe_get_check_runs(sha)
|
|
193
|
+
state = self._classify_from_check_runs(check_runs) if check_runs else None
|
|
194
|
+
if state is None:
|
|
195
|
+
state = self._classify_from_combined_status(sha)
|
|
196
|
+
return self._icon_for_state(state)
|
|
197
|
+
|
|
198
|
+
def _safe_get_check_runs(self, sha):
|
|
199
|
+
try:
|
|
200
|
+
commit = self._repo.get_commit(sha)
|
|
201
|
+
return list(commit.get_check_runs())
|
|
202
|
+
except Exception:
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
def _classify_from_check_runs(self, check_runs):
|
|
206
|
+
statuses = {r.status for r in check_runs if getattr(r, "status", None)}
|
|
207
|
+
conclusions = {
|
|
208
|
+
r.conclusion for r in check_runs if getattr(r, "conclusion", None)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if self._any_in_progress(statuses):
|
|
212
|
+
return "in_progress"
|
|
213
|
+
if self._any_failure(conclusions):
|
|
214
|
+
return "failure"
|
|
215
|
+
if "cancelled" in conclusions:
|
|
216
|
+
return "cancelled"
|
|
217
|
+
if self._is_success(conclusions):
|
|
218
|
+
return "success"
|
|
219
|
+
return "unknown"
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _any_in_progress(statuses):
|
|
223
|
+
return any(
|
|
224
|
+
s in {"in_progress", "queued", "waiting", "pending"} for s in statuses
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _any_failure(conclusions):
|
|
229
|
+
return any(
|
|
230
|
+
c in {"failure", "timed_out", "action_required", "stale"}
|
|
231
|
+
for c in conclusions
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _is_success(conclusions):
|
|
236
|
+
return bool(conclusions) and (
|
|
237
|
+
all(c == "success" for c in conclusions)
|
|
238
|
+
or ("success" in conclusions and not Release._any_failure(conclusions))
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _classify_from_combined_status(self, sha):
|
|
242
|
+
try:
|
|
243
|
+
status = self._github.get_build_status(self._project, sha)
|
|
244
|
+
combined = (status.state or "unknown").lower()
|
|
245
|
+
except Exception:
|
|
246
|
+
combined = "unknown"
|
|
247
|
+
if combined in {"pending", "queued", "waiting", "in_progress"}:
|
|
248
|
+
return "in_progress"
|
|
249
|
+
if combined == "success":
|
|
250
|
+
return "success"
|
|
251
|
+
if combined == "failure":
|
|
252
|
+
return "failure"
|
|
253
|
+
if combined == "cancelled":
|
|
254
|
+
return "cancelled"
|
|
255
|
+
return "unknown"
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _icon_for_state(state):
|
|
259
|
+
icon_mapping = {
|
|
260
|
+
"success": "✅",
|
|
261
|
+
"failure": "❌",
|
|
262
|
+
"in_progress": "🏗️",
|
|
263
|
+
"cancelled": "🚫",
|
|
264
|
+
"unknown": "❓",
|
|
265
|
+
}
|
|
266
|
+
return icon_mapping.get(state, "❓")
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _build_commits_message(commits):
|
|
270
|
+
return "\n".join(
|
|
271
|
+
[
|
|
272
|
+
"* " + c.commit.message.splitlines()[0] + " by " + c.commit.author.name
|
|
273
|
+
for c in commits
|
|
274
|
+
]
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _augment_message_with_changelog(self, new_version, message):
|
|
278
|
+
if not self._changelog_handler.changelog_exists():
|
|
279
|
+
return message
|
|
280
|
+
latest_tag, latest_entry = self._changelog_handler.get_latest_entry_with_tag()
|
|
281
|
+
if latest_tag != new_version:
|
|
87
282
|
if not prompt_utils.ask_confirm(
|
|
88
|
-
"
|
|
283
|
+
"You didn't update your changelog, are you sure you want to proceed?"
|
|
89
284
|
):
|
|
90
285
|
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}"
|
|
286
|
+
return message
|
|
287
|
+
return f"{latest_entry}\n\n# Commits\n\n{message}"
|
|
107
288
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
289
|
+
def _resolve_target_sha(self, commits):
|
|
290
|
+
if not self._commit:
|
|
291
|
+
return commits[0].commit.sha if commits else ""
|
|
292
|
+
try:
|
|
293
|
+
sha = self._repo.get_commit(self._commit).sha
|
|
294
|
+
except Exception:
|
|
295
|
+
logger.error(
|
|
296
|
+
f"Unable to resolve commit '{self._commit}' during release creation."
|
|
297
|
+
)
|
|
298
|
+
sys.exit(-1)
|
|
299
|
+
self._validate_target_commit_not_tagged(sha)
|
|
300
|
+
return sha
|
|
301
|
+
|
|
302
|
+
def _validate_target_commit_not_tagged(self, sha):
|
|
303
|
+
try:
|
|
304
|
+
for t in self._repo.get_tags():
|
|
305
|
+
if t.commit.sha == sha:
|
|
306
|
+
logger.error(
|
|
307
|
+
f"The commit {sha[:7]} is already tagged with {t.name}. Aborting."
|
|
308
|
+
)
|
|
309
|
+
sys.exit(-1)
|
|
310
|
+
except Exception:
|
|
311
|
+
logger.warning("Could not verify if commit already tagged; continuing.")
|
|
111
312
|
|
|
112
313
|
def _create_release(self, new_version, message, commit):
|
|
113
314
|
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
|
Metadata-Version: 2.1
|
|
2
2
|
Name: suite-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.49.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: larrywax, EugenioLaghi, michelangelomo
|
|
6
6
|
Author-email: devops@prima.it
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pytest (>=7.0.0)
|
|
|
28
28
|
Requires-Dist: python-dateutil (>=2.8.2)
|
|
29
29
|
Requires-Dist: requests (>=2.26.0)
|
|
30
30
|
Requires-Dist: requests-toolbelt (>=0.9.1)
|
|
31
|
-
Requires-Dist: rich (==14.
|
|
31
|
+
Requires-Dist: rich (==14.2.0)
|
|
32
32
|
Requires-Dist: semver (>=3.0.4,<4.0.0)
|
|
33
33
|
Requires-Dist: termcolor (>=1.1.0)
|
|
34
34
|
Requires-Dist: truststore (>=0.7,<0.11) ; python_version >= "3.10"
|
|
@@ -1,19 +1,19 @@
|
|
|
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=Eb0XNt9hhrNhC3w5nV7sDvRlGwgZ9ZlajjY8sZ0n9XQ,49
|
|
3
|
+
suite_py/cli.py,sha256=IV73nNqBwLKLAfucLxyIne_V7uBQT6OBY3Z2VryQt2M,12582
|
|
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
|
|
7
7
|
suite_py/commands/check.py,sha256=jCX59g6DgTA55yD_75mgcqJ5zCjRwl_eIRGDeUFjUWY,3316
|
|
8
8
|
suite_py/commands/common.py,sha256=aWCEvO3hqdheuMUmZcHuc9EGZPQTk7VkzkHJk283MxQ,566
|
|
9
|
-
suite_py/commands/context.py,sha256=
|
|
10
|
-
suite_py/commands/create_branch.py,sha256=
|
|
9
|
+
suite_py/commands/context.py,sha256=NAMZjJtTIi8xKIhdWEYNKwz0f4wUubW2eAj08gxRRbw,1267
|
|
10
|
+
suite_py/commands/create_branch.py,sha256=eilFIsCkMjU51maeJlMMMMBpnM6rlUauXIFA-92j7no,5277
|
|
11
11
|
suite_py/commands/estimate_cone.py,sha256=_RekBWzPlzInlZRpSIeKUVkx-A8Phx0IEYVouTbN7z4,3411
|
|
12
12
|
suite_py/commands/login.py,sha256=A59e1HsbN7Ocv2L_2H0Eb7MZK7AzLkLb72QxBthnIqU,258
|
|
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=e0I6xVDVoa3nb1gMfKrm6NmFhMWADfCMAUi0hrcUw50,15545
|
|
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.49.0.dist-info/METADATA,sha256=a6KzrK5fxwkUyjiSRjHxKbBLf1hZvQbw5qDiDMejPxY,1250
|
|
44
|
+
suite_py-1.49.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
45
|
+
suite_py-1.49.0.dist-info/entry_points.txt,sha256=dVKLC-9Infy-dHJT_MkK6LcDjOgBCJ8lfPkURJhBjxE,46
|
|
46
|
+
suite_py-1.49.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|