azdev 0.1.84__tar.gz → 0.1.85__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {azdev-0.1.84 → azdev-0.1.85}/HISTORY.rst +5 -0
  2. {azdev-0.1.84/azdev.egg-info → azdev-0.1.85}/PKG-INFO +6 -1
  3. {azdev-0.1.84 → azdev-0.1.85}/azdev/__init__.py +1 -1
  4. {azdev-0.1.84 → azdev-0.1.85}/azdev/help.py +1 -1
  5. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/cmdcov.py +2 -2
  6. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/constant.py +18 -0
  7. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/linter.py +31 -1
  8. azdev-0.1.85/azdev/operations/linter/rules/command_group_rules.py +77 -0
  9. azdev-0.1.85/azdev/operations/linter/rules/command_rules.py +72 -0
  10. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rules/help_rules.py +3 -3
  11. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rules/parameter_rules.py +35 -0
  12. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/util.py +43 -0
  13. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/setup.py +113 -0
  14. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/__init__.py +2 -0
  15. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/git_util.py +36 -0
  16. {azdev-0.1.84 → azdev-0.1.85/azdev.egg-info}/PKG-INFO +6 -1
  17. azdev-0.1.84/azdev/operations/linter/rules/command_group_rules.py +0 -42
  18. azdev-0.1.84/azdev/operations/linter/rules/command_rules.py +0 -37
  19. {azdev-0.1.84 → azdev-0.1.85}/LICENSE +0 -0
  20. {azdev-0.1.84 → azdev-0.1.85}/MANIFEST.in +0 -0
  21. {azdev-0.1.84 → azdev-0.1.85}/README.md +0 -0
  22. {azdev-0.1.84 → azdev-0.1.85}/README.rst +0 -0
  23. {azdev-0.1.84 → azdev-0.1.85}/azdev/__main__.py +0 -0
  24. {azdev-0.1.84 → azdev-0.1.85}/azdev/commands.py +0 -0
  25. {azdev-0.1.84 → azdev-0.1.85}/azdev/completer.py +0 -0
  26. {azdev-0.1.84 → azdev-0.1.85}/azdev/config/__init__.py +0 -0
  27. {azdev-0.1.84 → azdev-0.1.85}/azdev/config/cli.flake8 +0 -0
  28. {azdev-0.1.84 → azdev-0.1.85}/azdev/config/cli_pylintrc +0 -0
  29. {azdev-0.1.84 → azdev-0.1.85}/azdev/config/ext.flake8 +0 -0
  30. {azdev-0.1.84 → azdev-0.1.85}/azdev/config/ext_pylintrc +0 -0
  31. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/HISTORY.rst +0 -0
  32. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/README.rst +0 -0
  33. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/_client_factory.py +0 -0
  34. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/_help.py +0 -0
  35. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/_params.py +0 -0
  36. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/_validators.py +0 -0
  37. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/azext_metadata.json +0 -0
  38. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/blank__init__.py +0 -0
  39. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/commands.py +0 -0
  40. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/custom.py +0 -0
  41. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/module__init__.py +0 -0
  42. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/pkg_declare__init__.py +0 -0
  43. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/setup.cfg +0 -0
  44. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/setup.py +0 -0
  45. {azdev-0.1.84 → azdev-0.1.85}/azdev/mod_templates/test_service_scenario.py +0 -0
  46. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/__init__.py +0 -0
  47. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/breaking_change/__init__.py +0 -0
  48. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/breaking_change/markdown_template.jinja2 +0 -0
  49. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/__init__.py +0 -0
  50. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/_macros.j2 +0 -0
  51. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/component.css +0 -0
  52. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/component.js +0 -0
  53. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/favicon.ico +0 -0
  54. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/index.j2 +0 -0
  55. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/index2.j2 +0 -0
  56. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/cmdcov/module.j2 +0 -0
  57. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/code_gen.py +0 -0
  58. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/command_change/__init__.py +0 -0
  59. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/command_change/custom.py +0 -0
  60. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/command_change/util.py +0 -0
  61. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/extensions/__init__.py +0 -0
  62. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/extensions/util.py +0 -0
  63. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/extensions/version_upgrade.py +0 -0
  64. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/help/__init__.py +0 -0
  65. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/help/refdoc/__init__.py +0 -0
  66. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/help/refdoc/conf.py +0 -0
  67. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/legal.py +0 -0
  68. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/__init__.py +0 -0
  69. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/pylint_checkers/__init__.py +0 -0
  70. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/pylint_checkers/show_command.py +0 -0
  71. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rule_decorators.py +0 -0
  72. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rules/__init__.py +0 -0
  73. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rules/ci_exclusions.yml +0 -0
  74. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rules/command_coverage_rules.py +0 -0
  75. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/linter/rules/linter_exclusions.yml +0 -0
  76. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/performance.py +0 -0
  77. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/pypi.py +0 -0
  78. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/python_sdk.py +0 -0
  79. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/regex.py +0 -0
  80. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/resource.py +0 -0
  81. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/secret.py +0 -0
  82. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/statistics/__init__.py +0 -0
  83. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/statistics/util.py +0 -0
  84. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/style.py +0 -0
  85. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/testtool/__init__.py +0 -0
  86. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/testtool/incremental_strategy.py +0 -0
  87. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/testtool/profile_context.py +0 -0
  88. {azdev-0.1.84 → azdev-0.1.85}/azdev/operations/testtool/pytest_runner.py +0 -0
  89. {azdev-0.1.84 → azdev-0.1.85}/azdev/params.py +0 -0
  90. {azdev-0.1.84 → azdev-0.1.85}/azdev/transformers.py +0 -0
  91. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/command.py +0 -0
  92. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/config.py +0 -0
  93. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/const.py +0 -0
  94. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/display.py +0 -0
  95. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/path.py +0 -0
  96. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/pypi.py +0 -0
  97. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/testing.py +0 -0
  98. {azdev-0.1.84 → azdev-0.1.85}/azdev/utilities/tools.py +0 -0
  99. {azdev-0.1.84 → azdev-0.1.85}/azdev.egg-info/SOURCES.txt +0 -0
  100. {azdev-0.1.84 → azdev-0.1.85}/azdev.egg-info/dependency_links.txt +0 -0
  101. {azdev-0.1.84 → azdev-0.1.85}/azdev.egg-info/entry_points.txt +0 -0
  102. {azdev-0.1.84 → azdev-0.1.85}/azdev.egg-info/requires.txt +0 -0
  103. {azdev-0.1.84 → azdev-0.1.85}/azdev.egg-info/top_level.txt +0 -0
  104. {azdev-0.1.84 → azdev-0.1.85}/pyproject.toml +0 -0
  105. {azdev-0.1.84 → azdev-0.1.85}/setup.cfg +0 -0
  106. {azdev-0.1.84 → azdev-0.1.85}/setup.py +0 -0
