From 000966660ca80a2bd5fe233f9d727edfe6dd5875 Mon Sep 17 00:00:00 2001 From: Alina Kladieva Date: Thu, 9 Nov 2023 08:38:58 +0100 Subject: [PATCH] Smart CI POC (#19825) * Try using a custom action directly from repo * Run smart CI under ubuntu-latest * Set output + add a sample step * Update linux.yml * Add components.yml * Add some conditions * Just to check if reference to "needs" work in job context * Update linux.yml * More example cases * Dummy change to CPU * Fix typo * Fix SAMPLES_AFFECTED variable * Use more correct dependents key * Fighting with messy GHA conditions * No brackets and no double quotes in conditions * Revert "Dummy change to CPU" This reverts commit 4eae09e5b51816a63f77c868276e8edc4869f816. * Use refactored action * Move action implementation to openvino repo * Extend components.yml config * Update labeler.yml * Dummy change to TF FE * Fix indentation * Add missing needs * Add missing records * Allow missing records for components in validation * install_openvino_dependencies as a separate step for Python_Unit_Tests * Improve config validation * Revert "Dummy change to TF FE" This reverts commit 01190864d1001c21ccc6d228ba445f4d94d8fc62. * Dummy change to model hub tests * Update CPU component config * Dummy change to Python API * Dummy change to Python API * Revert "Dummy change to Python API" This reverts commit 3fce0bb3fbdbdd0fcb70d65bf6cae0d06be1a881. * Dummy change to Python API * Simplify conditions. Cover "no components changed" case * Update components.yml * Update .gitignore * Revert "Dummy change to Python API" This reverts commit e57ea9852c6bcfd6091ff1db3ae7c8e285a9b9f4. * Fix dependencies scopes * Add simple unit tests for smart ci functionality * Revert "Dummy change to model hub tests" This reverts commit c3d6837e224117eb70083297aabd6a978aa1632d. * Use ghapi module with permissive license * Cover install_build_dependencies.sh script by labeler * More labels * Use ghapi. Apply review comments * Enable dot files to be matched by labeler * Warning instead of error in artifacts upload where smart ci is enabled * Fix master merge * Fix condition for TF FE common tests * Fix condition for Pytorch FE tests * Remove condition for pytorch model tests * Allow any label as a component * Refactor tests log handling * Allow any defined label as a component * Rearrange config structure. Fill the config with actual data * Run full scope on changes to non-matching files * Add missing conditions --------- Co-authored-by: Ilya Lavrenov --- .github/actions/smart-ci/action.yml | 86 ++++++++ .../actions/smart-ci/components_schema.yml | 35 ++++ .github/actions/smart-ci/requirements.txt | 3 + .github/actions/smart-ci/smart_ci.py | 197 +++++++++++++++++ .github/actions/smart-ci/smart_ci_test.py | 198 ++++++++++++++++++ .github/components.yml | 198 ++++++++++++++++++ .github/workflows/linux.yml | 90 +++++++- .gitignore | 1 + 8 files changed, 798 insertions(+), 10 deletions(-) create mode 100644 .github/actions/smart-ci/action.yml create mode 100644 .github/actions/smart-ci/components_schema.yml create mode 100644 .github/actions/smart-ci/requirements.txt create mode 100644 .github/actions/smart-ci/smart_ci.py create mode 100644 .github/actions/smart-ci/smart_ci_test.py create mode 100644 .github/components.yml diff --git a/.github/actions/smart-ci/action.yml b/.github/actions/smart-ci/action.yml new file mode 100644 index 00000000000..ff04d53f943 --- /dev/null +++ b/.github/actions/smart-ci/action.yml @@ -0,0 +1,86 @@ +name: "Smart CI action" +description: "Returns product components affected by PR or commit" +inputs: + repository: + description: "GitHub repository" + required: true + repo_token: + description: "Token for access to GitHub repository" + required: true + pr: + description: "GitHub PR number. If not set - commit is used" + required: false + commit_sha: + description: "GitHub commit hash. Used if no PR number is set" + required: false + component_pattern: + description: "Pattern to extract component name from PR label. If not set, any label is considered a component name" + required: false + labeler_check_name: + description: "Name of the labeler check" + required: false + default: "triage" + components_config: + description: "Path to components configuration file" + required: false + default: ".github/components.yml" + components_config_schema: + description: "Path to the schema file for components configuration" + required: false + default: ".github/actions/smart-ci/components_schema.yml" + labeler_config: + description: "Path to labeler configuration file" + required: false + default: ".github/labeler.yml" + +outputs: + all_components: + description: "All components listed in configuration" + value: ${{ steps.smart_ci.outputs.all_components }} + affected_components: + description: "Affected components to run validation for and their validation scope" + value: ${{ steps.smart_ci.outputs.affected_components }} + +runs: + using: "composite" + steps: + - name: Wait for labeler to finish + uses: lewagon/wait-on-check-action@v1.3.1 + if: ${{ github.event_name == 'pull_request' }} + with: + ref: ${{ github.event.pull_request.head.sha }} + check-name: ${{ inputs.labeler_check_name }} + repo-token: ${{ inputs.repo_token }} + wait-interval: 10 + + - name: checkout components file + uses: actions/checkout@v4 + with: + sparse-checkout: .github/components.yml + sparse-checkout-cone-mode: false + + - name: Install Python dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: "${{ github.action_path }}/requirements.txt" + update-setuptools: "false" + update-wheel: "false" + + - name: Test functionality + run: | + python ${{ github.action_path }}/smart_ci_test.py + shell: bash + + - name: Smart CI + id: smart_ci + run: | + python ${{ github.action_path }}/smart_ci.py \ + $([[ -n "${{ inputs.pr }}" ]] && echo '--pr ${{ inputs.pr }}' || echo '-s ${{ inputs.commit_sha }}') \ + -r ${{ inputs.repository }} \ + -p "${{ inputs.component_pattern }}" \ + -c "${{ inputs.components_config }}" \ + -m "${{ inputs.components_config_schema }}" \ + -l "${{ inputs.labeler_config }}" + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.repo_token }} diff --git a/.github/actions/smart-ci/components_schema.yml b/.github/actions/smart-ci/components_schema.yml new file mode 100644 index 00000000000..7e9d0e4483d --- /dev/null +++ b/.github/actions/smart-ci/components_schema.yml @@ -0,0 +1,35 @@ +# YAML schema for Smart CI configuration file components.yml (see https://json-schema.org) + +definitions: + component_name: + type: string + pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$" + + component_data: + type: object # dict + additionalProperties: false + properties: + cmake: + type: array + uniqueItems: true + items: + '$ref': '#/definitions/component_name' + + revalidate: + type: array + uniqueItems: true + items: + '$ref': '#/definitions/component_name' + + build: + type: array + uniqueItems: true + items: + '$ref': '#/definitions/component_name' + +propertyNames: # Validates component names + '$ref': '#/definitions/component_name' +patternProperties: + ".*": # Component (name validated via propertyNames) + '$ref': '#/definitions/component_data' +additionalProperties: false diff --git a/.github/actions/smart-ci/requirements.txt b/.github/actions/smart-ci/requirements.txt new file mode 100644 index 00000000000..0b63ed63322 --- /dev/null +++ b/.github/actions/smart-ci/requirements.txt @@ -0,0 +1,3 @@ +ghapi~=1.0.4 +pyyaml~=6.0.1 +jsonschema~=4.19.1 \ No newline at end of file diff --git a/.github/actions/smart-ci/smart_ci.py b/.github/actions/smart-ci/smart_ci.py new file mode 100644 index 00000000000..3430519f717 --- /dev/null +++ b/.github/actions/smart-ci/smart_ci.py @@ -0,0 +1,197 @@ +import os +import re +import argparse +import yaml +import json +import jsonschema +import logging +from pathlib import Path +from ghapi.all import GhApi + + +class ComponentConfig: + FullScope = {'build', 'test'} + ScopeKeys = {'build', 'revalidate'} + + def __init__(self, config: dict, schema: dict, all_possible_components: set): + self.config = config + self.log = logging.getLogger(self.__class__.__name__) + self.all_defined_components = set(self.config.keys()) # already defined in components.yml + self.all_possible_components = all_possible_components # can be added to components.yml (based on labeler.yml) + + self.validate(schema, all_possible_components) + + def validate(self, schema: dict, all_possible_components: set) -> None: + """Validates syntax of configuration file""" + jsonschema.validate(self.config, schema) + + invalid_components = self.all_defined_components.difference(all_possible_components) + if invalid_components: + error_msg = f"components are invalid: " \ + f"{invalid_components} are not listed in labeler config: {all_possible_components}" + raise jsonschema.exceptions.ValidationError(error_msg) + + for component_name, data in self.config.items(): + dependent_components = set(data.get('dependent_components', dict()).keys()) if data else set() + + invalid_dependents = dependent_components.difference(all_possible_components) + if invalid_dependents: + error_msg = f"dependent_components of {component_name} are invalid: " \ + f"{invalid_dependents} are not listed in components config: {all_possible_components}" + raise jsonschema.exceptions.ValidationError(error_msg) + + def get_affected_components(self, changed_components_names: set) -> dict: + """Returns changed components, their dependencies and validation scope for them""" + affected_components = dict() + + # If some changed components were not defined in config or no changed components detected at all, + # run full scope for everything (just in case) + changed_not_defined_components = changed_components_names.difference(self.all_defined_components) + if not changed_components_names or changed_not_defined_components: + self.log.info(f"Changed components {changed_not_defined_components} are not defined in smart ci config, " + "run full scope") + affected_components.update({name: self.FullScope for name in self.all_possible_components}) + return affected_components + + # Else check changed components' dependencies and add them to affected + for name in changed_components_names: + component_scopes = {k: v for k, v in self.config.get(name, dict()).items() if k in self.ScopeKeys} + for key, dependents in component_scopes.items(): + for dep_name in dependents: + affected_components[dep_name] = affected_components.get(dep_name, set()) + scope = self.FullScope if key == 'revalidate' else {key} + affected_components[dep_name] = affected_components[dep_name].union(scope) + + if not component_scopes: + self.log.info(f"Changed component '{name}' doesn't have {self.ScopeKeys} keys in components config. " + f"Assuming that it affects everything, the whole scope will be started") + for dep_name in self.all_possible_components: + affected_components[dep_name] = self.FullScope + + # If the component was explicitly changed, run full scope for it + affected_components.update({name: self.FullScope for name in changed_components_names}) + self.log.info(f"Changed components with dependencies: {affected_components}") + + # For non-affected components that are not defined in config - run full scope + affected_components.update({name: self.FullScope for name in self.all_possible_components + if name not in self.all_defined_components}) + + return affected_components + + def get_static_data(self, components_names: set, data_key: str, default: str = None) -> dict: + """Returns requested generic static data defined for each component""" + data = {name: self.config[name].get(data_key, default) for name in components_names} + return data + + +def component_name_from_label(label: str, component_pattern: str = None) -> str: + """Extracts component name from label""" + component = label + if component_pattern: + matches = re.findall(component_pattern, label) + component = matches[0] if matches else None + component = component.replace(' ', '_') if component else None + return component + + +def get_changed_component_names(pr, all_possible_components: set, component_pattern: str = None) -> set: + """Returns component names changed in a given PR""" + components = set() + for label in pr.labels: + component = component_name_from_label(label.name, component_pattern) + if component: + components.add(component) + elif label.name in all_possible_components: + # Allow any labels defined explicitly in labeler config as components + # (predefined labels, such as "do not merge", are still ignored) + components.add(label.name) + + return components + + +def parse_args(): + parser = argparse.ArgumentParser(description='Returns product components changed in a given PR or commit') + parser.add_argument('--pr', type=int, required=False, help='PR number. If not set, --commit is used') + parser.add_argument('-s', '--commit-sha', required=False, help='Commit SHA. If not set, --pr is used') + parser.add_argument('-r', '--repo', help='GitHub repository') + parser.add_argument('-p', '--pattern', default=None, help='Pattern to extract component name from PR label. ' + 'If not set, any label is considered a component name') + parser.add_argument('-c', '--components-config', default='.github/components.yml', + help='Path to config file with info about dependencies between components') + parser.add_argument('-m', '--components-config-schema', default='.github/actions/smart-ci/components_schema.yml', + help='Path to the schema file for components config') + parser.add_argument('-l', '--labeler-config', default='.github/labeler.yml', + help='Path to PR labeler config file') + args = parser.parse_args() + return args + + +def init_logger(): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(name)-15s %(levelname)-8s %(message)s', + datefmt='%m-%d-%Y %H:%M:%S') + + +def set_github_output(name: str, value: str, github_output_var_name: str = 'GITHUB_OUTPUT'): + """Sets output variable for a GitHub Action""" + logger = logging.getLogger(__name__) + # In an environment variable "GITHUB_OUTPUT" GHA stores path to a file to write outputs to + with open(os.environ.get(github_output_var_name), 'a+') as file: + logger.info(f"Add {name}={value} to {github_output_var_name}") + print(f'{name}={value}', file=file) + + +def main(): + init_logger() + logger = logging.getLogger(__name__) + args = parse_args() + for arg, value in sorted(vars(args).items()): + logger.info(f"Argument {arg}: {value}") + + with open(Path(args.components_config), 'r') as config: + components_config = yaml.safe_load(config) + + owner, repository = args.repo.split('/') + gh_api = GhApi(owner=owner, repo=repository, token=os.getenv("GITHUB_TOKEN")) + pr = gh_api.pulls.get(args.pr) if args.pr else None + + with open(Path(args.components_config_schema), 'r') as schema_file: + schema = yaml.safe_load(schema_file) + + with open(Path(args.labeler_config), 'r') as labeler_file: + labeler_config = yaml.safe_load(labeler_file) + + all_possible_components = set() + for label in labeler_config.keys(): + component_name = component_name_from_label(label, args.pattern) + all_possible_components.add(component_name if component_name else label) + + # For now, we don't want to apply smart ci rules for post-commits + is_postcommit = not pr + if is_postcommit: + logger.info(f"The run is a post-commit run, executing full validation scope for all components") + + no_match_files_changed = 'no-match-files' in [label.name for label in pr.labels] + if no_match_files_changed: + logger.info(f"There are changed files that don't match any pattern in labeler config, " + f"executing full validation scope for all components") + + run_full_scope = is_postcommit or no_match_files_changed + + # In post-commits - validate all components regardless of changeset + # In pre-commits - validate only changed components with their dependencies + all_defined_components = components_config.keys() + changed_component_names = set(all_defined_components) if run_full_scope else \ + get_changed_component_names(pr, all_possible_components, args.pattern) + logger.info(f"changed_component_names: {changed_component_names}") + + cfg = ComponentConfig(components_config, schema, all_possible_components) + affected_components = cfg.get_affected_components(changed_component_names) + + # Syntactic sugar for easier use in GHA pipeline + affected_components_output = {name: {s: True for s in scope} for name, scope in affected_components.items()} + set_github_output("affected_components", json.dumps(affected_components_output)) + + +if __name__ == '__main__': + main() diff --git a/.github/actions/smart-ci/smart_ci_test.py b/.github/actions/smart-ci/smart_ci_test.py new file mode 100644 index 00000000000..2c9aa1e4644 --- /dev/null +++ b/.github/actions/smart-ci/smart_ci_test.py @@ -0,0 +1,198 @@ +import logging +import sys +import unittest +from smart_ci import ComponentConfig + +log = logging.getLogger() +log.level = logging.DEBUG + + +def log_handler(func): + def wrapper(*args, **kwargs): + stream_handler = logging.StreamHandler(sys.stdout) + log.addHandler(stream_handler) + result = func(*args, **kwargs) + log.removeHandler(stream_handler) + return result + return wrapper + + +class TestComponentConfig(unittest.TestCase): + def setUp(self): + self.all_possible_components = {'comp1', 'comp2', 'comp3', 'comp4'} + ComponentConfig.ScopeKeys = {'build', 'revalidate', '_scope_1', '_scope_2', '_scope_3'} + + @log_handler + def validate(self, config_data: dict, changed_components: set, expected_result: dict): + log.info(f"{self._testMethodName}:") + config = ComponentConfig(config_data, {}, self.all_possible_components) + result = config.get_affected_components(changed_components) + self.assertEqual(expected_result, result) + + def test_no_changed_components(self): + config_data = { + 'comp1': {'build': {}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = set() + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': ComponentConfig.FullScope, + 'comp3': ComponentConfig.FullScope, + 'comp4': ComponentConfig.FullScope, + } + self.validate(config_data, changed_components, expected_result) + + def test_all_components_changed(self): + config_data = { + 'comp1': {'build': {}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1', 'comp2', 'comp3', 'comp4'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': ComponentConfig.FullScope, + 'comp3': ComponentConfig.FullScope, + 'comp4': ComponentConfig.FullScope, + } + self.validate(config_data, changed_components, expected_result) + + def test_changed_component_not_defined(self): + config_data = { + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': ComponentConfig.FullScope, + 'comp3': ComponentConfig.FullScope, + 'comp4': ComponentConfig.FullScope, + } + self.validate(config_data, changed_components, expected_result) + + def test_component_changed_no_scope_keys(self): + config_data = { + 'comp1': {}, + 'comp2': {}, + 'comp3': {}, + 'comp4': {}, + } + changed_components = {'comp1'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': ComponentConfig.FullScope, + 'comp3': ComponentConfig.FullScope, + 'comp4': ComponentConfig.FullScope, + } + self.validate(config_data, changed_components, expected_result) + + def test_one_component_changed_dependents_empty(self): + config_data = { + 'comp1': {'build': {}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + } + self.validate(config_data, changed_components, expected_result) + + def test_not_changed_dependent_component(self): + config_data = { + 'comp1': {'build': {'comp2'}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': {'build'} + } + self.validate(config_data, changed_components, expected_result) + + def test_changed_dependent_component(self): + config_data = { + 'comp1': {'build': {'comp2'}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1', 'comp2'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': ComponentConfig.FullScope + } + self.validate(config_data, changed_components, expected_result) + + def test_dependent_component_multiple_parents(self): + config_data = { + 'comp1': {'_scope_1': {'comp2'}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, '_scope_2': {'comp2'}, '_scope_3': {'comp2'}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1', 'comp3'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': {'_scope_1', '_scope_2', '_scope_3'}, + 'comp3': ComponentConfig.FullScope + } + self.validate(config_data, changed_components, expected_result) + + def test_dependent_component_empty_scopes(self): + config_data = { + 'comp1': {'build': {}, 'revalidate': {'comp2'}}, + 'comp2': {}, + 'comp3': {}, + 'comp4': {}, + } + changed_components = {'comp1', 'comp3'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': ComponentConfig.FullScope, + 'comp3': ComponentConfig.FullScope, + 'comp4': ComponentConfig.FullScope + } + self.validate(config_data, changed_components, expected_result) + + def test_changed_component_empty_dependencies(self): + config_data = { + 'comp1': {'build': {}, 'revalidate': {}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + } + self.validate(config_data, changed_components, expected_result) + + def test_multiple_dependents(self): + config_data = { + 'comp1': {'build': {'comp2'}, 'revalidate': {'comp3'}}, + 'comp2': {'build': {}, 'revalidate': {}}, + 'comp3': {'build': {'comp4'}, 'revalidate': {}}, + 'comp4': {'build': {}, 'revalidate': {}}, + } + changed_components = {'comp1'} + expected_result = { + 'comp1': ComponentConfig.FullScope, + 'comp2': {'build'}, + 'comp3': ComponentConfig.FullScope, + # We don't consider dependencies of dependencies affected, so comp4 is not expected here + } + self.validate(config_data, changed_components, expected_result) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/.github/components.yml b/.github/components.yml new file mode 100644 index 00000000000..e414e61443d --- /dev/null +++ b/.github/components.yml @@ -0,0 +1,198 @@ +CPU: + revalidate: + - C_API + - Python_API + - samples + build: + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - IR_FE + +GPU: + build: + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - IR_FE + +GNA: + build: + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - IR_FE + +HETERO: + revalidate: + - CPU + - GPU + - GNA + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - C_API + - Python_API + build: + - IR_FE + +AUTO_BATCH: + revalidate: + - CPU + - GPU + - GNA + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - C_API + - Python_API + build: + - IR_FE + +TEMPLATE: + revalidate: + - CPU + - GPU + - GNA + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - C_API + - Python_API + build: + - IR_FE + +AUTO: + revalidate: + - CPU + - GPU + - GNA + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - C_API + - Python_API + build: + - IR_FE + +IR_FE: + revalidate: + - C_API + - Python_API + - samples + build: + - CPU + +ONNX_FE: + revalidate: + - MO + build: + - CPU + - Python_API + +PDPD_FE: + revalidate: + - MO + build: + - CPU + - Python_API + +TF_FE: + revalidate: + - MO + build: + - CPU + - Python_API + +TFL_FE: + revalidate: + - MO + build: + - CPU + - Python_API + +PyTorch_FE: + revalidate: + - MO + build: + - CPU + - Python_API + +C_API: + build: + - CPU + - HETERO + - AUTO_BATCH + - AUTO + - IR_FE + +Python_API: + revalidate: + - samples + - MO + - POT + - tools + build: + - CPU + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + - IR_FE + - ONNX_FE + - PDPD_FE + - TF_FE + - TFL_FE + - PyTorch_FE + +samples: + build: + - CPU + - AUTO_BATCH + - AUTO + - IR_FE + - C_API + - Python_API + +IE_Tests: + revalidate: + - CPU + - GPU + - GNA + - HETERO + - AUTO_BATCH + - TEMPLATE + - AUTO + build: + - IR_FE + +MO: + revalidate: + - POT + build: + - Python_API + +POT: + build: + - CPU + - Python_API + +tools: + build: + - CPU + - Python_API + +docs: + revalidate: [] + build: [] + +licensing: + revalidate: [] + build: [] diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index ec23f7aba5a..51c8146ae3b 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -34,6 +34,31 @@ env: PYTHON_VERSION: '3.11' jobs: + Smart_CI: + runs-on: ubuntu-latest + outputs: + affected_components: "${{ steps.smart_ci.outputs.affected_components }}" + steps: + - name: checkout action + uses: actions/checkout@v4 + with: + sparse-checkout: .github/actions/smart-ci + + - name: Get affected components + id: smart_ci + uses: ./.github/actions/smart-ci + with: + repository: ${{ github.repository }} + pr: ${{ github.event.pull_request.number }} + commit_sha: ${{ github.sha }} + component_pattern: "category: (.*)" + repo_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Show affected components + run: | + echo "${{ toJSON(steps.smart_ci.outputs.affected_components) }}" + shell: bash + Build: timeout-minutes: 150 defaults: @@ -303,7 +328,7 @@ jobs: ovc --help Samples: - needs: Build + needs: [Build, Smart_CI] timeout-minutes: 20 defaults: run: @@ -316,6 +341,7 @@ jobs: INSTALL_DIR: /__w/openvino/openvino/install INSTALL_TEST_DIR: /__w/openvino/openvino/install/tests BUILD_DIR: /__w/openvino/openvino/build + if: fromJSON(needs.smart_ci.outputs.affected_components).samples steps: - name: Download OpenVINO package @@ -381,6 +407,7 @@ jobs: # - name: Samples tests + if: fromJSON(needs.smart_ci.outputs.affected_components).samples.test run: | export WORKSPACE=${INSTALL_DIR} export IE_APP_PATH=${INSTALL_DIR}/samples_bin @@ -402,7 +429,7 @@ jobs: with: name: test-results-samples path: ${{ env.INSTALL_TEST_DIR }}/TEST*.xml - if-no-files-found: 'error' + if-no-files-found: 'warn' Conformance: needs: Build @@ -646,7 +673,7 @@ jobs: CXX_Unit_Tests: name: C++ unit tests - needs: Build + needs: [Build, Smart_CI] timeout-minutes: 20 defaults: run: @@ -706,6 +733,7 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-InferenceUnit.xml - name: Low Precision Transformations Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).LP_transformations.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_lp_transformations_tests --gtest_print_time=1 \ @@ -718,6 +746,7 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-ConditionalCompilation.xml - name: IR frontend tests + if: fromJSON(needs.smart_ci.outputs.affected_components).IR_FE.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_ir_frontend_tests --gtest_print_time=1 \ @@ -731,6 +760,7 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-PaddleTests.xml - name: ONNX frontend tests + if: fromJSON(needs.smart_ci.outputs.affected_components).ONNX_FE.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_onnx_frontend_tests --gtest_print_time=1 \ @@ -738,30 +768,36 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-ONNXFrontend.xml - name: TensorFlow Common frontend tests + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test || + fromJSON(needs.smart_ci.outputs.affected_components).TFL_FE.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_tensorflow_common_tests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-TensorFlowCommonFrontend.xml - name: TensorFlow frontend tests + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_tensorflow_frontend_tests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-TensorFlowFrontend.xml - name: TensorFlow Lite frontend tests + if: fromJSON(needs.smart_ci.outputs.affected_components).TFL_FE.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_tensorflow_lite_frontend_tests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-TensorFlowLiteFrontend.xml - name: Transformations func tests + if: fromJSON(needs.smart_ci.outputs.affected_components).transformations.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_transformations_tests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-Transformations.xml - name: Legacy Transformations func tests + if: fromJSON(needs.smart_ci.outputs.affected_components).transformations.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_legacy_transformations_tests --gtest_print_time=1 \ @@ -780,12 +816,14 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-CommonUtilTests.xml - name: Snippets func tests + if: fromJSON(needs.smart_ci.outputs.affected_components).CPU.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_snippets_func_tests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-SnippetsFuncTests.xml - name: CPU plugin unit tests + if: fromJSON(needs.smart_ci.outputs.affected_components).CPU.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_cpu_unit_tests --gtest_print_time=1 \ @@ -804,18 +842,21 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-OpImplTests.xml - name: AUTO unit tests + if: fromJSON(needs.smart_ci.outputs.affected_components).AUTO.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_auto_unit_tests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-ov_auto_unit_tests.xml - name: AUTO func Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).AUTO.test run: | source ${{ env.INSTALL_DIR }}/setupvars.sh ${{ env.INSTALL_TEST_DIR }}/ov_auto_func_tests --gtest_print_time=1 \ --gtest_output=xml:${{ env.INSTALL_TEST_DIR }}/TEST-ov_auto_func_tests.xml - name: Template plugin func tests + if: fromJSON(needs.smart_ci.outputs.affected_components).TEMPLATE.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_template_func_tests --gtest_print_time=1 \ @@ -823,23 +864,27 @@ jobs: --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-TemplateFuncTests.xml - name: Inference Engine C API tests + if: fromJSON(needs.smart_ci.outputs.affected_components).C_API.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/InferenceEngineCAPITests --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-InferenceEngineCAPITests.xml - name: OpenVINO C API tests + if: fromJSON(needs.smart_ci.outputs.affected_components).C_API.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_capi_test --gtest_print_time=1 \ --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-OpenVINOCAPITests.xml - name: AutoBatch unit tests + if: fromJSON(needs.smart_ci.outputs.affected_components).AUTO_BATCH.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_auto_batch_unit_tests --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-ov_auto_batch_unit_tests.xml - name: AutoBatch func tests + if: fromJSON(needs.smart_ci.outputs.affected_components).AUTO_BATCH.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_auto_batch_func_tests --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-ov_auto_batch_func_tests.xml @@ -850,11 +895,13 @@ jobs: ${INSTALL_TEST_DIR}/ov_proxy_plugin_tests --gtest_print_time=1 --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-OVProxyTests.xml - name: Hetero unit tests + if: fromJSON(needs.smart_ci.outputs.affected_components).HETERO.test run: | source ${{ env.INSTALL_DIR }}/setupvars.sh ${{ env.INSTALL_TEST_DIR }}/ov_hetero_unit_tests --gtest_print_time=1 --gtest_output=xml:${{ env.INSTALL_TEST_DIR }}/TEST-OVHeteroUnitTests.xml - name: Hetero func tests + if: fromJSON(needs.smart_ci.outputs.affected_components).HETERO.test run: | source ${INSTALL_DIR}/setupvars.sh ${INSTALL_TEST_DIR}/ov_hetero_func_tests --gtest_print_time=1 --gtest_output=xml:${INSTALL_TEST_DIR}/TEST-OVHeteroFuncTests.xml @@ -865,11 +912,11 @@ jobs: with: name: test-results-cpp path: ${{ env.INSTALL_TEST_DIR }}/TEST*.xml - if-no-files-found: 'error' + if-no-files-found: 'warn' Python_Unit_Tests: name: Python unit tests - needs: Build + needs: [Build, Smart_CI] timeout-minutes: 40 defaults: run: @@ -926,6 +973,9 @@ jobs: pip-cache-path: ${{ env.PIP_CACHE_PATH }} should-setup-pip-paths: 'true' + - name: Install OpenVINO dependencies + run: ${INSTALL_DIR}/install_dependencies/install_openvino_dependencies.sh -c=core -y + - name: Install OpenVINO Python wheels run: | # Install the core OV wheel @@ -952,6 +1002,7 @@ jobs: # - name: Python API 1.0 Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).Python_API.test run: | python3 -m pytest -s ${INSTALL_TEST_DIR}/pyngraph \ --junitxml=${INSTALL_TEST_DIR}/TEST-Pyngraph.xml \ @@ -959,6 +1010,7 @@ jobs: --ignore=${INSTALL_TEST_DIR}/pyngraph/tests_compatibility/test_onnx/test_backend.py - name: Python API 2.0 Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).Python_API.test run: | # for 'template' extension export LD_LIBRARY_PATH=${INSTALL_TEST_DIR}:$LD_LIBRARY_PATH @@ -967,6 +1019,7 @@ jobs: --ignore=${INSTALL_TEST_DIR}/pyopenvino/tests/test_utils/test_utils.py - name: Model Optimizer unit tests + if: fromJSON(needs.smart_ci.outputs.affected_components).MO.test run: | # required for MxNet apt-get install -y libgomp1 libquadmath0 @@ -982,6 +1035,7 @@ jobs: --ignore=${INSTALL_TEST_DIR}/onnx/test_python/test_zoo_models.py - name: OVC unit tests + if: fromJSON(needs.smart_ci.outputs.affected_components).MO.test run: python3 -m pytest -s ${INSTALL_TEST_DIR}/ovc/unit_tests --junitxml=${INSTALL_TEST_DIR}/TEST-OpenVinoConversion.xml - name: Install Python Layer tests dependencies @@ -990,6 +1044,7 @@ jobs: python3 -m pip install -r ${LAYER_TESTS_INSTALL_DIR}/requirements.txt - name: MO Python API Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).MO.test run: | # Import 'test_utils' installed in '/tests/python/openvino' export LD_LIBRARY_PATH=${PIP_INSTALL_PATH}/openvino/libs:$LD_LIBRARY_PATH @@ -1000,6 +1055,7 @@ jobs: TEST_PRECISION: FP16 - name: OVC Python API Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).MO.test run: | # Import 'test_utils' installed in '/tests/python/openvino' export PYTHONPATH=${INSTALL_TEST_DIR}/python @@ -1010,6 +1066,8 @@ jobs: TEST_PRECISION: FP16 - name: Python Frontend tests + if: fromJSON(needs.smart_ci.outputs.affected_components).PyTorch_FE.test || + fromJSON(needs.smart_ci.outputs.affected_components).PDPD_FE.test run: | # to allow 'libtest_builtin_extensions.so' to find 'libopenvino_onnx_frontend.so' export LD_LIBRARY_PATH=${PIP_INSTALL_PATH}/openvino/libs:$LD_LIBRARY_PATH @@ -1022,6 +1080,7 @@ jobs: TEST_PRECISION: FP32 - name: PyTorch torch.compile TORCHFX Layer Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).PyTorch_FE.test run: | python3 -m pytest ${LAYER_TESTS_INSTALL_DIR}/pytorch_tests -m precommit_fx_backend --junitxml=${INSTALL_TEST_DIR}/TEST-pytorch.xml env: @@ -1030,6 +1089,7 @@ jobs: PYTORCH_TRACING_MODE: TORCHFX - name: PyTorch torch.compile TORCHSCRIPT Layer Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).PyTorch_FE.test run: | python3 -m pytest ${LAYER_TESTS_INSTALL_DIR}/pytorch_tests -m precommit_ts_backend --junitxml=${INSTALL_TEST_DIR}/TEST-pytorch.xml env: @@ -1038,6 +1098,7 @@ jobs: PYTORCH_TRACING_MODE: TORCHSCRIPT - name: ONNX Layer Tests + if: fromJSON(needs.smart_ci.outputs.affected_components).ONNX_FE.test run: | # requires 'unit_tests' from 'tools/mo' export PYTHONPATH=${INSTALL_TEST_DIR}/mo:$PYTHONPATH @@ -1047,6 +1108,7 @@ jobs: TEST_PRECISION: FP16 - name: TensorFlow 1 Layer Tests - TF FE + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test run: | # requires 'unit_tests' from 'mo' export PYTHONPATH=${INSTALL_TEST_DIR}/mo @@ -1056,6 +1118,7 @@ jobs: TEST_PRECISION: FP16 - name: TensorFlow 2 Layer Tests - TF FE + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test run: | # requires 'unit_tests' from 'mo' export PYTHONPATH=${INSTALL_TEST_DIR}/mo @@ -1065,20 +1128,24 @@ jobs: TEST_PRECISION: FP16 - name: JAX Layer Tests - TF FE + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test run: python3 -m pytest ${LAYER_TESTS_INSTALL_DIR}/jax_tests/ -m precommit --junitxml=${INSTALL_TEST_DIR}/TEST-jax.xml env: TEST_DEVICE: CPU - name: TensorFlow 1 Layer Tests - Legacy FE + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test run: python3 -m pytest ${LAYER_TESTS_INSTALL_DIR}/tensorflow_tests/test_tf_Roll.py --ir_version=10 --junitxml=${INSTALL_TEST_DIR}/TEST-tf_Roll.xml - name: TensorFlow 2 Layer Tests - Legacy FE + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test run: python3 -m pytest ${LAYER_TESTS_INSTALL_DIR}/tensorflow2_keras_tests/test_tf2_keras_activation.py --ir_version=11 -k "sigmoid" --junitxml=${INSTALL_TEST_DIR}/TEST-tf2_Activation.xml env: TEST_DEVICE: CPU TEST_PRECISION: FP16 - name: TensorFlow Lite Layer Tests - TFL FE + if: fromJSON(needs.smart_ci.outputs.affected_components).TFL_FE.test run: python3 -m pytest ${LAYER_TESTS_INSTALL_DIR}/tensorflow_lite_tests/ --junitxml=${INSTALL_TEST_DIR}/TEST-tfl_fe.xml env: TEST_DEVICE: CPU @@ -1107,11 +1174,11 @@ jobs: path: | ${{ env.INSTALL_TEST_DIR }}/TEST*.html ${{ env.INSTALL_TEST_DIR }}/TEST*.xml - if-no-files-found: 'error' + if-no-files-found: 'warn' CPU_Functional_Tests: name: CPU functional tests - needs: Build + needs: [Build, Smart_CI] timeout-minutes: 25 defaults: run: @@ -1125,7 +1192,7 @@ jobs: INSTALL_TEST_DIR: /__w/openvino/openvino/install/tests PARALLEL_TEST_SCRIPT: /__w/openvino/openvino/install/tests/functional_test_utils/layer_tests_summary/run_parallel.py PARALLEL_TEST_CACHE: /__w/openvino/openvino/install/tests/test_cache.lst - + if: fromJSON(needs.smart_ci.outputs.affected_components).CPU.test steps: - name: Download OpenVINO package uses: actions/download-artifact@v3 @@ -1207,7 +1274,7 @@ jobs: TensorFlow_Hub_Models_Tests: name: TensorFlow Hub Models tests - needs: Build + needs: [Build, Smart_CI] defaults: run: shell: bash @@ -1223,6 +1290,8 @@ jobs: INSTALL_DIR: ${{ github.workspace }}/install INSTALL_TEST_DIR: ${{ github.workspace }}/install/tests MODEL_HUB_TESTS_INSTALL_DIR: ${{ github.workspace }}/install/tests/model_hub_tests + if: fromJSON(needs.smart_ci.outputs.affected_components).TF_FE.test || + fromJSON(needs.smart_ci.outputs.affected_components).TFL_FE.test steps: - name: Check sudo @@ -1291,7 +1360,7 @@ jobs: PyTorch_Models_Tests: name: PyTorch Models tests - needs: Build + needs: [Build, Smart_CI] timeout-minutes: ${{ github.event_name == 'schedule' && 400 || 30 }} defaults: run: @@ -1307,6 +1376,7 @@ jobs: INSTALL_DIR: ${{ github.workspace }}/install INSTALL_TEST_DIR: ${{ github.workspace }}/install/tests MODEL_HUB_TESTS_INSTALL_DIR: ${{ github.workspace }}/install/tests/model_hub_tests + if: fromJSON(needs.smart_ci.outputs.affected_components).PyTorch_FE.test steps: - name: Check sudo diff --git a/.gitignore b/.gitignore index 0b0d07a220a..769c4a4e581 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ __pycache__ /tools/mo/*.svg /src/plugins/intel_cpu/tools/commit_slider/*.json /src/plugins/intel_cpu/tools/commit_slider/slider_cache/* +.github/GITHUB_OUTPUT