diff --git a/.github/workflows/ado-migration-process-full.yml b/.github/workflows/ado-migration-process-full.yml index 8a9f685..eeb604d 100644 --- a/.github/workflows/ado-migration-process-full.yml +++ b/.github/workflows/ado-migration-process-full.yml @@ -1,4 +1,5 @@ name: Full ADO Project Migration +run-name: "${{ github.event.inputs.SourceOrganizationName }}/${{ github.event.inputs.SourceProjectName }}=>${{ github.event.inputs.TargetOrganizationName }}/${{ github.event.inputs.TargetProjectName }} [Full Migration]" on: workflow_dispatch: @@ -6,19 +7,19 @@ on: SourceOrganizationName: description: "Name of the Source Organization" required: true - default: "" + default: "AIZ-GL" SourceProjectName: description: "Name of the Source Project" required: true - default: "" + default: "GL.CL-Elita" TargetOrganizationName: description: "Name of the Target Organization" required: true - default: "" + default: "AIZ-Global" TargetProjectName: description: "Name of the Target Project" required: true - default: "" + default: "GL.CL-Elita-migrated" WhatIf: type: boolean description: "WhatIf: Simulated Run" @@ -28,7 +29,7 @@ on: jobs: run-powershell-script: name: Run PowerShell Script - runs-on: 'ubuntu-latest' + runs-on: 'AEC0WGEMP001' timeout-minutes: 7200 env: AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} @@ -44,13 +45,13 @@ jobs: az --version - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Clean Azure DevOps Migration Tools Log and Attachments Directory + - name: Clean Azure DevOps Migration Tools Log Directory shell: pwsh run: | - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}\\WorkItemAttachmentWorkingFolder" -Recurse | Remove-Item -Recurse + Get-ChildItem "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}/logs" -Recurse | Remove-Item -Recurse + - name: Set Migration Configuration shell: pwsh run: | @@ -70,7 +71,7 @@ jobs: $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" $configuration.ProjectDirectory = $projectDirectory - $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $configuration.WorkItemMigratorDirectory = "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}" $configuration.RepositoryCloneTempDirectory = "${{ vars.RepositoryCloneTempDirectory }}" $configuration.DevOpsMigrationToolConfigurationFile = "${{ vars.DevOpsMigrationToolConfigurationFile }}" $configuration.ArtifactFeedPackageVersionLimit = ${{ vars.ArtifactFeedPackageVersionLimit }} @@ -88,7 +89,7 @@ jobs: & ./Step_0_Migrate_Project.ps1 -WhatIf $WhatIf - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migration-run-logs path: "./Projects" diff --git a/.github/workflows/ado-migration-process-org-users.yml b/.github/workflows/ado-migration-process-org-users.yml index af95ca0..a2af68e 100644 --- a/.github/workflows/ado-migration-process-org-users.yml +++ b/.github/workflows/ado-migration-process-org-users.yml @@ -6,11 +6,11 @@ on: SourceOrganizationName: description: "Name of the Source Organization" required: true - default: "" + default: "AIZ-GL" TargetOrganizationName: description: "Name of the Target Organization" required: true - default: "" + default: "AIZ-Global" WhatIf: type: boolean description: "WhatIf: Simulated Run" @@ -21,7 +21,7 @@ on: jobs: run-powershell-script: name: Run PowerShell Script - runs-on: 'ubuntu-latest' + runs-on: 'AEC0WGEMP001' timeout-minutes: 7200 env: AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} @@ -37,12 +37,12 @@ jobs: az --version - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Clean Azure DevOps Migration Tools Log Directory shell: pwsh run: | - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse + Get-ChildItem "${{ var.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}/logs" -Recurse | Remove-Item -Recurse - name: Set Migration Configuration shell: pwsh @@ -63,7 +63,7 @@ jobs: $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetOrganizationName }}" $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" $configuration.ProjectDirectory = $projectDirectory - $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $configuration.WorkItemMigratorDirectory = "${{ var.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}" $configuration.RepositoryCloneTempDirectory = "${{ vars.RepositoryCloneTempDirectory }}" $configuration.DevOpsMigrationToolConfigurationFile = "${{ vars.DevOpsMigrationToolConfigurationFile }}" $configuration.ArtifactFeedPackageVersionLimit = ${{ vars.ArtifactFeedPackageVersionLimit }} @@ -81,14 +81,14 @@ jobs: & ./Step_X_Migrate_Org_Level_Users.ps1 -WhatIf $WhatIf - name: Archive DevOps-Enablement-ADO-to-ADO-migration results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migration-run-logs path: "./Projects" - name: Archive Azure DevOps Migration Tools (Martin's Tools) results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migration-tools-logs - path: "${{ vars.WorkItemMigratorDirectory }}/logs" + path: "${{ var.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}/logs" diff --git a/.github/workflows/ado-migration-process-partial.yml b/.github/workflows/ado-migration-process-partial.yml index 7957286..ee78382 100644 --- a/.github/workflows/ado-migration-process-partial.yml +++ b/.github/workflows/ado-migration-process-partial.yml @@ -1,4 +1,5 @@ name: Partial ADO Project Migration +run-name: "[${{ github.event.inputs.MigrationSelection }}] ${{ github.event.inputs.SourceOrganizationName }}/${{ github.event.inputs.SourceProjectName }}=>${{ github.event.inputs.TargetOrganizationName }}/${{ github.event.inputs.TargetProjectName }}" on: workflow_dispatch: # Allows manual triggering via the GitHub Actions UI @@ -6,19 +7,19 @@ on: SourceOrganizationName: description: "Name of the Source Organization" required: true - default: "" + default: "AIZ-Global" SourceProjectName: description: "Name of the Source Project" required: true - default: "" + default: "GH.Lending-Digital" TargetOrganizationName: description: "Name of the Target Organization" required: true - default: "" + default: "AIZ-SSP" TargetProjectName: description: "Name of the Target Project" required: true - default: "" + default: "GH.AppDev-SSP-Backup-POC-V2" MigrationSelection: description: "Migration Selection" required: true @@ -28,7 +29,7 @@ on: - Select an area to migrate - Areas and Iterations - Artifacts - - Build Pipelines + - Build Pipelines & Task Groups - Build Queues & Build Environments - Dashboards - Delivery Plans @@ -38,10 +39,9 @@ on: - Repositories - Service Connections - Service Hooks - - Task Groups - Teams - Test Configurations - - Test Plans and Suites + - Test Plans, Suites, and Cases - Test Variables - Variable Groups - Wikis @@ -56,7 +56,7 @@ on: jobs: run-powershell-script: name: Run PowerShell Script - runs-on: 'ubuntu-latest' + runs-on: 'AEC0WGEMP001' timeout-minutes: 7200 env: AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} @@ -72,13 +72,12 @@ jobs: az --version - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Clean Azure DevOps Migration Tools Log and Attachments Directory + - name: Clean Azure DevOps Migration Tools Log Directory shell: pwsh run: | - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}\\WorkItemAttachmentWorkingFolder" -Recurse | Remove-Item -Recurse + Get-ChildItem "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}/logs" -Recurse | Remove-Item -Recurse - name: Set Migration Configuration shell: pwsh @@ -99,7 +98,7 @@ jobs: $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" $configuration.ProjectDirectory = $projectDirectory - $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $configuration.WorkItemMigratorDirectory = "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}" $configuration.RepositoryCloneTempDirectory = "${{ vars.RepositoryCloneTempDirectory }}" $configuration.DevOpsMigrationToolConfigurationFile = "${{ vars.DevOpsMigrationToolConfigurationFile }}" $configuration.ArtifactFeedPackageVersionLimit = ${{ vars.ArtifactFeedPackageVersionLimit }} @@ -124,8 +123,8 @@ jobs: { & .\MigrateProject.ps1 -SkipMigrateTfsAreaAndIterations $WhatIf } "Artifacts" { & .\MigrateProject.ps1 -SkipMigrateArtifacts $WhatIf } - "Build Pipelines" - { & .\MigrateProject.ps1 -SkipMigrateBuildPipelines $WhatIf } + "Build Pipelines & Task Groups" + { & .\MigrateProject.ps1 -SkipMigrateBuildPipelines $WhatIf } "Build Queues & Build Environments" { & .\MigrateProject.ps1 -SkipMigrateBuildQueues $WhatIf } "Dashboards" @@ -135,46 +134,35 @@ jobs: "Groups" { & .\MigrateProject.ps1 -SkipMigrateGroups $WhatIf } "Policies" - { - & .\helper-scripts\ADODeletePolicies.ps1 -OrgName ${{ github.event.inputs.TargetOrganizationName }} -ProjectName ${{ github.event.inputs.TargetProjectName }} -PAT $env:AZURE_DEVOPS_MIGRATION_PAT -DoDelete (-not $WhatIf) - & .\MigrateProject.ps1 -SkipMigratePolicies $WhatIf - } + { & .\MigrateProject.ps1 -SkipMigratePolicies $WhatIf } "Release Pipelines" { & .\MigrateProject.ps1 -SkipMigrateReleasePipelines $WhatIf } "Repositories" - { - & .\helper-scripts\ADODeleteRepos.ps1 -OrgName ${{ github.event.inputs.TargetOrganizationName }} -ProjectName ${{ github.event.inputs.TargetProjectName }} -PAT $env:AZURE_DEVOPS_MIGRATION_PAT -DoDelete (-not $WhatIf) - & .\MigrateProject.ps1 -SkipMigrateRepos $WhatIf - } + { & .\MigrateProject.ps1 -SkipMigrateRepos $WhatIf } "Service Connections" { & .\MigrateProject.ps1 -SkipMigrateServiceConnections $WhatIf } "Service Hooks" { & .\MigrateProject.ps1 -SkipMigrateServiceHooks $WhatIf } - "Task Groups" - { & .\MigrateProject.ps1 -SkipMigrateTaskGroups $WhatIf } "Teams" { & .\MigrateProject.ps1 -SkipMigrateTeams $WhatIf } "Test Configurations" { & .\MigrateProject.ps1 -SkipMigrateTestConfigurations $WhatIf } - "Test Plans and Suites" - { & .\MigrateProject.ps1 -SkipMigrateTestPlansAndSuites $WhatIf } + "Test Plans, Suites, and Cases" + { & .\MigrateProject.ps1 -SkipMigrateTestPlansAndSuites $WhatIf -SkipMigrateWorkItems $WhatIf -SkipAddReflectedWorkItemIdField $WhatIf -WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] IN ('Test Case')"} "Test Variables" { & .\MigrateProject.ps1 -SkipMigrateTestVariables $WhatIf } "Variable Groups" - { - & .\helper-scripts\ADODeleteVariableGroups.ps1 -OrgName ${{ github.event.inputs.TargetOrganizationName }} -ProjectName ${{ github.event.inputs.TargetProjectName }} -PAT $env:AZURE_DEVOPS_MIGRATION_PAT -DoDelete (-not $WhatIf) - & .\MigrateProject.ps1 -SkipMigrateVariableGroups $WhatIf - } + { & .\MigrateProject.ps1 -SkipMigrateVariableGroups $WhatIf } "Wikis" { & .\MigrateProject.ps1 -SkipMigrateWikis $WhatIf } - "Work Item Queries" + "Work Item Queries" { & .\MigrateProject.ps1 -SkipMigrateWorkItemQuerys $WhatIf } "Work-Items (Including 'Test Cases')" { & .\Step_3_Migrate_Project.ps1 -WhatIf $WhatIf} } - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migration-run-logs path: "./Projects" diff --git a/.github/workflows/ado-migration-process-workitem-backfill-between.yml b/.github/workflows/ado-migration-process-workitem-backfill-between.yml index 68880c5..4cc222c 100644 --- a/.github/workflows/ado-migration-process-workitem-backfill-between.yml +++ b/.github/workflows/ado-migration-process-workitem-backfill-between.yml @@ -1,4 +1,4 @@ -name: Between Dates Work Item Backfill ADO Project Migration +name: sBetween Dates Work Item Backfill ADO Project Migration on: workflow_dispatch: # Allows manual triggering via the GitHub Actions UI @@ -20,11 +20,11 @@ on: required: true default: "" StartDate: - description: "Start Changed Date (dd/mm/yyyy)" + description: "Start Changed Date (month/day/year)" required: true default: "Today" EndDate: - description: "End Changed Date (dd/mm/yyyy)" + description: "End Changed Date (month/day/year)" required: false default: "" WorkItemSelection: @@ -65,7 +65,7 @@ on: jobs: run-powershell-script: name: Run PowerShell Script - runs-on: 'ubuntu-latest' + runs-on: "AEC0WGEMP001" timeout-minutes: 7200 env: AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} @@ -81,15 +81,14 @@ jobs: az --version - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Clean Azure DevOps Migration Tools Log and Attachments Directory + - name: Clean Azure DevOps Migration Tools Log Directory shell: pwsh run: | - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}\\WorkItemAttachmentWorkingFolder" -Recurse | Remove-Item -Recurse + Get-ChildItem "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}/logs" -Recurse | Remove-Item -Recurse - - name: Set Migration Configuration + - name: Set Migration Configuration shell: pwsh run: | $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" @@ -99,7 +98,7 @@ jobs: $LocalConfigPath = "configuration\configuration.json" $filePath = Resolve-Path -Path "$LocalConfigPath" Write-Host "FILEPATH: $filePath" - + $configuration = [Object](Get-Content $LocalConfigPath | Out-String | ConvertFrom-Json) $configuration.SourceProject.Organization = "$SourceOrganizationUrl" $configuration.SourceProject.ProjectName = "${{ github.event.inputs.SourceProjectName }}" @@ -108,16 +107,18 @@ jobs: $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" $configuration.ProjectDirectory = $projectDirectory - $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $configuration.WorkItemMigratorDirectory = "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}" $configuration.RepositoryCloneTempDirectory = "${{ vars.RepositoryCloneTempDirectory }}" $configuration.DevOpsMigrationToolConfigurationFile = "${{ vars.DevOpsMigrationToolConfigurationFile }}" $configuration.ArtifactFeedPackageVersionLimit = ${{ vars.ArtifactFeedPackageVersionLimit }} $configuration | ConvertTo-Json -Depth 100 | Set-Content $LocalConfigPath - + $configuration2 = [Object](Get-Content $LocalConfigPath | Out-String | ConvertFrom-Json) Write-Host (ConvertTo-Json -Depth 100 $configuration2) - run: | + - name: Run Migrate-Project PowerShell script + shell: pwsh + run: | $selection = "${{ github.event.inputs.WorkItemSelection }}" if($selection -eq "Any") { $selection = "" @@ -126,8 +127,8 @@ jobs: $end = "${{ github.event.inputs.EndDate }}" $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" $WhatIf = $whatIfDryRun -match "true" - $format = "dd/MM/yyyy" - + $format = "MM/dd/yyyy" + echo $selection echo $start echo $end @@ -158,17 +159,13 @@ jobs: $WhatIf = $whatIfDryRun -match "true" if($isDateStart -and $isDateEnd) { - if($startDate -eq $endDate){ - $startDate = $startDate.addDays(-1) - } $startDateString = $startDate.ToString($format) $endDateString = $endDate.ToString($format) - & .\Work-Item-Backfill_Migrate_Project.ps1 -StartDate $startDateString -EndDate -endDateString -ItemType $selection -WhatIf $WhatIf + & .\Work-Item-Backfill_Migrate_Project.ps1 -StartDate $startDateString -EndDate $endDateString -ItemType $selection -WhatIf $WhatIf } - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migration-run-logs path: "./Projects" - diff --git a/.github/workflows/ado-migration-process-workitem-backfill.yml b/.github/workflows/ado-migration-process-workitem-backfill.yml index 331c435..7b1a2b8 100644 --- a/.github/workflows/ado-migration-process-workitem-backfill.yml +++ b/.github/workflows/ado-migration-process-workitem-backfill.yml @@ -61,7 +61,7 @@ on: jobs: run-powershell-script: name: Run PowerShell Script - runs-on: 'ubuntu-latest' + runs-on: "AEC0WGEMP001" timeout-minutes: 7200 env: AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} @@ -77,15 +77,15 @@ jobs: az --version - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Clean Azure DevOps Migration Tools Log and Attachments Directory + - name: Clean Azure DevOps Migration Tools Log Directory shell: pwsh run: | - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse - Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}\\WorkItemAttachmentWorkingFolder" -Recurse | Remove-Item -Recurse + Get-ChildItem "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}/logs" -Recurse | Remove-Item -Recurse + Get-ChildItem "c:\\temp\\WorkItemAttachmentWorkingFolder" -Recurse | Remove-Item -Recurse - - name: Set Migration Configuration + - name: Set Migration Configuration shell: pwsh run: | $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" @@ -95,7 +95,7 @@ jobs: $LocalConfigPath = "configuration\configuration.json" $filePath = Resolve-Path -Path "$LocalConfigPath" Write-Host "FILEPATH: $filePath" - + $configuration = [Object](Get-Content $LocalConfigPath | Out-String | ConvertFrom-Json) $configuration.SourceProject.Organization = "$SourceOrganizationUrl" $configuration.SourceProject.ProjectName = "${{ github.event.inputs.SourceProjectName }}" @@ -104,12 +104,12 @@ jobs: $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" $configuration.ProjectDirectory = $projectDirectory - $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $configuration.WorkItemMigratorDirectory = "${{ vars.WORKITEMMIGRATORDIRECTORY_V16_0_9 }}" $configuration.RepositoryCloneTempDirectory = "${{ vars.RepositoryCloneTempDirectory }}" $configuration.DevOpsMigrationToolConfigurationFile = "${{ vars.DevOpsMigrationToolConfigurationFile }}" $configuration.ArtifactFeedPackageVersionLimit = ${{ vars.ArtifactFeedPackageVersionLimit }} $configuration | ConvertTo-Json -Depth 100 | Set-Content $LocalConfigPath - + $configuration2 = [Object](Get-Content $LocalConfigPath | Out-String | ConvertFrom-Json) Write-Host (ConvertTo-Json -Depth 100 $configuration2) @@ -136,7 +136,7 @@ jobs: & .\Work-Item-Backfill_Migrate_Project.ps1 -NumberOfDays $numOfDays -ItemType $selection -WhatIf $WhatIf - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: migration-run-logs path: "./Projects" diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml index 266307b..8a7e81b 100644 --- a/.github/workflows/code-scanning.yml +++ b/.github/workflows/code-scanning.yml @@ -28,7 +28,7 @@ jobs: name: PSScriptAnalyzer runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run PSScriptAnalyzer uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..bd155eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "PowerShell: Interactive Session", + "type": "PowerShell", + "request": "launch" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..23fd35f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/ADO Migration Plan.xlsx b/ADO Migration Plan.xlsx new file mode 100644 index 0000000..81050a4 Binary files /dev/null and b/ADO Migration Plan.xlsx differ diff --git a/MigrateProject.ps1 b/MigrateProject.ps1 new file mode 100644 index 0000000..2efa29a --- /dev/null +++ b/MigrateProject.ps1 @@ -0,0 +1,331 @@ + +Param ( + # -------------- What parts of the migration should NOT be executed --------------- \ + # IntelliTect AzureDevOps-Tools Items + # Pre-step + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateOrganizationUsers = $TRUE, + + # Step 1 + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateBuildQueues = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateRepos = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateWikis = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateServiceConnections = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateTfsAreaAndIterations = $TRUE, + # Step 4 + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateGroups = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateServiceHooks = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigratePolicies = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateDashboards = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateDeliveryPlans = $TRUE, + # Step 5 + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateArtifacts = $TRUE, + + # Azure DevOps Migration Tool Items (Martin's Tool) + # Step 2 + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateTeams = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateTestVariables = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateTestConfigurations = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateTestPlansAndSuites = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateWorkItemQuerys = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateVariableGroups = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateBuildPipelines = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateReleasePipelines = $TRUE, + + # Step 3 + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateWorkItems = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipAddReflectedWorkItemIdField = $TRUE, + [parameter(Mandatory = $FALSE)] [String]$WorkItemQueryBit = "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') ORDER BY [System.ChangedDate] DESC" +) + + +# Import-Module Migrate-ADO -Force + +Import-Module .\modules\Migrate-ADO-AreaPaths.psm1 +Import-Module .\modules\Migrate-ADO-IterationPaths.psm1 +Import-Module .\modules\Migrate-ADO-Users.psm1 +Import-Module .\modules\Migrate-ADO-Teams.psm1 +Import-Module .\modules\Migrate-ADO-Groups.psm1 +Import-Module .\modules\Migrate-ADO-BuildQueues.psm1 +Import-Module .\modules\Migrate-ADO-BuildEnvironments.psm1 +Import-Module .\modules\Migrate-ADO-Repos.psm1 +Import-Module .\modules\Migrate-ADO-Retention.psm1 +Import-Module .\modules\Migrate-ADO-Wikis.psm1 +Import-Module .\modules\Migrate-ADO-Common.psm1 +Import-Module .\modules\Migrate-ADO-Pipelines.psm1 +Import-Module .\modules\Migrate-ADO-Project.psm1 +Import-Module .\modules\Migrate-ADO-ServiceHooks.psm1 +Import-Module .\modules\Migrate-ADO-ServiceConnections.psm1 +Import-Module .\modules\Migrate-ADO-VariableGroups.psm1 +Import-Module .\modules\Migrate-ADO-Policies.psm1 +Import-Module .\modules\Migrate-ADO-Dashboards.psm1 +Import-Module .\modules\Migrate-ADO-BuildDefinitions.psm1 +Import-Module .\modules\Migrate-ADO-ReleaseDefinitions.psm1 +Import-Module .\modules\Migrate-ADO-Artifacts.psm1 +Import-Module .\modules\Migrate-ADO-DeliveryPlans.psm1 +Import-Module .\modules\ADO-AddCustomField.psm1 +Import-Module .\modules\Migrate-Packages.psm1 + + +Write-Log -Message "SkipMigrateBuildQueues $($SkipMigrateBuildQueues)" +Write-Log -Message "SkipMigrateRepos $($SkipMigrateRepos)" +Write-Log -Message "SkipMigrateWikis $($SkipMigrateWikis)" +Write-Log -Message "SkipMigrateServiceConnections $($SkipMigrateServiceConnections)" +Write-Log -Message "SkipMigrateGroups $($SkipMigrateGroups)" +Write-Log -Message "SkipMigrateServiceHooks $($SkipMigrateServiceHooks)" +Write-Log -Message "SkipMigratePolicies $($SkipMigratePolicies)" +Write-Log -Message "SkipMigrateDashboards $($SkipMigrateDashboards)" +Write-Log -Message "SkipMigrateDeliveryPlans $($SkipMigrateDeliveryPlans)" +Write-Log -Message "SkipMigrateArtifacts $($SkipMigrateArtifacts)" + + +# Azure DevOps Migration Tool Items +Write-Log -Message "SkipMigrateTfsAreaAndIterations $($SkipMigrateTfsAreaAndIterations)" +Write-Log -Message "SkipMigrateTeams $($SkipMigrateTeams)" +Write-Log -Message "SkipMigrateTestVariables $($SkipMigrateTestVariables)" +Write-Log -Message "SkipMigrateTestConfigurations $($SkipMigrateTestConfigurations)" +Write-Log -Message "SkipMigrateTestPlansAndSuites $($SkipMigrateTestPlansAndSuites)" +Write-Log -Message "SkipMigrateWorkItemQuerys $($SkipMigrateWorkItemQuerys)" +Write-Log -Message "SkipMigrateVariableGroups $($SkipMigrateVariableGroups)" +Write-Log -Message "SkipMigrateBuildPipelines $($SkipMigrateBuildPipelines)" +Write-Log -Message "SkipMigrateReleasePipelines $($SkipMigrateReleasePipelines)" +Write-Log -Message "SkipMigrateWorkItems $($SkipMigrateWorkItems)" +Write-Log -Message " " +Write-Log -Message "WorkItemQueryBit: $($WorkItemQueryBit)" +Write-Log -Message " " + + + +# ------------------------------------------------------------------------------------- +# ---------------- Set up files for logging & get configuration values ---------------- +#region ------------------------------------------------------------------------------- +$runDate = (get-date).ToString('yyyy-MM-dd HHmmss') + +$configFile = 'configuration.json' +$configPath = 'configuration\' +$filePath = Resolve-Path -Path "$configPath$configFile" + +if ($NULL -eq $filePath) { + Write-Log -Message 'Unable to locate configuration.json file which is required!' -LogLevel ERROR + exit +} + +Write-Host "Configuration.json file found.." +$configuration = [Object](Get-Content "$configPath$configFile" | Out-String | ConvertFrom-Json -Depth 100) + +$SourceProject = $configuration.SourceProject +$TargetProject = $configuration.TargetProject +$SourceProjectName = $configuration.SourceProject.ProjectName +$TargetProjectName = $configuration.TargetProject.ProjectName +$ProjectDirectory = Get-Location +$WorkItemMigratorDirectory = $configuration.WorkItemMigratorDirectory +$RepositoryCloneTempDirectory = $configuration.RepositoryCloneTempDirectory +$DevOpsMigrationToolConfigurationFile = $configuration.DevOpsMigrationToolConfigurationFile +$ArtifactFeedPackageVersionLimit = $configuration.ArtifactFeedPackageVersionLimit + +Write-Host "CONFIGURATION:" +Write-Host $configuration + +# Get project folder & set logging path w/ env variable +$projectPath = Get-ProjectFolderPath ` + -RunDate $runDate ` + -SourceProject $SourceProjectName ` + -TargetProject $TargetProjectName ` + -Root $ProjectDirectory + +$env:MIGRATION_LOGS_PATH = $projectPath + +# Either separate source and target tokens or same token for source and target +$sourcePat = $env:AZURE_DEVOPS_MIGRATION_SOURCE_PAT +$targetPat = $env:AZURE_DEVOPS_MIGRATION_TARGET_PAT +$pat = $env:AZURE_DEVOPS_MIGRATION_PAT +If ($NULL -eq $sourcePat) { $sourcePat = $pat } +If ($NULL -eq $targetPat) { $targetPat = $pat } + + +# ========================================== +# = Configure Azure DevOps Migration Tool = +# Martin's Tool +#region ==================================== + +Write-Host "Configure Azure DevOps Migration Tool (Martin's Tool).." + +$martinConfigPath = "$($ProjectDirectory)\$($configPath)$DevOpsMigrationToolConfigurationFile" +$martinConfiguration = [Object](Get-Content $martinConfigPath | Out-String | ConvertFrom-Json -Depth 100) +$martinPreviousConfiguration = [Object](Get-Content $martinConfigPath | Out-String | ConvertFrom-Json -Depth 100) + + +# --------------------------------------- +# -- End Point Source/Target settings -- +# --------------------------------------- +$targetOrg = $configuration.TargetProject.OrgName +Write-Log "targetOrg: $targetOrg" +$url = "https://dev.azure.com/$($targetOrg)/_apis/wit/fields/ReflectedWorkItemId?api-version=7.1-preview.2" +$targetHeaders = New-HTTPHeaders -PersonalAccessToken $targetPat +$DesiredProcessFieldResponse = Invoke-RestMethod -Uri $url -Headers $targetHeaders + +$AlternateNameFieldForReflectedWorkItemId = "" +if ($null -ne $DesiredProcessFieldResponse -AND $DesiredProcessFieldResponse.referenceName -ne "Custom.ReflectedWorkItemId") { + $AlternateNameFieldForReflectedWorkItemId = $DesiredProcessFieldResponse.referenceName + Write-Log "Found existing RefelectedWorkItemId field to be configured in migration-configuration.json" +} + +foreach ($endpoint in $martinConfiguration.MigrationTools.Endpoints.PSObject.Properties) { + + Write-Host "Name: $($endpoint.Name)" + + if ($endpoint.Name -like "*Source") { + $endpoint.Value.Project = $SourceProject.ProjectName + + if ($endpoint.Value.EndpointType -eq "AzureDevOpsEndpoint") { + $endpoint.Value.Organisation = $SourceProject.Organization + $endpoint.Value.AccessToken = $sourcePat + } + else { + $endpoint.Value.Collection = $SourceProject.Organization + $endpoint.Value.Authentication.AccessToken = $sourcePat + } + } + elseif ($endpoint.Name -like "*Target") { + $endpoint.Value.Project = $TargetProject.ProjectName + if ($endpoint.Value.EndpointType -eq "AzureDevOpsEndpoint") { + $endpoint.Value.Organisation = $TargetProject.Organization + $endpoint.Value.AccessToken = $targetPat + } + else { + $endpoint.Value.Collection = $TargetProject.Organization + $endpoint.Value.Authentication.AccessToken = $targetPat + } + + # This replacement only occurs when there is an existing process field named 'ReflectedWorkItemId' which does not have a reference name of Custom.RefelctedWorkItemId + if (-not [string]::IsNullOrEmpty($AlternateNameFieldForReflectedWorkItemId)) { + $endpoint.Value.ReflectedWorkItemIdField = $AlternateNameFieldForReflectedWorkItemId + } + } + +} + +# -------------------------------------------------- +# ----- Azure DevOps Migration Tool Processors ----- +# - enable which processors we execute - +# -------------------------------------------------- +foreach ($processor in $martinConfiguration.MigrationTools.Processors) { + if ($processor.ProcessorType -eq "TfsTeamSettingsProcessor") { + if (($processor.Enabled -ne !$SkipMigrateTeams)) { + $processor.Enabled = !$SkipMigrateTeams + } + } + elseif ($processor.ProcessorType -eq "TfsTestVariablesMigrationProcessor") { + if (($processor.Enabled -ne !$SkipMigrateTestVariables)) { + $processor.Enabled = !$SkipMigrateTestVariables + } + } + elseif ($processor.ProcessorType -eq "TfsTestConfigurationsMigrationProcessor") { + if (($processor.Enabled -ne !$SkipMigrateTestConfigurations)) { + $processor.Enabled = !$SkipMigrateTestConfigurations + } + } + elseif ($processor.ProcessorType -eq "TfsTestPlansAndSuitesMigrationProcessor") { + if (($processor.Enabled -ne !$SkipMigrateTestPlansAndSuites)) { + $processor.Enabled = !$SkipMigrateTestPlansAndSuites + } + } + elseif ($processor.ProcessorType -eq "TfsSharedQueryProcessor") { + if (($processor.Enabled -ne !$SkipMigrateWorkItemQuerys)) { + $processor.Enabled = !$SkipMigrateWorkItemQuerys + } + } + elseif ($processor.ProcessorType -eq "AzureDevOpsPipelineProcessor") { + # MigrateBuildPipelines + if (($processor.MigrateBuildPipelines -ne !$SkipMigrateBuildPipelines)) { + $processor.MigrateBuildPipelines = !$SkipMigrateBuildPipelines + } + + if (($processor.Enabled -ne !$SkipMigrateBuildPipelines) -or (!$SkipMigrateBuildPipelines)) { + $processor.Enabled = !$SkipMigrateBuildPipelines + + # RepositoryNameMaps + if ($processor.Enabled -eq $TRUE) { + $processor.RepositoryNameMaps = @{ + "$($SourceProject.ProjectName)" = "$($TargetProject.ProjectName)" + } + } + else { + $processor.RepositoryNameMaps = $NULL + } + } + } + elseif (($processor.ProcessorType -eq "TfsWorkItemMigrationProcessor") -or ($processor.ProcessorType -eq "WorkItemTrackingProcessorOptions")) { + if (($processor.Enabled -ne !$SkipMigrateWorkItems) -or ($processor.WIQLQuery -ne $WorkItemQueryBit)) { + $processor.Enabled = !$SkipMigrateWorkItems + $processor.WIQLQuery = $WorkItemQueryBit + } + } +} + +$SkipAzureDevOpsMigrationTool = ( ` + $SkipMigrateTeams -and ` + $SkipMigrateTestVariables -and ` + $SkipMigrateTestConfigurations -and ` + $SkipMigrateTestPlansAndSuites -and ` + $SkipMigrateWorkItemQuerys -and ` + $SkipMigrateBuildPipelines -and ` + $SkipMigrateWorkItems +) + + +$martinConfiguration | ConvertTo-Json -Depth 100 | Set-Content $martinConfigPath +$configString = $martinConfiguration | ConvertTo-Json -Depth 100 +Write-Host "Configuration after substitution:" +Write-Host $configString +#endregion + + +# ======================================== +# ========== Important Notes ============= +# ======================================== +# - When migrating service connections make sure you have proper permissions on +# zure Active Directory and you can grant Contributor role to the subscription +# that was chosen. + + +# ======================================== +# ========== Migrate Project ============= +#region ================================== +Start-ADOProjectMigration ` + -SourceOrgName $configuration.SourceProject.OrgName ` + -SourceProjectName $SourceProjectName ` + -SourcePAT $sourcePat ` + -TargetOrgName $configuration.TargetProject.OrgName ` + -TargetProjectName $TargetProjectName ` + -TargetPAT $targetPat ` + -ProjectPath $projectPath ` + -RepositoryCloneTempDirectory $RepositoryCloneTempDirectory ` + -MartinsToolConfigurationFile $martinConfigPath ` + -WorkItemMigratorDirectory $WorkItemMigratorDirectory ` + -DevOpsMigrationToolConfigurationFile $DevOpsMigrationToolConfigurationFile ` + -ArtifactFeedPackageVersionLimit $ArtifactFeedPackageVersionLimit ` + -SkipMigrateGroups $SkipMigrateGroups ` + -SkipMigrateBuildQueues $SkipMigrateBuildQueues ` + -SkipMigrateRepos $SkipMigrateRepos ` + -SkipMigrateWikis $SkipMigrateWikis ` + -SkipMigrateServiceHooks $SkipMigrateServiceHooks ` + -SkipMigratePolicies $SkipMigratePolicies ` + -SkipMigrateDashboards $SkipMigrateDashboards ` + -SkipMigrateServiceConnections $SkipMigrateServiceConnections ` + -SkipMigrateArtifacts $SkipMigrateArtifacts ` + -SkipMigrateDeliveryPlans $SkipMigrateDeliveryPlans ` + -SkipAzureDevOpsMigrationTool $SkipAzureDevOpsMigrationTool ` + -SkipMigrateOrganizationUsers $SkipMigrateOrganizationUsers ` + -SkipAddReflectedWorkItemIdField $SkipAddReflectedWorkItemIdField ` + -SkipMigrateVariableGroups $SkipMigrateVariableGroups ` + -SkipMigrateBuildPipelines $SkipMigrateBuildPipelines ` + -SkipMigrateReleasePipelines $SkipMigrateReleasePipelines ` + -SkipMigrateTfsAreaAndIterations $SkipMigrateTfsAreaAndIterations +#endregion + + +# Clean up old martin's tool Configuration +Write-Host "Clean up Configuration file for Azure DevOps Migration Tool (Martin's Tool).." + +$martinPreviousConfiguration | ConvertTo-Json -Depth 100 | Set-Content $martinConfigPath + diff --git a/Migration steps list.md b/Migration steps list.md new file mode 100644 index 0000000..0c8e7b2 --- /dev/null +++ b/Migration steps list.md @@ -0,0 +1,50 @@ +Migration Step Order (Full Migration) +--------------------------------------- + +- Build Queues (Project Agent Pools) +- Build Environments done with Build Queues +- Repositories +- Wikis +- Service Connections +- Areas and Iterations +- Teams +- Work Item Querys +- Variable Groups +- Build Pipelines +- Release Pipelines +- Task Groups +- Work Items +- Test Cases +- Test Configurations +- Test Variables +- Test Plans, Suites, and Cases +- Service Hooks +- Policies +- Dashboards +- Delivery Plans +- Artifacts + +Partial Migrations Execution Order +--------------------------------- +- Repositories +- Service Connections +- Wikis +- Areas and Iterations +- Teams +- Work Item Querys +- Variable Groups +- Build Pipelines & Task Groups +- Release Pipelines +- Work-Items +- Groups +- Test Configurations +- Test Variables +- Test Plans, Suites, and Cases +- Service Hooks +- Policies (this will delete polices, then re-migrate them ) +- Dashboards +- Delivery Plans +- Artifacts + + + diff --git a/README - Github Action Workflow migration.md b/README - Github Action Workflow migration.md new file mode 100644 index 0000000..0b0cab5 --- /dev/null +++ b/README - Github Action Workflow migration.md @@ -0,0 +1,47 @@ +# Migration through GitHub Action Workflows +There are three GitHub Action Workflows to for ADO project migration using the Project Migration scripts outlined in the "README - Project Migration.md" file. + +These Workflows are outlined below: + +## "Run ADO Organization User Migration" +Use this Action Workflow in order to migrate Azure DevOps organization level users from a Source organization to a Target organization. +Fill in the input boxes with the Source and target information and run the workflow. + +***Please Note:***
+On all of the workflows there is a "Whatif" checkbox input option which allows you to run a Dry Run to test connectivity to the powershell scripts. +No data will actually be migrated if the WhatIf checkbox is checked. + +![Alt text](images/user-migration-workflow.png) + +## "Run Full ADO Project Migration" +The Full Run Action Workflow is used to process a FULL ADO project to Project migration. This will perform all of the migrations scripts described in the "README - Project Migration.md" file. +The process is run in consecutive steps which provide the correct sequence for dependencies. + +![Alt text](images/user-migration-workflow.png) + +## "Run Partial ADO Project Migration" + +The last Action Workflow is the Partial migration. Use this to re-run sections of a full migration. This workflow will be used to do delta-backflow migrations in areas such as work-items and also for testing and correcting any migration issues. Each of the areas of migration are contained in a drop-down selection box labeled "Migration Selection". Use this input option to select the area that you would like to migrate separately. + +![Alt text](images/partial-migration-workflow.png) + +## Settings for the GitHub Action Workflows + +### Variables +There are a few variables that each of the workflows share. These variables are define under the Settings tab for the repository. +![Alt text](images/settings.png) + +Under "Secrets and Varables" for Actions there are bth Variables and Secrets defined. + +The following three variables need to be defined here for the migration process to function. +- DEVOPSMIGRATIONTOOLCONFIGURATIONFILE = The name of the configuration file for the Azure DevOps Migration Tool (Martin's Tool). The default value is "migrator-configuration.json". +- WORKITEMMIGRATORDIRECTORY = The file path on the server where the GitHub Action Workflow runner can fine the Azure DevOps Migration Tool (Martin's Tool) executable. +- ARTIFACTFEEDPACKAGEVERSIONLIMIT = An integer value representing the maximum number of Artifact Feed Package versions to migrate. Default is -1 which tells the migration script to migrate all package versions. + +![Alt text](images/variables.png) + +### Secrets +There is one required secret that needs to be defined here for the migration process to run. This is the Personal Access Token that the process will use to make calls to the Azure DevOps REST API. +The Token name is AZURE_DEVOPS_MIGRATION_PAT and should contain a token that has access to both Source and Target projects and has "Basic + Test Plans" licensing access. + +![Alt text](images/secret.png) diff --git a/README - Project Migration.md b/README - Project Migration.md new file mode 100644 index 0000000..83fe11f --- /dev/null +++ b/README - Project Migration.md @@ -0,0 +1,262 @@ + +# Azure DevOps Project Migration + +## prerequisites to migration +- The target project needs to be created using a process process template that mirrors the source process template. If needed the source template can be migrated to the target organization. + + - We use the Microsoft Process Migrator (https://github.com/microsoft/process-migrator) to migrate the process template by exporting, editing if needing and importing the template in json format. + - process-migrator --mode=export --config="C:\Users\JohnEvans\Working\Process_Migrate\configuration.json" + - process-migrator --mode=import --config="C:\Users\JohnEvans\Working\Process_Migrate\configuration.json" + +- Users in source organization that are not also in the target organization need to be migrated + - There is a script to perform this user migration. + +- Token needs to be created that can access both source and target organizations and has "Basic + Test Plans" licensing access. + +- Install any extensions etc used in Source that are not installed already in the target organization. + +- Delete any unneeded/unused Service Connections, Agent Pools, Teams, Groups, Pipelines, Dashboards etc. so that they are not migrated minimizing chances for failures. + +This tool is used for migrating an Azure DevOps (ADO) project to another project location either within the same organization or to another. +It consists of a set of PowerShell and an external .NET application that handles to migration of various components of the ADO project. + +The PowerShell scripts are comprised of a set of modules and a set of helper scripts that each perform various tasks. + +An external migration tool is also utilized called "Azure DevOps Migration Tools" by Naked Agility, also known as Martin's Tool (https://nkdagility.com/learn/azure-devops-migration-tools/) after the auther Martin Hinshelwood. This tool performs the majority of the ADP migration tasks while the PowerShell Scripts picks up where this migration lacks. +* Follow the installation instructions here https://nkdagility.com/learn/azure-devops-migration-tools/getting-started/ to install this tool locally. + +## "modules" Directory +The modules directory contains the PowerShell *.psm1 files each performing a migration of a particular ADO component. + +### The following module files are contained in the modules directory: +``` +Migrate-ADO-AreaPaths.psm1 +Migrate-ADO-IterationPaths.psm1 +Migrate-ADO-Users.psm1 +Migrate-ADO-Teams.psm1 +Migrate-ADO-Groups.psm1 +Migrate-ADO-BuildQueues.psm1 +Migrate-ADO-BuildEnvironments.psm1 +Migrate-ADO-Repos.psm1 +Migrate-ADO-Wikis.psm1 +Migrate-ADO-Common.psm1 +Migrate-ADO-Pipelines.psm1 +Migrate-ADO-Project.psm1 +Migrate-ADO-ServiceHooks.psm1 +Migrate-ADO-ServiceConnections.psm1 +Migrate-ADO-VariableGroups.psm1 +Migrate-ADO-Policies.psm1 +Migrate-ADO-Dashboards.psm1 +Migrate-ADO-BuildDefinitions.psm1 +Migrate-ADO-ReleaseDefinitions.psm1 +Migrate-ADO-Artifacts.psm1 +Migrate-ADO-DeliveryPlans.psm1 +ADO-AddCustomField.psm1 +Migrate-Packages.psm +``` + +## "helper-scripts" Directory +This directory contains scripts that were written to provide reports or information that aided in the preperations for project migrations. These PowerShell scripts are not needed for migration but may prove to be useful during the process. + +## "configuration" Directory +This directory is critical to the process of migrating ADO projects. This directory contains to json formatted process configuration files which will provide the PowerShell scripts with required data to execute on. +The first file is named configuration.json which will need to be edited and filled out per source project being migrated. In this file you will define inforamtion for the source ADO project and the target ADO project along with the organization(s) and directory file paths. +Below is what this information looks like: + +``` +{ + "SourceProject": { + "Organization": "https://dev.azure.com/[ORGANIZATION]", + "ProjectName": "[PROJECT_NAME]", + "OrgName": "[ORGANIZATION-NAME]" + }, + "TargetProject": { + "Organization": "https://dev.azure.com/[ORGANIZATION]", + "ProjectName": "[PROJECT_NAME]", + "OrgName": "[ORGANIZATION-NAME]" + }, + "ProjectDirectory": "C:\\DevOps-ADO-migration", + "WorkItemMigratorDirectory": "C:\\tools\\MigrationTools", + "DevOpsMigrationToolConfigurationFile": "migrator-configuration.json" +} +``` + + +## Configuration.json +The `Configuration.json` file is used to set up file locations for logging, and other information required for running the `MigrateProject.ps1` script. This script is the entry point for executing all other PowerShell script migration steps. + +##### PROPERTIES +| Property Name | Data Type | Description +|---------------------------|-----------|------------- +| TargetProject | Object | An object consisting of an OrgName and a PAT +| └─ Organization | String | The organization name for the target project +| └─ ProjectName | String | The name of the project being migrated +| SourceProject | Object | An object consisting of an Organization and a PAT +| └─ Organization | String | The organization name for the source project +| └─ ProjectName | String | The name of the project on the target after migration +| ProjectDirectory | String | The directory where logging, repos and auto-generated configuration files will be placed. Make sure this path is not nested too deeply or file paths may be too long. +| WorkItemMigratorDirectory | String | This is the directory where the "Azure DevOps Migration Tools" aka Martin's Tool was installed. +---------- +