@@ -2,6 +2,11 @@
2
2
 
3
3
  Release History
4
4
  ===============
5
+ 0.1.85
6
+ ++++++
7
+ * `azdev setup`: Setup the upstream and enable .githooks for azure-cli and azure-cli-extensions repos
8
+ * `azdev linter`: Add `disallowed_html_tags` and `broken_site_link` detection in linter rule and set them as `Medium` for unblock CI pipeline temporarily
9
+
5
10
  0.1.84
6
11
  ++++++
7
12
  * `azdev generate-breaking-change-report`: Fix `azdev -h` error caused by global importing `azure.cli.core` in `breaking-change.py` module.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: azdev
3
- Version: 0.1.84
3
+ Version: 0.1.85
4
4
  Summary: Microsoft Azure CLI Developer Tools
5
5
  Home-page: https://github.com/Azure/azure-cli-dev-tools
6
6
  Author: Microsoft Corporation
@@ -148,6 +148,11 @@ License
148
148
 
149
149
  Release History
150
150
  ===============
151
+ 0.1.85
152
+ ++++++
153
+ * `azdev setup`: Setup the upstream and enable .githooks for azure-cli and azure-cli-extensions repos
154
+ * `azdev linter`: Add `disallowed_html_tags` and `broken_site_link` detection in linter rule and set them as `Medium` for unblock CI pipeline temporarily
155
+
151
156
  0.1.84
152
157
  ++++++
153
158
  * `azdev generate-breaking-change-report`: Fix `azdev -h` error caused by global importing `azure.cli.core` in `breaking-change.py` module.
@@ -4,4 +4,4 @@
4
4
  # license information.
5
5
  # -----------------------------------------------------------------------------
6
6
 
7
- __VERSION__ = '0.1.84'
7
+ __VERSION__ = '0.1.85'
@@ -160,7 +160,7 @@ helps['linter'] = """
160
160
  text: azdev linter --repo azure-cli --tgt upstream/master --src upstream/dev
161
161
  """
162
162
 
163
- helps['scan'] = """
163
+ helps['scan'] = r"""
164
164
  short-summary: Scan secrets for files or string
165
165
  long-summary: Check built-in scanning rules at https://github.com/microsoft/security-utilities/blob/main/GeneratedRegexPatterns/PreciselyClassifiedSecurityKeys.json
166
166
  examples:
@@ -239,8 +239,8 @@ class CmdcovManager:
239
239
  self.command_test_coverage['Total'][0] += count
240
240
  self.command_test_coverage['Total'][1] += len(self.all_untested_commands[module])
