ci: lint yaml files that use dependencies: [] together with needs

This commit is contained in:
Fu Hanxi 2024-01-25 12:45:13 +01:00
parent a5b261f699
commit fc802da68c
No known key found for this signature in database
GPG Key ID: 19399699CF3C4B16
5 changed files with 102 additions and 34 deletions

View File

@ -172,6 +172,7 @@ build_clang_test_apps_esp32c6:
extends: extends:
- .build_template - .build_template
- .rules:build:check - .rules:build:check
dependencies: # set dependencies to null to avoid missing artifacts issue
needs: needs:
- job: fast_template_app - job: fast_template_app
artifacts: false artifacts: false
@ -272,6 +273,7 @@ build_template_app:
- .build_template_app_template - .build_template_app_template
- .rules:build - .rules:build
stage: host_test stage: host_test
dependencies: # set dependencies to null to avoid missing artifacts issue
needs: needs:
- job: fast_template_app - job: fast_template_app
artifacts: false artifacts: false

View File

@ -3,7 +3,7 @@
image: $ESP_ENV_IMAGE image: $ESP_ENV_IMAGE
tags: tags:
- host_test - host_test
dependencies: [] dependencies: # set dependencies to null to avoid missing artifacts issue
check_pre_commit: check_pre_commit:
extends: extends:

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD # SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import argparse import argparse
import inspect import inspect
import os import os
@ -11,7 +10,8 @@ from collections import defaultdict
from itertools import product from itertools import product
import yaml import yaml
from idf_ci_utils import IDF_PATH, GitlabYmlConfig from idf_ci_utils import GitlabYmlConfig
from idf_ci_utils import IDF_PATH
try: try:
import pygraphviz as pgv import pygraphviz as pgv
@ -201,9 +201,13 @@ class RulesWriter:
def new_rules_str(self): # type: () -> str def new_rules_str(self): # type: () -> str
res = [] res = []
for k, v in sorted(self.rules.items()): for k, v in sorted(self.rules.items()):
if '.rules:' + k not in self.yml_config.used_rules: if k.startswith('pattern'):
continue
if '.rules:' + k not in self.yml_config.used_templates:
print(f'WARNING: unused rule: {k}, skipping...') print(f'WARNING: unused rule: {k}, skipping...')
continue continue
res.append(self.RULES_TEMPLATE.format(k, self._format_rule(k, v))) res.append(self.RULES_TEMPLATE.format(k, self._format_rule(k, v)))
return '\n\n'.join(res) return '\n\n'.join(res)

View File