+ +## Migration Steps PowerShell Scripts +The entire process is initiated through PowerShell Scripts. Use of "Martin's Tool" is done through the PowerShell migration scripts. The entire process is set up in steps which are executed sequentially. +There is also a script that will esecute the entire process by calling the step scripts in the proper sequence. + +**Note:** The follwoing scripts may require editing depending on project requirements, Work Item counts per ChangedDate period used or other cases where errors occur due to project specifics. + +#### Step_X_Migrate_Org_Level_Users.ps1 - will execute all other steps sequentially +#### Step_1_Migrate_Project.ps1 + Build Queues + Repos + Wikis + Service Connections +#### Step_2_Migrate_Project.ps1 + Area and Iterations + Teams + Work Item Querys + Variable Groups + Build Pipelines + Release Pipelines + Task Groups +#### Step_3_Migrate_Project.ps1 + Work Items (Including 'Test Cases') + - Work items are batched due to the limitation of the Azure DevOps REST API which is 20,000 items. + - When doing a full Work-Item migration, items are migrated based on the CreatedDate attribute. + This is because the query used to search for Work-Items is executed both on the Source and Target + projects. Since all items will have a changed date of the date the migration took place, the query + will include all items when run against the Target project. If there are over 20,000 items, this + will results in an error because there is a limit of 20,000 items for the REST API dealing with + work-items. + + In steps where CreatedDate Between + 0 - 100 + 100 - 200 + 200 - 300 + 300 - 400 + 400 - 500 + 500 - 600 + 600 - 700 + 800 - 800 + 800 - 900 + 900 - 1000 + 1000 - 1100 + 1100 - 1200 + 1200 - 1300 + 1300 - 1500 + 1500 - 3000 + 3000 + + +#### Step_4_Migrate_Project.ps1 + This step is executed in two parts 4A and 4B. The first step is performed by "Martin's Tool" and the second half is performed by PowerShell scripts. + first: + Test Configurations + TestV ariables + Test PlansAndSuites + + second: + Groups + Service Hooks + Policies + Dashboards + Delivery Plans + +#### Step_5_Migrate_Project.ps1 + Artifacts + + +These steps can each be called one at a time or the Step_0_Migrate_Project.ps1 file can be called to call all of the steps sequentially. + +There is an additional script that is used prior to the actual project migration to migrate organization user accounts from one organization to another named "Step_X_Migrate_Org_Level_Users.ps1". This is needed and requried to be run before the project migration or components such as work items can fail. + + +# migrator-configuration.json +This configuration file is used by the "Azure DevOps Migration Tools" aka Martin's Tool to perform various migration steps. + +**THIS FILE SHOULD NOT BE EDITED MANUALLY** + +This file will be edied when calls are made to the MigrateProject.ps1 script. The MigrateProject.ps1 script is either called directly in order to execure select component items or is called by the Step_X_Migrate scripts. + + + +# create-manifest.ps1 +The `create-manifest.ps1` script creates a new PowerShell distribution manifest file (.psd1) under your `Documents\WindowsPowerShell\Modules` directory. This allows you to use the command `Import-Module Migrate-ADO` to import all of the modules listed under the `$IncludedModules` list in the file. + +If used, this script should be run when the repo is first cloned and whenever the `create-manifest.ps1` script is updated. + +The module files are imported as separate module files and not as a published module. It is not needed to publish the files in the modules directory to execute the project migration. + +# MigrateProject.ps1 +The `MigrateProject.ps1` script is the starting point for preforming a full migration of the following DevOps items: + +----------- +### Step 1 +- Build Queues + - Using the `Start-ADOBuildQueuesMigration` cmdlet under `modules` directory. +- Build Environments + - Using the `Start-ADOBuildEnvironmentsMigration` cmdlet under `modules` directory. +- Repositories + - Using the `Start-ADORepoMigration` cmdlet under `modules` directory. +- Wikis + - Using the `Start-ADOWikiMigration` cmdlet under `modules` directory. +- Service Connections + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +
+ +----------- +### Step 2 +#### via Martin's Tool +- Areas and Iterations +- Teams +- Test Variables +- Test Configurations +- Test Plans and Suites +- Work Item Queries +- Variable Groups +- Build Pipelines +- Release Pipelines +- Task Groups +
+ +----------- +### Step 3 +#### via Martin's Tool +- Work Items +
+ +----------- +### Step 4 +- Groups + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +- Service Hooks + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +- Policies + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +- Dashboards + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +- Delivery Plans + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +
+----------- +### Step 5 +- Artifacts + - Using the `Start-ADOServiceConnectionsMigration` cmdlet under `modules` directory. +

+----------- + +# Migration Notes +- Default iteration path is not set for a team +- Default area path is not set for a team +- Wikis get migrated as repositories and need to be re-connected to wiki after migration + - https://learn.microsoft.com/en-us/azure/devops/project/wiki/provisioned-vs-published-wiki?view=azure-devops +- Dashboard Widgets will need to be re-tied to Work-Item queries + +# Set source to read only +- set repos isDisabled flag to true (manually via UI this pass) +- Move all members of Contributors to Readers. members of groups such as Project Admins, Build Admins, project Collection admins are not affected. Additionally, any specific user assignments will still be valid + +# Prior to Project Migration +- If migrating from one organization to another, it is recommended that all User Identities in the Source organization be migrated to the target migration. This allows any ADO components assigned to that user, such as Work Items and Test Plans etc., to be nigrated without error. The user can then be changed after migration. +- Azure RM Service Connection must be created prior to project migration. Service Connection credentials cannot be migrated. +- Target organization must have a new enherited Process Template created and a custom field named xxxxx added to all of the Work-Item types. See this documentation for more details: https://nkdagility.com/learn/azure-devops-migration-tools/server-configuration/ + diff --git a/README.md b/README.md index 69adf65..de6ba8e 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,8 @@ -# Introduction -Azure DevOps references, tools, how-tos - -## DevOps Related Links and References - -- [IntelliTect's Kevin Bost on GitKracken](https://www.youtube.com/watch?time_continue=2&v=4UvCz4BQnW0) - -- [MS Build Parameters](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference?view=vs-2015&redirectedfrom=MSDN) - -
- -# Migration Tool Summary +# Migration Scripts Directory This directory holds pre-written scripts and configuration files that links all of the migration modules under the `supporting-modules` directory as well as the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) to preform a full DevOps migration. --- -
-The AzureDevOps-Tools for project migration consists of a collection of PowerShell Scripts used in conjunction with the [Azure DevOps Migration Tools](https://nkdagility.com/learn/azure-devops-migration-tools/)) to migrate a project from one Azure DevOps Organization to another. The Azure DevOps Migration Tools is used to migrate areas such as Work-Items while other areas of migration not supported by this tool are handled via PowerShell Scripts usind the Azure REST API. -

-Depending on your needs, there is also the option of using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator)) tool for some of your migration needs. -

- -## .github Directory - -This directory is user by GitHub and contains GitHub Action Workflow yml files for executing ADO to ADO project migrations using GitHub Action Workflows. - -#### ado-migration-process-full Workflow - Ths workflow is used to initiate a full project migration which consists of executing these areas of migration: - - Areas and Iterations -- Artifacts -- Build Pipelines -- Build Queues & Build Environments -- Dashboards -- Delivery Plans -- Groups -- Policies -- Release Pipelines -- Repositories -- Service Connections -- Service Hooks -- Task Groups -- Teams -- Test Configurations -- Test Plans and Suites -- Test Variables -- Variable Groups -- Wikis -- Work Item Queries -- Work-Items (Including 'Test Cases') - - -#### ado-migration-process-org-users - This Workflow is used to migrate all Users from the Source organization to the Target organization. This is usually done first so that the migration tools can locate and assiciate users to Work-Items and other data points. - -#### ado-migration-process-partial - This Workflow is used to execute partial migrations. You would supply the area to be migrated from a dropdown input parameter based on the areas of migration listed above. - -> **Note** -> : Some areas of migration are dependent upon others. Use caution when selecting areas to migrate when dependent areas have not been migrated first. For example, Areas and Iterations should be migrated before migrating work-Items. - -There are two GitHub Action Workflows specifically for migrating Work-Items outside of a full or partial migration. - -#### ado-migration-process-workitem-backfill-between - This Workflow will provide a means to migate Work-Items based on a ChangedDate value between two dates. - -#### ado-migration-process-workitem-backfill - Use this workflow when you have run a migration but are in the process of testing prior to a Production Cut-Over in order to update Work-Items from the Source that have changed. This Work-Flow allows you to set a number-of-days value. This value tells the migration script to look for items that have changed today back a select number of days prior. -If the Default value of 0 is left configured then the tool will look for items that have a ChangedDate the day of the Workflow execution date. A value of 1 would locate items the day of and 1 day prior to the execution date etc. - - -## Configuration Directory - -The configuration directory contains json formatted configuration files for the AzureDevOps-Tools, Azure DevOps Migration Tools and Microsoft VSTS Work Item Migrator tool. -For more information read more here: : [README - Configuration.md](/configuration/README%20-%20Configuration.md) - -## Documentation Directory - -Find more information regarding the migration process for Azure DevOps as well as GitHub here. - -## Github Tools Directory - -If migrating from ADO to GitHub there is a MigrateProject PowerShell script for - -## Helper-Scripts - -This directory contains powerShell scripts that are used during the ADO project migration. They are called by other scripts mainly in migration areas where compenents are deleted and re-created in the Target project. - -For more information read more here: : [README - Helper-Scripts.md](/helper-scripts/README - Helper-Scripts.md) - -## Images - -The images directory contains images that are shown within documentation *.md files. - -# Modules - -This directory contains the many module (*.psm1) files that perform the bulk of ADO project component migrations. - -For more information read more here: : [README - Helper-Scripts.md](/modules/README - Modules.md) - - - - - - - - - - - - - - - - - - - - - - - - - - - - -






















