Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
8007d2a
Merge origin/master into elm-integration-1p
May 19, 2026
3468b3e
Add ELM pipeline rewiring CLI commands
May 11, 2026
f0c1e60
ELM pipelines: address code-review feedback
May 19, 2026
b7a29a2
ELM pipelines: camelCase top-level payload keys; fix acknowledge exam…
May 19, 2026
d6a03b0
ELM pipelines: typed exit-code exceptions, api-version bump, failed-m…
May 19, 2026
36da54d
ELM migrations create: add --enable-boards-github-connection opt-in flag
May 19, 2026
9596b85
ELM migrations create: add --enable-auto-discover-pipelines and --pip…
May 20, 2026
8b7a8fa
ELM pipelines submit: relax mandatory --service-connection-id
May 20, 2026
821bef0
fix(elm): allow --service-endpoint-id and user PAT together
May 20, 2026
deae3e0
chore: gitignore ELM dev-session scratch helpers and fixtures
May 20, 2026
1bf0e38
gitignore: exclude docs/ (lives in separate Proxima docs repo)
May 21, 2026
207bf95
ELM CLI: friendlier errors based on server probe results
May 21, 2026
4bb03a1
Revert SC-drop from 207bf95: design says CLI must send pipelineServic…
May 21, 2026
e7d1f46
ELM CLI: confirm-prompt on pipelines acknowledge + name fallback in t…
May 21, 2026
ba1e5af
Wire cutover --pipelines-verified and migrations list --include-all
Jun 4, 2026
fdc6df1
Enumerate supported --skip-validation policy names in migrations crea…
Jun 8, 2026
0db0192
Fix lint: group knack imports, signature indentation, suppress false-…
Jun 8, 2026
1f3144c
Remove dead pipeline acknowledge surface (acknowledge moves to cutove…
Jun 9, 2026
b78e57c
docs: remove stray 1.0.3 snapshot dir; refresh TSG wheel version to 1…
Jun 9, 2026
d1af1ee
scope: limit PR to ELM changes (restore .gitignore and stray 1.0.3 sn…
Jun 9, 2026
54b6496
docs(elm tsg): install latest CLI + extension instead of wheel file
Jun 9, 2026
0cc7f18
docs(elm tsg): document cutover review/approve, pipelines rewiring, n…
Jun 9, 2026
830b37f
fix(elm): treat readyForCutover/reviewForCutover as active stages
Jun 9, 2026
11beaf0
docs(elm): clarify migrations abandon does not delete the record
Jun 9, 2026
bf914ed
fix(elm): block migrations create auto-discover without pipeline serv…
Jun 9, 2026
394f7eb
docs(elm): remove internal tracking reference from cancel_cutover com…
Jun 10, 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
31 changes: 31 additions & 0 deletions azure-devops/azext_devops/dev/migration/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def transform_cutover_review_table_output(result):
blocked_count = result.get('blockedCount')
pending_count = result.get('pendingCount')
total_unprocessed = result.get('totalUnprocessedCount')
requires_pipeline_ack = result.get('requiresPipelineVerificationAcknowledgment')
failed_items = result.get('failedItems') if isinstance(result.get('failedItems'), list) else []

if not failed_items:
Expand All @@ -50,6 +51,7 @@ def transform_cutover_review_table_output(result):
row['BlockedCount'] = blocked_count
row['PendingCount'] = pending_count
row['TotalUnprocessedCount'] = total_unprocessed
row['RequiresPipelineVerification'] = requires_pipeline_ack
row['State'] = None
row['Type'] = None
row['PullRequestUrl'] = None
Expand All @@ -62,6 +64,7 @@ def transform_cutover_review_table_output(result):
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['RequiresPipelineVerification'] = requires_pipeline_ack 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
Expand All @@ -70,6 +73,21 @@ def transform_cutover_review_table_output(result):
return rows


def transform_pipelines_list_table_output(result):
if not isinstance(result, dict):
return []
entries = result.get('pipelines') if isinstance(result.get('pipelines'), list) else []
return [_transform_pipeline_entry_row(entry) for entry in entries]


def transform_pipeline_entries_table_output(result):
if isinstance(result, list):
return [_transform_pipeline_entry_row(entry) for entry in result]
if isinstance(result, dict) and isinstance(result.get('pipelines'), list):
return [_transform_pipeline_entry_row(entry) for entry in result.get('pipelines')]
return []


def _unwrap_migration_list(result):
if isinstance(result, dict) and 'value' in result:
return result['value']
Expand All @@ -90,3 +108,16 @@ def _transform_migration_row(row):
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


def _transform_pipeline_entry_row(entry):
table_row = OrderedDict()
table_row['DefinitionId'] = entry.get('definitionId')
# Server may return name=null until pipeline-name hydration ships; fall back to yamlFilename
# so operators can still identify the row.
display_name = entry.get('name') or entry.get('yamlFilename')
table_row['Name'] = trim_for_display(display_name, _TARGET_TRUNCATION_LENGTH)
table_row['Classification'] = entry.get('classification')
table_row['Status'] = entry.get('status')
table_row['ErrorMessage'] = trim_for_display(entry.get('errorMessage'), _TARGET_TRUNCATION_LENGTH)
return table_row
70 changes: 65 additions & 5 deletions azure-devops/azext_devops/dev/migration/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ def load_migration_help():
helps['devops migrations list'] = """
type: command
short-summary: List migrations in an organization.
long-summary: 'By default the latest migration per repository is returned, regardless of state. Use --include-all to return the full migration history.'
examples:
- name: List migrations.
- name: List the latest migration per repository.
text: |
az devops migrations list --org https://dev.azure.com/myorg
- name: List all migrations including inactive ones.
- name: List the full migration history for every repository.
text: |
az devops migrations list --org https://dev.azure.com/myorg --include-inactive
az devops migrations list --org https://dev.azure.com/myorg --include-all
"""

helps['devops migrations status'] = """
Expand Down Expand Up @@ -72,7 +73,8 @@ def load_migration_help():

helps['devops migrations abandon'] = """
type: command
short-summary: Abandon and delete a migration.
short-summary: Abandon a migration.
long-summary: 'Moves the migration to an abandoned/failed state; the migration record is not purged. Pipeline rewiring data is left intact so a subsequent migration can reuse it.'
examples:
- name: Abandon and keep repository read-only (default).
text: |
Expand All @@ -90,6 +92,7 @@ def load_migration_help():
helps['devops migrations cutover review'] = """
type: command
short-summary: Review unprocessed migration items before cutover.
long-summary: 'The response includes requiresPipelineVerificationAcknowledgment. When true, cutover approve must be re-run with --pipelines-verified before the migration can proceed.'
examples:
- name: Review failures before approving cutover.
text: |
Expand All @@ -98,11 +101,18 @@ def load_migration_help():

helps['devops migrations cutover approve'] = """
type: command
short-summary: Approve cutover by accepting a count of unprocessed items.
short-summary: Approve cutover by accepting unprocessed items and/or verifying rewired pipelines.
long-summary: 'Provide --accept-failures when cutover review surfaces unprocessed items, and/or --pipelines-verified when cutover review reports requiresPipelineVerificationAcknowledgment: true. At least one of the two must be supplied; both may be sent together in a single call.'
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
- name: Acknowledge pipeline verification only (no unprocessed items).
text: |
az devops migrations cutover approve --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --pipelines-verified
- name: Combine failure acceptance and pipeline verification in one call.
text: |
az devops migrations cutover approve --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --accept-failures 3 --pipelines-verified
"""

helps['devops migrations cutover set'] = """
Expand All @@ -118,3 +128,53 @@ def load_migration_help():
type: command
short-summary: Cancel a scheduled cutover.
"""

helps['devops migrations pipelines'] = """
type: group
short-summary: Manage pipeline rewiring for migrations. (Preview)
"""

helps['devops migrations pipelines list'] = """
type: command
short-summary: List pipeline rewiring configuration and per-pipeline status.
examples:
- name: List pipeline rewiring status.
text: |
az devops migrations pipelines list --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000
"""

helps['devops migrations pipelines submit'] = """
type: command
short-summary: Submit pipelines for rewiring. (Preview)
examples:
- name: Submit pipelines with service connection and repository mappings.
text: |
az devops migrations pipelines submit --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --pipeline-ids 42 43 44 --service-connection-id 11111111-1111-1111-1111-111111111111 --repository-mapping aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa=myorg/shared-templates
"""

helps['devops migrations pipelines update'] = """
type: command
short-summary: Bulk update pipeline rewiring configuration. (Preview)
examples:
- name: Add, remove, and update service connection.
text: |
az devops migrations pipelines update --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --add-ids 50 51 --remove-ids 42 --service-connection-id 22222222-2222-2222-2222-222222222222
"""

helps['devops migrations pipelines retry'] = """
type: command
short-summary: Retry failed pipeline rewiring entries. (Preview)
examples:
- name: Retry specific failed pipelines.
text: |
az devops migrations pipelines retry --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --pipeline-ids 42 43
"""

helps['devops migrations pipelines delete'] = """
type: command
short-summary: Delete pipeline rewiring data for a migration. (Preview)
examples:
- name: Delete rewiring config and cloned definitions.
text: |
az devops migrations pipelines delete --org https://dev.azure.com/myorg --repository-id 00000000-0000-0000-0000-000000000000 --migration-id 7 --yes
"""
95 changes: 86 additions & 9 deletions azure-devops/azext_devops/dev/migration/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,52 @@ def load_migration_arguments(self, _):
context.argument('repository_id', options_list='--repository-id',
help='ID of the Azure Repos repository (GUID).')

with self.argument_context('devops migrations pipelines') as context:
context.argument('repository_mapping', options_list='--repository-mapping', action='append',
help='Repository mapping in the format <sourceRepoId>=<targetOwner>/<targetRepo>. '
'Can be provided multiple times.')

with self.argument_context('devops migrations pipelines submit') as context:
context.argument('pipeline_ids', options_list='--pipeline-ids', nargs='+',
help='Pipeline definition IDs. Accepts space-separated values '
'(for example, 42 43 44) or comma-separated values '
'(for example, 42,43,44).')
context.argument('service_connection_id', options_list='--service-connection-id',
help='Project-scoped GitHub service connection ID (GUID). '
'Optional if a connection was already attached via '
'migrations create --pipeline-service-connection-id or '
'pipelines update --service-connection-id.')

with self.argument_context('devops migrations pipelines update') as context:
context.argument('add_ids', options_list='--add-ids', nargs='+',
help='Pipeline IDs to add. Accepts space-separated or comma-separated values.')
context.argument('remove_ids', options_list='--remove-ids', nargs='+',
help='Pipeline IDs to remove. Accepts space-separated or comma-separated values.')
context.argument('retry_ids', options_list='--retry-ids', nargs='+',
help='Failed pipeline IDs to retry. Accepts space-separated or comma-separated values.')
context.argument('service_connection_id', options_list='--service-connection-id',
help='Project-scoped GitHub service connection ID (GUID).')

with self.argument_context('devops migrations pipelines retry') as context:
context.argument('pipeline_ids', options_list='--pipeline-ids', nargs='+',
help='Pipeline definition IDs to retry. Accepts space-separated '
'or comma-separated values.')

with self.argument_context('devops migrations pipelines delete') as context:
context.argument('migration_id', options_list='--migration-id', type=int,
help='Migration ID used for pipeline rewiring cleanup.')
context.argument('yes', options_list=['--yes', '-y'], action='store_true',
help='Do not prompt for confirmation.')

with self.argument_context('devops migrations list') as context:
context.argument('include_all', options_list='--include-all', action='store_true',
help='Return the full migration history (all records per repository). '
'By default only the latest migration per repository is returned, '
'regardless of its state.')
context.argument('include_inactive', options_list='--include-inactive', action='store_true',
help='Include inactive (completed, abandoned, failed) migrations in the results.')
help='Deprecated. Use --include-all instead.',
deprecate_info=context.deprecate(redirect='--include-all',
target='--include-inactive', hide=False))
context.argument('project', options_list='--project',
help='Optional project name or ID to filter migrations.')

Expand All @@ -27,9 +70,12 @@ def load_migration_arguments(self, _):
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.')
help='GitHub user token used for user-identity verification on the target '
'host. Independent of --service-endpoint-id. If omitted and '
'--service-endpoint-id is not provided, the CLI checks ELM_GITHUB_TOKEN '
'and then runs GitHub device flow. When --service-endpoint-id is '
'provided, device flow is skipped; pass --github-token or set '
'ELM_GITHUB_TOKEN to supply the user token.')
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 @@ -40,12 +86,38 @@ def load_migration_arguments(self, _):
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,MaxFileSize) or a non-negative '
'integer bitmask.')
'integer bitmask. Supported policy names (case-insensitive): None, '
'ActivePullRequestCount, PullRequestDeltaSize, AgentPoolExists, MaxFileSize, '
'MaxPullRequestSize, MaxPushPackSize, MaxReferenceNameLength, '
'TargetRepositoryDoesNotExist, SourceRepositoryContainsLfsObjects, '
'SourceRepositoryNotReadOnly, BoardsGitHubConnectionProvisioning, All.')
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.')
help='Service endpoint ID (GUID) for the GitHub Enterprise Server connection '
'used to sync commits to the target. Independent of user-identity '
'verification: --github-token / ELM_GITHUB_TOKEN can be supplied '
'alongside this flag. Device flow is skipped when this flag is set.')
context.argument('enable_boards_github_connection',
options_list='--enable-boards-github-connection', action='store_true',
help='Opt in to provisioning the Azure Boards GitHub connection at '
'cutover. Off by default. Requires the Azure Boards GitHub App '
'to be installed on the target GitHub Enterprise organization '
'before the migration runs.')
context.argument('enable_auto_discover_pipelines',
options_list='--enable-auto-discover-pipelines', action='store_true',
help='Opt in to automatic pipeline discovery at cutover. Off by default. '
'When enabled, the ELM sync job walks the source repository and '
'creates clone definitions for every pipeline that references it. '
'Requires --pipeline-service-connection-id; without it discovery '
'runs as a no-op and enrolls 0 pipelines. '
'Pipeline rewiring itself is always available via '
'az devops migrations pipelines submit / update.')
context.argument('pipeline_service_connection_id',
options_list='--pipeline-service-connection-id',
help='Project-scoped GitHub service connection ID (GUID) attached at '
'create time for pipeline rewiring. Required for full auto-discovery '
'when combined with --enable-auto-discover-pipelines; optional in '
'manual mode (pre-attaches the connection so subsequent '
'pipelines submit calls only need --pipeline-ids).')

with self.argument_context('devops migrations cutover set') as context:
context.argument('cutover_date', options_list='--date',
Expand All @@ -56,6 +128,11 @@ def load_migration_arguments(self, _):
context.argument('accept_failures', options_list='--accept-failures', type=int,
help='Number of unprocessed migration resources to accept before '
'proceeding with cutover.')
context.argument('pipelines_verified', options_list='--pipelines-verified', action='store_true',
help='Acknowledge that all rewired pipelines have been verified. '
'Required when "cutover review" returns '
'requiresPipelineVerificationAcknowledgment: true. Can be combined '
'with --accept-failures in a single approve call.')

with self.argument_context('devops migrations resume') as context:
context.argument('validate_only', options_list='--validate-only', action='store_true',
Expand Down
Loading