241
241
  self.command_test_coverage['Total'][2] = f'''{self.command_test_coverage["Total"][0] /
242
- (self.command_test_coverage["Total"][0] +
243
- self.command_test_coverage["Total"][1]):.3%}'''
242
+ (self.command_test_coverage["Total"][0] +
243
+ self.command_test_coverage["Total"][1]):.3%}'''
244
244
  logger.warning(self.command_test_coverage)
245
245
  return self.command_test_coverage
246
246
 
@@ -3,6 +3,7 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # -----------------------------------------------------------------------------
6
+ # pylint: disable=line-too-long
6
7
 
7
8
  ENCODING = 'utf-8'
8
9
 
@@ -79,6 +80,23 @@ EXCLUDE_MODULES = [
79
80
  'util'
80
81
  ]
81
82
 
83
+ # refer to doc: https://review.learn.microsoft.com/en-us/help/platform/metadata-taxonomies/allowed-html?branch=main
84
+ ALLOWED_HTML_TAG = [
85
+ "a", "address", "article", "b", "blockquote", "br", "button", "br /",
86
+ "caption", "center", "cite", "code", "col", "colgroup",
87
+ "dd", "del", "details", "div", "dl", "dt", "em", "figcaption", "figure", "form",
88
+ "h1", "h2", "h3", "h4", "head", "hr",
89
+ "i", "iframe", "image", "img", "input", "ins", "kbd",
90
+ "label", "li", "nav", "nobr", "ol", "p", "pre", "rgn",
91
+ "s", "section", "source", "span", "strike", "strong", "sub", "summary", "sup",
92
+ "table", "tbody", "td", "tfoot", "th", "thead", "tr",
93
+ "u", "ul", "wbr"
94
+ ]
95
+
96
+ DISALLOWED_HTML_TAG_RULE_LINK = "https://review.learn.microsoft.com/en-us/help/platform/validation-ref/disallowed-html-tag?branch=main"
97
+
98
+ BROKEN_LINK_RULE_LINK = "https://review.learn.microsoft.com/en-us/help/platform/validation-ref/other-site-link-broken?branch=main"
99
+
82
100
  GLOBAL_EXCLUDE_COMMANDS = ['wait']
83
101
 
84
102
  EXCLUDE_COMMANDS = {
@@ -20,7 +20,7 @@ from azdev.operations.regex import (
20
20
  search_argument_context,
21
21
  search_command,
22
22
  search_command_group)
23
- from azdev.utilities import diff_branches_detail
23
+ from azdev.utilities import diff_branches_detail, diff_branch_file_patch
24
24
  from azdev.utilities.path import get_cli_repo_path, get_ext_repo_paths
25
25
  from .util import share_element, exclude_commands, LinterError
26
26
 
@@ -63,6 +63,8 @@ class Linter: # pylint: disable=too-many-public-methods, too-many-instance-attr
63
63
  self.git_target = git_target
64
64
  self.git_repo = git_repo
65
65
  self.exclusions = exclusions
66
+ self.diffed_lines = set()
67
+ self._get_diffed_patches()
66
68
 
67
69
  @property
68
70
  def commands(self):
@@ -150,6 +152,17 @@ class Linter: # pylint: disable=too-many-public-methods, too-many-instance-attr
150
152
  def get_parameter_settings(self, command_name, parameter_name):
151
153
  return self.get_command_metadata(command_name).arguments.get(parameter_name).type.settings
152
154
 
155
+ def get_parameter_help_info(self, command_name, parameter_name):
156
+ options = self.get_parameter_options(command_name, parameter_name)
157
+ command_help = self._loaded_help.get(command_name, None)
158
+
159
+ if not command_help:
160
+ return None
161
+
162
+ parameter_helps = command_help.parameters
163
+ param_help = next((param for param in parameter_helps if share_element(options, param.name.split())), None)
164
+ return param_help
165
+
153
166
  def command_expired(self, command_name):
154
167
  deprecate_info = self._command_loader.command_table[command_name].deprecate_info
155
168
  if deprecate_info:
@@ -193,6 +206,10 @@ class Linter: # pylint: disable=too-many-public-methods, too-many-instance-attr
193
206
  return help_entry.short_summary or help_entry.long_summary
194
207
  return help_entry
195
208
 
209
+ def get_loaded_help_entry(self, entry):
210
+ help_entry = self._loaded_help.get(entry, None)
211
+ return help_entry
212
+
196
213
  def get_command_test_coverage(self):
197
214
  diff_index = diff_branches_detail(repo=self.git_repo, target=self.git_target, source=self.git_source)
198
215
  commands, _ = self._detect_new_command(diff_index)
@@ -354,6 +371,19 @@ class Linter: # pylint: disable=too-many-public-methods, too-many-instance-attr
354
371
  'Or add the parameter with missing_parameter_test_coverage rule in linter_exclusions.yml'])
355
372
  return exec_state, violations
356
373
 
374
+ def _get_diffed_patches(self):
375
+ if not self.git_source or not self.git_target or not self.git_repo:
376
+ return
377
+ diff_patches = diff_branch_file_patch(repo=self.git_repo, target=self.git_target, source=self.git_source)
378
+ for change in diff_patches:
379
+ patch = change.diff.decode("utf-8")
380
+ added_lines = [line for line in patch.splitlines() if line.startswith('+') and not line.startswith('+++')]
381
+ self.diffed_lines |= set(added_lines)
382
+ if added_lines:
383
+ _logger.info("Changes in file '%s':", change.a_path)
384
+ for line in added_lines:
385
+ _logger.info(line)
386
+
357
387
 
358
388
  # pylint: disable=too-many-instance-attributes
359
389
  class LinterManager:
@@ -0,0 +1,77 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # -----------------------------------------------------------------------------
6
+ # pylint: disable=duplicate-code
7
+
8
+ from azdev.operations.constant import DISALLOWED_HTML_TAG_RULE_LINK
9
+ from ..rule_decorators import CommandGroupRule
10
+ from ..linter import RuleError, LinterSeverity
11
+ from ..util import has_illegal_html_tag, has_broken_site_links
12
+
13
+
14
+ @CommandGroupRule(LinterSeverity.HIGH)
15
+ def missing_group_help(linter, command_group_name):
16
+ if not linter.get_command_group_help(command_group_name) and not linter.command_group_expired(command_group_name) \
17
+ and command_group_name != '':
18
+ raise RuleError('Missing help')
19
+
20
+
21
+ @CommandGroupRule(LinterSeverity.HIGH)
22
+ def expired_command_group(linter, command_group_name):
23
+ if linter.command_group_expired(command_group_name):
24
+ raise RuleError("Deprecated command group is expired and should be removed.")
25
+
26
+
27
+ @CommandGroupRule(LinterSeverity.MEDIUM)
28
+ def require_wait_command_if_no_wait(linter, command_group_name):
29
+ # If any command within a command group or subgroup exposes the --no-wait parameter,
30
+ # the wait command should be exposed.
31
+
32
+ # find commands under this group. A command in this group has one more token than the group name.
33
+ group_command_names = [cmd for cmd in linter.commands if cmd.startswith(command_group_name) and
34
+ len(cmd.split()) == len(command_group_name.split()) + 1]
35
+
36
+ # if one of the commands in this group ends with wait we are good
37
+ for cmd in group_command_names:
38
+ cmds = cmd.split()
39
+ if cmds[-1].lower() == "wait":
40
+ return
41
+
42
+ # otherwise there is no wait command. If a command in this group has --no-wait, then error out.
43
+ for cmd in group_command_names:
44
+ if linter.get_command_metadata(cmd).supports_no_wait:
45
+ raise RuleError("Group does not have a 'wait' command, yet '{}' exposes '--no-wait'".format(cmd))
46
+
47
+
48
+ @CommandGroupRule(LinterSeverity.MEDIUM)
49
+ def disallowed_html_tag_from_command_group(linter, command_group_name):
50
+ if command_group_name == '' or not linter.get_loaded_help_entry(command_group_name):
51
+ return
52
+ help_entry = linter.get_loaded_help_entry(command_group_name)
53
+ if help_entry.short_summary and (disallowed_tags := has_illegal_html_tag(help_entry.short_summary,
54
+ linter.diffed_lines)):
55
+ raise RuleError("Disallowed html tags {} in short summary. "
56
+ "If the content is a placeholder, please remove <> or wrap it with backtick. "
57
+ "For more info please refer to: {}".format(disallowed_tags,
58
+ DISALLOWED_HTML_TAG_RULE_LINK))
59
+ if help_entry.long_summary and (disallowed_tags := has_illegal_html_tag(help_entry.long_summary,
60
+ linter.diffed_lines)):
61
+ raise RuleError("Disallowed html tags {} in long summary. "
62
+ "If content is a placeholder, please remove <> or wrap it with backtick. "
63
+ "For more info please refer to: {}".format(disallowed_tags,
64
+ DISALLOWED_HTML_TAG_RULE_LINK))
65
+
66
+
67
+ @CommandGroupRule(LinterSeverity.MEDIUM)
68
+ def broken_site_link_from_command_group(linter, command_group_name):
69
+ if command_group_name == '' or not linter.get_loaded_help_entry(command_group_name):
70
+ return
71
+ help_entry = linter.get_loaded_help_entry(command_group_name)
72
+ if help_entry.short_summary and (broken_links := has_broken_site_links(help_entry.short_summary)):
73
+ raise RuleError("Broken links {} in short summary. "
74
+ "If link is an example, please wrap it with backtick. ".format(broken_links))
75
+ if help_entry.long_summary and (broken_links := has_broken_site_links(help_entry.long_summary)):
76
+ raise RuleError("Broken links {} in long summary. "
77
+ "If link is an example, please wrap it with backtick. ".format(broken_links))
@@ -0,0 +1,72 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # -----------------------------------------------------------------------------
6
+ # pylint: disable=duplicate-code
7
+
8
+ from azdev.operations.constant import DISALLOWED_HTML_TAG_RULE_LINK
9
+ from ..rule_decorators import CommandRule
10
+ from ..linter import RuleError, LinterSeverity
11
+ from ..util import has_illegal_html_tag, has_broken_site_links
12
+
13
+
14
+ @CommandRule(LinterSeverity.HIGH)
15
+ def missing_command_help(linter, command_name):
16
+ if not linter.get_command_help(command_name) and not linter.command_expired(command_name):
17
+ raise RuleError('Missing help')
18
+
19
+
20
+ @CommandRule(LinterSeverity.HIGH)
21
+ def no_ids_for_list_commands(linter, command_name):
22
+ if command_name.split()[-1] == 'list' and 'ids' in linter.get_command_parameters(command_name):
23
+ raise RuleError('List commands should not expose --ids argument')
24
+
25
+
26
+ @CommandRule(LinterSeverity.HIGH)
27
+ def expired_command(linter, command_name):
28
+ if linter.command_expired(command_name):
29
+ raise RuleError('Deprecated command is expired and should be removed.')
30
+
31
+
32
+ @CommandRule(LinterSeverity.LOW)
33
+ def group_delete_commands_should_confirm(linter, command_name):
34
+ # We cannot detect from cmd table etc whether a delete command deletes a collection, group or set of resources.
35
+ # so warn users for every delete command.
36
+
37
+ if command_name.split()[-1].lower() == "delete":
38
+ if 'yes' not in linter.get_command_parameters(command_name):
39
+ raise RuleError("If this command deletes a collection, or group of resources. "
40
+ "Please make sure to ask for confirmation.")
41
+
42
+
43
+ @CommandRule(LinterSeverity.MEDIUM)
44
+ def disallowed_html_tag_from_command(linter, command_name):
45
+ if command_name == '' or not linter.get_loaded_help_entry(command_name):
46
+ return
47
+ help_entry = linter.get_loaded_help_entry(command_name)
48
+ if help_entry.short_summary and (disallowed_tags := has_illegal_html_tag(help_entry.short_summary,
49
+ linter.diffed_lines)):
50
+ raise RuleError("Disallowed html tags {} in short summary. "
51
+ "If the content is a placeholder, please remove <> or wrap it with backtick. "
52
+ "For more info please refer to: {}".format(disallowed_tags,
53
+ DISALLOWED_HTML_TAG_RULE_LINK))
54
+ if help_entry.long_summary and (disallowed_tags := has_illegal_html_tag(help_entry.long_summary,
55
+ linter.diffed_lines)):
56
+ raise RuleError("Disallowed html tags {} in long summary. "
57
+ "If content is a placeholder, please remove <> or wrap it with backtick. "
58
+ "For more info please refer to: {}".format(disallowed_tags,
59
+ DISALLOWED_HTML_TAG_RULE_LINK))
60
+
61
+
62
+ @CommandRule(LinterSeverity.MEDIUM)
63
+ def broken_site_link_from_command(linter, command_name):
64
+ if command_name == '' or not linter.get_loaded_help_entry(command_name):
65
+ return
66
+ help_entry = linter.get_loaded_help_entry(command_name)
67
+ if help_entry.short_summary and (broken_links := has_broken_site_links(help_entry.short_summary)):
68
+ raise RuleError("Broken links {} in short summary. "
69
+ "If link is an example, please wrap it with backtick. ".format(broken_links))
70
+ if help_entry.long_summary and (broken_links := has_broken_site_links(help_entry.long_summary)):
71
+ raise RuleError("Broken links {} in long summary. "
72
+ "If link is an example, please wrap it with backtick. ".format(broken_links))
@@ -20,10 +20,10 @@ from ..util import LinterError
20
20
 
21
21
 
22
22
  # 'az' space then repeating runs of quoted tokens or non quoted characters
23
- _az_pattern = 'az\s*' + '(([^\"\'])*|' + '((\"[^\"]*\"\s*)|(\'[^\']*\'\s*))' + ')' # noqa: W605
23
+ _az_pattern = r'az\s*' + '(([^\"\'])*|' + r'((\"[^\"]*\"\s*)|(\'[^\']*\'\s*))' + ')'
24
24
  # match the two types of command substitutions
25
- _CMD_SUB_1 = re.compile("\$\(\s*" + "(" + _az_pattern + ")" + "\)") # noqa: W605
26
- _CMD_SUB_2 = re.compile("`\s*" + "(" + _az_pattern + ")" + "`") # noqa: W605
25
+ _CMD_SUB_1 = re.compile(r"\$\(\s*" + "(" + _az_pattern + ")" + r"\)")
26
+ _CMD_SUB_2 = re.compile(r"`\s*" + "(" + _az_pattern + ")" + "`")
27
27
 
28
28
  logger = get_logger(__name__)
29
29
 
@@ -6,8 +6,10 @@
6
6
 
7
7
  from knack.deprecation import Deprecated
8
8
 
9
+ from azdev.operations.constant import DISALLOWED_HTML_TAG_RULE_LINK
9
10
  from ..rule_decorators import ParameterRule
10
11
  from ..linter import RuleError, LinterSeverity
12
+ from ..util import has_illegal_html_tag, has_broken_site_links
11
13
 
12
14
 
13
15
  @ParameterRule(LinterSeverity.HIGH)
@@ -170,3 +172,36 @@ def option_should_not_contain_under_score(linter, command_name, parameter_name):
170
172
  return
171
173
  if '_' in option:
172
174
  raise RuleError("Argument's option {} contains '_' which should be '-' instead.".format(option))
175
+
176
+
177
+ @ParameterRule(LinterSeverity.MEDIUM)
178
+ def disallowed_html_tag_from_parameter(linter, command_name, parameter_name):
179
+ if linter.command_expired(command_name) or not linter.get_parameter_help_info(command_name, parameter_name):
180
+ return
181
+ help_entry = linter.get_parameter_help_info(command_name, parameter_name)
182
+ if help_entry.short_summary and (disallowed_tags := has_illegal_html_tag(help_entry.short_summary,
183
+ linter.diffed_lines)):
184
+ raise RuleError("Disallowed html tags {} in short summary. "
185
+ "If the content is a placeholder, please remove <> or wrap it with backtick. "
186
+ "For more info please refer to: {}".format(disallowed_tags,
187
+ DISALLOWED_HTML_TAG_RULE_LINK))
188
+
189
+ if help_entry.long_summary and (disallowed_tags := has_illegal_html_tag(help_entry.long_summary,
190
+ linter.diffed_lines)):
191
+ raise RuleError("Disallowed html tags {} in long summary. "
192
+ "If content is a placeholder, please remove <> or wrap it with backtick. "
193
+ "For more info please refer to: {}".format(disallowed_tags,
194
+ DISALLOWED_HTML_TAG_RULE_LINK))
195
+
196
+
197
+ @ParameterRule(LinterSeverity.MEDIUM)
198
+ def broken_site_link_from_parameter(linter, command_name, parameter_name):
199
+ if linter.command_expired(command_name) or not linter.get_parameter_help_info(command_name, parameter_name):
200
+ return
201
+ help_entry = linter.get_parameter_help_info(command_name, parameter_name)
202
+ if help_entry.short_summary and (broken_links := has_broken_site_links(help_entry.short_summary)):
203
+ raise RuleError("Broken links {} in short summary. "
204
+ "If link is an example, please wrap it with backtick. ".format(broken_links))
205
+ if help_entry.long_summary and (broken_links := has_broken_site_links(help_entry.long_summary)):
206
+ raise RuleError("Broken links {} in long summary. "
207
+ "If link is an example, please wrap it with backtick. ".format(broken_links))
@@ -6,10 +6,12 @@
6
6
 
7
7
  import copy
8
8
  import re
9
+ import requests
9
10
 
10
11
  from knack.log import get_logger
11
12
 
12
13
  from azdev.utilities import get_name_index
14
+ from azdev.operations.constant import ALLOWED_HTML_TAG
13
15
 
14
16
 
15
17
  logger = get_logger(__name__)
@@ -17,6 +19,12 @@ logger = get_logger(__name__)
17
19
 
18
20
  _LOADER_CLS_RE = re.compile('.*azure/cli/command_modules/(?P<module>[^/]*)/__init__.*')
19
21
 
22
+ # add html tag extraction for <abd>, <lun1>, <edge zone>
23
+ # html tag search for <os_des>, <lun1_des> is enabled in cli ci but skipped in doc build cause internal issue
24
+ _HTML_TAG_RE = re.compile(r'<([^\n>]+)>')
25
+
26
+ _HTTP_LINK_RE = re.compile(r'(?<!`)(https?://[^\s`]+)(?!`)')
27
+
20
28
 
21
29
  def filter_modules(command_loader, help_file_entries, modules=None, include_whl_extensions=False):
22
30
  """ Modify the command table and help entries to only include certain modules/extensions.
@@ -112,3 +120,38 @@ class LinterError(Exception):
112
120
  Exception thrown by linter for non rule violation reasons
113
121
  """
114
122
  pass # pylint: disable=unnecessary-pass
123
+
124
+
125
+ # pylint: disable=line-too-long
126
+ def has_illegal_html_tag(help_message, filtered_lines=None):
127
+ """
128
+ Detect those content wrapped with <> but illegal html tag.
129
+ Refer to rule doc: https://review.learn.microsoft.com/en-us/help/platform/validation-ref/disallowed-html-tag?branch=main
130
+ """
131
+ html_matches = re.findall(_HTML_TAG_RE, help_message)
132
+ unbackticked_matches = [match for match in html_matches if not re.search(r'`[^`]*' + re.escape('<' + match + '>') + r'[^`]*`', help_message)]
133
+ disallowed_html_tags = set(unbackticked_matches) - set(ALLOWED_HTML_TAG)
134
+ if filtered_lines:
135
+ disallowed_html_tags = [s for s in disallowed_html_tags if any(('<' + s + '>') in diff_line for diff_line in filtered_lines)]
136
+ return ['<' + s + '>' for s in disallowed_html_tags]
137
+
138
+
139
+ def has_broken_site_links(help_message, filtered_lines=None):
140
+ """
141
+ Detect broken link in help message.
142
+ Refer to rule doc: https://review.learn.microsoft.com/en-us/help/platform/validation-ref/other-site-link-broken?branch=main
143
+ """
144
+ urls = re.findall(_HTTP_LINK_RE, help_message)
145
+ invalid_urls = []
146
+
147
+ for url in urls:
148
+ url = re.sub(r'[.")\'\s]*$', '', url)
149
+ try:
150
+ response = requests.get(url, timeout=5)
151
+ if response.status_code != 200:
152
+ invalid_urls.append(url)
153
+ except requests.exceptions.RequestException:
154
+ invalid_urls.append(url)
155
+ if filtered_lines:
156
+ invalid_urls = [s for s in invalid_urls if any(s in diff_line for diff_line in filtered_lines)]
157
+ return invalid_urls
@@ -5,6 +5,7 @@
5
5
  # -----------------------------------------------------------------------------
6
6
 
7
7
  import os
8
+ import subprocess
8
9
  from shutil import copytree, rmtree
9
10
  import time
10
11
 
@@ -256,6 +257,111 @@ def _interactive_setup():
256
257
  raise CLIError('Installation aborted.')
257
258
 
258
259
 
260
+ def _setup_azure_cli_repo(cli_path):
261
+ if cli_path and cli_path != 'EDGE':
262
+ # Store original directory
263
+ original_dir = os.getcwd()
264
+ try:
265
+ # Change to CLI repo root directory
266
+ os.chdir(cli_path)
267
+
268
+ display(f"\nSetting up Azure CLI repo: {cli_path}\n")
269
+ # Change git hooks path
270
+ _change_git_hooks_path(cli_path)
271
+
272
+ # Check existing remotes
273
+ remotes = subprocess.check_output(['git', 'remote', '-v'], text=True)
274
+
275
+ # If upstream already exists, nothing to do
276
+ if 'upstream' in remotes:
277
+ return
278
+
279
+ # Check origin remote URL
280
+ origin_url = None
281
+ for line in remotes.splitlines():
282
+ if line.startswith('origin') and '(fetch)' in line:
283
+ origin_url = line.split()[1]
284
+ break
285
+
286
+ # Only add upstream if origin is an azure-cli fork
287
+ if origin_url and origin_url.endswith('/azure-cli.git'):
288
+ upstream_url = 'https://github.com/Azure/azure-cli.git'
289
+ subprocess.check_call(['git', 'remote', 'add', 'upstream', upstream_url])
290
+ display(f"Added upstream remote: {upstream_url}")
291
+ # fetch the upstream/dev branch
292
+ subprocess.check_call(['git', 'fetch', 'upstream', 'dev'])
293
+ display(f"Fetched upstream/dev branch for CLI in {cli_path}")
294
+ except subprocess.CalledProcessError as e:
295
+ logger.warning("Failed to add upstream remote: %s", str(e))
296
+ finally:
297
+ # Always return to original directory
298
+ os.chdir(original_dir)
299
+
300
+
301
+ def _setup_azure_cli_extension_repo(ext_repo_path):
302
+ if not ext_repo_path:
303
+ return
304
+
305
+ try:
306
+ # Handle both single path and list of paths
307
+ repo_paths = ext_repo_path if isinstance(ext_repo_path, list) else [ext_repo_path]
308
+
309
+ # Iterate over all repository paths
310
+ for repo_path in repo_paths:
311
+ _setup_single_extension_repo(repo_path)
312
+
313
+ except subprocess.CalledProcessError as e:
314
+ logger.warning("Failed to add upstream remote for extensions: %s", str(e))
315
+
316
+
317
+ def _setup_single_extension_repo(repo_path):
318
+ # Store original directory
319
+ original_dir = os.getcwd()
320
+
321
+ try:
322
+ # Change to extension repo root directory
323
+ os.chdir(repo_path)
324
+
325
+ display(f"\nSetting up Azure CLI extension repo: {repo_path}\n")
326
+ # Change git hooks path
327
+ _change_git_hooks_path(repo_path)
328
+
329
+ # Check existing remotes
330
+ remotes = subprocess.check_output(['git', 'remote', '-v'], text=True)
331
+
332
+ # If upstream already exists, return
333
+ if 'upstream' in remotes:
334
+ return
335
+
336
+ # Check origin remote URL
337
+ origin_url = None
338
+ for line in remotes.splitlines():
339
+ if line.startswith('origin') and '(fetch)' in line:
340
+ origin_url = line.split()[1]
341
+ break
342
+
343
+ # Only add upstream if origin is an azure-cli-extensions fork
344
+ if origin_url and origin_url.endswith('/azure-cli-extensions.git'):
345
+ upstream_url = 'https://github.com/Azure/azure-cli-extensions.git'
346
+ subprocess.check_call(['git', 'remote', 'add', 'upstream', upstream_url])
347
+ display(f"Added upstream remote for extensions in {repo_path}: {upstream_url}")
348
+ # fetch the upstream/main branch
349
+ subprocess.check_call(['git', 'fetch', 'upstream', 'main'])
350
+ display(f"Fetched upstream/main branch for extensions in {repo_path}")
351
+
352
+ finally:
353
+ # Always return to original directory
354
+ os.chdir(original_dir)
355
+
356
+
357
+ def _change_git_hooks_path(repo_path):
358
+ # if .githooks folder exists in the repo folder, change the git config to use the .githooks folder in the repo
359
+ githooks_path = os.path.join(repo_path, '.githooks')
360
+ if os.path.exists(githooks_path):
361
+ subprocess.check_call(['git', 'config', 'core.hooksPath', githooks_path])
362
+ display(f"Changed git hooks path to {githooks_path}")
363
+
364
+
259
365
  def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None):
260
366
 
261
367
  require_virtual_env()
@@ -319,6 +425,13 @@ def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None):
319
425
  config.set_value('ext', 'repo_paths', dev_sources if dev_sources else '_NONE_')
320
426
  config.set_value('cli', 'repo_path', cli_path if cli_path else '_NONE_')
321
427
 
428
+ # Add upstreams for CLI and extensions repos if they are forks
429
+ if cli_path:
430
+ _setup_azure_cli_repo(cli_path)
431
+
432
+ if ext_repo_path:
433
+ _setup_azure_cli_extension_repo(ext_repo_path)
434
+
322
435
  # install packages
323
436
  subheading('Installing packages')
324
437
 
@@ -35,6 +35,7 @@ from .display import (
35
35
  from .git_util import (
36
36
  diff_branches,
37
37
  filter_by_git_diff,
38
+ diff_branch_file_patch,
38
39
  diff_branches_detail
39
40
  )
40
41
  from .path import (
@@ -94,5 +95,6 @@ __all__ = [
94
95
  'require_virtual_env',
95
96
  'require_azure_cli',
96
97
  'diff_branches_detail',
98
+ 'diff_branch_file_patch',
97
99
  'calc_selected_mod_names',
98
100
  ]
@@ -127,3 +127,39 @@ def diff_branches_detail(repo, target, source):
127
127
 
128
128
  diff_index = target_commit.diff(source_commit)
129
129
  return diff_index
130
+
131
+
132
+ def diff_branch_file_patch(repo, target, source):
133
+ """ Returns compare results of files that have changed in a given repo between two branches.
134
+ Only focus on these files: _params.py, commands.py, test_*.py """
135
+ try:
136
+ import git # pylint: disable=unused-import,unused-variable
137
+ import git.exc as git_exc
138
+ import gitdb
139
+ except ImportError as ex:
140
+ raise CLIError(ex)
141
+
142
+ from git import Repo
143
+ try:
144
+ git_repo = Repo(repo)
145
+ except (git_exc.NoSuchPathError, git_exc.InvalidGitRepositoryError):
146
+ raise CLIError('invalid git repo: {}'.format(repo))
147
+
148
+ def get_commit(branch):
149
+ try:
150
+ return git_repo.commit(branch)
151
+ except gitdb.exc.BadName:
152
+ raise CLIError('usage error, invalid branch: {}'.format(branch))
153
+
154
+ if source:
155
+ source_commit = get_commit(source)
156
+ else:
157
+ source_commit = git_repo.head.commit
158
+ target_commit = get_commit(target)
159
+
160
+ logger.info('Filtering down to modules which have changed based on:')
161
+ logger.info('cd %s', repo)
162
+ logger.info('git --no-pager diff %s..%s --name-only -- .\n', target_commit, source_commit)
163
+
164
+ diff_index = target_commit.diff(source_commit, create_patch=True)
165
+ return diff_index
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: azdev
3
- Version: 0.1.84
3
+ Version: 0.1.85
4
4
  Summary: Microsoft Azure CLI Developer Tools
5
5
  Home-page: https://github.com/Azure/azure-cli-dev-tools
6
6
  Author: Microsoft Corporation
@@ -148,6 +148,11 @@ License
148
148
 
149
149
  Release History
150
150
  ===============
151
+ 0.1.85
152
+ ++++++
153
+ * `azdev setup`: Setup the upstream and enable .githooks for azure-cli and azure-cli-extensions repos
154
+ * `azdev linter`: Add `disallowed_html_tags` and `broken_site_link` detection in linter rule and set them as `Medium` for unblock CI pipeline temporarily
155
+
151
156
  0.1.84
152
157
  ++++++
153
158
  * `azdev generate-breaking-change-report`: Fix `azdev -h` error caused by global importing `azure.cli.core` in `breaking-change.py` module.
@@ -1,42 +0,0 @@
1
- # -----------------------------------------------------------------------------
2
- # Copyright (c) Microsoft Corporation. All rights reserved.
3
- # Licensed under the MIT License. See License.txt in the project root for
4
- # license information.
5
- # -----------------------------------------------------------------------------
6
-
7
- from ..rule_decorators import CommandGroupRule
8
- from ..linter import RuleError, LinterSeverity
9
-
10
-
11
- @CommandGroupRule(LinterSeverity.HIGH)
12
- def missing_group_help(linter, command_group_name):
13
- if not linter.get_command_group_help(command_group_name) and not linter.command_group_expired(command_group_name) \
14
- and command_group_name != '':
15
- raise RuleError('Missing help')
16
-
17
-
18
- @CommandGroupRule(LinterSeverity.HIGH)
19
- def expired_command_group(linter, command_group_name):
20
- if linter.command_group_expired(command_group_name):
21
- raise RuleError("Deprecated command group is expired and should be removed.")
22
-
23
-
24
- @CommandGroupRule(LinterSeverity.MEDIUM)
25
- def require_wait_command_if_no_wait(linter, command_group_name):
26
- # If any command within a command group or subgroup exposes the --no-wait parameter,
27
- # the wait command should be exposed.
28
-
29
- # find commands under this group. A command in this group has one more token than the group name.
30
- group_command_names = [cmd for cmd in linter.commands if cmd.startswith(command_group_name) and
31
- len(cmd.split()) == len(command_group_name.split()) + 1]
32
-
33
- # if one of the commands in this group ends with wait we are good
34
- for cmd in group_command_names:
35
- cmds = cmd.split()
36
- if cmds[-1].lower() == "wait":
37
- return
38
-
39
- # otherwise there is no wait command. If a command in this group has --no-wait, then error out.
40
- for cmd in group_command_names:
41
- if linter.get_command_metadata(cmd).supports_no_wait:
42
- raise RuleError("Group does not have a 'wait' command, yet '{}' exposes '--no-wait'".format(cmd))
@@ -1,37 +0,0 @@
1
- # -----------------------------------------------------------------------------
2
- # Copyright (c) Microsoft Corporation. All rights reserved.
3
- # Licensed under the MIT License. See License.txt in the project root for
4
- # license information.
5
- # -----------------------------------------------------------------------------
6
-
7
- from ..rule_decorators import CommandRule
8
- from ..linter import RuleError, LinterSeverity
9
-
10
-
11
- @CommandRule(LinterSeverity.HIGH)
12
- def missing_command_help(linter, command_name):
13
- if not linter.get_command_help(command_name) and not linter.command_expired(command_name):
14
- raise RuleError('Missing help')
15
-
16
-
17
- @CommandRule(LinterSeverity.HIGH)
18
- def no_ids_for_list_commands(linter, command_name):
19
- if command_name.split()[-1] == 'list' and 'ids' in linter.get_command_parameters(command_name):
20
- raise RuleError('List commands should not expose --ids argument')
21
-
22
-
23
- @CommandRule(LinterSeverity.HIGH)
24
- def expired_command(linter, command_name):
25
- if linter.command_expired(command_name):
26
- raise RuleError('Deprecated command is expired and should be removed.')
27
-
28
-
29
- @CommandRule(LinterSeverity.LOW)
30
- def group_delete_commands_should_confirm(linter, command_name):
31
- # We cannot detect from cmd table etc whether a delete command deletes a collection, group or set of resources.
32
- # so warn users for every delete command.
33
-
34
- if command_name.split()[-1].lower() == "delete":
35
- if 'yes' not in linter.get_command_parameters(command_name):
36
- raise RuleError("If this command deletes a collection, or group of resources. "
37
- "Please make sure to ask for confirmation.")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes