Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1e90bf3
Add ELM migrations commands
bhuvanshah7 Feb 25, 2026
78bab25
Add migration create options
bhuvanshah7 Mar 12, 2026
6c28ae0
Fix style checks
bhuvanshah7 Mar 13, 2026
a56dddd
Use org base for ELM migrations
bhuvanshah7 Mar 18, 2026
a65cc09
Adjust BuildWheel task for Windows paths
bhuvanshah7 Mar 18, 2026
01f61fb
Allow GitHub.com targets and fix help YAML
bhuvanshah7 Mar 18, 2026
de12e5b
Simplify migration resume flow
bhuvanshah7 Mar 18, 2026
bbd23ba
Add migrations command reference
bhuvanshah7 Mar 18, 2026
5b2e17e
Fix BuildWheel task on Windows
bhuvanshah7 Mar 19, 2026
3a17f54
Fix markdown lint spacing
bhuvanshah7 Mar 19, 2026
e2af141
Fix markdown lint and flake8 indentation errors
bhuvanshah7 Mar 20, 2026
667c47e
Add --include-inactive help example and unit test for list migrations
bhuvanshah7 Mar 20, 2026
1315885
Fix convert_date_string_to_iso8601 returning datetime instead of stri…
bhuvanshah7 Mar 24, 2026
11fba39
Apply dev feedback: remove URL validation, fix validate-only default,…
bhuvanshah7 Mar 27, 2026
010c80e
Update ELM migrations TSG with current params, codedev.ms troubleshoo…
bhuvanshah7 Mar 27, 2026
8dc649c
TSG: add complete command/param reference, list step, cutover cancel,…
bhuvanshah7 Mar 27, 2026
f714a52
TSG: restructure as end-to-end guide with setup, lifecycle, walkthrou…
bhuvanshah7 Apr 2, 2026
3922888
TSG: add fresh-user onboarding, concrete examples, status interpretat…
bhuvanshah7 Apr 2, 2026
b17ee0e
docs: update TSG to use ADO org URL instead of separate ELM service URL
bhuvanshah7 Apr 2, 2026
30708f0
docs: update migrations.md to use ADO org URL instead of ELM service URL
bhuvanshah7 Apr 2, 2026
70a3dda
Fix statusRequested field handling in pause/resume and update help URLs
bhuvanshah7 Apr 6, 2026
0e468eb
Make --agent-pool optional; server assigns default pool
bhuvanshah7 Apr 6, 2026
e28df3a
Update migration changes and tests
bhuvanshah7 Apr 10, 2026
1c3c38b
Promote validate-only migrations via PUT state update
bhuvanshah7 Apr 13, 2026
4433f87
Improve migrations CLI UX validations and guidance
bhuvanshah7 Apr 13, 2026
5cba73e
Expand ELM migration docs and TSG guidance
bhuvanshah7 Apr 13, 2026
acb79a0
Support skip-validation names and integer values
bhuvanshah7 Apr 13, 2026
d50de9a
Add project filter support for migrations list
bhuvanshah7 Apr 13, 2026
f7b4943
Fix flake8 W503 in migration state check
bhuvanshah7 Apr 15, 2026
eb4d3a2
Fix markdown lint spacing and list formatting
bhuvanshah7 Apr 15, 2026
6bb3221
Fix remaining markdown lint issues in migration docs
bhuvanshah7 Apr 15, 2026
8deb6ac
Fix Run_Style_Check missing dependency on Build_Publish_Azure_CLI_Tes…
bhuvanshah7 Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ exclude =
scripts
doc
build_scripts
env
venv
.venv
*/test/*
*/devops_sdk/*
1 change: 1 addition & 0 deletions .github/workflows/pr-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ jobs:
path: ${{ github.workspace }}/azure-devops/htmlcov

Run_Style_Check:
needs: Build_Publish_Azure_CLI_Test_SDK
runs-on: macOS-latest
steps:
- uses: actions/checkout@v4
Expand Down
9 changes: 6 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
"tasks": [
{
"label": "BuildWheel",
"command": "${command:python.interpreterPath}",
"type": "shell",
"command": "python",
"args": [
"setup.py",
"sdist",
"bdist_wheel"
],
"type": "shell",
"options": {
"cwd": "${workspaceRoot}/azure-devops/"
"cwd": "${workspaceRoot}/azure-devops/",
"env": {
"PATH": "${workspaceRoot}\\env\\Scripts;${env:PATH}"
}
},
"presentation": {
"echo": true,
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ $az [group] [subgroup] [command] {parameters}
```

Adding the Azure DevOps Extension adds `devops`, `pipelines`, `artifacts`, `boards` and `repos` groups.
Enterprise live migrations are available under `az devops migrations`.
For usage and help content for any command, pass in the -h parameter, for example:

```bash
Expand All @@ -43,6 +44,7 @@ Group

Subgroups:
admin : Manage administration operations.
migrations : Manage enterprise live migrations.
extension : Manage extensions.
project : Manage team projects.
security : Manage security related operations.
Expand All @@ -64,6 +66,7 @@ Commands:
- Checkout the CLI docs at [docs.microsoft.com - Azure DevOps CLI](https://docs.microsoft.com/azure/devops/cli/).
- Check out other examples in the [How-to guides](https://docs.microsoft.com/azure/devops/cli/?view=azure-devops#how-to-guides) section.
- You can view the various commands and its usage here - [docs.microsoft.com - Azure DevOps Extension Reference](https://docs.microsoft.com/en-us/cli/azure/devops?view=azure-cli-latest)
- Enterprise live migrations guide: [doc/migrations.md](doc/migrations.md)

## Contribute

Expand Down
8 changes: 6 additions & 2 deletions azure-devops/azext_devops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def load_command_table(self, args):
load_admin_commands(self, args)
from azext_devops.dev.boards.commands import load_work_commands
load_work_commands(self, args)
from azext_devops.dev.migration.commands import load_migration_commands
load_migration_commands(self, args)
from azext_devops.dev.pipelines.commands import load_build_commands
load_build_commands(self, args)
from azext_devops.dev.repos.commands import load_code_commands
Expand All @@ -36,6 +38,8 @@ def load_arguments(self, command):
load_admin_arguments(self, command)
from azext_devops.dev.boards.arguments import load_work_arguments
load_work_arguments(self, command)
from azext_devops.dev.migration.arguments import load_migration_arguments
load_migration_arguments(self, command)
from azext_devops.dev.pipelines.arguments import load_build_arguments
load_build_arguments(self, command)
from azext_devops.dev.repos.arguments import load_code_arguments
Expand All @@ -47,8 +51,8 @@ def load_arguments(self, command):

@staticmethod
def post_parse_args(_cli_ctx, **kwargs):
if (kwargs.get('command', None) and
kwargs['command'].startswith(('devops', 'boards', 'artifacts', 'pipelines', 'repos'))):
command = kwargs.get('command', None)
if command and command.startswith(('devops', 'boards', 'artifacts', 'pipelines', 'repos', 'migrations')):
from azext_devops.dev.common.telemetry import set_tracking_data
# we need to set tracking data only after we know that all args are valid,
# otherwise we may log EUII data that a user inadvertently sent as an argument
Expand Down
3 changes: 1 addition & 2 deletions azure-devops/azext_devops/dev/common/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ def convert_date_string_to_iso8601(value, argument=None):
if d.tzinfo is None:
from dateutil.tz import tzlocal
d = d.replace(tzinfo=tzlocal())
d = d.isoformat()
return d
return d.isoformat()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this change ?



def convert_date_only_string_to_iso8601(value, argument=None):
Expand Down
8 changes: 8 additions & 0 deletions azure-devops/azext_devops/dev/migration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from ._help import load_migration_help

load_migration_help()
46 changes: 46 additions & 0 deletions azure-devops/azext_devops/dev/migration/_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from collections import OrderedDict
from azext_devops.dev.common.format import trim_for_display, date_time_to_only_date


_TARGET_TRUNCATION_LENGTH = 60


def transform_migrations_table_output(result):
migrations = _unwrap_migration_list(result)
table_output = []
for item in migrations:
table_output.append(_transform_migration_row(item))
return table_output


def transform_migration_table_output(result):
if result is None:
return []
return [_transform_migration_row(result)]


def _unwrap_migration_list(result):
if isinstance(result, dict) and 'value' in result:
return result['value']
if isinstance(result, list):
return result
return []


def _transform_migration_row(row):
table_row = OrderedDict()
table_row['RepositoryId'] = row.get('repositoryId')
table_row['TargetRepository'] = trim_for_display(row.get('targetRepository'),
_TARGET_TRUNCATION_LENGTH)
table_row['Status'] = row.get('status')
table_row['Stage'] = row.get('stage')
table_row['ValidateOnly'] = row.get('validateOnly')
table_row['CutoverDate'] = date_time_to_only_date(row.get('scheduledCutoverDate'))
table_row['CodeSyncDate'] = date_time_to_only_date(row.get('codeSyncDate'))
table_row['PrSyncDate'] = date_time_to_only_date(row.get('pullRequestSyncDate'))
return table_row
91 changes: 91 additions & 0 deletions azure-devops/azext_devops/dev/migration/_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from knack.help_files import helps


def load_migration_help():
helps['devops migrations'] = """
type: group
short-summary: Manage enterprise live migrations.
long-summary: 'This command group is a part of the azure-devops extension. For ELM migrations, --org should be your Azure DevOps organization URL (for example: https://dev.azure.com/myorg).'
"""

helps['devops migrations list'] = """
type: command
short-summary: List migrations in an organization.
examples:
- name: List migrations.
text: |
az devops migrations list --org https://dev.azure.com/myorg
- name: List all migrations including inactive ones.
text: |
az devops migrations list --org https://dev.azure.com/myorg --include-inactive
"""

helps['devops migrations status'] = """
type: command
short-summary: Get migration status for a repository.
examples:
- name: Get migration status by repository id.
text: |
az devops migrations status --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000
"""

helps['devops migrations create'] = """
type: command
short-summary: Create a migration for a repository.
examples:
- name: Create a migration.
text: |
az devops migrations create --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --target-repository https://github.com/OrgName/RepoName --target-owner-user-id OwnerUserId --agent-pool MigrationPool
- name: Create a validate-only migration.
text: |
az devops migrations create --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --target-repository https://github.com/OrgName/RepoName --target-owner-user-id OwnerUserId --agent-pool MigrationPool --validate-only --skip-validation ActivePullRequestCount,PullRequestDeltaSize
"""

helps['devops migrations pause'] = """
type: command
short-summary: Pause an active migration.
"""

helps['devops migrations resume'] = """
type: command
short-summary: Resume a stopped (paused, failed) migration.
examples:
- name: Resume using the current mode.
text: |
az devops migrations resume --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000
- name: Resume in validate-only mode.
text: |
az devops migrations resume --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --validate-only
- name: Continue migration (clears validate-only mode).
text: |
az devops migrations resume --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --migration
"""

helps['devops migrations abandon'] = """
type: command
short-summary: Abandon and delete a migration.
"""

helps['devops migrations cutover'] = """
type: group
short-summary: Manage migration cutover.
"""

helps['devops migrations cutover set'] = """
type: command
short-summary: Schedule cutover for a migration.
examples:
- name: Schedule cutover.
text: |
az devops migrations cutover set --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --date 2030-12-31T11:59:00Z
"""

helps['devops migrations cutover cancel'] = """
type: command
short-summary: Cancel a scheduled cutover.
"""
50 changes: 50 additions & 0 deletions azure-devops/azext_devops/dev/migration/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azext_devops.dev.common.arguments import convert_date_string_to_iso8601
from azext_devops.dev.team.arguments import load_global_args


# pylint: disable=too-many-statements
def load_migration_arguments(self, _):
with self.argument_context('devops migrations') as context:
load_global_args(context)
context.argument('repository_id', options_list='--repository-id',
help='ID of the Azure Repos repository (GUID).')

with self.argument_context('devops migrations list') as context:
context.argument('include_inactive', options_list='--include-inactive', action='store_true',
help='Include inactive (completed, abandoned, failed) migrations in the results.')
context.argument('project', options_list='--project',
help='Optional project name or ID to filter migrations.')

with self.argument_context('devops migrations create') as context:
context.argument('target_repository', options_list='--target-repository',
help='Target repository URL (must start with http:// or https://).')
context.argument('target_owner_user_id', options_list='--target-owner-user-id',
help='Target repository owner user ID.')
context.argument('validate_only', options_list='--validate-only', action='store_true',
help='Create in validate-only mode (pre-migration checks only).')
context.argument('cutover_date', options_list='--cutover-date',
type=convert_date_string_to_iso8601,
help='Scheduled cutover date/time (ISO 8601).')
context.argument('agent_pool', options_list='--agent-pool',
help='Agent pool name to use for migration work.')
context.argument('skip_validation', options_list='--skip-validation',
help='Validation policies to skip. Accepts either a comma-separated list of '
'policy names (for example, AgentPoolExists,MaxRepoSize) or a non-negative '
'integer bitmask.')

with self.argument_context('devops migrations cutover set') as context:
context.argument('cutover_date', options_list='--date',
type=convert_date_string_to_iso8601,
help='The date and time for cutover (ISO 8601).')

with self.argument_context('devops migrations resume') as context:
context.argument('validate_only', options_list='--validate-only', action='store_true',
help='Resume in validate-only mode.')
context.argument('migration', options_list='--migration', action='store_true',
help='Promote a succeeded validate-only migration to a full migration '
'(sets validateOnly=false and statusRequested=active).')
29 changes: 29 additions & 0 deletions azure-devops/azext_devops/dev/migration/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.commands import CliCommandType
from azext_devops.dev.common.exception_handler import azure_devops_exception_handler
from ._format import transform_migrations_table_output, transform_migration_table_output


migrationOps = CliCommandType(
operations_tmpl='azext_devops.dev.migration.migration#{}',
exception_handler=azure_devops_exception_handler
)


def load_migration_commands(self, _):
with self.command_group('devops migrations', command_type=migrationOps) as g:
g.command('list', 'list_migrations', table_transformer=transform_migrations_table_output)
g.command('status', 'get_migration', table_transformer=transform_migration_table_output)
g.command('create', 'create_migration', table_transformer=transform_migration_table_output)
g.command('pause', 'pause_migration', table_transformer=transform_migration_table_output)
g.command('resume', 'resume_migration', table_transformer=transform_migration_table_output)
g.command('abandon', 'delete_migration',
confirmation='Are you sure you want to abandon this migration?')

with self.command_group('devops migrations cutover', command_type=migrationOps) as g:
g.command('set', 'schedule_cutover', table_transformer=transform_migration_table_output)
g.command('cancel', 'cancel_cutover', table_transformer=transform_migration_table_output)
Loading
Loading