spl-core 7.14.0rc4.dev3__tar.gz → 7.15.0rc1__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 (72) hide show
  1. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/PKG-INFO +2 -1
  2. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/pyproject.toml +3 -2
  3. spl_core-7.15.0rc1/src/spl_core/__init__.py +1 -0
  4. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/common.cmake +23 -1
  5. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/test_utils/artifacts_archiver.py +183 -22
  6. spl_core-7.15.0rc1/src/spl_core/test_utils/junit_merger.py +122 -0
  7. spl_core-7.14.0rc4.dev3/src/spl_core/__init__.py +0 -1
  8. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/LICENSE +0 -0
  9. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/README.md +0 -0
  10. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/__run.py +0 -0
  11. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/common/__init__.py +0 -0
  12. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/common/path.py +0 -0
  13. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/conan.cmake +0 -0
  14. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/config/KConfig +0 -0
  15. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/gcov_maid/__init__.py +0 -0
  16. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/gcov_maid/gcov_maid.py +0 -0
  17. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kconfig/__init__.py +0 -0
  18. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kconfig/kconfig.py +0 -0
  19. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kconfig.cmake +0 -0
  20. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/__init__.py +0 -0
  21. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/create.py +0 -0
  22. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/.vscode/cmake-variants.json +0 -0
  23. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/KConfig +0 -0
  24. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/greeter/CMakeLists.txt +0 -0
  25. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/greeter/doc/_images/screenshot.png +0 -0
  26. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/greeter/doc/index.md +0 -0
  27. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/greeter/src/greeter.c +0 -0
  28. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/greeter/src/greeter.h +0 -0
  29. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/greeter/test/test_greeter.cc +0 -0
  30. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/main/CMakeLists.txt +0 -0
  31. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/main/doc/index.md +0 -0
  32. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/src/main/src/main.c +0 -0
  33. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/test/EnglishVariant/test__EnglishVariant.py +0 -0
  34. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/test/German/test__GermanVariant.py +0 -0
  35. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/variants/EnglishVariant/config.cmake +0 -0
  36. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/variants/EnglishVariant/parts.cmake +0 -0
  37. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/variants/GermanVariant/config.cmake +0 -0
  38. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/variants/GermanVariant/config.txt +0 -0
  39. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/application/variants/GermanVariant/parts.cmake +0 -0
  40. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/.gitignore +0 -0
  41. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/.vscode/cmake-kits.json +0 -0
  42. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/.vscode/extensions.json +0 -0
  43. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/.vscode/launch.json +0 -0
  44. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/.vscode/settings.json +0 -0
  45. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/.vscode/tasks.json +0 -0
  46. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/CMakeLists.txt +0 -0
  47. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/README.md +0 -0
  48. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/bootstrap.json +0 -0
  49. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/build.bat +0 -0
  50. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/build.ps1 +0 -0
  51. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/conf.py +0 -0
  52. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/Doxyfile.in +0 -0
  53. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/common/index.md +0 -0
  54. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/components/index.md +0 -0
  55. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/doxygen-awesome/LICENSE +0 -0
  56. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/doxygen-awesome/doxygen-awesome.css +0 -0
  57. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/software_architecture/index.md +0 -0
  58. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/software_requirements/index.md +0 -0
  59. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/doc/test_report_template.txt +0 -0
  60. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/index.md +0 -0
  61. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/pypeline.yaml +0 -0
  62. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/pyproject.toml +0 -0
  63. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/pytest.ini +0 -0
  64. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/scoopfile.json +0 -0
  65. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/tools/toolchains/clang/toolchain.cmake +0 -0
  66. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/kickstart/templates/project/tools/toolchains/gcc/toolchain.cmake +0 -0
  67. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/main.py +0 -0
  68. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/spl.cmake +0 -0
  69. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/steps/collect_pr_changes.py +0 -0
  70. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/test_utils/archive_artifacts_collection.py +0 -0
  71. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/test_utils/base_variant_test_runner.py +0 -0
  72. {spl_core-7.14.0rc4.dev3 → spl_core-7.15.0rc1}/src/spl_core/test_utils/spl_build.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spl-core
3
- Version: 7.14.0rc4.dev3
3
+ Version: 7.15.0rc1
4
4
  Summary: Software Product Line Support for CMake
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -20,6 +20,7 @@ Requires-Dist: cookiecutter (==2.6.0)
20
20
  Requires-Dist: doxysphinx (>=3.3,<4.0)
