-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathazdev_linter_style.py
More file actions
288 lines (225 loc) · 10.5 KB
/
azdev_linter_style.py
File metadata and controls
288 lines (225 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
"""
This script is used to run azdev linter and azdev style on extensions.
It's only working on ADO by default. If want to run locally,
please update the target branch in find_modified_files_against_master_branch() in util.py.
"""
import json
import logging
import os
import re
import shutil
from subprocess import CalledProcessError, check_call, check_output
import service_name
from packaging.version import Version
from util import get_ext_metadata, find_modified_files_against_master_branch
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)
def separator_line():
logger.info('-' * 100)
class ModifiedFilesNotAllowedError(Exception):
"""
Exception raise for the scenario that modified files is conflict against publish requirement.
Scenario 1: if modified files contain only src/index.json, don't raise
Scenario 2: if modified files contain not only extension code but also src/index.json, raise.
Scenario 3: if modified files don't contain src/index.json, don't raise.
"""
def __str__(self):
msg = """
---------------------------------------------------------------------------------------------------------
You have modified both source code and src/index.json!
There is a release pipeline will help you to build, upload and publish your extension.
Once your PR is merged into master branch, a new PR will be created to update src/index.json automatically.
If you want us to help to build, upload and publish your extension, src/index.json must not be modified.
---------------------------------------------------------------------------------------------------------
"""
return msg
class AzExtensionHelper:
def __init__(self, extension_name):
self.extension_name = extension_name
@staticmethod
def _cmd(cmd):
logger.info(cmd)
check_call(cmd, shell=True)
def add_from_url(self, url):
self._cmd('az extension add -s {} -y'.format(url))
def remove(self):
self._cmd('az extension remove -n {}'.format(self.extension_name))
class AzdevExtensionHelper:
def __init__(self, extension_name):
self.extension_name = extension_name
@staticmethod
def _cmd(cmd):
logger.info(cmd)
check_call(cmd, shell=True)
def add_from_code(self):
self._cmd('azdev extension add {}'.format(self.extension_name))
def remove(self):
self._cmd('azdev extension remove {}'.format(self.extension_name))
def linter(self):
self._cmd('azdev linter --include-whl-extensions {} --min-severity medium'.format(self.extension_name))
def style(self):
self._cmd('azdev style {}'.format(self.extension_name))
def build(self):
self._cmd('azdev extension build {}'.format(self.extension_name))
def check_extension_name(self):
extension_root_dir_name = self.extension_name
original_cwd = os.getcwd()
dist_dir = os.path.join(original_cwd, 'dist')
files = os.listdir(dist_dir)
logger.info(f"wheel files in the dist directory: {files}")
for f in files:
if f.endswith('.whl'):
NAME_REGEX = r'(.*)-\d+.\d+.\d+'
extension_name = re.findall(NAME_REGEX, f)[0]
extension_name = extension_name.replace('_', '-')
logger.info(f"extension name is: {extension_name}")
ext_file = os.path.join(dist_dir, f)
break
metadata = get_ext_metadata(dist_dir, ext_file, extension_name)
pretty_metadata = json.dumps(metadata, indent=2)
logger.info(f"metadata in the wheel file is: {pretty_metadata}")
shutil.rmtree(dist_dir)
if '_' in extension_root_dir_name:
raise ValueError(f"Underscores `_` are not allowed in the extension root directory, "
f"please change it to a hyphen `-`.")
if metadata['name'] != extension_name:
raise ValueError(f"The name {metadata['name']} in setup.py "
f"is not the same as the extension name {extension_name}! \n"
f"Please fix the name in setup.py!")
def contain_index_json(files):
return 'src/index.json' in files
def contain_extension_code(files):
with open('src/index.json', 'r') as fd:
current_extensions = json.loads(fd.read()).get("extensions")
current_extension_homes = set('src/{}'.format(name) for name in current_extensions)
for file in files:
if any([file.startswith(prefix) for prefix in current_extension_homes]):
return True
# for new added extensions
for file in files:
if 'src/' in file and os.path.isfile(file) and os.path.isdir(os.path.dirname(file)):
new_extension_home = os.path.dirname(file)
if os.path.isfile(os.path.join(new_extension_home, 'setup.py')):
return True
return False
def azdev_on_external_extension(index_json, azdev_type):
"""
Check if the modified metadata items in index.json refer to the extension in repo.
If not, az extension check on wheel. Otherwise skip it.
"""
public_extensions = json.loads(check_output('az extension list-available -d', shell=True))
with open(index_json, 'r') as fd:
current_extensions = json.loads(fd.read()).get("extensions")
def entry_equals_ignore_url(entry1, entry2):
"""Compare two entries ignoring downloadUrl field"""
entry1_copy = entry1.copy()
entry2_copy = entry2.copy()
entry1_copy.pop('downloadUrl', None)
entry2_copy.pop('downloadUrl', None)
return entry1_copy == entry2_copy
for name in current_extensions:
public_entries = public_extensions.get(name, [])
# Find modified entries by comparing without downloadUrl
modified_entries = []
for entry in current_extensions[name]:
is_modified = True
for public_entry in public_entries:
if entry_equals_ignore_url(entry, public_entry):
is_modified = False
break
if is_modified:
modified_entries.append(entry)
if not modified_entries:
continue
# check if source code exists, if so, skip
if os.path.isdir('src/{}'.format(name)):
continue
separator_line()
latest_entry = max(modified_entries, key=lambda c: Version(c['metadata']['version']))
az_extension = AzExtensionHelper(name)
az_extension.add_from_url(latest_entry['downloadUrl'])
azdev_extension = AzdevExtensionHelper(name)
if azdev_type in ['all', 'linter']:
azdev_extension.linter()
# TODO:
# azdev style support external extension
# azdev test support external extension
# azdev_extension.style()
logger.info('Checking service name for external extensions: %s', name)
service_name.check()
az_extension.remove()
def azdev_on_internal_extension(modified_files, azdev_type):
extension_names = set()
for f in modified_files:
src, name, *_ = f.split('/')
if os.path.isdir(os.path.join(src, name)):
extension_names.add(name)
if not extension_names:
separator_line()
logger.info('no extension source code modified, no extension needs to be checked')
for name in extension_names:
separator_line()
azdev_extension = AzdevExtensionHelper(name)
azdev_extension.add_from_code()
if azdev_type in ['all', 'linter']:
azdev_extension.linter()
azdev_extension.build()
azdev_extension.check_extension_name()
if azdev_type in ['all', 'style']:
try:
azdev_extension.style()
except CalledProcessError as e:
statement_msg = """
------------------- Please note -------------------
This task does not block the PR merge.
And it is recommended if you want to create a separate PR to fix these style issues.
CLI will modify it to force block PR merge on 2025.
---------------------- Thanks ----------------------
"""
logger.error(statement_msg)
exit(1)
logger.info('Checking service name for internal extensions: %s', name)
service_name.check()
azdev_extension.remove()
def main():
import argparse
parser = argparse.ArgumentParser(description='azdev linter and azdev style on modified extensions')
parser.add_argument('--type',
type=str,
help='Control whether azdev linter, azdev style, azdev test needs to be run. '
'Supported values: linter, style, test, all, all is the default.', default='all')
args = parser.parse_args()
azdev_type = args.type
logger.info('azdev type: %s', azdev_type)
modified_files = find_modified_files_against_master_branch()
if len(modified_files) == 1 and contain_index_json(modified_files):
# Scenario 1.
# This scenarios is for modify index.json only.
# If the modified metadata items refer to the extension code exits in this repo, PR is be created via Pipeline.
# If the modified metadata items refer to the extension code doesn't exist, PR is created from Service Team.
# We try to run azdev linter and azdev style on it.
azdev_on_external_extension(modified_files[0], azdev_type)
else:
# modified files contain more than one file
if contain_extension_code(modified_files):
# Scenario 2, we reject.
if contain_index_json(modified_files):
raise ModifiedFilesNotAllowedError()
azdev_on_internal_extension(modified_files, azdev_type)
else:
separator_line()
logger.info('no extension source code modified, no extension needs to be checked')
separator_line()
if __name__ == '__main__':
try:
main()
except ModifiedFilesNotAllowedError as e:
logger.error(e)
exit(1)