Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2e6b07c
Add ELM migrations commands
bhuvanshah7 Feb 25, 2026
1e92f2e
Add migration create options
bhuvanshah7 Mar 12, 2026
61cddbb
Fix style checks
bhuvanshah7 Mar 13, 2026
762f90c
Merge remote-tracking branch 'origin/master' into feature/elm-cli
bhuvanshah7 Mar 18, 2026
9353a59
Use org base for ELM migrations
bhuvanshah7 Mar 18, 2026
d015343
Adjust BuildWheel task for Windows paths
bhuvanshah7 Mar 18, 2026
5fb8af4
Allow GitHub.com targets and fix help YAML
bhuvanshah7 Mar 18, 2026
c0f5266
Simplify migration resume flow
bhuvanshah7 Mar 18, 2026
95688f4
Add migrations command reference
bhuvanshah7 Mar 18, 2026
b51113c
Fix BuildWheel task on Windows
bhuvanshah7 Mar 19, 2026
9d7a0ff
Fix markdown lint spacing
bhuvanshah7 Mar 19, 2026
5f3da21
Fix markdown lint and flake8 indentation errors
bhuvanshah7 Mar 20, 2026
83979d4
Add --include-inactive help example and unit test for list migrations
bhuvanshah7 Mar 20, 2026
18d4343
Fix convert_date_string_to_iso8601 returning datetime instead of stri…
bhuvanshah7 Mar 24, 2026
fe078f6
Apply dev feedback: remove URL validation, fix validate-only default,…
bhuvanshah7 Mar 27, 2026
f770757
Update ELM migrations TSG with current params, codedev.ms troubleshoo…
bhuvanshah7 Mar 27, 2026
2719059
TSG: add complete command/param reference, list step, cutover cancel,…
bhuvanshah7 Mar 27, 2026
32b3312
TSG: restructure as end-to-end guide with setup, lifecycle, walkthrou…
bhuvanshah7 Apr 2, 2026
0c5376c
TSG: add fresh-user onboarding, concrete examples, status interpretat…
bhuvanshah7 Apr 2, 2026
74c560b
docs: update TSG to use ADO org URL instead of separate ELM service URL
bhuvanshah7 Apr 2, 2026
f275d9f
docs: update migrations.md to use ADO org URL instead of ELM service URL
bhuvanshah7 Apr 2, 2026
d075a60
Fix statusRequested field handling in pause/resume and update help URLs
bhuvanshah7 Apr 6, 2026
60b71d3
Make --agent-pool optional; server assigns default pool
bhuvanshah7 Apr 6, 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/*
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()


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.
"""
45 changes: 45 additions & 0 deletions azure-devops/azext_devops/dev/migration/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# --------------------------------------------------------------------------------------------
# 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 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.')

with self.argument_context('devops migrations create') as context:
context.argument('target_repository', options_list='--target-repository',
help='Target repository URL.')
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='Comma-separated list of validation policies to skip.')

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='Continue the migration (clears any validate-only mode).')
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