Skip to content

Commit 74611f5

Browse files
committed
Support skip-validation names and integer values
1 parent 3bbee5c commit 74611f5

5 files changed

Lines changed: 212 additions & 3 deletions

File tree

azure-devops/azext_devops/dev/migration/arguments.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ def load_migration_arguments(self, _):
3131
context.argument('agent_pool', options_list='--agent-pool',
3232
help='Agent pool name to use for migration work.')
3333
context.argument('skip_validation', options_list='--skip-validation',
34-
help='Comma-separated list of validation policies to skip (e.g. MaxFileSize,ActivePullRequestCount).')
34+
help='Validation policies to skip. Accepts either a comma-separated list of '
35+
'policy names (for example, AgentPoolExists,MaxRepoSize) or a non-negative '
36+
'integer bitmask.')
3537

3638
with self.argument_context('devops migrations cutover set') as context:
3739
context.argument('cutover_date', options_list='--date',

azure-devops/azext_devops/dev/migration/migration.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@
1717

1818
API_VERSION = '7.2-preview'
1919
MIGRATIONS_API_PATH = '/_apis/elm/migrations'
20+
_SKIP_VALIDATION_POLICIES = {
21+
'none': 0,
22+
'activepullrequestcount': 1,
23+
'pullrequestdeltasize': 2,
24+
'agentpoolexists': 4,
25+
'maxfilesize': 8,
26+
'maxpullrequestsize': 16,
27+
'maxpushpacksize': 32,
28+
'maxreferencenamelength': 64,
29+
'maxreposize': 128,
30+
'targetrepositorydoesnotexist': 256,
31+
'all': 2147483647,
32+
}
2033
_NON_ACTIVE_STATES = {
2134
'succeeded',
2235
'failed',
@@ -47,6 +60,62 @@ def _normalize_optional_text(value):
4760
return normalized if normalized else None
4861

4962

63+
def _parse_skip_validation(skip_validation):
64+
if skip_validation is None:
65+
return None
66+
67+
if isinstance(skip_validation, int):
68+
if skip_validation < 0:
69+
raise CLIError('--skip-validation must be a non-negative integer or a comma-separated list '
70+
'of policy names.')
71+
return skip_validation
72+
73+
normalized = _normalize_optional_text(skip_validation)
74+
if normalized is None:
75+
raise CLIError('--skip-validation cannot be empty. Provide a non-negative integer or a '
76+
'comma-separated list of policy names.')
77+
78+
if normalized.isdigit():
79+
return int(normalized)
80+
81+
policies = [policy.strip() for policy in normalized.split(',')]
82+
if any(not policy for policy in policies):
83+
raise CLIError('--skip-validation contains an empty policy name. Provide a comma-separated '
84+
'list such as "AgentPoolExists,MaxRepoSize".')
85+
86+
mask = 0
87+
invalid_policies = []
88+
for policy in policies:
89+
key = _normalize_state(policy)
90+
if key not in _SKIP_VALIDATION_POLICIES:
91+
invalid_policies.append(policy)
92+
continue
93+
value = _SKIP_VALIDATION_POLICIES[key]
94+
if value == _SKIP_VALIDATION_POLICIES['all']:
95+
return value
96+
mask |= value
97+
98+
if invalid_policies:
99+
raise CLIError('--skip-validation contains unsupported policy name(s): {}. Supported values: {}. '
100+
'You can also pass a non-negative integer bitmask.'
101+
.format(', '.join(invalid_policies),
102+
', '.join([
103+
'None',
104+
'ActivePullRequestCount',
105+
'PullRequestDeltaSize',
106+
'AgentPoolExists',
107+
'MaxFileSize',
108+
'MaxPullRequestSize',
109+
'MaxPushPackSize',
110+
'MaxReferenceNameLength',
111+
'MaxRepoSize',
112+
'TargetRepositoryDoesNotExist',
113+
'All'
114+
])))
115+
116+
return mask
117+
118+
50119
def get_migration(repository_id=None, organization=None, detect=None):
51120
organization = _resolve_org_for_auth(organization, detect)
52121
repository_id = _resolve_repository_id(repository_id)
@@ -61,6 +130,7 @@ def create_migration(repository_id=None, target_repository=None, target_owner_us
61130
target_repository = _normalize_optional_text(target_repository)
62131
target_owner_user_id = _normalize_optional_text(target_owner_user_id)
63132
agent_pool = _normalize_optional_text(agent_pool)
133+
skip_validation = _parse_skip_validation(skip_validation)
64134

65135
if not target_repository:
66136
raise CLIError('--target-repository must be specified.')

azure-devops/azext_devops/tests/latest/migration/test_migration.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,87 @@ def test_create_migration_payload_includes_optional_fields(self):
138138
self.assertEqual(payload['agentPoolName'], 'MigrationPool')
139139
self.assertEqual(payload['skipValidation'], 2147483647)
140140

141+
def test_create_migration_skip_validation_accepts_integer_string(self):
142+
with patch('azext_devops.dev.migration.migration.resolve_instance') as mock_resolve, \
143+
patch('azext_devops.dev.migration.migration._get_service_client') as mock_client, \
144+
patch('azext_devops.dev.migration.migration._send_request') as mock_send:
145+
mock_send.return_value = {}
146+
mock_resolve.return_value = self._TEST_ORG
147+
148+
create_migration(
149+
repository_id='00000000-0000-0000-0000-000000000000',
150+
target_repository='https://example.ghe.com/OrgName/RepoName',
151+
target_owner_user_id='GeoffCoxMSFT',
152+
skip_validation='2147483647',
153+
organization=self._TEST_ORG,
154+
detect=False
155+
)
156+
157+
payload = mock_send.call_args[0][3]
158+
self.assertEqual(payload['skipValidation'], 2147483647)
159+
160+
def test_create_migration_skip_validation_accepts_policy_names(self):
161+
with patch('azext_devops.dev.migration.migration.resolve_instance') as mock_resolve, \
162+
patch('azext_devops.dev.migration.migration._get_service_client') as mock_client, \
163+
patch('azext_devops.dev.migration.migration._send_request') as mock_send:
164+
mock_send.return_value = {}
165+
mock_resolve.return_value = self._TEST_ORG
166+
167+
create_migration(
168+
repository_id='00000000-0000-0000-0000-000000000000',
169+
target_repository='https://example.ghe.com/OrgName/RepoName',
170+
target_owner_user_id='GeoffCoxMSFT',
171+
skip_validation='PullRequestDeltaSize, AgentPoolExists',
172+
organization=self._TEST_ORG,
173+
detect=False
174+
)
175+
176+
payload = mock_send.call_args[0][3]
177+
self.assertEqual(payload['skipValidation'], 6)
178+
179+
def test_create_migration_skip_validation_accepts_all_name(self):
180+
with patch('azext_devops.dev.migration.migration.resolve_instance') as mock_resolve, \
181+
patch('azext_devops.dev.migration.migration._get_service_client') as mock_client, \
182+
patch('azext_devops.dev.migration.migration._send_request') as mock_send:
183+
mock_send.return_value = {}
184+
mock_resolve.return_value = self._TEST_ORG
185+
186+
create_migration(
187+
repository_id='00000000-0000-0000-0000-000000000000',
188+
target_repository='https://example.ghe.com/OrgName/RepoName',
189+
target_owner_user_id='GeoffCoxMSFT',
190+
skip_validation='All',
191+
organization=self._TEST_ORG,
192+
detect=False
193+
)
194+
195+
payload = mock_send.call_args[0][3]
196+
self.assertEqual(payload['skipValidation'], 2147483647)
197+
198+
def test_create_migration_skip_validation_rejects_invalid_policy_name(self):
199+
with self.assertRaises(CLIError) as ctx:
200+
create_migration(
201+
repository_id='00000000-0000-0000-0000-000000000000',
202+
target_repository='https://example.ghe.com/OrgName/RepoName',
203+
target_owner_user_id='GeoffCoxMSFT',
204+
skip_validation='BogusPolicy',
205+
organization=self._TEST_ORG,
206+
detect=False
207+
)
208+
self.assertIn('unsupported policy name', str(ctx.exception))
209+
210+
def test_create_migration_skip_validation_rejects_empty_policy_name(self):
211+
with self.assertRaises(CLIError) as ctx:
212+
create_migration(
213+
repository_id='00000000-0000-0000-0000-000000000000',
214+
target_repository='https://example.ghe.com/OrgName/RepoName',
215+
target_owner_user_id='GeoffCoxMSFT',
216+
skip_validation='AgentPoolExists,,MaxRepoSize',
217+
organization=self._TEST_ORG,
218+
detect=False
219+
)
220+
self.assertIn('contains an empty policy name', str(ctx.exception))
221+
141222
def test_create_migration_empty_agent_pool_omitted(self):
142223
with patch('azext_devops.dev.migration.migration.resolve_instance') as mock_resolve, \
143224
patch('azext_devops.dev.migration.migration._get_service_client') as mock_client, \

doc/elm_migrations_tsg.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,15 @@ az devops migrations resume --detect false --repository-id <GUID> --validate-onl
308308
|---|---|---|---|
309309
| `--org` | URL | All | Azure DevOps org URL (e.g., `https://dev.azure.com/myorg`). Can be set as default. |
310310
| `--repository-id` | GUID | All except `list` | Azure Repos repository GUID. Get from `az repos show --query id`. |
311+
| `--target-repository` | URL | `create` | Target repository URL (e.g., `https://example.ghe.com/OrgName/RepoName`). Validated by the server. |
311312
| `--target-repository` | URL | `create` | Target repository URL (e.g., `https://example.ghe.com/OrgName/RepoName`). Must start with `http://` or `https://`. |
312313
| `--target-owner-user-id` | string | `create` | Target repository owner user ID. |
313314
| `--agent-pool` | string | `create` | Agent pool name for migration work. Optional. |
314315
| `--validate-only` | flag | `create`, `resume` | On `create`: run pre-migration checks only. On `resume`: switch to validate-only mode. |
315316
| `--migration` | flag | `resume` | Promote succeeded validate-only to full migration (`validateOnly=false`, `statusRequested=active`). Mutually exclusive with `--validate-only`. |
316317
| `--cutover-date` | ISO 8601 | `create` | Pre-schedule cutover at creation time. E.g., `2030-12-31T11:59:00Z`. |
317318
| `--date` | ISO 8601 | `cutover set` | Schedule cutover date/time. E.g., `2030-12-31T11:59:00Z`. |
318-
| `--skip-validation` | string | `create` | Comma-separated list of validation policies to skip. |
319+
| `--skip-validation` | string or int | `create` | Validation policies to skip. Accepts either comma-separated policy names (recommended) or a non-negative integer bitmask. |
319320
| `--include-inactive` | flag | `list` | Include completed, failed, and suspended migrations. |
320321
| `--detect` | flag | All | Auto-detect org from git remote (default: `true`). Use `--detect false` to disable. |
321322

@@ -327,7 +328,7 @@ az devops migrations resume --detect false --repository-id <GUID> --validate-onl
327328
| **Stale default org in config** | Requests go to old/dev URL (e.g., `codedev.ms`) | Run `az devops configure -d organization=https://dev.azure.com/<your-org>` to update |
328329
| **Resume on an active migration** | Error: "Migration is active..." | Pause first with `az devops migrations pause`, then resume |
329330
| **Both `--validate-only` and `--migration` on resume** | Error: "Please specify only one..." | Use only one flag at a time |
330-
| **Missing `--target-repository` on create** | Error: "--target-repository must be specified." | Provide `--target-repository <URL>` |
331+
| **Missing `--agent-pool` on create** | Error: "--agent-pool must be specified." | Always provide `--agent-pool <PoolName>` |
331332
| **Invalid `--target-repository` format** | Error: "--target-repository must be a valid URL..." | Use a fully qualified URL starting with `http://` or `https://` |
332333
| **Invalid `--repository-id`** | Error: "--repository-id must be a valid GUID." | Use `az repos show --query id` to get the correct GUID |
333334
| **Bad date format** | Error: "must be a valid date or datetime string" | Use ISO 8601 format, e.g., `2030-12-31T11:59:00Z` |
@@ -359,6 +360,34 @@ az devops migrations resume --detect false --repository-id <GUID> --validate-onl
359360
1. Check date values are valid ISO 8601 strings (e.g., `2030-12-31T11:59:00Z`).
360361
2. Ensure `--target-repository` is a valid URL.
361362
3. Ensure `--agent-pool` matches a pool name the service recognizes.
363+
4. Ensure `--skip-validation` uses supported policy names or a non-negative integer bitmask.
364+
365+
### skip-validation examples
366+
367+
Recommended form using policy names:
368+
369+
```powershell
370+
az devops migrations create --detect false --repository-id <GUID> --target-repository <TARGET_URL> --target-owner-user-id <OWNER> --skip-validation AgentPoolExists,MaxRepoSize
371+
```
372+
373+
Advanced form using integer bitmask:
374+
375+
```powershell
376+
az devops migrations create --detect false --repository-id <GUID> --target-repository <TARGET_URL> --target-owner-user-id <OWNER> --skip-validation 132
377+
```
378+
379+
Supported policy names:
380+
- `None`
381+
- `ActivePullRequestCount`
382+
- `PullRequestDeltaSize`
383+
- `AgentPoolExists`
384+
- `MaxFileSize`
385+
- `MaxPullRequestSize`
386+
- `MaxPushPackSize`
387+
- `MaxReferenceNameLength`
388+
- `MaxRepoSize`
389+
- `TargetRepositoryDoesNotExist`
390+
- `All`
362391

363392
### Promote validate-only does not start
364393

doc/migrations.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ Use all three fields together when troubleshooting state transitions.
3636
- `--target-owner-user-id` is required for create.
3737
- `--agent-pool` is optional for create.
3838
- `--cutover-date` / `--date` must be ISO 8601, for example: `2030-12-31T11:59:00Z`.
39+
- `--skip-validation` accepts either comma-separated policy names or a non-negative integer bitmask.
3940

4041
Validation enforced by the CLI:
4142

4243
- `--target-repository` must start with `http://` or `https://`.
4344
- `--repository-id` must be a valid GUID.
45+
- `--skip-validation` policy names must be recognized values.
4446

4547
### How to find `--repository-id`
4648

@@ -129,6 +131,28 @@ az devops migrations create --org https://dev.azure.com/myorg \
129131
--validate-only
130132
```
131133

134+
### Create a migration with skip-validation
135+
136+
Recommended form using policy names:
137+
138+
```bash
139+
az devops migrations create --org https://dev.azure.com/myorg \
140+
--repository-id 00000000-0000-0000-0000-000000000000 \
141+
--target-repository https://example.ghe.com/OrgName/RepoName \
142+
--target-owner-user-id OwnerId \
143+
--skip-validation AgentPoolExists,MaxRepoSize
144+
```
145+
146+
Advanced form using integer bitmask:
147+
148+
```bash
149+
az devops migrations create --org https://dev.azure.com/myorg \
150+
--repository-id 00000000-0000-0000-0000-000000000000 \
151+
--target-repository https://example.ghe.com/OrgName/RepoName \
152+
--target-owner-user-id OwnerId \
153+
--skip-validation 132
154+
```
155+
132156
### Pause and resume
133157

134158
```bash
@@ -197,6 +221,9 @@ az devops migrations pause --org https://dev.azure.com/myorg \
197221
- Error: `--target-repository` must be valid.
198222
Ensure it is a fully qualified URL starting with `http://` or `https://`.
199223

224+
- Error: `--skip-validation` contains unsupported policy names.
225+
Use supported names such as `AgentPoolExists`, `MaxRepoSize`, or pass a non-negative integer bitmask.
226+
200227
- Error: requests are sent to the wrong org.
201228
Use `--org <url> --detect false`, and verify defaults via `az devops configure -l`.
202229

0 commit comments

Comments
 (0)