suite-py 1.47.1__tar.gz → 1.48.0__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.
- {suite_py-1.47.1 → suite_py-1.48.0}/PKG-INFO +1 -1
- {suite_py-1.47.1 → suite_py-1.48.0}/pyproject.toml +1 -1
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/__version__.py +1 -1
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/cli.py +24 -2
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/release.py +136 -47
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/pre_commit_handler.py +43 -30
- {suite_py-1.47.1 → suite_py-1.48.0}/LICENSE-APACHE +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/LICENSE-MIT +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/__init__.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/__init__.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/ask_review.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/bump.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/check.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/common.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/context.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/create_branch.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/estimate_cone.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/login.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/merge_pr.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/open_pr.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/project_lock.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/set_token.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/commands/status.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/__init__.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/config.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/__init__.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/aws_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/captainhook_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/changelog_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/frequent_reviewers_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/git_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/github_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/metrics_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/okta_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/prompt_utils.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/version_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/handler/youtrack_handler.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/logger.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/metrics.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/oauth.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/requests/__init__.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/requests/auth.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/requests/session.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/symbol.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/lib/tokens.py +0 -0
- {suite_py-1.47.1 → suite_py-1.48.0}/suite_py/templates/login.html +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.48.0"
|
|
@@ -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")
|
|
@@ -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(
|
|
@@ -16,18 +16,20 @@ class PreCommit:
|
|
|
16
16
|
self._git = GitHandler(project, config)
|
|
17
17
|
|
|
18
18
|
def check_and_warn(self):
|
|
19
|
-
if self.
|
|
20
|
-
self.
|
|
19
|
+
if self._is_enabled() and not self._is_pre_commit_hooks_installed():
|
|
20
|
+
self._warn_missing_pre_commit_hook()
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def _is_enabled(self):
|
|
23
|
+
return self._git.get_git_config("suite-py.disable-pre-commit-warning") != "true"
|
|
24
|
+
|
|
25
|
+
def _is_pre_commit_hooks_installed(self):
|
|
23
26
|
"""
|
|
24
|
-
Apply some
|
|
27
|
+
Apply some heuristics to check whether the gitleaks pre-commit hook is installed.
|
|
25
28
|
This is extremely imperfect, and only supports direct calls in shell scripts.
|
|
26
|
-
More hooks, like husky should be added later
|
|
27
29
|
"""
|
|
28
|
-
return self.
|
|
30
|
+
return self._is_shell_script_hook_setup() or self._is_pre_commit_py_hook_setup()
|
|
29
31
|
|
|
30
|
-
def
|
|
32
|
+
def _warn_missing_pre_commit_hook(self):
|
|
31
33
|
logger.warning(
|
|
32
34
|
"""
|
|
33
35
|
Looks like the current repo is missing the gitleaks pre-commit hook!
|
|
@@ -42,28 +44,32 @@ to disable it globally
|
|
|
42
44
|
"""
|
|
43
45
|
)
|
|
44
46
|
|
|
45
|
-
def
|
|
47
|
+
def _is_shell_script_hook_setup(self):
|
|
46
48
|
"""
|
|
47
|
-
Check whether the gitleaks hook is setup as a regular bash script
|
|
48
|
-
"""
|
|
49
|
-
pre_commit_file = self.read_pre_commit_hook()
|
|
50
|
-
|
|
51
|
-
# Assume everything is a shell script.
|
|
52
|
-
# Technically you could use a binary, or even python code,
|
|
53
|
-
# But those are out of scope for us, and the user should just disable the warning themselves
|
|
54
|
-
lines = pre_commit_file.splitlines()
|
|
49
|
+
Check whether the gitleaks hook is setup as a regular bash script:
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
* is there a `.git/hooks/pre-commit` shell script that contains keyword "gitleaks"
|
|
52
|
+
* is there a `.husky/pre-commit` shell script that contains keyword "security-hooks" (primait/security-hooks repo)
|
|
53
|
+
"""
|
|
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
|
+
]
|
|
58
61
|
|
|
59
|
-
return any(
|
|
62
|
+
return any(
|
|
63
|
+
self._script_contains_keyword(path, keyword) for path, keyword in checks
|
|
64
|
+
)
|
|
60
65
|
|
|
61
|
-
def
|
|
66
|
+
def _is_pre_commit_py_hook_setup(self):
|
|
62
67
|
"""
|
|
63
68
|
Check whether the gitleaks hook is setup with the pre-commit python framework
|
|
64
69
|
"""
|
|
65
|
-
#
|
|
66
|
-
|
|
70
|
+
# is there a `.git/hooks/pre-commit` shell script that contains keyword "pre-commit"
|
|
71
|
+
pre_commit_file_path = os.path.join(self._git.hooks_path(), "pre-commit")
|
|
72
|
+
if not self._script_contains_keyword(pre_commit_file_path, "pre-commit"):
|
|
67
73
|
logger.debug("pre-commit.com not installed, skipping config check")
|
|
68
74
|
return False
|
|
69
75
|
|
|
@@ -83,14 +89,21 @@ to disable it globally
|
|
|
83
89
|
for repo in config.get("repos", [])
|
|
84
90
|
)
|
|
85
91
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
def _script_contains_keyword(self, file_path, keyword):
|
|
93
|
+
"""
|
|
94
|
+
Check if a keyword appears in a shell script, ignoring comment lines
|
|
95
|
+
(binaries and python code are out of scope for us).
|
|
96
|
+
"""
|
|
89
97
|
try:
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
logger.debug("checking pre-commit script(%s)", file_path)
|
|
99
|
+
with open(file_path, encoding="utf-8") as f:
|
|
100
|
+
content = f.read()
|
|
92
101
|
except FileNotFoundError:
|
|
93
|
-
|
|
102
|
+
logger.debug("pre-commit script(%s) not found", file_path)
|
|
103
|
+
return False
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
# Filter out comments (lines starting with '#').
|
|
106
|
+
lines_without_comments = (
|
|
107
|
+
line for line in content.splitlines() if not line.strip().startswith("#")
|
|
108
|
+
)
|
|
109
|
+
return any(keyword in line for line in lines_without_comments)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|