qubership-pipelines-common-library 2.0.1__tar.gz → 2.0.3__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.
Files changed (85) hide show
  1. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/PKG-INFO +2 -2
  2. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/pyproject.toml +2 -3
  3. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_cli.py +37 -26
  4. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_logging.py +0 -1
  5. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/jira/jira_add_ticket_comment_command.py +100 -0
  6. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/jira/jira_client.py +215 -0
  7. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/jira/jira_create_ticket_command.py +126 -0
  8. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/jira/jira_update_ticket_command.py +139 -0
  9. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/jira/jira_utils.py +19 -0
  10. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/notifications/__init__.py +0 -0
  11. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/notifications/send_email_command.py +150 -0
  12. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/notifications/send_webex_message_command.py +131 -0
  13. qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/podman/__init__.py +0 -0
  14. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/utils/crypto_utils.py +2 -0
  15. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/LICENSE +0 -0
  16. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/README.md +0 -0
  17. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/__init__.py +0 -0
  18. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/__init__.py +0 -0
  19. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/artifactory_client.py +0 -0
  20. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/execution/__init__.py +0 -0
  21. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/execution/exec_command.py +0 -0
  22. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/execution/exec_context.py +0 -0
  23. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/execution/exec_context_file.py +0 -0
  24. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/execution/exec_info.py +0 -0
  25. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/execution/exec_logger.py +0 -0
  26. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/git_client.py +0 -0
  27. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/github_client.py +0 -0
  28. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/gitlab_client.py +0 -0
  29. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/jenkins_client.py +0 -0
  30. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/kube_client.py +0 -0
  31. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/log_client.py +0 -0
  32. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/maven_client.py +0 -0
  33. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/minio_client.py +0 -0
  34. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/__init__.py +0 -0
  35. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/rest.py +0 -0
  36. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils.py +0 -0
  37. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_aws.py +0 -0
  38. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_context.py +0 -0
  39. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_dictionary.py +0 -0
  40. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_file.py +0 -0
  41. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_json.py +0 -0
  42. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/utils/utils_string.py +0 -0
  43. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v1/webex_client.py +0 -0
  44. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/__init__.py +0 -0
  45. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/__init__.py +0 -0
  46. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/artifact_finder.py +0 -0
  47. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/auth/__init__.py +0 -0
  48. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/auth/aws_credentials.py +0 -0
  49. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/auth/azure_credentials.py +0 -0
  50. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/auth/gcp_credentials.py +0 -0
  51. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/model/__init__.py +0 -0
  52. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/model/artifact.py +0 -0
  53. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/model/artifact_provider.py +0 -0
  54. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/model/credentials.py +0 -0
  55. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/model/credentials_provider.py +0 -0
  56. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/providers/__init__.py +0 -0
  57. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/providers/artifactory.py +0 -0
  58. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/providers/aws_code_artifact.py +0 -0
  59. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/providers/azure_artifacts.py +0 -0
  60. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/providers/gcp_artifact_registry.py +0 -0
  61. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/artifacts_finder/providers/nexus.py +0 -0
  62. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/extensions/pipeline_data_importer.py +0 -0
  63. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/github/__init__.py +0 -0
  64. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/github/github_client.py +0 -0
  65. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/github/github_pipeline_data_importer.py +0 -0
  66. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/github/github_run_pipeline_command.py +0 -0
  67. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/github/safe_github_client.py +0 -0
  68. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/gitlab/__init__.py +0 -0
  69. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/gitlab/custom_extensions.py +0 -0
  70. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/gitlab/gitlab_client.py +0 -0
  71. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/gitlab/gitlab_pipeline_data_importer.py +0 -0
  72. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/gitlab/gitlab_run_pipeline_command.py +0 -0
  73. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/gitlab/safe_gitlab_client.py +0 -0
  74. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/jenkins/__init__.py +0 -0
  75. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/jenkins/custom_extensions.py +0 -0
  76. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/jenkins/jenkins_client.py +0 -0
  77. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/jenkins/jenkins_pipeline_data_importer.py +0 -0
  78. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/jenkins/jenkins_run_pipeline_command.py +0 -0
  79. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/jenkins/safe_jenkins_client.py +0 -0
  80. {qubership_pipelines_common_library-2.0.1/qubership_pipelines_common_library/v2/podman → qubership_pipelines_common_library-2.0.3/qubership_pipelines_common_library/v2/jira}/__init__.py +0 -0
  81. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/podman/podman_command.md +0 -0
  82. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/podman/podman_command.py +0 -0
  83. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/sops/sops_client.py +0 -0
  84. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/utils/extension_utils.py +0 -0
  85. {qubership_pipelines_common_library-2.0.1 → qubership_pipelines_common_library-2.0.3}/qubership_pipelines_common_library/v2/utils/retry_decorator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubership-pipelines-common-library
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Qubership Pipelines common library
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -20,7 +20,7 @@ Requires-Dist: google-cloud-artifact-registry (>=1.16.1,<2.0.0)
20
20
  Requires-Dist: http-exceptions (>=0.2.10,<0.3.0)
21
21
  Requires-Dist: kubernetes (>=34.1.0,<35.0.0)
22
22
  Requires-Dist: minio (>=7.2.12,<8.0.0)
23
- Requires-Dist: python-gitlab (>=4.13.0,<5.0.0)
23
+ Requires-Dist: python-gitlab (>=4.13.0,<6.0.0)
24
24
  Requires-Dist: python-jenkins (>=1.8.2,<2.0.0)
25
25
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
26
26
  Requires-Dist: requests (>=2.32.3,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "qubership-pipelines-common-library"
3
- version = "2.0.1"
3
+ version = "2.0.3"
4
4
  description = "Qubership Pipelines common library"
5
5
  authors = ["Qubership"]
6
6
  readme = "README.md"
@@ -16,7 +16,7 @@ urllib3 = "^2.2.3"
16
16
  python-jenkins = "^1.8.2"
17
17
  GitPython = "^3.1.43"
18
18
  http-exceptions = "^0.2.10"
19
- python-gitlab = "^4.13.0"
19
+ python-gitlab = ">=4.13.0,<6.0.0"
20
20
  minio = "^7.2.12"
21
21
  kubernetes = "^34.1.0"
22
22
  webexpythonsdk = "2.0.1"
@@ -24,7 +24,6 @@ ghapi = "^1.0.6"
24
24
  boto3 = "^1.39.4"
25
25
  google-cloud-artifact-registry = "^1.16.1"
26
26
  rich = "^14.2.0"
27
- # jira = "3.10.5"
28
27
 
29
28
  [tool.poetry.group.test.dependencies]
30
29
  pytest = "^6.0.0"
@@ -1,14 +1,11 @@
1
1
  import logging
2
+ import os
2
3
  import re
3
-
4
+ import sys
4
5
  import click
5
- from rich import box
6
- from rich.logging import RichHandler
7
- from rich.panel import Panel
8
6
 
9
7
  from qubership_pipelines_common_library.v1.execution.exec_logger import ExecutionLogger
10
- from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console, ExtendedReprHighlighter, \
11
- LevelColorFilter
8
+ from qubership_pipelines_common_library.v1.utils.utils_string import UtilsString
12
9
 
13
10
  DEFAULT_CONTEXT_FILE_PATH = 'context.yaml'
14
11
 
@@ -37,26 +34,34 @@ def utils_cli(func):
37
34
 
38
35
  def _configure_global_logger(global_logger: logging.Logger, log_level: str):
39
36
  """Configure the global logger with a specific log level and formatter."""
37
+ log_level_value = getattr(logging, log_level.upper(), logging.INFO)
40
38
  global_logger.setLevel(logging.DEBUG)
41
39
  if global_logger.hasHandlers():
42
40
  global_logger.handlers.clear()
43
41
  global_logger.propagate = True
44
- rich_handler = RichHandler(
45
- console=rich_console,
46
- show_time=False,
47
- show_level=False,
48
- show_path=False,
49
- enable_link_path=False,
50
- rich_tracebacks=True,
51
- tracebacks_show_locals=False,
52
- markup=True,
53
- highlighter=ExtendedReprHighlighter(),
54
- )
55
- rich_handler.addFilter(LevelColorFilter())
56
- rich_handler.setFormatter(logging.Formatter(ExecutionLogger.LEVELNAME_COLORED_FORMAT))
57
- log_level_value = getattr(logging, log_level.upper(), logging.INFO)
58
- rich_handler.setLevel(log_level_value)
59
- global_logger.addHandler(rich_handler)
42
+ if UtilsString.convert_to_bool(os.getenv('NO_RICH', False)):
43
+ stdout_handler = logging.StreamHandler(sys.stdout)
44
+ stdout_handler.setLevel(log_level_value)
45
+ stdout_handler.setFormatter(logging.Formatter(ExecutionLogger.DEFAULT_FORMAT))
46
+ global_logger.addHandler(stdout_handler)
47
+ else:
48
+ from rich.logging import RichHandler
49
+ from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console, ExtendedReprHighlighter, LevelColorFilter
50
+ rich_handler = RichHandler(
51
+ console=rich_console,
52
+ show_time=False,
53
+ show_level=False,
54
+ show_path=False,
55
+ enable_link_path=False,
56
+ rich_tracebacks=True,
57
+ tracebacks_show_locals=False,
58
+ markup=True,
59
+ highlighter=ExtendedReprHighlighter(),
60
+ )
61
+ rich_handler.addFilter(LevelColorFilter())
62
+ rich_handler.setFormatter(logging.Formatter(ExecutionLogger.LEVELNAME_COLORED_FORMAT))
63
+ rich_handler.setLevel(log_level_value)
64
+ global_logger.addHandler(rich_handler)
60
65
 
61
66
 
62
67
  def _print_command_name():
@@ -67,10 +72,16 @@ def _print_command_name():
67
72
  logging.getLogger().warning("Can't find command name.")
68
73
  command_name = ""
69
74
 
70
- command_panel = Panel(f"command_name = {command_name}", expand=False, padding=(0, 1), box=box.ROUNDED)
71
- rich_console.print()
72
- rich_console.print(command_panel)
73
- rich_console.print()
75
+ if UtilsString.convert_to_bool(os.getenv('NO_RICH', False)):
76
+ logging.info(f"command_name = {command_name}")
77
+ else:
78
+ from rich import box
79
+ from rich.panel import Panel
80
+ from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console
81
+ command_panel = Panel(f"command_name = {command_name}", expand=False, padding=(0, 1), box=box.ROUNDED)
82
+ rich_console.print()
83
+ rich_console.print(command_panel)
84
+ rich_console.print()
74
85
 
75
86
 
76
87
  def _transform_kwargs(kwargs):
@@ -18,7 +18,6 @@ soft_theme = Theme({
18
18
  "repr.str": "rgb(140,180,140)",
19
19
  "repr.tag_name": "rgb(200,170,220)",
20
20
  "repr.tag_value": "rgb(170,200,220)",
21
-
22
21
  "repr.time": "rgb(160,190,220) italic",
23
22
  })
24
23
 
@@ -0,0 +1,100 @@
1
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
2
+ from qubership_pipelines_common_library.v2.jira.jira_client import JiraClient, AuthType
3
+ from qubership_pipelines_common_library.v2.jira.jira_utils import JiraUtils
4
+
5
+
6
+ class JiraAddTicketComment(ExecutionCommand):
7
+ """
8
+ Adds comment to JIRA ticket and retrieves latest comments.
9
+
10
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
11
+ ```
12
+ {
13
+ "ticket": {
14
+ "id": "BUG-567", # REQUIRED: Ticket ID
15
+ "comment": "your comment body", # REQUIRED: Comment body
16
+ "latest_comments_count": 50, # OPTIONAL: Number of latest comments to fetch
17
+ },
18
+ "retry_timeout_seconds": 180, # OPTIONAL: Timeout for JIRA client operations in seconds (default: 180)
19
+ "retry_wait_seconds": 1, # OPTIONAL: Wait interval between retries in seconds (default: 1)
20
+ }
21
+ ```
22
+
23
+ Systems Configuration (expected in "systems.jira" block):
24
+ ```
25
+ {
26
+ "url": "https://your_cloud_jira.atlassian.net", # REQUIRED: JIRA server URL
27
+ "username": "your_username_or_email", # REQUIRED: JIRA user login or email
28
+ "password": "<your_token>", # REQUIRED: JIRA user token
29
+ "auth_type": "basic" # OPTIONAL: 'basic' or 'bearer'
30
+ }
31
+ ```
32
+
33
+ Command name: "jira-add-ticket-comment"
34
+ """
35
+
36
+ RETRY_TIMEOUT_SECONDS = 180 # default value, how many seconds to try
37
+ RETRY_WAIT_SECONDS = 1 # default value, how many seconds between tries
38
+ LATEST_COMMENTS_COUNT = 50 # default value, max amount of comments in response
39
+
40
+ def _validate(self):
41
+ names = [
42
+ "paths.input.params",
43
+ "paths.output.params",
44
+ "systems.jira.url",
45
+ "systems.jira.username",
46
+ "systems.jira.password",
47
+ "params.ticket.id",
48
+ "params.ticket.comment",
49
+ ]
50
+ if not self.context.validate(names):
51
+ return False
52
+
53
+ self.retry_timeout_seconds = int(self.context.input_param_get("params.retry_timeout_seconds", self.RETRY_TIMEOUT_SECONDS))
54
+ self.retry_wait_seconds = int(self.context.input_param_get("params.retry_wait_seconds", self.RETRY_WAIT_SECONDS))
55
+
56
+ self.jira_url = self.context.input_param_get("systems.jira.url").rstrip('/')
57
+ self.jira_username = self.context.input_param_get("systems.jira.username")
58
+ self.jira_password = self.context.input_param_get("systems.jira.password")
59
+ self.auth_type = self.context.input_param_get("systems.jira.auth_type", AuthType.BASIC)
60
+
61
+ self.ticket_key = self.context.input_param_get("params.ticket.id")
62
+ self.ticket_comment = self.context.input_param_get("params.ticket.comment")
63
+ self.latest_comments_count = int(self.context.input_param_get("params.ticket.latest_comments_count", self.LATEST_COMMENTS_COUNT))
64
+ return True
65
+
66
+ def _execute(self):
67
+ self.context.logger.info("Running jira-add-ticket-comment")
68
+ self.jira_client = JiraClient.create_jira_client(
69
+ self.jira_url, self.jira_username, self.jira_password, self.auth_type,
70
+ self.retry_timeout_seconds, self.retry_wait_seconds,
71
+ )
72
+
73
+ if self.ticket_comment:
74
+ JiraUtils.add_ticket_comment(self)
75
+
76
+ total_comments = 0
77
+ parsed_latest_comments = []
78
+ try:
79
+ self.latest_comments = self.jira_client.get_latest_ticket_comments(self.ticket_key, max_results=self.latest_comments_count)
80
+ total_comments = len(self.latest_comments)
81
+ parsed_latest_comments = [self._parse_comment(comment) for comment in self.latest_comments]
82
+ except Exception as e:
83
+ self.context.logger.warning(f"Can't get latest ticket comments. Response exception: {str(e)}")
84
+
85
+ self.context.output_param_set("params.ticket.id", self.ticket_key)
86
+ self.context.output_param_set("params.ticket.url", f"{self.jira_url}/browse/{self.ticket_key}")
87
+ self.context.output_param_set("params.ticket.total_comments", total_comments)
88
+ self.context.output_param_set("params.ticket.latest_comments", parsed_latest_comments)
89
+ self.context.output_params_save()
90
+ self.context.logger.info("Add ticket comment request executed. See output params for details")
91
+
92
+ @staticmethod
93
+ def _parse_comment(comment):
94
+ return {
95
+ "body": comment.get("body"),
96
+ "created": comment.get("created"),
97
+ "updated": comment.get("updated"),
98
+ "author": JiraClient.serialize_person_ref(comment.get("author", {})),
99
+ "updateAuthor": JiraClient.serialize_person_ref(comment.get("updateAuthor", {})),
100
+ }
@@ -0,0 +1,215 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ import requests
5
+
6
+ from enum import StrEnum
7
+ from typing import Any
8
+ from requests import Response
9
+ from requests.auth import HTTPBasicAuth
10
+ from qubership_pipelines_common_library.v2.utils.retry_decorator import RetryDecorator
11
+
12
+
13
+ class AuthType(StrEnum):
14
+ BASIC = 'basic'
15
+ BEARER = 'bearer'
16
+
17
+
18
+ class JiraClient:
19
+
20
+ DEFAULT_FIELD_NAMES_FILTER = [
21
+ "fixVersions",
22
+ "resolution",
23
+ "priority",
24
+ "labels",
25
+ "versions",
26
+ "assignee",
27
+ "status",
28
+ "components",
29
+ "creator",
30
+ "reporter",
31
+ "issuetype",
32
+ "project",
33
+ "resolutiondate",
34
+ "created",
35
+ "updated",
36
+ "description",
37
+ "summary",
38
+ "customfield_10014", # Found in
39
+ ]
40
+
41
+ API_VERSION = "2"
42
+
43
+ @classmethod
44
+ @RetryDecorator(condition_func=lambda client: client is not None)
45
+ def create_jira_client(cls, host: str, user: str, password: str, auth_type: str,
46
+ retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1):
47
+ return cls(host, user, password, auth_type)
48
+
49
+ def __init__(self, host: str, user: str, password: str, auth_type: str = AuthType.BASIC):
50
+ self.host = host.rstrip("/")
51
+ self.user = user
52
+ self.password = password
53
+ self.session = requests.Session()
54
+ self.session.verify = os.getenv("PYTHONHTTPSVERIFY", "1") != "0"
55
+ if auth_type.lower() == AuthType.BEARER:
56
+ self.session.headers.update({"Authorization": f"Bearer {password}"})
57
+ else:
58
+ self.session.auth = HTTPBasicAuth(user, password)
59
+ self.headers = {"Accept": "application/json", "Content-Type": "application/json"}
60
+ self.session.headers.update(self.headers)
61
+ self.logger = logging.getLogger()
62
+ try:
63
+ self.server_info = self.get_server_info()
64
+ self.deployment_type = self.server_info.get("deploymentType", "Server")
65
+ except Exception as e:
66
+ self.logger.info(f"Could not get Jira instance version, assuming 'Server': {e}")
67
+ self.deployment_type = "Server"
68
+ self.logger.info(f"Jira Client configured for {self.host}, deployment type: {self.deployment_type}")
69
+
70
+ @property
71
+ def _is_cloud(self) -> bool:
72
+ return self.deployment_type == "Cloud"
73
+
74
+ def get_server_info(self) -> dict[str, Any]:
75
+ retry = 0
76
+ j = self._get_json("serverInfo")
77
+ while not j and retry < 3:
78
+ retry += 1
79
+ j = self._get_json("serverInfo")
80
+ return j
81
+
82
+ @RetryDecorator(condition_func=lambda response: response is not None and (response.ok or response.status_code == 400)) # do not retry BadRequest
83
+ def add_ticket_comment(self, ticket_id: str, comment: str, retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1) -> Response:
84
+ response = self.session.post(
85
+ url=f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/comment",
86
+ data=json.dumps({"body": comment})
87
+ )
88
+ self.logger.debug(f"Add ticket (id={ticket_id}) comment response: status_code = {response.status_code}, body = {response.text}")
89
+ return response
90
+
91
+ def get_latest_ticket_comments(self, ticket_id: str, max_results: int = 50) -> list:
92
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/comment?maxResults={max_results}&orderBy=-created")
93
+ self.logger.debug(f"Get ticket (id={ticket_id}) comments response: status_code = {response.status_code}, body = {response.text}")
94
+ response.raise_for_status()
95
+ return response.json().get("comments", [])
96
+
97
+ @RetryDecorator(condition_func=lambda response: response is not None and (response.ok or response.status_code == 400))
98
+ def create_ticket(self, ticket_fields: dict, retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1) -> Response:
99
+ body = {"fields": ticket_fields}
100
+ response = self.session.post(f"{self.host}/rest/api/{self.API_VERSION}/issue", data=json.dumps(body))
101
+ self.logger.debug(f"Create ticket response: status_code = {response.status_code}, body = {response.text}")
102
+ return response
103
+
104
+ def get_createmeta_fields(self, project_key: str, issue_type_name: str) -> dict:
105
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/createmeta/{project_key}/issuetypes?maxResults=100")
106
+ self.logger.debug(f"Get createmeta for project (id={project_key}) response: status_code = {response.status_code}, body = {response.text}")
107
+ if not response.ok:
108
+ self.logger.warning(f"Can't get issuetypes by projectKey = {project_key}. Response status = {response.status_code}")
109
+ return {}
110
+
111
+ issue_types = response.json().get("issueTypes" if self._is_cloud else "values", [])
112
+ issue_type_ids = [issue_type.get("id") for issue_type in issue_types if issue_type.get("name") == issue_type_name]
113
+ if not issue_type_ids:
114
+ self.logger.warning(f"Can't find issue type id by issue_type_name = {issue_type_name}")
115
+ return {}
116
+
117
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/createmeta/{project_key}/issuetypes/{issue_type_ids[0]}?maxResults=100")
118
+ self.logger.debug(f"Get createmeta for issuetype (id={issue_type_ids[0]}) response: status_code = {response.status_code}, body = {response.text}")
119
+ if not response.ok:
120
+ self.logger.warning(f"Can't get createmeta by projectKey = {project_key} and issue_type_id = {issue_type_ids[0]}. Response status = {response.status_code}")
121
+ return {}
122
+
123
+ fields = response.json().get("fields" if self._is_cloud else "values", [])
124
+ return {field["fieldId"]: field for field in fields}
125
+
126
+ def get_ticket_fields(self, ticket_id: str, field_names_filter: list) -> dict:
127
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}?fields={','.join(field_names_filter)}")
128
+ self.logger.debug(f"Get ticket fields (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
129
+ if not response.ok:
130
+ self.logger.warning(f"Can't get ticket info by ticket_id = {ticket_id}. Response status = {response.status_code}")
131
+ return {}
132
+ return self._transform_ticket_fields(response.json().get("fields", {}))
133
+
134
+ def get_editmeta_fields(self, ticket_id: str) -> dict:
135
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/editmeta")
136
+ self.logger.debug(f"Get editmeta for ticket (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
137
+ if not response.ok:
138
+ self.logger.warning(f"Can't get ticket {ticket_id} editmeta. Response status = {response.status_code}")
139
+ return {}
140
+ return response.json().get("fields", {})
141
+
142
+ @RetryDecorator(condition_func=lambda response: response is not None and (response.ok or response.status_code == 400))
143
+ def update_ticket(self, ticket_id: str, ticket_fields: dict,
144
+ retry_timeout_seconds: int = 180, retry_wait_seconds: int = 1) -> Response:
145
+ body = {"fields": ticket_fields}
146
+ response = self.session.put(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}", data=json.dumps(body))
147
+ self.logger.debug(f"Update ticket (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
148
+ return response
149
+
150
+ def get_ticket_transitions(self, ticket_id: str):
151
+ response = self.session.get(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/transitions?expand=transitions.fields")
152
+ self.logger.debug(f"Get ticket (id={ticket_id}) transitions response: status_code = {response.status_code}, body = {response.text}")
153
+ if not response.ok:
154
+ self.logger.warning(f"Ticket {ticket_id} transitions are not found in response. Response status = {response.status_code}")
155
+ return []
156
+ return response.json().get("transitions", [])
157
+
158
+ def find_applicable_transition(self, transitions: list, status_name: str, transition_name: str = ""):
159
+ transitions_by_next_status = [transition for transition in transitions if status_name.lower() == transition.get("to", {}).get("name", "").lower()]
160
+ if not transitions_by_next_status:
161
+ self.logger.error(f"Status '{status_name}' is not found in transitions.")
162
+ return None
163
+
164
+ if len(transitions_by_next_status) > 1:
165
+ self.logger.warning(f"Found more than one transition to status '{status_name}'")
166
+ if transition_name:
167
+ transitions_by_name = [transition for transition in transitions_by_next_status if transition_name.lower() == transition.get("name", "").lower()]
168
+ if transitions_by_name:
169
+ return transitions_by_name[0]
170
+ else:
171
+ self.logger.warning(f"Transition '{transition_name}' is not found in transitions. Will use first found transition by status '{status_name}'.")
172
+
173
+ return transitions_by_next_status[0]
174
+
175
+ def perform_ticket_transition(self, ticket_id: str, transition_id: str, transition_fields):
176
+ body = {"transition": {"id": transition_id}}
177
+ if transition_fields:
178
+ body["fields"] = transition_fields
179
+ response = self.session.post(f"{self.host}/rest/api/{self.API_VERSION}/issue/{ticket_id}/transitions", data=json.dumps(body))
180
+ self.logger.debug(f"Perform transition for ticket (id={ticket_id}) response: status_code = {response.status_code}, body = {response.text}")
181
+ return response
182
+
183
+ def _get_json(self, path: str, params: dict[str, Any] | None = None, use_post: bool = False) -> dict | list:
184
+ url = f"{self.host}/rest/api/{self.API_VERSION}/{path}"
185
+ response = (
186
+ self.session.post(url, data=json.dumps(params))
187
+ if use_post
188
+ else self.session.get(url, params=params)
189
+ )
190
+ return response.json()
191
+
192
+ @staticmethod
193
+ def filter_ticket_fields(ticket_fields: dict, meta_fields_filter: dict) -> dict:
194
+ return {field_key: field_value for field_key, field_value in ticket_fields.items() if field_key in meta_fields_filter.keys()}
195
+
196
+ @staticmethod
197
+ def _transform_ticket_fields(ticket_fields_json: dict):
198
+ filtered_fields = {}
199
+ for k, v in ticket_fields_json.items():
200
+ if isinstance(v, dict) and "emailAddress" in v:
201
+ filtered_fields[k] = JiraClient.serialize_person_ref(v)
202
+ else:
203
+ filtered_fields[k] = v
204
+ return filtered_fields
205
+
206
+ @staticmethod
207
+ def serialize_person_ref(person: dict) -> dict | None:
208
+ if person:
209
+ return {
210
+ "displayName": person.get("displayName", None),
211
+ "emailAddress": person.get("emailAddress", None),
212
+ "name": person.get("name", None),
213
+ "key": person.get("key", None),
214
+ }
215
+ return None
@@ -0,0 +1,126 @@
1
+ import re
2
+
3
+ from qubership_pipelines_common_library.v1.execution.exec_command import ExecutionCommand
4
+ from qubership_pipelines_common_library.v2.jira.jira_client import JiraClient, AuthType
5
+ from qubership_pipelines_common_library.v2.jira.jira_utils import JiraUtils
6
+
7
+
8
+ class JiraCreateTicket(ExecutionCommand):
9
+ """
10
+ Creates new issue/ticket in JIRA project.
11
+
12
+ Input Parameters Structure (this structure is expected inside "input_params.params" block):
13
+ ```
14
+ {
15
+ "ticket": {
16
+ "fields: { # REQUIRED: Dict structure that will be used as ticket-creation-body, without transformations
17
+ "project": {"key": "<YOUR_PROJECT_KEY>"}, # REQUIRED: Project Key
18
+ "issuetype": {"name": "Bug"}, # REQUIRED: Issue type name
19
+ "priority": {"name": "High"}, # OPTIONAL: Other ticket fields with different formats, depending on your Project configuration
20
+ "duedate": "2030-02-20", # OPTIONAL: Text-value fields need no dict wrappers
21
+ "summary": "[SOME_LABEL] Ticket Subject",
22
+ "description": "Ticket body",
23
+ "components": [{"name":"COMPONENT NAME"}],
24
+ "labels": ["Test_Label1"],
25
+ },
26
+ "comment": "your comment body", # OPTIONAL: Comment to add to created ticket
27
+ "field_names_filter": "summary,issuetype,creator,status", # OPTIONAL: Comma-separated names of fields to extract from created ticket to output params
28
+ },
29
+ "retry_timeout_seconds": 180, # OPTIONAL: Timeout for JIRA client operations in seconds (default: 180)
30
+ "retry_wait_seconds": 1, # OPTIONAL: Wait interval between retries in seconds (default: 1)
31
+ }
32
+ ```
33
+
34
+ Systems Configuration (expected in "systems.jira" block):
35
+ ```
36
+ {
37
+ "url": "https://your_cloud_jira.atlassian.net", # REQUIRED: JIRA server URL
38
+ "username": "your_username_or_email", # REQUIRED: JIRA user login or email
39
+ "password": "<your_token>", # REQUIRED: JIRA user token
40
+ "auth_type": "basic" # OPTIONAL: 'basic' or 'bearer'
41
+ }
42
+ ```
43
+
44
+ Command name: "jira-create-ticket"
45
+ """
46
+ RETRY_TIMEOUT_SECONDS = 180 # default value, how many seconds to try
47
+ RETRY_WAIT_SECONDS = 1 # default value, how many seconds between tries
48
+
49
+ def _validate(self):
50
+ names = [
51
+ "paths.input.params",
52
+ "paths.output.params",
53
+ "systems.jira.url",
54
+ "systems.jira.username",
55
+ "systems.jira.password",
56
+ "params.ticket.fields",
57
+ ]
58
+ if not self.context.validate(names):
59
+ return False
60
+
61
+ self.retry_timeout_seconds = int(self.context.input_param_get("params.retry_timeout_seconds", self.RETRY_TIMEOUT_SECONDS))
62
+ self.retry_wait_seconds = int(self.context.input_param_get("params.retry_wait_seconds", self.RETRY_WAIT_SECONDS))
63
+
64
+ self.jira_url = self.context.input_param_get("systems.jira.url").rstrip('/')
65
+ self.jira_username = self.context.input_param_get("systems.jira.username")
66
+ self.jira_password = self.context.input_param_get("systems.jira.password")
67
+ self.auth_type = self.context.input_param_get("systems.jira.auth_type", AuthType.BASIC)
68
+
69
+ self.ticket_comment = self.context.input_param_get("params.ticket.comment")
70
+ self.ticket_fields = self.context.input_param_get("params.ticket.fields", {})
71
+ self.project_key = self.ticket_fields.get('project', {}).get('key')
72
+ self.issue_type_name = self.ticket_fields.get('issuetype', {}).get('name')
73
+
74
+ if not self.project_key or not self.issue_type_name:
75
+ self.context.logger.error("Can't find project.key and/or issuetype.name in input parameters")
76
+ return False
77
+
78
+ if not self._validate_mandatory_ticket_fields(self.ticket_fields):
79
+ return False
80
+
81
+ if field_names_filter := self.context.input_param_get("params.ticket.field_names_filter"):
82
+ self.field_names_filter = [x.strip() for x in re.split(r'[,;]+', field_names_filter)]
83
+ else:
84
+ self.field_names_filter = JiraClient.DEFAULT_FIELD_NAMES_FILTER
85
+
86
+ return True
87
+
88
+ def _validate_mandatory_ticket_fields(self, ticket_fields):
89
+ valid = True
90
+ for field_key in ["project", "issuetype", "summary"]:
91
+ if field_key not in ticket_fields:
92
+ valid = False
93
+ self.context.logger.error(f"Parameter '{field_key}' is mandatory but not found in ticket params map")
94
+ return valid
95
+
96
+ def _execute(self):
97
+ self.context.logger.info("Running jira-create-ticket")
98
+ self.context.logger.info(f"Creating ticket in project {self.project_key}, type {self.issue_type_name}")
99
+ self.jira_client = JiraClient.create_jira_client(
100
+ self.jira_url, self.jira_username, self.jira_password, self.auth_type,
101
+ self.retry_timeout_seconds, self.retry_wait_seconds,
102
+ )
103
+
104
+ createmeta_fields = self.jira_client.get_createmeta_fields(self.project_key, self.issue_type_name)
105
+ self.filtered_ticket_fields = JiraClient.filter_ticket_fields(self.ticket_fields, createmeta_fields)
106
+ self.context.logger.debug(f"Filtered ticket fields: {self.filtered_ticket_fields}")
107
+
108
+ create_ticket_response = self.jira_client.create_ticket(self.filtered_ticket_fields,
109
+ retry_timeout_seconds=self.retry_timeout_seconds,
110
+ retry_wait_seconds=self.retry_wait_seconds)
111
+ if not create_ticket_response.ok:
112
+ self._exit(False, f"Can't create ticket. Response status: {create_ticket_response.status_code}")
113
+ self.ticket_key = create_ticket_response.json().get('key')
114
+ self.context.logger.info(f"Ticket created successfully: {self.ticket_key}")
115
+
116
+ if self.ticket_comment:
117
+ JiraUtils.add_ticket_comment(self)
118
+
119
+ self.context.output_param_set("params.ticket.id", self.ticket_key)
120
+ self.context.output_param_set("params.ticket.url", f"{self.jira_url}/browse/{self.ticket_key}")
121
+
122
+ filtered_response_fields = self.jira_client.get_ticket_fields(self.ticket_key, self.field_names_filter)
123
+ self.context.output_param_set("params.ticket.fields", filtered_response_fields)
124
+
125
+ self.context.output_params_save()
126
+ self.context.logger.info("JIRA ticket creation completed successfully")