# Projects.csv The `Projects.csv` file is where you define a list of source projects and the corresponding target project to migrate to. ```csv diff --git a/Step_0_Migrate_Project.ps1 b/Step_0_Migrate_Project.ps1 new file mode 100644 index 0000000..505fb47 --- /dev/null +++ b/Step_0_Migrate_Project.ps1 @@ -0,0 +1,86 @@ + +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "-------------------------------------------" +Write-Host " Begin Project Migration " +Write-Host "-------------------------------------------" + +# Best to do a user migration first since all of the other items can reference users and groups + +<# + Step #1 migrate + - Build Queues (Project Agent Pools) + - Build Environments done with Build Queues + - Repositories + - Wikis + - Service Connections +#> +& .\Step_1_Migrate_Project.ps1 -WhatIf $WhatIf + +<# + Step #2 migrate + - Areas and Iterations + - Teams + - Work Item Querys + - Variable Groups + - Build Pipelines + - Release Pipelines + - Task Groups +#> +& .\Step_2_Migrate_Project.ps1 -WhatIf $WhatIf + +<# + Step #3 migrate + - Work Item Types belonging to the target process template will have ReflectedWorkItemId field added + - Work Items (Including 'Test Cases') + In batches where Created Date Between + 0 - 100 + 100 - 200 + 200 - 300 + 300 - 400 + 400 - 500 + 500 - 600 + 600 - 700 + 800 - 800 + 800 - 900 + 900 - 1000 + 1000 - 1100 + 1100 - 1200 + 1200 - 1300 + 1300 - 1500 + 1500 - 3000 + 3000 + + + Since the Azure REST API for work items has a query limit if 20,000, calls to the API have been broken up into batches based on the Work item's Created Date field + Each batch is listed below with the expected work item count to be migrated. The work item counts may vary since the work items are being updated daily. +#> +& .\Step_3_Migrate_Project.ps1 -WhatIf $WhatIf + +<# + Step #4 migrate + - Test Cases + - Groups + - Test Configurations + - Test Variables + - Test Plans and Suites + - Service Hooks + - Policies + - Dashboards + - Delivery Plans +#> +& .\Step_4_Migrate_Project.ps1 -WhatIf $WhatIf + +<# + Step #5 migrate + - Artifacts +#> +& .\Step_5_Migrate_Project.ps1 -WhatIf $WhatIf + + +Write-Host "------------------------------------------------" +Write-Host " Completed Project MIgration " +Write-Host "------------------------------------------------" + diff --git a/Step_1_Migrate_Project.ps1 b/Step_1_Migrate_Project.ps1 new file mode 100644 index 0000000..0ea9916 --- /dev/null +++ b/Step_1_Migrate_Project.ps1 @@ -0,0 +1,20 @@ +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Step 1 Migrate" +Write-Host " - Build Queues (Project Agent Pools)" +Write-Host " - Build Environments done with Build Queues" +Write-Host " - Repositories" +Write-Host " - Wikis" +Write-Host " - Service Connections" +Write-Host " " +Write-Host " " + + +.\MigrateProject.ps1 ` +-SkipMigrateBuildQueues $WhatIf ` +-SkipMigrateRepos $WhatIf ` +-SkipMigrateWikis $WhatIf ` +-SkipMigrateServiceConnections $WhatIf \ No newline at end of file diff --git a/Step_2_Migrate_Project.ps1 b/Step_2_Migrate_Project.ps1 new file mode 100644 index 0000000..c125c1b --- /dev/null +++ b/Step_2_Migrate_Project.ps1 @@ -0,0 +1,25 @@ +Param ( + [Parameter (Mandatory = $FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Step 2 Migrate:" +Write-Host " - Areas and Iterations" +Write-Host " - Teams" +Write-Host " - Work Item Querys" +Write-Host " - Variable Groups" +Write-Host " - Build Pipelines" +Write-Host " - Release Pipelines" +Write-Host " - Task Groups" +Write-Host " " +Write-Host " " + + +.\MigrateProject.ps1 ` + -SkipMigrateTfsAreaAndIterations $WhatIf ` + -SkipMigrateTeams $WhatIf ` + -SkipMigrateWorkItemQuerys $WhatIf ` + -SkipMigrateVariableGroups $WhatIf ` + -SkipMigrateBuildPipelines $WhatIf ` + -SkipMigrateReleasePipelines $WhatIf ` + -SkipMigrateServiceConnections $WhatIf \ No newline at end of file diff --git a/Step_3_Migrate_Project.ps1 b/Step_3_Migrate_Project.ps1 new file mode 100644 index 0000000..77902dc --- /dev/null +++ b/Step_3_Migrate_Project.ps1 @@ -0,0 +1,165 @@ +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Step 3 Migrate:" +Write-Host "- Work Items (Including 'Test Cases')" +Write-Host " In steps where Created Date Between" +Write-Host " 0 - 100" +Write-Host " 100 - 200" +Write-Host " 200 - 300" +Write-Host " 300 - 400" +Write-Host " 400 - 500" +Write-Host " 500 - 600" +Write-Host " 600 - 700" +Write-Host " 800 - 800" +Write-Host " 800 - 900" +Write-Host " 900 - 1000" +Write-Host " 1000 - 1100" +Write-Host " 1100 - 1200" +Write-Host " 1200 - 1300" +Write-Host " 1300 - 1500" +Write-Host " 1500 - 3000" +Write-Host " 3000 + " +Write-Host " " + +# Since the Azure REST API for work items has a query limit if 20,000, calls to the API have been broken up into batches based on the Work item's Created Date field +# Each batch is listed below with the expected work item count to be migrated. The work item counts may since the work items are being updated daily. +# + + +Write-Host " " +Write-Host "Since the Azure REST API for work items has a query limit if 20,000, calls to the API have been broken up into batches based on the Work item's Created Date field" +Write-Host " " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 0 days ago and 100 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-SkipAddReflectedWorkItemIdField $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 100 AND [System.CreatedDate] <= @Today - 0 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 100 days ago and 200 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 200 AND [System.CreatedDate] <= @Today - 100 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 200 days ago and 300 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 300 AND [System.CreatedDate] <= @Today - 200 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 300 days ago and 400 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 400 AND [System.CreatedDate] <= @Today - 300 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 400 days ago and 500 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 500 AND [System.CreatedDate] <= @Today - 400 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 500 days ago and 600 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 600 AND [System.CreatedDate] <= @Today - 500 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 600 days ago and 700 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 700 AND [System.CreatedDate] <= @Today - 600 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 700 days ago and 800 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 800 AND [System.CreatedDate] <= @Today - 700 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 800 days ago and 900 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 900 AND [System.CreatedDate] <= @Today - 800 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 900 days ago and 1000 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 1000 AND [System.CreatedDate] <= @Today - 900 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 1000 days ago and 1100 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 1100 AND [System.CreatedDate] <= @Today - 1000 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 1100 days ago and 1200 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 1200 AND [System.CreatedDate] <= @Today - 1100 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 1200 days ago and 1300 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 1300 AND [System.CreatedDate] <= @Today - 1200 " + + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 1300 days ago and 1500 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 1500 AND [System.CreatedDate] <= @Today - 1300 " + +Write-Host " " +Write-Host "Migrate Work Items with Created Date between 1500 days ago and 3000 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] > @Today - 3000 AND [System.CreatedDate] <= @Today - 1500 " + +Write-Host " " +Write-Host "Migrate Work Items with Created Date less than 3000 days ago" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] <= @Today - 3000 " + + + + diff --git a/Step_4_Migrate_Project.ps1 b/Step_4_Migrate_Project.ps1 new file mode 100644 index 0000000..4b304f9 --- /dev/null +++ b/Step_4_Migrate_Project.ps1 @@ -0,0 +1,49 @@ +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Step 4 Migrate:" +Write-Host " - Test Cases" +Write-Host " - Groups" +Write-Host " - Test Configurations" +Write-Host " - Test Variables" +Write-Host " - Test Plans and Suites" +Write-Host " - Service Hooks" +Write-Host " - Policies" +Write-Host " - Dashboards" +Write-Host " - Delivery Plans " +Write-Host " " +Write-Host " " + +Write-Host " " +Write-Host "Migrate Test Cases via Martin's Tool" +Write-Host " " + + +& .\MigrateProject.ps1 ` +-SkipMigrateWorkItems $WhatIf ` +-SkipAddReflectedWorkItemIdField $WhatIf ` +-WorkItemQueryBit "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] IN ('Test Case')" + + +Write-Host " " +Write-Host "Migrate Test Configurations, Test Variables, Test Plans and Suites via Martin's Tool" +Write-Host " " + + +& .\MigrateProject.ps1 ` +-SkipMigrateTestConfigurations $WhatIf ` +-SkipMigrateTestVariables $WhatIf ` +-SkipMigrateTestPlansAndSuites $WhatIf ` + + +Write-Host " " +Write-Host "Migrate Groups, Service hooks, Policies, Dashboards, and Delivery Plans" +Write-Host " " +& .\MigrateProject.ps1 ` +-SkipMigrateGroups $WhatIf ` +-SkipMigrateServiceHooks $WhatIf ` +-SkipMigratePolicies $WhatIf ` +-SkipMigrateDashboards $WhatIf ` +-SkipMigrateDeliveryPlans $WhatIf diff --git a/Step_5_Migrate_Project.ps1 b/Step_5_Migrate_Project.ps1 new file mode 100644 index 0000000..044acfe --- /dev/null +++ b/Step_5_Migrate_Project.ps1 @@ -0,0 +1,13 @@ +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Step 5 Migrate:" +Write-Host " - Artifacts " +Write-Host " " +Write-Host " " + + +.\MigrateProject.ps1 ` +-SkipMigrateArtifacts $WhatIf \ No newline at end of file diff --git a/Step_X_Migrate_Org_Level_Users.ps1 b/Step_X_Migrate_Org_Level_Users.ps1 new file mode 100644 index 0000000..f2c1f92 --- /dev/null +++ b/Step_X_Migrate_Org_Level_Users.ps1 @@ -0,0 +1,13 @@ +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + + +Write-Host " " +Write-Host " Migrate Organization Users" +Write-Host " from Source organization to Target organization" +Write-Host " " + + +.\MigrateProject.ps1 ` +-SkipMigrateOrganizationUsers $WhatIf \ No newline at end of file diff --git a/Work-Item-Backfill_MIgrate_Project.ps1 b/Work-Item-Backfill_MIgrate_Project.ps1 new file mode 100644 index 0000000..3484a4f --- /dev/null +++ b/Work-Item-Backfill_MIgrate_Project.ps1 @@ -0,0 +1,40 @@ + +Param ( + [Parameter (Mandatory=$FALSE)] [String]$NumberOfDays = "", + [Parameter (Mandatory=$FALSE)] [String]$StartDate = "", + [Parameter (Mandatory=$FALSE)] [String]$EndDate = "", + [Parameter (Mandatory=$FALSE)] [String]$ItemType = "", + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Work-Item back fill migration:" + +Write-Host " " +Write-Host "Since the Azure REST API for work items has a query limit if 20,000, calls to the API may require the 'Number of Days Changed' value to be reduced to avoid pulling too many items" +Write-Host " " + + +Write-Host " " +Write-Host "Migrate Work Items with Changed Date between 0 days Today and 'Number of Days Changed' ago" +Write-Host " " + +$queryBit = "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') " + +if($NumberOfDays -ne "") { + $queryBit += "AND [System.ChangedDate] >= @Today - $($NumberOfDays) " +} elseif(($StartDate -ne "" -and $EndDate -ne "") -and ($startDate -ne $endDate)) { + $queryBit += "AND [System.ChangedDate] >= '$($StartDate)' AND [System.ChangedDate] <= '$($endDate)' " +} elseif($StartDate -ne "") { + $queryBit += "AND [System.ChangedDate] >= '$($StartDate)' " +} + +if($ItemType -ne "") { + $queryBit += "AND [System.WorkItemType] = '$($ItemType)' " +} +$queryBit += "ORDER BY [System.ChangedDate] DESC" + +& .\MigrateProject.ps1 -SkipMigrateWorkItems $WhatIf -WorkItemQueryBit $queryBit + + + diff --git a/admin-tools/AzureDevOps-WorkItemHelpers.ps1 b/admin-tools/AzureDevOps-WorkItemHelpers.ps1 index 3bc8f4c..367669e 100644 --- a/admin-tools/AzureDevOps-WorkItemHelpers.ps1 +++ b/admin-tools/AzureDevOps-WorkItemHelpers.ps1 @@ -19,7 +19,7 @@ function Get-ADOWorkItemTypes([string]$ProcessId, [string]$WorkItemType, [string $results = $client.GetStringAsync($url) $workItemTypes = ($results.Result | convertfrom-json).value if ($WorkItemType) { - return $workItemTypes | Where-Object {$_.name -ieq $WorkItemType} + return $workItemTypes | Where-Object { $_.name -ieq $WorkItemType } } else { return $workItemTypes @@ -57,7 +57,7 @@ function Get-ADOLists([string]$listName, [string]$org) { $results = $client.GetStringAsync($url) $lists = ($results.Result | convertfrom-json).value if ($listName) { - return $lists | Where-Object {$_.name -ieq $listName} + return $lists | Where-Object { $_.name -ieq $listName } } else { return $lists @@ -77,26 +77,26 @@ function Add-ADOProjectFields([string]$project, [string]$witRefName, [string]$cs $isPickList = $false if ($_.picklist) { $pickListName = $_.picklist - $list = $lists | Where-Object {$_.name -ieq $pickListName} + $list = $lists | Where-Object { $_.name -ieq $pickListName } $isPickList = $true } $field = [PSCustomObject]@{ - _links = $null - canSortyBy = $true - description = $null - isIdentity = ($_.type -ieq "identity") - isPicklist = $isPickList + _links = $null + canSortyBy = $true + description = $null + isIdentity = ($_.type -ieq "identity") + isPicklist = $isPickList isPicklistSuggested = $false - isQueryable = $true - name = $_.name - picklistId = $list.id - readOnly = $false - referenceName = $_.refName + isQueryable = $true + name = $_.name + picklistId = $list.id + readOnly = $false + referenceName = $_.refName supportedOperations = $null - type = $_.type - url = $baseUrl + $_.refname - usage = "workItem" + type = $_.type + url = $baseUrl + $_.refname + usage = "workItem" } $fieldjson = $field | convertto-json @@ -120,26 +120,26 @@ function Add-ADOFields([string]$processId, [string]$witRefName, [string]$csvFile $picklist = $null if ($_.picklist) { $pickListName = $_.picklist - $list = $lists | Where-Object {$_.name -ieq $pickListName} + $list = $lists | Where-Object { $_.name -ieq $pickListName } $picklist = [PSCustomObject]@{ - id = $list.id + id = $list.id isSuggested = $null - Name = $pickListName - type = $null - url = $null + Name = $pickListName + type = $null + url = $null } } $field = [PSCustomObject]@{ referenceName = $_.refName - name = $_.name - type = $_.type - pickList = $picklist - readOnly = $false - required = $false - defaultValue = $null - url = $baseUrl + $_.refName - allowGroups = $null + name = $_.name + type = $_.type + pickList = $picklist + readOnly = $false + required = $false + defaultValue = $null + url = $baseUrl + $_.refName + allowGroups = $null } $fieldjson = $field | convertto-json @@ -157,7 +157,7 @@ function Get-ADOProjectFields([string]$projectName, [string]$org, [string]$pat) $headers = (New-HTTPHeaders -pat $pat) - $url = "$org/_apis/projects/"+$projectName+"?api-version=5.1" + $url = "$org/_apis/projects/" + $projectName + "?api-version=5.1" try { $project = Invoke-RestMethod -Method Get -uri $url -headers $headers } @@ -166,7 +166,7 @@ function Get-ADOProjectFields([string]$projectName, [string]$org, [string]$pat) return } - $fieldsUrl = "$org/"+$project.id+"/_apis/wit/fields?api-version=5.0-preview.2" + $fieldsUrl = "$org/" + $project.id + "/_apis/wit/fields?api-version=5.0-preview.2" try { $fields = Invoke-RestMethod -Method Get -uri $fieldsUrl -headers $headers } @@ -178,7 +178,7 @@ function Get-ADOProjectFields([string]$projectName, [string]$org, [string]$pat) } function Delete-ADOWorkItemField([string]$fieldName, [string]$org, [string]$pat) { - #DELETE https://dev.azure.com/aiz-test/_apis/wit/fields/Custom.ARCID + #DELETE https://dev.azure.com/org-name/_apis/wit/fields/Custom.ARCID try { $results = Invoke-RestMethod -Method Delete -uri "$org/_apis/wit/fields/$fieldName" -headers (New-HTTPHeaders -pat $pat) } diff --git a/configuration/configuration.json b/configuration/configuration.json index 1e70896..d290bff 100644 --- a/configuration/configuration.json +++ b/configuration/configuration.json @@ -1,17 +1,17 @@ { "SourceProject": { - "Organization": "https://dev.azure.com/Sample-Org/", - "ProjectName": "Sample-Project", - "OrgName": "Sample-Org" + "Organization": "", + "ProjectName": "", + "OrgName": "" }, "TargetProject": { - "Organization": "https://dev.azure.com/Sample-Org/", - "ProjectName": "Sample-Project", - "OrgName": "Sample-Org" + "Organization": "", + "ProjectName": "", + "OrgName": "" }, - "ProjectDirectory": "C:\\Users\\User1\\Working\\MigrationDirectory", - "WorkItemMigratorDirectory": "C:\\tools\\MigrationTool", - "RepositoryCloneTempDirectory": "C:\\tools\\RepoCloneTemp", - "DevOpsMigrationToolConfigurationFile": "migrator-configuration.json", - "ArtifactFeedPackageVersionLimit": 5 -} \ No newline at end of file + "ProjectDirectory": "", + "WorkItemMigratorDirectory": "", + "RepositoryCloneTempDirectory": "", + "DevOpsMigrationToolConfigurationFile": "", + "ArtifactFeedPackageVersionLimit": -1 +} diff --git a/configuration/migrator-configuration.json b/configuration/migrator-configuration.json index c3676d7..d70a514 100644 --- a/configuration/migrator-configuration.json +++ b/configuration/migrator-configuration.json @@ -7,13 +7,13 @@ "Endpoints": { "ProjectSource": { "EndpointType": "TfsTeamProjectEndpoint", - "Collection": "https://dev.azure.com/AIZ-GL/", - "Project": "GL.CL-Elita", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, - + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { "AreaPath": "Area", @@ -22,11 +22,11 @@ }, "ProjectTarget": { "EndpointType": "TfsTeamProjectEndpoint", - "Collection": "https://dev.azure.com/AIZ-Global/", - "Project": "GL.CL-Elita-migrated", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { @@ -36,13 +36,13 @@ }, "WorkItemSource": { "EndpointType": "TfsWorkItemEndpoint", - "Collection": "https://dev.azure.com/AIZ-GL/", - "Project": "GL.CL-Elita", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, - + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { "AreaPath": "Area", @@ -51,11 +51,11 @@ }, "WorkItemTarget": { "EndpointType": "TfsWorkItemEndpoint", - "Collection": "https://dev.azure.com/AIZ-Global/", - "Project": "GL.CL-Elita-migrated", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { @@ -65,13 +65,13 @@ }, "TeamSettingsSource": { "EndpointType": "TfsTeamSettingsEndpoint", - "Collection": "https://dev.azure.com/AIZ-GL/", - "Project": "GL.CL-Elita", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, - + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { "AreaPath": "Area", @@ -80,11 +80,11 @@ }, "TeamSettingsTarget": { "EndpointType": "TfsTeamSettingsEndpoint", - "Collection": "https://dev.azure.com/AIZ-Global/", - "Project": "GL.CL-Elita-migrated", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { @@ -92,28 +92,28 @@ "IterationPath": "Iteration" } }, - "TfsSource":{ + "TfsSource": { "EndpointType": "TfsEndpoint", - "Collection": "https://dev.azure.com/AIZ-GL/", - "Project": "GL.CL-Elita", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, - + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { "AreaPath": "Area", "IterationPath": "Iteration" } }, - "TfsTarget":{ + "TfsTarget": { "EndpointType": "TfsEndpoint", - "Collection": "https://dev.azure.com/AIZ-Global/", - "Project": "GL.CL-Elita-migrated", + "Collection": "https://dev.azure.com/org-name/", + "Project": "project-name", "Authentication": { "AccessToken": "", - "AuthenticationMode": "AccessToken" + "AuthenticationMode": "AccessToken" }, "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", "LanguageMaps": { @@ -121,29 +121,24 @@ "IterationPath": "Iteration" } }, - "PipelineSource":{ + "PipelineSource": { "EndpointType": "AzureDevOpsEndpoint", - "Collection": "https://dev.azure.com/AIZ-GL/", - "Project": "GL.CL-Elita", - "Authentication": { - "AccessToken": "", - "AuthenticationMode": "AccessToken" - }, - + "Organisation": "https://dev.azure.com/org-name/", + "Project": "project-name", + "AccessToken": "", + "AuthenticationMode": "AccessToken", "ReflectedWorkItemIdField": null, "LanguageMaps": { "AreaPath": "Area", "IterationPath": "Iteration" } }, - "PipelineTarget":{ + "PipelineTarget": { "EndpointType": "AzureDevOpsEndpoint", - "Collection": "https://dev.azure.com/AIZ-Global/", - "Project": "GL.CL-Elita-migrated", - "Authentication": { - "AccessToken": "", - "AuthenticationMode": "AccessToken" - }, + "Organisation": "https://dev.azure.com/org-name/", + "Project": "project-name", + "AccessToken": "", + "AuthenticationMode": "AccessToken", "ReflectedWorkItemIdField": null, "LanguageMaps": { "AreaPath": "Area", @@ -158,9 +153,8 @@ "MigrateTeamSettings": true, "UpdateTeamSettings": false, "PrefixProjectToNodes": false, - "MigrateTeamCapacities": false, + "MigrateTeamCapacities": true, "Teams": null, - "UseUserMapping": false, "SourceName": "TeamSettingsSource", "TargetName": "TeamSettingsTarget" }, @@ -181,7 +175,7 @@ "ProcessorType": "TfsTestPlansAndSuitesMigrationProcessor", "Enabled": false, "OnlyElementsWithTag": "", - "TestPlanQuery": null, + "TestPlanQuery": "", "RemoveAllLinks": false, "MigrationDelay": 0, "RemoveInvalidTestSuiteLinks": false, @@ -244,29 +238,23 @@ "FieldMappingTool": { "Enabled": true, "FieldSkipMap": { - "ApplyTo": [ - "*" - ], + "ApplyTo": ["*"], "targetField": "TfsMigrationTool.ReflectedWorkItemId" }, "MultiValueConditionalMap": { - "ApplyTo": [ - "*" - ], - "sourceFieldsAndValues": { - "Field1": "Value1", - "Field2": "Value2" - }, - "targetFieldsAndValues": { - "Field1": "Value1", - "Field2": "Value2" - } + "ApplyTo": ["*"], + "sourceFieldsAndValues": { + "Field1": "Value1", + "Field2": "Value2" }, + "targetFieldsAndValues": { + "Field1": "Value1", + "Field2": "Value2" + } + }, "FieldMaps": [ { - "ApplyTo": [ - "*" - ], + "ApplyTo": ["*"], "defaultValue": "New", "FieldMapType": "FieldValueMap", "sourceField": "System.State", @@ -280,76 +268,66 @@ "Done": "Closed", "Removed": "Removed" } - } + } ], "FieldToFieldMap": { - "ApplyTo": [ - "*" - ], + "ApplyTo": ["*"], "sourceField": "Microsoft.VSTS.Common.BacklogPriority", "targetField": "Microsoft.VSTS.Common.StackRank" }, "FieldToFieldMultiMap": { - "ApplyTo": [ - "*" - ], - "SourceToTargetMappings": { - "SourceField1": "TargetField1", - "SourceField2": "TargetField2" - } - }, - "FieldToTagMap": { - "ApplyTo": [ - "*" - ], - "formatExpression": "ScrumState:{0}", - "sourceField": "System.State" - }, - "FieldMergeMap": { - "ApplyTo": [ - "*" - ], - "formatExpression": "{0}

Acceptance Criteria

{1}", - "sourceFields": [ - "System.Description", - "Microsoft.VSTS.Common.AcceptanceCriteria" - ], - "targetField": "System.Description" - }, - "RegexFieldMap": { - "ApplyTo": [ - "*" - ], - "pattern": "PRODUCT \\d{4}.(\\d{1})", - "replacement": "$1", - "sourceField": "COMPANY.PRODUCT.Release", - "targetField": "COMPANY.DEVISION.MinorReleaseVersion" - }, - "FieldValueToTagMap": { - "ApplyTo": [ - "*" - ], - "formatExpression": "{0}", - "pattern": "Yes", - "sourceField": "Microsoft.VSTS.CMMI.Blocked" - }, - "TreeToTagMap": { - "ApplyTo": [ - "*" - ], - "timeTravel": "1", - "toSkip": "3" + "ApplyTo": ["*"], + "SourceToTargetMappings": { + "SourceField1": "TargetField1", + "SourceField2": "TargetField2" } + }, + "FieldToTagMap": { + "ApplyTo": ["*"], + "formatExpression": "ScrumState:{0}", + "sourceField": "System.State" + }, + "FieldMergeMap": { + "ApplyTo": ["*"], + "formatExpression": "{0}

Acceptance Criteria

