diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b599db1dc46..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: @@ -86,12 +112,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..e0e6ea7bf82 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 @@ -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"]) @@ -510,6 +523,33 @@ def test_gitlab(self): 'scenarios_groups=""', ] + @pytest.mark.parametrize( + ("pr_title", "expected"), + [ + ("", None), + ("[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/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/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: 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