From c619ab492c276ffdebf8bbe4271b226683824cca Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Tue, 28 Jul 2026 10:47:57 +0200 Subject: [PATCH 1/2] ci(buildv2): skip patching the fake_pass build child pipeline idf-ci emits a skip pipeline containing only a fake_pass job, with no include of test_child_pipeline.yml, when the modified files match no buildable apps. patch_buildv2_child_pipeline.py redefined generate_pytest_child_pipeline unconditionally, assuming the included template supplied its tags, image and assign_test stage via keyword merge. With the template absent, the injected job was bare: no tags, default test stage. It was scheduled on the wrong runner and failed, turning the allow_failure build_child_pipeline_buildv2 bridge red on every buildv2 MR whose changes produce an empty app matrix. Return early when a fake_pass job is present so the child pipeline is left untouched and skips, matching the default build path. Add a unit test for the patch script and a host_test job to run it. --- .gitlab/ci/host-test.yml | 6 ++ .gitlab/ci/rules.yml | 3 + .../scripts/patch_buildv2_child_pipeline.py | 7 ++ .../test_patch_buildv2_child_pipeline.py | 68 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 tools/ci/dynamic_pipelines/tests/test_patch_buildv2_child_pipeline.py diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml index 8699aa14070..05bcff62ef6 100644 --- a/.gitlab/ci/host-test.yml +++ b/.gitlab/ci/host-test.yml @@ -285,6 +285,12 @@ test_idf_build_apps_load_soc_caps: - cd tools/ci - pytest_for_ut ./test_soc_headers_load_in_idf_build_apps.py +test_dynamic_pipelines_scripts: + extends: .host_test_template + script: + - cd ${IDF_PATH}/tools/ci/dynamic_pipelines/tests + - pytest_for_ut ./test_patch_buildv2_child_pipeline.py + test_nvs_gen_check: extends: .host_test_template artifacts: diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml index 16c0a9a7ff9..b3f4724632e 100644 --- a/.gitlab/ci/rules.yml +++ b/.gitlab/ci/rules.yml @@ -142,6 +142,9 @@ - "tools/ci/test_soc_headers_load_in_idf_build_apps.py" + - "tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py" + - "tools/ci/dynamic_pipelines/tests/**/*" + .patterns-docker: &patterns-docker - "tools/docker/**/*" diff --git a/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py index 2a791ecfc37..e67325ff24b 100644 --- a/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py +++ b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py @@ -33,6 +33,13 @@ def patch(path: str) -> None: with open(path) as f: d = yaml.safe_load(f) + if 'fake_pass' in d: + # idf-ci emitted a skip pipeline (no apps matched the modified files): there + # are no build jobs to activate and no included generate_pytest_child_pipeline + # to redefine. Leave it untouched so Build system v2 skips like the default path. + print('Skip pipeline (fake_pass) detected, leaving it untouched') + return + injected = [] for k, v in d.items(): if isinstance(v, dict) and 'extends' in v: diff --git a/tools/ci/dynamic_pipelines/tests/test_patch_buildv2_child_pipeline.py b/tools/ci/dynamic_pipelines/tests/test_patch_buildv2_child_pipeline.py new file mode 100644 index 00000000000..bc22e5dfd39 --- /dev/null +++ b/tools/ci/dynamic_pipelines/tests/test_patch_buildv2_child_pipeline.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for patch_buildv2_child_pipeline.patch().""" + +import importlib.util +from pathlib import Path +from typing import Any + +import yaml + +_SCRIPT = Path(__file__).parent.parent / 'scripts' / 'patch_buildv2_child_pipeline.py' +_spec = importlib.util.spec_from_file_location('patch_buildv2_child_pipeline', _SCRIPT) +assert _spec is not None and _spec.loader is not None +patch_mod: Any = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(patch_mod) + + +def _write_and_patch(tmp_path: Path, content: dict[str, Any]) -> dict[str, Any]: + p = tmp_path / 'buildv2_child_pipeline.yml' + p.write_text(yaml.safe_dump(content, sort_keys=False)) + patch_mod.patch(str(p)) + result: dict[str, Any] = yaml.safe_load(p.read_text()) + return result + + +def test_fake_pass_skip_pipeline_is_left_untouched(tmp_path: Path) -> None: + # idf-ci emits a skip pipeline with only a `fake_pass` job when no apps match + # the modified files. There is no included generate_pytest_child_pipeline to + # redefine, so the patch must leave the pipeline untouched and let it skip. + # Mirrors idf_ci.idf_gitlab.pipeline._get_fake_pass_job(): the sole job is + # `fake_pass` and no test_child_pipeline.yml is included. + skip_pipeline = { + 'fake_pass': { + 'stage': 'build', + 'tags': ['build', 'shiny'], + 'before_script': [], + 'after_script': [], + 'cache': [], + 'needs': [], + 'script': ['echo "skip the entire child pipeline"'], + }, + 'workflow': {'name': 'build_child_pipeline', 'rules': [{'when': 'always'}]}, + } + + result = _write_and_patch(tmp_path, dict(skip_pipeline)) + + assert 'generate_pytest_child_pipeline' not in result + assert result == skip_pipeline + + +def test_build_jobs_get_buildv2_injection(tmp_path: Path) -> None: + # A real pipeline (apps matched) carries build jobs that `extends` the dynamic + # template. Those must get IDF_BUILD_V2 / PIPELINE_COMMIT_SHA injected and + # generate_pytest_child_pipeline redefined with the post-processing step. + pipeline = { + 'build_test_related_apps': { + 'extends': '.dynamic_build_template', + 'needs': [{'pipeline': '$PARENT_PIPELINE_ID', 'job': 'generate_build_child_pipeline'}], + }, + } + + result = _write_and_patch(tmp_path, pipeline) + + job = result['build_test_related_apps'] + assert job['variables']['IDF_BUILD_V2'] == '1' + assert job['variables']['PIPELINE_COMMIT_SHA'] == patch_mod.PIPELINE_COMMIT_SHA_V2 + assert 'generate_pytest_child_pipeline' in result + assert result['generate_pytest_child_pipeline']['variables']['IDF_BUILD_V2'] == '1' From 00aea6d1f0089f2fc1e62ecd8eb32cb3ba004db4 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Thu, 20 Aug 2026 11:30:32 +0200 Subject: [PATCH 2/2] ci(buildv2): collect target tests with IDF_BUILD_V2 active --- .../dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py index e67325ff24b..781cd5524fb 100644 --- a/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py +++ b/tools/ci/dynamic_pipelines/scripts/patch_buildv2_child_pipeline.py @@ -53,6 +53,8 @@ def patch(path: str) -> None: # keyword level, so only `script` is overridden; `needs`, `artifacts`, # `image`, etc. come from the included version. d['generate_pytest_child_pipeline'] = { + # Set IDF_BUILD_V2 so test-case collection honors `if IDF_BUILD_V2 == "1"` + 'variables': {'IDF_BUILD_V2': '1'}, 'script': [ 'python tools/ci/dynamic_pipelines/scripts/generate_target_test_child_pipeline.py', 'python tools/ci/dynamic_pipelines/scripts/patch_buildv2_target_test_pipeline.py '