{1}", + "sourceFields": [ + "System.Description", + "Microsoft.VSTS.Common.AcceptanceCriteria" + ], + "targetField": "System.Description" + }, + "RegexFieldMap": { + "ApplyTo": ["*"], + "pattern": "PRODUCT \\d{4}.(\\d{1})", + "replacement": "$1", + "sourceField": "COMPANY.PRODUCT.Release", + "targetField": "COMPANY.DEVISION.MinorReleaseVersion" + }, + "FieldValueToTagMap": { + "ApplyTo": ["*"], + "formatExpression": "{0}", + "pattern": "Yes", + "sourceField": "Microsoft.VSTS.CMMI.Blocked" + }, + "TreeToTagMap": { + "ApplyTo": ["*"], + "timeTravel": "1", + "toSkip": "3" + } }, "TfsNodeStructureTool": { "Enabled": true, "Areas": { - "Filters": [], - "Mappings": {} + "Filters": null, + "Mappings": { + "^\\\\oldproject1(?:\\\\([^\\\\]+))?\\\\([^\\\\]+)$": "TargetProject\\Q1$2" + } }, "Iterations": { - "Filters": [], - "Mappings": {} + "Filters": null, + "Mappings": { + "^\\\\oldproject1(?:\\\\([^\\\\]+))?\\\\([^\\\\]+)$": "TargetProject\\Q1$2" + } }, "ShouldCreateMissingRevisionPaths": true, "ReplicateAllExistingNodes": false @@ -357,12 +335,12 @@ "WorkItemTypeMappingTool": { "Enabled": "True", "Mappings": { - "Source Work Item Type Name": "Target Work Item Type Name" + "Product Backlog Item": "User Story" } }, "TfsAttachmentTool": { "Enabled": "True", - "ExportBasePath": "", + "ExportBasePath": "c:\\temp\\WorkItemAttachmentWorkingFolder", "MaxAttachmentSize": "480000000", "RefName": "TfsAttachmentTool" }, @@ -384,4 +362,4 @@ } } } -} \ No newline at end of file +} diff --git a/create-manifest.ps1 b/create-manifest.ps1 new file mode 100644 index 0000000..648864e --- /dev/null +++ b/create-manifest.ps1 @@ -0,0 +1,53 @@ +# ----------- CONFIGURE VARIABLES HERE +$IncludedModules = @( + "$(Get-Location)\modules\Migrate-ADO-AreaPaths.psm1", + "$(Get-Location)\modules\Migrate-ADO-IterationPaths.psm1", + "$(Get-Location)\modules\Migrate-ADO-Users.psm1", + "$(Get-Location)\modules\Migrate-ADO-Teams.psm1", + "$(Get-Location)\modules\Migrate-ADO-Groups.psm1", + "$(Get-Location)\modules\Migrate-ADO-BuildQueues.psm1", + "$(Get-Location)\modules\Migrate-ADO-BuildEnvironments.psm1", + "$(Get-Location)\modules\Migrate-ADO-Repos.psm1", + "$(Get-Location)\modules\Migrate-ADO-Retention.psm1" + "$(Get-Location)\modules\Migrate-ADO-Wikis.psm1", + "$(Get-Location)\modules\Migrate-ADO-Common.psm1", + "$(Get-Location)\modules\Migrate-ADO-Pipelines.psm1", + "$(Get-Location)\modules\Migrate-ADO-Project.psm1", + "$(Get-Location)\modules\Migrate-ADO-ServiceHooks.psm1", + "$(Get-Location)\modules\Migrate-ADO-ServiceConnections.psm1", + "$(Get-Location)\modules\Migrate-ADO-VariableGroups.psm1", + "$(Get-Location)\modules\Migrate-ADO-Policies.psm1", + "$(Get-Location)\modules\Migrate-ADO-Dashboards.psm1", + "$(Get-Location)\modules\Migrate-ADO-BuildDefinitions.psm1", + "$(Get-Location)\modules\Migrate-ADO-ReleaseDefinitions.psm1", + "$(Get-Location)\modules\Migrate-ADO-Artifacts.psm1", + "$(Get-Location)\modules\Migrate-ADO-DeliveryPlans.psm1", + "$(Get-Location)\modules\ADO-AddCustomField.psm1", + "$(Get-Location)\modules\Migrate-Packages.psm1" +) + +# Make sure files are the correct paths +$validPath = Test-Path $IncludedModules[0] + +if (!$validPath) { + throw "The file paths appear to be incorrect... `n + Make sure you are in the repo root directory when running this script." +} + +$Version = '1.0.0.0' +$Description = 'Azure Devops Migration classes, functions and enums.' +$Path = "$($env:PSModulePath.Split(";")[0])\Migrate-ADO" +$FileName = "Migrate-ADO.psd1" + +New-Item -Path $Path -ItemType Directory -Force + +Write-Host $Path -ForegroundColor Gray + +# ---------- CREATES A NEW MANIFEST FOR PACKAGED MODULES +New-ModuleManifest ` + -Path "$Path\$FileName" ` + -NestedModules $IncludedModules ` + -Guid (New-Guid) ` + -ModuleVersion $Version ` + -Description $Description ` + -PowerShellVersion 5.1.0.0 \ No newline at end of file diff --git a/env-example.ps1 b/env-example.ps1 new file mode 100644 index 0000000..d30b228 --- /dev/null +++ b/env-example.ps1 @@ -0,0 +1,18 @@ +# Module Imports +Import-Module "./modules/Migrate-ADO-Common.psm1" + +# Azure DevOps Migration Environment Variables +# Create a file named env.ps1 in the same directory as this script and copy the content below into it. +# You can use it to set up variables to run functions locally durring development. + +# Source Organization and Project +Set-Variable -Name "SourceOrgName" -Value "" -Visibility Public -Force +Set-Variable -Name "SourceProjectName" -Value "" -Visibility Public -Force +Set-Variable -Name "SourcePAT" -Value "" -Visibility Public -Force +Set-Variable -Name "SourceHeaders" -Value (New-HTTPHeaders -PersonalAccessToken $SourcePAT) -Visibility Public -Force + +# Target Organization and Project +Set-Variable -Name "TargetOrgName" -Value "" -Visibility Public -Force +Set-Variable -Name "TargetProjectName" -Value "" -Visibility Public -Force +Set-Variable -Name "TargetPAT" -Value "" -Visibility Public -Force +Set-Variable -Name "TargetHeaders" -Value (New-HTTPHeaders -PersonalAccessToken $TargetPAT) -Visibility Public -Forc \ No newline at end of file diff --git a/helper-scripts/ADOArtifactFeedPackageVersions.ps1 b/helper-scripts/ADOArtifactFeedPackageVersions.ps1 new file mode 100644 index 0000000..2abe140 --- /dev/null +++ b/helper-scripts/ADOArtifactFeedPackageVersions.ps1 @@ -0,0 +1,61 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT, + [Parameter (Mandatory=$FALSE)] [Switch]$GenerateAverages, + [Parameter (Mandatory=$TRUE)] [String]$OutputFile +) + +Write-Host "Begin Generate Artifact Feed Package Version Data found in ($OrgName/$ProjectName)... " +Write-Host " " + + # Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + +Start-Transcript -Path $OutputFile -Append + +$url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds?api-version=7.0" +$results = Invoke-RestMethod -Method Get -uri $url -Headers $headers +$feeds = $results.Value + +Write-Host "This process is time consuming and will take a while, be patient..." + +foreach($feed in $feeds) { + $url = $feed._links.Packages.href + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + $packages = $results.Value + + if($GenerateAverages -eq $TRUE) { + $versionCount = 0 + foreach($package in $packages) { + $url = $package._links.versions.href + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + $versions = $results.Value + $versionCount += $versions.Count + } + + $versionAvgCount = 0 + if($versionCount -gt 0){ + $versionAvgCount = [math]::ceiling($versionCount / $packages.Count) + } + + Write-Log -Message "Feed $($feed.Name) : $($packages.Count) Packages : $($versionAvgCount) Average Version Count" + + } else { + Write-Log -Message "Feed $($feed.Name) : $($packages.Count) Packages" + foreach($package in $packages) { + $url = $package._links.versions.href + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + $versions = $results.Value + Write-Log -Message " - Package: $($package.Name) : $($versions.Count) Versions" + } + } + +} +Write-Log ' ' +Stop-Transcript + +Write-Host "End Generate Artifact Feed Package Version Data... " + + diff --git a/helper-scripts/ADODeleteAllDashboards.ps1 b/helper-scripts/ADODeleteAllDashboards.ps1 new file mode 100644 index 0000000..397b258 --- /dev/null +++ b/helper-scripts/ADODeleteAllDashboards.ps1 @@ -0,0 +1,57 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT +) + +Write-Host "Begin Delete ALL Dashboards for Organization and Project" + + # Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + + +Write-Host "Begin Deleting ALL Dashboards found in ($OrgName/$ProjectName)... " +Write-Host " " + + +$url = "https://dev.azure.com/$OrgName/_apis/projects/$($ProjectName)?api-version=7.0" +$project = Invoke-RestMethod -Method GET -Uri $url -Headers $headers +$defaultTeam = $project.DefaultTeam.Id +Write-Log "Default Team ($defaultTeam)" + +# Get all Dashboards for the process/project +$url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/dashboard/dashboards?api-version=7.0-preview.3" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers +$dashboards = $results.Value + +foreach ($dashboard in $dashboards) { + try { + Write-Log -Message "Deleting Dashboard $($dashboard.Name) [$($dashboard.id)].. " + + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/dashboard/dashboards/$($dashboard.id)?api-version=7.0-preview.3" + + if($dashboard.dashboardScope -eq "project_Team") { + $team = $dashboard.groupId + + if($defaultTeam -eq $team) { + continue + } + Write-Log " Team ($team)" + $url = "https://dev.azure.com/$OrgName/$ProjectName/$team/_apis/dashboard/dashboards/$($dashboard.id)?api-version=7.0-preview.3" + } + + Invoke-RestMethod -Method DELETE -Uri $url -Headers $headers + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } +} + +Write-Host "End Deleting ALL Dashboards found in ($OrgName/$ProjectName)... " + + diff --git a/helper-scripts/ADODeleteAllServiceConnections.ps1 b/helper-scripts/ADODeleteAllServiceConnections.ps1 new file mode 100644 index 0000000..0c7b29a --- /dev/null +++ b/helper-scripts/ADODeleteAllServiceConnections.ps1 @@ -0,0 +1,45 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT +) + +Write-Host "Begin Delete ALL Service Connections for Organization and Project" + + # Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + + +Write-Host "Begin Deleting ALL Service Connections found in ($OrgName/$ProjectName)... " +Write-Host " " + +# Get project info +$url = "https://dev.azure.com/$OrgName/_apis/projects/$($ProjectName)?api-version=7.0" +$project = Invoke-RestMethod -Method GET -Uri $url -Headers $headers + +# Get all Service Connections for the process/project +$url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints?includeFailed=true&includeDetails=true&api-version=7.0" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers +$serviceConnections = $results.Value + +foreach ($serviceConnection in $serviceConnections) { + try { + Write-Log -Message "Deleting Service Connections $($serviceConnection.Name) [$($serviceConnection.id)].. " + + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints/$($serviceConnection.id)?projectIds=$($project.id)&api-version=7.0" + + Invoke-RestMethod -Method DELETE -Uri $url -Headers $headers + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } +} + +Write-Host "End Deleting ALL Service Connections found in ($OrgName/$ProjectName)... " + + diff --git a/helper-scripts/ADOGetCurrentUserInfo.ps1 b/helper-scripts/ADOGetCurrentUserInfo.ps1 new file mode 100644 index 0000000..4ea7c1e --- /dev/null +++ b/helper-scripts/ADOGetCurrentUserInfo.ps1 @@ -0,0 +1,24 @@ + +# ---------------------------------------------------------------------------------- + +$pat = $env:AZURE_DEVOPS_MIGRATION_PAT + +# ---------------------------------------------------------------------------------- +Write-Host "Begin Testing.." +Write-Host " " + +$Headers = New-HTTPHeaders -PersonalAccessToken $PAT + +try { + $url = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.0" + $result = Invoke-RestMethod -Method GET -uri $url -Headers $Headers + Write-Host ($result | ConvertTo-Json -Depth 100) +} catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR } catch {} +} + +Write-Host " " +Write-Host "End Testing.." +Write-Host " " \ No newline at end of file diff --git a/helper-scripts/ADOGetServiceConnectionsLastUsed.ps1 b/helper-scripts/ADOGetServiceConnectionsLastUsed.ps1 new file mode 100644 index 0000000..fd7c366 --- /dev/null +++ b/helper-scripts/ADOGetServiceConnectionsLastUsed.ps1 @@ -0,0 +1,55 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT +) + +Write-Host "Begin GET ALL Service Connections for Organization and Project" + + # Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + + +Write-Host "Begin getting ALL Service Connections found in ($OrgName/$ProjectName)... " +Write-Host " " + + +# Get all Service Connections for the process/project +$url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints?includeFailed=true&includeDetails=true&api-version=7.0" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers +$serviceConnections = $results.Value + +$output = @() +foreach ($serviceConnection in $serviceConnections) { + try { + Write-Log -Message "GET Service Connection history for $($serviceConnection.Name) [$($serviceConnection.id)].. " + + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/$($serviceConnection.id)/executionhistory?api-version=7.1-preview.1" + + $result = Invoke-RestMethod -Method GET -Uri $url -Headers $headers + $sc_history = $sc_history=$result.Value + $sc_history_top = $sc_history | Sort-Object -Property { $_.data.startTime} -Descending | Select-Object -First 1 + + $item = @{ + "Id" = $serviceConnection.Id + "Name" = $serviceConnection.Name + "latestStartTime" = $sc_history_top.data.startTime + } + + $output += $item + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } +} + +$output + +Write-Host "End Deleting ALL Service Connections found in ($OrgName/$ProjectName)... " + + diff --git a/helper-scripts/ADOIdentifyPlansAndSuitesForUnknowUsers.ps1 b/helper-scripts/ADOIdentifyPlansAndSuitesForUnknowUsers.ps1 new file mode 100644 index 0000000..f8e0114 --- /dev/null +++ b/helper-scripts/ADOIdentifyPlansAndSuitesForUnknowUsers.ps1 @@ -0,0 +1,111 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT, + [Parameter (Mandatory=$TRUE)] [String]$OutputFile +) + + +Write-Host "Begin - Identify Plans/Suites/Test-Cases whos owner is not a user identity in the organization" +Write-Host "Source Organization - $OrgName" +Write-Host " " + +Start-Transcript -Path $OutputFile -Append + + Write-Host "Get Organization User Identities.." + Set-AzDevOpsContext -PersonalAccessToken $PAT -OrgName $OrgName + + Write-Host "Calling az devops user list.." -NoNewline + $results = az devops user list --detect $False | ConvertFrom-Json + + $members = $results.members + $totalCount = $results.totalCount + $counter = $members.Count + do { + $UserResponse = az devops user list --detect $False --skip $counter | ConvertFrom-Json + Write-Host ".." -NoNewline + $members += $UserResponse.members + $counter += $UserResponse.members.Count + } while ($counter -lt $totalCount) + Write-Host " " + +$orgUsers = @() + foreach ($orgUser in $members ) { + $orgUsers += $orgUser + } + +$orgUserNames = (($orgUsers | Select-Object -ExpandProperty User) | Select-Object -ExpandPropert principalName).ToLower() +$orgUserNames = $orgUserNames | Sort-Object + + Write-Host "Get Plans/Suites/Test-Cases and validate that the owner is in the Organization list" + +# Create Headers +$Headers = New-HTTPHeaders -PersonalAccessToken $PAT + + +# Get all fields for the source organization +$url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/test/plans?api-version=5.0" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $Headers +$sourcePlans = $results.Value + +$tpsOwners = @() +$tpsWithBadOwner = @() +foreach ($plan in $sourcePlans) { + Write-Host "Plan Name: $($plan.name)" + + # Get all suites for the source Plan + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/testplan/Plans/$($plan.id)/suites?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $Headers + $sourceSuites = $results.Value + + foreach ($suite in $sourceSuites) { + Write-Host " Suite Name: $($suite.name)" + + # Get all Test-Cases for the source Suite + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/test/Plans/$($plan.id)/suites/$($suite.id)/testcases?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $Headers + $sourceTestCases = $results.Value + + foreach ($testcase in $sourceTestCases) { + Write-Host " Test-Case Name: $($testcase.name)" + foreach ($assignment in $testcase.pointAssignments) { + $tpsUser = $assignment.tester.uniqueName + Write-Host " User Name: $tpsUser" + $tpsOwners += $tpsUser + + # Check if owner is in known list + $userCheck = $orgUserNames | Where-Object { $_ -eq $tpsUser } + if ($NULL -eq $userCheck) { + Write-Host " Bad User Identity Assigned!" + $tpsWithBadOwner += "Plan Name: $($plan.name) :: Suite Name: $($suite.name) :: Test-Case Name: $($testcase.name) :: User Name: $tpsUser" + } + } + } + } +} + + + +# Compare the two collections - $users and $tpsOwners +$tpsOwners = $tpsOwners | Select-Object -Unique +$tpsOwners = $tpsOwners | Sort-Object +$diffUserNames = $tpsOwners | Where-Object { $_ -notin $orgUserNames } + +Write-Host "Diff Users: $($diffUserNames.Count)" +foreach ($orgUserName in $diffUserNames) { + Write-Host "$orgUserName " +} +Write-Host " " +Write-Host " " +Write-Host "Bad Plan/Suite/Test-Case User Identities: $($tpsWithBadOwner.Count)" +foreach ($tpsBadUserName in $tpsWithBadOwner) { + Write-Host "$tpsBadUserName " +} +Write-Host " " +Write-Host " " +Write-Host "End - Identify Plans/Suites/Test-Cases whos owner is not a user identity in the organization" + +Stop-Transcript + + diff --git a/helper-scripts/ADOVerifyFieldsForOrganizationProject.ps1 b/helper-scripts/ADOVerifyFieldsForOrganizationProject.ps1 new file mode 100644 index 0000000..5de6521 --- /dev/null +++ b/helper-scripts/ADOVerifyFieldsForOrganizationProject.ps1 @@ -0,0 +1,79 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$SourceOrgName, + [Parameter (Mandatory=$TRUE)] [String]$SourceProjectName, + [Parameter (Mandatory=$TRUE)] [String]$SourcePAT, + + [Parameter (Mandatory=$TRUE)] [String]$TargetOrgName, + [Parameter (Mandatory=$TRUE)] [String]$TargetProjectName, + [Parameter (Mandatory=$TRUE)] [String]$TargetPAT +) + +Write-Host "Begin Validate ADO custom Fields for the Processes" + + # Create Headers + $sourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT + $targetHeaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + + +Write-Host "Begin Validate ADO custom Fields for the Processes" +Write-Host "Source - $SourceOrgName/$SourceProjectName" +Write-Host "Target - $TargetOrgName/$TargetProjectName" +Write-Host " " + + +if(($SourceProjectName -ne "") -and ($TargetProjectName -ne "")) { + Write-Host " " + Write-Host "Begin Validate ADO custom Fields for the Source and Target Projects" + Write-Host "Source Process Id - $SourceProjectName" + Write-Host "Target Process Id - $TargetProjectName" + Write-Host " " + + # Get all fields for the Source process/project + $url = "https://dev.azure.com/$SourceOrgName/$SourceProjectName/_apis/wit/fields?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $sourceHeaders + $sourceProcessFields = $results.Value + + # Get all fields for the Target process/project + $url = "https://dev.azure.com/$TargetOrgName/$TargetProjectName/_apis/wit/fields?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $targetHeaders + $targetProcessFields = $results.Value + + + $writeHeader = $TRUE + foreach ($field in $sourceProcessFields) { + if ($null -ne ($targetProcessFields | Where-Object { $_.referenceName -ieq $field.referenceName })) { continue } + + if($writeHeader) { + Write-Log -Message "Work Item Fields that exists in Source project ($SourceOrgName/$SourceProjectName) but not in Target project($TargetOrgName/$TargetProjectName)... " + $writeHeader = $FALSE + } + Write-Log $field.referenceName + } +} else { + # Get all fields for the Source organization + $url = "https://dev.azure.com/$SourceOrgName/_apis/wit/fields?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $sourceHeaders + $sourceProcessFields = $results.Value + + # Get all fields for the Target organization + $url = "https://dev.azure.com/$TargetOrgName/_apis/wit/fields?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $targetHeaders + $targetProcessFields = $results.Value + + + $writeHeader = $TRUE + foreach ($field in $sourceProcessFields) { + if ($null -ne ($targetProcessFields | Where-Object { $_.referenceName -ieq $field.referenceName })) { continue } + + if($writeHeader) { + Write-Log -Message "Work Item Fields that exists in Source Process but not in Target Process... " + $writeHeader = $FALSE + } + Write-Log $field.referenceName + } +} + +Write-Host "End Validate ADO custom Fields " + + diff --git a/helper-scripts/ADOVerifyFieldsForWorkItemInProcess.ps1 b/helper-scripts/ADOVerifyFieldsForWorkItemInProcess.ps1 new file mode 100644 index 0000000..32f9c2d --- /dev/null +++ b/helper-scripts/ADOVerifyFieldsForWorkItemInProcess.ps1 @@ -0,0 +1,74 @@ + +Param ( + [Parameter (Mandatory=$FALSE)] [String]$SourceProjectName = "", + [Parameter (Mandatory=$FALSE)] [String]$SourceOrgName = "", + [Parameter (Mandatory=$FALSE)] [String]$SourcePAT = "", + [Parameter (Mandatory=$FALSE)] [String]$SourceProcessId = "", + + [Parameter (Mandatory=$FALSE)] [String]$TargetProjectName = "", + [Parameter (Mandatory=$FALSE)] [String]$TargetOrgName = "", + [Parameter (Mandatory=$FALSE)] [String]$TargetPAT = "", + [Parameter (Mandatory=$FALSE)] [String]$TargetProcessId = "" +) + +Write-Host "Begin Validate ADO custom Fields for the Processes" + + # Create Headers + $sourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT + $targetHeaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + + +Write-Host "Begin Validate ADO custom Fields for the Processes" +Write-Host "Source - $SourceProcessId" +Write-Host "Target - $TargetProcessId" +Write-Host " " + + +# Get Source work item types +$url = "https://dev.azure.com/$SourceOrgName/_apis/work/processdefinitions/$SourceProcessId/workitemtypes?api-version=7.0" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $sourceHeaders +$sourceWorkItemTypes = $results.Value + +# Get Target work item types +$url = "https://dev.azure.com/$TargetOrgName/_apis/work/processdefinitions/$TargetProcessId/workitemtypes?api-version=7.0" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $sourceHeaders +$targetWorkItemTypes = $results.Value + + +$sourceWorkItemFields = New-Object Collections.Generic.List[string] +foreach($sourceWorkItemType in $sourceWorkItemTypes){ + # Get all fields for the the work item type + $url = "https://dev.azure.com/$SourceOrgName/_apis/work/processes/$SourceProcessId/workItemTypes/$($sourceWorkItemType.id)/fields?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $sourceHeaders + + foreach($sourcefield in $results.Value){ + $sourceWorkItemFields.Add($sourcefield.referenceName) + } +} + + +$targetWorkItemFields = New-Object Collections.Generic.List[string] +foreach($targetWorkItemType in $targetWorkItemTypes){ + # Get all fields for the the work item type + $url = "https://dev.azure.com/$TargetOrgName/_apis/work/processes/$TargetProcessId/workItemTypes/$($targetWorkItemType.id)/fields?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $targetHeaders + + foreach($targetfield in $results.Value){ + $targetWorkItemFields.Add($targetfield.referenceName) + } +} + +$writeHeader = $TRUE +foreach ($field in $sourceWorkItemFields) { + if ($targetWorkItemFields -like $field) { continue } + + if($writeHeader) { + Write-Log -Message "Work Item Fields that exists in Source Process but not in Target Process... " + $writeHeader = $FALSE + } + Write-Log $field +} + +Write-Host "End Validate ADO custom Fields " + + diff --git a/images/full-migration-workflow.png b/images/full-migration-workflow.png new file mode 100644 index 0000000..702c8d2 Binary files /dev/null and b/images/full-migration-workflow.png differ diff --git a/images/partial-migration-workflow.png b/images/partial-migration-workflow.png new file mode 100644 index 0000000..d19ef8c Binary files /dev/null and b/images/partial-migration-workflow.png differ diff --git a/images/secret.png b/images/secret.png new file mode 100644 index 0000000..a725b30 Binary files /dev/null and b/images/secret.png differ diff --git a/images/settings.png b/images/settings.png new file mode 100644 index 0000000..121b261 Binary files /dev/null and b/images/settings.png differ diff --git a/images/user-migration-workflow.png b/images/user-migration-workflow.png new file mode 100644 index 0000000..702c8d2 Binary files /dev/null and b/images/user-migration-workflow.png differ diff --git a/images/variables.png b/images/variables.png new file mode 100644 index 0000000..23a7839 Binary files /dev/null and b/images/variables.png differ diff --git a/modules/ADO-AddCustomField.psm1 b/modules/ADO-AddCustomField.psm1 index c03befb..bfda9c6 100644 --- a/modules/ADO-AddCustomField.psm1 +++ b/modules/ADO-AddCustomField.psm1 @@ -39,26 +39,23 @@ function Start-ADO_AddCustomField { [Hashtable]$Headers, [Parameter (Mandatory = $TRUE)] - [String]$OrgName, + [String]$OrgName, [Parameter (Mandatory = $TRUE)] - [String]$PAT, + [String]$ProjectName, [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, + [String]$SourceOrgName, [Parameter (Mandatory = $TRUE)] - [String]$ProcessId, - - [Parameter (Mandatory = $TRUE)] - [String]$FieldName, + [String]$SourceProjectName, [Parameter (Mandatory = $FALSE)] [String]$FieldDefaultValue ) if ($PSCmdlet.ShouldProcess( "Project $OrgName/$ProjectName", - "Add ADO custom Field from source project $OrgName/$ProjectName") + "Add ADO custom Field from source project $SourceOrgName/$SourceProjectName") ) { Write-Log -Message ' ' Write-Log -Message '--------------------------------' @@ -71,17 +68,31 @@ function Start-ADO_AddCustomField { -LocalOrgName $OrgName ` -LocalProjectName $ProjectName ` -LocalHeaders $Headers - + $referenceFieldName = "Custom.ReflectedWorkItemId" if($NULL -ne $customFields) { - if ($null -eq ($customFields | Where-Object { $_.referenceName -ieq $FieldName })) { - Write-Log -Message "Creating Custom Field `"$FieldName`" for $OrgName/$ProjectName... " + # Checking if the desired field exists (ReflectedWorkItemId). If so creation can be skipped, as the migration-configuration.json file is appropriately modified in MigrateProject.ps1. + + $url = "https://dev.azure.com/$OrgName/_apis/wit/fields/ReflectedWorkItemId?api-version=7.1-preview.2" + Write-log "Url: $url" + $response = Invoke-RestMethod -Uri $url -Headers $Headers + + + if ($null -eq ($customFields | Where-Object { $_.referenceName -ieq "ReflectedWorkItemId" }) -AND $null -eq $response) { + Write-Log -Message "Creating Custom Field ReflectedWorkItemId for $OrgName/$ProjectName... " # Add a new custom field for this org/project so that it can be added to work item types for the process New-Customfield ` -LocalOrgName $OrgName ` - -LocalFieldName $FieldName ` + -LocalFieldName "Custom.ReflectedWorkItemId" ` -LocalHeaders $Headers + } elseif ($null -ne $response) { + $referenceFieldName = $response.referenceName } } + + $ProcessId = Get-ProcessId ` + -OrgName $OrgName ` + -ProjectName $ProjectName ` + -Headers $Headers # Get the associated work item types for this process by process Id $workitemTypes = Get-ProcessWorkItemTypes ` @@ -102,18 +113,18 @@ function Start-ADO_AddCustomField { -LocalWorkItemType $workitemType if($NULL -ne $processDefinitions) { - if ($null -ne ($processDefinitions | Where-Object { $_.referenceName -ieq $FieldName })) { - Write-Log -Message "Custom Field `"$FieldName`" already exists for $OrgName/$ProjectName Work Item Type [$($workitemType.Id)]... " + if ($null -ne ($processDefinitions | Where-Object { $_.referenceName -ieq "ReflectedWorkItemId" })) { + Write-Log -Message "Custom Field ReflectedWorkItemId already exists for $OrgName/$ProjectName Work Item Type [$($workitemType.Id)]... " continue } - Write-Log -Message "ADDing Custom Field `"$FieldName`" for $OrgName/$ProjectName Work Item Type [$($workitemType.Id)]... " + Write-Log -Message "ADDing Custom Field ReflectedWorkItemId for $OrgName/$ProjectName Work Item Type [$($workitemType.Id)]... " Add-CustomField ` -LocalOrgName $OrgName ` -LocalHeaders $Headers ` -LocalProcessId $ProcessId ` -LocalWorkItemType $workitemType ` - -LocalFieldName $FieldName + -LocalFieldName $referenceFieldName } } } @@ -134,7 +145,7 @@ function Get-ProcessWorkItemTypes { [Parameter (Mandatory = $TRUE)] [String]$LocalProcessId ) - $url = "https://dev.azure.com/$LocalOrgName/_apis/work/processes/$LocalProcessId/workitemtypes?api-version=7.0" + $url = "https://dev.azure.com/$LocalOrgName/_apis/work/processes/$LocalProcessId/workitemtypes?api-version=4.1-preview.1" $results = Invoke-RestMethod -Method GET -Uri $url -Headers $LocalHeaders @@ -273,10 +284,10 @@ function New-Customfield { [String]$LocalFieldName ) $url = "https://dev.azure.com/$LocalOrgName/$ProjectName/_apis/wit/fields?api-version=7.0" - + Write-Host $url $body = @" { - "name": "Custom Work Item Field - ReflectedWorkItemId", + "name": "ReflectedWorkItemId", "referenceName": "$LocalFieldName", "description": "Custom field used by data migration tool.", "type": "string", @@ -301,3 +312,28 @@ function New-Customfield { return $results } +function Get-ProcessID { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName + ) + $ProjectUrl = "https://dev.azure.com/$OrgName/_apis/projects/$($ProjectName)?api-version=7.0" + Write-Log $ProjectUrl + $response = Invoke-RestMethod -Uri $ProjectUrl -Method Get -Headers $Headers + $ProjectId = $response.id + Write-Log "ProjectId while getting Porcess ID: $ProjectId" + $url = "https://dev.azure.com/$OrgName/_apis/projects/$($ProjectId)/properties?api-version=7.0-preview" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $Headers + $process = $results.value | Where-Object { $_.name -eq "System.ProcessTemplateType" } + Write-Log "ProcessId: $($process.value)" + return $process.value +} + diff --git a/modules/Migrate-ADO-AreaPaths.psm1 b/modules/Migrate-ADO-AreaPaths.psm1 index d93ae54..4c3cb92 100644 --- a/modules/Migrate-ADO-AreaPaths.psm1 +++ b/modules/Migrate-ADO-AreaPaths.psm1 @@ -1,15 +1,3 @@ -class ADO_AreaPath { - [String]$Name - [ADO_AreaPath[]]$Children - - ADO_AreaPath( - [String]$name, - [ADO_AreaPath[]]$children - ) { - $this.Name = $name - $this.Children = $children - } -} function Start-ADOAreaPathsMigration { [CmdletBinding(SupportsShouldProcess)] @@ -42,18 +30,19 @@ function Start-ADOAreaPathsMigration { Write-Log -Message '------------------------' Write-Log -Message ' ' - $areaPaths = Get-AreaPaths ` + $rootNode = Get-ClassificationNodes ` -ProjectName $SourceProjectName ` -OrgName $SourceOrgName ` -Headers $SourceHeaders - if ($areaPaths) { - Push-AreaPaths ` - -ProjectName $TargetProjectName ` - -OrgName $TargetOrgName ` - -AreaPaths $areaPaths ` - -Headers $TargetHeaders + if ($rootNode) { + New-ClassificationNodesRecursive ` + -ProjectName $TargetProjectName ` + -OrgName $TargetOrgName ` + -Nodes $rootNode.Children ` + -Headers $TargetHeaders + Write-Log -Message "Migration of areas complete." } else { Write-Log -Message "No area paths to migrate in project $SourceProjectName" @@ -61,26 +50,51 @@ function Start-ADOAreaPathsMigration { } } -function ConvertTo-AreaPathObject { +function Start-ADOIterationsMigration { [CmdletBinding(SupportsShouldProcess)] param( [Parameter (Mandatory = $TRUE)] - [Object]$AreaPath + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders ) - if ($PSCmdlet.ShouldProcess($AreaPath.Name)) { - $ADOAreaPath = [ADO_AreaPath]::new($AreaPath.Name, [ADO_AreaPath[]]@()) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrgName/$TargetProjectName", + "Migrate iterations from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '------------------------' + Write-Log -Message '-- Migrate Iterations --' + Write-Log -Message '------------------------' + Write-Log -Message ' ' + - if ($AreaPath.hasChildren) { - foreach ($child in $AreaPath.Children) { - $ADOAreaPath.Children += (ConvertTo-AreaPathObject -AreaPath $child) - } - } + $sourceIterations = Get-ClassificationNodes -OrgName $SourceOrgName -ProjectName $SourceProjectName ` + -Headers $SourceHeaders -ClassificationNodeType Iterations + + Write-Log -Message "Migrating $($sourceIterations.Children.Count) iterations." + + New-ClassificationNodesRecursive -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -ClassificationNodeType Iterations -Headers $TargetHeaders -Nodes $sourceIterations.Children - return $ADOAreaPath + Write-Log -Message "Migration of iterations complete." } } -function Get-AreaPaths { +function Get-ClassificationNodes { [CmdletBinding(SupportsShouldProcess)] param( [Parameter (Mandatory = $TRUE)] @@ -93,25 +107,67 @@ function Get-AreaPaths { [Hashtable]$Headers, [Parameter (Mandatory = $FALSE)] - [Int]$Depth = 100 + [Int]$Depth = 100, + + [Parameter(Mandatory = $False)] + [ValidateSet("Areas", "Iterations")] + [String]$ClassificationNodeType = "Areas" ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/wit/classificationnodes/Areas?`$depth=$Depth&api-version=5.0-preview.2" + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/wit/classificationnodes" ` + + "/$($ClassificationNodeType)?`$depth=$Depth&api-version=5.0-preview.2" $results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers - [ADO_AreaPath[]]$areaPaths = @() + return $results + } +} + +function New-ClassificationNodesRecursive { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + $Nodes, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter(Mandatory = $False)] + [ValidateSet("Areas", "Iterations")] + [String]$ClassificationNodeType = "Areas", - foreach ($result in $results.Children) { - $areaPaths += (ConvertTo-AreaPathObject -AreaPath $result) + [String] + $ParentPath + ) + + foreach ($n in $Nodes) { + $node = @{ + "name" = $n.name + } + + if ($ClassificationNodeType -eq "Iterations") { + $node["attributes"] = $n.attributes } + + $newNode = New-ClassificationNode -ProjectName $ProjectName -OrgName $OrgName ` + -Headers $Headers -Node $node -ParentPath $ParentPath ` + -ClassificationNodeType $ClassificationNodeType - return $areaPaths + if ($n.Children.count -gt 0) { + + New-ClassificationNodesRecursive -ProjectName $ProjectName -OrgName $OrgName ` + -Headers $Headers -Nodes $n.Children -ParentPath $newNode.path ` + -ClassificationNodeType $ClassificationNodeType + } } } -function Push-AreaPaths { - [CmdletBinding(SupportsShouldProcess)] - param( +function New-ClassificationNode { + param ( [Parameter (Mandatory = $TRUE)] [String]$ProjectName, @@ -119,23 +175,78 @@ function Push-AreaPaths { [String]$OrgName, [Parameter (Mandatory = $TRUE)] - [ADO_AreaPath[]]$AreaPaths, + [hashtable]$Node, [Parameter (Mandatory = $TRUE)] - [Hashtable]$Headers + [Hashtable]$Headers, + + [Parameter(Mandatory = $False)] + [ValidateSet("Areas", "Iterations")] + [String]$ClassificationNodeType = "Areas", + + [String] + $ParentPath ) - if ($PSCmdlet.ShouldProcess($ProjectName)) { - $targetAreaPaths = Get-AreaPaths -ProjectName $ProjectName -OrgName $OrgName -Headers $Headers - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/wit/classificationnodes/Areas?api-version=6.0" + try { + $paths = "" + + if ($ParentPath) { + $singular = $ClassificationNodeType.Substring(0, $ClassificationNodeType.Length - 1) + $paths = $ParentPath.Substring($ParentPath.LastIndexOf($singular) + $singular.Length).Replace("\", "/").Replace(" ", "%20") + } + + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/wit/classificationnodes/$($ClassificationNodeType)$($paths)?api-version=6.0" + $body = $Node | ConvertTo-Json -Depth 32 + + $result = Invoke-RestMethod -Method POST -Uri $url -Body $body -Headers $headers ` + -ContentType "application/json" - foreach ($areaPath in $AreaPaths) { - if ($null -ne ($targetAreaPaths | Where-Object { $_.Name -ieq $areaPath.Name } )) { - Write-Log -Message "Area path [$($areaPath.Name)] already exists in target.. " - continue - } + Write-Log -Message "$($result.path) added to $($ClassificationNodeType)" + } + catch { + Write-Log -Message "Unable to migrate area: $($Node.name)" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message " " + } + + return $result +} + +function Remove-AllClassificationNodes { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, - $body = $areaPath | ConvertTo-Json - Invoke-RestMethod -Method POST -Uri $url -Body $body -Headers $headers -ContentType "application/json" + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter(Mandatory = $False)] + [ValidateSet("Areas", "Iterations")] + [String]$ClassificationNodeType = "Areas" + ) + + $nodes = Get-ClassificationNodes -ProjectName $ProjectName -OrgName $OrgName ` + -Headers $Headers -ClassificationNodeType $ClassificationNodeType + + foreach ($a in $nodes.Children) { + try { + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/wit/classificationnodes/$($ClassificationNodeType)/$($a.name)?api-version=7.1" + + Invoke-RestMethod -Method Delete -Uri $url -Headers $Headers + + Write-Log -Message "Node: $($a.name) deleted from $($ClassificationNodeType)" + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message ($_ | ConvertFrom-Json -Depth 10) -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message " " } } -} \ No newline at end of file +} + diff --git a/modules/Migrate-ADO-Artifacts.psm1 b/modules/Migrate-ADO-Artifacts.psm1 index ec6747e..7f47fff 100644 --- a/modules/Migrate-ADO-Artifacts.psm1 +++ b/modules/Migrate-ADO-Artifacts.psm1 @@ -1,3 +1,4 @@ +Using module "..\modules\Migrate-Packages.psm1" function Start-ADOArtifactsMigration { [CmdletBinding(SupportsShouldProcess)] @@ -26,9 +27,6 @@ function Start-ADOArtifactsMigration { [Parameter (Mandatory = $TRUE)] [string]$TargetPAT, - [Parameter (Mandatory = $TRUE)] - [String]$ProjectPath, - [Parameter (Mandatory = $TRUE)] [Int]$ArtifactFeedPackageVersionLimit ) @@ -47,8 +45,8 @@ function Start-ADOArtifactsMigration { $targetFeeds = Get-Feeds -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders $targetProject = Get-ADOProjects -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders - # Get the Target Organization ID to be used for the internalUpstreamCollectionId value when creating internal Upstream Sources - $targetInternalUpstreamCollectionId = Get-OrganizationId -OrgName $TargetOrgName -Headers $TargetHeaders + # Get the Target Organization ID to be used for the internalUpstreamCollectionId value when creating internal Upstream Sources + $targetInternalUpstreamCollectionId = Get-OrganizationId -OrgName $TargetOrgName -Headers $TargetHeaders # Create all Target Feeds before adding packages to each feed $newTargetFeeds = @() @@ -65,29 +63,33 @@ function Start-ADOArtifactsMigration { $publicUpstreamSources = @() foreach ($source in $feed.upstreamSources) { - if($source.upstreamSourceType -eq "public") { + if ($source.upstreamSourceType -eq "public") { $publicUpstreamSources += $source } } - $targetFeed = New-ADOFeed -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -SourceFeed $feed -UpstreamSources $publicUpstreamSources - if(($NULL -eq $targetFeed) -or ($targetFeed.GetType().Name -eq "FileInfo")) { + $targetFeed = New-ADOFeed -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -SourceFeed $feed -UpstreamSources $publicUpstreamSources + Write-Log "Feed Name: $($feed.name)" + Write-Log "Command: New-ADOFeed -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -SourceFeed $feed -UpstreamSources $publicUpstreamSources" + Write-Log "TargetFeed: $targetFeed" + if (($NULL -eq $targetFeed) -or ($targetFeed.GetType().Name -eq "FileInfo")) { if ($null -eq $targetFeed) { Write-Log -Message "Could not create a new feed with name '$($feed.Name)'. The feed name may be reserved by the system." -LogLevel ERROR } continue - } else { + } + else { # Make sure that the target view access is the same as the source view access $sourceViews = Get-Views -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders -FeedId $feed.Id $targetViews = Get-Views -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -FeedId $targetFeed.Id foreach ($targetView in $targetViews) { - $sourceView = $sourceViews | Where-Object { $_.name -ieq $targetView.name } - if($NULL -ne $sourceView) { - if($targetView.visibility -ine $sourceView.visibility ) { - Update-View -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -FeedId $targetFeed.Id -ViewId $targetView.Id -Visibility $sourceView.visibility - } + $sourceView = $sourceViews | Where-Object { $_.name -ieq $targetView.name } + if ($NULL -ne $sourceView) { + if ($targetView.visibility -ine $sourceView.visibility ) { + Update-View -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -FeedId $targetFeed.Id -ViewId $targetView.Id -Visibility $sourceView.visibility } + } } Write-Log -Message "Done!" -LogLevel SUCCESS @@ -100,7 +102,7 @@ function Start-ADOArtifactsMigration { foreach ($feed in $sourceFeeds) { $internalUpstreamSources = @() foreach ($source in $feed.upstreamSources) { - if($source.upstreamSourceType -eq "internal") { + if ($source.upstreamSourceType -eq "internal") { $internalUpstreamSources += $source } } @@ -118,8 +120,8 @@ function Start-ADOArtifactsMigration { Write-Log -Message "Feed [$($feed.Name)] internal Upstream Source [$($internalSource.Name)] already exists in target Feed.. " continue } - - if($internalSource.displayLocation -like "*$SourceOrgName/$SourceProjectName*") { + Write-Log "Display Location: $($internalSource.displayLocation)" + if ($internalSource.displayLocation -like "*$SourceOrgName/$SourceProjectName*") { $sourceInternalUpstreamFeed = Get-Feed -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders -FeedId $internalSource.internalUpstreamFeedId $sourceInternalUpstreamFeedViews = Get-Views -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders -FeedId $sourceInternalUpstreamFeed.Id $sourceInternalUpstreamFeedView = $sourceInternalUpstreamFeedViews | Where-Object { $_.Id -eq $internalSource.internalUpstreamViewId } @@ -131,27 +133,29 @@ function Start-ADOArtifactsMigration { $sourceInternalSourceFeed = Get-Feed -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders -FeedId $internalSource.internalUpstreamFeedId $targetInternalSourceFeed = $newTargetFeeds | Where-Object { $_.name -eq $sourceInternalSourceFeed.name } - if(($NULL -ne $targetInternalUpstreamFeedView) -and ($NULL -ne $targetInternalSourceFeed)) { + if (($NULL -ne $targetInternalUpstreamFeedView) -and ($NULL -ne $targetInternalSourceFeed)) { $newSource = @{ - "name" = $internalSource.name - "protocol" = $internalSource.protocol - "upstreamSourceType" = "internal" - "internalUpstreamCollectionId" = $targetInternalUpstreamCollectionId # $internalSource.internalUpstreamCollectionId - "internalUpstreamFeedId" = $targetInternalSourceFeed.Id - "internalUpstreamViewId" = $targetInternalUpstreamFeedView.id - "internalUpstreamProjectId" = $targetProject.Id + "name" = $internalSource.name + "protocol" = $internalSource.protocol + "upstreamSourceType" = "internal" + "internalUpstreamCollectionId" = $targetInternalUpstreamCollectionId # $internalSource.internalUpstreamCollectionId + "internalUpstreamFeedId" = $targetInternalSourceFeed.Id + "internalUpstreamViewId" = $targetInternalUpstreamFeedView.id + "internalUpstreamProjectId" = $targetProject.Id } $upstreamSources += $newSource - } else { + } + else { Write-Log -Message "Unable to identify upstream source feed in target.. " Write-Log -Message "Internal Upstream View Id: $internalUpstreamViewId " Write-Log -Message "Target's Internal Upstream Feed Id $targetSourceFeedId " } - } else { + } + else { $upstreamSources += $internalSource } - if($upstreamSources.count -gt 0) { + if ($upstreamSources.count -gt 0) { Update-Feed -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -FeedId $existingSourceFeed.Id -UpstreamSources $upstreamSources } } @@ -195,7 +199,7 @@ function Get-OrganizationId { ) if ($PSCmdlet.ShouldProcess($ProjectName)) { try { - # Get Context user info + # Get Context user info $url = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.0" $result1 = Invoke-RestMethod -Method GET -uri $url -Headers $Headers $id = $result1.id @@ -205,12 +209,13 @@ function Get-OrganizationId { $result2 = Invoke-RestMethod -Method GET -uri $url -Headers $Headers $organizations = $result2.value - foreach($org in $organizations) { - if($org.accountName -eq $OrgName) { + foreach ($org in $organizations) { + if ($org.accountName -eq $OrgName) { return $org.accountId } } - } catch { + } + catch { Write-Log -Message "FAILED!" -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR } catch {} @@ -233,15 +238,20 @@ function Get-Feeds { [Hashtable]$Headers ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds?api-version=7.0" + $projectUrl = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds?api-version=7.0" - $results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers + $projectResults = Invoke-RestMethod -Method GET -Uri $projectUrl -Headers $headers - return $results.Value + # $orgUrl = "https://feeds.dev.azure.com/$OrgName/_apis/packaging/feeds?api-version=7.0" + + # $orgResults = Invoke-RestMethod -Method GET -Uri $orgUrl -Headers $headers + + $results = $projectResults.Value + + return $results } } - function Get-Feed { [CmdletBinding(SupportsShouldProcess)] param( @@ -287,7 +297,7 @@ function Update-Feed { [Object[]]$UpstreamSources ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds/$($FeedId)?api-version=6.1-preview.1" + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds/$($FeedId)?api-version=6.1-preview.1" $body = @{ upstreamSources = @() + $UpstreamSources @@ -322,19 +332,18 @@ function New-ADOFeed { if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds?api-version=7.0" - # "url" = $url $hideDeletedPackageVersions = $FALSE - if(($NULL -ne $SourceFeed.hideDeletedPackageVersions) -and ($SourceFeed.hideDeletedPackageVersions -eq $TRUE)) { + if (($NULL -ne $SourceFeed.hideDeletedPackageVersions) -and ($SourceFeed.hideDeletedPackageVersions -eq $TRUE)) { $hideDeletedPackageVersions = $TRUE } $body = @{ - "name" = $SourceFeed.name - "description" = $SourceFeed.description + "name" = $SourceFeed.name + "description" = $SourceFeed.description "hideDeletedPackageVersions" = $hideDeletedPackageVersions - "capabilities" = $SourceFeed.capabilities - upstreamSources = @() + $UpstreamSources + "capabilities" = $SourceFeed.capabilities + upstreamSources = @() + $UpstreamSources } | ConvertTo-Json try { @@ -343,15 +352,38 @@ function New-ADOFeed { catch { Write-Log -Message "FAILED!" -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR - try { - Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message return $NULL } } } +function Remove-Feed { + param ( + [Parameter(Mandatory = $TRUE)] + [string] + $OrgName, + [Parameter()] + [AllowNull()] + [string] + $ProjectName, + [Parameter(Mandatory = $TRUE)] + $Headers, + [Parameter(Mandatory = $TRUE)] + [string] + $FeedId + ) + + if ($ProjectName) { + $url = "https://feeds.dev.azure.com/$($OrgName)/$($ProjectName)/_apis/packaging/feeds/$($FeedId)?api-version=7.1" + } + $url = "https://feeds.dev.azure.com/$($OrgName)/_apis/packaging/feeds/$($FeedId)?api-version=7.1" + + $result = Invoke-RestMethod -Method Delete -Uri $url -Headers $Headers + return $result +} function Get-Views { [CmdletBinding(SupportsShouldProcess)] @@ -369,7 +401,7 @@ function Get-Views { [String]$FeedId ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/Feeds/$feedId/views?api-version=7.0" + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/Feeds/$feedId/views?api-version=7.0" $results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers @@ -400,10 +432,10 @@ function Update-View { [String]$Visibility ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/Feeds/$FeedId/views/$($ViewId)?api-version=6.1-preview.1" + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/Feeds/$FeedId/views/$($ViewId)?api-version=6.1-preview.1" $body = @{ - "visibility" = $Visibility + "visibility" = $Visibility } | ConvertTo-Json $results = Invoke-RestMethod -Method PATCH -Uri $url -Headers $Headers -Body $body -ContentType "application/json" @@ -435,8 +467,7 @@ function Get-Packages { } } -function Start-Command -{ +function Start-Command { [CmdletBinding()] param ( @@ -466,8 +497,8 @@ function Start-Command $process.WaitForExit() $return = [pscustomobject]@{ - StdOut = $output - StdErr = $outerror + StdOut = $output + StdErr = $outerror ExitCode = $process.ExitCode } diff --git a/modules/Migrate-ADO-BuildEnvironments.psm1 b/modules/Migrate-ADO-BuildEnvironments.psm1 index 64be534..e486cc8 100644 --- a/modules/Migrate-ADO-BuildEnvironments.psm1 +++ b/modules/Migrate-ADO-BuildEnvironments.psm1 @@ -56,8 +56,8 @@ function Start-ADOBuildEnvironmentsMigration { Write-Log -Message "Get Target Environments.." $targetEnvironments = Get-BuildEnvironments -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -ProjectId $targetProject.Id -Top 1000000 - Write-Log -Message "Get Target Pipelines to do source lookups.." - $targetPipelines = Get-Pipelines -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName + # Write-Log -Message "Get Target Pipelines to do source lookups.." + # $targetPipelines = Get-BuildDefinitions -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName $newBuildEnvironments = @() foreach ($sourceEnvironment in $sourceEnvironments) { @@ -78,146 +78,15 @@ function Start-ADOBuildEnvironmentsMigration { Write-Log -Message "Done!" -LogLevel SUCCESS $newBuildEnvironments += $sourceEnvironment - } catch { + } + catch { Write-Log -Message "FAILED!" -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} - } - } - - Write-Log -Message "Reload Target Environments to get any newly created ones.." - $targetEnvironments = Get-BuildEnvironments -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -ProjectId $targetProject.Id -Top 1000000 - - foreach ($newEnvironment in $newBuildEnvironments) { - Write-Log -Message "------------------------------------------------------------------------------------------------------------------" - Write-Log -Message "----- Processing Environment $($newEnvironment.name) -----" - Write-Log -Message "------------------------------------------------------------------------------------------------------------------" - - # Get and Update Role Assignments - $sourceRoleAssignments = Get-BuildEnvironmentRoleAssignments -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -ProjectId $sourceProject.Id -EnvironmentId $newEnvironment.Id - $targetEnvironment = $targetEnvironments | Where-Object { $_.Name -ieq $newEnvironment.Name } - $targetRoleAssignments = $NULL - if($NULL -ne $targetEnvironment) { - Write-Log -Message "--- User permissions --- " - $targetRoleAssignments = Get-BuildEnvironmentRoleAssignments -ProjectName $targetProjectName -OrgName $targetOrgName -Headers $targetheaders -ProjectId $targetProject.Id -EnvironmentId $targetEnvironment.Id - - foreach($roleAssignment in $sourceRoleAssignments) { - # Search Users for the roleAssignment's Identity Id - $roleAssignmentIdentityId = $null - - $sourceIdentity = Get-IdentityInfo -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -IdentityId $roleAssignment.Identity.Id -SubjectDescriptor $NULL - $targetIdentity = Get-IdentityInfo -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -IdentityId $NULL -SubjectDescriptor $sourceIdentity.subjectDescriptor - - if ($null -ne $targetIdentity) { - $roleAssignmentIdentityId = $targetIdentity.Id - } else { - #Search Groups for the roleAssignment's Identity Id - $existingGroup = $sourceGroups | Where-Object { $_.Id -ceq $roleAssignment.Identity.Id } - $migratedGroup = $targetGroups | Where-Object { $_.Name -ceq $existingGroup.Name } - if ($null -ne $migratedGroup) { - $roleAssignmentIdentityId = $migratedGroup.Id - } - } - - if($NULL -ne $roleAssignmentIdentityId) { - # Try to find RoleAssignment in target - $targetRoleAssignment = $targetRoleAssignments | Where-Object { ($_.identity.id -eq $roleAssignmentIdentityId) -and ($_.role.name -eq $roleAssignment.role.name) } - - if ($NULL -ne $targetRoleAssignment) { - Write-Log -Message "Role Assignment [[ $($roleAssignment.Identity.displayName) / $($roleAssignmentIdentityId) / $($roleAssignment.role.name) ]] already exists in target.. " - } else { - try { - $data = @{ - "roleName" = $roleAssignment.Role.Name - "userId" = $roleAssignmentIdentityId - } - - $scope = $roleAssignment.Role.Scope - Write-Log -Message " " - Write-Log -Message "Create new Role Assignment $($roleAssignmentIdentityId) / $($roleAssignment.role.name) in target.." - Write-Log -Message "Scope: $scope" - Write-Log -Message "Source Environment ID: $($newEnvironment.Id)" - Write-Log -Message "Source Identity Display Name: $($roleAssignment.Identity.displayName)" - Write-Log -Message "Source Identity Id: $($roleAssignment.Identity.Id)" - Write-Log -Message "Source Role Name: $($roleAssignment.role.name)" - Write-Log -Message "Target Identity Id: $($roleAssignmentIdentityId)" - Write-Log -Message "Target Role Name: $($roleAssignment.role.name)" - Write-Log -Message "Target Environment ID: $($targetEnvironment.Id)" - Write-Log -Message "Target Org Name: $TargetOrgName" - Write-Log -Message "Target Project ID: $($targetProject.Id)" - Write-Log -Message " " - - Set-BuildEnvironmentRoleAssignment -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -ProjectId $targetProject.Id -EnvironmentId $targetEnvironment.Id -ScopeId $scope -RoleAssignment $data - } catch { - Write-Log -Message "FAILED to Update Build Environment User Permissions ROle Assignment!" -LogLevel ERROR - Write-Log -Message $_.Exception -LogLevel ERROR - try { - Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} - } - } - - } else { - Write-Log -Message "Unable to locate Identity $($roleAssignment.Identity.displayName) in target, unable to set role assignment Role Assignment $($roleAssignment.identity.id) / $($roleAssignment.role.name) in target.. " -LogLevel DEBUG - } - } - - - - # Get and Update pipeline permissions - Write-Log -Message "--- Pipline permissions --- " - - Write-Log -Message "Get Source Pipline permissions.." - $sourcePipelinePermissions = Get-BuildEnvironmentPipelinePermissions -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -EnvironmentId $newEnvironment.Id - $targetPipelinePermissions = Get-BuildEnvironmentPipelinePermissions -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -EnvironmentId $targetEnvironment.Id - - $newPipelinePermissions = @() - if ($TRUE -eq $ReplacePipelinePermissions) { - $newPipelinePermissions = $targetPipelinePermissions.Pipelines.Clone() - $newPipelinePermissions = @($newPipelinePermissions | Where-Object { $_.Id -notin $sourcePipelinePermissions.Pipelines.Id }) - - # Set to remove all items that are not in the Source Pipeline Permissions - foreach ($permission in $newPipelinePermissions) { - $permission.PSObject.Members.Remove("authorizedBy") - $permission.PSObject.Members.Remove("authorizedOn") - $permission.Authorized = $FALSE - } - } - - foreach ($pipelinePermission in $sourcePipelinePermissions.Pipelines) { - $sourcePipeline = Get-Pipeline -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName -DefinitionId $pipelinePermission.Id - $targetPipeline = ($targetPipelines | Where-Object {$_.Name -ceq $sourcePipeline.Name}) - - if ($NULL -ne $targetPipeline) { - $object = [PSCustomObject]@{ - id = $targetPipeline.Id - authorized = $pipelinePermission.Authorized - } - $newPipelinePermissions += $object - } else { - Write-Log -Message "Unable to map Source Pipeline ID [$($pipelinePermission.Id)] to a Target pipeline in order to set a Environment pipeline permission.." -LogLevel ERROR - } - } - - try { - Write-Log -Message "Update Target Pipline permissions.." - - Write-Log -Message " " - Write-Log -Message "Target Environment Id: $($targetEnvironment.Id)" - Write-Log -Message "PipelinePermissions: $(ConvertTo-Json -Depth 100 $newPipelinePermissions)" - - Set-BuildEnvironmentPipelinePermissions -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -EnvironmentId $targetEnvironment.Id -PipelinePermissions $newPipelinePermissions - } catch { - Write-Log -Message "FAILED to Update Build Environment User Permissions ROle Assignment!" -LogLevel ERROR - Write-Log -Message $_.Exception -LogLevel ERROR - try { - Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} } + catch {} } - Write-Log -Message "------------------------------------------------------------------------------------------------------------------" } } } @@ -243,11 +112,11 @@ function Get-BuildEnvironments { ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - if($Top -lt 0) {$Top = 0} + if ($Top -lt 0) { $Top = 0 } $url = "https://dev.azure.com/$OrgName/$ProjectId/_apis/distributedtask/environments?api-version=7.1-preview" - if($top -gt 0) { + if ($top -gt 0) { $url = "https://dev.azure.com/$OrgName/$ProjectId/_apis/distributedtask/environments?`$top=$($Top)&api-version=7.1-preview" } @@ -439,8 +308,8 @@ function New-BuildEnvironment { $url = "https://dev.azure.com/$OrgName/$ProjectId/_apis/distributedtask/environments?api-version=6.1-preview.1" $body = @{ - "name" = $environment.Name - "description" = $environment.Description + "name" = $environment.Name + "description" = $environment.Description } | ConvertTo-Json $results = Invoke-RestMethod -Method Post -uri $url -Headers $Headers -Body $body -ContentType "application/json" diff --git a/modules/Migrate-ADO-Common.psm1 b/modules/Migrate-ADO-Common.psm1 index 396cc65..735a934 100644 --- a/modules/Migrate-ADO-Common.psm1 +++ b/modules/Migrate-ADO-Common.psm1 @@ -506,12 +506,14 @@ function Get-ADOGroupMembers { $organization = "https://dev.azure.com/$OrgName/" try { $members = az devops security group membership list --id $GroupDescriptor --organization $organization --detect $false | ConvertFrom-Json - } catch { + } + catch { Write-Log -Message "FAILED!" -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} + } + catch {} } if ($members) { @@ -597,7 +599,7 @@ function Get-ADOProjectTeams { # Pipelines -function Get-Pipelines { +function Get-BuildDefinitions { [CmdletBinding(SupportsShouldProcess)] param( [Parameter (Mandatory = $TRUE)] @@ -625,7 +627,7 @@ function Get-Pipelines { } } -function Get-Pipeline { +function Get-BuildDefinition { [CmdletBinding(SupportsShouldProcess)] param( [Parameter (Mandatory = $TRUE)] @@ -669,7 +671,7 @@ function Get-Repo([string]$projectName, [string]$orgName, $headers, $repoId) { $url = "https://dev.azure.com/$orgName/$projectName/_apis/git/repositories/$repoId" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers return , $results } @@ -705,9 +707,31 @@ function New-GitRepository { } | ConvertTo-Json try { - Invoke-RestMethod -Method post -uri $url -Headers $Headers -Body $requestBody -ContentType 'application/json' + $result = Invoke-RestMethod -Method post -uri $url -Headers $Headers -Body $requestBody -ContentType 'application/json' } catch { Write-Log -Message "Error creating repo $RepoName in project $projectId : $($_.Exception) " } + + return $result +} + +function Get-IdentitiesByName { + param ( + [Parameter(Mandatory = $TRUE)] + [string] + $OrgName, + [Parameter(Mandatory = $TRUE)] + [string] + $DisplayName, + [Parameter(Mandatory = $TRUE)] + $Headers + ) + + $url = "https://vssps.dev.azure.com/$OrgName/_apis/identities?searchFilter=General" ` + + "&filterValue=$([uri]::EscapeDataString($DisplayName))&queryMembership=None&api-version=7.2-preview.1" + + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results } diff --git a/modules/Migrate-ADO-Dashboards.psm1 b/modules/Migrate-ADO-Dashboards.psm1 index 24b0c24..cdb15d9 100644 --- a/modules/Migrate-ADO-Dashboards.psm1 +++ b/modules/Migrate-ADO-Dashboards.psm1 @@ -101,10 +101,13 @@ function Start-ADODashboardsMigration { Write-Log -Message "--- Project Dashboards: ---" ForEach ($dashboard in $projectDashboards) { Write-Log -Message "dashboard: $($dashboard.name) dashboard scope: $($dashboard.dashboardScope)" + $MultipleDashboardsByName = $false + $targetDashboard = $targetDashboards | Where-Object { ($_.Name -eq $dashboard.name.Trim()) -and ($_.Position -eq $dashboard.position) } if($targetDashboard.Count -gt 1){ - Write-Log -Message "Multiple Dashboards found with name [$($targetDashboard.Name)] in target, widgets will need to be manually migrated or ensure that dashboard names are unique.. " + Write-Log -Message "Multiple Dashboards found with name [$($targetDashboard.Name)] in target. " + $MultipleDashboardsByName = $true } $fullSourceDashboard = Get-Dashboard -orgName $SourceOrgName -projectName $sourceProjectName -dashboardId $dashboard.Id -headers $SourceHeaders @@ -116,18 +119,32 @@ function Start-ADODashboardsMigration { if ($null -ine $targetDashboard) { Write-Log -Message "Dashboard [$($targetDashboard.Name) ($($targetDashboard.Id))] already exists in target.. " - - $fullTargetDashboard = Get-Dashboard -orgName $TargetOrgName -projectName $TargetProjectName -dashboardId $targetDashboard.Id -headers $TargetHeaders - - # See if the widgets for the Dashboard are migrated.. - if($fullTargetDashboard.Widgets.Count -lt $fullSourceDashboard.Widgets.Count) { - Write-Log -Message "Mapping Dashboard Widget query Ids for [$($dashboard.Name)].. " + if(!$MultipleDashboardsByName) { + $fullTargetDashboard = Get-Dashboard -orgName $TargetOrgName -projectName $TargetProjectName -dashboardId $targetDashboard.Id -headers $TargetHeaders - $fullTargetDashboard.Widgets = $fullSourceDashboard.Widgets - Write-Log -Message "Updating Dashboard Widgets for [$($fullTargetDashboard.Name)] in target.. " - Edit-Dashboard -orgName $targetOrgName -projectName $TargetProjectName -headers $TargetHeaders -dashboard $fullTargetDashboard + # See if the widgets for the Dashboard are migrated.. + if($fullTargetDashboard.Widgets.Count -lt $fullSourceDashboard.Widgets.Count) { + Write-Log -Message "Mapping Dashboard Widget query Ids for [$($dashboard.Name)].. " + + $fullTargetDashboard.Widgets = $fullSourceDashboard.Widgets + Write-Log -Message "Updating Dashboard Widgets for [$($fullTargetDashboard.Name)] in target.. " + Edit-Dashboard -orgName $targetOrgName -projectName $TargetProjectName -headers $TargetHeaders -dashboard $fullTargetDashboard + } + continue + } else { + # $TargetDashboard contains multiple dashboards with the same name since we have entered this else block + $matchingSourceDashboards = $projectDashboards | Where-Object { ($_.Name -eq $dashboard.name.Trim()) -and ($_.Position -eq $dashboard.position) } + $sourceDashboardIndex = $matchingSourceDashboards.IndexOf($dashboard) + $fullTargetDashboard = Get-Dashboard -orgName $TargetOrgName -projectName $TargetProjectName -dashboardId $($targetDashboard[$sourceDashboardIndex].Id) -headers $TargetHeaders + + if($fullTargetDashboard.Widgets.Count -lt $fullSourceDashboard.Widgets.Count) { + Write-Log -Message "Mapping Dashboard Widget query Ids for [$($fullTargetDashboard.Name)].. " + $fullTargetDashboard.Widgets = $fullSourceDashboard.Widgets + Write-Log -Message "Updating Dashboard Widgets for [$($fullTargetDashboard.Name)] with Source Dashboard Id [$($fullSourceDashboard.Id)] in target dashboard with Id [$($fullTargetDashboard.Id)] " + Edit-Dashboard -orgName $targetOrgName -projectName $TargetProjectName -headers $TargetHeaders -dashboard $fullTargetDashboard + } + continue } - continue } try { diff --git a/modules/Migrate-ADO-Groups.psm1 b/modules/Migrate-ADO-Groups.psm1 index 3d25577..79dec54 100644 --- a/modules/Migrate-ADO-Groups.psm1 +++ b/modules/Migrate-ADO-Groups.psm1 @@ -42,6 +42,9 @@ function Start-ADOGroupsMigration { -PersonalAccessToken $SourcePAT ` -GroupDisplayName $GroupDisplayName + $sourceGroupNames = $sourceGroups | Select-Object -ExpandProperty name + Write-Log "Group Display Name for getting source groups: $GroupDisplayName" + Write-Log "Source group names: $($sourceGroupNames -join ',')" Write-Log -Message 'Get target ADO Groups' $targetGroups = Get-ADOGroups ` -OrgName $TargetOrgName ` @@ -110,7 +113,8 @@ function Push-ADOGroups { $newGroup = [ADO_Group]::new($result.NewGroup.originId, $result.NewGroup.displayName, $result.NewGroup.principalName, $result.NewGroup.description, $result.NewGroup.descriptor) $targetGroups += $newGroup $processSourceGroups += $group - } else { + } + else { Write-Log -Message "unable to Create New Group [$($group.Name)] in target, it may need to be migrated manually.. " } } @@ -156,19 +160,21 @@ function New-ADOGroup { -OrgName $OrgName ` -ProjectName $ProjectName - $GroupDescription = $GroupDescription.Replace('"',"'") + $GroupDescription = $GroupDescription.Replace('"', "'") if ($Group.Description) { - if($VerboseOutput -eq $TRUE) { + if ($VerboseOutput -eq $TRUE) { $result = az devops security group create --name $GroupName --description $GroupDescription --detect $false --debug --verbose - } else { + } + else { $result = az devops security group create --name $GroupName --description $GroupDescription --detect $false } } else { - if($VerboseOutput -eq $TRUE) { + if ($VerboseOutput -eq $TRUE) { $result = az devops security group create --name $GroupName --detect $false --debug --verbose - } else { + } + else { $result = az devops security group create --name $GroupName --detect $false } } @@ -223,12 +229,14 @@ function Push-GroupMembers { } Write-Log -Message "Adding User Member [$($userMember.Name)] in target group [$($SourceGroup.Name)].. " - if($VerboseOutput -eq $TRUE) { + if ($VerboseOutput -eq $TRUE) { az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $userMember.PrincipalName --detect $false --debug --verbose - } else { + } + else { az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $userMember.PrincipalName --detect $false } - } catch { + } + catch { Write-Log -Message $_.Exception.Message -LogLevel ERROR } @@ -241,6 +249,9 @@ function Push-GroupMembers { -ProjectName $ProjectName ` -PersonalAccessToken $PersonalAccessToken ` -GroupDisplayName $groupMember.Name + + Write-Log "Group on target: $groupOnTarget" + if ($null -ne ($TargetGroup.GroupMembers | Where-Object { $_.Name -ieq $groupMember.Name } )) { Write-Log -Message "Group Member [$($groupMember.Name)] already exists in target group [$($SourceGroup.Name)].. " @@ -248,12 +259,16 @@ function Push-GroupMembers { } Write-Log -Message "Adding Group Member [$($groupMember.Name)] in target group [$($SourceGroup.Name)].. " - if($VerboseOutput -eq $TRUE) { + if ($VerboseOutput -eq $TRUE) { + Write-Log "Group on target principal name: $($groupOnTarget.PrincipalName)" az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $groupOnTarget.PrincipalName --detect $false --debug --verbose - } else { - az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $groupOnTarget.Descriptor --detect $false } - } catch { + else { + Write-Log "Group on target Descriptior: $($groupOnTarget.Descriptor)" + az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $groupOnTarget.Descriptor --detect $false + } + } + catch { Write-Log -Message $_.Exception.Message -LogLevel ERROR } } diff --git a/modules/Migrate-ADO-Pipelines.psm1 b/modules/Migrate-ADO-Pipelines.psm1 index 93718c2..e046220 100644 --- a/modules/Migrate-ADO-Pipelines.psm1 +++ b/modules/Migrate-ADO-Pipelines.psm1 @@ -1,5 +1,200 @@ -function Get-Pipelines { +Using Module "..\modules\Migrate-ADO-Common.psm1" +Using Module "..\modules\Migrate-ADO-ServiceConnections.psm1" +Using Module "..\modules\Migrate-ADO-BuildEnvironments.psm1" + +function Start-ClassicBuildPipelinesMigration { [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders, + + [Parameter (Mandatory = $FALSE)] + [Boolean]$MigrateOnlyProblematicPipelines = $true + ) + if ($PSCmdlet.ShouldProcess( + "Project $TargetOrgName/$TargetProjectName", + "Migrate Classic Build Pipelines from source project $SourceOrgName/$SourceProjectName") + ) { + + Write-Log -Message ' ' + Write-Log -Message '-------------------------------------------------------------' + Write-Log -Message '-- Migrate Classic Build Pipelines (Exception Cases Only) --' + Write-Log -Message '-------------------------------------------------------------' + Write-Log -Message ' ' + + $sourcePipelines = Get-Pipelines -Headers $SourceHeaders -ProjectName $SourceProjectName -OrgName $SourceOrgName + $targetPipelines = Get-Pipelines -Headers $TargetHeaders -ProjectName $targetProjectName -OrgName $TargetOrgName + + $targetPipelineNames = $targetPipelines | Select-Object -ExpandProperty name + + $sourceEndpoints = Get-ServiceEndpoints -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $sourceHeaders + $targetEndpoints = Get-ServiceEndpoints -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders + $targetProject = Get-ADOProjects -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders + $targetAgentPoolsUrl = "https://dev.azure.com/$TargetOrgName/$TargetProjectName/_apis/distributedtask/queues?api-version=7.1" + $targetAgentPools = Invoke-RestMethod -Method GET -uri $targetAgentPoolsUrl -Headers $TargetHeaders + + $sourceTaskGroups = Get-TaskGroups -OrgName $SourceOrgName -ProjectName $SourceProjectName ` + -Headers $SourceHeaders + + # Check if there are any task groups to migrate + if ($sourceTaskGroups.value -and $sourceTaskGroups.value.Count -gt 0) { + Move-TaskGroups -SourceTaskGroups $sourceTaskGroups.value -TargetProjectName $TargetProjectName ` + -TargetOrgName $TargetOrgName -TargetHeaders $TargetHeaders + } + else { + Write-Log -Message "No task groups found in the source project to migrate." + } + + $targetTaskGroups = Get-TaskGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -Headers $TargetHeaders + + $targetRepos = Get-Repos -projectName $TargetProjectName -orgName $TargetOrgName -headers $TargetHeaders + + $pipelinesToMigrate = $sourcePipelines | Where-Object { $targetPipelineNames -notcontains $_.name } + + $CreatedPipelinesCount = 0 + $FailedPipelinesCount = 0 + foreach ($pipeline in $pipelinesToMigrate) { + try { + $CreatePipeline = $false + + $definition = Get-BuildDefinition -OrgName $SourceOrgName -ProjectName $SourceProjectName ` + -Headers $SourceHeaders -DefinitionId $pipeline.id + + foreach ($phase in $definition.process.phases) { + foreach ($step in $phase.steps) { + #If the service connection ID is located, the we have to swap the value for the appropriate target service connection + $ServiceConnectionIdforRunningFortifyScan = $step.inputs.cloudScanfortifyServerName + if ($step.inputs.externalEndpoints) { + $ExternalEndpointsServiceConnectionIds = $step.inputs.externalEndpoints -split "," + + $endpointsString = "" + forEach ($endpoint in $ExternalEndpointsServiceConnectionIds) { + if ($endpoint -ne "") { + $serviceConnectionName = $sourceEndpoints | Where-Object { $_.id -eq $ServiceConnectionIdforRunningFortifyScan } | Select-Object -ExpandProperty name + $targetServiceConnectionId = $targetEndpoints | Where-Object { $_.name -eq $serviceConnectionName } | Select-Object -ExpandProperty id + $endpointsString += ",$targetServiceConnectionId" + } + } + $step.inputs.externalEndpoints = $endpointsString + } + + $AzureServiceConnectionId = $step.inputs.azureSubscription + $ConnectedServiceNameARMServiceConnectionId = $step.inputs.connectedServiceNameARM + if ($null -ne $ServiceConnectionIdforRunningFortifyScan) { + + $serviceConnectionName = $sourceEndpoints | Where-Object { $_.id -eq $ServiceConnectionIdforRunningFortifyScan } | Select-Object -ExpandProperty name + $targetServiceConnectionId = $targetEndpoints | Where-Object { $_.name -eq $serviceConnectionName } | Select-Object -ExpandProperty id + $step.inputs.cloudScanfortifyServerName = $targetServiceConnectionId + if ($MigrateOnlyProblematicPipelines) { + $CreatePipeline = $true + } + } + + if ($MigrateOnlyProblematicPipelines) { + $CreatePipeline = $true + } + if ($null -ne $AzureServiceConnectionId) { + $serviceConnectionName = $sourceEndpoints | Where-Object { $_.id -eq $ServiceConnectionIdforRunningFortifyScan } | Select-Object -ExpandProperty name + $targetServiceConnectionId = $targetEndpoints | Where-Object { $_.name -eq $serviceConnectionName } | Select-Object -ExpandProperty id + $step.inputs.azureSubscription = $targetServiceConnectionId + if ($MigrateOnlyProblematicPipelines) { + $CreatePipeline = $true + } + } + if ($null -ne $ConnectedServiceNameARMServiceConnectionId) { + $serviceConnectionName = $sourceEndpoints | Where-Object { $_.id -eq $ConnectedServiceNameARMServiceConnectionId } | Select-Object -ExpandProperty name + $targetServiceConnectionId = $targetEndpoints | Where-Object { $_.name -eq $serviceConnectionName } | Select-Object -ExpandProperty id + $step.inputs.connectedServiceNameARM = $targetServiceConnectionId + if ($MigrateOnlyProblematicPipelines) { + $CreatePipeline = $true + } + } + } + } + if (!$MigrateOnlyProblematicPipelines -OR $CreatePipeline -eq $true) { + Write-Log "Creating Pipeline $($definition.name) using PowerShell due to hardcoded a harcoded service connection id input" + $targetRepo = $TargetRepos | Where-Object { $_.name -eq $definition.repository.name } + + if ($null -eq $targetRepo) { + Write-Warning -Message "Unable to create pipeline: $($definition.name) because missing repo: $($definition.repository.name)" + Write-Warning -Message "Most likey caused by repo being disabled." + continue + } + + $targetAgentPool = $targetAgentPools.value | Where-Object { $_.name -eq $definition.queue.name } + + $definition.repository.id = $targetRepo.id + $definition.repository.url = "https://dev.azure.com/$targetOrgName/$targetProjectName/_git/$repoName" + $definition.project.id = $targetProject.id + $definition.queue.id = $targetAgentPool.id + $definition.queue.url = "https://dev.azure.com/$targetOrgName/_apis/build/Queues/$($targetAgentPool.id)" + $definition.queue.pool.id = $targetAgentPool.pool.id + + forEach ($phase in $definition.process.phases) { + if ($null -ne $phase.target.queue) { + $phase.target.queue.id = $targetAgentPool.id + $phase.target.queue.url = "https://dev.azure.com/$targetOrgName/_apis/build/Queues/$($targetAgentPool.id)" + } + + foreach ($step in $phase.steps) { + # Only map task IDs if there are source task groups + if ($sourceTaskGroups.value -and $sourceTaskGroups.value.Count -gt 0) { + $step.task.id = Get-TargetTaskId -SourceTaskGroups $sourceTaskGroups.value -TargetTaskGroups ` + $targetTaskGroups.value -SourceTaskGroupId $step.task.id + } + } + } + + if ($null -ne $definition.queue.pool.isHosted) { + $definition.queue.pool.isHosted = $targetAgentPool.pool.isHosted + } + + $newPipeline = New-Pipeline -PipelineDefinition $definition -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $TargetHeaders + if ($null -ne $newPipeline) { + Write-Log "Created Classic Build Pipeline $($newPipeline.name)" + $CreatedPipelinesCount += 1 + } + else { + Write-Log "Failed to create Classic Build Pipeline $($definition.name)" + $FailedPipelinesCount += 1 + } + + Move-BuildEnvironmentPipelinePermissions -SourceProjectName $SourceProjectName ` + -SourceOrgName $SourceOrgName -SourceHeaders $SourceHeaders -TargetProjectName ` + $TargetProjectName -TargetOrgName $TargetOrgName -TargetHeaders $TargetHeaders + } + } + catch { + $FailedPipelinesCount += 1 + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message " " + } + } + + Write-Log "Successfully migrated $CreatedPipelinesCount classic pipeline(s) with a hardcoded service connection id input" + Write-Log "Failed to migrate $FailedPipelinesCount classic pipeline(s) with a hardcoded service connection id input" + } +} + +function New-Pipeline { param( [Parameter (Mandatory = $TRUE)] [String]$ProjectName, @@ -10,18 +205,386 @@ function Get-Pipelines { [Parameter (Mandatory = $TRUE)] [Hashtable]$Headers, - [Parameter (Mandatory = $FALSE)] - [String]$RepoId = $NULL + [Parameter (Mandatory = $TRUE)] + [Object]$PipelineDefinition + ) + + $PipelineDefinition.process.phases[0].jobAuthorizationScope = $PipelineDefinition.jobAuthorizationScope + + $definition = @{ + name = $PipelineDefinition.name + type = $PipelineDefinition.type + queue = $PipelineDefinition.queue + process = @{ + type = $PipelineDefinition.process.type + phases = $PipelineDefinition.process.phases + target = $PipelineDefinition.process.target + } + repository = $PipelineDefinition.repository + project = $PipelineDefinition.project + path = $PipelineDefinition.path + jobAuthorizationScope = $PipelineDefinition.jobAuthorizationScope + authoredBy = $PipelineDefinition.authoredBy + jobCancelTimeoutInMinutes = $PipelineDefinition.jobCancelTimeoutInMinutes + jobTimeoutInMinutes = $PipelineDefinition.jobTimeoutInMinutes + createdDate = $PipelineDefinition.createdDate + triggers = $PipelineDefinition.triggers + } + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/build/definitions?api-version=7.1" + $body = $definition | ConvertTo-Json -Depth 10 + + $response = Invoke-RestMethod -Method POST -uri $url -Headers $Headers -Body $body -ContentType "application/json" + return $response +} + +function Move-BuildEnvironmentPipelinePermissions { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders + ) + + $sourceProject = Get-ADOProjects -OrgName $SourceOrgName -ProjectName ` + $SourceProjectName -Headers $SourceHeaders + $targetProject = Get-ADOProjects -OrgName $TargetOrgName -ProjectName ` + $TargetProjectName -Headers $TargetHeaders + + $targetEnvironments = Get-BuildEnvironments -ProjectName $TargetProjectName ` + -OrgName $TargetOrgName -Headers $Targetheaders -ProjectId ` + $targetProject.Id -Top 1000000 + + $sourceEnvironments = Get-BuildEnvironments -ProjectName $SourceProjectName ` + -OrgName $SourceOrgName -Headers $SourceHeaders -ProjectId ` + $sourceProject.Id -Top 1000000 + + foreach ($targetEnvironment in $targetEnvironments) { + Write-Log -Message "------------------------------------------------------------------------------------------------------------------" + Write-Log -Message "----- Processing Environment $($sourceEnvironment.name) -----" + Write-Log -Message "------------------------------------------------------------------------------------------------------------------" + + $sourceEnvironment = $sourceEnvironments | Where-Object { $_.name -eq $targetEnvironment.name } + + if ($NULL -ne $sourceEnvironment) { + # Get and Update Role Assignments + $sourceRoleAssignments = Get-BuildEnvironmentRoleAssignments -ProjectName ` + $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -ProjectId ` + $sourceProject.Id -EnvironmentId $sourceEnvironment.Id + $targetRoleAssignments = $NULL + Write-Log -Message "--- User permissions --- " + $targetRoleAssignments = Get-BuildEnvironmentRoleAssignments -ProjectName ` + $targetProjectName -OrgName $targetOrgName -Headers $targetheaders ` + -ProjectId $targetProject.Id -EnvironmentId $targetEnvironment.Id + + foreach ($roleAssignment in $sourceRoleAssignments) { + $roleName = $roleAssignment.identity.displayName.Replace($SourceProjectName, $TargetProjectName) + + try { + Write-Log -Message "Attempting to create role assignment [$($roleName)] in target.. " + + $query = $targetRoleAssignments | Where-Object { $_.name -eq $roleAssignment.name } + + if ($query) { + Write-Log -Message "Role assignment $($roleAssignment.name) already exists." + continue + } + + $identities = Get-IdentitiesByName -OrgName $TargetOrgName -Headers $TargetHeaders -DisplayName $roleName + + if ($identities.Count -eq 1) { + $data = @{ + "roleName" = $roleAssignment.Role.Name + "userId" = $identities.Value[0].id + } + + $scope = $roleAssignment.Role.Scope + Write-Log -Message " " + Write-Log -Message "Create new Role Assignment $($roleAssignmentIdentityId) / $($roleAssignment.role.name) in target.." + Write-Log -Message "Scope: $scope" + Write-Log -Message "Source Environment ID: $($sourceEnvironment.Id)" + Write-Log -Message "Source Identity Display Name: $($roleAssignment.Identity.displayName)" + Write-Log -Message "Source Identity Id: $($roleAssignment.Identity.Id)" + Write-Log -Message "Source Role Name: $($roleAssignment.role.name)" + Write-Log -Message "Target Identity Id: $($identities.Value[0].id)" + Write-Log -Message "Target Role Name: $($roleAssignment.role.name)" + Write-Log -Message "Target Environment ID: $($targetEnvironment.Id)" + Write-Log -Message "Target Org Name: $TargetOrgName" + Write-Log -Message "Target Project ID: $($targetProject.Id)" + Write-Log -Message " " + + Set-BuildEnvironmentRoleAssignment -ProjectName $TargetProjectName ` + -OrgName $TargetOrgName -Headers $Targetheaders -ProjectId ` + $targetProject.Id -EnvironmentId $targetEnvironment.Id -ScopeId ` + $scope -RoleAssignment $data + } + else { + Write-Log -Message "Unable to find role $roleName, please add it manually" -LogLevel WARNING + } + + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message " " + } + } + + + + # Get and Update pipeline permissions + Write-Log -Message "--- Pipline permissions --- " + + Write-Log -Message "Get Source Pipline permissions.." + $sourcePipelinePermissions = Get-BuildEnvironmentPipelinePermissions -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -EnvironmentId $sourceEnvironment.Id + $targetPipelinePermissions = Get-BuildEnvironmentPipelinePermissions -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -EnvironmentId $targetEnvironment.Id + + $newPipelinePermissions = @() + if ($TRUE -eq $ReplacePipelinePermissions) { + $newPipelinePermissions = $targetPipelinePermissions.Pipelines.Clone() + $newPipelinePermissions = @($newPipelinePermissions | Where-Object { $_.Id -notin $sourcePipelinePermissions.Pipelines.Id }) + + # Set to remove all items that are not in the Source Pipeline Permissions + foreach ($permission in $newPipelinePermissions) { + $permission.PSObject.Members.Remove("authorizedBy") + $permission.PSObject.Members.Remove("authorizedOn") + $permission.Authorized = $FALSE + } + } + + foreach ($pipelinePermission in $sourcePipelinePermissions.Pipelines) { + $sourcePipeline = Get-BuildDefinition -Headers $SourceHeaders -OrgName ` + $SourceOrgName -ProjectName $SourceProjectName -DefinitionId ` + $pipelinePermission.Id + $targetPipeline = ($targetPipelines | Where-Object { $_.Name -ceq $sourcePipeline.Name }) + + if ($NULL -ne $targetPipeline) { + $object = [PSCustomObject]@{ + id = $targetPipeline.Id + authorized = $pipelinePermission.Authorized + } + $newPipelinePermissions += $object + } + else { + Write-Log -Message "Unable to map Source Pipeline ID [$($pipelinePermission.Id)] to a Target pipeline in order to set a Environment pipeline permission.." -LogLevel ERROR + } + } + + try { + Write-Log -Message "Update Target Pipline permissions.." + + Write-Log -Message " " + Write-Log -Message "Target Environment Id: $($targetEnvironment.Id)" + Write-Log -Message "PipelinePermissions: $(ConvertTo-Json -Depth 100 $newPipelinePermissions)" + + Set-BuildEnvironmentPipelinePermissions -ProjectName $TargetProjectName -OrgName $TargetOrgName ` + -Headers $Targetheaders -EnvironmentId $targetEnvironment.Id -PipelinePermissions $newPipelinePermissions + } + catch { + Write-Log -Message "FAILED to Update Build Environment User Permissions ROle Assignment!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } + catch {} + } + } + Write-Log -Message "------------------------------------------------------------------------------------------------------------------" + } +} + +function Get-Pipelines { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/pipelines?api-version=7.2-preview.1" + + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results.value +} + +function Move-TaskGroups { + param ( + [Parameter (Mandatory = $TRUE)] + [array]$SourceTaskGroups, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders + ) + + $targetTaskGroups = Get-TaskGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -Headers $TargetHeaders + + Write-Log -Message "Migrating $($SourceTaskGroups.count) Task Groups." + + foreach ($stg in $SourceTaskGroups) { + Write-Log -Message "Migrating Task Group: $($stg.name)." + $query = $targetTaskGroups.value | Where-Object { $_.name -eq $stg.name } + + if ($null -eq $query) { + try { + New-TaskGroup -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers ` + $TargetHeaders -TaskGroup $stg + + Write-Log -Message "Migrated Successfully." + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + } + } + else { + Write-Log -Message "Task Group $($stg.name) already exists." + } + } +} + +function New-TaskGroup { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + $TaskGroup + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/distributedtask/" ` + + "taskgroups?api-version=7.1" + $body = ConvertTo-Json -Depth 32 $TaskGroup + + $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -ContentType ` + "application/json" -Headers $TargetHeaders + + return $result +} + +function Get-TaskGroups { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers ) - if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/distributedtask/taskgroups" ` + + "?api-version=7.1" - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/build/definitions?api-version=7.0" - if ($RepoId) { - $url = "https://dev.azure.com//$OrgName/$ProjectName/_apis/build/definitions?repositoryId=$RepoId&repositoryType=TfsGit"; + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results +} + +function Get-TargetTaskId { + param ( + [Parameter (Mandatory = $FALSE)] # Changed to not mandatory + $SourceTaskGroups = @(), + + [Parameter (Mandatory = $FALSE)] # Changed to not mandatory + $TargetTaskGroups = @(), + + [Parameter (Mandatory = $TRUE)] + [string]$SourceTaskGroupId + ) + + # Return original ID if source task groups are empty + if ($null -eq $SourceTaskGroups -or $SourceTaskGroups.Count -eq 0) { + return $SourceTaskGroupId + } + + $stg = $SourceTaskGroups | Where-Object { $_.id -eq $SourceTaskGroupId } + $result = $SourceTaskGroupId + + if ($stg) { + $ttg = $TargetTaskGroups | Where-Object { $_.name -eq $stg.name } + + if ($ttg) { + $result = $ttg.id } + } + + return $result +} + +function Remove-AllBuildDefinitions { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + $buildDefinitions = Get-BuildDefinition -OrgName $OrgName -ProjectName $ProjectName -Headers ` + $Headers + + foreach ($bd in $buildDefinitions.value) { + Remove-BuildDefinition -OrgName $OrgName -ProjectName $ProjectName -Headers ` + $Headers -BuildDefinitionId $bd.id - return $results.value + Write-Log -Message "Build definition $($bd.name) removed." } } + +function Remove-BuildDefinition { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$BuildDefinitionId + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/build/definitions" ` + + "/$($BuildDefinitionId)?api-version=7.1" + + Invoke-RestMethod -Method Delete -Uri $url -Headers $Headers +} \ No newline at end of file diff --git a/modules/Migrate-ADO-Policies.psm1 b/modules/Migrate-ADO-Policies.psm1 index ab6f3c0..bb3ef11 100644 --- a/modules/Migrate-ADO-Policies.psm1 +++ b/modules/Migrate-ADO-Policies.psm1 @@ -26,12 +26,12 @@ function Start-ADOPoliciesMigration { Write-Log -Message "Get Source Policies.." $sourcePolicies = Get-Policies -ProjectName $SourceProjectName -orgName $SourceOrgName -headers $SourceHeaders # Write-Log -Message "Get Target Policies.." - # $targetPolicies = Get-Policies -ProjectName $targetProjectName -orgName $targetOrgName -headers $targetHeaders + $targetPolicies = Get-Policies -ProjectName $targetProjectName -orgName $targetOrgName -headers $targetHeaders # Write-Log -Message "Get Source Pipelines for source to target mapping.." - $sourcePipelines = Get-Pipelines -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName + $sourcePipelines = Get-BuildDefinitions -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName # Write-Log -Message "Get Target Pipelines for source to target mapping.." - $targetPipelines = Get-Pipelines -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName + $targetPipelines = Get-BuildDefinitions -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName # Write-Log -Message "Get Target Repositories for source to target mapping.." $sourceRepos = Get-Repos -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders @@ -63,8 +63,8 @@ function Start-ADOPoliciesMigration { $haveMissingComponents = $FALSE foreach ($entry in $processPolicy.settings.scope) { if ($null -ne $entry.repositoryId) { + Write-Log -Message "Mapping repository id $($entry.repositoryId).. " - # $sourceRepo = Get-Repo -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $TargetHeaders -repoId $entry.repositoryId $sourceRepo = $sourceRepos | Where-Object { $_.Id -eq $entry.repositoryId } if ($null -eq $sourceRepo) { $sourceRepo = Get-Repo -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $TargetHeaders -repoId $entry.repositoryId @@ -77,10 +77,12 @@ function Start-ADOPoliciesMigration { # Write-Log -Message "Could not find the repositoryId $($entry.name) [$($entry.repositoryId)] in target while attempting to migrate policy." -LogLevel WARNING $haveMissingComponents = $TRUE $entry.repositoryId = $NULL - } else { + } + else { $entry.repositoryId = $targetRepo.id } - } else { + } + else { $strMsg += ("Could not find the repositoryId $($entry.repositoryId) in source while attempting to migrate policy." + "`n") # Write-Log -Message "Could not find the repositoryId $($entry.repositoryId) in source while attempting to migrate policy." -LogLevel WARNING $haveMissingComponents = $TRUE @@ -88,49 +90,53 @@ function Start-ADOPoliciesMigration { } } - if($NULL -ne $processPolicy.settings.buildDefinitionId) { + if ($NULL -ne $processPolicy.settings.buildDefinitionId) { Write-Log -Message "Mapping buildDefinitionId id $($processPolicy.settings.buildDefinitionId).. " - # $sourcePipeline = Get-Pipeline -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName -DefinitionId $processPolicy.settings.buildDefinitionId + # $sourcePipeline = Get-BuildDefinition -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName -DefinitionId $processPolicy.settings.buildDefinitionId $sourcePipeline = $sourcePipelines | Where-Object { $_.Id -eq $processPolicy.settings.buildDefinitionId } - if($NULL -eq $sourcePipeline) { - $sourcePipeline = Get-Pipeline -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName -DefinitionId $processPolicy.settings.buildDefinitionId + if ($NULL -eq $sourcePipeline) { + $sourcePipeline = Get-BuildDefinition -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName -DefinitionId $processPolicy.settings.buildDefinitionId } - if($NULL -ne $sourcePipeline) { - $targetPipeline = ($targetPipelines | Where-Object {$_.Name -eq $sourcePipeline.Name}) + if ($NULL -ne $sourcePipeline) { + $targetPipeline = ($targetPipelines | Where-Object { $_.Name -eq $sourcePipeline.Name }) if ($null -ne $targetPipeline) { $processPolicy.settings.buildDefinitionId = $targetPipeline.id - } else { + } + else { $strMsg += ("Could not find Target pipeline for settings.buildDefinitionId $($processPolicy.settings.buildDefinitionId) in Policy ID [$($processPolicy.id)] while attempting to migrate policy." + "`n") # Write-Log -Message "Could not find Target pipeline in settings.buildDefinitionId $($processPolicy.settings.buildDefinitionId) in Policy ID [$($processPolicy.id)] while attempting to migrate policy." -LogLevel WARNING $haveMissingComponents = $TRUE #continue # $processPolicy.settings.buildDefinitionId = $NULL } - } else { + } + else { $strMsg += ("Could not find Source pipeline for settings.buildDefinitionId $($processPolicy.settings.buildDefinitionId) in Policy ID [$($processPolicy.id)] while attempting to migrate policy." + "`n") $haveMissingComponents = $TRUE } } - if($NULL -ne $processPolicy.settings.requiredReviewerIds) { + if ($NULL -ne $processPolicy.settings.requiredReviewerIds) { Write-Log -Message "Mapping Required Reviewer Ids ($($policy.settings.requiredReviewerIds)) in Policy ID [$($policy.id)] from source to target." $failedToFindReviewerId = $FALSE $newRequiredReviewerIds = @() - foreach($Id in $processPolicy.settings.requiredReviewerIds){ + foreach ($Id in $processPolicy.settings.requiredReviewerIds) { # Search Users for the requiredReviewerId # Write-Log -Message "Attempting to locate Required Reviewer Id ($($Id)) in Policy ID [$($policy.id)] while attempting to migrate policy." $existingGroup = $sourceGroups | Where-Object { $_.Id -eq $Id } $migratedGroup = $targetGroups | Where-Object { $_.Name -eq $existingGroup.Name } if ($NULL -ne $migratedGroup) { $newRequiredReviewerIds += $migratedGroup.Id - } else { + } + else { $sourceUser = ($sourceUsers | Where-Object { $_.Id -eq $Id }) $targetUser = ($targetUsers | Where-Object { $_.MailAddress -eq $sourceUser.MailAddress }) if ($NULL -ne $targetUser) { $newRequiredReviewerIds += $targetUser.Id - } else { + } + else { $strMsg += ("Could not find Required Reviewer Id: ($($Id)) for Policy ID [$($policy.id)] in target Groups or users." + "`n") # Write-Log -Message "Could not find Required Reviewer Id: ($($Id)) for Policy ID [$($policy.id)] in target Groups or users." -LogLevel WARNING $failedToFindReviewerId = $TRUE @@ -138,14 +144,15 @@ function Start-ADOPoliciesMigration { } } - if($failedToFindReviewerId -eq $TRUE) { + if ($failedToFindReviewerId -eq $TRUE) { $haveMissingComponents = $TRUE - } else { + } + else { $processPolicy.settings.requiredReviewerIds = $newRequiredReviewerIds } } - if($haveMissingComponents) { + if ($haveMissingComponents) { Write-Log -Message "Unable to create NEW Policy for Source Policy '$($processPolicy.type.displayName)' [Id: $($processPolicy.id)] in target!" -LogLevel ERROR Write-Log -Message $strMsg -LogLevel ERROR # $policyJson = ConvertTo-Json -Depth 100 $processPolicy @@ -159,23 +166,27 @@ function Start-ADOPoliciesMigration { Write-Log -Message "Created NEW Policy '$($processPolicy.type.displayName)' [Id: $($processPolicy.id)] in target!" Write-Log -Message "Done!" -LogLevel SUCCESS Write-Host $result - } catch { + } + catch { $err = ConvertFrom-json -Depth 100 $_ - if($err.typeKey -eq "PolicyChangeRejectedByPolicyException") { + if ($err.typeKey -eq "PolicyChangeRejectedByPolicyException") { Write-Log -Message "Policy '$($processPolicy.type.displayName)' [Id: $($processPolicy.id)] already exist in target." - } else { + } + else { Write-Log -Message ($_) -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR } } Write-Host " " - } catch { + } + catch { Write-Log -Message "FAILED!" -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} + } + catch {} } } } diff --git a/modules/Migrate-ADO-Project.psm1 b/modules/Migrate-ADO-Project.psm1 index b9f21b6..e1d154b 100644 --- a/modules/Migrate-ADO-Project.psm1 +++ b/modules/Migrate-ADO-Project.psm1 @@ -16,18 +16,25 @@ function Start-ADOProjectMigration { [Parameter (Mandatory = $TRUE)] [String]$ArtifactFeedPackageVersionLimit, # -------------- What parts of the migration should NOT be executed --------------- - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateGroups = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateBuildQueues = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateRepos = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateWikis = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateServiceHooks = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigratePolicies = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateDashboards = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateServiceConnections = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateArtifacts = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateDeliveryPlans = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipAzureDevOpsMigrationTool = $TRUE, - [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateOrganizationUsers = $TRUE + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateGroups = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateBuildQueues = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateRepos = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateWikis = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateServiceHooks = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigratePolicies = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateDashboards = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateServiceConnections = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateArtifacts = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateDeliveryPlans = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipAzureDevOpsMigrationTool = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateOrganizationUsers = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipAddReflectedWorkItemIdField = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateVariableGroups = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateBuildPipelines = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateReleasePipelines = $TRUE, + [parameter(Mandatory = $FALSE)] [Boolean]$SkipMigrateTfsAreaAndIterations = $TRUE + + ) if ($PSCmdlet.ShouldProcess( "Target project $TargetOrg/$TargetProjectName", @@ -53,11 +60,11 @@ function Start-ADOProjectMigration { # ====== Migrate Users On Org Level ====== #region ================================== Start-ADOUserMigration ` - -SourceOrgName $SourceOrgName ` - -SourcePat $SourcePAT ` - -TargetOrgName $TargetOrgName ` - -TargetPAT $TargetPAT ` - -WhatIf: $SkipMigrateOrganizationUsers + -SourceOrgName $SourceOrgName ` + -SourcePat $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetPAT $TargetPAT ` + -WhatIf: $SkipMigrateOrganizationUsers #endregion @@ -82,13 +89,36 @@ function Start-ADOProjectMigration { # Migrate-ADO-BuildQueues.psm1 #region ================================== Start-ADOBuildQueuesMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SkipMigrateBuildQueues + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateBuildQueues + #endregion + + # ======================================== + # ========= Migrate Build Queues ========= + # Migrate-ADO-AreaPaths.psm1 + #region ================================== + Start-ADOAreaPathsMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateTfsAreaAndIterations + + Start-ADOIterationsMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateTfsAreaAndIterations #endregion # ============================================== @@ -96,36 +126,47 @@ function Start-ADOProjectMigration { # Migrate-ADO-BuildEnvironments.psm1 #region ======================================== Start-ADOBuildEnvironmentsMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -SourcePat $SourcePAT ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -TargetPAT $TargetPAT ` - -ReplacePipelinePermissions $TRUE ` - -WhatIf:$SkipMigrateBuildQueues + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -SourcePat $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -TargetPAT $TargetPAT ` + -ReplacePipelinePermissions $TRUE ` + -WhatIf:$SkipMigrateBuildQueues #endregion - + # ============================================== + # ========= Migrate Retension Policies ========= + # Migrate-ADO-Retentions.psm1 + #region ======================================== + Start-ADORetentionMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateBuildQueues + #endregion - # ======================================== # ============ Migrate Repos ============= # Migrate-ADO-Repos.psm1 #region ================================== Start-ADORepoMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourcePat $SourcePAT ` - -SourceHeaders $sourceHeaders ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetPAT $TargetPAT ` - -TargetHeaders $targetHeaders ` - -ReposPath $RepositoryCloneTempDirectory ` - -WhatIf:$SkipMigrateRepos + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourcePat $SourcePAT ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetPAT $TargetPAT ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateRepos + # -ReposPath $RepositoryCloneTempDirectory ` #endregion # ======================================== @@ -133,16 +174,16 @@ function Start-ADOProjectMigration { # Migrate-ADO-Repos.psm1 #region ================================== Start-ADOWikiMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -SourcePat $SourcePAT ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -TargetPAT $TargetPAT ` - -ReposPath $RepositoryCloneTempDirectory ` - -WhatIf:$SkipMigrateWikis + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -SourcePat $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -TargetPAT $TargetPAT ` + -ReposPath $RepositoryCloneTempDirectory ` + -WhatIf: $SkipMigrateWikis #endregion # ======================================== @@ -150,16 +191,54 @@ function Start-ADOProjectMigration { # Migrate-ADO-ServiceConnections.psm1 #region ================================== Start-ADOServiceConnectionsMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SkipMigrateServiceConnections + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf: $SkipMigrateServiceConnections #endregion - + Start-ADOVariableGroupsMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateVariableGroups + + # ======================================== + # ===== Add Refelcted WorkItem ID to Test Suites, Plans, and Cases ====== + # Migrate-ADO-ServiceConnections.psm1 + #region ================================== + + Start-ADO_AddCustomField ` + -Headers $targetHeaders ` + -OrgName $TargetOrgName ` + -ProjectName $TargetProjectName ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -WhatIf: $SkipAddReflectedWorkItemIdField + + #endregion + + # ======================================== + # ===== Add Classic Pipelines (which have service connection IDs as inputs) ====== + # Migrate-ADO-Pipelines.psm1 + #region ================================== + Write-Log "SkipMigrateBuildPipelines: $SkipMigrateBuildPipelines" + Start-ClassicBuildPipelinesMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf: $SkipMigrateBuildPipelines + + #endregion # ========================================== # ====== Azure DevOps Migration Tool ====== @@ -178,7 +257,8 @@ function Start-ADOProjectMigration { Start-Process -NoNewWindow -Wait -FilePath .\devopsmigration.exe -ArgumentList $arguments Set-Location -Path $savedpath - } else { + } + else { Write-Host "What if: Preforming the operation `"Running Azure DevOps Migration Tool Migration from source project $SourceProjectName`" on target `"Target project $TargetProjectName`"" } #endregion @@ -188,13 +268,13 @@ function Start-ADOProjectMigration { # Migrate-ADO-Groups.psm1 #region ================================== Start-ADOGroupsMigration ` - -SourcePAT $SourcePAT ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -TargetPAT $TargetPAT ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -WhatIf:$SkipMigrateGroups + -SourcePAT $SourcePAT ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -TargetPAT $TargetPAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -WhatIf:$SkipMigrateGroups #endregion # ======================================== @@ -204,13 +284,13 @@ function Start-ADOProjectMigration { #region ================================== # .\migrateServiceHooks.ps1 Start-ADOServiceHooksMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SkipMigrateServiceHooks + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateServiceHooks # #endregion # ======================================== @@ -219,15 +299,15 @@ function Start-ADOProjectMigration { #region ================================== # .\migratePolicies.ps1 Start-ADOPoliciesMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -SourcePAT $SourcePAT ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -TargetPAT $TargetPAT ` - -WhatIf:$SkipMigratePolicies + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -SourcePAT $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -TargetPAT $TargetPAT ` + -WhatIf:$SkipMigratePolicies # #endregion # ======================================== @@ -236,13 +316,13 @@ function Start-ADOProjectMigration { #region ================================== # .\migrateDashboards.ps1 Start-ADODashboardsMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SkipMigrateDashboards + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateDashboards # #endregion # =========================================== @@ -250,15 +330,15 @@ function Start-ADOProjectMigration { # Migrate-ADO-DeliveryPlans.psm1 #region ===================================== Start-ADODeliveryPlansMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -SourcePAT $SourcePAT ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -TargetPAT $TargetPAT ` - -WhatIf:$SkipMigrateDeliveryPlans + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -SourcePAT $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -TargetPAT $TargetPAT ` + -WhatIf:$SkipMigrateDeliveryPlans # #endregion # ======================================== @@ -266,19 +346,32 @@ function Start-ADOProjectMigration { # Migrate-ADO-Artifacts.psm1 #region ================================== Start-ADOArtifactsMigration ` - -SourceOrgName $SourceOrgName ` - -SourceProjectName $SourceProjectName ` - -SourceHeaders $sourceHeaders ` - -SourcePAT $SourcePAT ` - -TargetOrgName $TargetOrgName ` - -TargetProjectName $TargetProjectName ` - -TargetHeaders $targetHeaders ` - -TargetPAT $TargetPAT ` - -ProjectPath $projectPath ` - -ArtifactFeedPackageVersionLimit $ArtifactFeedPackageVersionLimit ` - -WhatIf:$SkipMigrateArtifacts + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -SourcePAT $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -TargetPAT $TargetPAT ` + -ArtifactFeedPackageVersionLimit $ArtifactFeedPackageVersionLimit ` + -WhatIf:$SkipMigrateArtifacts # #endregion + # ======================================== + # =========== Release Pipelines ============= + # Migrate-ADO-ReleaseDefinitions.psm1 + #region ================================== + Start-ADOReleaseDefinitionsMigration ` + -SourceHeaders $sourceHeaders ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -TargetHeaders $targetHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -WhatIf:$SkipMigrateReleasePipelines + #endregion + # ======================================== # ========== Migration Finished ========== # ======================================== diff --git a/modules/Migrate-ADO-ReleaseDefinitions.psm1 b/modules/Migrate-ADO-ReleaseDefinitions.psm1 index 34f14e0..1e6aa8b 100644 --- a/modules/Migrate-ADO-ReleaseDefinitions.psm1 +++ b/modules/Migrate-ADO-ReleaseDefinitions.psm1 @@ -1,3 +1,7 @@ +Using Module "..\modules\Migrate-ADO-Common.psm1" +Using module "..\modules\Migrate-ADO-VariableGroups.psm1" +Using module "..\modules\Migrate-ADO-Pipelines.psm1" +Using module "..\modules\Migrate-ADO-ServiceConnections.psm1" function Start-ADOReleaseDefinitionsMigration { [CmdletBinding(SupportsShouldProcess)] @@ -13,13 +17,656 @@ function Start-ADOReleaseDefinitionsMigration { "Target project $TargetOrg/$TargetProjectName", "Migrate Release Definitions from source project $SourceOrgName/$SourceProjectName") ) { + $ErrorActionPreference = "Continue" Write-Log -Message ' ' Write-Log -Message '---------------------------------' Write-Log -Message '-- Migrate Release Definitions --' Write-Log -Message '---------------------------------' Write-Log -Message ' ' - # DO WORK HERE + $sourceReleases = Get-ReleaseDefinitions -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders + $targetReleases = Get-ReleaseDefinitions -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $TargetHeaders + $sourceProject = Get-ADOProjects -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders + $targetProject = Get-ADOProjects -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $TargetHeaders + + $targetReleasePipelineNames = $targetReleases.value | Select-Object -ExpandProperty name + $releasesToMigrate = $sourceReleases.value | Where-Object { $targetReleasePipelineNames -notcontains $_.name } + + $sourceVariableGroups = Get-VariableGroups -projectName $SourceProjectName -orgName $SourceOrgName -headers $SourceHeaders + $targetVariableGroups = Get-VariableGroups -projectName $TargetProjectName -orgName $TargetOrgName -headers $TargetHeaders + + $sourceEndpoints = Get-ServiceEndpoints -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders + $targetEndpoints = Get-ServiceEndpoints -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders + + Write-Log "Attempting to migrate $($releasesToMigrate.count) releases" + $CreatedPipelinesCount = 0 + $FailedPipelinesCount = 0 + + Move-DeploymentPools -SourceProjectName $SourceProjectName -SourceOrgName $SourceOrgName ` + -SourceHeaders $SourceHeaders -TargetProjectName $TargetProjectName -TargetOrgName $TargetOrgName ` + -TargetHeaders $TargetHeaders + Move-AgentPools -SourceOrgName $SourceOrgName -SourceProjectName $SourceProjectName ` + -SourceHeaders $SourceHeaders -TargetOrgName $TargetOrgName -TargetProjectName $TargetProjectName ` + -TargetHeaders $TargetHeaders + Move-DeploymentGroups -SourceProjectName $SourceProjectName -SourceOrgName $SourceOrgName ` + -SourceHeaders $SourceHeaders -TargetProjectName $TargetProjectName -TargetOrgName $TargetOrgName ` + -TargetHeaders $TargetHeaders + + $sourceTaskGroups = Get-TaskGroups -OrgName $SourceOrgName -ProjectName $SourceProjectName ` + -Headers $SourceHeaders + $targetTaskGroups = Get-TaskGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -Headers $TargetHeaders + + ForEach ($release in $releasesToMigrate) { + Write-Log "Migrating Release Pipeline: $($release.name)" + $releaseDetail = Get-ReleaseDetail -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders + + forEach ($environment in $releaseDetail.environments) { + $environment.currentRelease.url = $environment.currentRelease.url.Replace($SourceOrgName, $TargetOrgName).Replace($sourceProject.id, $targetProject.id) + $environment.badgeUrl = "" + + forEach ($phase in $environment.deployPhases) { + forEach ($workflowTask in $phase.workflowTasks) { + if (($workflowTask.name -like "Azure Logic Apps Standard Release*" -OR + $workflowTask.name -like "Restart App Service" -OR + $workflowTask.name -like "Azure App Service Deploy*" -OR + $workflowTask.name -like "VsTest - testAssemblies") -AND + $workflowTask.inputs.connectedServiceName) { + $targetServiceConnectionId = Get-TargetServiceConnectionId -SourceEndpoints $sourceEndpoints ` + -TargetEndpoints $targetEndpoints -SourceServiceConnectionId $($workflowTask.inputs.connectedServiceName) + $workflowTask.inputs.connectedServiceName = $targetServiceConnectionId + } + + $workflowTask.taskId = Get-TargetTaskId -SourceTaskGroups $sourceTaskGroups.value -TargetTaskGroups ` + $targetTaskGroups.value -SourceTaskGroupId $workflowTask.taskId + } + + # Remvoe disabled workflow tasks + $phase.workflowTasks = @($phase.workflowTasks | Where-Object { $_.enabled -eq $true }) + + # Remove deploymentInput property from request body + $phase.PSObject.Properties.Remove("deploymentInput") + } + if ($null -ne $environment.variableGroups -AND $environment.variableGroups.Count -gt 0) { + $variableGroups = @() + forEach ($variableGroupId in $environment.variableGroups) { + $variableGroupName = $sourceVariableGroups | Where-Object { $_.id -eq $variableGroupId } | Select-Object -ExpandProperty name + $targetVariableGroupId = $targetVariableGroups | Where-Object { $_.name -eq $variableGroupName } | Select-Object -ExpandProperty id + $variableGroups += $targetVariableGroupId + } + $environment.variableGroups = $variableGroups + } + } + forEach ($artifact in $releaseDetail.artifacts) { + $artifact.sourceId = $artifact.sourceId.Replace($sourceProject.id, $targetProject.id).Replace($sourceProject.id, $targetProject.id) + if ($null -ne $artifact.definitionReference.artifactSourceDefinitionUrl) { + $artifact.definitionReference.artifactSourceDefinitionUrl.id = $artifact.definitionReference.artifactSourceDefinitionUrl.id.Replace($SourceOrgName, $TargetOrgName) + } + $artifact.definitionReference.project.id = $TargetProject.id + $artifact.definitionReference.project.name = $TargetProjectName + } + $releaseDetail.id = 0 + $releaseDetail.url = "" + $releaseDetail._links = "" + + # Set variable groups to target Ids + if ($releaseDetail.variableGroups) { + $vgs = @() + foreach ($vgId in $releaseDetail.variableGroups) { + $vg = $sourceVariableGroups | Where-Object { $_.id -eq $vgId } + if ($vg) { + $tvg = $targetVariableGroups | Where-Object { $_.name -eq $vg.name } + if ($tvg) { + $vgs += $tvg.id + } + } + } + $releaseDetail.variableGroups = $vgs + } + + try { + $newPipeline = New-ReleaseDefinition -ProjectName $targetProjectName -OrgName $targetOrgName -Headers $TargetHeaders -DefinitionDetail $releaseDetail + if ($null -ne $newPipeline) { + Write-Log "Created Release Pipeline $($release.name)" + $CreatedPipelinesCount += 1 + } + else { + Write-Log "Failed to create Release Pipeline $($release.name)" + $FailedPipelinesCount += 1 + } + } + catch { + Write-Log "Catch!" + Write-Log "Failed to create Release Pipeline $($release.name)" + $FailedPipelinesCount += 1 + Write-Log "$($_)" + } + } + Write-Log "Successfully migrated $CreatedPipelinesCount release pipeline(s)" + Write-Log "Failed to migrate $FailedPipelinesCount release pipeline(s)" + } +} + +function Get-ReleaseDetail { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://vsrm.dev.azure.com/$($OrgName)/$($ProjectName)/_apis/release/definitions" ` + + "/$($release.id)?api-version=7.1-preview.4" + + $result = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $result +} + +function Get-ReleaseDefinitions { + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + $url = "https://vsrm.dev.azure.com/$OrgName/$ProjectName/_apis/release/definitions?api-version=7.1" + $response = Invoke-RestMethod -Method GET -uri $url -Headers $Headers + return $response +} + +function Get-ReleaseDefinition { + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$DefinitionId + ) + $url = "https://vsrm.dev.azure.com/$OrgName/$ProjectName/_apis/release/definitions/$($release.id)?api-version=7.1-preview.4" + $response = Invoke-RestMethod -Method GET -uri $url -Headers $Headers + return $response +} + +function New-ReleaseDefinition { + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [Object]$DefinitionDetail + ) + $url = "https://vsrm.dev.azure.com/$($OrgName)/$($ProjectName)/_apis/release/definitions?api-version=7.1" + + $body = $DefinitionDetail | ConvertTo-Json -Depth 32 + $result = Invoke-WebRequest -Uri $url -Method POST -Header $Headers -Body $body -ContentType "application/json" + + return $result +} + +function Get-TargetServiceConnectionId { + param( + [Parameter (Mandatory = $TRUE)] + $SourceEndpoints, + + [Parameter (Mandatory = $TRUE)] + $TargetEndpoints, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceServiceConnectionId + ) + $serviceConnectionName = $SourceEndpoints | Where-Object { $_.id -eq $SourceServiceConnectionId } | Select-Object -ExpandProperty name + $targetServiceConnectionId = $TargetEndpoints | Where-Object { $_.name -eq $serviceConnectionName } | Select-Object -ExpandProperty id + return $targetServiceConnectionId +} + +function Move-DeploymentGroups { + param( + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders + ) + $sourceDeploymentGroups = Get-DeploymentGroups -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders + $targetDeploymentGroups = Get-DeploymentGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders + $targetDeploymentPools = Get-Pools -OrgName $TargetOrgName -Headers $TargetHeaders -PoolType "deployment" + + forEach ($sdg in $sourceDeploymentGroups.value) { + try { + $query = $targetDeploymentGroups.value | Where-Object { $_.name -eq $sdg.name } + + if ($query) { + Write-Log "Deployment group $($sdg.name) already exist." + continue + } + + Write-Log "Attempting to migrate deployment group $($sdg.name)" + + $deploymentPool = $targetDeploymentPools.value | Where-Object { $_.name -eq $sdg.pool.name } | Select-Object -First 1 + + if ($deploymentPool) { + $deploymentGroup = @{ + "description" = $sdg.description + "name" = $sdg.name + "poolId" = $deploymentPool.id + } + + $newDeploymentGroup = New-DeploymentGroup -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -Headers $TargetHeaders -DeploymentGroup $deploymentGroup + + if ($null -ne $newDeploymentGroup) { + + Write-Log "Deployment group $($deploymentGroup.name) migrated successfully." + } + else { + Write-Log "Deployment group $($deploymentGroup.name) failed to migrate." + } + } + else { + Write-Log "Unable to find Agent Pool $($sdg.pool.name)" + } + + + } + catch { + Write-Log "Catch!" + Write-Log "Failed to migrate deployment group $($deploymentGroup.name)" + Write-Log "$($_)" + } + } +} + +function Move-AgentPools { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders + ) + + $sourceAgentPools = Get-Pools -OrgName $SourceOrgName -Headers $SourceHeaders + $targetAgentPools = Get-Pools -OrgName $TargetOrgName -Headers $TargetHeaders + + foreach ($sap in $sourceAgentPools.value) { + $query = $targetAgentPools.value | Where-Object { $_.name -eq $sap.name } + + if ($query.count -eq 0) { + try { + $newAgentPool = New-Pool -OrgName $TargetOrgName -Headers $TargetHeaders -Pool $sap + + Move-Agents -SourceOrgName $SourceOrgName -SourceHeaders $SourceHeaders ` + -TargetOrgName $TargetOrgName -TargetHeaders $TargetHeaders -SourcePoolId $sap.id -TargetPoolId $newAgentPool.id + + Write-Log "Created Agent Pool $($newAgentPool.name)" + } + catch { + Write-Log "Catch!" + Write-Log "Failed to add Pool $($sap.name)" + Write-Log "$($_)" + } + } + } +} + +function Move-Agents { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders, + + [Parameter(Mandatory = $TRUE)] + [String] + $SourcePoolId, + + [Parameter(Mandatory = $TRUE)] + [String] + $TargetPoolId + ) + + $agents = Get-Agents -OrgName $SourceOrgName -Headers $SourceHeaders -PoolId $SourcePoolId + + foreach ($a in $agents.value) { + try { + New-Agent -OrgName $TargetOrgName -Headers $TargetHeaders -PoolId $TargetPoolId -Agent $a + + Write-Log "Created Agent $($a.name)" + } + catch { + Write-Log "Catch!" + Write-Log "Failed to add Pool $($a.name)" + Write-Log "$($_)" + } + } +} + +function New-Agent { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter(Mandatory = $TRUE)] + [String] + $PoolId, + + [Parameter(Mandatory = $TRUE)] + [Object] + $Agent + ) + + $url = "https://dev.azure.com/$($OrgName)/_apis/distributedtask/pools/$($PoolId)/agents?api-version=7.1" + + $body = $Agent | ConvertTo-Json -Depth 32 + + $result = Invoke-RestMethod -Method Post -Uri $url -Body $body ` + -ContentType "application/json" -Headers $Headers + + return $result +} + +function Get-Agents { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter(Mandatory = $TRUE)] + [String] + $PoolId + ) + + $url = "https://dev.azure.com/$($OrgName)/_apis/distributedtask/pools/$($PoolId)/agents?api-version=7.1" + + $resuls = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $resuls +} + +function Move-DeploymentPools { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders + ) + + $sourceDeploymentPools = Get-Pools -OrgName $SourceOrgName -Headers $SourceHeaders -PoolType "deployment" + $targetDeploymentPools = Get-Pools -OrgName $TargetOrgName -Headers $TargetHeaders -PoolType "deployment" + + foreach ($sdp in $sourceDeploymentPools.value) { + $query = $targetDeploymentPools.value | Where-Object { $_.name -eq $sdp.name } + + if ($query.count -eq 0) { + try { + $newDeploymentPool = New-Pool -OrgName $TargetOrgName -Headers $TargetHeaders -Pool $sdp + + Write-Log "Created Deployment Pool $($newDeploymentPool.name)" + } + catch { + Write-Log "Catch!" + Write-Log "Failed to add Pool $($sdp.name)" + Write-Log "$($_)" + } + } + } +} + +function New-Pool { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter(Mandatory = $TRUE)] + $Pool + ) + $url = "https://dev.azure.com/$OrgName/_apis/distributedtask/pools?api-version=7.2-preview.1" + + $body = $Pool | ConvertTo-Json -Depth 32 + + $result = Invoke-RestMethod -Method Post -Uri $url -Headers $Headers ` + -Body $body -ContentType "application/json" + + return $result +} + +function Get-Pools { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [String]$PoolType + ) + + $url = "https://dev.azure.com/$OrgName/_apis/distributedtask/pools?api-version=7.2-preview.1" + + if ($PoolType) { + $url = $url + "&poolType=$PoolType" } + + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results } +function Remove-AllPools { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $agentPools = Get-Pools -OrgName $OrgName -Headers $Headers + + foreach ($ap in $agentPools.value) { + try { + if ($false -eq $ap.owner.displayName.StartsWith("Microsoft")) { + + Remove-Pool -OrgName $OrgName -PoolId $ap.id -Headers $Headers + + Write-Log "Pool $($ap.name) removed." + } + } + catch { + Write-Log "Catch!" + Write-Log "Failed to remove Pool $($ap.name)" + Write-Log "$($_)" + } + } +} + +function Remove-Pool { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$PoolId, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://dev.azure.com/$OrgName/_apis/distributedtask/pools/$($PoolId)?api-version=7.2-preview.1" + + $result = Invoke-RestMethod -Method Delete -Uri $url -Headers $Headers + + return $result +} + +function Remove-AllReleaseDefinitions { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://vsrm.dev.azure.com/$($OrgName)/$($ProjectName)/_apis/release/definitions?api-version=7.2-preview.4" + + $releaseDefinitions = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + foreach ($rd in $releaseDefinitions.value) { + try { + Remove-ReleaseDefinition -OrgName $OrgName -ProjectName $ProjectName -DefinitionId $rd.id -Headers $Headers + + Write-Log "Release definition $($rd.name) removed." + } + catch { + Write-Log "Catch!" + Write-Log "Failed to remove release definition $($rd.name)" + Write-Log "$($_)" + } + } +} + +function Remove-ReleaseDefinition { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$DefinitionId, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://vsrm.dev.azure.com/$($OrgName)/$($ProjectName)/_apis/release/definitions" ` + + "/$($DefinitionId)?api-version=7.2-preview.4" + + $result = Invoke-RestMethod -Method Delete -Uri $url -Headers $Headers + + return $result; +} +function Get-DeploymentGroups { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/distributedtask/deploymentgroups?api-version=7.2-preview.1" + + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results; +} + +function New-DeploymentGroup { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + $DeploymentGroup, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/distributedtask/deploymentgroups?api-version=7.2-preview.1" + + $body = $DeploymentGroup | ConvertTo-Json -Depth 32 + + $results = Invoke-RestMethod -Method Post -ContentType "application/json" -Uri $url -Headers $Headers -Body $body + + return $results; +} + + + + diff --git a/modules/Migrate-ADO-Repos.psm1 b/modules/Migrate-ADO-Repos.psm1 index e65ce1a..cb6d8e2 100644 --- a/modules/Migrate-ADO-Repos.psm1 +++ b/modules/Migrate-ADO-Repos.psm1 @@ -2,6 +2,298 @@ Using Module ".\Migrate-ADO-Common.psm1" function Start-ADORepoMigration { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourcePAT, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetPAT, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders, + + [Parameter (Mandatory = $FALSE)] + [Object[]]$RepoIds = $() + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate repos from source project $SourceOrg/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '-------------------' + Write-Log -Message '-- Migrate Repos --' + Write-Log -Message '-------------------' + Write-Log -Message ' ' + + try { + $targetProject = Get-ADOProjects -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -Headers $TargetHeaders + + $sourceRepos = Get-Repos -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders + Write-Log -Message "Source repository Count $($sourceRepos.Count).." + $targetRepos = Get-Repos -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $TargetHeaders + Write-Log -Message "Target repository Count $($targetRepos.Count).." + + $repos + if ($RepoIds.Count -gt 0) { + $repos = $sourceRepos | Where-Object { $_.Id -in $RepoIds } + Write-Log -Message "Repo Ids passed in Count $($repos.Count).." + } + else { + $repos = $sourceRepos + } + + if ($repos.Count -gt 0) { + $count = 1 + + foreach ($sourceRepo in $repos ) { + Write-Log -Message "Migrating repo $($sourceRepo.Name), $($count) of $($sourceRepos.count)" + $count += 1 + + $targetRepo = $targetRepos | Where-Object { $_.name -ieq $sourceRepo.name } + if ($null -ne $targetRepo) { + Write-Log -Message "Repo [$($sourceRepo.name)] already exists in target.. " + continue + } + if ($sourceRepo.isDisabled -eq $true) { + Write-Log -Message "Unable to migrate [$($sourceRepo.name)] it is disabled." + continue + } + + try { + $newRepo = New-GitRepository -ProjectName $TargetProjectName -OrgName $TargetOrgName ` + -RepoName $sourceRepo.name -Headers $TargetHeaders + + $gitService = @{ + "name" = "$($sourceRepo.name)-migrate-endpoint" + "type" = "git" + "url" = $sourceRepo.remoteUrl + "authorization" = @{ + "scheme" = "UsernamePassword" + "parameters" = @{ + "username" = "john.leach1" + "password" = $SourcePAT + } + } + } + + $sep = New-ServiceEndpoint -OrgName $TargetOrgName -ProjectName $targetProject.id ` + -Headers $TargetHeaders -ServiceEndpoint $gitService + + Write-Log -Message "Starting import of repo: $($newRepo.name)." + + $request = New-RepositoryImportRequest -OrgName $TargetOrgName -ProjectName ` + $TargetProjectName -RepositoryName $sourceRepo.name -Headers $TargetHeaders ` + -GitSourceUri $sourceRepo.remoteUrl -ServiceEndpointId $sep.id + + $status + $timeout = 960 + $isRunning = $true + + for ($index = 0; $index -lt $timeout -AND $isRunning; $index++) { + $status = Get-RepositoryImportRequest -OrgName $TargetOrgName -ProjectName ` + $TargetProjectName -Headers $TargetHeaders -RepositoryId $newRepo.id ` + -ImportRequestId $request.importRequestId + + Write-Log -Message "-- Import status: $($status.status)" + + if ($status.status -eq "failed") { + throw "Import of $($newRepo.name) failed" + } + + $isRunning = $status.status -ne "completed" + + if ($isRunning) { + Start-Sleep -Seconds 4 + } + } + + Set-RepositoryDefaultBranch -ProjectName $TargetProjectName -OrgName $TargetOrgName ` + -RepositoryId $newRepo.id -Headers $TargetHeaders -defaultBranch $sourceRepo.defaultBranch + + Write-Log -Message "Migration of repo: $($newRepo.name) complete." + + } + catch { + Write-Log -Message "Error migrating repo: $($sourceRepo.name) " -LogLevel ERROR + Write-Log -Message 'Repository cannot be migrated, please migrate manually... ' + Write-Log -Message $_ -LogLevel ERROR + continue + } + } + } + } + catch { + Write-Log -Message "Fatal-Error migrating repos from org $SourceOrgName and project $SourceProjectName" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + return + } + } +} + +function Get-RepositoryRefs { + param ( + [Parameter(Mandatory = $TRUE)] + [string] $OrgName, + + [Parameter(Mandatory = $TRUE)] + [string] $ProjectName, + + [Parameter(Mandatory = $TRUE)] + [hashtable] $Headers, + + [Parameter(Mandatory = $TRUE)] + [string] $RepositoryId + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/git/repositories" ` + + "/$($RepositoryId)/refs?api-version=7.1" + + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results +} + +function Get-RepositoryImportRequest { + param ( + [Parameter(Mandatory = $TRUE)] + [string] $OrgName, + + [Parameter(Mandatory = $TRUE)] + [string] $ProjectName, + + [Parameter(Mandatory = $TRUE)] + [hashtable] $Headers, + + [Parameter(Mandatory = $TRUE)] + [string] $RepositoryId, + + [Parameter(Mandatory = $TRUE)] + [string] $ImportRequestId + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/git/repositories/" ` + + "$($RepositoryId)/importRequests/$($ImportRequestId)?api-version=7.1" + + $result = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $result +} + +function New-RepositoryImportRequest { + param ( + [Parameter(Mandatory = $TRUE)] + [string] $OrgName, + + [Parameter(Mandatory = $TRUE)] + [string] $ProjectName, + + [Parameter(Mandatory = $TRUE)] + [string] $RepositoryName, + + [Parameter(Mandatory = $TRUE)] + [hashtable] $Headers, + + [Parameter(Mandatory = $TRUE)] + [string] + $GitSourceUri, + + [Parameter(Mandatory = $TRUE)] + [string] + $ServiceEndpointId + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/git/repositories/$($RepositoryName)" ` + + "/importRequests?api-version=7.1" + + $body = ConvertTo-Json -Depth 32 @{ + "parameters" = @{ + "deleteServiceEndpointAfterImportIsDone" = $true + "gitSource" = @{ + "overwrite" = $false + "url" = $GitSourceUri + } + "serviceEndpointId" = $ServiceEndpointId + } + } + + $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers $Headers -ContentType ` + "application/json" + + return $result +} + +function Set-RepositoryDefaultBranch { + param ( + [Parameter(Mandatory = $TRUE)] + [string] $OrgName, + + [Parameter(Mandatory = $TRUE)] + [string] $ProjectName, + + [Parameter(Mandatory = $TRUE)] + [string] $RepositoryId, + + [Parameter(Mandatory = $TRUE)] + [hashtable] $Headers, + + [Parameter(Mandatory = $TRUE)] + [string] $defaultBranch + ) + + Write-Log -Message "Setting default branch to $($sourceRepo.defaultBranch)." + + $branches + $retries = 4 + $isRunning = $true + + for ($index = 0; $index -lt $retries -AND $isRunning; $index++) { + $branches = Get-RepositoryRefs -OrgName $OrgName -ProjectName $ProjectName ` + -Headers $Headers -RepositoryId $RepositoryId + + $isRunning = $branches.Count -eq 0 + + if ($isRunning) { + Start-Sleep -Seconds 4 + } + } + + $query = $branches.value | Where-Object { $_.name -eq $defaultBranch } + + if ($query) { + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/git/repositories/$($RepositoryId)" ` + + "?api-version=7.1" + + $body = ConvertTo-Json -Depth 32 @{ + "defaultBranch" = $defaultBranch + } + + Invoke-RestMethod -Method Patch -Uri $url -ContentType "application/json" ` + -Body $body -Headers $Headers + } + else { + Write-Log -Message "WARNING: Unable to find branch $($sourceRepo.defaultBranch)." + } +} + +function Start-ADORepoMigration-Old { [CmdletBinding(SupportsShouldProcess)] param( [Parameter (Mandatory = $TRUE)] @@ -31,7 +323,7 @@ function Start-ADORepoMigration { [Parameter (Mandatory = $TRUE)] [String]$ReposPath, - [Parameter (Mandatory=$FALSE)] + [Parameter (Mandatory = $FALSE)] [Object[]]$RepoIds = $() ) if ($PSCmdlet.ShouldProcess( @@ -57,18 +349,20 @@ function Start-ADORepoMigration { if ($RepoIds.Count -gt 0) { $repos = $sourceRepos | Where-Object { $_.Id -in $RepoIds } Write-Log -Message "Repo Ids passed in Count $($repos.Count).." - } else { + } + else { $repos = $sourceRepos } - if($repos.Count -gt 0) { + if ($repos.Count -gt 0) { # First clean out the temp repo directory $tempPath = "$ReposPath\temp" if (-not (Test-Path -Path $tempPath)) { New-Item -Path $tempPath -ItemType Directory - } else { + } + else { Get-ChildItem -Path $tempPath | Remove-Item -Recurse -Force } @@ -100,7 +394,7 @@ function Start-ADORepoMigration { try { Write-Log -Message "Cloning repository $($sourceRepo.name)" - $remoteUrl = $sourceRepo.remoteURL.Replace("@",":$SourcePAT@") + $remoteUrl = $sourceRepo.remoteURL.Replace("@", ":$SourcePAT@") git clone --mirror $remoteUrl "$tempPath\$($sourceRepo.name)" Write-Log -Message "Entering path `"$tempPath\$($sourceRepo.name)`"" diff --git a/modules/Migrate-ADO-Retention.psm1 b/modules/Migrate-ADO-Retention.psm1 new file mode 100644 index 0000000..533b6e6 --- /dev/null +++ b/modules/Migrate-ADO-Retention.psm1 @@ -0,0 +1,85 @@ +function Start-ADORetentionMigration { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] [String]$SourceOrgName, + [Parameter (Mandatory = $TRUE)] [String]$SourceProjectName, + [Parameter (Mandatory = $TRUE)] [Hashtable]$SourceHeaders, + [Parameter (Mandatory = $TRUE)] [String]$TargetOrgName, + [Parameter (Mandatory = $TRUE)] [String]$TargetProjectName, + [Parameter (Mandatory = $TRUE)] [Hashtable]$TargetHeaders + + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate Retension from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '---------------------------------------------' + Write-Log -Message '-- Migrate Retension --' + Write-Log -Message '---------------------------------------------' + Write-Log -Message ' ' + + try { + $sourceRetention = Get-Retention -OrgName $SourceOrgName -ProjectName ` + $SourceProjectName -Headers $SourceHeaders + + if ($sourceRetention) { + Update-Retention -OrgName $TargetOrgName -ProjectName $TargetProjectName ` + -Headers $TargetHeaders -Retention $sourceRetention + + Write-Log -Message "Retention Policy update for project $TargetProjectName" + } + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message " " + } + } +} + +function Get-Retention { + param ( + [Parameter(Mandatory = $TRUE)] + [string]$OrgName, + [Parameter(Mandatory = $TRUE)] + [string]$ProjectName, + [Parameter(Mandatory = $TRUE)] + [hashtable]$Headers + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/build/retention?api-version=7.1" + + $results = Invoke-RestMethod -Method Get -Uri $url -Headers $Headers + + return $results +} + +function Update-Retention { + param ( + [Parameter(Mandatory = $TRUE)] + [string]$OrgName, + [Parameter(Mandatory = $TRUE)] + [string]$ProjectName, + [Parameter(Mandatory = $TRUE)] + [hashtable]$Headers, + [Parameter(Mandatory = $TRUE)] + $Retention + ) + + $url = "https://dev.azure.com/$($OrgName)/$($ProjectName)/_apis/build/retention?api-version=7.1" + + $wat = @{ + "artifactsRetention" = $Retention.purgeArtifacts + "pullRequestRunRetention" = $Retention.purgePullRequestRuns + "retainRunsPerProtectedBranch" = $Retention.retainRunsPerProtectedBranch + "runRetention" = $Retention.purgeRuns + } + $body = $wat | ConvertTo-Json -Depth 32 + + $results = Invoke-RestMethod -Method Patch -Uri $url -Headers $Headers ` + -Body $body -ContentType "application/json" + + return $results +} \ No newline at end of file diff --git a/modules/Migrate-ADO-ServiceConnections.psm1 b/modules/Migrate-ADO-ServiceConnections.psm1 index 10e7a90..a04e747 100644 --- a/modules/Migrate-ADO-ServiceConnections.psm1 +++ b/modules/Migrate-ADO-ServiceConnections.psm1 @@ -20,22 +20,22 @@ function Start-ADOServiceConnectionsMigration { Write-Log -Message '---------------------------------------------' Write-Log -Message ' ' - # $sourceProject = Get-ADOProjects -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders + $sourceProject = Get-ADOProjects -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders $targetProject = Get-ADOProjects -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders - $sourceEndpoints = Get-ServiceEndpoints -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $sourceHeaders - $targetEndpoints = Get-ServiceEndpoints -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $sourceHeaders + $sourceEndpoints = Get-ServiceEndpoints -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders + $targetEndpoints = Get-ServiceEndpoints -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders #$sourceEndpoints | ConvertTo-Json -Depth 10 | Out-File -FilePath "DEBUG_endpoints.json" foreach ($endpoint in $sourceEndpoints) { - if ($null -ne ($targetEndpoints | Where-Object {$_.description.ToUpper().Contains("#ORIGINSERVICEENDPOINTID:$($endpoint.id.ToUpper())")})) { + if ($null -ne ($targetEndpoints | Where-Object { $_.description.ToUpper().Contains("#ORIGINSERVICEENDPOINTID:$($endpoint.id.ToUpper())") })) { Write-Log -Message "Service endpoint [$($endpoint.id)] already exists in target.. " continue } - if ($null -ne ($targetEndpoints | Where-Object {($_.name -eq $endpoint.name) -and ($_.type -eq $endpoint.type)})) { + if ($null -ne ($targetEndpoints | Where-Object { ($_.name -eq $endpoint.name) -and ($_.type -eq $endpoint.type) })) { Write-Log -Message "Service endpoint [$($endpoint.name)] [$($endpoint.id)] already exists in target.. " continue } @@ -43,89 +43,84 @@ function Start-ADOServiceConnectionsMigration { Write-Log -Message "Attempting to create [$($endpoint.name)] in target.. " $projectReference = @{ - "id" = $targetProject.id - "name" = $TargetProjectName + "id" = $targetProject.id + "name" = $TargetProjectName } $endpointProjectReference = @{ - "name" = $endpoint.name - "description" = "" - "projectReference" = $projectReference + "name" = $endpoint.name + "description" = "" + "projectReference" = $projectReference } $endpoint.serviceEndpointProjectReferences = @($endpointProjectReference) - - if($endpoint.data.creationMode -eq "Automatic") { - if($null -ne $endpoint.data.azureSpnRoleAssignmentId){ - $endpoint.data.azureSpnRoleAssignmentId = $null - } - $endpoint.data.azureSpnPermissions = $null - $endpoint.data.spnObjectId = $null - $endpoint.data.appObjectId = $null - $endpoint.authorization.parameters.serviceprincipalid = $NULL - if($NULL -ne $endpoint.authorization.parameters.authenticationType) { - $endpoint.authorization.parameters.authenticationType = $NULL - } - } # provide default values for specified endpoint types - if ($endpoint.type -eq "github") { - $parameters = @{ - "accesstoken" = "0123456789" - } - $endpoint.authorization | Add-Member -NotePropertyName parameters -NotePropertyValue $parameters - } elseif ($endpoint.type -eq "azurerm") { + if ($endpoint.type -eq "azurerm") { # Azurerm Service Connection types will need to be edited after migration to adhere to org/project naming conventions. - if($endpoint.data.creationMode -eq "Automatic") { - if($null -ne $endpoint.data.azureSpnRoleAssignmentId){ + if ($endpoint.authorization.scheme -eq "WorkloadIdentityFederation" -OR $endpoint.authorization.scheme -eq "ServicePrincipal") { + if ($endpoint.authorization.parameters.tenantId) { + Set-Parameters -Authorization $endpoint.authorization -MemberName ` + "tenantid" -MemberValue $endpoint.authorization.parameters.tenantId + } + if ($endpoint.authorization.parameters.serviceprincipalId) { + Set-Parameters -Authorization $endpoint.authorization -MemberName ` + "serviceprincipalId" -MemberValue $endpoint.authorization.parameters.serviceprincipalId + } + + # $endpoint.authorization.scheme = "WorkloadIdentityFederation" + } + elseif ($endpoint.authorization.scheme -eq "PublishProfile") { + Set-Parameters -Authorization $endpoint.authorization -MemberName "tenantid" -MemberValue $endpoint.authorization.tenantId + Set-Parameters -Authorization $endpoint.authorization -MemberName "resourceId" -MemberValue $endpoint.authorization.resourceId + } + if ($endpoint.data.creationMode -eq "Automatic") { + if ($null -ne $endpoint.data.azureSpnRoleAssignmentId) { $endpoint.data.azureSpnRoleAssignmentId = $null } $endpoint.data.azureSpnPermissions = $null $endpoint.data.spnObjectId = $null $endpoint.data.appObjectId = $null $endpoint.authorization.parameters.serviceprincipalid = $NULL - if($NULL -ne $endpoint.authorization.parameters.authenticationType) { - $endpoint.authorization.parameters.authenticationType = $NULL - } - } elseif($endpoint.data.creationMode -eq "Manual") { + + } + elseif ($endpoint.data.creationMode -eq "Manual") { Write-Log -Message "Service endpoints of type `"azurerm`" with a creationMode of `"Manual`" cannot be migrated as is .. " Write-Log -Message "setting the creationMode to `"Automatic`", this will need to be updated manually after migration.. " $endpoint.data.creationMode = "Automatic" $endpoint.authorization.parameters.serviceprincipalid = $NULL - if($NULL -ne $endpoint.authorization.parameters.authenticationType) { - $endpoint.authorization.parameters.authenticationType = $NULL - } - } - - } elseif ($endpoint.type -eq "externaltfs") { - $parameters = @{ - "apitoken" = "0123456789" - } - $endpoint.authorization | Add-Member -NotePropertyName parameters -NotePropertyValue $parameters - } elseif ($endpoint.type -eq "stormrunner") { - $endpoint.authorization.parameters.username = "abcdefghij" - $endpoint.authorization.parameters | Add-Member -NotePropertyName password -NotePropertyValue "0123456789" - } elseif ($endpoint.type -eq "OctopusEndpoint") { - $parameters = @{ - "apitoken" = "0123456789" - } - $endpoint.authorization | Add-Member -NotePropertyName parameters -NotePropertyValue $parameters - } elseif ($endpoint.type -eq "sonarqube") { - $parameters = @{ - "username" = "abcdefghij" + } + if ($NULL -ne $endpoint.authorization.parameters.authenticationType) { + $endpoint.authorization.parameters.PSObject.Properties.Remove("authenticationType") } - $endpoint.authorization | Add-Member -NotePropertyName parameters -NotePropertyValue $parameters + } + elseif ($endpoint.authorization.scheme -eq "Token") { + Set-Parameters -Authorization $endpoint.authorization -MemberName "apitoken" -MemberValue "Update-Please" + } + elseif ($endpoint.authorization.scheme -eq "OAuth") { + Set-Parameters -Authorization $endpoint.authorization -MemberName "accessToken" -MemberValue "Update-Please" + } + elseif ($endpoint.authorization.scheme -eq "InstallationToken") { + Set-Parameters -Authorization $endpoint.authorization -MemberName "IdSignature" -MemberValue $null + Set-Parameters -Authorization $endpoint.authorization -MemberName "IdToken" -MemberValue $null + } + elseif ($endpoint.authorization.scheme -eq "UsernamePassword") { + Set-Parameters -Authorization $endpoint.authorization -MemberName "username" -MemberValue "Update-Please" + Set-Parameters -Authorization $endpoint.authorization -MemberName "password" -MemberValue "Update-Please" } try { - New-ServiceEndpoint -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $targetHeaders -ServiceEndpoint $endpoint + $newEndpoint = New-ServiceEndpoint -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $targetHeaders -ServiceEndpoint $endpoint Write-Log -Message "Done!" -LogLevel SUCCESS + + Start-ADOServiceConnectionRolesMigration -SourceProjectId $sourceProject.id ` + -TargetProjectId $targetProject.id -SourceEndpointId $endpoint.id -TargetEndpointId $newEndpoint.id + } catch { Write-Log -Message "FAILED!" -LogLevel ERROR Write-Log -Message $_.Exception -LogLevel ERROR - Write-Log -Message ($_ | ConvertFrom-Json -Depth 10) -LogLevel ERROR Write-Log -Message $_ -LogLevel ERROR Write-Log -Message " " } @@ -133,10 +128,42 @@ function Start-ADOServiceConnectionsMigration { } } +function Set-Parameters { + param ( + [Parameter(Mandatory = $TRUE)] + $Authorization, + + [Parameter(Mandatory = $TRUE)] + [String] + $MemberName, + + [Parameter(Mandatory = $TRUE)] + [AllowNull()] + $MemberValue + ) + + if ($Authorization.parameters) { + if ($Authorization.parameters | Get-Member -Name $MemberName) { + $Authorization.parameters.$MemberName = $MemberValue + } + else { + $Authorization.parameters | Add-Member -NotePropertyName $MemberName -NotePropertyValue $MemberValue + } + } + else { + $parameters = [PSCustomObject]@{ + $MemberName = $MemberValue + } + + $Authorization | Add-Member -NotePropertyName parameters -NotePropertyValue $parameters + } +} + # Get ALl Service Connection Endpoints function Get-ServiceEndpoints([string]$OrgName, [string]$ProjectName, $Headers) { - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints?includeFailed=true&includeDetails=true&api-version=7.0" + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints?" ` + + "includeFailed=true&includeDetails=true&actionFilter=manage&api-version=7.0" $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers @@ -146,14 +173,140 @@ function Get-ServiceEndpoints([string]$OrgName, [string]$ProjectName, $Headers) # Create NEW Service Connection Endpoint function New-ServiceEndpoint([string]$OrgName, [string]$ProjectName, $Headers, $ServiceEndpoint) { - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints?api-version=7.0" + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/serviceendpoint/endpoints?api-version=5.0-preview.2" $body = $ServiceEndpoint | ConvertTo-Json -Depth 32 $results = Invoke-RestMethod -ContentType "application/json" -Method Post -uri $url -Headers $Headers -Body $body return $results +} + +function Start-ADOServiceConnectionRolesMigration { + param( + [Parameter(Mandatory = $TRUE)] + [string] + $SourceProjectId, + [Parameter(Mandatory = $TRUE)] + [string] + $TargetProjectId, + [Parameter(Mandatory = $TRUE)] + [string] + $SourceEndpointId, + [Parameter(Mandatory = $TRUE)] + [string] + $TargetEndpointId + ) + $sourceRoleAssignments = Get-RoleAssignments -OrgName $SourceOrgName -ProjectId $SourceProjectId ` + -EndpointId $SourceEndpointId -Headers $SourceHeaders + $targetRoleAssignments = Get-RoleAssignments -OrgName $TargetOrgName -ProjectId $TargetProjectId ` + -EndpointId $TargetEndpointId -Headers $TargetHeaders + foreach ($roleAssignment in $sourceRoleAssignments) { + $roleName = $roleAssignment.identity.displayName.Replace($SourceProjectName, $TargetProjectName) + + $query = $targetRoleAssignments | Where-Object { $_.name -eq $roleName } + + if ($query) { + Write-Log -Message "Service Connection Role: $($roleName) already exists." + continue + } + + try { + Write-Log -Message "Attempting to create role assignment [$($roleName)] in target.. " + + $identities = Get-IdentitiesByName -OrgName $TargetOrgName -Headers $TargetHeaders -DisplayName $roleName + + if ($identities.Count -eq 1) { + $role = @{ + "roleName" = $roleAssignment.role.name + "userId" = $identities.Value[0].id + } + New-RoleAssignment -OrgName $TargetOrgName -IdentityId $role["userId"] -ProjectId $TargetProjectId ` + -EndpointId $TargetEndpointId -Role $role -Headers $TargetHeaders + Write-Log -Message "Done!" -LogLevel SUCCESS + } + else { + Write-Log -Message "Unable to find role $roleName, please add it manually" -LogLevel WARNING + } + + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log -Message $_ -LogLevel ERROR + Write-Log -Message " " + } + } } +function Get-RoleAssignments([string]$OrgName, [string] $ProjectId, [string] $EndpointId, $Headers) { + $url = "https://dev.azure.com/$OrgName/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roleassignments/resources/{0}_{1}" -f $ProjectId, $EndpointId + + $results = Invoke-RestMethod -ContentType "application/json" -Method Get -uri $url -Headers $Headers + + return , $results.value +} + +function Get-RoleDefinitions { + param ( + [Parameter(Mandatory = $TRUE)] + [string] + $OrgName, + [Parameter(Mandatory = $TRUE)] + [string] + $Headers + ) + $url = "https://dev.azure.com/$OrgName/_apis/securityroles/scopes/distributedtask.serviceendpointrole/roledefinitions?api-version=7.2-preview.1" + $results = Invoke-RestMethod -ContentType "application/json" -Method Get -uri $url -Headers $Headers + + return , $results.value +} + +function New-RoleAssignment([string]$OrgName, [string] $IdentityId, [string] $EndpointId, [String] $ProjectId, $Role, $Headers) { + $url = "https://dev.azure.com/$OrgName/_apis/securityroles/scopes/distributedtask.serviceendpointrole" ` + + "/roleassignments/resources/$($ProjectId)_$($EndpointId)?api-version=5.0-preview.1" + + $body = ConvertTo-Json -Depth 32 @($Role) + + $result = Invoke-RestMethod -ContentType "application/json" -Method Put -uri $url -Headers $Headers -Body $body + + return $result +} + +function Remove-AllServiceConnections { + param ( + [Parameter(Mandatory = $TRUE)] + [string] + $OrgName, + [Parameter(Mandatory = $TRUE)] + [string] + $ProjectName, + [Parameter(Mandatory = $TRUE)] + [string] + $ProjectId, + [Parameter(Mandatory = $TRUE)] + $Headers + ) + + $endpoints = Get-ServiceEndpoints -OrgName $OrgName ` + -ProjectName $ProjectName -Headers $TargetHeaders + + foreach ($ep in $endpoints) { + try { + $url = "https://dev.azure.com/$OrgName/_apis/serviceendpoint" ` + + "/endpoints/$($ep.id)?projectIds=$($ProjectId)" ` + + "&api-version=7.2-preview.4" + + Invoke-RestMethod -Uri $url -Method Delete ` + -Headers $Headers + + Write-Log "Service Connection $($ep.id) removed." + } + catch { + Write-Log "Unable to remove service endpoint $($ep.id)" + Write-Log "$($_)" + } + } +} diff --git a/modules/Migrate-ADO-ServiceHooks.psm1 b/modules/Migrate-ADO-ServiceHooks.psm1 index 9cd9d03..a123944 100644 --- a/modules/Migrate-ADO-ServiceHooks.psm1 +++ b/modules/Migrate-ADO-ServiceHooks.psm1 @@ -28,8 +28,8 @@ function Start-ADOServiceHooksMigration { $SourceTeams = Get-ADOProjectTeams -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName $TargetTeams = Get-ADOProjectTeams -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName - $SourcePipelines = Get-Pipelines -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName - $TargetPipelines = Get-Pipelines -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName + $SourcePipelines = Get-BuildDefinitions -Headers $SourceHeaders -OrgName $SourceOrgName -ProjectName $SourceProjectName + $TargetPipelines = Get-BuildDefinitions -Headers $TargetHeaders -OrgName $TargetOrgName -ProjectName $TargetProjectName $targetRepos = Get-Repos -projectName $targetProject.name -headers $targetHeaders -org $TargetOrgName @@ -68,7 +68,7 @@ function Start-ADOServiceHooksMigration { if ($null -ne $hook.publisherInputs) { $hook.publisherInputs.projectId = $targetProjectOrg.id - if($null -ne $hook.publisherInputs.repository) { + if ($null -ne $hook.publisherInputs.repository) { $sourceRepo = Get-Repo -headers $sourceHeaders -org $SourceOrgName -repoId $hook.publisherInputs.repository #Try to map to target repo - Note this will have issues if target repo is in a different project and either that project's repos have not been migrated @@ -127,9 +127,9 @@ function Start-ADOServiceHooksMigration { if ($hook.consumerId -eq "teams") { # Take source team ID, get Team name, lookup team in target by name to get id to set subscriberId foreach ($s_team in $SourceTeams) { - if($s_team.id -eq $hook.publisherInputs.subscriberId) { + if ($s_team.id -eq $hook.publisherInputs.subscriberId) { foreach ($t_team in $TargetTeams) { - if($t_team.name -eq $s_team.name){ + if ($t_team.name -eq $s_team.name) { $hook.publisherInputs.subscriberId = $t_team.id break } @@ -141,9 +141,9 @@ function Start-ADOServiceHooksMigration { if (($hook.consumerId -eq "workplaceMessagingApps") -AND ($hook.publisherId -eq "pipelines")) { # Take source team ID, get Team name, lookup team in target by name to get id to set subscriberId foreach ($s_pipeline in $SourcePipelines) { - if($s_pipeline.id -eq $hook.publisherInputs.pipelineId) { + if ($s_pipeline.id -eq $hook.publisherInputs.pipelineId) { foreach ($t_pipeline in $TargetPipelines) { - if($t_pipeline.name -eq $s_pipeline.name){ + if ($t_pipeline.name -eq $s_pipeline.name) { $hook.publisherInputs.pipelineId = $t_pipeline.id break } @@ -172,7 +172,8 @@ function Start-ADOServiceHooksMigration { Write-Log -Message $_.Exception -LogLevel ERROR try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR - } catch {} + } + catch {} } } } @@ -208,7 +209,7 @@ function New-ServiceHook([string]$projectName, [string]$orgName, $serviceHook, $ $url = "https://dev.azure.com/$orgName/_apis/hooks/subscriptions?api-version=7.0" # Service Hook subscriptions for Release events require the vsrm sub-domain endpoint - if($serviceHook.publisherId -eq "rm"){ + if ($serviceHook.publisherId -eq "rm") { $url = "https://vsrm.dev.azure.com/$orgName/_apis/hooks/subscriptions?api-version=7.0" } @@ -218,3 +219,49 @@ function New-ServiceHook([string]$projectName, [string]$orgName, $serviceHook, $ return $results } + +function Remove-AllServiceHooks { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + $project = Get-ADOProjects -OrgName $OrgName -ProjectName $ProjectName -Headers $Headers + + $serviceHooks = Get-ServiceHooks -orgName $OrgName -headers $Headers -projectId $project.id + + foreach ($sh in $serviceHooks) { + try { + + } + catch { + + } + } +} + +function Remove-ServiceHook { + param ( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + + $url = "" + + $result = Invoke-RestMethod -Method Delete -Uri $url + + return $result +} + + diff --git a/modules/Migrate-ADO-VariableGroups.psm1 b/modules/Migrate-ADO-VariableGroups.psm1 index e0ecc4f..d608758 100644 --- a/modules/Migrate-ADO-VariableGroups.psm1 +++ b/modules/Migrate-ADO-VariableGroups.psm1 @@ -8,7 +8,9 @@ function Start-ADOVariableGroupsMigration { [Parameter (Mandatory = $TRUE)] [String]$TargetOrgName, [Parameter (Mandatory = $TRUE)] [String]$TargetProjectName, [Parameter (Mandatory = $TRUE)] [Hashtable]$TargetHeaders, - [Parameter (Mandatory = $FALSE)] [String]$secretsMapPath = "" + [Parameter (Mandatory = $FALSE)] [String]$secretsMapPath = "", + [Parameter (Mandatory = $FALSE)] [Boolean]$migrateKeyVaultConnectedOnly = $false + ) if ($PSCmdlet.ShouldProcess( "Target project $TargetOrgName/$TargetProjectName", @@ -41,7 +43,9 @@ function Start-ADOVariableGroupsMigration { } $groups = Get-VariableGroups -projectName $sourceProject.name -orgName $SourceOrgName -headers $sourceHeaders - + if($migrateKeyVaultConnectedOnly) { + $groups = $groups | Where-Object {$_.type -eq "AzureKeyVault"} + } foreach ($groupHeader in $groups) { if ($null -ne ($targetVariableGroups | Where-Object {$_.name -ieq $groupHeader.name})) { @@ -55,19 +59,19 @@ function Start-ADOVariableGroupsMigration { $groupObj = (Get-VariableGroup -projectName $sourceProject.name -orgName $SourceOrgName -headers $sourceHeaders -groupId $groupHeader.id) $group = $groupObj | ConvertTo-Hashtable - if ($null -ne $secretsMap.variableGroups -and $null -ne $secretsMap.variableGroups[$group.name]) { - foreach ($key in $secretsMap.variableGroups[$group.name].Keys) { - if ($null -ne $group.variables[$key]) { - $group.variables[$key].value = $secretsMap.variableGroups[$group.name][$key] - } - } - } + # if ($null -ne $secretsMap.variableGroups -and $null -ne $secretsMap.variableGroups[$group.name]) { + # foreach ($key in $secretsMap.variableGroups[$group.name].Keys) { + # if ($null -ne $group.variables[$key]) { + # $group.variables[$key].value = $secretsMap.variableGroups[$group.name][$key] + # } + # } + # } - foreach ($key in $group.variables.Keys) { - if ($null -eq $group.variables[$key].value) { - throw "Missing secrets mapped variable '$($varProp.Name)' in variable group '$($group.name)'" - } - } + # foreach ($key in $group.variables.Keys) { + # if ($null -eq $group.variables[$key].value) { + # throw "Missing secrets mapped variable '$($varProp.Name)' in variable group '$($group.name)'" + # } + # } foreach ($ref in $groupObj.variableGroupProjectReferences) { $ref.name = $group.name @@ -76,10 +80,26 @@ function Start-ADOVariableGroupsMigration { $ref.projectReference.name = $targetProject.name } + $providerData = $group.providerData + if($group.providerData -ne $Null) { + $sourceEndpoints = Get-ServiceEndpoints -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $sourceHeaders + $targetEndpoints = Get-ServiceEndpoints -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $targetHeaders + + $sourceEndpoint = $sourceEndpoints | Where-Object {$_.id -eq $group.providerData.serviceEndpointId} + $targetEndpoint = $targetEndpoints | Where-Object {$_.name -eq $sourceEndpoint.name } + if($targetEndpoint -eq $null -OR $targetEndpoint.Count -gt 1){ + Write-Error "There was an issue identitfying the correct service connection to link to variable group $($group.name)" -ErrorAction Continue + } + $providerData = @{ + "serviceEndpointId" = $targetEndpoint.id + "vault" = $group.providerData.vault + } + } + $json = @{ "description" = $groupHeader.description "name" = $group.name - "providerData" = $group.providerData + "providerData" = $providerData "type" = $group.type "variableGroupProjectReferences" = $groupObj.variableGroupProjectReferences "variables" = $group.variables diff --git a/nuget.exe b/nuget.exe new file mode 100644 index 0000000..d70c80b Binary files /dev/null and b/nuget.exe differ diff --git a/set-readonly.ps1 b/set-readonly.ps1 new file mode 100644 index 0000000..6031760 --- /dev/null +++ b/set-readonly.ps1 @@ -0,0 +1,25 @@ +# Move all Contributors to Readers +# +# Members of groups such as Project Administrator or Build Administrator groups are not affected + +$groups = Get-ADOGroups ` + -OrgName $SourceOrgName ` + -ProjectName $SourceProjectName ` + -PersonalAccessToken $SourcePAT +$contributors = $groups | Where-Object { $_.Name -eq "Contributors" } +$readers = $groups | Where-Object { $_.Name -eq "Readers" } + +write-host "Adding all Contributor group user members to Readers ..." +foreach ($u in $contributors.UserMembers) { + az devops security group membership add --group-id $readers.Descriptor --member-id $u.PrincipalName --detect $false +} +foreach ($g in $contributors.GroupMembers) { + az devops security group membership add --group-id $readers.Descriptor --member-id $g.PrincipalName --detect $false +} +foreach ($g in $contributors.GroupMembers) { + az devops security group membership remove --group-id $contributors.Descriptor --member-id $g.PrincipalName --detect $false +} +foreach ($u in $contributors.UserMembers) { + az devops security group membership remove --group-id $contributors.Descriptor --member-id $u.PrincipalName --detect $false --yes +} + diff --git a/test-script.ps1 b/test-script.ps1 new file mode 100644 index 0000000..c435251 --- /dev/null +++ b/test-script.ps1 @@ -0,0 +1,6 @@ +Write-Host " " +Write-Host " TESTING TESTING TESTING " +Write-Host " Hello World " +Write-Host " TESTING TESTING TESTING " +Write-Host " " +