Skip to content

Commit 9443361

Browse files
committed
Merge branch 'ci/fix_missing_artifacts_with_dependencies_and_needs_set' into 'master'
ci: lint yaml files that use `dependencies: []` together with `needs` See merge request espressif/esp-idf!28707
2 parents b056ac7 + fc802da commit 9443361

File tree

5 files changed

+103
-35
lines changed

5 files changed

+103
-35
lines changed

.gitlab/ci/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ build_clang_test_apps_esp32c6:
172172
extends:
173173
- .build_template
174174
- .rules:build:check
175+
dependencies: # set dependencies to null to avoid missing artifacts issue
175176
needs:
176177
- job: fast_template_app
177178
artifacts: false
@@ -272,6 +273,7 @@ build_template_app:
272273
- .build_template_app_template
273274
- .rules:build
274275
stage: host_test
276+
dependencies: # set dependencies to null to avoid missing artifacts issue
275277
needs:
276278
- job: fast_template_app
277279
artifacts: false

.gitlab/ci/pre_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
image: $ESP_ENV_IMAGE
44
tags:
55
- host_test
6-
dependencies: []
6+
dependencies: # set dependencies to null to avoid missing artifacts issue
77

88
check_pre_commit:
99
extends:

tools/ci/generate_rules.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
#!/usr/bin/env python
22
#
3-
# SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
3+
# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
44
# SPDX-License-Identifier: Apache-2.0
5-
65
import argparse
76
import inspect
87
import os
@@ -11,7 +10,8 @@
1110
from itertools import product
1211

1312
import yaml
14-
from idf_ci_utils import IDF_PATH, GitlabYmlConfig
13+
from idf_ci_utils import GitlabYmlConfig
14+
from idf_ci_utils import IDF_PATH
1515

1616
try:
1717
import pygraphviz as pgv
@@ -201,9 +201,13 @@ def bot_label_str(label): # type: (str) -> str
201201
def new_rules_str(self): # type: () -> str
202202
res = []
203203
for k, v in sorted(self.rules.items()):
204-
if '.rules:' + k not in self.yml_config.used_rules:
204+
if k.startswith('pattern'):
205+
continue
206+
207+
if '.rules:' + k not in self.yml_config.used_templates:
205208
print(f'WARNING: unused rule: {k}, skipping...')
206209
continue
210+
207211
res.append(self.RULES_TEMPLATE.format(k, self._format_rule(k, v)))
208212
return '\n\n'.join(res)
209213

tools/ci/gitlab_yaml_linter.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
#!/usr/bin/env python
2-
3-
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
2+
# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
43
# SPDX-License-Identifier: Apache-2.0
5-
64
"""
75
Check gitlab ci yaml files
86
"""
9-
107
import argparse
118
import os
129
import typing as t
1310
from functools import cached_property
1411

15-
from idf_ci_utils import IDF_PATH, GitlabYmlConfig, get_submodule_dirs
12+
from idf_ci_utils import get_submodule_dirs
13+
from idf_ci_utils import GitlabYmlConfig
14+
from idf_ci_utils import IDF_PATH
1615

1716

1817
class YmlLinter:
@@ -50,9 +49,10 @@ def _lint_1_yml_parser(self) -> None:
5049
if (
5150
k not in self.yml_config.global_keys
5251
and k not in self.yml_config.anchors
52+
and k not in self.yml_config.templates
5353
and k not in self.yml_config.jobs
5454
):
55-
raise SystemExit(f'Parser incorrect. Key {k} not in global keys, rules or jobs')
55+
raise SystemExit(f'Parser incorrect. Key {k} not in global keys, anchors, templates, or jobs')
5656

5757
def _lint_default_values_artifacts(self) -> None:
5858
defaults_artifacts = self.yml_config.default.get('artifacts', {})
@@ -79,13 +79,30 @@ def _lint_submodule_patterns(self) -> None:
7979
for item in undefined_patterns:
8080
self._errors.append(f'undefined pattern {item}. Please add {item} to .patterns-submodule')
8181

82-
def _lint_gitlab_yml_rules(self) -> None:
83-
unused_rules = self.yml_config.rules - self.yml_config.used_rules
84-
for item in unused_rules:
85-
self._errors.append(f'Unused rule: {item}, please remove it')
86-
undefined_rules = self.yml_config.used_rules - self.yml_config.rules
87-
for item in undefined_rules:
88-
self._errors.append(f'Undefined rule: {item}')
82+
def _lint_gitlab_yml_templates(self) -> None:
83+
unused_templates = self.yml_config.templates.keys() - self.yml_config.used_templates
84+
for item in unused_templates:
85+
# known unused ones
86+
if item not in [
87+
'.before_script:fetch:target_test', # used in dynamic pipeline
88+
]:
89+
self._errors.append(f'Unused template: {item}, please remove it')
90+
91+
undefined_templates = self.yml_config.used_templates - self.yml_config.templates.keys()
92+
for item in undefined_templates:
93+
self._errors.append(f'Undefined template: {item}')
94+
95+
def _lint_dependencies_and_needs(self) -> None:
96+
"""
97+
Use `dependencies: []` together with `needs: []` could cause missing artifacts issue.
98+
"""
99+
for job_name, d in self.yml_config.jobs.items():
100+
if 'dependencies' in d and 'needs' in d:
101+
if d['dependencies'] is not None and d['needs']:
102+
self._errors.append(
103+
f'job {job_name} has both `dependencies` and `needs` defined. '
104+
f'Please set `dependencies:` (to null) explicitly to avoid missing artifacts issue'
105+
)
89106