21
21
  Requires-Dist: gcovr (>=8.3,<9.0)
22
22
  Requires-Dist: hammocking (>=0.8,<0.10)
23
+ Requires-Dist: junitparser
23
24
  Requires-Dist: kconfiglib (>=14.1,<15.0)
24
25
  Requires-Dist: mlx-traceability (>=10,<12)
25
26
  Requires-Dist: myst-parser (>=0.16)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "spl-core"
3
- version = "7.14.0-rc4.dev.3"
3
+ version = "7.15.0-rc.1"
4
4
  description = "Software Product Line Support for CMake"
5
5
  authors = ["Avengineers <karsten.guenther@kamg.de>"]
6
6
  license = "MIT"
@@ -18,6 +18,7 @@ packages = [{ include = "spl_core", from = "src" }]
18
18
 
19
19
  [tool.poetry.scripts]
20
20
  please = "spl_core.main:main"
21
+ junit_merger = "spl_core.test_utils.junit_merger:main"
21
22
 
22
23
  [tool.poetry.urls]
23
24
  "Bug Tracker" = "https://github.com/avengineers/spl-core/issues"
@@ -49,6 +50,7 @@ sphinx-design = ">=0.5,<0.7"
49
50
  pypeline-semantic-release = ">=0.4.1,<=0.5.0"
50
51
  pypeline-runner = ">=1,<=2"
51
52
  py7zr = "^1.0.0"
53
+ junitparser = "*"
52
54
 
53
55
  [tool.poetry.group.dev.dependencies]
54
56
  pytest = ">=7,<9"
@@ -57,7 +59,6 @@ pre-commit = "^3.1.1"
57
59
  ruff = ">=0.5,<0.13"
58
60
  jinja2 = "*"
59
61
  testfixtures = "*"
60
- junitparser = "*"
61
62
  mashumaro = "*"
62
63
  loguru = "*"
63
64
  flake8 = "*"
@@ -0,0 +1 @@
1
+ __version__ = "7.15.0-rc.1"
@@ -606,15 +606,36 @@ set(COV_OUT_JSON coverage.json)
606
606
  function(_spl_coverage_create_overall_report)
607
607
  if(_SPL_COVERAGE_CREATE_OVERALL_REPORT_IS_NECESSARY)
608
608
  set(COV_OUT_VARIANT_HTML reports/coverage/index.html)
609
+ set(COV_OUT_VARIANT_JSON variant-coverage.json)
610
+ set(JUNIT_OUT_VARIANT_XML variant-junit.xml)
611
+
612
+ # Generate variant-level merged coverage JSON
613
+ add_custom_command(
614
+ OUTPUT ${COV_OUT_VARIANT_JSON}
615
+ COMMAND gcovr --root ${CMAKE_SOURCE_DIR} --add-tracefile \"${CMAKE_CURRENT_BINARY_DIR}/**/${COV_OUT_JSON}\" --json --output ${COV_OUT_VARIANT_JSON}
616
+ DEPENDS ${GLOBAL_COMPONENTS_COVERAGE_JSON_LIST}
617
+ COMMENT "Generating variant-level merged coverage JSON ${COV_OUT_VARIANT_JSON} ..."
618
+ )
619
+
620
+ # Generate variant-level merged JUnit XML
621
+ add_custom_command(
622
+ OUTPUT ${JUNIT_OUT_VARIANT_XML}
623
+ COMMAND junit_merger --output ${JUNIT_OUT_VARIANT_XML} --inputs ${GLOBAL_COMPONENTS_JUNIT_XML_LIST}
624
+ DEPENDS ${GLOBAL_COMPONENTS_JUNIT_XML_LIST}
625
+ COMMENT "Generating variant-level merged JUnit XML ${JUNIT_OUT_VARIANT_XML} ..."
626
+ )
627
+
628
+ # Generate variant-level HTML coverage report
609
629
  add_custom_command(
610
630
  OUTPUT ${COV_OUT_VARIANT_HTML}
611
631
  COMMAND gcovr --root ${CMAKE_SOURCE_DIR} --add-tracefile \"${CMAKE_CURRENT_BINARY_DIR}/**/${COV_OUT_JSON}\" --html --html-details --output ${COV_OUT_VARIANT_HTML}
612
632
  DEPENDS ${GLOBAL_COMPONENTS_COVERAGE_JSON_LIST}
613
633
  COMMENT "Generating overall code coverage report ${COV_OUT_VARIANT_HTML} ..."
614
634
  )
