suite-py 1.47.1__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 CHANGED
@@ -1,2 +1,2 @@
1
1
  # -*- encoding: utf-8 -*-
2
- __version__ = "1.47.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")
@@ -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__(self, action, project, captainhook, config, tokens):
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
- commits = []
57
- if latest != "":
58
- logger.info(f"The current release is {latest}")
59
- commits = self._github.get_commits_since_release(self._repo, latest)
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
- logger.info(f"\nCommits list:\n{message}\n")
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
- if not prompt_utils.ask_confirm("Do you want to continue?"):
76
- return
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
- new_version = self._version.select_new_version(
79
- latest, allow_prerelease=True
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
- else:
83
- # Se non viene trovata la release e non ci sono tag, viene saltato il check delle migrations e l'update delle card su youtrack
84
- logger.warning(
85
- f"No tags found, I'm about to push the tag {DEFAULT_VERSION}"
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
- "Are you sure you want to continue?", default=False
171
+ "You didn't update your changelog, are you sure you want to proceed?"
89
172
  ):
90
173
  sys.exit()
91
- new_version = DEFAULT_VERSION
92
- message = f"First release with tag {new_version}"
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
- message = common.ask_for_release_description(message)
109
- sha = commits[0].commit.sha if len(commits) > 0 else ""
110
- self._create_release(new_version, message, sha)
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.is_enabled() and not self.is_pre_commit_hooks_installed():
20
- self.warn_missing_pre_commit_hook()
19
+ if self._is_enabled() and not self._is_pre_commit_hooks_installed():
20
+ self._warn_missing_pre_commit_hook()
21
21
 
22
- def is_pre_commit_hooks_installed(self):
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 heuteristics to check whether the gitleaks pre-commit hook is installed.
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.is_vanilla_hook_setup() or self.is_pre_commit_py_hook_setup()
30
+ return self._is_shell_script_hook_setup() or self._is_pre_commit_py_hook_setup()
29
31
 
30
- def warn_missing_pre_commit_hook(self):
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 is_vanilla_hook_setup(self):
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
- # Filter out lines that start with '#' since those are probably just comments.
57
- without_comments = filter(lambda l: not l.strip().startswith("#"), lines)
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("gitleaks" in line for line in without_comments)
62
+ return any(
63
+ self._script_contains_keyword(path, keyword) for path, keyword in checks
64
+ )
60
65
 
61
- def is_pre_commit_py_hook_setup(self):
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
- # If the framework is not setup skip
66
- if "pre-commit" not in self.read_pre_commit_hook():
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 read_pre_commit_hook(self):
87
- pre_commit_file_path = os.path.join(self._git.hooks_path(), "pre-commit")
88
- logger.debug("Reading pre-commit script from %s", pre_commit_file_path)
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
- with open(pre_commit_file_path, encoding="utf-8") as f:
91
- return f.read()
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
- return ""
102
+ logger.debug("pre-commit script(%s) not found", file_path)
103
+ return False
94
104
 
95
- def is_enabled(self):
96
- return self._git.get_git_config("suite-py.disable-pre-commit-warning") != "true"
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: suite-py
3
- Version: 1.47.1
3
+ Version: 1.48.0
4
4
  Summary:
5
5
  Author: larrywax, EugenioLaghi, michelangelomo
6
6
  Author-email: devops@prima.it
@@ -1,6 +1,6 @@
1
1
  suite_py/__init__.py,sha256=REmi3D0X2G1ZWnYpKs8Ffm3NIj-Hw6dMuvz2b9NW344,142
2
- suite_py/__version__.py,sha256=cKvpK8UUahCMZ1joS87DCaUD5LYTrc2oLgpeZj9IiiQ,49
3
- suite_py/cli.py,sha256=hBbNXb9vVk8lSJXRrb1GctTp3EUjMYFWS9mn_HaNG9I,11155
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=u6pdT1Vk9qBLnfgxcWtpk_KV-hh_NL9yHfFQz6ZcbUs,7909
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=nwqVgC-KnUjn7NVIHZ0VmDdUfp6Q1ieFIth-1_U76Gw,3740
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.47.1.dist-info/METADATA,sha256=wC0GWaZMoj1UKf0XQ_X9I4EfvqRz0ODqXJn3xE_1WQg,1250
44
- suite_py-1.47.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
45
- suite_py-1.47.1.dist-info/entry_points.txt,sha256=dVKLC-9Infy-dHJT_MkK6LcDjOgBCJ8lfPkURJhBjxE,46
46
- suite_py-1.47.1.dist-info/RECORD,,
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,,