90107

91108
if __name__ == '__main__':

tools/ci/idf_ci_utils.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,26 @@ def _load(self, root_yml_filepath: str) -> None:
125125

126126
all_config = dict()
127127
root_yml = yaml.load(open(root_yml_filepath), Loader=yaml.FullLoader)
128-
for item in root_yml['include']:
128+
129+
# expanding "include"
130+
for item in root_yml.pop('include', []) or []:
129131
all_config.update(yaml.load(open(os.path.join(IDF_PATH, item)), Loader=yaml.FullLoader))
130132

131133
if 'default' in all_config:
132134
self._defaults = all_config.pop('default')
133135

134136
self._config = all_config
135137

138+
# anchor is the string that will be reused in templates
139+
self._anchor_keys: t.Set[str] = set()
140+
# template is a dict that will be extended
141+
self._template_keys: t.Set[str] = set()
142+
self._used_template_keys: t.Set[str] = set() # tracing the used templates
143+
# job is a dict that will be executed
144+
self._job_keys: t.Set[str] = set()
145+
146+
self.expand_extends()
147+
136148
@property
137149
def default(self) -> t.Dict[str, t.Any]:
138150
return self._defaults
@@ -147,33 +159,66 @@ def global_keys(self) -> t.List[str]:
147159

148160
@cached_property
149161
def anchors(self) -> t.Dict[str, t.Any]:
150-
return {k: v for k, v in self.config.items() if k.startswith('.')}
162+
return {k: v for k, v in self.config.items() if k in self._anchor_keys}
151163

152164
@cached_property
153165
def jobs(self) -> t.Dict[str, t.Any]:
154-
return {k: v for k, v in self.config.items() if not k.startswith('.') and k not in self.global_keys}
166+
return {k: v for k, v in self.config.items() if k in self._job_keys}
155167

156168
@cached_property
157-
def rules(self) -> t.Set[str]:
158-
return {k for k, _ in self.anchors.items() if self._is_rule_key(k)}
169+
def templates(self) -> t.Dict[str, t.Any]:
170+
return {k: v for k, v in self.config.items() if k in self._template_keys}
159171

160172
@cached_property
161-
def used_rules(self) -> t.Set[str]:
162-
res = set()
163-
164-
for v in self.config.values():
165-
if not isinstance(v, dict):
173+
def used_templates(self) -> t.Set[str]:
174+
return self._used_template_keys
175+
176+
def expand_extends(self) -> None:
177+
"""
178+
expand the `extends` key in-place.
179+
"""
180+
for k, v in self.config.items():
181+
if k in self.global_keys:
166182
continue
167183

168-
for item in to_list(v.get('extends')):
169-
if self._is_rule_key(item):
170-
res.add(item)
184+
if isinstance(v, (str, list)):
185+
self._anchor_keys.add(k)
186+
elif k.startswith('.if-'):
187+
self._anchor_keys.add(k)
188+
elif k.startswith('.'):
189+
self._template_keys.add(k)
190+
elif isinstance(v, dict):
191+
self._job_keys.add(k)
192+
else:
193+
raise ValueError(f'Unknown type for key {k} with value {v}')
194+
195+
# no need to expand anchor
196+
197+
# expand template first
198+
for k in self._template_keys:
199+
self._expand_extends(k)
200+
201+
# expand job
202+
for k in self._job_keys:
203+
self._expand_extends(k)
204+
205+
def _expand_extends(self, name: str) -> t.Dict[str, t.Any]:
206+
extends = to_list(self.config[name].pop('extends', None))
207+
original_d = self.config[name].copy()
208+
if not extends:
209+
return self.config[name] # type: ignore
210+
211+
d = {}
212+
while extends:
213+
self._used_template_keys.update(extends)
214+
215+
for i in extends:
216+
d.update(self._expand_extends(i))
171217

172-
return res
218+
extends = to_list(self.config[name].pop('extends', None))
173219

174-
@staticmethod
175-
def _is_rule_key(key: str) -> bool:
176-
return key.startswith('.rules:') or key.endswith('template')
220+
self.config[name] = {**d, **original_d}
221+
return self.config[name] # type: ignore
177222

178223

179224
def get_all_manifest_files() -> t.List[str]:

0 commit comments

Comments
 (0)