635
+
615
636
  add_custom_target(
616
637
  unittests
617
- DEPENDS coverage ${COV_OUT_VARIANT_HTML}
638
+ DEPENDS coverage ${COV_OUT_VARIANT_HTML} ${COV_OUT_VARIANT_JSON} ${JUNIT_OUT_VARIANT_XML}
618
639
  )
619
640
  add_custom_target(
620
641
  coverage_overall_report
@@ -718,6 +739,7 @@ macro(_spl_add_test_suite COMPONENT_NAME PROD_SRC TEST_SOURCES)
718
739
  DEPENDS ${exe_name}
719
740
  )
720
741
 
742
+ set(GLOBAL_COMPONENTS_JUNIT_XML_LIST "${GLOBAL_COMPONENTS_JUNIT_XML_LIST};${CMAKE_CURRENT_BINARY_DIR}/${TEST_OUT_JUNIT}" CACHE INTERNAL "List of all ${TEST_OUT_JUNIT} files")
721
743
  set(GLOBAL_COMPONENTS_COVERAGE_JSON_LIST "${GLOBAL_COMPONENTS_COVERAGE_JSON_LIST};${CMAKE_CURRENT_BINARY_DIR}/${COV_OUT_JSON}" CACHE INTERNAL "List of all ${COV_OUT_JSON} files")
722
744
 
723
745
  # Create coverage results (coverage.json)
@@ -3,9 +3,45 @@ import os
3
3
  from dataclasses import dataclass
4
4
  from datetime import datetime, timezone
5
5
  from pathlib import Path
6
- from typing import Dict, List, Optional
6
+ from typing import Any, Dict, List, Optional
7
7
 
8
8
  import py7zr
9
+ from py_app_dev.core.logging import logger
10
+ from py_app_dev.core.subprocess import SubprocessExecutor
11
+
12
+
13
+ @dataclass
14
+ class BuildMetadata:
15
+ """
16
+ Contains build metadata extracted from environment variables.
17
+
18
+ Attributes:
19
+ branch_name: The branch name, PR identifier (e.g., "PR-123"), or tag name
20
+ build_number: The build number or "local_build"
21
+ is_tag: Whether this is a tag build
22
+ pr_number: The PR number (without "PR-" prefix) for pull request builds, None otherwise
23
+ """
24
+
25
+ branch_name: str
26
+ build_number: str
27
+ is_tag: bool
28
+ pr_number: Optional[str]
29
+
30
+
31
+ @dataclass
32
+ class GitMetadata:
33
+ """
34
+ Contains git metadata extracted from environment variables or git commands.
35
+
36
+ Attributes:
37
+ commit_id: The git commit SHA (full hash)
38
+ commit_message: The git commit message subject line (first line only)
39
+ repository_url: The git repository URL (from remote.origin.url)
40
+ """
41
+
42
+ commit_id: Optional[str]
43
+ commit_message: Optional[str]
44
+ repository_url: Optional[str]
9
45
 
10
46
 
11
47
  class ArtifactsArchive:
@@ -84,7 +120,7 @@ class ArtifactsArchive:
84
120
  archive_path.unlink()
85
121
 
86
122
  if not self.archive_artifacts:
87
- print("Warning: No artifacts registered for archiving")
123
+ logger.warning("No artifacts registered for archiving")
88
124
  # Create empty 7z file
89
125
  with py7zr.SevenZipFile(archive_path, "w") as archive:
90
126
  pass
@@ -94,7 +130,7 @@ class ArtifactsArchive:
94
130
  with py7zr.SevenZipFile(archive_path, "w") as archive:
95
131
  for artifact in self.archive_artifacts:
96
132
  if not artifact.absolute_path.exists():
97
- print(f"Warning: Artifact {artifact.absolute_path} does not exist, skipping")
133
+ logger.warning(f"Artifact {artifact.absolute_path} does not exist, skipping")
98
134
  continue
99
135
 
100
136
  try:
@@ -104,13 +140,13 @@ class ArtifactsArchive:
104
140
  # py7zr can handle directories directly
