Skip to content

Commit 8e22834

Browse files
Add generate and verify commands (#542)
* feature: add commands for generate/verify json * fix: formatting error in .rst * feature: add version bump and history msg * fix: remove python 3.9 and add 3.13 to test matrix * fix: readme formatting * readme * readme
1 parent 363af3b commit 8e22834

File tree

10 files changed

+280
-13
lines changed

10 files changed

+280
-13
lines changed

HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
33
Release History
44
===============
5+
0.2.9
6+
+++++
7+
* `azdev latest-index`: Add `generate` and `verify` commands to manage Azure CLI packaged latest indices (`commandIndex.latest.json`, `helpIndex.latest.json`) with CI-friendly verify exit behavior.
8+
59
0.2.8
610
++++++
711
* Pin pip to 25.2 as pip 25.3 remove support for the legacy setup.py develop editable method in setuptools editable installs; setuptools >= 64 is now required. (#11457)

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,25 @@ For instructions on manually writing the commands and tests, see more in
148148
149149
By default, test is running in `once` mode. If there are no corresponding recording files (in yaml format), it will run live tests and generate recording files. If recording files are found, the tests will be run in `playback` mode against the recording files. You can use `--live` to force a test run in `live` mode and regenerate the recording files.
150150
151+
## Latest packaged indices
152+
153+
Use azdev wrappers around Azure CLI's latest index generation script:
154+
155+
```
156+
azdev latest-index generate
157+
azdev latest-index verify
158+
```
159+
160+
You can pass an explicit Azure CLI checkout path when needed:
161+
162+
```
163+
azdev latest-index generate --cli /path/to/azure-cli
164+
azdev latest-index verify --repo /path/to/azure-cli
165+
```
166+
167+
`azdev latest-index verify` exits non-zero when generated output differs from the checked-in
168+
`commandIndex.latest.json` or `helpIndex.latest.json`, making it CI-friendly.
169+
151170
## Submitting a pull request to merge the code
152171

153172
1. After committing your code locally, push it to your forked repository:

README.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,49 @@ Setting up your development environment
6666

6767
This will launch the interactive setup process. To see non-interactive options run `azdev setup -h`.
6868

69+
Latest packaged indices
70+
+++++++++++++++++++++++
71+
72+
Use azdev wrappers around Azure CLI's latest index generation script:
73+
74+
::
75+
76+
azdev latest-index generate
77+
azdev latest-index verify
78+
79+
You can pass an explicit Azure CLI checkout path when needed:
80+
81+
::
82+
83+
azdev latest-index generate --cli /path/to/azure-cli
84+
azdev latest-index verify --repo /path/to/azure-cli
85+
86+
``azdev latest-index verify`` exits non-zero when generated output differs from checked-in
87+
``commandIndex.latest.json`` or ``helpIndex.latest.json``.
88+
89+
Common azdev commands
90+
+++++++++++++++++++++++++++++
91+
92+
This README is not an exhaustive command reference. For the complete command surface, use:
93+
94+
::
95+
96+
azdev --help
97+
azdev <group> --help
98+
99+
Frequently used commands include:
100+
101+
::
102+
103+
azdev setup
104+
azdev style <module-or-extension>
105+
azdev linter <module-or-extension>
106+
azdev test <module-or-extension>
107+
azdev extension add <extension-name>
108+
azdev extension build <extension-name>
109+
azdev latest-index generate
110+
azdev latest-index verify
111+
69112
Reporting issues and feedback
70113
+++++++++++++++++++++++++++++
71114

azdev/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# license information.
55
# -----------------------------------------------------------------------------
66

7-
__VERSION__ = '0.2.8'
7+
__VERSION__ = '0.2.9'

azdev/commands.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def operation_group(name):
8282
with CommandGroup(self, 'cli', operation_group('help')) as g:
8383
g.command('generate-docs', 'generate_cli_ref_docs')
8484

85+
with CommandGroup(self, 'latest-index', operation_group('latest_index')) as g:
86+
g.command('generate', 'generate_latest_index')
87+
g.command('verify', 'verify_latest_index')
88+
8589
with CommandGroup(self, 'extension', operation_group('help')) as g:
8690
g.command('generate-docs', 'generate_extension_ref_docs')
8791

azdev/help.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,34 @@
9999
short-summary: Verify the README and HISTORY files for each module so they format correctly on PyPI.
100100
"""
101101

102+
helps['latest-index'] = """
103+
short-summary: Generate or verify Azure CLI packaged latest index files.
104+
long-summary: >
105+
Wraps azure-cli's scripts/generate_latest_indices.py for deterministic, CI-friendly
106+
generation and verification of commandIndex.latest.json and helpIndex.latest.json.
107+
"""
108+
109+
helps['latest-index generate'] = """
110+
short-summary: Generate commandIndex.latest.json and helpIndex.latest.json in an Azure CLI repo.
111+
examples:
112+
- name: Generate latest index files using the configured Azure CLI repo.
113+
text: azdev latest-index generate
114+
115+
- name: Generate latest index files for an explicit repo checkout.
116+
text: azdev latest-index generate --cli /path/to/azure-cli
117+
"""
118+
119+
helps['latest-index verify'] = """
120+
short-summary: Verify latest index files are up-to-date.
121+
long-summary: Returns a non-zero exit code when generated content differs from checked-in files.
122+
examples:
123+
- name: Verify latest index files in CI.
124+
text: azdev latest-index verify
125+
126+
- name: Verify latest index files for an explicit repo checkout.
127+
text: azdev latest-index verify --repo /path/to/azure-cli
128+
"""
129+
102130

103131
helps['style'] = """
104132
short-summary: Check code style (pylint and PEP8).

azdev/operations/latest_index.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
import os
8+
import sys
9+
10+
from knack.util import CLIError
11+
12+
from azdev.utilities import display, heading, py_cmd
13+
from azdev.utilities.path import get_cli_repo_path
14+
15+
16+
_LATEST_INDEX_SCRIPT = os.path.join('scripts', 'generate_latest_indices.py')
17+
18+
19+
def _resolve_cli_repo_path(cli_path):
20+
if cli_path:
21+
resolved = os.path.abspath(os.path.expanduser(cli_path))
22+
else:
23+
resolved = get_cli_repo_path()
24+
25+
if not resolved or resolved == '_NONE_':
26+
raise CLIError('Azure CLI repo path is not configured. Specify `--cli` or run `azdev setup`.')
27+
28+
if not os.path.isdir(resolved):
29+
raise CLIError('Azure CLI repo path does not exist: {}'.format(resolved))
30+
31+
return resolved
32+
33+
34+
def _run_latest_index(mode, cli_path=None, profile='latest', all_profiles=False):
35+
if all_profiles:
36+
raise CLIError('`--all-profiles` is not supported yet. Use `--profile latest`.')
37+
38+
if profile != 'latest':
39+
raise CLIError("Unsupported profile '{}'. Only `latest` is currently supported.".format(profile))
40+
41+
repo_path = _resolve_cli_repo_path(cli_path)
42+
script_path = os.path.join(repo_path, _LATEST_INDEX_SCRIPT)
43+
if not os.path.isfile(script_path):
44+
raise CLIError('Unable to find azure-cli script: {}'.format(script_path))
45+
46+
heading('Latest Index: {}'.format(mode.capitalize()))
47+
display('Azure CLI repo: {}'.format(repo_path))
48+
49+
command = '{} {}'.format(_LATEST_INDEX_SCRIPT, mode)
50+
result = py_cmd(command, is_module=False, cwd=repo_path)
51+
52+
output = result.result
53+
if isinstance(output, bytes):
54+
output = output.decode('utf-8', errors='replace')
55+
if output:
56+
output = output.replace(
57+
'python scripts/generate_latest_indices.py generate',
58+
'azdev latest-index generate'
59+
)
60+
display(output)
61+
62+
if result.exit_code:
63+
sys.exit(result.exit_code)
64+
65+
66+
def generate_latest_index(cli_path=None, profile='latest', all_profiles=False):
67+
_run_latest_index('generate', cli_path=cli_path, profile=profile, all_profiles=all_profiles)
68+
69+
70+
def verify_latest_index(cli_path=None, profile='latest', all_profiles=False):
71+
_run_latest_index('verify', cli_path=cli_path, profile=profile, all_profiles=all_profiles)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
import unittest
8+
from unittest import mock
9+
import os
10+
11+
from knack.util import CLIError, CommandResultItem
12+
13+
from azdev.operations.latest_index import generate_latest_index, verify_latest_index
14+
15+
16+
class LatestIndexTestCase(unittest.TestCase):
17+
18+
@mock.patch('azdev.operations.latest_index.py_cmd')
19+
@mock.patch('azdev.operations.latest_index.os.path.isfile', return_value=True)
20+
@mock.patch('azdev.operations.latest_index.os.path.isdir', return_value=True)
21+
def test_generate_with_explicit_repo_path(self, _, __, mock_py_cmd):
22+
mock_py_cmd.return_value = CommandResultItem('generated', exit_code=0, error=None)
23+
24+
generate_latest_index(cli_path='/fake/azure-cli')
25+
26+
self.assertTrue(mock_py_cmd.called)
27+
command = mock_py_cmd.call_args.args[0]
28+
self.assertIn('generate_latest_indices.py generate', command)
29+
self.assertEqual(os.path.abspath('/fake/azure-cli'), mock_py_cmd.call_args.kwargs['cwd'])
30+
self.assertFalse(mock_py_cmd.call_args.kwargs['is_module'])
31+
32+
@mock.patch('azdev.operations.latest_index.py_cmd')
33+
@mock.patch('azdev.operations.latest_index.os.path.isfile', return_value=True)
34+
@mock.patch('azdev.operations.latest_index.os.path.isdir', return_value=True)
35+
@mock.patch('azdev.operations.latest_index.get_cli_repo_path', return_value='/configured/azure-cli')
36+
def test_verify_uses_configured_repo_path(self, _, __, ___, mock_py_cmd):
37+
mock_py_cmd.return_value = CommandResultItem('verified', exit_code=0, error=None)
38+
39+
verify_latest_index()
40+
41+
self.assertEqual('/configured/azure-cli', mock_py_cmd.call_args.kwargs['cwd'])
42+
43+
@mock.patch('azdev.operations.latest_index.py_cmd')
44+
@mock.patch('azdev.operations.latest_index.os.path.isfile', return_value=True)
45+
@mock.patch('azdev.operations.latest_index.os.path.isdir', return_value=True)
46+
def test_verify_non_zero_exit_is_propagated(self, _, __, mock_py_cmd):
47+
# simulate bytes output as returned by py_cmd on failure
48+
mock_py_cmd.return_value = CommandResultItem(b'stale\r\n', exit_code=1, error='mismatch')
49+
50+
with self.assertRaises(SystemExit) as ex:
51+
verify_latest_index(cli_path='/fake/azure-cli')
52+
53+
self.assertEqual(1, ex.exception.code)
54+
55+
def test_non_latest_profile_is_rejected(self):
56+
with self.assertRaises(CLIError):
57+
generate_latest_index(cli_path='/fake/azure-cli', profile='2019-03-01-hybrid')
58+
59+
def test_all_profiles_flag_is_rejected(self):
60+
with self.assertRaises(CLIError):
61+
verify_latest_index(cli_path='/fake/azure-cli', all_profiles=True)
62+
63+
@mock.patch('azdev.operations.latest_index.display')
64+
@mock.patch('azdev.operations.latest_index.py_cmd')
65+
@mock.patch('azdev.operations.latest_index.os.path.isfile', return_value=True)
66+
@mock.patch('azdev.operations.latest_index.os.path.isdir', return_value=True)
67+
def test_bytes_output_decoded_and_hint_replaced(self, _, __, mock_py_cmd, mock_display):
68+
raw = b'files are out of date\r\nRun:\r\n python scripts/generate_latest_indices.py generate\r\n'
69+
mock_py_cmd.return_value = CommandResultItem(raw, exit_code=1, error='mismatch')
70+
71+
with self.assertRaises(SystemExit):
72+
verify_latest_index(cli_path='/fake/azure-cli')
73+
74+
displayed = mock_display.call_args.args[0]
75+
self.assertNotIn("b'", displayed)
76+
self.assertNotIn('\\r\\n', displayed)
77+
self.assertNotIn('python scripts/generate_latest_indices.py generate', displayed)
78+
self.assertIn('azdev latest-index generate', displayed)

azdev/params.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,11 @@ def load_arguments(self, _):
282282
c.argument('no_tail', action='store_true', help='Skip tail when displaying as markdown.')
283283
c.argument('include_whl_extensions', action='store_true',
284284
help="Allow scanning on extensions installed by `az extension add --source xxx.whl`.")
285+
286+
with ArgumentsContext(self, 'latest-index') as c:
287+
c.argument('cli_path', options_list=['--cli', '--repo'],
288+
help='Path to an Azure CLI repo checkout. If omitted, use the path configured by `azdev setup`.')
289+
c.argument('profile', choices=['latest'], default='latest',
290+
help='Cloud profile to process. Only `latest` is currently supported.')
291+
c.argument('all_profiles', action='store_true',
292+
help='Not supported yet. Reserved for future multi-profile support.')

azure-pipelines-cli.yml

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,12 @@ jobs:
8282
name: ${{ variables.ubuntu_pool }}
8383
strategy:
8484
matrix:
85-
Python39:
86-
python.version: '3.9'
85+
Python310:
86+
python.version: '3.10'
8787
Python312:
8888
python.version: '3.12'
89+
Python313:
90+
python.version: '3.13'
8991
steps:
9092
- task: UsePythonVersion@0
9193
displayName: 'Use Python $(python.version)'
@@ -139,10 +141,12 @@ jobs:
139141
name: ${{ variables.ubuntu_pool }}
140142
strategy:
141143
matrix:
142-
Python39:
143-
python.version: '3.9'
144+
Python310:
145+
python.version: '3.10'
144146
Python312:
145147
python.version: '3.12'
148+
Python313:
149+
python.version: '3.13'
146150
steps:
147151
- task: DownloadPipelineArtifact@1
148152
displayName: 'Download Build'
@@ -171,10 +175,12 @@ jobs:
171175
name: ${{ variables.ubuntu_pool }}
172176
strategy:
173177
matrix:
174-
Python39:
175-
python.version: '3.9'
178+
Python310:
179+
python.version: '3.10'
176180
Python312:
177181
python.version: '3.12'
182+
Python313:
183+
python.version: '3.13'
178184
steps:
179185
- task: DownloadPipelineArtifact@1
180186
displayName: 'Download Build'
@@ -203,10 +209,12 @@ jobs:
203209
name: ${{ variables.ubuntu_pool }}
204210
strategy:
205211
matrix:
206-
Python39:
207-
python.version: '3.9'
212+
Python310:
213+
python.version: '3.10'
208214
Python312:
209215
python.version: '3.12'
216+
Python313:
217+
python.version: '3.13'
210218
steps:
211219
- task: DownloadPipelineArtifact@1
212220
displayName: 'Download Build'
@@ -235,10 +243,12 @@ jobs:
235243
name: ${{ variables.ubuntu_pool }}
236244
strategy:
237245
matrix:
238-
Python39:
239-
python.version: '3.9'
246+
Python310:
247+
python.version: '3.10'
240248
Python312:
241249
python.version: '3.12'
250+
Python313:
251+
python.version: '3.13'
242252
steps:
243253
- task: DownloadPipelineArtifact@1
244254
displayName: 'Download Build'
@@ -266,10 +276,12 @@ jobs:
266276
name: ${{ variables.ubuntu_pool }}
267277
strategy:
268278
matrix:
269-
Python39:
270-
python.version: '3.9'
279+
Python310:
280+
python.version: '3.10'
271281
Python312:
272282
python.version: '3.12'
283+
Python313:
284+
python.version: '3.13'
273285
steps:
274286
- task: DownloadPipelineArtifact@1
275287
displayName: 'Download Build'

0 commit comments

Comments
 (0)