From a57320ef54452785073f3cf22c3e688b907dfc18 Mon Sep 17 00:00:00 2001 From: Roberto Montero Date: Thu, 5 Nov 2026 15:19:26 +0100 Subject: [PATCH 1/2] Filter GitLab SSI tests by PR title Co-authored-by: Cursor --- .gitlab-ci.yml | 34 +++++++++++++++---- .../test_compute_libraries_and_scenarios.py | 28 ++++++++++++++- .../compute_libraries_and_scenarios.py | 20 ++++++++--- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b599db1dc46..f0433b04636 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,19 @@ compute_pipeline: fi git diff --name-only "$BASE_COMMIT" "$CI_COMMIT_SHA" >> modified_files.txt # List modified files cat modified_files.txt + export GITHUB_PR_TITLE="" + if [ "$CI_COMMIT_REF_NAME" != "main" ]; then + if PR_DATA=$(curl -fsS \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/DataDog/system-tests/commits/$CI_COMMIT_SHA/pulls"); then + GITHUB_PR_TITLE=$(jq -r \ + '[.[] | select(.state == "open")] | if length == 1 then .[0].title else "" end' <<< "$PR_DATA") + export GITHUB_PR_TITLE + else + echo "⚠️ Unable to read the GitHub PR title. Running all SSI languages." + fi + fi # Deliberate AWS SSI opt-in; shared callers keep the full matrix on main. python utils/scripts/compute_libraries_and_scenarios.py --select-main-push-from-diff >> impacted_scenarios.txt cat impacted_scenarios.txt @@ -86,12 +99,21 @@ compute_pipeline: SYSTEM_TEST_BUILD_ATTEMPTS=3 SYSTEM_TEST_BUILD_TIMEOUT=240 ./build.sh -i runner source venv/bin/activate fi - - python utils/scripts/compute-workflow-parameters.py nodejs -s "$scenarios" -g "$scenarios_groups" --parametric-job-count 1 --ci-environment "${CI_ENVIRONMENT}" --format gitlab - - python utils/scripts/compute-workflow-parameters.py java -s "$scenarios" -g "$scenarios_groups" --parametric-job-count 1 --ci-environment "${CI_ENVIRONMENT}" --format gitlab - - python utils/scripts/compute-workflow-parameters.py dotnet -s "$scenarios" -g "$scenarios_groups" --parametric-job-count 1 --ci-environment "${CI_ENVIRONMENT}" --format gitlab - - python utils/scripts/compute-workflow-parameters.py python -s "$scenarios" -g "$scenarios_groups" --parametric-job-count 1 --ci-environment "${CI_ENVIRONMENT}" --format gitlab - - python utils/scripts/compute-workflow-parameters.py php -s "$scenarios" -g "$scenarios_groups" --parametric-job-count 1 --ci-environment "${CI_ENVIRONMENT}" --format gitlab - - python utils/scripts/compute-workflow-parameters.py ruby -s "$scenarios" -g "$scenarios_groups" --parametric-job-count 1 --ci-environment "${CI_ENVIRONMENT}" --format gitlab + - | + for library in nodejs java dotnet python php ruby; do + selected_scenarios="$scenarios" + selected_scenario_groups="$scenarios_groups" + if [ -n "${ssi_library:-}" ] && [ "$library" != "$ssi_library" ]; then + selected_scenarios="" + selected_scenario_groups="" + fi + python utils/scripts/compute-workflow-parameters.py "$library" \ + -s "$selected_scenarios" \ + -g "$selected_scenario_groups" \ + --parametric-job-count 1 \ + --ci-environment "${CI_ENVIRONMENT}" \ + --format gitlab + done - | if [ -n "$SYSTEM_TESTS_SCENARIOS" ] || [ -n "$SYSTEM_TESTS_SCENARIOS_GROUPS" ]; then cp *.yml "$CI_PROJECT_DIR" diff --git a/tests/test_the_test/test_compute_libraries_and_scenarios.py b/tests/test_the_test/test_compute_libraries_and_scenarios.py index d513dcd5f39..6b1c87c2c84 100644 --- a/tests/test_the_test/test_compute_libraries_and_scenarios.py +++ b/tests/test_the_test/test_compute_libraries_and_scenarios.py @@ -5,7 +5,7 @@ from functools import wraps from utils import pytest -from utils.scripts.compute_libraries_and_scenarios import Inputs, process +from utils.scripts.compute_libraries_and_scenarios import Inputs, get_gitlab_ssi_library, process from utils import scenarios @@ -510,6 +510,32 @@ def test_gitlab(self): 'scenarios_groups=""', ] + @pytest.mark.parametrize( + ("pr_title", "expected"), + [ + ("[nodejs] Test Node.js", "nodejs"), + ("[java] Test Java", "java"), + ("[dotnet] Test .NET", "dotnet"), + ("[PYTHON] Test Python", "python"), + ("[php] Test PHP", "php"), + ("[ruby] Test Ruby", "ruby"), + ("[golang] Test Go", None), + ("[java@feature] Test a Java branch", None), + ("Test Java [java]", None), + ], + ) + def test_gitlab_ssi_library_title(self, pr_title: str, expected: str | None): + assert get_gitlab_ssi_library(pr_title) == expected + + @set_env("GITLAB_CI", "true") + @set_env("CI_PIPELINE_SOURCE", "pull_request") + @set_env("CI_COMMIT_REF_NAME", "") + @set_env("GITHUB_PR_TITLE", "[java] Test Java") + def test_gitlab_ssi_library_output(self): + inputs = build_inputs(["README.md"]) + + assert 'ssi_library="java"' in process(inputs) + def test_manifest_no_edit(self): inputs = build_inputs( ["manifests/java.yml"], diff --git a/utils/scripts/compute_libraries_and_scenarios.py b/utils/scripts/compute_libraries_and_scenarios.py index d88a1277c27..78cc34f2f07 100644 --- a/utils/scripts/compute_libraries_and_scenarios.py +++ b/utils/scripts/compute_libraries_and_scenarios.py @@ -62,6 +62,7 @@ def _get_scenarios_by_definition_file() -> dict[str, set[str]]: }, } GITLAB_MAIN = {"python"} +GITLAB_SSI_LIBRARIES = tuple(sorted(COMPONENT_GROUPS.ssi & COMPONENT_GROUPS.lib_injection)) def check_scenarios(scenarios: set[str]) -> bool: @@ -76,6 +77,14 @@ def check_libraries(val: set[str]) -> bool: return all(library in ALL_LIBRARIES for library in val) +def get_gitlab_ssi_library(pr_title: str) -> str | None: + normalized_title = pr_title.lower() + for library in GITLAB_SSI_LIBRARIES: + if normalized_title.startswith(f"[{library}]"): + return library + return None + + def _setify(value: list[str] | set[str] | str | None) -> set[str]: if isinstance(value, set): return value @@ -403,17 +412,16 @@ def __init__( def load_git_info(self) -> None: # Get all relevant environment variables. + self.pr_title = os.environ.get("GITHUB_PR_TITLE", "").lower() if "GITLAB_CI" in os.environ: source = os.environ.get("CI_PIPELINE_SOURCE", "push") self.event_name = "pull_request" if source == "merge_request_event" else source branch = os.environ.get("CI_COMMIT_REF_NAME", "") self.ref = f"refs/heads/{branch}" if branch else "" - self.pr_title = "" self.is_gitlab = True else: self.event_name = os.environ.get("GITHUB_EVENT_NAME", "pull_request") self.ref = os.environ.get("GITHUB_REF", "") - self.pr_title = os.environ.get("GITHUB_PR_TITLE", "").lower() def load_raw_impacts(self) -> None: # Gets the raw pattern matching data that maps file to impacted @@ -459,7 +467,10 @@ def load_scenario_mappings(self) -> None: def extra_gitlab_output(inputs: Inputs) -> dict[str, str]: - return {"CI_PIPELINE_SOURCE": inputs.event_name, "CI_COMMIT_REF_NAME": inputs.ref} + outputs = {"CI_PIPELINE_SOURCE": inputs.event_name, "CI_COMMIT_REF_NAME": inputs.ref} + if ssi_library := get_gitlab_ssi_library(inputs.pr_title): + outputs["ssi_library"] = ssi_library + return outputs def stringify_outputs(outputs: dict[str, Any]) -> list[str]: @@ -500,7 +511,8 @@ def process(inputs: Inputs) -> list[str]: scenario_processor.process_manifests(inputs) scenario_processor.compute_scenarios_by_files(inputs) - library_processor.process_pr_title(inputs) + if not inputs.is_gitlab: + library_processor.process_pr_title(inputs) assert inputs.modified_files is not None for file in inputs.modified_files: From 7a2f93ac90df0fa17bb4029fdd8d4132684cdae9 Mon Sep 17 00:00:00 2001 From: Roberto Montero Date: Thu, 5 Nov 2026 15:56:23 +0100 Subject: [PATCH 2/2] Authenticate GitHub PR title lookup Co-authored-by: Cursor --- .gitlab-ci.yml | 39 ++++++++++++------- .../test_compute_libraries_and_scenarios.py | 14 +++++++ .../test_external_gitlab_pipeline.py | 22 +++++++++++ .../resolve_gitlab_pr_title.sh | 33 ++++++++++++++++ .../scripts/libraries_and_scenarios_rules.yml | 3 ++ 5 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f0433b04636..6f4d2e912fc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,10 +27,36 @@ variables: # Destination registry for mirrored CI images. MIRROR_DEST_REGISTRY: "registry.ddbuild.io/system-tests/mirror" +resolve_ssi_pr_title: + image: registry.ddbuild.io/system-tests/ci-runner:9b1520f98ee9 + tags: ["arch:amd64"] + stage: configure + id_tokens: + DDOCTOSTS_ID_TOKEN: + aud: dd-octo-sts + rules: + - if: >- + $CI_PROJECT_PATH == "DataDog/system-tests" && + $CI_PIPELINE_SOURCE == "push" && + $CI_COMMIT_BRANCH && + $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH && + ($SYSTEM_TESTS_SCENARIOS == null || $SYSTEM_TESTS_SCENARIOS == "") && + ($SYSTEM_TESTS_SCENARIOS_GROUPS == null || $SYSTEM_TESTS_SCENARIOS_GROUPS == "") + - when: never + script: + - bash utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh + artifacts: + reports: + dotenv: pr-title.env + compute_pipeline: image: registry.ddbuild.io/ci/libdatadog-build/system-tests:146143169 tags: ["arch:amd64"] stage: configure + needs: + - job: resolve_ssi_pr_title + artifacts: true + optional: true variables: CI_ENVIRONMENT: "prod" script: @@ -68,19 +94,6 @@ compute_pipeline: fi git diff --name-only "$BASE_COMMIT" "$CI_COMMIT_SHA" >> modified_files.txt # List modified files cat modified_files.txt - export GITHUB_PR_TITLE="" - if [ "$CI_COMMIT_REF_NAME" != "main" ]; then - if PR_DATA=$(curl -fsS \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/DataDog/system-tests/commits/$CI_COMMIT_SHA/pulls"); then - GITHUB_PR_TITLE=$(jq -r \ - '[.[] | select(.state == "open")] | if length == 1 then .[0].title else "" end' <<< "$PR_DATA") - export GITHUB_PR_TITLE - else - echo "⚠️ Unable to read the GitHub PR title. Running all SSI languages." - fi - fi # Deliberate AWS SSI opt-in; shared callers keep the full matrix on main. python utils/scripts/compute_libraries_and_scenarios.py --select-main-push-from-diff >> impacted_scenarios.txt cat impacted_scenarios.txt diff --git a/tests/test_the_test/test_compute_libraries_and_scenarios.py b/tests/test_the_test/test_compute_libraries_and_scenarios.py index 6b1c87c2c84..e0e6ea7bf82 100644 --- a/tests/test_the_test/test_compute_libraries_and_scenarios.py +++ b/tests/test_the_test/test_compute_libraries_and_scenarios.py @@ -151,6 +151,19 @@ def test_complete_file_path(self): "docker_ssi", ) + def test_gitlab_ssi_title_resolver_file_does_not_select_scenarios(self): + inputs = build_inputs(["utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh"]) + + assert_github_processor( + inputs, + [], + [], + 3600, + "false", + "DEFAULT", + "", + ) + def test_multiple_file_changes(self): inputs = build_inputs([".github/workflows/run-docker-ssi.yml", "README.md"]) @@ -513,6 +526,7 @@ def test_gitlab(self): @pytest.mark.parametrize( ("pr_title", "expected"), [ + ("", None), ("[nodejs] Test Node.js", "nodejs"), ("[java] Test Java", "java"), ("[dotnet] Test .NET", "dotnet"), diff --git a/tests/test_the_test/test_external_gitlab_pipeline.py b/tests/test_the_test/test_external_gitlab_pipeline.py index 4e5b3e1ebf9..896f7503510 100644 --- a/tests/test_the_test/test_external_gitlab_pipeline.py +++ b/tests/test_the_test/test_external_gitlab_pipeline.py @@ -1,5 +1,9 @@ """Tests for utils/scripts/ci_orchestrators/external_gitlab_pipeline.py.""" +from pathlib import Path + +import yaml + from utils import pytest from utils import scenarios @@ -45,3 +49,21 @@ def test_filter_yaml_keeps_only_requested_language(self): assert "python_job" in result assert "configure_job" in result assert result["stages"] == ["configure", "python", "pipeline-status"] + + def test_external_pipeline_keeps_compute_contract(self) -> None: + data = yaml.safe_load(Path(".gitlab-ci.yml").read_text()) + _strip_local_includes(data) + + result = filter_yaml(data, "java") + + assert result["compute_pipeline"]["image"] == ("registry.ddbuild.io/ci/libdatadog-build/system-tests:146143169") + assert result["compute_pipeline"]["needs"] == [ + {"job": "resolve_ssi_pr_title", "artifacts": True, "optional": True} + ] + + resolver = result["resolve_ssi_pr_title"] + assert resolver["image"] == "registry.ddbuild.io/system-tests/ci-runner:9b1520f98ee9" + assert '$CI_PROJECT_PATH == "DataDog/system-tests"' in resolver["rules"][0]["if"] + assert '$CI_PIPELINE_SOURCE == "push"' in resolver["rules"][0]["if"] + assert "$SYSTEM_TESTS_SCENARIOS == null" in resolver["rules"][0]["if"] + assert "$SYSTEM_TESTS_SCENARIOS_GROUPS == null" in resolver["rules"][0]["if"] diff --git a/utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh b/utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh new file mode 100644 index 00000000000..32b2b848854 --- /dev/null +++ b/utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -euo pipefail + +GITHUB_PR_TITLE="" + +if GITHUB_TOKEN=$(dd-octo-sts token \ + --scope DataDog/system-tests \ + --policy gitlab.github-access.read); then + trap 'dd-octo-sts revoke -t "$GITHUB_TOKEN" >/dev/null 2>&1 || true' EXIT + + if PR_DATA=$(curl -fsS --retry 3 --retry-delay 2 --retry-connrefused \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/DataDog/system-tests/commits/$CI_COMMIT_SHA/pulls"); then + if ! GITHUB_PR_TITLE=$(jq -r \ + '[.[] | select(.state == "open")] | if length == 1 then .[0].title else "" end' <<< "$PR_DATA"); then + echo "⚠️ Unable to parse the GitHub PR title. Running all SSI languages." + GITHUB_PR_TITLE="" + fi + else + echo "⚠️ Unable to read the GitHub PR title. Running all SSI languages." + fi + + dd-octo-sts revoke -t "$GITHUB_TOKEN" || true + trap - EXIT + unset GITHUB_TOKEN +else + echo "⚠️ Unable to authenticate to GitHub. Running all SSI languages." +fi + +printf 'GITHUB_PR_TITLE=%s\n' "$GITHUB_PR_TITLE" > pr-title.env diff --git a/utils/scripts/libraries_and_scenarios_rules.yml b/utils/scripts/libraries_and_scenarios_rules.yml index f44cb7ea2a6..a0dc7f34cee 100644 --- a/utils/scripts/libraries_and_scenarios_rules.yml +++ b/utils/scripts/libraries_and_scenarios_rules.yml @@ -258,6 +258,9 @@ patterns: scenario_groups: null utils/scripts/check_version.sh: scenario_groups: null + utils/scripts/ci_orchestrators/resolve_gitlab_pr_title.sh: + scenario_groups: null + libraries: null utils/scripts/ci_orchestrators/compute-time-stats.py: scenario_groups: null libraries: null