105
141
  archive.writeall(artifact.absolute_path, arcname=str(artifact.archive_path))
106
142
  except Exception as file_error:
107
- print(f"Warning: Failed to add {artifact.absolute_path} to archive: {file_error}")
143
+ logger.warning(f"Failed to add {artifact.absolute_path} to archive: {file_error}")
108
144
  continue
109
145
 
110
- print(f"7z file created at: {archive_path}")
146
+ logger.info(f"7z file created at: {archive_path}")
111
147
  return archive_path
112
148
  except Exception as e:
113
- print(f"Error creating artifacts 7z file: {e}")
149
+ logger.error(f"Error creating artifacts 7z file: {e}")
114
150
  raise e
115
151
 
116
152
 
@@ -120,10 +156,10 @@ class ArtifactsArchiver:
120
156
  It provides a unified interface for registering artifacts to different archives.
121
157
  """
122
158
 
123
- def __init__(self) -> None:
159
+ def __init__(self, artifactory_base_url: Optional[str] = None) -> None:
124
160
  self.archives: Dict[str, ArtifactsArchive] = {}
125
161
  self._target_repos: Dict[str, str] = {}
126
- # self._artifacts_metadata: Dict[str, Dict[str, Dict[str, str]]] = {} # variant -> category -> artifacts
162
+ self.artifactory_base_url = artifactory_base_url
127
163
 
128
164
  def add_archive(self, out_dir: Path, archive_filename: str, target_repo: Optional[str] = None, archive_name: str = "default") -> ArtifactsArchive:
129
165
  """
@@ -190,7 +226,7 @@ class ArtifactsArchiver:
190
226
  The full Artifactory URL for the archive, or None if no target repo configured
191
227
 
192
228
  Example:
193
- "https://artifactory.marquardt.de/artifactory/spled-generic-snapshot-rietheim/develop/123/Disco.7z"
229
+ "https://artifactory.example.com/artifactory/my-repo/results/develop/123/result.7z"
194
230
  """
195
231
  if archive_name not in self.archives:
196
232
  return None
@@ -198,12 +234,15 @@ class ArtifactsArchiver:
198
234
  if archive_name not in self._target_repos:
199
235
  return None
200
236
 
237
+ if self.artifactory_base_url is None:
238
+ return None
239
+
201
240
  archive = self.archives[archive_name]
202
241
  target_repo = self._target_repos[archive_name]
203
- branch_name, build_number, _ = self._get_build_metadata()
242
+ metadata = self._get_build_metadata()
204
243
 
205
244
  # Construct the URL following the same pattern as create_rt_upload_json
206
- archive_url = f"https://artifactory.marquardt.de/artifactory/{target_repo}/{branch_name}/{build_number}/{archive.archive_name}"
245
+ archive_url = f"{self.artifactory_base_url}/{target_repo}/{metadata.branch_name}/{metadata.build_number}/{archive.archive_name}"
207
246
 
208
247
  return archive_url
209
248
 
@@ -245,7 +284,7 @@ class ArtifactsArchiver:
245
284
  return 28 # 4 weeks for PRs, feature branches, and other branches
246
285
 
247
286
  @staticmethod
248
- def _get_build_metadata() -> tuple[str, str, bool]:
287
+ def _get_build_metadata() -> BuildMetadata:
249
288
  """
250
289
  Get build metadata from environment variables or defaults.
251
290
 
@@ -253,14 +292,16 @@ class ArtifactsArchiver:
253
292
  to local development defaults.
254
293
 
255
294
  Returns:
256
- Tuple of (branch_name, build_number, is_tag):
257
- - branch_name: The branch or PR identifier
295
+ BuildMetadata instance containing:
296
+ - branch_name: The branch name, PR identifier (e.g., "PR-123"), or tag name
258
297
  - build_number: The build number or "local_build"
259
298
  - is_tag: Whether this is a tag build
299
+ - pr_number: The PR number (without "PR-" prefix) for pull requests, None otherwise
260
300
  """
261
301
  branch_name = "local_branch"
262
302
  build_number = "local_build"
263
303
  is_tag = False
304
+ pr_number = None
264
305
 
265
306
  if os.environ.get("JENKINS_URL"):
266
307
  change_id = os.environ.get("CHANGE_ID")
@@ -271,6 +312,7 @@ class ArtifactsArchiver:
271
312
  if change_id:
272
313
  # Pull request case
273
314
  branch_name = f"PR-{change_id}"
315
+ pr_number = change_id
274
316
  elif tag_name:
275
317
  # Tag build case
276
318
  branch_name = tag_name
@@ -282,7 +324,79 @@ class ArtifactsArchiver:
282
324
  if jenkins_build_number:
283
325
  build_number = jenkins_build_number
284
326
 
285
- return branch_name, build_number, is_tag
327
+ return BuildMetadata(
328
+ branch_name=branch_name,
329
+ build_number=build_number,
330
+ is_tag=is_tag,
331
+ pr_number=pr_number,
332
+ )
333
+
334
+ @staticmethod
335
+ def _get_git_metadata() -> GitMetadata:
336
+ """
337
+ Get git metadata from environment variables or git commands.
338
+
339
+ Attempts to retrieve git information in the following order:
340
+ 1. Environment variables (GIT_COMMIT, GIT_URL) - typically set by Jenkins Git plugin
341
+ 2. Git commands as fallback - executed locally using git CLI
342
+
343
+ The commit message captured is only the subject line (first line), not the full message.
344
+
345
+ Returns:
346
+ GitMetadata instance containing:
347
+ - commit_id: The git commit SHA, or None if unavailable
348
+ - commit_message: The commit subject line (first line only), or None if unavailable
349
+ - repository_url: The git repository URL, or None if unavailable
350
+ """
351
+ commit_id = None
352
+ commit_message = None
353
+ repository_url = None
354
+
355
+ # Try environment variables first (Jenkins Git plugin)
356
+ env_commit = os.environ.get("GIT_COMMIT")
357
+ env_url = os.environ.get("GIT_URL")
358
+
359
+ if env_commit:
360
+ commit_id = env_commit if env_commit.strip() else None
361
+ if env_url:
362
+ repository_url = env_url if env_url.strip() else None
363
+
364
+ # Fallback to git commands if environment variables not available
365
+ # Get commit ID
366
+ if not commit_id:
367
+ try:
368
+ result = SubprocessExecutor(["git", "rev-parse", "HEAD"]).execute(handle_errors=False)
369
+ if result and result.returncode == 0:
370
+ value = result.stdout.strip()
371
+ commit_id = value if value else None
372
+ except Exception as e:
373
+ logger.warning(f"Failed to get commit ID from git: {e}")
374
+
375
+ # Get commit message (subject line only)
376
+ if not commit_message:
377
+ try:
378
+ result = SubprocessExecutor(["git", "log", "-1", "--format=%s"]).execute(handle_errors=False)
379
+ if result and result.returncode == 0:
380
+ value = result.stdout.strip()
381
+ commit_message = value if value else None
382
+ except Exception as e:
383
+ logger.warning(f"Failed to get commit message from git: {e}")
384
+
385
+ # Get repository URL
386
+ if not repository_url:
387
+ try:
388
+ result = SubprocessExecutor(["git", "config", "--get", "remote.origin.url"]).execute(handle_errors=False)
389
+ if result and result.returncode == 0:
390
+ value = result.stdout.strip()
391
+ repository_url = value if value else None
392
+ except Exception as e:
393
+ logger.warning(f"Failed to get repository URL from git: {e}")
394
+
395
+ return GitMetadata(
396
+ commit_id=commit_id,
397
+ commit_message=commit_message,
398
+ repository_url=repository_url,
399
+ )
286
400
 
287
401
  def create_rt_upload_json(self, out_dir: Path) -> Path:
288
402
  """
@@ -299,10 +413,10 @@ class ArtifactsArchiver:
299
413
  Path to the created rt-upload.json file
