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
22a9432
Add ELM GitHub token auth flow for migration create
bhuvanshah7 Apr 22, 2026
1b753e6
Improve ELM create conflict error messaging
bhuvanshah7 Apr 22, 2026
c738a74
Document 409 conflict error behavior for migration create
bhuvanshah7 Apr 22, 2026
e804cf0
Harden device-flow response handling and fallback guidance
bhuvanshah7 Apr 22, 2026
78d26f6
Handle device-flow 401/403 with generic guidance
bhuvanshah7 Apr 22, 2026
b2eb604
Use strict target repo validation and pre-check issue details
bhuvanshah7 Apr 23, 2026
e1fc264
Mark ELM migrations command group as preview
bhuvanshah7 Apr 28, 2026
846015f
ELM migrations: treat 'completed' equivalent to 'succeeded' for termi…
bhuvanshah7 Apr 28, 2026
b2a6ca7
ELM migrations abandon: return success message instead of empty object
bhuvanshah7 Apr 28, 2026
77d7967
ELM migrations: UX improvements for pause, cancel, list, abandon and …
bhuvanshah7 Apr 28, 2026
d9ad217
ELM migrations: add service-endpoint-id parameter for GitHub Enterpri…
bhuvanshah7 Apr 29, 2026
e24da3f
Add tests for service-endpoint-id parameter
bhuvanshah7 Apr 29, 2026
3e39607
ELM device flow: copy user code to clipboard when available
bhuvanshah7 Apr 29, 2026
0b3eb43
Merge device-flow auth into elm-migrations preview branch
bhuvanshah7 Apr 29, 2026
2b3cd04
Fix: skip github token resolution when service-endpoint-id is provided
bhuvanshah7 Apr 30, 2026
14067aa
ELM abandon: add optional remove-read-only flag
bhuvanshah7 Apr 30, 2026
e629790
ELM cutover: add review and approve CLI flow
bhuvanshah7 Apr 30, 2026
7c403f6
fix: always resolve github user token regardless of service endpoint
bhuvanshah7 May 1, 2026
7b96f92
Initial commit: Add README and hello.js
May 6, 2026
4eaab8c
Fix ELM style checks
May 7, 2026
ed0bb92
Add migration workflow guide for operators and repo owners
May 8, 2026
fef8a84
Fix R0917: use keyword-only args in create_migration and _update_migr…
May 8, 2026
0c7cd7b
Revert "Add migration workflow guide for operators and repo owners"
May 8, 2026
16f0c06
Remove ELM_Demo_Script.md
May 8, 2026
3de1822
Revert "Remove ELM_Demo_Script.md"
May 11, 2026
0e84488
Remove ELM_Demo_Script.md
May 11, 2026
ba8b15c
Restore upstream README and remove stray E2E_TEST_REPORT to fix markd…
May 12, 2026
d4a4857
Remove stray hello.js and docs gitlink accidentally added
May 13, 2026
a686a03
Skip GitHub device flow when --service-endpoint-id is provided
May 13, 2026
919fe5c
fix(migration): send DateTimeOffset.MinValue sentinel to clear schedu…
May 13, 2026
35ad24b
chore(migration): sanitize internal identifiers from ELM help/docs/tests
May 14, 2026
93d8846
fix(migration): block cutover cancel once stage is Cutover
May 14, 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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ exclude =
env
venv
.venv
.venv-disabled
*/test/*
*/devops_sdk/*
46 changes: 46 additions & 0 deletions azure-devops/azext_devops/dev/migration/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
_TARGET_TRUNCATION_LENGTH = 60


def transform_message_output(result):
if result is None:
return []
if isinstance(result, dict) and 'message' in result:
row = OrderedDict()
row['Message'] = result['message']
return [row]
return transform_migration_table_output(result)


def transform_migrations_table_output(result):
migrations = _unwrap_migration_list(result)
table_output = []
Expand All @@ -24,6 +34,42 @@ def transform_migration_table_output(result):
return [_transform_migration_row(result)]


def transform_cutover_review_table_output(result):
if not isinstance(result, dict):
return []

failed_count = result.get('failedCount')
blocked_count = result.get('blockedCount')
pending_count = result.get('pendingCount')
total_unprocessed = result.get('totalUnprocessedCount')
failed_items = result.get('failedItems') if isinstance(result.get('failedItems'), list) else []

if not failed_items:
row = OrderedDict()
row['FailedCount'] = failed_count
row['BlockedCount'] = blocked_count
row['PendingCount'] = pending_count
row['TotalUnprocessedCount'] = total_unprocessed
row['State'] = None
row['Type'] = None
row['PullRequestUrl'] = None
return [row]

rows = []
for index, item in enumerate(failed_items):
row = OrderedDict()
row['FailedCount'] = failed_count if index == 0 else None
row['BlockedCount'] = blocked_count if index == 0 else None
row['PendingCount'] = pending_count if index == 0 else None
row['TotalUnprocessedCount'] = total_unprocessed if index == 0 else None
row['State'] = item.get('state') if isinstance(item, dict) else None
row['Type'] = item.get('type') if isinstance(item, dict) else None
row['PullRequestUrl'] = item.get('pullRequestUrl') if isinstance(item, dict) else None
rows.append(row)

return rows


def _unwrap_migration_list(result):
if isinstance(result, dict) and 'value' in result:
return result['value']
Expand Down
35 changes: 32 additions & 3 deletions azure-devops/azext_devops/dev/migration/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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).'
long-summary: 'This command group is a part of the azure-devops extension and is in preview. Availability may be limited (for example, to 1P/allowlisted users). For ELM migrations, --org should be your Azure DevOps organization URL (for example: https://dev.azure.com/myorg).'
"""

helps['devops migrations list'] = """
Expand All @@ -37,13 +37,17 @@ def load_migration_help():
helps['devops migrations create'] = """
type: command
short-summary: Create a migration for a repository.
long-summary: 'If --github-token is not provided, the CLI checks ELM_GITHUB_TOKEN and then runs GitHub device flow to acquire a token.'
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
az devops migrations create --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --target-repository https://github.com/OrgName/RepoName --agent-pool <your-agent-pool>
- 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
az devops migrations create --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --target-repository https://github.com/OrgName/RepoName --agent-pool <your-agent-pool> --validate-only --skip-validation ActivePullRequestCount,PullRequestDeltaSize
- name: Create using a pre-generated GitHub token or PAT.
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 --github-token <token>
"""

helps['devops migrations pause'] = """
Expand All @@ -69,13 +73,38 @@ def load_migration_help():
helps['devops migrations abandon'] = """
type: command
short-summary: Abandon and delete a migration.
examples:
- name: Abandon and keep repository read-only (default).
text: |
az devops migrations abandon --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000
- name: Abandon and set repository back to read-write.
text: |
az devops migrations abandon --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --remove-read-only
"""

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

helps['devops migrations cutover review'] = """
type: command
short-summary: Review unprocessed migration items before cutover.
examples:
- name: Review failures before approving cutover.
text: |
az devops migrations cutover review --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000
"""

helps['devops migrations cutover approve'] = """
type: command
short-summary: Approve cutover by accepting a count of unprocessed items.
examples:
- name: Approve cutover after reviewing failures.
text: |
az devops migrations cutover approve --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --accept-failures 3
"""

helps['devops migrations cutover set'] = """
type: command
short-summary: Schedule cutover for a migration.
Expand Down
22 changes: 21 additions & 1 deletion azure-devops/azext_devops/dev/migration/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ def load_migration_arguments(self, _):
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.')
help='Target repository owner user ID. Deprecated and ignored when server-side '
'token-based owner resolution is enabled.')
context.argument('github_token', options_list='--github-token',
help='GitHub token used for migration authorization. Ignored when '
'--service-endpoint-id is specified. If omitted, the CLI first '
'checks ELM_GITHUB_TOKEN and then runs GitHub device flow.')
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',
Expand All @@ -36,15 +41,30 @@ def load_migration_arguments(self, _):
help='Validation policies to skip. Accepts either a comma-separated list of '
'policy names (for example, AgentPoolExists,MaxRepoSize) or a non-negative '
'integer bitmask.')
context.argument('service_endpoint_id', options_list='--service-endpoint-id',
help='Service endpoint ID (GUID) for the GitHub Enterprise Server connection. '
'When specified, the server uses the service connection for GitHub '
'authentication and the CLI skips GitHub device flow. Mutually exclusive '
'with --github-token.')

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 cutover approve') as context:
context.argument('accept_failures', options_list='--accept-failures', type=int,
help='Number of unprocessed migration resources to accept before '
'proceeding with cutover.')

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).')

with self.argument_context('devops migrations abandon') as context:
context.argument('remove_read_only', options_list='--remove-read-only', action='store_true',
help='Also set the Azure Repos repository back to read-write state by '
'sending removeReadOnly=true.')
18 changes: 12 additions & 6 deletions azure-devops/azext_devops/dev/migration/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

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
from ._format import (transform_migrations_table_output,
transform_migration_table_output,
transform_message_output,
transform_cutover_review_table_output)


migrationOps = CliCommandType(
Expand All @@ -15,15 +18,18 @@


def load_migration_commands(self, _):
with self.command_group('devops migrations', command_type=migrationOps) as g:
with self.command_group('devops migrations', command_type=migrationOps, is_preview=True) 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('pause', 'pause_migration', table_transformer=transform_message_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?')
confirmation='Are you sure you want to abandon this migration?',
table_transformer=transform_message_output)

with self.command_group('devops migrations cutover', command_type=migrationOps) as g:
with self.command_group('devops migrations cutover', command_type=migrationOps, is_preview=True) as g:
g.command('review', 'get_cutover_review', table_transformer=transform_cutover_review_table_output)
g.command('approve', 'approve_cutover', table_transformer=transform_migration_table_output)
g.command('set', 'schedule_cutover', table_transformer=transform_migration_table_output)
g.command('cancel', 'cancel_cutover', table_transformer=transform_migration_table_output)
g.command('cancel', 'cancel_cutover', table_transformer=transform_message_output)
Loading
Loading