diff --git a/.gitlab/ci/common.yml b/.gitlab/ci/common.yml
index 8b2717d0422..de04a236d7e 100644
--- a/.gitlab/ci/common.yml
+++ b/.gitlab/ci/common.yml
@@ -60,9 +60,6 @@ variables:
TARGET_TEST_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.5-target-test:1"
SONARQUBE_SCANNER_IMAGE: "${CI_DOCKER_REGISTRY}/sonarqube-scanner:5"
- # cache python dependencies
- PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
-
# Set this variable to the branch of idf-constraints repo in order to test a custom Python constraint file. The
# branch name must be without the remote part ("origin/"). Keep the variable empty in order to use the constraint
# file from https://dl.espressif.com/dl/esp-idf.
@@ -334,12 +331,6 @@ default:
cache:
# pull only for most of the use cases since it's cache dir.
# Only set "push" policy for "upload_cache" stage jobs
- - key: pip-cache-${LATEST_GIT_TAG}
- fallback_keys:
- - pip-cache
- paths:
- - .cache/pip
- policy: pull
- key: submodule-cache-${LATEST_GIT_TAG}
fallback_keys:
- submodule-cache
diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml
index 7e7044c88b2..3cd3a96ade2 100644
--- a/.gitlab/ci/host-test.yml
+++ b/.gitlab/ci/host-test.yml
@@ -7,9 +7,6 @@
# run host_test jobs immediately, only after upload cache
needs:
- pipeline_variables
- - job: upload-pip-cache
- optional: true
- artifacts: false
- job: upload-submodules-cache
optional: true
artifacts: false
diff --git a/.gitlab/ci/post_deploy.yml b/.gitlab/ci/post_deploy.yml
index c17e94089f7..6b8e91bfcf5 100644
--- a/.gitlab/ci/post_deploy.yml
+++ b/.gitlab/ci/post_deploy.yml
@@ -2,22 +2,6 @@
stage: post_deploy
image: $ESP_ENV_IMAGE
-generate_failed_jobs_report:
- extends:
- - .post_deploy_template
- tags: [build, shiny]
- when: always
- dependencies: # Do not download artifacts from the previous stages
- needs:
- - pipeline_variables
- artifacts:
- expire_in: 2 week
- when: always
- paths:
- - job_report.html
- script:
- - python tools/ci/dynamic_pipelines/scripts/generate_report.py --report-type job
-
sync_support_status:
extends:
- .post_deploy_template
diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml
index e1a2fc88470..e2e95909e4d 100644
--- a/.gitlab/ci/rules.yml
+++ b/.gitlab/ci/rules.yml
@@ -13,9 +13,6 @@
- "components/**/Kconfig"
- "components/**/CMakeLists.txt"
-.patterns-python-cache: &patterns-python-cache
- - "tools/requirements.json"
- - "tools/requirements/requirements.*.txt"
.patterns-python-files: &patterns-python-files
- ".gitlab/ci/static-code-analysis.yml"
@@ -227,15 +224,6 @@
rules:
- <<: *if-dev-push
-.rules:upload-python-cache:
- rules:
- - <<: *if-release-tag
- - <<: *if-schedule-nightly
- - <<: *if-protected-branch-push
- changes: *patterns-python-cache
- - <<: *if-label-upload_cache
- when: manual
-
.rules:upload-submodule-cache:
rules:
- <<: *if-release-tag
diff --git a/.gitlab/ci/test-win.yml b/.gitlab/ci/test-win.yml
index f592716cfd9..f8b6d8c6b48 100644
--- a/.gitlab/ci/test-win.yml
+++ b/.gitlab/ci/test-win.yml
@@ -6,9 +6,6 @@
dependencies: # set dependencies to null to avoid missing artifacts issue
# run host_test jobs immediately, only after upload cache
needs:
- - job: upload-pip-cache
- optional: true
- artifacts: false
- job: upload-submodules-cache
optional: true
artifacts: false
diff --git a/.gitlab/ci/upload_cache.yml b/.gitlab/ci/upload_cache.yml
index fdd7e799cc1..320c0cd4244 100644
--- a/.gitlab/ci/upload_cache.yml
+++ b/.gitlab/ci/upload_cache.yml
@@ -6,26 +6,6 @@
stage: upload_cache
image: $ESP_ENV_IMAGE
-upload-pip-cache:
- extends:
- - .upload_cache_template
- - .before_script:minimal
- - .rules:upload-python-cache
- tags:
- - $GEO
- - cache
- cache:
- key: pip-cache-${LATEST_GIT_TAG}
- paths:
- - .cache/pip
- policy: push
- script:
- - rm -rf .cache/pip # clear old packages
- - bash install.sh --enable-ci --enable-test-specific
- parallel:
- matrix:
- - GEO: [ 'shiny', 'brew' ]
-
upload-submodules-cache:
extends:
- .upload_cache_template
diff --git a/tools/ci/dynamic_pipelines/constants.py b/tools/ci/dynamic_pipelines/constants.py
index 40dbe346d11..2f64e515e73 100644
--- a/tools/ci/dynamic_pipelines/constants.py
+++ b/tools/ci/dynamic_pipelines/constants.py
@@ -1,26 +1,9 @@
-# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
+# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
from idf_ci_utils import IDF_PATH
-COMMENT_START_MARKER = '### Dynamic Pipeline Report'
-
-REPORT_TEMPLATE_FILEPATH = os.path.join(
- IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'report.template.html'
-)
-CSS_STYLES_FILEPATH = os.path.join(IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'styles.css')
-JS_SCRIPTS_FILEPATH = os.path.join(IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'scripts.js')
-TOP_N_APPS_BY_SIZE_DIFF = 10
-SIZE_DIFFERENCE_BYTES_THRESHOLD = 500
-BINARY_SIZE_METRIC_NAME = 'binary_size'
-
KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH = os.path.join(
IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'known_generate_test_child_pipeline_warnings.yml'
)
-
-CI_JOB_TOKEN = os.getenv('CI_JOB_TOKEN', '')
-CI_DASHBOARD_API = os.getenv('CI_DASHBOARD_API', '')
-CI_PAGES_URL = os.getenv('CI_PAGES_URL', '')
-CI_PROJECT_URL = os.getenv('CI_PROJECT_URL', '')
-CI_MERGE_REQUEST_SOURCE_BRANCH_SHA = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')
diff --git a/tools/ci/dynamic_pipelines/models.py b/tools/ci/dynamic_pipelines/models.py
deleted file mode 100644
index 2cd5022a112..00000000000
--- a/tools/ci/dynamic_pipelines/models.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
-# SPDX-License-Identifier: Apache-2.0
-import os
-import typing as t
-import urllib.parse
-from dataclasses import dataclass
-from xml.etree.ElementTree import Element
-
-from idf_ci_utils import IDF_PATH
-
-
-@dataclass
-class TestCase:
- name: str
- file: str
- time: float
- app_path: t.Optional[str] = None
- failure: t.Optional[str] = None
- skipped: t.Optional[str] = None
- ci_job_url: t.Optional[str] = None
- ci_dashboard_url: t.Optional[str] = None
- dut_log_url: t.Optional[str] = None
- latest_total_count: int = 0
- latest_failed_count: int = 0
-
- @property
- def is_failure(self) -> bool:
- return self.failure is not None
-
- @property
- def is_skipped(self) -> bool:
- return self.skipped is not None
-
- @property
- def is_success(self) -> bool:
- return not self.is_failure and not self.is_skipped
-
- @classmethod
- def _get_idf_rel_path(cls, path: str) -> str:
- if path.startswith(IDF_PATH):
- return os.path.relpath(path, IDF_PATH)
- else:
- return path
-
- @classmethod
- def from_test_case_node(cls, node: Element) -> t.Optional['TestCase']:
- if 'name' not in node.attrib:
- print('WARNING: Node Invalid: ', node)
- return None
-
- # url to test cases dashboard
- grafana_base_url = urllib.parse.urljoin(os.getenv('CI_DASHBOARD_HOST', ''), '/d/Ucg477Fnz/case-list')
- encoded_params = urllib.parse.urlencode({'var-case_id': node.attrib['name']}, quote_via=urllib.parse.quote)
-
- kwargs = {
- 'name': node.attrib['name'],
- 'file': node.attrib.get('file'),
- 'app_path': '|'.join(
- cls._get_idf_rel_path(path) for path in node.attrib.get('app_path', 'unknown').split('|')
- ),
- 'time': float(node.attrib.get('time') or 0),
- 'ci_job_url': node.attrib.get('ci_job_url') or 'Not found',
- 'ci_dashboard_url': f'{grafana_base_url}?{encoded_params}',
- 'dut_log_url': node.attrib.get('dut_log_url') or 'Not found',
- }
-
- failure_node = node.find('failure')
- # bool(failure_node) is False, so compare with None
- if failure_node is None:
- failure_node = node.find('error')
- if failure_node is not None:
- message = failure_node.attrib.get('message', '')
- kwargs['failure'] = message
-
- skipped_node = node.find('skipped')
- if skipped_node is not None:
- kwargs['skipped'] = skipped_node.attrib['message']
-
- return cls(**kwargs) # type: ignore
-
-
-@dataclass
-class GitlabJob:
- id: int
- name: str
- stage: str
- status: str
- url: str
- ci_dashboard_url: str
- failure_reason: t.Optional[str] = None
- failure_log: t.Optional[str] = None
- latest_total_count: int = 0
- latest_failed_count: int = 0
-
- @property
- def is_failed(self) -> bool:
- return self.status == 'failed'
-
- @property
- def is_success(self) -> bool:
- return self.status == 'success'
-
- @classmethod
- def from_json_data(cls, job_data: dict, failure_data: dict) -> t.Optional['GitlabJob']:
- grafana_base_url = urllib.parse.urljoin(os.getenv('CI_DASHBOARD_HOST', ''), '/d/LoUa-qLWz/job-list')
- encoded_params = urllib.parse.urlencode({'var-job_name': job_data['name']}, quote_via=urllib.parse.quote)
-
- kwargs = {
- 'id': job_data['id'],
- 'name': job_data['name'],
- 'stage': job_data['stage'],
- 'status': job_data['status'],
- 'url': job_data['url'],
- 'ci_dashboard_url': f'{grafana_base_url}?{encoded_params}',
- 'failure_reason': job_data['failure_reason'],
- 'failure_log': job_data['failure_log'],
- 'latest_total_count': failure_data.get('total_count', 0),
- 'latest_failed_count': failure_data.get('failed_count', 0),
- }
-
- return cls(**kwargs) # type: ignore
diff --git a/tools/ci/dynamic_pipelines/report.py b/tools/ci/dynamic_pipelines/report.py
deleted file mode 100644
index d9918bc89f2..00000000000
--- a/tools/ci/dynamic_pipelines/report.py
+++ /dev/null
@@ -1,739 +0,0 @@
-# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
-# SPDX-License-Identifier: Apache-2.0
-import abc
-import html
-import re
-import typing as t
-from textwrap import dedent
-
-from gitlab import GitlabUpdateError
-from gitlab_api import Gitlab
-from idf_build_apps.constants import BuildStatus
-from idf_ci_local.app import AppWithMetricsInfo
-from idf_ci_utils import idf_relpath
-from prettytable import PrettyTable
-
-from .constants import BINARY_SIZE_METRIC_NAME
-from .constants import CI_DASHBOARD_API
-from .constants import COMMENT_START_MARKER
-from .constants import CSS_STYLES_FILEPATH
-from .constants import JS_SCRIPTS_FILEPATH
-from .constants import REPORT_TEMPLATE_FILEPATH
-from .constants import SIZE_DIFFERENCE_BYTES_THRESHOLD
-from .constants import TOP_N_APPS_BY_SIZE_DIFF
-from .models import GitlabJob
-from .models import TestCase
-from .utils import format_permalink
-from .utils import get_artifacts_url
-from .utils import is_url
-
-
-class ReportGenerator:
- REGEX_PATTERN = r'#### {}\n[\s\S]*?(?=\n#### |$)'
-
- def __init__(
- self,
- project_id: int,
- mr_iid: int,
- pipeline_id: int,
- job_id: int,
- commit_id: str,
- local_commit_id: str,
- *,
- title: str,
- ):
- gl_project = Gitlab(project_id).project
- if mr_iid is not None:
- self.mr = gl_project.mergerequests.get(mr_iid)
- else:
- self.mr = None
- self.pipeline_id = pipeline_id
- self.job_id = job_id
- self.commit_id = commit_id
- self.local_commit_id = local_commit_id
-
- self.title = title
- self.output_filepath = self.title.lower().replace(' ', '_') + '.html'
- self.additional_info = ''
-
- @property
- def get_commit_summary(self) -> str:
- return f'with CI commit SHA: {self.commit_id[:8]}, local commit SHA: {self.local_commit_id[:8]}'
-
- @staticmethod
- def get_download_link_for_url(url: str) -> str:
- if url:
- return f'Download'
-
- return ''
-
- @staticmethod
- def write_report_to_file(report_str: str, job_id: int, output_filepath: str) -> t.Optional[str]:
- """
- Writes the report to a file and constructs a modified URL based on environment settings.
-
- :param report_str: The report content to be written to the file.
- :param job_id: The job identifier used to construct the URL.
- :param output_filepath: The path to the output file.
- :return: The modified URL pointing to the job's artifacts.
- """
- if not report_str:
- return None
- with open(output_filepath, 'w') as file:
- file.write(report_str)
-
- # for example, {URL}/-/esp-idf/-/jobs/{id}/artifacts/app_info_84.txt
- # CI_PAGES_URL is {URL}/esp-idf, which missed one `-`
- report_url: str = get_artifacts_url(job_id, output_filepath)
- return report_url
-
- @staticmethod
- def _load_file_content(filepath: str) -> str:
- """
- Load the content of a file as string
-
- :param filepath: Path to the file to load
- :return: Content of the file as string
- """
- try:
- with open(filepath, encoding='utf-8') as f:
- return f.read()
- except (OSError, FileNotFoundError) as e:
- print(f'Warning: Could not read file {filepath}: {e}')
- return ''
-
- def generate_html_report(self, table_str: str) -> str:
- # we're using bootstrap table
- table_str = table_str.replace(
- '
',
- '
',
- )
-
- template = self._load_file_content(REPORT_TEMPLATE_FILEPATH)
- css_content = self._load_file_content(CSS_STYLES_FILEPATH)
- js_content = self._load_file_content(JS_SCRIPTS_FILEPATH)
-
- template = template.replace('{{css_content}}', css_content)
- template = template.replace('{{js_content}}', js_content)
- template = template.replace('{{pipeline_id}}', str(self.pipeline_id))
- template = template.replace('{{apiBaseUrl}}', CI_DASHBOARD_API)
-
- return template.replace('{{title}}', self.title).replace('{{table}}', table_str)
-
- @staticmethod
- def table_to_html_str(table: PrettyTable) -> str:
- return html.unescape(table.get_html_string()) # type: ignore
-
- def create_table_section(
- self,
- title: str,
- items: list,
- headers: list,
- row_attrs: list,
- value_functions: t.Optional[list] = None,
- ) -> t.List:
- """
- Appends a formatted section to a report based on the provided items. This section includes
- a header and a table constructed from the items list with specified headers and attributes.
-
- :param title: Title for the report section. This title is used as a header above the table.
- :param items: List of item objects to include in the table. Each item should have attributes
- that correspond to the row_attrs and value_functions specified.
- :param headers: List of strings that will serve as the column headers in the generated table.
- :param row_attrs: List of attributes to include from each item for the table rows. These
- should be attributes or keys that exist on the items in the 'items' list.
- :param value_functions: Optional list of tuples containing additional header and corresponding
- value function. Each tuple should specify a header (as a string) and
- a function that takes an item and returns a string. This is used for
- generating dynamic columns based on item data.
-
- :return: List with appended HTML sections.
- """
- if not items:
- return []
-
- report_sections = [
- f"""
{title}
""",
- self._create_table_for_items(
- items=items, headers=headers, row_attrs=row_attrs, value_functions=value_functions or []
- ),
- ]
- return report_sections
-
- @staticmethod
- def generate_additional_info_section(
- title: str, count: int, report_url: t.Optional[str] = None, add_permalink: bool = True
- ) -> str:
- """
- Generate a section for the additional info string.
-
- :param title: The title of the section.
- :param count: The count of test cases.
- :param report_url: The URL of the report. If count = 0, only the count will be included.
- :param add_permalink: Whether to include a permalink in the report URL. Defaults to True.
- :return: The formatted additional info section string.
- """
- if count != 0 and report_url:
- if add_permalink:
- return f'- **{title}:** [{count}]({report_url}/#{format_permalink(title)})\n'
- else:
- return f'- **{title}:** [{count}]({report_url})\n'
- else:
- return f'- **{title}:** {count}\n'
-
- def _create_table_for_items(
- self,
- items: t.Union[t.List[TestCase], t.List[GitlabJob]],
- headers: t.List[str],
- row_attrs: t.List[str],
- value_functions: t.Optional[t.List[t.Tuple[str, t.Callable[[t.Union[TestCase, GitlabJob]], str]]]] = None,
- ) -> str:
- """
- Create a PrettyTable and convert it to an HTML string for the provided test cases.
- :param items: List of item objects to include in the table.
- :param headers: List of strings for the table headers.
- :param row_attrs: List of attributes to include in each row.
- :param value_functions: List of tuples containing additional header and corresponding value function.
- :return: HTML table string.
- """
- table = PrettyTable()
- table.field_names = headers
-
- # Create a mapping of header names to their corresponding index in the headers list
- header_index_map = {header: i for i, header in enumerate(headers)}
-
- for item in items:
- row = []
- for attr in row_attrs:
- value = str(getattr(item, attr, ''))
- if is_url(value):
- link = f'link'
- row.append(link)
- else:
- row.append(value)
-
- # Insert values computed by value functions at the correct column position based on their headers
- if value_functions:
- for header, func in value_functions:
- index = header_index_map.get(header)
- if index is not None:
- computed_value = func(item)
- row.insert(index, computed_value)
-
- table.add_row(row)
-
- return self.table_to_html_str(table)
-
- @staticmethod
- def _filter_items(
- items: t.Union[t.List[TestCase], t.List[GitlabJob]], condition: t.Callable[[t.Union[TestCase, GitlabJob]], bool]
- ) -> t.List[TestCase]:
- """
- Filter items s based on a given condition.
-
- :param items: List of items to filter by given condition.
- :param condition: A function that evaluates to True or False for each items.
- :return: List of filtered instances.
- """
- return [item for item in items if condition(item)]
-
- @staticmethod
- def _sort_items(
- items: t.List[t.Union[TestCase, GitlabJob, AppWithMetricsInfo]],
- key: t.Union[str, t.Callable[[t.Union[TestCase, GitlabJob, AppWithMetricsInfo]], t.Any]],
- order: str = 'asc',
- sort_function: t.Optional[t.Callable[[t.Any], t.Any]] = None,
- ) -> t.List[t.Union[TestCase, GitlabJob, AppWithMetricsInfo]]:
- """
- Sort items based on a given key, order, and optional custom sorting function.
-
- :param items: List of items to sort.
- :param key: A string representing the attribute name or a function to extract the sorting key.
- :param order: Order of sorting ('asc' for ascending, 'desc' for descending).
- :param sort_function: A custom function to control sorting logic
- (e.g., prioritizing positive/negative/zero values).
- :return: List of sorted instances.
- """
- key_func = None
- if isinstance(key, str):
-
- def key_func(item: t.Any) -> t.Any:
- return getattr(item, key)
-
- sorting_key = sort_function if sort_function is not None else key_func
- try:
- items = sorted(items, key=sorting_key, reverse=(order == 'desc'))
- except TypeError:
- print(f'Comparison for the key {key} is not supported')
-
- return items
-
- @abc.abstractmethod
- def _get_report_str(self) -> str:
- raise NotImplementedError
-
- def _generate_comment(self) -> str:
- # Report in HTML format to avoid exceeding length limits
- comment = f'#### {self.title}\n'
- report_str = self._get_report_str()
- comment += f'{self.additional_info}\n'
- self.write_report_to_file(report_str, self.job_id, self.output_filepath)
-
- return comment
-
- def _update_mr_comment(self, comment: str) -> None:
- new_comment = f'{COMMENT_START_MARKER}\n\n{comment}'
-
- for note in self.mr.notes.list(iterator=True):
- if note.body.startswith(COMMENT_START_MARKER):
- updated_str = self._get_updated_comment(note.body, comment)
-
- note.body = updated_str
- try:
- note.save()
- except GitlabUpdateError:
- print('Failed to update MR comment, Creating a new comment')
- self.mr.notes.create({'body': new_comment})
- break
- else:
- self.mr.notes.create({'body': new_comment})
-
- def _get_updated_comment(self, existing_comment: str, new_comment: str) -> str:
- updated_str = re.sub(self.REGEX_PATTERN.format(self.title), new_comment, existing_comment)
- if updated_str == existing_comment:
- updated_str = f'{existing_comment.strip()}\n\n{new_comment}'
- return updated_str
-
- def post_report(self) -> None:
- comment = self._generate_comment()
-
- print(comment)
-
- if self.mr is None:
- print('No MR found, skip posting comment')
- return
-
- self._update_mr_comment(comment)
-
-
-class BuildReportGenerator(ReportGenerator):
- def __init__(
- self,
- project_id: int,
- mr_iid: int,
- pipeline_id: int,
- job_id: int,
- commit_id: str,
- local_commit_id: str,
- *,
- title: str = 'Build Report',
- apps: list[AppWithMetricsInfo],
- ) -> None:
- super().__init__(project_id, mr_iid, pipeline_id, job_id, commit_id, local_commit_id, title=title)
- self.apps = apps
- self.report_titles_map = {
- 'failed_apps': 'Failed Apps',
- 'built_test_related_apps': 'Built Apps - Test Related',
- 'built_non_test_related_apps': 'Built Apps - Non Test Related',
- 'new_test_related_apps': 'New Apps - Test Related',
- 'new_non_test_related_apps': 'New Apps - Non Test Related',
- 'skipped_apps': 'Skipped Apps',
- }
- self.failed_apps_report_file = 'failed_apps.html'
- self.built_apps_report_file = 'built_apps.html'
- self.skipped_apps_report_file = 'skipped_apps.html'
-
- @staticmethod
- def custom_sort(item: AppWithMetricsInfo) -> tuple[int, t.Any]:
- """
- Custom sort function to:
- 1. Push items with zero binary sizes to the end.
- 2. Sort other items by absolute size_difference_percentage.
- """
- # Priority: 0 for zero binaries, 1 for non-zero binaries
- zero_binary_priority = (
- 1
- if item.metrics[BINARY_SIZE_METRIC_NAME].source_value != 0
- or item.metrics[BINARY_SIZE_METRIC_NAME].target_value != 0
- else 0
- )
- # Secondary sort: Negative absolute size_difference_percentage for descending order
- size_difference_sort = abs(item.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage)
- return zero_binary_priority, size_difference_sort
-
- def _generate_top_n_apps_by_size_table(self) -> str:
- """
- Generate a markdown table for the top N apps by size difference.
- Only includes apps with size differences greater than 500 bytes.
- """
- filtered_apps = [
- app
- for app in self.apps
- if abs(app.metrics[BINARY_SIZE_METRIC_NAME].difference) > SIZE_DIFFERENCE_BYTES_THRESHOLD
- ]
-
- top_apps = sorted(
- filtered_apps, key=lambda app: abs(app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage), reverse=True
- )[:TOP_N_APPS_BY_SIZE_DIFF]
-
- if not top_apps:
- return ''
-
- table = (
- f'\n⚠️⚠️⚠️ Top {len(top_apps)} Apps with Binary Size Sorted by Size Difference\n'
- f'Note: Apps with changes of less than {SIZE_DIFFERENCE_BYTES_THRESHOLD} bytes are not shown.\n'
- )
- table += '| App Dir | Build Dir | Size Diff (bytes) | Size Diff (%) |\n'
- table += '|---------|-----------|-------------------|---------------|\n'
- for app in top_apps:
- table += dedent(
- f'| {app.app_dir} | {app.build_dir} | '
- f'{app.metrics[BINARY_SIZE_METRIC_NAME].difference} | '
- f'{app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage}% |\n'
- )
- table += (
- '\n**For more details, please click on the numbers in the summary above '
- 'to view the corresponding report files.** ⬆️⬆️⬆️\n\n'
- )
-
- return table
-
- @staticmethod
- def split_new_and_existing_apps(
- apps: t.Iterable[AppWithMetricsInfo],
- ) -> tuple[list[AppWithMetricsInfo], list[AppWithMetricsInfo]]:
- """
- Splits apps into new apps and existing apps.
-
- :param apps: Iterable of apps to process.
- :return: A tuple (new_apps, existing_apps).
- """
- new_apps = [app for app in apps if app.is_new_app]
- existing_apps = [app for app in apps if not app.is_new_app]
- return new_apps, existing_apps
-
- def filter_apps_by_criteria(self, build_status: str, preserve: bool) -> list[AppWithMetricsInfo]:
- """
- Filters apps based on build status and preserve criteria.
-
- :param build_status: Build status to filter by.
- :param preserve: Whether to filter preserved apps.
- :return: Filtered list of apps.
- """
- return [app for app in self.apps if app.build_status == build_status and app.preserve == preserve]
-
- def get_built_apps_report_parts(self) -> list[str]:
- """
- Generates report parts for new and existing apps.
-
- :return: List of report parts.
- """
- new_test_related_apps, built_test_related_apps = self.split_new_and_existing_apps(
- self.filter_apps_by_criteria(BuildStatus.SUCCESS, True)
- )
-
- new_non_test_related_apps, built_non_test_related_apps = self.split_new_and_existing_apps(
- self.filter_apps_by_criteria(BuildStatus.SUCCESS, False)
- )
-
- sections = []
-
- if new_test_related_apps:
- new_test_related_apps_table_section = self.create_table_section(
- title=self.report_titles_map['new_test_related_apps'],
- items=new_test_related_apps,
- headers=[
- 'App Dir',
- 'Build Dir',
- 'Download Command',
- 'Your Branch App Size',
- ],
- row_attrs=[
- 'app_dir',
- 'build_dir',
- ],
- value_functions=[
- ('Your Branch App Size', lambda _app: str(_app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
- (
- 'Download Command',
- lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
- f'{idf_relpath(_app.build_path)}',
- ),
- ],
- )
- sections.extend(new_test_related_apps_table_section)
-
- if built_test_related_apps:
- built_test_related_apps = self._sort_items(
- built_test_related_apps,
- key='metrics.binary_size.difference_percentage',
- order='desc',
- sort_function=self.custom_sort,
- )
-
- built_test_related_apps_table_section = self.create_table_section(
- title=self.report_titles_map['built_test_related_apps'],
- items=built_test_related_apps,
- headers=[
- 'App Dir',
- 'Build Dir',
- 'Download Command',
- 'Your Branch App Size',
- 'Target Branch App Size',
- 'Size Diff',
- 'Size Diff, %',
- ],
- row_attrs=[
- 'app_dir',
- 'build_dir',
- ],
- value_functions=[
- ('Your Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
- ('Target Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].target_value)),
- ('Size Diff', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference)),
- ('Size Diff, %', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage)),
- (
- 'Download Command',
- lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
- f'{idf_relpath(_app.build_path)}',
- ),
- ],
- )
- sections.extend(built_test_related_apps_table_section)
-
- if new_non_test_related_apps:
- new_non_test_related_apps_table_section = self.create_table_section(
- title=self.report_titles_map['new_non_test_related_apps'],
- items=new_non_test_related_apps,
- headers=[
- 'App Dir',
- 'Build Dir',
- 'Download Command',
- 'Your Branch App Size',
- ],
- row_attrs=[
- 'app_dir',
- 'build_dir',
- ],
- value_functions=[
- (
- 'Download Command',
- lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
- f'{idf_relpath(_app.build_path)}',
- ),
- ('Your Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
- ],
- )
- sections.extend(new_non_test_related_apps_table_section)
-
- if built_non_test_related_apps:
- built_non_test_related_apps = self._sort_items(
- built_non_test_related_apps,
- key='metrics.binary_size.difference_percentage',
- order='desc',
- sort_function=self.custom_sort,
- )
- built_non_test_related_apps_table_section = self.create_table_section(
- title=self.report_titles_map['built_non_test_related_apps'],
- items=built_non_test_related_apps,
- headers=[
- 'App Dir',
- 'Build Dir',
- 'Download Command',
- 'Your Branch App Size',
- 'Target Branch App Size',
- 'Size Diff',
- 'Size Diff, %',
- ],
- row_attrs=[
- 'app_dir',
- 'build_dir',
- ],
- value_functions=[
- ('Your Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
- ('Target Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].target_value)),
- ('Size Diff', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference)),
- ('Size Diff, %', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage)),
- (
- 'Download Command',
- lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
- f'{idf_relpath(_app.build_path)}',
- ),
- ],
- )
- sections.extend(built_non_test_related_apps_table_section)
-
- built_apps_report_url = self.write_report_to_file(
- self.generate_html_report(''.join(sections)),
- self.job_id,
- self.built_apps_report_file,
- )
-
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['built_test_related_apps'],
- len(built_test_related_apps),
- built_apps_report_url,
- )
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['built_non_test_related_apps'],
- len(built_non_test_related_apps),
- built_apps_report_url,
- )
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['new_test_related_apps'],
- len(new_test_related_apps),
- built_apps_report_url,
- )
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['new_non_test_related_apps'],
- len(new_non_test_related_apps),
- built_apps_report_url,
- )
-
- self.additional_info += self._generate_top_n_apps_by_size_table()
-
- return sections
-
- def get_failed_apps_report_parts(self) -> list[str]:
- failed_apps = [app for app in self.apps if app.build_status == BuildStatus.FAILED]
- if not failed_apps:
- return []
-
- failed_apps_table_section = self.create_table_section(
- title=self.report_titles_map['failed_apps'],
- items=failed_apps,
- headers=['App Dir', 'Build Dir', 'Failed Reason', 'Download Command'],
- row_attrs=['app_dir', 'build_dir', 'build_comment'],
- value_functions=[
- (
- 'Download Command',
- lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
- f'{idf_relpath(_app.build_path)}',
- ),
- ],
- )
- failed_apps_report_url = self.write_report_to_file(
- self.generate_html_report(''.join(failed_apps_table_section)),
- self.job_id,
- self.failed_apps_report_file,
- )
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['failed_apps'], len(failed_apps), failed_apps_report_url
- )
- return failed_apps_table_section
-
- def get_skipped_apps_report_parts(self) -> list[str]:
- skipped_apps = [app for app in self.apps if app.build_status == BuildStatus.SKIPPED]
- if not skipped_apps:
- return []
-
- skipped_apps_table_section = self.create_table_section(
- title=self.report_titles_map['skipped_apps'],
- items=skipped_apps,
- headers=['App Dir', 'Build Dir', 'Skipped Reason'],
- row_attrs=['app_dir', 'build_dir', 'build_comment'],
- )
- skipped_apps_report_url = self.write_report_to_file(
- self.generate_html_report(''.join(skipped_apps_table_section)),
- self.job_id,
- self.skipped_apps_report_file,
- )
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['skipped_apps'], len(skipped_apps), skipped_apps_report_url
- )
- return skipped_apps_table_section
-
- def _get_report_str(self) -> str:
- self.additional_info = (
- f'**Build Summary ({self.get_commit_summary}):**\n'
- '\n'
- '> ℹ️ Note: Binary artifacts stored in MinIO are retained for 4 DAYS from their build date\n'
- )
- failed_apps_report_parts = self.get_failed_apps_report_parts()
- skipped_apps_report_parts = self.get_skipped_apps_report_parts()
- built_apps_report_parts = self.get_built_apps_report_parts()
-
- return self.generate_html_report(
- ''.join(failed_apps_report_parts + built_apps_report_parts + skipped_apps_report_parts)
- )
-
-
-class JobReportGenerator(ReportGenerator):
- def __init__(
- self,
- project_id: int,
- mr_iid: int,
- pipeline_id: int,
- job_id: int,
- commit_id: str,
- local_commit_id: str,
- *,
- title: str = 'Job Report',
- jobs: list[GitlabJob],
- ):
- super().__init__(project_id, mr_iid, pipeline_id, job_id, commit_id, local_commit_id, title=title)
- self.jobs = jobs
- self.report_titles_map = {
- 'failed_jobs': 'Failed Jobs (Excludes "integration_test" and "target_test" jobs)',
- 'succeeded': 'Succeeded Jobs',
- }
- self.failed_jobs_report_file = 'job_report.html'
-
- def _get_report_str(self) -> str:
- """
- Generate a complete HTML report string by processing jobs.
- :return: Complete HTML report string.
- """
- report_str: str = ''
-
- if not self.jobs:
- print('No jobs found, skip generating job report')
- return 'No Job Found'
-
- relevant_failed_jobs = self._sort_items(
- self._filter_items(
- self.jobs, lambda job: job.is_failed and job.stage not in ['integration_test', 'target_test']
- ),
- key='latest_failed_count',
- )
- succeeded_jobs = self._filter_items(self.jobs, lambda job: job.is_success)
-
- self.additional_info = f'**Job Summary ({self.get_commit_summary}):**\n'
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['succeeded'], len(succeeded_jobs)
- )
-
- if not relevant_failed_jobs:
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['failed_jobs'], len(relevant_failed_jobs)
- )
- return report_str
-
- report_sections = self.create_table_section(
- title='Failed Jobs (Excludes "integration_test" and "target_test" jobs)',
- items=relevant_failed_jobs,
- headers=[
- 'Job Name',
- 'Failure Reason',
- 'Failure Log',
- 'Failures across all other branches (10 latest jobs)',
- 'URL',
- 'CI Dashboard URL',
- ],
- row_attrs=['name', 'failure_reason', 'failure_log', 'url', 'ci_dashboard_url'],
- value_functions=[
- (
- 'Failures across all other branches (10 latest jobs)',
- lambda item: f'{getattr(item, "latest_failed_count", "")} '
- f'/ {getattr(item, "latest_total_count", "")}',
- )
- ],
- )
- relevant_failed_jobs_report_url = get_artifacts_url(self.job_id, self.failed_jobs_report_file)
- self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['failed_jobs'], len(relevant_failed_jobs), relevant_failed_jobs_report_url
- )
-
- report_str = self.generate_html_report(''.join(report_sections))
- return report_str
diff --git a/tools/ci/dynamic_pipelines/scripts/generate_report.py b/tools/ci/dynamic_pipelines/scripts/generate_report.py
deleted file mode 100644
index 1f40fff50ad..00000000000
--- a/tools/ci/dynamic_pipelines/scripts/generate_report.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
-# SPDX-License-Identifier: Apache-2.0
-import argparse
-import glob
-import os
-import subprocess
-import typing as t
-
-import __init__ # noqa: F401 # inject the system path
-from idf_build_apps import json_list_files_to_apps
-from idf_ci import GitlabEnvVars
-from idf_ci_local.app import enrich_apps_with_metrics_info
-
-from dynamic_pipelines.report import BuildReportGenerator
-from dynamic_pipelines.report import JobReportGenerator
-from dynamic_pipelines.utils import fetch_app_metrics
-from dynamic_pipelines.utils import fetch_failed_jobs
-
-
-def main() -> None:
- parser: argparse.ArgumentParser = setup_argument_parser()
- args: argparse.Namespace = parser.parse_args()
-
- report_actions: dict[str, t.Callable[[argparse.Namespace], None]] = {
- 'build': generate_build_report,
- 'job': generate_jobs_report,
- }
-
- report_action = report_actions.get(args.report_type)
- if report_action is None:
- raise ValueError('Unknown report type is requested to be generated.')
-
- report_action(args)
-
-
-def setup_argument_parser() -> argparse.ArgumentParser:
- report_type_parser: argparse.ArgumentParser = argparse.ArgumentParser(add_help=False)
- report_type_parser.add_argument(
- '--report-type', choices=['build', 'job'], required=True, help='Type of report to generate'
- )
- report_type_args: argparse.Namespace
- remaining_args: list[str]
- report_type_args, remaining_args = report_type_parser.parse_known_args()
-
- parser: argparse.ArgumentParser = argparse.ArgumentParser(
- description='Update reports in MR pipelines based on the selected report type',
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- parents=[report_type_parser],
- )
-
- common_arguments(parser)
- conditional_arguments(report_type_args, parser)
-
- return parser
-
-
-def common_arguments(parser: argparse.ArgumentParser) -> None:
- parser.add_argument('--project-id', type=int, default=os.getenv('CI_PROJECT_ID'), help='Project ID')
- parser.add_argument('--mr-iid', type=int, default=os.getenv('CI_MERGE_REQUEST_IID'), help='Merge Request IID')
- parser.add_argument('--pipeline-id', type=int, default=os.getenv('PARENT_PIPELINE_ID'), help='Pipeline ID')
- parser.add_argument('--job-id', type=int, default=os.getenv('CI_JOB_ID'), help='Job ID')
- parser.add_argument('--commit-id', default=os.getenv('CI_COMMIT_SHA', ''), help='MR merged result commit ID')
- parser.add_argument('--local-commit-id', default=os.getenv('PIPELINE_COMMIT_SHA', ''), help='local dev commit ID')
-
-
-def conditional_arguments(report_type_args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
- if report_type_args.report_type == 'build':
- parser.add_argument('--app-list-filepattern', default='app_info*.txt', help='Pattern to match app list files')
- elif report_type_args.report_type == 'target_test':
- parser.add_argument(
- '--junit-report-filepattern', default='XUNIT_RESULT*.xml', help='Pattern to match JUnit report files'
- )
-
-
-def generate_build_report(args: argparse.Namespace) -> None:
- # generate presigned url for the artifacts
- subprocess.check_output(
- [
- 'idf-ci',
- 'gitlab',
- 'generate-presigned-json',
- '--commit-sha',
- args.local_commit_id,
- '--output',
- 'presigned.json',
- ],
- )
- print('generated presigned.json')
-
- # generate report
- apps = json_list_files_to_apps(glob.glob(args.app_list_filepattern))
- print(f'loaded {len(apps)} apps')
- app_metrics = fetch_app_metrics(
- source_commit_sha=args.commit_id,
- target_commit_sha=os.environ.get('CI_MERGE_REQUEST_TARGET_BRANCH_SHA'),
- )
- apps = enrich_apps_with_metrics_info(app_metrics, apps)
- report_generator = BuildReportGenerator(
- args.project_id, args.mr_iid, args.pipeline_id, args.job_id, args.commit_id, args.local_commit_id, apps=apps
- )
- report_generator.post_report()
-
-
-def generate_jobs_report(args: argparse.Namespace) -> None:
- jobs: list[t.Any] = fetch_failed_jobs(args.commit_id)
-
- if not jobs:
- return
-
- report_generator = JobReportGenerator(
- args.project_id, args.mr_iid, args.pipeline_id, args.job_id, args.commit_id, args.local_commit_id, jobs=jobs
- )
- report_generator.post_report()
-
- if GitlabEnvVars().IDF_CI_IS_DEBUG_PIPELINE:
- print('Debug pipeline detected, exit non-zero to fail the pipeline in order to block merge')
- exit(30)
-
-
-if __name__ == '__main__':
- main()
diff --git a/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml b/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
index f5fc1cbbea6..6f0b1949e4c 100644
--- a/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
+++ b/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
@@ -56,12 +56,6 @@
needs:
- pipeline: $PARENT_PIPELINE_ID
job: pipeline_variables
- cache:
- # Usually do not need submodule-cache in target_test
- - key: pip-cache-${LATEST_GIT_TAG}
- paths:
- - .cache/pip
- policy: pull
artifacts:
paths:
- XUNIT_RESULT*.xml
diff --git a/tools/ci/dynamic_pipelines/templates/report.template.html b/tools/ci/dynamic_pipelines/templates/report.template.html
deleted file mode 100644
index adbdf4710f3..00000000000
--- a/tools/ci/dynamic_pipelines/templates/report.template.html
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
-
-
- {{title}}
-
-
-
-
-
-
-
-
-
-
-
-