300
414
  """
301
415
  # Get build metadata from environment or defaults
302
- branch_name, build_number, is_tag = self._get_build_metadata()
416
+ metadata = self._get_build_metadata()
303
417
 
304
418
  # Calculate retention period based on branch/tag
305
- retention_period = self.calculate_retention_period(branch_name, is_tag)
419
+ retention_period = self.calculate_retention_period(metadata.branch_name, metadata.is_tag)
306
420
 
307
421
  # Create the files array for Artifactory upload format
308
422
  files_array = []
@@ -312,7 +426,7 @@ class ArtifactsArchiver:
312
426
  target_repo = self._target_repos[archive_name]
313
427
 
314
428
  # Construct the RT target path
315
- rt_target = f"{target_repo}/{branch_name}/{build_number}/"
429
+ rt_target = f"{target_repo}/{metadata.branch_name}/{metadata.build_number}/"
316
430
 
317
431
  # Add this archive to the files array with retention_period property
318
432
  files_array.append(
@@ -344,6 +458,17 @@ class ArtifactsArchiver:
344
458
  but no artifacts. Use update_artifacts_json() to add artifact categories.
345
459
  It uses Jenkins environment variables when available, otherwise falls back to default values.
346
460
 
461
+ The JSON file includes conditional keys based on the build type:
462
+ - For pull requests: includes "pull_request" key with the PR number (e.g., "117")
463
+ - For tag builds: includes "tag" key with the tag name (e.g., "v1.2.3")
464
+ - For regular branch builds: includes "branch" key with the branch name (e.g., "develop")
465
+
466
+ Optional fields (included only if available):
467
+ - build_url: Jenkins build URL from BUILD_URL environment variable
468
+ - commit_id: Git commit SHA from GIT_COMMIT env var or git rev-parse HEAD
469
+ - commit_message: Git commit subject line from git log (first line only)
470
+ - repository_url: Git repository URL from GIT_URL env var or git config
471
+
347
472
  Args:
348
473
  variant: The variant name (e.g., "Disco")
349
474
  out_dir: Directory where the artifacts.json file will be created
@@ -358,11 +483,43 @@ class ArtifactsArchiver:
358
483
  if not variant or not variant.strip():
359
484
  raise ValueError("Variant name cannot be empty or None")
360
485
 
361
- # Get build metadata from environment or defaults
362
- branch_name, build_number, _ = self._get_build_metadata()
486
+ # Get metadata from environment or defaults
487
+ build_metadata = self._get_build_metadata()
488
+ git_metadata = self._get_git_metadata()
489
+
490
+ # Create the initial artifacts.json structure with base metadata
491
+ artifacts_data: Dict[str, Any] = {
492
+ "variant": variant,
493
+ "build_timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z",
494
+ "build_number": build_metadata.build_number,
495
+ }
496
+
497
+ # Add build_url if available
498
+ build_url = os.environ.get("BUILD_URL")
499
+ if build_url:
500
+ artifacts_data["build_url"] = build_url
501
+
502
+ # Add conditional keys based on build type
503
+ if build_metadata.pr_number:
504
+ # Pull request build
505
+ artifacts_data["pull_request"] = build_metadata.pr_number
506
+ elif build_metadata.is_tag:
507
+ # Tag build
508
+ artifacts_data["tag"] = build_metadata.branch_name
509
+ else:
510
+ # Regular branch build (or local build)
511
+ artifacts_data["branch"] = build_metadata.branch_name
512
+
513
+ # Add git metadata if available
514
+ if git_metadata.commit_id:
515
+ artifacts_data["commit_id"] = git_metadata.commit_id
516
+ if git_metadata.commit_message:
517
+ artifacts_data["commit_message"] = git_metadata.commit_message
518
+ if git_metadata.repository_url:
519
+ artifacts_data["repository_url"] = git_metadata.repository_url
363
520
 
364
- # Create the initial artifacts.json structure
365
- artifacts_data = {"variant": variant, "build_timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z", "build_number": build_number, "branch_name": branch_name, "artifacts": {}}
521
+ # Add empty artifacts dictionary
522
+ artifacts_data["artifacts"] = {}
366
523
 
367
524
  # Create the artifacts.json file
368
525
  json_path = out_dir / "artifacts.json"
@@ -487,6 +644,10 @@ class ArtifactsArchiver:
487
644
  # out_dir = Path("./build/output")
488
645
  #
489
646
  # # Create initial artifacts.json file first, then add categories
647
+ # # The resulting JSON will contain conditional keys based on build type:
648
+ # # - For PRs: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "pull_request": "117", "artifacts": {}}
649
+ # # - For tags: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "tag": "v1.2.3", "artifacts": {}}
650
+ # # - For branches: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "branch": "develop", "artifacts": {}}
490
651
  # artifacts_json_path = archiver.create_artifacts_json(variant, out_dir)
491
652
  # archiver.update_artifacts_json("test_reports", test_reports, artifacts_json_path)
492
653
  # archiver.update_artifacts_json("sca_reports", sca_reports, artifacts_json_path)
@@ -0,0 +1,122 @@
1
+ """
2
+ JUnit XML Merger
3
+
4
+ Merges multiple JUnit XML files into a single variant-level JUnit XML file.
5
+ This enables CI/CD tooling to consume aggregated test results from all components.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import List
12
+
13
+ from junitparser import JUnitXml
14
+
15
+
16
+ class JUnitMergerError(Exception):
17
+ """Exception raised when JUnit XML merging fails."""
18
+
19
+ pass
20
+
21
+
22
+ class JUnitMerger:
23
+ """
24
+ Merges multiple JUnit XML files into a single variant-level JUnit XML file.
25
+
26
+ This class provides functionality to aggregate test results from multiple
27
+ components into a unified JUnit XML file for CI/CD tooling.
28
+ """
29
+
30
+ def __init__(self, input_files: List[str], output_file: str):
31
+ """
32
+ Initialize the JUnit merger.
33
+
34
+ Args:
35
+ input_files: List of paths to input JUnit XML files
36
+ output_file: Path to output merged JUnit XML file
37
+
38
+ Raises:
39
+ ValueError: If input_files list is empty
40
+ """
41
+ if not input_files:
42
+ raise ValueError("No input files provided for merging")
43
+
44
+ self.input_files = input_files
45
+ self.output_file = output_file
46
+
47
+ def merge(self) -> None:
48
+ """
49
+ Merge all input JUnit XML files into the output file.
50
+
51
+ Raises:
52
+ FileNotFoundError: If any input file doesn't exist
53
+ Exception: If any input file contains malformed XML or validation fails
54
+ """
55
+ # Create merged JUnit XML container
56
+ merged_xml = JUnitXml()
57
+
58
+ # Process each input file
59
+ for input_path in self.input_files:
60
+ input_file = Path(input_path)
61
+
62
+ if not input_file.exists():
63
+ raise FileNotFoundError(f"Input file not found: {input_path}")
64
+
65
+ try:
66
+ # Parse the input JUnit XML file
67
+ xml = JUnitXml.fromfile(str(input_file))
68
+
69
+ # Add all test suites from this file to the merged result
70
+ for suite in xml:
71
+ merged_xml.add_testsuite(suite)
72
+
73
+ except Exception as e:
74
+ raise JUnitMergerError(f"Failed to parse JUnit XML file '{input_path}': {e}") from e
75
+
76
+ # Write merged result to output file
77
+ output_path = Path(self.output_file)
78
+ output_path.parent.mkdir(parents=True, exist_ok=True)
79
+ merged_xml.write(str(output_path))
80
+
81
+ # Validate the output
82
+ self._validate_output()
83
+
84
+ def _validate_output(self) -> None:
85
+ """
86
+ Validate that the merged output file is valid JUnit XML.
87
+
88
+ Raises:
89
+ Exception: If the output file cannot be parsed as valid JUnit XML
90
+ """
91
+ try:
92
+ JUnitXml.fromfile(str(self.output_file))
93
+ except Exception as e:
94
+ raise JUnitMergerError(f"Validation failed: merged output is not valid JUnit XML: {e}") from e
95
+
96
+
97
+ def main() -> int:
98
+ """
99
+ Main entry point for command-line interface.
100
+
101
+ Returns:
102
+ Exit code (0 for success, non-zero for failure)
103
+ """
104
+ parser = argparse.ArgumentParser(description="Merge multiple JUnit XML files into a single variant-level file")
105
+ parser.add_argument("--output", required=True, help="Path to output merged JUnit XML file")
106
+ parser.add_argument("--inputs", nargs="+", required=True, help="Paths to input JUnit XML files to merge")
107
+
108
+ args = parser.parse_args()
109
+
110
+ try:
111
+ merger = JUnitMerger(args.inputs, args.output)
112
+ merger.merge()
113
+ print(f"Successfully merged {len(args.inputs)} JUnit XML file(s) into {args.output}")
114
+ return 0
115
+
116
+ except Exception as e:
117
+ print(f"ERROR: Failed to merge JUnit XML files: {e}", file=sys.stderr)
118
+ return 1
119
+
120
+
121
+ if __name__ == "__main__":
122
+ sys.exit(main())
@@ -1 +0,0 @@
1
- __version__ = "7.14.0-rc4.dev.3"
File without changes