@ -1,18 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
""" """
Check gitlab ci yaml files Check gitlab ci yaml files
""" """
import argparse import argparse
import os import os
import typing as t import typing as t
from functools import cached_property from functools import cached_property
from idf_ci_utils import IDF_PATH, GitlabYmlConfig, get_submodule_dirs from idf_ci_utils import get_submodule_dirs
from idf_ci_utils import GitlabYmlConfig
from idf_ci_utils import IDF_PATH
class YmlLinter: class YmlLinter:
@ -50,9 +49,10 @@ class YmlLinter:
if ( if (
k not in self.yml_config.global_keys k not in self.yml_config.global_keys
and k not in self.yml_config.anchors and k not in self.yml_config.anchors
and k not in self.yml_config.templates
and k not in self.yml_config.jobs and k not in self.yml_config.jobs
): ):
raise SystemExit(f'Parser incorrect. Key {k} not in global keys, rules or jobs') raise SystemExit(f'Parser incorrect. Key {k} not in global keys, anchors, templates, or jobs')
def _lint_default_values_artifacts(self) -> None: def _lint_default_values_artifacts(self) -> None:
defaults_artifacts = self.yml_config.default.get('artifacts', {}) defaults_artifacts = self.yml_config.default.get('artifacts', {})
@ -79,13 +79,30 @@ class YmlLinter:
for item in undefined_patterns: for item in undefined_patterns:
self._errors.append(f'undefined pattern {item}. Please add {item} to .patterns-submodule') self._errors.append(f'undefined pattern {item}. Please add {item} to .patterns-submodule')
def _lint_gitlab_yml_rules(self) -> None: def _lint_gitlab_yml_templates(self) -> None:
unused_rules = self.yml_config.rules - self.yml_config.used_rules unused_templates = self.yml_config.templates.keys() - self.yml_config.used_templates
for item in unused_rules: for item in unused_templates:
self._errors.append(f'Unused rule: {item}, please remove it') # known unused ones
undefined_rules = self.yml_config.used_rules - self.yml_config.rules if item not in [
for item in undefined_rules: '.before_script:fetch:target_test', # used in dynamic pipeline
self._errors.append(f'Undefined rule: {item}') ]:
self._errors.append(f'Unused template: {item}, please remove it')
undefined_templates = self.yml_config.used_templates - self.yml_config.templates.keys()
for item in undefined_templates:
self._errors.append(f'Undefined template: {item}')
def _lint_dependencies_and_needs(self) -> None:
"""
Use `dependencies: []` together with `needs: []` could cause missing artifacts issue.
"""
for job_name, d in self.yml_config.jobs.items():
if 'dependencies' in d and 'needs' in d:
if d['dependencies'] is not None and d['needs']:
self._errors.append(
f'job {job_name} has both `dependencies` and `needs` defined. '
f'Please set `dependencies:` (to null) explicitly to avoid missing artifacts issue'
)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -125,7 +125,9 @@ class GitlabYmlConfig:
all_config = dict() all_config = dict()
root_yml = yaml.load(open(root_yml_filepath), Loader=yaml.FullLoader) root_yml = yaml.load(open(root_yml_filepath), Loader=yaml.FullLoader)
for item in root_yml['include']:
# expanding "include"
for item in root_yml.pop('include', []) or []:
all_config.update(yaml.load(open(os.path.join(IDF_PATH, item)), Loader=yaml.FullLoader)) all_config.update(yaml.load(open(os.path.join(IDF_PATH, item)), Loader=yaml.FullLoader))
if 'default' in all_config: if 'default' in all_config:
@ -133,6 +135,16 @@ class GitlabYmlConfig:
self._config = all_config self._config = all_config
# anchor is the string that will be reused in templates
self._anchor_keys: t.Set[str] = set()
# template is a dict that will be extended
self._template_keys: t.Set[str] = set()
self._used_template_keys: t.Set[str] = set() # tracing the used templates
# job is a dict that will be executed
self._job_keys: t.Set[str] = set()
self.expand_extends()
@property @property
def default(self) -> t.Dict[str, t.Any]: def default(self) -> t.Dict[str, t.Any]:
return self._defaults return self._defaults
@ -147,33 +159,66 @@ class GitlabYmlConfig:
@cached_property @cached_property
def anchors(self) -> t.Dict[str, t.Any]: def anchors(self) -> t.Dict[str, t.Any]:
return {k: v for k, v in self.config.items() if k.startswith('.')} return {k: v for k, v in self.config.items() if k in self._anchor_keys}
@cached_property @cached_property
def jobs(self) -> t.Dict[str, t.Any]: def jobs(self) -> t.Dict[str, t.Any]:
return {k: v for k, v in self.config.items() if not k.startswith('.') and k not in self.global_keys} return {k: v for k, v in self.config.items() if k in self._job_keys}
@cached_property @cached_property
def rules(self) -> t.Set[str]: def templates(self) -> t.Dict[str, t.Any]:
return {k for k, _ in self.anchors.items() if self._is_rule_key(k)} return {k: v for k, v in self.config.items() if k in self._template_keys}
@cached_property @cached_property
def used_rules(self) -> t.Set[str]: def used_templates(self) -> t.Set[str]:
res = set() return self._used_template_keys
for v in self.config.values(): def expand_extends(self) -> None:
if not isinstance(v, dict): """
expand the `extends` key in-place.
"""
for k, v in self.config.items():
if k in self.global_keys:
continue continue
for item in to_list(v.get('extends')): if isinstance(v, (str, list)):
if self._is_rule_key(item): self._anchor_keys.add(k)
res.add(item) elif k.startswith('.if-'):
self._anchor_keys.add(k)
elif k.startswith('.'):
self._template_keys.add(k)
elif isinstance(v, dict):
self._job_keys.add(k)
else:
raise ValueError(f'Unknown type for key {k} with value {v}')
return res # no need to expand anchor
@staticmethod # expand template first
def _is_rule_key(key: str) -> bool: for k in self._template_keys:
return key.startswith('.rules:') or key.endswith('template') self._expand_extends(k)
# expand job
for k in self._job_keys:
self._expand_extends(k)
def _expand_extends(self, name: str) -> t.Dict[str, t.Any]:
extends = to_list(self.config[name].pop('extends', None))
original_d = self.config[name].copy()
if not extends:
return self.config[name] # type: ignore
d = {}
while extends:
self._used_template_keys.update(extends)
for i in extends:
d.update(self._expand_extends(i))
extends = to_list(self.config[name].pop('extends', None))
self.config[name] = {**d, **original_d}
return self.config[name] # type: ignore
def get_all_manifest_files() -> t.List[str]: def get_all_manifest_files() -> t.List[str]: