From 19c57e111846bc7f815bd95d27abc6735a550d6a Mon Sep 17 00:00:00 2001 From: JohnEvans Date: Mon, 6 Nov 2023 16:33:47 -0800 Subject: [PATCH] Update Azure DevOps Migration Tools --- .../workflows/ado-migration-process-full.yml | 95 ++ .../ado-migration-process-org-users.yml | 94 ++ .../ado-migration-process-partial.yml | 180 ++++ ...tion-process-workitem-backfill-between.yml | 170 ++++ ...do-migration-process-workitem-backfill.yml | 141 +++ .../{powershell.yml => code-scanning.yml} | 13 +- README.md | 197 +++- admin-tools/AzureDevOps-AgentPoolHelpers.ps1 | 18 - admin-tools/AzureDevOps-Helpers.ps1 | 292 ------ admin-tools/AzureDevOps-ProjectHelpers.ps1 | 419 -------- admin-tools/AzureDevOps-UserHelpers.ps1 | 82 -- admin-tools/AzureDevOps-WorkItemHelpers.ps1 | 190 ---- admin-tools/migrateBuildDefinitions.ps1 | 174 ---- admin-tools/migrateBuildQueues.ps1 | 52 - admin-tools/migrateDashboards.ps1 | 52 - admin-tools/migratePolicies.ps1 | 72 -- admin-tools/migrateProject.ps1 | 39 - admin-tools/migrateServiceEndpoints.ps1 | 60 -- admin-tools/migrateServiceHooks.ps1 | 141 --- admin-tools/migrateTeamMembers.ps1 | 69 -- admin-tools/migrateVariableGroups.ps1 | 97 -- configuration/README - Configuration.md | 57 ++ .../base-configuration.json | 108 +- configuration/configuration.json | 17 + configuration/migrator-configuration.json | 377 +++++++ docs/.order | 1 - docs/overview.md | 12 - docs/project-migration-process.md | 49 - {docs => documentaion}/.gitignore | 0 .../.images/clone-wiki-annotation.jpg | Bin .../.images/full-migration-workflow.png | Bin 0 -> 42893 bytes .../.images/partial-migration-workflow.png | Bin 0 -> 42934 bytes documentaion/.images/secret.png | Bin 0 -> 8334 bytes documentaion/.images/settings.png | Bin 0 -> 36269 bytes .../.images/user-migration-workflow.png | Bin 0 -> 42893 bytes documentaion/.images/variables.png | Bin 0 -> 21434 bytes {docs => documentaion}/FastTrack/.order | 0 {docs => documentaion/FastTrack}/FastTrack.md | 0 .../FastTrack/Migrator-Prepare-Step.md | 0 .../FastTrack/OData-Views.md | 0 {docs => documentaion}/FastTrack/Tools.md | 0 {docs => documentaion}/FastTrack/users.md | 0 documentaion/Migration steps list.md | 56 ++ {devops-docs => documentaion/devops}/git.md | 0 .../devops}/migrating-wikis.md | 0 .../devops}/overview.md | 0 .../using-the-azure-devops-cli-extension.md | 0 .../devops}/work-item-migrator.md | 0 .../git/branching-strategy.md | 0 .../git/cleaning-tfs-branches.md | 0 .../git/create-repo-from-folder.md | 0 .../git/folder-clone-steps.md | 0 .../git/git-helpers.md | 0 .../git/git-tfs-branches-compared.md | 0 {docs => documentaion}/migrate-modules.md | 178 ++-- .../migrating-project-source-code.md | 0 {docs => documentaion}/migrating-wikis.md | 0 .../using-the-azure-devops-cli-extension.md | 0 {docs => documentaion}/work-item-migrator.md | 0 helper-scripts/ADODeletePolicies.ps1 | 57 ++ helper-scripts/ADODeleteRepos.ps1 | 68 ++ helper-scripts/ADODeleteVariableGroups.ps1 | 55 ++ helper-scripts/README - Helper-Scripts.md | 21 + images/full-migration-workflow.png | Bin 0 -> 42893 bytes images/partial-migration-workflow.png | Bin 0 -> 42934 bytes images/secret.png | Bin 0 -> 8334 bytes images/settings.png | Bin 0 -> 36269 bytes images/user-migration-workflow.png | Bin 0 -> 42893 bytes images/variables.png | Bin 0 -> 21434 bytes migration-scripts/AllProjects.ps1 | 198 ---- migration-scripts/Configuration.json | 15 - migration-scripts/README.md | 81 -- migration-scripts/create-manifest.ps1 | 38 - modules/ADO-AddCustomField.psm1 | 303 ++++++ modules/Migrate-ADO-Artifacts.psm1 | 476 +++++++++ modules/Migrate-ADO-BuildDefinitions.psm1 | 25 + modules/Migrate-ADO-BuildEnvironments.psm1 | 450 +++++++++ modules/Migrate-ADO-BuildQueues.psm1 | 9 +- modules/Migrate-ADO-Common.psm1 | 466 ++++++++- modules/Migrate-ADO-Dashboards.psm1 | 260 +++++ modules/Migrate-ADO-DeliveryPlans.psm1 | 86 ++ modules/Migrate-ADO-Groups.psm1 | 261 ++--- modules/Migrate-ADO-Pipelines.psm1 | 2 +- modules/Migrate-ADO-Policies.psm1 | 236 +++++ modules/Migrate-ADO-Project.psm1 | 288 ++++++ modules/Migrate-ADO-ReleaseDefinitions.psm1 | 25 + modules/Migrate-ADO-Repos.psm1 | 271 ++--- modules/Migrate-ADO-ServiceConnections.psm1 | 159 +++ modules/Migrate-ADO-ServiceHooks.psm1 | 220 +++++ modules/Migrate-ADO-Teams.psm1 | 46 +- modules/Migrate-ADO-Template.psm1 | 36 + modules/Migrate-ADO-Users.psm1 | 103 +- modules/Migrate-ADO-VariableGroups.psm1 | 129 +++ modules/Migrate-ADO-Wikis.psm1 | 132 +++ modules/Migrate-Packages.psm1 | 929 ++++++++++++++++++ modules/README - Modules.md | 387 ++++++++ modules/README.md | 111 --- project-migration/MigrateProject.ps1 | 414 ++++++++ ...ADME - Github Action Workflow migration.md | 47 + .../README - Project Migration.md | 262 +++++ project-migration/Step_0_Migrate_Project.ps1 | 72 ++ project-migration/Step_1_Migrate_Project.ps1 | 20 + project-migration/Step_2_Migrate_Project.ps1 | 25 + project-migration/Step_3_Migrate_Project.ps1 | 164 ++++ project-migration/Step_4_Migrate_Project.ps1 | 36 + project-migration/Step_5_Migrate_Project.ps1 | 13 + .../Step_X_Migrate_Org_Level_Users.ps1 | 13 + .../Work-Item-Backfill_MIgrate_Project.ps1 | 39 + tool-scripts/Clone-All-Project-Repos.ps1 | 49 + tool-scripts/Create-Manifest.ps1 | 52 + tool-scripts/DeleteDashboards.ps1 | 57 ++ tool-scripts/DeleteGroups.ps1 | 39 + tool-scripts/DeleteServiceConnections.ps1 | 45 + ...ate_Artifact_Feed_Package_Version_Data.ps1 | 61 ++ tool-scripts/GetCurrentUserInfo.ps1 | 24 + tool-scripts/GetItemsNotMigrated.ps1 | 160 +++ tool-scripts/GetMigratedItemCounts.ps1 | 74 ++ .../GetServiceConnectionsLastUsed.ps1 | 55 ++ .../IdentifyPlansAndSuitesForUnknowUsers.ps1 | 111 +++ .../IdentifySuitesAndCasesForTestPlans.ps1 | 63 ++ tool-scripts/README - Tool-Scripts.md | 62 ++ .../Set-Readonly.ps1 | 50 +- tool-scripts/ValidateBuildEnvironments.ps1 | 103 ++ tool-scripts/ValidateBuildGroupsAndUsers.ps1 | 150 +++ .../VerifyFieldsForOrganizationProject.ps1 | 79 ++ .../VerifyFieldsForWorkItemInProcess.ps1 | 74 ++ tools/clone-all-project-repos.ps1 | 40 - tools/migrate-external-repos.ps1 | 65 -- 128 files changed, 8928 insertions(+), 3035 deletions(-) create mode 100644 .github/workflows/ado-migration-process-full.yml create mode 100644 .github/workflows/ado-migration-process-org-users.yml create mode 100644 .github/workflows/ado-migration-process-partial.yml create mode 100644 .github/workflows/ado-migration-process-workitem-backfill-between.yml create mode 100644 .github/workflows/ado-migration-process-workitem-backfill.yml rename .github/workflows/{powershell.yml => code-scanning.yml} (89%) delete mode 100644 admin-tools/AzureDevOps-AgentPoolHelpers.ps1 delete mode 100644 admin-tools/AzureDevOps-Helpers.ps1 delete mode 100644 admin-tools/AzureDevOps-ProjectHelpers.ps1 delete mode 100644 admin-tools/AzureDevOps-UserHelpers.ps1 delete mode 100644 admin-tools/AzureDevOps-WorkItemHelpers.ps1 delete mode 100644 admin-tools/migrateBuildDefinitions.ps1 delete mode 100644 admin-tools/migrateBuildQueues.ps1 delete mode 100644 admin-tools/migrateDashboards.ps1 delete mode 100644 admin-tools/migratePolicies.ps1 delete mode 100644 admin-tools/migrateProject.ps1 delete mode 100644 admin-tools/migrateServiceEndpoints.ps1 delete mode 100644 admin-tools/migrateServiceHooks.ps1 delete mode 100644 admin-tools/migrateTeamMembers.ps1 delete mode 100644 admin-tools/migrateVariableGroups.ps1 create mode 100644 configuration/README - Configuration.md rename {migration-scripts => configuration}/base-configuration.json (97%) create mode 100644 configuration/configuration.json create mode 100644 configuration/migrator-configuration.json delete mode 100644 docs/.order delete mode 100644 docs/overview.md delete mode 100644 docs/project-migration-process.md rename {docs => documentaion}/.gitignore (100%) rename {devops-docs => documentaion}/.images/clone-wiki-annotation.jpg (100%) create mode 100644 documentaion/.images/full-migration-workflow.png create mode 100644 documentaion/.images/partial-migration-workflow.png create mode 100644 documentaion/.images/secret.png create mode 100644 documentaion/.images/settings.png create mode 100644 documentaion/.images/user-migration-workflow.png create mode 100644 documentaion/.images/variables.png rename {docs => documentaion}/FastTrack/.order (100%) rename {docs => documentaion/FastTrack}/FastTrack.md (100%) rename {docs => documentaion}/FastTrack/Migrator-Prepare-Step.md (100%) rename {docs => documentaion}/FastTrack/OData-Views.md (100%) rename {docs => documentaion}/FastTrack/Tools.md (100%) rename {docs => documentaion}/FastTrack/users.md (100%) create mode 100644 documentaion/Migration steps list.md rename {devops-docs => documentaion/devops}/git.md (100%) rename {devops-docs => documentaion/devops}/migrating-wikis.md (100%) rename {devops-docs => documentaion/devops}/overview.md (100%) rename {devops-docs => documentaion/devops}/using-the-azure-devops-cli-extension.md (100%) rename {devops-docs => documentaion/devops}/work-item-migrator.md (100%) rename {devops-docs => documentaion}/git/branching-strategy.md (100%) rename {devops-docs => documentaion}/git/cleaning-tfs-branches.md (100%) rename {devops-docs => documentaion}/git/create-repo-from-folder.md (100%) rename {devops-docs => documentaion}/git/folder-clone-steps.md (100%) rename {devops-docs => documentaion}/git/git-helpers.md (100%) rename {devops-docs => documentaion}/git/git-tfs-branches-compared.md (100%) rename {docs => documentaion}/migrate-modules.md (98%) rename {docs => documentaion}/migrating-project-source-code.md (100%) rename {docs => documentaion}/migrating-wikis.md (100%) rename {docs => documentaion}/using-the-azure-devops-cli-extension.md (100%) rename {docs => documentaion}/work-item-migrator.md (100%) create mode 100644 helper-scripts/ADODeletePolicies.ps1 create mode 100644 helper-scripts/ADODeleteRepos.ps1 create mode 100644 helper-scripts/ADODeleteVariableGroups.ps1 create mode 100644 helper-scripts/README - Helper-Scripts.md create mode 100644 images/full-migration-workflow.png create mode 100644 images/partial-migration-workflow.png create mode 100644 images/secret.png create mode 100644 images/settings.png create mode 100644 images/user-migration-workflow.png create mode 100644 images/variables.png delete mode 100644 migration-scripts/AllProjects.ps1 delete mode 100644 migration-scripts/Configuration.json delete mode 100644 migration-scripts/README.md delete mode 100644 migration-scripts/create-manifest.ps1 create mode 100644 modules/ADO-AddCustomField.psm1 create mode 100644 modules/Migrate-ADO-Artifacts.psm1 create mode 100644 modules/Migrate-ADO-BuildDefinitions.psm1 create mode 100644 modules/Migrate-ADO-BuildEnvironments.psm1 create mode 100644 modules/Migrate-ADO-Dashboards.psm1 create mode 100644 modules/Migrate-ADO-DeliveryPlans.psm1 create mode 100644 modules/Migrate-ADO-Policies.psm1 create mode 100644 modules/Migrate-ADO-Project.psm1 create mode 100644 modules/Migrate-ADO-ReleaseDefinitions.psm1 create mode 100644 modules/Migrate-ADO-ServiceConnections.psm1 create mode 100644 modules/Migrate-ADO-ServiceHooks.psm1 create mode 100644 modules/Migrate-ADO-Template.psm1 create mode 100644 modules/Migrate-ADO-VariableGroups.psm1 create mode 100644 modules/Migrate-ADO-Wikis.psm1 create mode 100644 modules/Migrate-Packages.psm1 create mode 100644 modules/README - Modules.md delete mode 100644 modules/README.md create mode 100644 project-migration/MigrateProject.ps1 create mode 100644 project-migration/README - Github Action Workflow migration.md create mode 100644 project-migration/README - Project Migration.md create mode 100644 project-migration/Step_0_Migrate_Project.ps1 create mode 100644 project-migration/Step_1_Migrate_Project.ps1 create mode 100644 project-migration/Step_2_Migrate_Project.ps1 create mode 100644 project-migration/Step_3_Migrate_Project.ps1 create mode 100644 project-migration/Step_4_Migrate_Project.ps1 create mode 100644 project-migration/Step_5_Migrate_Project.ps1 create mode 100644 project-migration/Step_X_Migrate_Org_Level_Users.ps1 create mode 100644 project-migration/Work-Item-Backfill_MIgrate_Project.ps1 create mode 100644 tool-scripts/Clone-All-Project-Repos.ps1 create mode 100644 tool-scripts/Create-Manifest.ps1 create mode 100644 tool-scripts/DeleteDashboards.ps1 create mode 100644 tool-scripts/DeleteGroups.ps1 create mode 100644 tool-scripts/DeleteServiceConnections.ps1 create mode 100644 tool-scripts/Generate_Artifact_Feed_Package_Version_Data.ps1 create mode 100644 tool-scripts/GetCurrentUserInfo.ps1 create mode 100644 tool-scripts/GetItemsNotMigrated.ps1 create mode 100644 tool-scripts/GetMigratedItemCounts.ps1 create mode 100644 tool-scripts/GetServiceConnectionsLastUsed.ps1 create mode 100644 tool-scripts/IdentifyPlansAndSuitesForUnknowUsers.ps1 create mode 100644 tool-scripts/IdentifySuitesAndCasesForTestPlans.ps1 create mode 100644 tool-scripts/README - Tool-Scripts.md rename migration-scripts/set-readonly.ps1 => tool-scripts/Set-Readonly.ps1 (97%) create mode 100644 tool-scripts/ValidateBuildEnvironments.ps1 create mode 100644 tool-scripts/ValidateBuildGroupsAndUsers.ps1 create mode 100644 tool-scripts/VerifyFieldsForOrganizationProject.ps1 create mode 100644 tool-scripts/VerifyFieldsForWorkItemInProcess.ps1 delete mode 100644 tools/clone-all-project-repos.ps1 delete mode 100644 tools/migrate-external-repos.ps1 diff --git a/.github/workflows/ado-migration-process-full.yml b/.github/workflows/ado-migration-process-full.yml new file mode 100644 index 0000000..e094422 --- /dev/null +++ b/.github/workflows/ado-migration-process-full.yml @@ -0,0 +1,95 @@ +name: Full ADO Project Migration + +on: + workflow_dispatch: + inputs: + SourceOrganizationName: + description: "Name of the Source Organization" + required: true + default: "AIZ-GL" + SourceProjectName: + description: "Name of the Source Project" + required: true + default: "GL.CL-Elita" + TargetOrganizationName: + description: "Name of the Target Organization" + required: true + default: "AIZ-Global" + TargetProjectName: + description: "Name of the Target Project" + required: true + default: "GL.CL-Elita-migrated" + WhatIf: + type: boolean + description: "WhatIf: Simulated Run" + required: false + default: true + +jobs: + run-powershell-script: + name: Run PowerShell Script + runs-on: 'AEC0WGEMP001' + timeout-minutes: 7200 + env: + AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} + + steps: + - name: Verify Azure CLI installation + run: | + $env:PATH + if ($env:PATH -notcontains "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin") { + $env:PATH += ";C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + } + + az --version + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Clean Azure DevOps Migration Tools Log Directory + shell: pwsh + run: | + Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse + + - name: Set Migration Configuration + shell: pwsh + run: | + $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" + $TargetOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.TargetOrganizationName }}/" + $projectDirectory = "./" + + $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 }}" + $configuration.SourceProject.OrgName = "${{ github.event.inputs.SourceOrganizationName }}" + $configuration.TargetProject.Organization = "$TargetOrganizationUrl" + $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" + $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" + $configuration.ProjectDirectory = $projectDirectory + $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $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) + + - name: Run Migrate-Project PowerShell script + shell: pwsh + run: | + $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" + $WhatIf = $whatIfDryRun -match "true" + + & ./Step_0_Migrate_Project.ps1 -WhatIf $WhatIf + + - name: Archive code coverage results + uses: actions/upload-artifact@v3 + 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 new file mode 100644 index 0000000..074e8a3 --- /dev/null +++ b/.github/workflows/ado-migration-process-org-users.yml @@ -0,0 +1,94 @@ +name: ADO Organization User Migration + +on: + workflow_dispatch: + inputs: + SourceOrganizationName: + description: "Name of the Source Organization" + required: true + default: "AIZ-GL" + TargetOrganizationName: + description: "Name of the Target Organization" + required: true + default: "AIZ-Global" + WhatIf: + type: boolean + description: "WhatIf: Simulated Run" + required: false + default: true + + +jobs: + run-powershell-script: + name: Run PowerShell Script + runs-on: 'AEC0WGEMP001' + timeout-minutes: 7200 + env: + AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} + + steps: + - name: Verify Azure CLI installation + run: | + $env:PATH + if ($env:PATH -notcontains "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin") { + $env:PATH += ";C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + } + + az --version + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Clean Azure DevOps Migration Tools Log Directory + shell: pwsh + run: | + Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse + + - name: Set Migration Configuration + shell: pwsh + run: | + $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" + $TargetOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.TargetOrganizationName }}/" + $projectDirectory = "./" + + $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.SourceOrganizationName }}" + $configuration.SourceProject.OrgName = "${{ github.event.inputs.SourceOrganizationName }}" + $configuration.TargetProject.Organization = "$TargetOrganizationUrl" + $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetOrganizationName }}" + $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" + $configuration.ProjectDirectory = $projectDirectory + $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $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) + + - name: Run Migrate-Project PowerShell script + shell: pwsh + run: | + $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" + $WhatIf = $whatIfDryRun -match "true" + + & ./Step_X_Migrate_Org_Level_Users.ps1 -WhatIf $WhatIf + + - name: Archive DevOps-Enablement-ADO-to-ADO-migration results + uses: actions/upload-artifact@v3 + with: + name: migration-run-logs + path: "./Projects" + + - name: Archive Azure DevOps Migration Tools (Martin's Tools) results + uses: actions/upload-artifact@v3 + with: + name: migration-tools-logs + path: "${{ vars.WorkItemMigratorDirectory }}/logs" + diff --git a/.github/workflows/ado-migration-process-partial.yml b/.github/workflows/ado-migration-process-partial.yml new file mode 100644 index 0000000..fd7a054 --- /dev/null +++ b/.github/workflows/ado-migration-process-partial.yml @@ -0,0 +1,180 @@ +name: Partial ADO Project Migration + +on: + workflow_dispatch: # Allows manual triggering via the GitHub Actions UI + inputs: + SourceOrganizationName: + description: "Name of the Source Organization" + required: true + default: "AIZ-GL" + SourceProjectName: + description: "Name of the Source Project" + required: true + default: "GL.CL-Elita" + TargetOrganizationName: + description: "Name of the Target Organization" + required: true + default: "AIZ-Global" + TargetProjectName: + description: "Name of the Target Project" + required: true + default: "GL.CL-Elita-migrated" + MigrationSelection: + description: "Migration Selection" + required: true + default: "Select an area to migrate" + type: choice + options: + - Select an area to migrate + - 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') + WhatIf: + type: boolean + description: "WhatIf: Simulated Run" + required: false + default: true + +jobs: + run-powershell-script: + name: Run PowerShell Script + runs-on: 'AEC0WGEMP001' + timeout-minutes: 7200 + env: + AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} + + steps: + - name: Verify Azure CLI installation + run: | + $env:PATH + if ($env:PATH -notcontains "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin") { + $env:PATH += ";C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + } + + az --version + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Clean Azure DevOps Migration Tools Log Directory + shell: pwsh + run: | + Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse + + - name: Set Migration Configuration + shell: pwsh + run: | + $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" + $TargetOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.TargetOrganizationName }}/" + $projectDirectory = "./" + + $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 }}" + $configuration.SourceProject.OrgName = "${{ github.event.inputs.SourceOrganizationName }}" + $configuration.TargetProject.Organization = "$TargetOrganizationUrl" + $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" + $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" + $configuration.ProjectDirectory = $projectDirectory + $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $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) + + - name: Run Migrate-Project PowerShell script + shell: pwsh + run: | + $selection = "${{ github.event.inputs.MigrationSelection }}" + echo $selection + + $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" + $WhatIf = $whatIfDryRun -match "true" + + switch -Exact ($selection) + { + "Select an area to migrate" { return } + "Areas and Iterations" + { & .\MigrateProject.ps1 -SkipMigrateTfsAreaAndIterations $WhatIf } + "Artifacts" + { & .\MigrateProject.ps1 -SkipMigrateArtifacts $WhatIf } + "Build Pipelines" + { & .\MigrateProject.ps1 -SkipMigrateBuildPipelines $WhatIf } + "Build Queues & Build Environments" + { & .\MigrateProject.ps1 -SkipMigrateBuildQueues $WhatIf } + "Dashboards" + { & .\MigrateProject.ps1 -SkipMigrateDashboards $WhatIf } + "Delivery Plans" + { & .\MigrateProject.ps1 -SkipMigrateDeliveryPlans $WhatIf } + "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 + } + "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 + } + "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 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 + } + "Wikis" + { & .\MigrateProject.ps1 -SkipMigrateWikis $WhatIf } + "Work Item Querys" + { & .\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 + 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 new file mode 100644 index 0000000..5f15586 --- /dev/null +++ b/.github/workflows/ado-migration-process-workitem-backfill-between.yml @@ -0,0 +1,170 @@ +name: sBetween Dates Work Item Backfill ADO Project Migration + +on: + workflow_dispatch: # Allows manual triggering via the GitHub Actions UI + inputs: + SourceOrganizationName: + description: "Name of the Source Organization" + required: true + default: "AIZ-GL" + SourceProjectName: + description: "Name of the Source Project" + required: true + default: "GL.CL-Elita" + TargetOrganizationName: + description: "Name of the Target Organization" + required: true + default: "AIZ-Global" + TargetProjectName: + description: "Name of the Target Project" + required: true + default: "GL.CL-Elita-migrated" + StartDate: + description: "Start Changed Date (dd/mm/yyyy)" + required: true + default: "Today" + EndDate: + description: "End Changed Date (dd/mm/yyyy)" + required: false + default: "" + WorkItemSelection: + description: "Work-Item Type Selection" + required: false + default: "Select a Work-Item Type" + type: choice + options: + - Any + - Bug + - Change Request + - Code Review Request + - Code Review Response + - Epic + - Features + - Feedback Request + - Feedback Response + - Impediment + - Incident + - Issue + - Portfolio Epic + - Product Backlog Item + - Proposal + - Requirement + - Review + - Risk + - Shared Parameter + - Shared Steps + - Tasks + - Test Case + - User Story + WhatIf: + type: boolean + description: "WhatIf: Simulated Run" + required: false + default: true + +jobs: + run-powershell-script: + name: Run PowerShell Script + runs-on: 'AEC0WGEMP001' + timeout-minutes: 7200 + env: + AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} + + steps: + - name: Verify Azure CLI installation + run: | + $env:PATH + if ($env:PATH -notcontains "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin") { + $env:PATH += ";C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + } + + az --version + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Clean Azure DevOps Migration Tools Log Directory + shell: pwsh + run: | + Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse + + - name: Set Migration Configuration + shell: pwsh + run: | + $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" + $TargetOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.TargetOrganizationName }}/" + $projectDirectory = "./" + + $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 }}" + $configuration.SourceProject.OrgName = "${{ github.event.inputs.SourceOrganizationName }}" + $configuration.TargetProject.Organization = "$TargetOrganizationUrl" + $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" + $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" + $configuration.ProjectDirectory = $projectDirectory + $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $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: | + $selection = "${{ github.event.inputs.WorkItemSelection }}" + if($selection -eq "Any") { + $selection = "" + } + $start = "${{ github.event.inputs.StartDate }}" + $end = "${{ github.event.inputs.EndDate }}" + $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" + $WhatIf = $whatIfDryRun -match "true" + $format = "dd/MM/yyyy" + + echo $selection + echo $start + echo $end + echo $WhatIf + + if($start -eq "Today") { + $start = (get-date).ToString($format) + } + if($end -eq "Today") { + $end = (get-date).ToString($format) + } + + [DateTime] $startDate = New-Object DateTime + $isDateStart = [DateTime]::TryParse($start, [ref]$startDate) + if($isDateStart -eq $FALSE) { + echo "Start Date is not a valid date value in the format ($format)..." + } + + [DateTime] $endDate = $startDate + $isDateEnd = $TRUE + if($end -ne "") { + $isDateEnd = [DateTime]::TryParse($end, [ref]$endDate) + if($isDateEnd -eq $FALSE) { + echo "End Date is not a valid date value in the format ($format)..." + } + } + $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" + $WhatIf = $whatIfDryRun -match "true" + + if($isDateStart -and $isDateEnd) { + $startDateString = $startDate.ToString($format) + $endDateString = $endDate.ToString($format) + & .\Work-Item-Backfill_Migrate_Project.ps1 -StartDate $startDateString -EndDate -endDateString -ItemType $selection -WhatIf $WhatIf + } + + - name: Archive code coverage results + uses: actions/upload-artifact@v3 + 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 new file mode 100644 index 0000000..be2aecf --- /dev/null +++ b/.github/workflows/ado-migration-process-workitem-backfill.yml @@ -0,0 +1,141 @@ +name: Work Item Backfill ADO Project Migration + +on: + workflow_dispatch: # Allows manual triggering via the GitHub Actions UI + inputs: + SourceOrganizationName: + description: "Name of the Source Organization" + required: true + default: "AIZ-GL" + SourceProjectName: + description: "Name of the Source Project" + required: true + default: "GL.CL-Elita" + TargetOrganizationName: + description: "Name of the Target Organization" + required: true + default: "AIZ-Global" + TargetProjectName: + description: "Name of the Target Project" + required: true + default: "GL.CL-Elita-migrated" + NumberOfDays: + description: "Number of days with Changes" + required: false + default: "0" + WorkItemSelection: + description: "Work-Item Type Selection" + required: false + default: "Select a Work-Item Type" + type: choice + options: + - Any + - Bug + - Change Request + - Code Review Request + - Code Review Response + - Epic + - Features + - Feedback Request + - Feedback Response + - Impediment + - Incident + - Issue + - Portfolio Epic + - Product Backlog Item + - Proposal + - Requirement + - Review + - Risk + - Shared Parameter + - Shared Steps + - Tasks + - Test Case + - User Story + WhatIf: + type: boolean + description: "WhatIf: Simulated Run" + required: false + default: true + +jobs: + run-powershell-script: + name: Run PowerShell Script + runs-on: 'AEC0WGEMP001' + timeout-minutes: 7200 + env: + AZURE_DEVOPS_MIGRATION_PAT: ${{ secrets.AZURE_DEVOPS_MIGRATION_PAT }} + + steps: + - name: Verify Azure CLI installation + run: | + $env:PATH + if ($env:PATH -notcontains "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin") { + $env:PATH += ";C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + } + + az --version + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Clean Azure DevOps Migration Tools Log Directory + shell: pwsh + run: | + Get-ChildItem "${{ vars.WorkItemMigratorDirectory }}/logs" -Recurse | Remove-Item -Recurse + + - name: Set Migration Configuration + shell: pwsh + run: | + $SourceOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.SourceOrganizationName }}/" + $TargetOrganizationUrl = "https://dev.azure.com/${{ github.event.inputs.TargetOrganizationName }}/" + $projectDirectory = "./" + + $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 }}" + $configuration.SourceProject.OrgName = "${{ github.event.inputs.SourceOrganizationName }}" + $configuration.TargetProject.Organization = "$TargetOrganizationUrl" + $configuration.TargetProject.ProjectName = "${{ github.event.inputs.TargetProjectName }}" + $configuration.TargetProject.OrgName = "${{ github.event.inputs.TargetOrganizationName }}" + $configuration.ProjectDirectory = $projectDirectory + $configuration.WorkItemMigratorDirectory = "${{ vars.WorkItemMigratorDirectory }}" + $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) + + - name: Run Migrate-Project PowerShell script + shell: pwsh + run: | + $selection = "${{ github.event.inputs.WorkItemSelection }}" + if($selection -eq "Any") { + $selection = "" + } + + $numOfDays = "${{ github.event.inputs.NumberOfDays }}" + $number = $NULL + $isNum = [int]::TryParse($numOfDays, [ref]$number) + if($isNum -eq $FALSE) { + echo "Number of Days is not a number, defaulting to 0..." + } + $numOfDays = "$number" + echo "Number of Days Changed: $numOfDays" + + $whatIfDryRun = "${{ github.event.inputs.WhatIf }}" + $WhatIf = $whatIfDryRun -match "true" + + & .\Work-Item-Backfill_Migrate_Project.ps1 -NumberOfDays $numOfDays -ItemType $selection -WhatIf $WhatIf + + - name: Archive code coverage results + uses: actions/upload-artifact@v3 + with: + name: migration-run-logs + path: "./Projects" \ No newline at end of file diff --git a/.github/workflows/powershell.yml b/.github/workflows/code-scanning.yml similarity index 89% rename from .github/workflows/powershell.yml rename to .github/workflows/code-scanning.yml index f22457e..266307b 100644 --- a/.github/workflows/powershell.yml +++ b/.github/workflows/code-scanning.yml @@ -13,10 +13,9 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] schedule: - - cron: '33 13 * * 4' - + - cron: '39 11 * * 0' + permissions: contents: read @@ -25,7 +24,7 @@ jobs: permissions: contents: read # for actions/checkout to fetch code security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status name: PSScriptAnalyzer runs-on: ubuntu-latest steps: @@ -37,11 +36,11 @@ jobs: # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. path: .\ - recurse: true - # Include your own basic security rules. Removing this option will run all the rules + recurse: true + # Include your own basic security rules. Removing this option will run all the rules includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' output: results.sarif - + # Upload the SARIF file generated in the previous step - name: Upload SARIF results file uses: github/codeql-action/upload-sarif@v2 diff --git a/README.md b/README.md index 2050346..69adf65 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,198 @@ -# AzureDevOps-Tools +# Introduction +Azure DevOps references, tools, how-tos -Collection of PowerShell scripts and modules and documentation for Azure DevOps primarily focused on project to project migrations +## 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 +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 +SourceProject,TargetProject +Source-Project-Name1,Target-Project-Name1 +Source-Project-Name2,Target-Project-Name2 +Source-Project-Name3,Target-Project-Name3 +``` +- Lines are separated by new lines, not commas. +- Source project names and target project names are separated by commas. + +The first line of the CSV acts as the header, these lines should not be modified (if they are modified you will need to update the header names in the `AllProjects.ps1` script as well. + +All following lines define a source project to migrate from and a target project to migrate to. + +# Configuration.json +The `Configuration.json` file is used to set up file locations for logging, PAT tokens for authentication and other information required for running the `migration-scripts/AllProjects.ps1` script. + +##### PROPERTIES +| Property Name | VSTS Only? | Data Type | Description +|---------------------------|------------|-----------|------------- +| TargetProject | | Object | An object consisting of an OrgName and a PAT +| └─ OrgName | | String | The organization name for the target project +| └─ PAT | | String | The personal access token you created (or need to create) for the target project +| SourceProject | | Object | An object consisting of an OrgName and a PAT +| └─ OrgName | | String | The organization name for the source project +| └─ PAT | | String | The personal access token you created (or need to create) for the source project +| SavedAzureQuery | ✔️ | String | Only required if using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) , read more here: ['query' parameter documentation](https://github.com/microsoft/vsts-work-item-migrator/blob/master/WiMigrator/migration-configuration.md#query-the-name-of-the-query-to-use-for-identifying-work-items-to-migrate-note-query-must-be-a-flat) +| 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. +| ProjectscCsv | | String | The path of the csv file holding the list of projects you want to migrate. This csv is included in the repo and the path is provided as a relative path, so you should not need to update this setting. +| MsConfigPath | ✔️ | String | Only required if using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) . This is the configuration file that will be copied and modified for each project. This path is set relatively and the configuration file is provided in the repo so you should not need to update this setting. +| WorkItemMigratorDirectory | ✔️ | String | Only required if using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) . This is the directory you cloned the migration tool too. Be sure to include the directory `WiMigrator` at the end of the cloned repository path. + +---------- + +**VSTS Only** means that the configuration property is only required if you are using the VSTS work item migrator. + +# base-configuration.json ([VSTS only](https://github.com/microsoft/vsts-work-item-migrator)) +A pre-configured configuration file used by the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator). +Read more about [base-configuration here](https://github.com/microsoft/vsts-work-item-migrator/blob/master/WiMigrator/migration-configuration.md) + +For each project migration defined in the `Projects.csv` the `base-configuration.json` file is copied, modified and saved in that projects directory before a migration is preformed. + +# 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. + +This script should be run when the repo is first cloned and whenever the `create-manifest.ps1` script is updated. + +# AllProjects.ps1 +The `AllProjects.ps1` script preforms a full migration of the following DevOps items: +- Area Paths + - Using the `Start-ADOAreaPathsMigration` cmdlet under `supporting-modules` +- Iteration Paths + - Using the `StartADOIterationPathsMigration` cmdlet under `supporting-modules` +- Build Queues + - Using the `Start-ADOBuildQueuesMigration` cmdlet under `supporting-modules` +- Repos + - Using the `Start-ADORepoMigration` cmdlet under `supporting-modules` +- Work Items + - Using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) + +The script starts importing the `Projects.csv` and setting a migration run date, which is then used to create a migration directory under the path specified in the `Configuration.json` file. + +Each migration defined under `Projects.csv` gets it's own folder where a copy of `base-configuration.json` is created and configured specifically for that migration. All of the migrations are nested under a folder dated with the migration run date set above. + +After the project directories are created for each project, the script preforms a migration for each project. + + +# Migration Notes +- default iteration path is not set a team +- default area path is not set for a team + +# 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 diff --git a/admin-tools/AzureDevOps-AgentPoolHelpers.ps1 b/admin-tools/AzureDevOps-AgentPoolHelpers.ps1 deleted file mode 100644 index 04e26d5..0000000 --- a/admin-tools/AzureDevOps-AgentPoolHelpers.ps1 +++ /dev/null @@ -1,18 +0,0 @@ - -function Get-ADOPools ($Headers, [string]$Org) { - $url = "$org/_apis/distributedtask/pools?api-version=5.0" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value -} - -function Get-ADODeploymentPools ($Headers, [string]$Org) { - $url = "$org/_apis/distributedtask/pools?poolType=deployment&api-version=5.0" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value -} - -function Get-ADOPoolAgents($Headers, [string]$Org, [int]$PoolId) { - $url = "$org/_apis/distributedtask/pools/$PoolId/agents?includeCapabilities=true&includeLastCompletedRequest=true&api-version=5.0" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value -} diff --git a/admin-tools/AzureDevOps-Helpers.ps1 b/admin-tools/AzureDevOps-Helpers.ps1 deleted file mode 100644 index fe08c75..0000000 --- a/admin-tools/AzureDevOps-Helpers.ps1 +++ /dev/null @@ -1,292 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter()] - [String] - $LogLocation = $PSScriptRoot -) -#todo help - -function New-HTTPHeaders([string]$pat) { - if (!($pat)) { - throw "Azure DevOps PAT must be provided" - } - $authToken = [System.Convert]::ToBase64String([System.Text.ASCIIEncoding]::ASCII.GetBytes([string]::Format("{0}:{1}", "", $pat))) - $headers = @{'Authorization' = "Basic $authToken"} - return $headers -} - -function Get-ADOProcesses($Headers, [string]$Org, [string]$ProcessName) { - - $url = "$org/_apis/work/processes?api-version=5.0-preview.2" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - if ($ProcessName) { - return $results.value | Where-Object {$_.name -ieq $ProcessName} - } - else { - return $results.value - } -} - -function Get-ADOProjects($Headers, [string]$Org, [string]$ProjectName) { - if ($ProjectName) { - $url = "$org/_apis/projects/$ProjectName" - return Invoke-RestMethod -Method Get -uri $url -Headers $Headers - } - else { - $url = "$org/_apis/projects?`$top=600&api-version=5.1" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers - return $results.value - } -} - -function Get-ADOProjectProperties($Headers, [string]$Org, [string]$ProjectId, [string]$PropertyKey) { - # GET https://dev.azure.com/fabrikam/_apis/projects/{projectId}/properties?keys=System.CurrentProcessTemplateId,*SourceControl*&api-version=5.1-preview.1 - - $keys="" - if ($PropertyKey) { - $keys="keys=$PropertyKey" - } - $url = "$org/_apis/projects/$ProjectId/properties?$keys&api-version=5.1-preview.1" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers - if ($ProjectName) { - return $results.value | Where-Object {$_.name -ieq $ProjectName} - } - else { - return $results.value - } -} - -function Get-ADOProjectProcessTemplates($Headers, [string]$Org) { - $projectTemplates = @() - - $projects = Get-ADOProjects -Headers $Headers -Org $Org - $projects | ForEach-Object { - $template = Get-ADOProjectProperties -Headers $headers -Org $org -ProjectId $_.id -PropertyKey "System.Process Template" - $projectTemplates += [PSCustomObject]@{ - Name = $_.Name - Template = $template[0].value - } - - } - return $projectTemplates -} - - - -function Get-AllRepos([string]$org) { - - # GET https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/commits?api-version=5.0 - $url = "$org/_apis/git/repositories?api-version=5.0" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value -} - -function Get-Teams([string]$org, $headers) { - - # GET https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/commits?api-version=5.0 - $url = "$org/_apis/teams?`$top=5000" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value -} - - -function Get-AllReposWithLastCommit([string]$listName, [string]$org) { - $final = New-Object System.Collections.ArrayList - $repos = Get-Repos -org $org - - foreach ($repo in $repos) { - $repoId = $repo.id - $url = "$org/_apis/git/repositories/$repoId/commits?api-version=5.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - if ($results.value.Count -gt 0) { - $final.Add(@{ - "id" = $repo.id - "name" = $repo.name - "sizeInBytes" = $repo.size - "sizeInMegaBytes" = [math]::Round($repo.size / 1024 / 1024, 6) - "projectName" = $repo.project.name - "projectId" = $repo.project.id - "lastCommit" = $results.value[0].committer.date - }) - } else { - $final.Add(@{ - "id" = $repo.id - "name" = $repo.name - "sizeInBytes" = $repo.size - "sizeInMegaBytes" = [math]::Round($repo.size / 1024 / 1024, 6) - "projectName" = $repo.project.name - "projectId" = $repo.project.id - "lastCommit" = null - }) - } - - } - return $final -} - -function New-GitRepository($org, $projectId, $repoName, $pat) { - - #"POST https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=5.1" - $url = "$org/_apis/git/repositories?api-version=5.1" - - $requestBody = @{ - name = $repoName - project = @{ - id = $projectId - } - } | ConvertTo-Json - - try { - $results = Invoke-RestMethod -Method post -uri $url -Headers (New-HTTPHeaders -pat $pat) -Body $requestBody -ContentType 'application/json' - } - catch { - Write-Log -msg "Error_ $($_.Exception) creating repo $repoName in project $projectId" - } - -} - -function Delete-WorkItemById($headers, $org, $projectName, $workItemId, $destroy) { - # DELETE https://dev.azure.com/{organization}/{project}/_apis/wit/workitems/{id}?destroy={destroy}&api-version=5.1 - - $destroyTerm = "" - if ($destroy) { - $destroyTerm = "destroy=$destroy&" - } - - "workitemid is $workItemId" - $url = "$org/$projectName/_apis/wit/workitems/"+$workItemId+"?$destroyTerm&api-version=5.1" - $url - - try { - $results = Invoke-RestMethod -Method Delete -uri $url -Headers $headers - } - catch { - Write-Log -msg "Error_ $($_.Exception) creating repo $repoName in project $projectId" - } - return $results - -} - -function ConvertTo-Hashtable { - [CmdletBinding()] - [OutputType('hashtable')] - param ( - [Parameter(ValueFromPipeline)] - $InputObject - ) - - process { - ## Return null if the input is null. This can happen when calling the function - ## recursively and a property is null - if ($null -eq $InputObject) { - return $null - } - - ## Check if the input is an array or collection. If so, we also need to convert - ## those types into hash tables as well. This function will convert all child - ## objects into hash tables (if applicable) - if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { - $collection = @( - foreach ($object in $InputObject) { - ConvertTo-Hashtable -InputObject $object - } - ) - - ## Return the array but don't enumerate it because the object may be pretty complex - Write-Output -NoEnumerate $collection - } elseif ($InputObject -is [psobject]) { ## If the object has properties that need enumeration - ## Convert it to its own hash table and return it - $hash = @{} - foreach ($property in $InputObject.PSObject.Properties) { - $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value - } - $hash - } else { - ## If the object isn't an array, collection, or other object, it's already a hash table - ## So just return it. - $InputObject - } - } -} - -function ConvertTo-Object { - - begin { $object = New-Object Object } - - process { - - $_.GetEnumerator() | ForEach-Object { Add-Member -inputObject $object -memberType NoteProperty -name $_.Name -value $_.Value } - - } - - end { $object } - - } - -function Write-Log([string]$msg, [string]$logLevel = "INFO", $ForegroundColor = $null ) { - $currentColor = [System.Console]::ForegroundColor - $path = "$LogLocation\migration-$($logLevel.ToLower()).log" - $masterpath = "$LogLocation\migration.log" - - if ($null -eq $ForegroundColor) { - $ForegroundColor = $currentColor - } - - Write-Host $msg -ForegroundColor $ForegroundColor - - Write-Log-Async $msg $logLevel $path $true - Write-Log-Async $msg $logLevel $masterpath $true -} - -function Write-Log-Async -{ - param - ( - [Parameter(Mandatory = $true, - Position = 0)] - [ValidateNotNull()] - [string]$text, - [Parameter(Mandatory = $true, - Position = 1)] - [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')] - [string]$level, - [Parameter(Mandatory = $true, - Position = 2)] - [string]$log, - [Parameter(Position = 3)] - [boolean]$UseMutex - ) - - Write-Verbose "Log: $log" - $date = (get-date).ToString() - if (Test-Path $log) - { - if ((Get-Item $log).length -gt 5mb) - { - $filenamedate = get-date -Format 'MM-dd-yy hh.mm.ss' - $archivelog = ($log + '.' + $filenamedate + '.archive').Replace('/', '-') - copy-item $log -Destination $archivelog - Remove-Item $log -force - Write-Verbose "Rolled the log." - } - } - $line = "[$date] [$level] $text" - if ($UseMutex) - { - $LogMutex = New-Object System.Threading.Mutex($false, "LogMutex") - $LogMutex.WaitOne()|out-null - - $line | out-file -FilePath $log -Append - $LogMutex.ReleaseMutex()|out-null - } - else - { - $line | out-file -FilePath $log -Append - } -} - - diff --git a/admin-tools/AzureDevOps-ProjectHelpers.ps1 b/admin-tools/AzureDevOps-ProjectHelpers.ps1 deleted file mode 100644 index fb8783e..0000000 --- a/admin-tools/AzureDevOps-ProjectHelpers.ps1 +++ /dev/null @@ -1,419 +0,0 @@ -Param( - [string]$Organization = "IntelliTect-Samples", - [string]$PersonalAccessToken -) - -Import-Module .\AzureDevOps-Helpers.ps1 - -function Get-ServiceHooks([string]$projectSk, [string]$org, $headers) { - $url = "$org/_apis/hooks/subscriptionsquery?api-version=5.1" - - $body = @{ - "publisherInputFilters" = @( - @{ - "conditions" = @( - @{ - "inputId" = "projectId" - "inputValue" = $projectSk - } - ) - } - ) - } - $temp = $body | ConvertTo-Json -Depth 10 - - $results = Invoke-RestMethod -Method "POST" -uri $url -Headers $headers -Body $temp -ContentType "application/json" - - return , $results.results -} - -function Get-WorkItemCount([string]$projectSk, [string]$org, $headers) { - $analyticsOrg = $org.ToString().Replace("dev.azure.com", "analytics.dev.azure.com") - $url = "$analyticsOrg/$projectSk/_odata/v3.0-preview/WorkItems?`$apply=aggregate(`$count as Count, Revision with sum as TotalRevisions)" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value[0] - -} - -function Get-WorkItemLastChanged([string]$projectSk, [string]$org, $headers) { - ## $organization/_odata/v3.0-preview/WorkItems?`$apply=groupby((WorkItemType),aggregate(`$count as Count)) - $analyticsOrg = $org.ToString().Replace("dev.azure.com", "analytics.dev.azure.com") - $url = "$analyticsOrg/$projectSk/_odata/v3.0-preview/WorkItems?`$apply=aggregate(ChangedDate with max as LastChangedDate)" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - return $results.value[0].LastChangedDate - -} - -function Get-ReposWithLastCommit([string]$projectSk, [string]$org, $headers) { - $final = @() - $repos = Get-Repos -org $org -projectSk $projectSk -headers $headers - - foreach ($repo in $repos) { - $repoId = $repo.id - $url = "$org/$projectSk/_apis/git/repositories/$repoId/commits?api-version=5.1" - - $commits = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - if ($commits.count -gt 0) { - - $final += @{ - "id" = $repo.id - "name" = $repo.name - "sizeInBytes" = $repo.size - "sizeInMegaBytes" = [math]::Round($repo.size / 1024 / 1024, 6) - "projectName" = $repo.project.name - "projectId" = $repo.project.id - "lastCommit" = $commits.value[0].committer.date - } - } - else { - - $final += @{ - "id" = $repo.id - "name" = $repo.name - "sizeInBytes" = $repo.size - "sizeInMegaBytes" = [math]::Round($repo.size / 1024 / 1024, 6) - "projectName" = $repo.project.name - "projectId" = $repo.project.id - "lastCommit" = $null - } - } - - } - return $final -} -function Get-Repos([string]$projectSk, [string]$org, $headers) { - - # GET https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/commits?api-version=5.0 - $url = "$org/$projectSk/_apis/git/repositories?api-version=5.0" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - if ($ProcessName) { - return $results.value | Where-Object { $_.name -ieq $ProcessName } - } - else { - return , $results.value - } -} -function Get-Repo([string]$projectSk, [string]$org, $headers, $repoId) { - - $url = "$org/$projectSk/_apis/git/repositories/$repoId" - - return Invoke-RestMethod -Method Get -uri $url -Headers $headers -} -function Get-ProjectProperties([string]$projectSk, [string]$org, $headers) { - - # GET https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/commits?api-version=5.0 - $url = "$org/_apis/projects/$projectSk/properties?api-version=5.0" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - foreach ($pair in $results.value) { - - } -} - -function Get-Releases([string]$projectSk, [string]$org, $headers) { - $temporg = $org.ToString().Replace("dev.azure.com", "vsrm.dev.azure.com") - - $url = "$temporg/$projectSk/_apis/release/releases?api-version=5.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return , $results.value - -} - -function Get-ServiceEndpoints([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/serviceendpoint/endpoints" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return , $results.value - -} - -function Get-ServiceEndpoint([string]$projectSk, [string]$org, $headers, $serviceEndpointId) { - - $url = "$org/$projectSk/_apis/serviceendpoint/endpoints/$serviceEndpointId" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results - -} - -function New-ServiceEndpoint([string]$projectSk, [string]$org, $serviceEndpoint, $headers) { - - $url = "$org/$projectSk/_apis/serviceendpoint/endpoints?api-version=5.1-preview.2" - - $body = $serviceEndpoint | ConvertTo-Json - - $results = Invoke-RestMethod -ContentType "application/json" -Method Post -uri $url -Headers $headers -Body $body - - return $results - -} - -function New-ServiceHook([string]$projectSk, [string]$org, $serviceHook, $headers) { - - $url = "$org/_apis/hooks/subscriptions?api-version=5.1" - - $body = $serviceHook | ConvertTo-Json - - $results = Invoke-RestMethod -ContentType "application/json" -Method Post -uri $url -Headers $headers -Body $body - - return $results - -} - - -function Get-ReleaseDefinitions([string]$projectSk, [string]$org, $headers) { - $temporg = $org.ToString().Replace("dev.azure.com", "vsrm.dev.azure.com") - - $url = "$temporg/$projectSk/_apis/release/definitions?api-version=5.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return , $results.value - -} - - -function Get-LastBuildTime([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/build/builds?api-version=5.1&queryOrder=finishTimeDescending&`$top=1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - - if ($results.count -gt 0) { return $results.value[0].finishTime } else { return $null } - -} - - -function Get-LastReleaseTime([string]$projectSk, [string]$org, $headers) { - $temporg = $org.ToString().Replace("dev.azure.com", "vsrm.dev.azure.com") - - $url = "$temporg/$projectSk/_apis/release/releases?api-version=5.1&queryOrder=descending&`$top=1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - if ($results.count -gt 0) { return $results.value[0].createdOn } else { return $null } - -} - -function Get-Builds([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/build/builds?api-version=5.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return , $results.value - -} - - -function Get-BuildDefinitions([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/build/definitions?api-version=5.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results.value -} - - -function Get-VariableGroups([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/distributedtask/variablegroups?api-version=5.1-preview" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results.value -} - - -function Get-VariableGroup([string]$projectSk, [string]$org, $headers, $groupId) { - - $url = "$org/$projectSk/_apis/distributedtask/variablegroups/$groupId" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results -} - - -function New-VariableGroup([string]$projectSk, [string]$org, $headers, $group) { - - $url = "$org/$projectSk/_apis/distributedtask/variablegroups?api-version=5.1-preview.1" - - $body = $group | ConvertTo-Json - - $results = Invoke-RestMethod -Method Post -uri $url -Headers $headers -Body $body -ContentType "application/json" - - return $results -} - - -function Get-BuildQueues([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/distributedtask/queues?api-version=5.1-preview" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results.value -} - - -function New-BuildQueue([string]$projectSk, [string]$org, $headers, $queue) { - - $url = "$org/$projectSk/_apis/distributedtask/queues?api-version=5.1-preview&authorizePipelines=true" - - $body = $queue | ConvertTo-Json - - $results = Invoke-RestMethod -Method Post -uri $url -Headers $headers -Body $body -ContentType "application/json" - - return $results -} - - -function New-Policy([string]$projectSk, [string]$org, $headers, $policy) { - - $url = "$org/$projectSk/_apis/policy/configurations?api-version=5.0" - - $body = $policy | ConvertTo-Json -Depth 10 - - $results = Invoke-RestMethod -Method Post -uri $url -Headers $headers -Body $body -ContentType "application/json" - - return $results -} - - -function Get-BuildDefinition([string]$projectSk, [string]$org, $headers, $buildDefinitionId, $revision = $null) { - - $url = "$org/$projectSk/_apis/build/definitions/$buildDefinitionId`?api-version=5.1" - - if ($null -ne $revision) { - $url = "$url&revision=$revision" - } - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results -} - - -function Save-BuildDefinition([string]$projectSk, [string]$org, $headers, $buildDefinition, $revision = 1) { - - if ($revision -gt 1) { - - $url = "$org/$projectSk/_apis/build/definitions/$($buildDefinition.id)?api-version=5.0-preview.6" - - $buildDefinition.revision = $revision - 1 - - $body = $buildDefinition | ConvertTo-Json -Depth 20 - - $results = Invoke-RestMethod -Method Put -uri $url -Headers $headers -Body $body -ContentType "application/json" - - return $results - } - else { - - $url = "$org/$projectSk/_apis/build/definitions?api-version=5.0" - - $body = $buildDefinition | ConvertTo-Json -Depth 20 - - $body | Out-File -FilePath "builddef-$($buildDefinition.name)-POST.json" - try { - $results = Invoke-RestMethod -Method Post -uri $url -Headers $headers -Body $body -ContentType "application/json" - return $results - } catch { - Write-Error -Message $_ - throw $_ - } - } -} - - -function Get-Policies([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/policy/configurations?api-version=5.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return , $results.value - -} -function Get-TestPlans([string]$projectSk, [string]$org, $headers) { - - $url = "$org/$projectSk/_apis/testplan/plans?api-version=5.1-preview.1" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return , $results.value - -} - -function Get-Dashboards([string]$projectSk, [string]$org, [string]$team, $headers) { - - if ($team) { - $url = "$org/$projectSk/$team/_apis/dashboard/dashboards" - } - else { - $url = "$org/$projectSk/_apis/dashboard/dashboards" - } - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results -} - -function Get-DashboardDetails([string]$project, [string]$org, [string]$team, [string]$dashboardId, $headers) { - - $url = "$org/$project/$team/_apis/dashboard/dashboards/$dashboardId" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results - -} - -function Get-Teams([string]$projectSk, [string]$org, $headers) { - - $url = "$org/_apis/projects/$projectSk/teams?api-version=5.1&`$top=1000" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results.value -} - -function Get-AllTeams([string]$org, [string]$pat) { - - $url = "$org/_apis/teams?api-version=5.1-preview.3&`$top=1000" - $url - - try { - $results = Invoke-RestMethod -Method Get -uri $url -Headers (New-HTTPHeaders -pat $pat) - } - catch { - Write-Error "Error getting all teams for org $org : $_" - } - - return $results -} - -function Get-TeamMembers([string]$team, [string]$project, [string]$org, [string]$pat) { - $url = "$org/_apis/projects/$project/teams/$team/members?`$top=1000&api-version=5.1" - - try { - $results = Invoke-RestMethod -Method Get -uri $url -Headers (New-HTTPHeaders -pat $pat) - } - catch { - Write-Error "Error getting team members for team $team from $project in org $org : $_" - } - - return $results -} diff --git a/admin-tools/AzureDevOps-UserHelpers.ps1 b/admin-tools/AzureDevOps-UserHelpers.ps1 deleted file mode 100644 index 5809432..0000000 --- a/admin-tools/AzureDevOps-UserHelpers.ps1 +++ /dev/null @@ -1,82 +0,0 @@ -# https://docs.microsoft.com/en-us/rest/api/azure/devops/memberentitlementmanagement/user%20entitlements/add?view=azure-devops-rest-5.1 - -function Add-ADOUser([string]$pat, [string]$orgName, [string]$user, [string]$licenseType = "stakeholder", [string]$org) { - if ($org) { - $url = $org - } else { - $url = "https://vsaex.dev.azure.com/$orgName/_apis/userentitlements?api-version=5.1-preview.1" - } - $body = @{ - "accessLevel" = @{ - "accountLicenseType" = $licenseType - } - "user" = @{ - "principalName" = $user - "subjectKind" = "user" - } - } | ConvertTo-Json -Depth 3 - - try { - $result = Invoke-RestMethod -Uri $url -Method "Post" -Headers (New-HTTPHeaders -pat $pat) -Body $body - UseBasicParsing -ContentType 'application/json' - } - catch { - Write-Error "Error adding user $user to org $org : $_" - } - return $result -} - -function get-ADOUsers($pat, [string]$OrgName, [string]$org) { - if ($org) { - $url = $org - } else { - #todo OAuth token: https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops - $url = "https://vsaex.dev.azure.com/$orgName/_apis/userentitlements?api-version=5.1-preview.1" - } - $results = Invoke-RestMethod -Method Get -uri $url -Headers (New-HTTPHeaders -pat $pat) - return $results -} - -function Get-ADOUserEntitlements($pat, [string]$OrgName, [string]$org) { - #todo Service apis - # check if url is passed in - if ($org) { - $url = $org - } else { - # GET https://vsaex.dev.azure.com/{organization}/_apis/userentitlementsummary?api-version=5.1-preview.1 - $url = "https://vsaex.dev.azure.com/$orgName/_apis/userentitlements?api-version=5.1-preview.1" - } - $results = Invoke-RestMethod -Method Get -uri $url -Headers (New-HTTPHeaders -pat $pat) - return $results -} - - -function Get-ADOSecurityNamespaes($pat, [string]$org, [string]$namespace) { - if ($namespace) { - $url = "$org/_apis/securitynamespaces/$namespace?api-version=5.1" - } - else { - $url = "$org/_apis/securitynamespaces?api-version=5.1" - } - - $results = Invoke-RestMethod -Method Get -uri $url -Headers (New-HTTPHeaders -pat $pat) - if ($ProcessName) { - return $results.value | Where-Object {$_.name -ieq $ProcessName} - } - else { - return $results.value - } -} - -function Get-ADOAccessControlList([string]$namespaceId, [string]$org, [string]$pat) { - # POST https://dev.azure.com/{organization}/_apis/accesscontrollists/{securityNamespaceId}?api-version=5.1 - $url = "$org/_apis/accesscontrollists/"+$namespaceId+"?api-version=5.0" - - try { - $results = Invoke-RestMethod -Method Get -uri $url -Headers (New-HTTPHeaders -pat $pat) - } - catch { - Write-Error "Error getting ACL for $namespaceId : $_" - } - return $results -} - diff --git a/admin-tools/AzureDevOps-WorkItemHelpers.ps1 b/admin-tools/AzureDevOps-WorkItemHelpers.ps1 deleted file mode 100644 index 216f5f6..0000000 --- a/admin-tools/AzureDevOps-WorkItemHelpers.ps1 +++ /dev/null @@ -1,190 +0,0 @@ - -# todo: import process template -# https://docs.microsoft.com/en-us/rest/api/azure/devops/processadmin/processes/import%20process%20template?view=azure-devops-rest-5.1 - -# todo: replace client with InvokeRest -[Reflection.Assembly]::LoadWithPartialName('System.Net.Http') - -#todo help - -function init-HTTPClient([string]$pat) { - return "not needed, refactor to use Invoke-RestMethod" -} - -function Get-ADOWorkItemTypes([string]$ProcessId, [string]$WorkItemType, [string]$org) { - - # Work Items for Process - #GET https://dev.azure.com/{organization}/_apis/work/processes/{processId}/workitemtypes?api-version=5.0-preview.2 - - $url = "$org/_apis/work/processes/$ProcessId/workitemtypes?api-version=5.0-preview.2" - $results = $client.GetStringAsync($url) - $workItemTypes = ($results.Result | convertfrom-json).value - if ($WorkItemType) { - return $workItemTypes | Where-Object {$_.name -ieq $WorkItemType} - } - else { - return $workItemTypes - } -} - -function Add-ADOPicklist([string]$listFile, [string]$org) { - # doc https://docs.microsoft.com/en-us/rest/api/azure/devops/processes/lists/create?view=azure-devops-rest-5.1 - - # POST https://dev.azure.com/{organization}/_apis/work/processes/lists?api-version=5.1-preview.1 - $url = "$org/_apis/work/processes/lists?api-version=5.1-preview.1" - - $listjson = Get-Content $listFile -Raw - $sct = new-object System.Net.Http.StringContent($listjson, [System.Text.Encoding]::ASCII, 'application/json') - $results = $cl.PostAsync($url, $sct) - $results.Result - $results.Result.Content.ReadAsStringAsync().Result -} - -function Add-ADOLists([string]$workItemTypeName, [string]$org) { - - $listInputPath = Join-Path -Path $inputDir -ChildPath ($workItemTypeName + "_lists.csv") - $newLists = import-csv $listInputPath - - $newLists | ForEach-Object { - $listInputPath = Join-Path $inputDir -ChildPath ($_.refname + ".json") - Add-ADOPicklist -listFile $listInputPath - } -} - -function Get-ADOLists([string]$listName, [string]$org) { - - # GET https://dev.azure.com/{organization}/_apis/work/processdefinitions/lists?api-version=4.1-preview.1 - $url = "$org/_apis/work/processdefinitions/lists?api-version=4.1-preview.1" - $results = $client.GetStringAsync($url) - $lists = ($results.Result | convertfrom-json).value - if ($listName) { - return $lists | Where-Object {$_.name -ieq $listName} - } - else { - return $lists - } -} - -function Add-ADOProjectFields([string]$project, [string]$witRefName, [string]$csvFile, $lists, [string]$org) { - - $url = "$org/$project/_apis/wit/fields?api-version=5.1-preview.2" - $baseUrl = "$org/_apis/wit/fields/" - - $newFields = import-csv $csvFile - - $newFields | ForEach-Object { - - $list = $null - $isPickList = $false - if ($_.picklist) { - $pickListName = $_.picklist - $list = $lists | Where-Object {$_.name -ieq $pickListName} - $isPickList = $true - } - - $field = [PSCustomObject]@{ - _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 - supportedOperations = $null - type = $_.type - url = $baseUrl + $_.refname - usage = "workItem" - } - - $fieldjson = $field | convertto-json - $header = new-object System.Net.Http.StringContent($fieldjson, [System.Text.Encoding]::ASCII, 'application/json') - $results = $client.PostAsync($url, $header) - $results.Result - $results.Result.Content.ReadAsStringAsync().Result - } -} - -function Add-ADOFields([string]$processId, [string]$witRefName, [string]$csvFile, $lists, [string]$org) { - - # Add Field to Process Work Item Type - $url = "$org/_apis/work/processdefinitions/$processId/fields?api-version=4.1-preview.1" - $baseUrl = "$org/_apis/wit/fields/" - - $newFields = import-csv $csvFile - - $newFields | ForEach-Object { - - $picklist = $null - if ($_.picklist) { - $pickListName = $_.picklist - $list = $lists | Where-Object {$_.name -ieq $pickListName} - $picklist = [PSCustomObject]@{ - id = $list.id - isSuggested = $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 - } - - $fieldjson = $field | convertto-json - $header = new-object System.Net.Http.StringContent($fieldjson, [System.Text.Encoding]::ASCII, 'application/json') - $results = $client.PostAsync($url, $header) - $results.Result - $results.Result.Content.ReadAsStringAsync().Result - } -} - -function Get-ADOProjectFields([string]$projectName, [string]$org, [string]$pat) { - if (!($org)) { - $org = $defaultOrg - } - - $headers = (New-HTTPHeaders -pat $pat) - - $url = "$org/_apis/projects/"+$projectName+"?api-version=5.1" - try { - $project = Invoke-RestMethod -Method Get -uri $url -headers $headers - } - catch { - write-Error "Error getting project $projectname : $_" - return - } - - $fieldsUrl = "$org/"+$project.id+"/_apis/wit/fields?api-version=5.0-preview.2" - try { - $fields = Invoke-RestMethod -Method Get -uri $fieldsUrl -headers $headers - } - catch { - write-Error "Error getting project fields for $projectname : $_" - } - - return $fields -} - -function Delete-ADOWorkItemField([string]$fieldName, [string]$org, [string]$pat) { - #DELETE https://dev.azure.com/aiz-test/_apis/wit/fields/Custom.ARCID - try { - $results = Invoke-RestMethod -Method Delete -uri "$org/_apis/wit/fields/$fieldName" -headers (New-HTTPHeaders -pat $pat) - } - catch { - write-Error "Error getting project fields for $projectname : $_" - } - return $results -} diff --git a/admin-tools/migrateBuildDefinitions.ps1 b/admin-tools/migrateBuildDefinitions.ps1 deleted file mode 100644 index c39515c..0000000 --- a/admin-tools/migrateBuildDefinitions.ps1 +++ /dev/null @@ -1,174 +0,0 @@ -# TODO: TFVC based projects require some manual fix-up when it comes to mapping repository pathing - -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName, - [bool]$withHistory = $true, - - [string]$secretsMapPath = "" -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "------------------------" -Write-Log -msg "-- Migrate Build Defs --" -Write-Log -msg "------------------------" -Write-Log -msg " " - -function MigrateDefinition($def) { - - #$body | Out-File -FilePath "builddef-$($buildDefinition.name).json" - - $def.project.id = $targetProject.id - $def.project.url = $targetProject.url - $def.project.description = $def.project.description -replace ":", "" - - $def.queue = ($targetBuildQueues | Where-Object { $_.name -eq $def.queue.name }) - - if ($null -ne $def.repository -and $def.repository.id.Contains("http")) { - # It looks like it may be a repo based on a service endpoint.. lets try and map it up - Write-Log -msg "Mapping Repo via service endpoint .." - $sourceEndpoint = Get-ServiceEndpoint -headers $sourceHeaders -org $sourceOrg -serviceEndpointId $def.repository.properties.connectedServiceId -projectSk $sourceProject.id - $targetEndpoint = ($targetServiceEndpoints | Where-Object { $_.description.ToUpper().Contains("#ORIGINSERVICEENDPOINTID:$($sourceEndpoint.id.ToUpper())") }) - if ($null -ne $targetEndpoint) { - $def.repository.properties.connectedServiceId = $targetEndpoint.id - } - else { - throw "Failed to locate service endpoint repository [$($sourceRepo.name)] in target. " - } - } - elseif ($null -ne $def.repository ) { - if ("TfsVersionControl" -eq $def.repository.type) { - Write-Log -msg "Mapping TFVC Repo.." - $def.repository.name = $targetProject.name - $def.repository.url = $targetOrg - $def.repository.defaultBranch = $def.repository.defaultBranch -replace $sourceProject.name, "$($targetProject.name)/$($sourceProject.name)" - $def.repository.rootFolder = $def.repository.rootFolder -replace $sourceProject.name, "$($targetProject.name)/$($sourceProject.name)" - } - else { - Write-Log -msg "Mapping Git Repo.." - Write-Host ($def.repository | ConvertTo-Json -Depth 10) - $sourceRepo = Get-Repo -headers $sourceHeaders -org $sourceOrg -repoId $def.repository.id - $targetRepo = ($targetRepos | Where-Object { $_.name -ieq $sourceRepo.name }) - if ($null -ne $targetRepo) { - $def.repository.id = $targetRepo.id - $def.repository.url = $targetRepo.url - } - else { - throw "Failed to locate repository [$($sourceRepo.name)] in target. " - } - } - } - Write-Log -msg "Matching Variable Groups.." - if ($null -ne $def.variableGroups) { - foreach ($varGroup in $def.variableGroups) { - $targetVG = $targetVariableGroups | Where-Object { $_.name -ieq $varGroup.name } - if ($null -ne $targetVG) { - $varGroup.id = $targetVG.id - } - else { - throw "Failed to locate variable group [$($varGroup.name)] in target. " - } - } - } - - Write-Log -msg "Mapping Variables.." - if ($null -ne $def.variables) { - foreach ($var in $def.variables.psobject.properties) { - if ($true -eq $var.Value.isSecret) { - if ($null -ne $secretsMap.buildDefinitions -and - $null -ne $secretsMap.buildDefinitions[$def.name] -and - $null -ne $secretsMap.buildDefinitions[$def.name][$var.Name]) { - $var.Value.value = $secretsMap.buildDefinitions[$def.name][$var.Name] - } - else { - throw "Secrets mapping for BuildDefinition - $($def.name) is missing or doesn`'t contain required [$($var.Name)] field." - } - } - } - } - return Save-BuildDefinition -headers $targetHeaders -projectSk $targetProject.id -org $targetOrg -revision $def.revision -buildDefinition $def -} - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$sourceProject = Get-ADOProjects -org $sourceOrg -Headers $sourceHeaders -ProjectName $SourceProjectName -$targetProject = Get-ADOProjects -org $targetOrg -Headers $targetHeaders -ProjectName $targetProjectName - -$buildDefinitions = Get-BuildDefinitions -projectSk $SourceProject.id -org $SourceOrg -headers $sourceHeaders -$targetBuildDefinitions = Get-BuildDefinitions -projectSk $TargetProject.id -org $TargetOrg -headers $targetHeaders - -if ($secretsMapPath -ne "") { - $secretsMap = ((Get-Content -Raw -Path $secretsMapPath) | ConvertFrom-Json) | ConvertTo-HashTable - Write-Log -msg "Loaded secrets map from $secretsMapPath" -} -else { - $secretsMap = @{ - serviceHooks = @{ - webHooks = @{} - jenkins = @{} - } - } - Write-Log -msg "Loaded default secrets map" -} - -$targetRepos = Get-Repos -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg -$targetVariableGroups = Get-VariableGroups -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg -$targetBuildQueues = Get-BuildQueues -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg -$targetServiceEndpoints = Get-ServiceEndpoints -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg - -Write-Log -msg "Located $($buildDefinitions.Count) build defs in source." - -foreach ($buildDefinition in $buildDefinitions) { - - if ($null -ne ($targetBuildDefinitions | Where-Object {$_.name -ieq $buildDefinition.name})) { - Write-Log -msg "Build definition [$($buildDefinition.name)] already exists in target.. " -NoNewline - continue - } - - Write-Log -msg "Attempting to create $($buildDefinition.name) in target.. " - - try { - if ($true -eq $withHistory) { - Write-Log -msg "Found $($buildDefinition.revision) revisions for $($buildDefinition.name)." - $newId = 0 - $newUrl = "" - for ($rev = 1; $rev -le $buildDefinition.revision; $rev++) { - $def = Get-BuildDefinition -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders -buildDefinitionId $buildDefinition.id -revision $rev - if ($rev -gt 1) { - $def.id = $newId - $def.url = $newUrl - $def.uri = "vstfs:///Build/Definition/$($newId)" - } - Write-Log -msg "Migrating revision $rev for $($def.name).. " - $saved = MigrateDefinition($def) - if ($rev -eq 1) { - $newId = $saved.id - $newUrl = $saved.url - } - } - } - else { - $def = Get-BuildDefinition -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders -buildDefinitionId $buildDefinition.id - Write-Log -msg "Migrating $($def.name).. " - $def.revision = 1 - $saved = MigrateDefinition($def) - } - Write-Log "Done migrating $($buildDefinition.name)." -ForegroundColor "Green" - } - catch { - Write-Log -msg ($_.Exception | Format-List -Force | Out-String) -logLevel "ERROR" - Write-Log -msg ($_.InvocationInfo | Format-List -Force | Out-String) -logLevel "ERROR" - throw - } -} -Write-Log "Done migrating build definitions." -ForegroundColor "Green" - diff --git a/admin-tools/migrateBuildQueues.ps1 b/admin-tools/migrateBuildQueues.ps1 deleted file mode 100644 index 5c6b72f..0000000 --- a/admin-tools/migrateBuildQueues.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "--------------------------" -Write-Log -msg "-- Migrate Build Queues --" -Write-Log -msg "--------------------------" -Write-Log -msg " " - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$sourceProject = Get-ADOProjects -org $sourceOrg -Headers $sourceHeaders -ProjectName $sourceProjectName -$targetProject = Get-ADOProjects -org $targetOrg -Headers $targetHeaders -ProjectName $targetProjectName - -$queues = Get-BuildQueues -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders -$targetQueues = Get-BuildQueues -projectSk $targetProject.id -org $TargetOrg -headers $targetHeaders - -foreach ($queue in $queues) { - if ($queue.pool.isHosted -or $queue.pool.name -eq "Default") { - continue - } - - if ($null -ne ($targetQueues | Where-Object {$_.name -ieq $queue.name})) { - Write-Log -msg "Build queue [$($queue.name)] already exists in target.. " -NoNewline - continue - } - - Write-Log -msg "Attempting to create [$($queue.name)] in target.. " -NoNewline - try { - New-BuildQueue -headers $targetHeaders -projectSk $targetProject.id -org $targetOrg -queue @{ - "projectId" = $queue.projectId - "name" = $queue.name - "id" = $queue.id - } - Write-Log -msg "Done!" -ForegroundColor "Green" - } - catch { - Write-Log -msg "FAILED!" -ForegroundColor "Red" - Write-Log -msg ($_ | ConvertFrom-Json).message -ForegroundColor "Red" - } -} diff --git a/admin-tools/migrateDashboards.ps1 b/admin-tools/migrateDashboards.ps1 deleted file mode 100644 index 755900b..0000000 --- a/admin-tools/migrateDashboards.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "------------------------" -Write-Log -msg "-- Migrate Dashboards --" -Write-Log -msg "------------------------" -Write-Log -msg " " - -set-alias CopyDashboard "C:\wrk\ups\azure-devops-utils\CopyDashboard\CopyDashboard\bin\Debug\netcoreapp3.1\CopyDashboard.exe" - -# get list of dashboards and teams from the source project -# Excute the copy dashboard command for each - -$sourceHeaders = New-HTTPHeaders -pat $SourcePat -$targetHeaders = New-HTTPHeaders -pat $TargetPat - -$teams = [array](Get-Teams -projectSk $sourceProjectName -org $SourceOrg -headers $sourceHeaders) - -ForEach ($team in $teams) { - Write-Log -msg "--- Dashboard: ---" - $dashboardResults = Get-Dashboards -projectSK $sourceProjectName -org $sourceOrg -team $team.name -headers $sourceHeaders - if ($sourceOrg.Contains("tfs")) { - $dashboards = $dashboardResults.dashboardEntries; - } - else { - $dashboards = $dashboardResults.value; - } - ForEach ($dashboard in $dashboards) { - Write-Log -msg "team: $($team.name) dashboard: $($dashboard.name) scope: copy$($dashboard.dashboardScope)" - #todo don't assume 1 - $targetDashboards = Get-Dashboards -projectSK $targetProjectName -org $targetOrg -team $team.name -headers $targetHeaders - ForEach ($targetDashboard in $targetDashboards.value) { - CopyDashboard --org $sourceOrg --pat $sourcePat --source-project "$sourceProjectName" --target-org $targetOrg ` - --target-pat $targetPat --target-project "$targetProjectName" --source-team "$($team.name)" --target-team "$($team.name)" ` - --source-dashboard "$($dashboard.name)" --target-dashboard "$($dashboard.name)" --target-dashboard-id $targetDashboard.id - } - } -} - - - diff --git a/admin-tools/migratePolicies.ps1 b/admin-tools/migratePolicies.ps1 deleted file mode 100644 index ed4b9ec..0000000 --- a/admin-tools/migratePolicies.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -# TODO: Need to add requiredReviewerIds mapping as seen below -# "settings": { -# >>> "requiredReviewerIds": ["7fbd9aa3-570f-41d1-bac5-3208840abc20", "d35c67af-9765-473b-804d-d80d6f0ab4f6", "e708e037-da6f-4bb8-8e41-2816cbf1d404", "cb58f60d-4667-4efe-b64e-9b15b6896bea"], -# "scope": [{ -# "refName": "refs/heads/develop", -# "matchKind": "Exact", -# "repositoryId": "87109ee6-6b78-4295-8204-f94a46bffa2e" -# }] -# }, - -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "----------------------" -Write-Log -msg "-- Migrate Policies --" -Write-Log -msg "----------------------" -Write-Log -msg " " - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$sourceProject = Get-ADOProjects -org $sourceOrg -Headers $sourceHeaders -ProjectName $sourceProjectName -$targetProject = Get-ADOProjects -org $targetOrg -Headers $targetHeaders -ProjectName $targetProjectName - -$policies = Get-Policies -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders -$targetRepos = Get-Repos -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg - -Write-Log -msg "Found $($policies.Count) policies in source.. " -NoNewline - -foreach ($policy in $policies) { - Write-Log -msg "Attempting to create [$($policy.id)] in target.. " -NoNewline - try { - - foreach ($entry in $policy.settings.scope) { - if ($null -ne $entry.repositoryId) { - $sourceRepo = Get-Repo -headers $sourceHeaders -org $sourceOrg -repoId $entry.repositoryId - if ($null -eq $sourceRepo) { - Write-Error "Could not find $($entry.repositoryId) in source while attempting to migrate policy." -ErrorAction SilentlyContinue - } - $targetRepo = ($targetRepos | Where-Object { $_.name -ieq $sourceRepo.name }) - if ($null -eq $sourceRepo) { - Write-Error "Could not find $($entry.repositoryId) in target while attempting to migrate policy." -ErrorAction SilentlyContinue - } - $entry.repositoryId = $targetRepo.id - } - } - - New-Policy -headers $targetHeaders -projectSk $targetProject.id -org $targetOrg -policy @{ - "isEnabled" = $policy.isEnabled - "isBlocking" = $policy.isBlocking - "isDeleted" = $policy.isDeleted - "settings" = $policy.settings - "type" = @{ id = $policy.type.id } - } - Write-Log -msg "Done!" -ForegroundColor "Green" - } - catch { - Write-Log -msg "FAILED!" -ForegroundColor "Red" - Write-Log -msg $_ -ForegroundColor "Red" - } -} diff --git a/admin-tools/migrateProject.ps1 b/admin-tools/migrateProject.ps1 deleted file mode 100644 index c439624..0000000 --- a/admin-tools/migrateProject.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetOrgName = $targetOrgName, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName, - - [string]$secretsMapPath = "", - [string]$witConfigPath = "", - [string]$WorkingDir = "$((Get-Location).Path)\_work" -) - -. .\AzureDevOps-Helpers.ps1 - -Write-Log -msg " " -Write-Log -msg "---------------------" -Write-Log -msg "-- Migrate Project --" -Write-Log -msg "---------------------" -Write-Log -msg " " - -(New-Item -Path $WorkingDir -ItemType Directory -Force) | Out-Null - -Write-Log -msg "Starting migration of $sourceOrg/$sourceProjectName to $targetOrg/$targetProjectName." - -# .\migrateWorkItems.ps1 -# .\migrateTeamMembers.ps1 -.\migrateBuildQueues.ps1 -$repos = .\clonerepos.ps1 -.\pushrepos.ps1 -repos $repos -.\migrateServiceHooks.ps1 -.\migrateServiceEndpoints.ps1 -.\migrateVariableGroups.ps1 -.\migratePolicies.ps1 -.\migrateDashboards.ps1 -.\migrateBuildDefinitions.ps1 -# .\migrateReleaseDefinitions.ps1 diff --git a/admin-tools/migrateServiceEndpoints.ps1 b/admin-tools/migrateServiceEndpoints.ps1 deleted file mode 100644 index 4aab069..0000000 --- a/admin-tools/migrateServiceEndpoints.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -Param( - - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "-------------------------------" -Write-Log -msg "-- Migrate Service Endpoints --" -Write-Log -msg "-------------------------------" -Write-Log -msg " " - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$sourceProject = Get-ADOProjects -org $sourceOrg -Headers $sourceHeaders -ProjectName $sourceProjectName -$targetProject = Get-ADOProjects -org $targetOrg -Headers $targetHeaders -ProjectName $targetProjectName - -$endpoints = Get-ServiceEndpoints -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders - -$targetEndpoints = Get-ServiceEndpoints -projectSk $TargetProject.id -org $SourceOrg -headers $sourceHeaders - -#$endpoints | ConvertTo-Json -Depth 10 | Out-File -FilePath "DEBUG_endpoints.json" - -foreach ($endpoint in $endpoints) { - - if ($null -ne ($targetEndpoints | Where-Object {$_.description.ToUpper().Contains("#ORIGINSERVICEENDPOINTID:$($endpoint.id.ToUpper())")})) { - Write-Log -msg "Service endpoint [$($endpoint.id)] already exists in target.. " -NoNewline - continue - } - - Write-Log -msg "Attempting to create [$($endpoint.name)] in target.. " -NoNewline - - $data = @{ - "data" = $endpoint.data - "name" = $endpoint.name - "type" = $endpoint.type - "url" = $endpoint.url - "authorization" = $endpoint.authorization - "description" = "$($endpoint.description) #OriginServiceEndpointId:$($endpoint.id)" - "isReady" = $endpoint.isReady - } - - try { - New-ServiceEndpoint -headers $targetHeaders -projectSk $targetProject.id -org $targetOrg -serviceEndpoint $data - Write-Log -msg "Done!" -ForegroundColor "Green" - } - catch { - Write-Log -msg "FAILED!" -ForegroundColor "Red" - Write-Log -msg ($_ | ConvertFrom-Json).message -ForegroundColor "Red" - } -} diff --git a/admin-tools/migrateServiceHooks.ps1 b/admin-tools/migrateServiceHooks.ps1 deleted file mode 100644 index 581fed6..0000000 --- a/admin-tools/migrateServiceHooks.ps1 +++ /dev/null @@ -1,141 +0,0 @@ -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName, - - [string]$consumer = $null, - - [string]$secretsMapPath = "" -) -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "---------------------------" -Write-Log -msg "-- Migrate Service Hooks --" -Write-Log -msg "---------------------------" -Write-Log -msg " " - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$sourceProject = Get-ADOProjects -org $sourceOrg -Headers $sourceHeaders -ProjectName $sourceProjectName -$targetProject = Get-ADOProjects -org $targetOrg -Headers $targetHeaders -ProjectName $targetProjectName - -$targetRepos = Get-Repos -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg - -$maskedValue = "********" - -if ($secretsMapPath -ne "") { - $secretsMap = ((Get-Content -Raw -Path $secretsMapPath) | ConvertFrom-Json) | ConvertTo-HashTable - Write-Log -msg "Loaded secrets map from $secretsMapPath" -} -else { - $secretsMap = @{ - serviceHooks = @{ - webHooks = @{} - jenkins = @{} - } - } - Write-Log -msg "Loaded default secrets map" -} - -$hooks = Get-ServiceHooks -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders - -Write-Log -msg "Located $($hooks.Count) in source." - -$hooks | ConvertTo-Json -depth 10 | Out-File -FilePath "hooks.json" - -foreach ($hook in $hooks) { - - if ($null -ne $consumer -and $consumer -ne $hook.consumerId) { - #continue - } - - Write-Log -msg "Attempting to create [$($hook.id)] in target.. " -NoNewline - try { - if ($null -ne $hook.publisherInputs -and $null -ne $hook.publisherInputs.repository) { - $hook.publisherInputs.projectId = $targetProject.id - - $sourceRepo = Get-Repo -headers $sourceHeaders -org $sourceOrg -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 - if ($null -ne $hook.publisherInputs.repository -and "" -ne $hook.publisherInputs.repository) { - $targetRepo = ($targetRepos | Where-Object { $_.name -ieq $sourceRepo.name }) - if ($null -ne $targetRepo) { - $hook.publisherInputs.repository = $targetRepo.id - } - else { - throw "Failed to locate repository [$($sourceRepo.name)] in target. " - } - } - } - - if ($hook.consumerId -eq "webHooks") { - if ($null -ne $hook.consumerInputs) { - if ($maskedValue -eq $hook.consumerInputs.basicAuthPassword) { - # Check hook secrets mapping for this hook's "basicAuthPassword" - if ($null -eq $secretsMap.serviceHooks.webHooks[$hook.consumerInputs.url] -or - $null -eq $secretsMap.serviceHooks.webHooks[$hook.consumerInputs.url].basicAuthPassword) { - throw "Secrets mapping for WebHook - $($hook.consumerInputs.url) is missing or doesn't contain required 'basicAuthPassword' field." - } - else { - $hook.consumerInputs.basicAuthPassword = $secretsMap.serviceHooks.webhooks[$hook.consumerInputs.url].basicAuthPassword - } - } - } - } - - if ($hook.consumerId -eq "jenkins") { - if ($null -ne $hook.consumerInputs) { - if ($maskedValue -eq $hook.consumerInputs.password) { - # Check hook secrets mapping for this hook's "password" - if ($null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl] -or - $null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].password) { - throw "Secrets mapping for Jenkins - $($hook.consumerInputs.serverBaseUrl) is missing or doesn't contain required 'password' field." - } - else { - $hook.consumerInputs.password = $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].password - } - } - if ($maskedValue -eq $hook.consumerInputs.buildAuthToken) { - # Check hook secrets mapping for this hook's "buildAuthToken" - if ($null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl] -or - $null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].buildAuthToken) { - throw "Secrets mapping for Jenkins - $($hook.consumerInputs.url) is missing or doesn't contain required 'buildAuthToken' field." - } - else { - $hook.consumerInputs.buildAuthToken = $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].buildAuthToken - } - } - } - } - - New-ServiceHook -headers $targetHeaders -projectSk $targetProject.id -org $targetOrg -serviceHook @{ - "publisherId" = $hook.publisherId - "eventType" = $hook.eventType - "resourceVersion" = $hook.resourceVersion - "consumerId" = $hook.consumerId - "consumerActionId" = $hook.consumerActionId - "publisherInputs" = $hook.publisherInputs - "consumerInputs" = $hook.consumerInputs - "status" = $hook.status - } - - Write-Log -msg "Done!" -ForegroundColor "Green" - } - catch { - Write-Log -logLevel "ERROR" -msg "FAILED!" -ForegroundColor "Red" - Write-Log -logLevel "ERROR" -msg $_.Exception -ForegroundColor "Red" - try { - Write-Log -logLevel "ERROR" -msg ($_ | ConvertFrom-Json).message -ForegroundColor "Red" - } - catch { - - } - } -} diff --git a/admin-tools/migrateTeamMembers.ps1 b/admin-tools/migrateTeamMembers.ps1 deleted file mode 100644 index b355590..0000000 --- a/admin-tools/migrateTeamMembers.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -Param( - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName -) - -#-sourcePat $sourcePat -sourceOrg $sourceOrg -sourceProjectName "SampleCRM" -targetPat $targetPat - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "---------------------------" -Write-Log -msg "-- Migrate Team Members --" -Write-Log -msg "---------------------------" -Write-Log -msg " " - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$teams = Get-Teams -project $SourceProjectName -org $SourceOrg -headers $sourceHeaders - -Class TeamMember { - [string]$teamName - [string]$teamId - [string]$userId - [string]$userName - [string]$userDescriptor - [bool]$isTeamAdmin -} -$teamMembers = @() - -# $teamMembers = @() - -$teams | ForEach-Object { - $teamName = $_.name - $teamId = $_.id - Write-Host "Team: $teamName" - - $result = Get-TeamMembers -team $teamName -project $SourceProjectName -org $SourceOrg -pat $sourcePat - if ($result) { - $result.value | foreach-object { - $teamMember = [TeamMember]@{ - teamName = $teamName; - teamId = $teamId; - userId = $_.identity.id; - userName = $_.identity.uniqueName - isTeamAdmin = $_.isTeamAdmin - } - $teamMembers += $teamMember - } - } -} - -# for now just return team members -return $teamMembers - -#todo lookup AD ID for Azure Identity for each unique team member -#todo check user provisioning (basic, VS, ...) -#todo add unique team member to the target -#todo add members to teams - - - - diff --git a/admin-tools/migrateVariableGroups.ps1 b/admin-tools/migrateVariableGroups.ps1 deleted file mode 100644 index ddb6da6..0000000 --- a/admin-tools/migrateVariableGroups.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -Param( - - [string]$TargetOrg = $targetOrg, - [string]$TargetProjectName = $targetProjectName, - [string]$TargetPat = $targetPat, - - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName, - - [string]$secretsMapPath = "" -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "-----------------------------" -Write-Log -msg "-- Migrate Variable Groups --" -Write-Log -msg "-----------------------------" -Write-Log -msg " " - -$sourceHeaders = New-HTTPHeaders -pat $sourcePat -$targetHeaders = New-HTTPHeaders -pat $targetPat - -$sourceProject = Get-ADOProjects -org $sourceOrg -Headers $sourceHeaders -ProjectName $sourceProjectName -$targetProject = Get-ADOProjects -org $targetOrg -Headers $targetHeaders -ProjectName $targetProjectName - -$targetVariableGroups = Get-VariableGroups -projectSk $targetProject.id -headers $targetHeaders -org $targetOrg - -$maskedValue = "********" - -if ($secretsMapPath -ne "") { - $secretsMap = ((Get-Content -Raw -Path $secretsMapPath) | ConvertFrom-Json) | ConvertTo-HashTable - Write-Log -msg "Loaded secrets map from $secretsMapPath" -} -else { - $secretsMap = @{ - serviceHooks = @{ - webHooks = @{} - jenkins = @{} - } - } - Write-Log -msg "Loaded default secrets map" -} - -$groups = Get-VariableGroups -projectSk $sourceProject.id -org $SourceOrg -headers $sourceHeaders - -foreach ($groupHeader in $groups) { - - if ($null -ne ($targetVariableGroups | Where-Object {$_.name -ieq $groupHeader.name})) { - Write-Log -msg "Variable group [$($groupHeader.name)] already exists in target.. " -NoNewline - continue - } - - Write-Log -msg "Attempting to create [$($groupHeader.name)] in target.. " -NoNewline - try { - - $groupObj = (Get-VariableGroup -projectSk $sourceProject.id -org $SourceOrg -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] - } - } - } - - 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 - #$ref.description = $groupHeader.description - $ref.projectReference.id = $targetProject.id - $ref.projectReference.name = $targetProject.name - } - - New-VariableGroup -headers $targetHeaders -projectSk $targetProject.id -org $targetOrg -group @{ - #"description" = $groupHeader.description - "name" = $group.name - "type" = $group.type - "providerData" = $group.providerData - "variables" = $group.variables - "variableGroupProjectReferences" = $groupObj.variableGroupProjectReferences - } - Write-Log -msg "Done!" -ForegroundColor "Green" - } - catch { - Write-Error ($_.Exception | Format-List -Force | Out-String) -ErrorAction Continue - Write-Error ($_.InvocationInfo | Format-List -Force | Out-String) -ErrorAction Continue - } -} diff --git a/configuration/README - Configuration.md b/configuration/README - Configuration.md new file mode 100644 index 0000000..091d6c7 --- /dev/null +++ b/configuration/README - Configuration.md @@ -0,0 +1,57 @@ + +# "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. +---------- +
+ +**VSTS Only** means that the configuration property is only required if you are using the VSTS work item migrator. + + + +## base-configuration.json ([VSTS only](https://github.com/microsoft/vsts-work-item-migrator)) +#### (If using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator)): +A pre-configured configuration file used by the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator). +Read more about [base-configuration here](https://github.com/microsoft/vsts-work-item-migrator/blob/master/WiMigrator/migration-configuration.md) +

+ +## migrator-configuration.json + +The migrator-configuration.json file is use in conjunction with the Azure DevOps Migration Tools (aka. Martin's Tool). This tool is used to migration Work-Items and other areas of data. When running a full project migration using the PowerShell script MigrateProject.ps1, this file is automatically configured for you based on your selections of area or migration. You can configure this file manually and run the tool separately from the MigrateProject.ps1 tool if deisred. + + diff --git a/migration-scripts/base-configuration.json b/configuration/base-configuration.json similarity index 97% rename from migration-scripts/base-configuration.json rename to configuration/base-configuration.json index b96bde2..4a76936 100644 --- a/migration-scripts/base-configuration.json +++ b/configuration/base-configuration.json @@ -1,54 +1,54 @@ -{ - "source-connection": { - "account": "", - "project": "", - "access-token": "", - "use-integrated-auth": "false" - }, - "target-connection": { - "account": "", - "project": "", - "access-token": "", - "use-integrated-auth": "false" - }, - "query": "My Queries/All Items", - "heartbeat-frequency-in-seconds": 30, - "query-page-size": 20000, - "parallelism": 1, - "max-attachment-size": 62914560, - "link-parallelism": 1, - "attachment-upload-chunk-size": 1048576, - "skip-existing": false, - "move-history": false, - "move-history-limit": 200, - "move-git-links": false, - "move-attachments": false, - "move-links": true, - "source-post-move-tag": "", - "target-post-move-tag": "", - "skip-work-items-with-type-missing-fields": false, - "skip-work-items-with-missing-area-path": false, - "skip-work-items-with-missing-iteration-path": false, - "default-area-path": "contoso-project\\missing area path", - "default-iteration-path": "contoso-project\\missing iteration path", - "clear-identity-display-names": false, - "ensure-identities": false, - "include-web-link": true, - "log-level-for-file": "information", - "field-replacements": { - - }, - "send-email-notification": false, - "email-notification": { - "smtp-server": "127.0.0.1", - "use-ssl": false, - "port": "25", - "from-address": "wimigrator@example.com", - "user-name": "un", - "password": "pw", - "recipient-addresses": [ - "test1@test.com", - "test2@test.com" - ] - } -} +{ + "source-connection": { + "account": "", + "project": "", + "access-token": "", + "use-integrated-auth": "false" + }, + "target-connection": { + "account": "", + "project": "", + "access-token": "", + "use-integrated-auth": "false" + }, + "query": "My Queries/All Items", + "heartbeat-frequency-in-seconds": 30, + "query-page-size": 20000, + "parallelism": 1, + "max-attachment-size": 62914560, + "link-parallelism": 1, + "attachment-upload-chunk-size": 1048576, + "skip-existing": false, + "move-history": false, + "move-history-limit": 200, + "move-git-links": false, + "move-attachments": false, + "move-links": true, + "source-post-move-tag": "", + "target-post-move-tag": "", + "skip-work-items-with-type-missing-fields": false, + "skip-work-items-with-missing-area-path": false, + "skip-work-items-with-missing-iteration-path": false, + "default-area-path": "contoso-project\\missing area path", + "default-iteration-path": "contoso-project\\missing iteration path", + "clear-identity-display-names": false, + "ensure-identities": false, + "include-web-link": true, + "log-level-for-file": "information", + "field-replacements": { + + }, + "send-email-notification": false, + "email-notification": { + "smtp-server": "127.0.0.1", + "use-ssl": false, + "port": "25", + "from-address": "wimigrator@example.com", + "user-name": "un", + "password": "pw", + "recipient-addresses": [ + "test1@test.com", + "test2@test.com" + ] + } +} diff --git a/configuration/configuration.json b/configuration/configuration.json new file mode 100644 index 0000000..15a7586 --- /dev/null +++ b/configuration/configuration.json @@ -0,0 +1,17 @@ +{ + "SourceProject": { + "Organization": "https://dev.azure.com/AIZ-GL/", + "ProjectName": "GL.CL-Elita", + "OrgName": "AIZ-GL" + }, + "TargetProject": { + "Organization": "https://dev.azure.com/AIZ-Global/", + "ProjectName": "GL.CL-Elita-migrated", + "OrgName": "AIZ-Global" + }, + "ProjectDirectory": "C:\\Users\\JohnEvans\\Working\\Assurant AIZ-GT- ADO Migration Tools\\DevOps-Enablement-ADO-migration", + "WorkItemMigratorDirectory": "C:\\tools\\MigrationTool", + "RepositoryCloneTempDirectory": "C:\\tools\\RepoCloneTemp", + "DevOpsMigrationToolConfigurationFile": "migrator-configuration.json", + "ArtifactFeedPackageVersionLimit": 5 +} \ No newline at end of file diff --git a/configuration/migrator-configuration.json b/configuration/migrator-configuration.json new file mode 100644 index 0000000..c85cfbd --- /dev/null +++ b/configuration/migrator-configuration.json @@ -0,0 +1,377 @@ +{ + "Version": "0.0", + "LogLevel": "Verbose", + "MappingTools": [ + { + "$type": "WorkItemTypeMappingTool", + "Enabled": true, + "WorkItemTypeDefinition": { + "sourceWorkItemTypeName": "targetWorkItemTypeName" + } + }, + { + "$type": "WorkItemFieldMappingTool", + "Enabled": true, + "WorkItemFieldMaps": [ + { + "$type": "MultiValueConditionalMapConfig", + "WorkItemTypeName": "*", + "sourceFieldsAndValues": { + "Field1": "Value1", + "Field2": "Value2" + }, + "targetFieldsAndValues": { + "Field1": "Value1", + "Field2": "Value2" + } + }, + { + "$type": "FieldSkipMapConfig", + "WorkItemTypeName": "*", + "targetField": "TfsMigrationTool.ReflectedWorkItemId" + }, + { + "$type": "FieldValueMapConfig", + "WorkItemTypeName": "*", + "sourceField": "System.State", + "targetField": "System.State", + "defaultValue": "New", + "valueMapping": { + "Approved": "New", + "New": "New", + "Committed": "Active", + "In Progress": "Active", + "To Do": "New", + "Done": "Closed", + "Removed": "Removed" + } + }, + { + "$type": "FieldtoFieldMapConfig", + "WorkItemTypeName": "*", + "sourceField": "Microsoft.VSTS.Common.BacklogPriority", + "targetField": "Microsoft.VSTS.Common.StackRank" + }, + { + "$type": "FieldtoFieldMultiMapConfig", + "WorkItemTypeName": "*", + "SourceToTargetMappings": { + "SourceField1": "TargetField1", + "SourceField2": "TargetField2" + } + }, + { + "$type": "FieldtoTagMapConfig", + "WorkItemTypeName": "*", + "sourceField": "System.State", + "formatExpression": "ScrumState:{0}" + }, + { + "$type": "FieldMergeMapConfig", + "WorkItemTypeName": "*", + "sourceField1": "System.Description", + "sourceField2": "Microsoft.VSTS.Common.AcceptanceCriteria", + "targetField": "System.Description", + "formatExpression": "{0}

Acceptance Criteria

{1}", + "doneMatch": "##DONE##" + }, + { + "$type": "RegexFieldMapConfig", + "WorkItemTypeName": "*", + "sourceField": "COMPANY.PRODUCT.Release", + "targetField": "COMPANY.DEVISION.MinorReleaseVersion", + "pattern": "PRODUCT \\d{4}.(\\d{1})", + "replacement": "$1" + }, + { + "$type": "FieldValuetoTagMapConfig", + "WorkItemTypeName": "*", + "sourceField": "Microsoft.VSTS.CMMI.Blocked", + "pattern": "Yes", + "formatExpression": "{0}" + }, + { + "$type": "TreeToTagMapConfig", + "WorkItemTypeName": "*", + "toSkip": 3, + "timeTravel": 1 + } + ] + }, + { + "$type": "WorkItemGitRepoMappingTool", + "Enabled": true, + "WorkItemGitRepos": { + "sourceRepoName": "targetRepoName" + } + } + ], + "Endpoints": { + "TfsWorkItemEndpoints": [ + { + "Name": "WorkItemSource", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "Query": { + "Query": "SELECT [System.Id], [System.Tags] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan') ORDER BY [System.ChangedDate] desc", + "Parameters": { + "TeamProject": "GL.CL-Elita" + } + }, + "Organisation": "https://dev.azure.com/AIZ-GL/", + "Project": "GL.CL-Elita", + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", + "AllowCrossProjectLinking": false, + "PersonalAccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "LanguageMaps": { + "AreaPath": "Area", + "IterationPath": "Iteration" + } + }, + { + "Name": "WorkItemTarget", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "Query": { + "Query": "SELECT [System.Id], [System.Tags] FROM WorkItems WHERE [System.TeamProject] = @TeamProject AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan') ORDER BY [System.ChangedDate] desc" + }, + "Organisation": "https://dev.azure.com/AIZ-Global/", + "Project": "GL.CL-Elita-migrated", + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", + "AllowCrossProjectLinking": false, + "PersonalAccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "LanguageMaps": { + "AreaPath": "Area", + "IterationPath": "Iteration" + } + } + ], + "TfsTeamSettingsEndpoints": [ + { + "$type": "TfsTeamSettingsEndpointOptions", + "Name": "TeamSettingsSource", + "Direction": "Source", + "Organisation": "https://dev.azure.com/AIZ-GL/", + "Project": "GL.CL-Elita", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "ReflectedWorkItemIdField": "Custom.ReflectedItemId", + "LanguageMaps": { + "$type": "TfsLanguageMapOptions", + "AreaPath": "Area", + "IterationPath": "Iteration" + }, + "EndpointEnrichers": null + }, + { + "$type": "TfsTeamSettingsEndpointOptions", + "Name": "TeamSettingsTarget", + "Direction": "Target", + "Organisation": "https://dev.azure.com/AIZ-Global/", + "Project": "GL.CL-Elita-migrated", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "ReflectedWorkItemIdField": "Custom.ReflectedItemId", + "LanguageMaps": { + "$type": "TfsLanguageMapOptions", + "AreaPath": "Area", + "IterationPath": "Iteration" + }, + "EndpointEnrichers": null + } + ], + "TfsEndpoints": [ + { + "Name": "tfsSource", + "Organisation": "https://dev.azure.com/AIZ-GL/", + "Project": "GL.CL-Elita", + "AuthenticationMode": "AccessToken", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", + "LanguageMaps": { + "AreaPath": "Area", + "IterationPath": "Iteration" + }, + "EndpointEnrichers": null + }, + { + "Name": "tfsTarget", + "Organisation": "https://dev.azure.com/AIZ-Global/", + "Project": "GL.CL-Elita-migrated", + "AuthenticationMode": "AccessToken", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "ReflectedWorkItemIdField": "Custom.ReflectedWorkItemId", + "LanguageMaps": { + "AreaPath": "Area", + "IterationPath": "Iteration" + }, + "EndpointEnrichers": null + } + ], + "AzureDevOpsEndpoints": [ + { + "name": "PipelineSource", + "$type": "AzureDevOpsEndpointOptions", + "Organisation": "https://dev.azure.com/AIZ-GL/", + "Project": "GL.CL-Elita", + "AuthenticationMode": "AccessToken", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "EndpointEnrichers": null + }, + { + "Name": "PipelineTarget", + "$type": "AzureDevOpsEndpointOptions", + "Organisation": "https://dev.azure.com/AIZ-Global/", + "Project": "GL.CL-Elita-migrated", + "AuthenticationMode": "AccessToken", + "AccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "EndpointEnrichers": null + } + ] + }, + "Source": { + "$type": "TfsTeamProjectConfig", + "Collection": "https://dev.azure.com/AIZ-GL/", + "Project": "GL.CL-Elita", + "ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId", + "AuthenticationMode": "AccessToken", + "AllowCrossProjectLinking": false, + "PersonalAccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "LanguageMaps": { + "AreaPath": "Area", + "IterationPath": "Iteration" + } + }, + "Target": { + "$type": "TfsTeamProjectConfig", + "Collection": "https://dev.azure.com/AIZ-Global/", + "Project": "GL.CL-Elita-migrated", + "ReflectedWorkItemIDFieldName": "Custom.ReflectedWorkItemId", + "AuthenticationMode": "AccessToken", + "AllowCrossProjectLinking": false, + "PersonalAccessToken": "oltlmvyhhzmi5wacj4awkaly7ahhptkp2wtyeu3ppeypfwgqktmq", + "LanguageMaps": { + "AreaPath": "Area", + "IterationPath": "Iteration" + } + }, + "CommonEnrichersConfig": [ + { + "$type": "TfsNodeStructureOptions", + "Enabled": true, + "PrefixProjectToNodes": false, + "NodeBasePaths": [], + "AreaMaps": {}, + "IterationMaps": {}, + "ShouldCreateMissingRevisionPaths": true + } + ], + "Processors": [ + { + "$type": "TfsAreaAndIterationProcessorOptions", + "Enabled": false, + "PrefixProjectToNodes": false, + "NodeBasePaths": null, + "AreaMaps": {}, + "IterationMaps": {}, + "ProcessorEnrichers": null, + "SourceName": "WorkItemSource", + "TargetName": "WorkItemTarget" + }, + { + "$type": "TfsTeamSettingsProcessorOptions", + "Enabled": false, + "MigrateTeamSettings": true, + "UpdateTeamSettings": false, + "PrefixProjectToNodes": false, + "MigrateTeamCapacities": false, + "Teams": null, + "ProcessorEnrichers": null, + "SourceName": "TeamSettingsSource", + "TargetName": "TeamSettingsTarget" + }, + { + "$type": "TestVariablesMigrationConfig", + "Enabled": false + }, + { + "$type": "TestConfigurationsMigrationConfig", + "Enabled": false + }, + { + "$type": "TestPlansAndSuitesMigrationConfig", + "Enabled": false, + "PrefixProjectToNodes": false, + "OnlyElementsWithTag": null, + "TestPlanQueryBit": null, + "RemoveAllLinks": false, + "MigrationDelay": 0, + "UseCommonNodeStructureEnricherConfig": true, + "NodeBasePaths": [], + "AreaMaps": {}, + "IterationMaps": {}, + "RemoveInvalidTestSuiteLinks": false, + "FilterCompleted": false + }, + { + "$type": "TfsSharedQueryProcessorOptions", + "Enabled": false, + "PrefixProjectToNodes": false, + "SharedFolderName": "Shared Queries", + "SourceToTargetFieldMappings": null, + "ProcessorEnrichers": null, + "SourceName": "tfsSource", + "TargetName": "tfsTarget" + }, + { + "$type": "AzureDevOpsPipelineProcessorOptions", + "Enabled": true, + "MigrateBuildPipelines": false, + "MigrateReleasePipelines": false, + "MigrateTaskGroups": true, + "MigrateVariableGroups": false, + "MigrateServiceConnections": false, + "BuildPipelines": null, + "ReleasePipelines": null, + "RepositoryNameMaps": { + "GL.CL-Elita": "GL.CL-Elita-migrated" + }, + "ProcessorEnrichers": null, + "SourceName": "PipelineSource", + "TargetName": "PipelineTarget" + }, + { + "$type": "WorkItemMigrationConfig", + "Enabled": false, + "ReplayRevisions": false, + "PrefixProjectToNodes": false, + "UpdateCreatedDate": true, + "UpdateCreatedBy": true, + "BuildFieldTable": true, + "WIQLQueryBit": "AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') ", + "WIQLOrderBit": "[System.ChangedDate] desc", + "LinkMigration": true, + "AttachmentMigration": true, + "AttachmentWorkingPath": "c:\\temp\\WorkItemAttachmentWorkingFolder\\", + "FixHtmlAttachmentLinks": true, + "SkipToFinalRevisedWorkItemType": true, + "WorkItemCreateRetryLimit": 5, + "FilterWorkItemsThatAlreadyExistInTarget": false, + "PauseAfterEachWorkItem": false, + "AttachmentMaxSize": 480000000, + "AttachRevisionHistory": true, + "LinkMigrationSaveEachAsAdded": false, + "GenerateMigrationComment": true, + "WorkItemIDs": null, + "MaxRevisions": 0, + "UseCommonNodeStructureEnricherConfig": true, + "StopMigrationOnMissingAreaIterationNodes": false, + "NodeBasePaths": [], + "AreaMaps": { + "GL.CL-Logistics": "GL.CL-Elita-migrated" + }, + "IterationMaps": { + "GL.CL-Logistics": "GL.CL-Elita-migrated" + }, + "MaxGracefulFailures": 0, + "SkipRevisionWithInvalidIterationPath": true, + "SkipRevisionWithInvalidAreaPath": true + } + ] +} diff --git a/docs/.order b/docs/.order deleted file mode 100644 index f2ccf7c..0000000 --- a/docs/.order +++ /dev/null @@ -1 +0,0 @@ -FastTrack \ No newline at end of file diff --git a/docs/overview.md b/docs/overview.md deleted file mode 100644 index b5031fa..0000000 --- a/docs/overview.md +++ /dev/null @@ -1,12 +0,0 @@ -# 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) - - - - diff --git a/docs/project-migration-process.md b/docs/project-migration-process.md deleted file mode 100644 index 2e8f890..0000000 --- a/docs/project-migration-process.md +++ /dev/null @@ -1,49 +0,0 @@ -# Overview - -[Documentation](https://nkdagility.github.io/azure-devops-migration-tools/) -[github](https://github.com/nkdAgility/azure-devops-migration-tools) -[Azure DevOps Site](https://dev.azure.com/nkdagility/migration-tools) - -# Project Migration Process - -Step by step for migrating a single Project's work items using the [Azure DevOps Migration Tools](https://dev.azure.com/nkdagility/migration-tools) (aka Martin's Tool) to an existing Azure DevOps account - -Source is *not* migrated with this tool. See [migrating source code](migrating-project-source-code) for more information. - -The migration process copies work items and related data from one project to another. - -The tool used expects the target project to be created prior to execution. It will not create the project. - -If the source project has custom fields, the target project must have the same fields, or be mapped to existing fields, or the custom data will not be copied. - -Process customization in newly created Azure DevOps is different than proces customization in on premise Team Foundation Server (TFS) installations. - -## Process Overview - -1. Fill out project [Survey Spreadsheet]() for the project -2. Determine if an existing Azure DevOps Process Template will work for the project -3. If a new Process Template is Required, create a new process Template -4. Migrate source code -5. Configure and run the migration tool -6. Validate results -7. User testing and verification - -## Notes -- For new projects, peform test migrations to a test organization or project. -- If the source project has work items that link to source code, source code must be migrated prior to creating the links. Additionally, the source path must match so be sure when migrating to leave same structure in place. - -## Creating a New Process Template for Migration - -1. Copy an existing inherited template used for migration -2. Review any custom fields in the source process. Custom fields will be need to be specially defined on the target account and added to the process prior to migration. -2. Review work items for state changes. Non default states need to either be added or re-mapped to standard states in configuration. - -## Adding Custom Fields to your Azure DevOps Process Template - - - -## Configure and Run the migration tool - -Use an existing configuration template from *** as existing configuration can be leveraged -Commit the project specific configuration in the event it's needed for reference, or, to use it as a template for similar projects - diff --git a/docs/.gitignore b/documentaion/.gitignore similarity index 100% rename from docs/.gitignore rename to documentaion/.gitignore diff --git a/devops-docs/.images/clone-wiki-annotation.jpg b/documentaion/.images/clone-wiki-annotation.jpg similarity index 100% rename from devops-docs/.images/clone-wiki-annotation.jpg rename to documentaion/.images/clone-wiki-annotation.jpg diff --git a/documentaion/.images/full-migration-workflow.png b/documentaion/.images/full-migration-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..702c8d2ef2d6901760acbc3385e273780bf0561c GIT binary patch literal 42893 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 literal 0 HcmV?d00001 diff --git a/documentaion/.images/partial-migration-workflow.png b/documentaion/.images/partial-migration-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..d19ef8c3b4db4f1ed0948acb07596b68ff071bcd GIT binary patch literal 42934 zcmdqIRa70p*Dr`9A;A-Z1SbRt7Tn$49fAdScMHKixI=IV?he7--TmP1KE?muZ>^cT z=9`&^S@SSFaZdN?uHLn4*H3l_$x4eL!DGWiK|vvji3-X?K|yChLBT}6eFMI^9qAr{ zf+Bh%)w6WNNBM6JgwekS=qUXU*s zp0TOQ?jCC=m`$q-w~bUOWsX&2CNfh}@A&vMTVgg-eV)SIvG2Lxf5q0qlfFN{?b5uG ze!YDzmny}A{ht3V(MSC(duU%&{}hdAQ1Z2Ibn?}(CX(# zMdx%R=*c#)X2QtL<(J!x{&BPwI9%6yy4}iDArdsPyi!MWvx zkWip{#Nlhkg~!A&A~G}2x+r&aYbii(NkV;5N&3h3N42>lk_#Eq($b)M zGgbXBKAPLv<}3CI{1S~(lMas*kEO^MrqdCXA)nyRWOs+S+FZ*A&WS>@T2cXIVFFXP+V z_A?gyMuHG_iDA7X@|8{bUq7r5vva2ZQMRDgN_95x3@Acw&b9Bej5aIss-)(#vdp}p z9!G05!Jy$zYkWkAXsedmcx%cOei`Ico{yfe=*{=mM#bL4jC*7nwkl7p)%0DZHiy(S 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 WS_4b9GzWoz*F7ygWQ~S(^nU>06*sp4 literal 0 HcmV?d00001 diff --git a/documentaion/.images/secret.png b/documentaion/.images/secret.png new file mode 100644 index 0000000000000000000000000000000000000000..a725b306078edce28297f72b609672ba202d7138 GIT binary patch literal 8334 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 Kic)|6_J07+F8mt+ literal 0 HcmV?d00001 diff --git a/documentaion/.images/settings.png b/documentaion/.images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..121b2614da5f66ddbd973fb7fdaa0e5ac4a0580c GIT binary patch literal 36269 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 G`uq>nn+&`F literal 0 HcmV?d00001 diff --git a/documentaion/.images/user-migration-workflow.png b/documentaion/.images/user-migration-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..702c8d2ef2d6901760acbc3385e273780bf0561c GIT binary patch literal 42893 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 literal 0 HcmV?d00001 diff --git a/documentaion/.images/variables.png b/documentaion/.images/variables.png new file mode 100644 index 0000000000000000000000000000000000000000..23a7839492660a3a39cecf574c8b2f98a65b00a1 GIT binary patch literal 21434 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 literal 0 HcmV?d00001 diff --git a/docs/FastTrack/.order b/documentaion/FastTrack/.order similarity index 100% rename from docs/FastTrack/.order rename to documentaion/FastTrack/.order diff --git a/docs/FastTrack.md b/documentaion/FastTrack/FastTrack.md similarity index 100% rename from docs/FastTrack.md rename to documentaion/FastTrack/FastTrack.md diff --git a/docs/FastTrack/Migrator-Prepare-Step.md b/documentaion/FastTrack/Migrator-Prepare-Step.md similarity index 100% rename from docs/FastTrack/Migrator-Prepare-Step.md rename to documentaion/FastTrack/Migrator-Prepare-Step.md diff --git a/docs/FastTrack/OData-Views.md b/documentaion/FastTrack/OData-Views.md similarity index 100% rename from docs/FastTrack/OData-Views.md rename to documentaion/FastTrack/OData-Views.md diff --git a/docs/FastTrack/Tools.md b/documentaion/FastTrack/Tools.md similarity index 100% rename from docs/FastTrack/Tools.md rename to documentaion/FastTrack/Tools.md diff --git a/docs/FastTrack/users.md b/documentaion/FastTrack/users.md similarity index 100% rename from docs/FastTrack/users.md rename to documentaion/FastTrack/users.md diff --git a/documentaion/Migration steps list.md b/documentaion/Migration steps list.md new file mode 100644 index 0000000..9c62c6d --- /dev/null +++ b/documentaion/Migration steps list.md @@ -0,0 +1,56 @@ + +Migration script steps +- [ ] Migrate Teams +- [ ] Migrate Area And Iterations +- [ ] Migrate Groups +- [ ] Migrate Test Variables +- [ ] Migrate Test Configurations +- [ ] Migrate Test Plans And Suites +- [ ] Migrate Work Item Querys +- [ ] Shared Queries +- [ ] Migrate Repos +- [ ] Migrate Wikis +- [ ] Migrate Task Groups +- [ ] Migrate Variable Groups +- [ ] Migrate Service Connections +- [ ] Migrate Build Queues (Agent Pools in UI) +- [ ] Migrate Build Pipelines +- [ ] Migrate Release Pipelines +- [ ] Migrate Service Hookss +- [ ] Migrate Policies +- [ ] Migrate Dashboards +- [ ] Migrate WorkItems + + + +Migration Step Order +-------------------- +Teams +Area And Iterations +Groups +Test Variables +Test Configurations +Test Plans And Suites +Work Item Queries +Shared Queries +Repos +Wikis +Task Groups +Variable Groups +Service Connections +Build Queues +Build Pipelines +Release Pipelines +Service Hooks +Policies +Dashboards +Custom Field For Work Item Migration +WorkItems + + + + + + + + diff --git a/devops-docs/git.md b/documentaion/devops/git.md similarity index 100% rename from devops-docs/git.md rename to documentaion/devops/git.md diff --git a/devops-docs/migrating-wikis.md b/documentaion/devops/migrating-wikis.md similarity index 100% rename from devops-docs/migrating-wikis.md rename to documentaion/devops/migrating-wikis.md diff --git a/devops-docs/overview.md b/documentaion/devops/overview.md similarity index 100% rename from devops-docs/overview.md rename to documentaion/devops/overview.md diff --git a/devops-docs/using-the-azure-devops-cli-extension.md b/documentaion/devops/using-the-azure-devops-cli-extension.md similarity index 100% rename from devops-docs/using-the-azure-devops-cli-extension.md rename to documentaion/devops/using-the-azure-devops-cli-extension.md diff --git a/devops-docs/work-item-migrator.md b/documentaion/devops/work-item-migrator.md similarity index 100% rename from devops-docs/work-item-migrator.md rename to documentaion/devops/work-item-migrator.md diff --git a/devops-docs/git/branching-strategy.md b/documentaion/git/branching-strategy.md similarity index 100% rename from devops-docs/git/branching-strategy.md rename to documentaion/git/branching-strategy.md diff --git a/devops-docs/git/cleaning-tfs-branches.md b/documentaion/git/cleaning-tfs-branches.md similarity index 100% rename from devops-docs/git/cleaning-tfs-branches.md rename to documentaion/git/cleaning-tfs-branches.md diff --git a/devops-docs/git/create-repo-from-folder.md b/documentaion/git/create-repo-from-folder.md similarity index 100% rename from devops-docs/git/create-repo-from-folder.md rename to documentaion/git/create-repo-from-folder.md diff --git a/devops-docs/git/folder-clone-steps.md b/documentaion/git/folder-clone-steps.md similarity index 100% rename from devops-docs/git/folder-clone-steps.md rename to documentaion/git/folder-clone-steps.md diff --git a/devops-docs/git/git-helpers.md b/documentaion/git/git-helpers.md similarity index 100% rename from devops-docs/git/git-helpers.md rename to documentaion/git/git-helpers.md diff --git a/devops-docs/git/git-tfs-branches-compared.md b/documentaion/git/git-tfs-branches-compared.md similarity index 100% rename from devops-docs/git/git-tfs-branches-compared.md rename to documentaion/git/git-tfs-branches-compared.md diff --git a/docs/migrate-modules.md b/documentaion/migrate-modules.md similarity index 98% rename from docs/migrate-modules.md rename to documentaion/migrate-modules.md index 16d0aef..4069ac8 100644 --- a/docs/migrate-modules.md +++ b/documentaion/migrate-modules.md @@ -1,90 +1,90 @@ -# Introduction - -DevOps Migrator modules and driver script are used to migrate multiple projects in bulk. Read through the documentation below to learn how to get started with your first migration! - -The modules provided support the following migration options: -- Migrate Users (On the ORG level) -- Migrate Teams (On the project level) -- Migrate Team Members (On the project level) -- Migrate Area Paths -- Migrate Iterations -- Migrate Repos -- Migrate Build Queues - -# Dependencies -The following tools are required to complete migrations: - -- Work Items are migrated using [Microsoft VSTS Work Item Migrator](https://github.com/microsoft/vsts-work-item-migrator) -- PowerShell 5.1.0 or later -- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) - - [Azure CLI DevOps Extension](https://docs.microsoft.com/en-us/azure/devops/cli/?view=azure-devops) - -The [Azure DevOps Migration Tool](https://github.com/nkdAgility/azure-devops-migration-tools) can also be used to migrate work items. This is an excellent tool that better supports work item transforms and history though can be complex to configure. - -# Getting Started -There is some simple set up that needs to be done before you can run any of the migration modules. Included in the repo are two directories... -- 📂 Migration-Scripts -- 📂 Supporting-Modules - -The `migration-scripts` directory holds scripts, configuration files and a list of projects in the form of a `.csv`. -The `supporting-modules` directory holds `psm1` module files that do the heavy lifting in the migration. Theoretically, a migration can be run completely from the command-line, the items nested under the `migration-scripts` directory are not technically required, but rather act as a centralized place to run repeat migrations in bulk. - ---- -### STEP 1: Installing the modules manifest -The first step to running a migration will be installing the modules manifest. There is also a pre-defined script provided under the `migration-scripts` directory that will automatically do this for you. - -##### STEPS: -- Open the PowerShell script `migration-scripts/create-manifest.ps1` -- Run the script -- The script will create a manifest of all the required modules in your `WindowsPowerShell/Modules` directory. This will allow you to use `Import-Module` to load all of the required modules. - -You should only have to run this script the first time cloning this repo or when the `migration-scripts/create-manifest.ps1` file is changed. Read more about the [`create-manifest.ps1` script here](migration-scripts/README.md#create-manifest.ps1) - ---- -### All steps listed below are only required if you plan on using the `migration-scripts/AllProjects.ps1` script, which is recommended. ---- - - -### STEP 2: Clone the Microsoft VSTS Work Item Migrator - -The modules provided in this repo do not handle any kind of work item migration, so if you would like to migrate work items I recommend the tool written by Microsoft, but you are not limited to this tool, if you choose to go another route just edit the `migration-scripts/AllProjects.ps1` file to use whatever tool you go with. - -##### STEPS: - -- Navigate to the [Microsoft VSTS Work Item Migrator](https://github.com/microsoft/vsts-work-item-migrator) GitHub and [clone the repo](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github/cloning-a-repository) to your local machine. - ---- - -### STEP 3: Configuring the Configuration.json file - -The `Configuration.json` file is used to set up file locations for logging, PAT tokens for authentication and other information required for running the `migration-scripts/AllProjects.ps1` script. - -##### STEPS - -- Open the file `migration-scripts/Configuration.json` and fill out the required fields... - - Read more about the [configuration fields here](migration-scripts/README.md#configuration.json) - ---- - -### STEP 4: Defining projects to migrate in the Projects.csv file - -##### STEPS - -- Open the file `migration-scripts/Projects.csv` and add the list of source projects to target projects you wish to migrate. -- Read more about the [Projects.csv file here](migration-scripts/README.md#Projects.csv) - -You should now be able to run a migration. - - - -# Running a Migration With The `AllProjects.ps1` Script - -After going through the above steps, navigate to `migration-scripts/AllProjects.ps1` and either invoke the script via the command line or run it within an IDE. - -All of the `Start-ADOMigration...` modules are independent of one another and can be commented out or removed is that particular migration is not desired. - -_(Area paths and Iteration paths need to be migrated if you plan to migrate work items with the microsoft tool)_ - -The `AllProjects.ps1` script is not required to run the migration modules. New scripts can be written around the modules for a more custom migration experience. - +# Introduction + +DevOps Migrator modules and driver script are used to migrate multiple projects in bulk. Read through the documentation below to learn how to get started with your first migration! + +The modules provided support the following migration options: +- Migrate Users (On the ORG level) +- Migrate Teams (On the project level) +- Migrate Team Members (On the project level) +- Migrate Area Paths +- Migrate Iterations +- Migrate Repos +- Migrate Build Queues + +# Dependencies +The following tools are required to complete migrations: + +- Work Items are migrated using [Microsoft VSTS Work Item Migrator](https://github.com/microsoft/vsts-work-item-migrator) +- PowerShell 5.1.0 or later +- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) + - [Azure CLI DevOps Extension](https://docs.microsoft.com/en-us/azure/devops/cli/?view=azure-devops) + +The [Azure DevOps Migration Tool](https://github.com/nkdAgility/azure-devops-migration-tools) can also be used to migrate work items. This is an excellent tool that better supports work item transforms and history though can be complex to configure. + +# Getting Started +There is some simple set up that needs to be done before you can run any of the migration modules. Included in the repo are two directories... +- 📂 Migration-Scripts +- 📂 Supporting-Modules + +The `migration-scripts` directory holds scripts, configuration files and a list of projects in the form of a `.csv`. +The `supporting-modules` directory holds `psm1` module files that do the heavy lifting in the migration. Theoretically, a migration can be run completely from the command-line, the items nested under the `migration-scripts` directory are not technically required, but rather act as a centralized place to run repeat migrations in bulk. + +--- +### STEP 1: Installing the modules manifest +The first step to running a migration will be installing the modules manifest. There is also a pre-defined script provided under the `migration-scripts` directory that will automatically do this for you. + +##### STEPS: +- Open the PowerShell script `migration-scripts/create-manifest.ps1` +- Run the script +- The script will create a manifest of all the required modules in your `WindowsPowerShell/Modules` directory. This will allow you to use `Import-Module` to load all of the required modules. + +You should only have to run this script the first time cloning this repo or when the `migration-scripts/create-manifest.ps1` file is changed. Read more about the [`create-manifest.ps1` script here](migration-scripts/README.md#create-manifest.ps1) + +--- +### All steps listed below are only required if you plan on using the `migration-scripts/AllProjects.ps1` script, which is recommended. +--- + + +### STEP 2: Clone the Microsoft VSTS Work Item Migrator + +The modules provided in this repo do not handle any kind of work item migration, so if you would like to migrate work items I recommend the tool written by Microsoft, but you are not limited to this tool, if you choose to go another route just edit the `migration-scripts/AllProjects.ps1` file to use whatever tool you go with. + +##### STEPS: + +- Navigate to the [Microsoft VSTS Work Item Migrator](https://github.com/microsoft/vsts-work-item-migrator) GitHub and [clone the repo](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github/cloning-a-repository) to your local machine. + +--- + +### STEP 3: Configuring the Configuration.json file + +The `Configuration.json` file is used to set up file locations for logging, PAT tokens for authentication and other information required for running the `migration-scripts/AllProjects.ps1` script. + +##### STEPS + +- Open the file `migration-scripts/Configuration.json` and fill out the required fields... + - Read more about the [configuration fields here](migration-scripts/README.md#configuration.json) + +--- + +### STEP 4: Defining projects to migrate in the Projects.csv file + +##### STEPS + +- Open the file `migration-scripts/Projects.csv` and add the list of source projects to target projects you wish to migrate. +- Read more about the [Projects.csv file here](migration-scripts/README.md#Projects.csv) + +You should now be able to run a migration. + + + +# Running a Migration With The `AllProjects.ps1` Script + +After going through the above steps, navigate to `migration-scripts/AllProjects.ps1` and either invoke the script via the command line or run it within an IDE. + +All of the `Start-ADOMigration...` modules are independent of one another and can be commented out or removed is that particular migration is not desired. + +_(Area paths and Iteration paths need to be migrated if you plan to migrate work items with the microsoft tool)_ + +The `AllProjects.ps1` script is not required to run the migration modules. New scripts can be written around the modules for a more custom migration experience. + Read more about the [`AllProjects.ps1` script here](migration-scripts/README.md#AllProjects.ps1). \ No newline at end of file diff --git a/docs/migrating-project-source-code.md b/documentaion/migrating-project-source-code.md similarity index 100% rename from docs/migrating-project-source-code.md rename to documentaion/migrating-project-source-code.md diff --git a/docs/migrating-wikis.md b/documentaion/migrating-wikis.md similarity index 100% rename from docs/migrating-wikis.md rename to documentaion/migrating-wikis.md diff --git a/docs/using-the-azure-devops-cli-extension.md b/documentaion/using-the-azure-devops-cli-extension.md similarity index 100% rename from docs/using-the-azure-devops-cli-extension.md rename to documentaion/using-the-azure-devops-cli-extension.md diff --git a/docs/work-item-migrator.md b/documentaion/work-item-migrator.md similarity index 100% rename from docs/work-item-migrator.md rename to documentaion/work-item-migrator.md diff --git a/helper-scripts/ADODeletePolicies.ps1 b/helper-scripts/ADODeletePolicies.ps1 new file mode 100644 index 0000000..802f8f7 --- /dev/null +++ b/helper-scripts/ADODeletePolicies.ps1 @@ -0,0 +1,57 @@ +Using Module "..\modules\Migrate-ADO-Common.psm1" + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT, + [Parameter (Mandatory=$FALSE)] [bool]$DoDelete = $TRUE, + [Parameter (Mandatory=$FALSE)] [Object[]]$PolicyIds = $() +) + +Write-Host "Begin Delete ALL Policies for Organization and Project" + +# Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + + +Write-Host "Begin Deleting ALL Policies found in ($OrgName/$ProjectName)... " +Write-Host " " + + +# Get all policies for the process/project +$url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/policy/configurations?api-version=7.0" +$results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers +$policyConfigurations = $results.Value + +$policies +if ($PolicyIds.Count -gt 0) { + $policies = $policyConfigurations | Where-Object { $_.Id -in $PolicyIds } + Write-Host "Begin Deleting set Policies with Ids $(ConvertTo-json -Depth 100 $PolicyIds)... " + +} else { + $policies = $policyConfigurations + Write-Host "Begin Deleting ALL Policies found in ($OrgName/$ProjectName)... " +} + +foreach ($policy in $policies) { + try { + if($DoDelete) { + Write-Log -Message "Deleting Policy with policy type display name: '$($policy.type.displayName)' and id: [$($policy.id)].. " + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/policy/configurations/$($policy.id)?api-version=7.0" + Invoke-RestMethod -Method DELETE -Uri $url -Headers $headers + } else { + Write-Log -Message "TESTING - Test call to delete Policy with policy type display name: '$($policy.type.displayName)' and id: [$($policy.id)]" + } + } + 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 Policies found in ($OrgName/$ProjectName)... " + + diff --git a/helper-scripts/ADODeleteRepos.ps1 b/helper-scripts/ADODeleteRepos.ps1 new file mode 100644 index 0000000..49402f0 --- /dev/null +++ b/helper-scripts/ADODeleteRepos.ps1 @@ -0,0 +1,68 @@ +Using Module "..\modules\Migrate-ADO-Common.psm1" + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT, + [Parameter (Mandatory=$FALSE)] [bool]$DoDelete = $TRUE, + [Parameter (Mandatory=$FALSE)] [Object[]]$RepoIds = $() +) + +Write-Host "Begin process to Delete Repositories for Organization $OrgName and Project $ProjectName.." + +# Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + + +$url = "https://dev.azure.com/$orgName/$projectName/_apis/git/repositories?api-version=7.0" +$results = Invoke-RestMethod -Method GET -uri $url -Headers $headers +$repositories = $results.Value + +$repos +if ($RepoIds.Count -gt 0) { + $repos = $repositories | Where-Object { $_.Id -in $RepoIds } + Write-Host "Begin Deleting set Repositories with Ids $(ConvertTo-json -Depth 100 $RepoIds)... " + +} else { + $repos = $repositories + Write-Host "Begin Deleting ALL Repositories... " +} +Write-Host " " + +foreach ($repository in $repos) { + try { + if($repository.name -eq $ProjectName) { + Write-Log -Message "Skipping the Default Repository `"$($repository.name)`" [$($repository.id)] because there must be at least one repository defined at all times.. " + continue + } + + Write-Log -Message "Deleting Repository `"$($repository.name)`" [$($repository.id)].. " + + # Delete Repo to recycle bin due to soft delete + if($DoDelete) { + $url1 = "https://dev.azure.com/$OrgName/$ProjectName/_apis/git/repositories/$($repository.id)?api-version=7.0" + Invoke-RestMethod -Method DELETE -Uri $url1 -Headers $headers + } else { + Write-Log -Message "TESTING - Test call to delete repo`"$($repository.name)`" [$($repository.id)]" + } + + # Delete Repo from recycle bin for permanent deletion + if($DoDelete) { + $url2 = "https://dev.azure.com/$OrgName/$ProjectName/_apis/git/recycleBin/repositories/$($repository.id)?api-version=7.0" + Invoke-RestMethod -Method DELETE -Uri $url2 -Headers $headers + } else { + Write-Log -Message "TESTING - Test call to delete repo `"$($repository.name)`" [$($repository.id)] from recylce bin" + } + } + 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 Repositories found in ($OrgName/$ProjectName)... " + + diff --git a/helper-scripts/ADODeleteVariableGroups.ps1 b/helper-scripts/ADODeleteVariableGroups.ps1 new file mode 100644 index 0000000..23a908b --- /dev/null +++ b/helper-scripts/ADODeleteVariableGroups.ps1 @@ -0,0 +1,55 @@ +Using Module "..\modules\Migrate-ADO-Common.psm1" + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT, + [Parameter (Mandatory=$FALSE)] [bool]$DoDelete = $TRUE +) + + +Write-Host "Begin process to Delete ADO Library Variable Groups for Organization $OrgName and Project $ProjectName.." +Write-Host "Get ADO Library Variable Groups for Organization $OrgName and Project $ProjectName.." + +# Create Headers +$headers = New-HTTPHeaders -PersonalAccessToken $PAT + +$targetProject = Get-ADOProjects -OrgName $OrgName -ProjectName $ProjectName -Headers $headers + +$url = "https://dev.azure.com/$orgName/$projectName/_apis/distributedtask/variablegroups?api-version=7.0" +$results = Invoke-RestMethod -Method Get -uri $url -Headers $headers +$groups = $results.value + + +try { + Write-Host "Begin Deleting ALL ADO Variable Groupss... " + Write-Host " " + foreach ($group in $groups) { + try { + # Delete Group + Write-Log -Message "Deleting ADO Variable Group `"$($group.name)`".. " + if($DoDelete -eq $TRUE) { + $d_url = "https://dev.azure.com/$orgName/_apis/distributedtask/variablegroups/$($group.Id)?projectIds=$($targetProject.Id)&api-version=7.1-preview.2" + $results = Invoke-RestMethod -Method DELETE -uri $d_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 {} + } + } +} +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 ADO Library Variable Groups found in ($OrgName/$ProjectName)... " + + diff --git a/helper-scripts/README - Helper-Scripts.md b/helper-scripts/README - Helper-Scripts.md new file mode 100644 index 0000000..954b6c3 --- /dev/null +++ b/helper-scripts/README - Helper-Scripts.md @@ -0,0 +1,21 @@ +# Helper Scripts + +This directory contains PowerShell scripts that are used by the migration process during a ADO project migration. + +### Files: + +ADODeletePolicies.ps1:
+Used by the `Migrate-ADO-Policies.psm1` module file to delete all of the Policies prior to migrating them. + +ADODeleteRepos.ps1:
+Used by the `Migrate-ADO-Repos.psm1` module file to delete and re-create all of the Repositories in the Target project. + +ADODeleteVariableGroups.ps1:
+Used by the `Migrate-ADO-VariableGroups.psm1` module file to re-migrate Variable Groups + + + + + + + diff --git a/images/full-migration-workflow.png b/images/full-migration-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..702c8d2ef2d6901760acbc3385e273780bf0561c GIT binary patch literal 42893 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 literal 0 HcmV?d00001 diff --git a/images/partial-migration-workflow.png b/images/partial-migration-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..d19ef8c3b4db4f1ed0948acb07596b68ff071bcd GIT binary patch literal 42934 zcmdqIRa70p*Dr`9A;A-Z1SbRt7Tn$49fAdScMHKixI=IV?he7--TmP1KE?muZ>^cT z=9`&^S@SSFaZdN?uHLn4*H3l_$x4eL!DGWiK|vvji3-X?K|yChLBT}6eFMI^9qAr{ zf+Bh%)w6WNNBM6JgwekS=qUXU*s zp0TOQ?jCC=m`$q-w~bUOWsX&2CNfh}@A&vMTVgg-eV)SIvG2Lxf5q0qlfFN{?b5uG ze!YDzmny}A{ht3V(MSC(duU%&{}hdAQ1Z2Ibn?}(CX(# zMdx%R=*c#)X2QtL<(J!x{&BPwI9%6yy4}iDArdsPyi!MWvx zkWip{#Nlhkg~!A&A~G}2x+r&aYbii(NkV;5N&3h3N42>lk_#Eq($b)M zGgbXBKAPLv<}3CI{1S~(lMas*kEO^MrqdCXA)nyRWOs+S+FZ*A&WS>@T2cXIVFFXP+V z_A?gyMuHG_iDA7X@|8{bUq7r5vva2ZQMRDgN_95x3@Acw&b9Bej5aIss-)(#vdp}p z9!G05!Jy$zYkWkAXsedmcx%cOei`Ico{yfe=*{=mM#bL4jC*7nwkl7p)%0DZHiy(S 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 WS_4b9GzWoz*F7ygWQ~S(^nU>06*sp4 literal 0 HcmV?d00001 diff --git a/images/secret.png b/images/secret.png new file mode 100644 index 0000000000000000000000000000000000000000..a725b306078edce28297f72b609672ba202d7138 GIT binary patch literal 8334 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 Kic)|6_J07+F8mt+ literal 0 HcmV?d00001 diff --git a/images/settings.png b/images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..121b2614da5f66ddbd973fb7fdaa0e5ac4a0580c GIT binary patch literal 36269 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 G`uq>nn+&`F literal 0 HcmV?d00001 diff --git a/images/user-migration-workflow.png b/images/user-migration-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..702c8d2ef2d6901760acbc3385e273780bf0561c GIT binary patch literal 42893 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 literal 0 HcmV?d00001 diff --git a/images/variables.png b/images/variables.png new file mode 100644 index 0000000000000000000000000000000000000000..23a7839492660a3a39cecf574c8b2f98a65b00a1 GIT binary patch literal 21434 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 literal 0 HcmV?d00001 diff --git a/migration-scripts/AllProjects.ps1 b/migration-scripts/AllProjects.ps1 deleted file mode 100644 index e2d979e..0000000 --- a/migration-scripts/AllProjects.ps1 +++ /dev/null @@ -1,198 +0,0 @@ -Import-Module Migrate-ADO -Force - -# ------------------------------------------------------------------------------------- -# -------------- Specifiy What Parts of the Migration Should Be Skipped --------------- -#region ------------------------------------------------------------------------------- -# Setting any of the below values to true will trigger a whatif condition rather than -# the actual migration. -[Boolean]$SKIP_MigrateOrgUsers = $FALSE -[Boolean]$SKIP_MigrateTeams = $FALSE -[Boolean]$SKIP_MigrateGroups = $FALSE -[Boolean]$SKIP_MigrateAreaPaths = $FALSE -[Boolean]$SKIP_MigrateIterationPaths = $FALSE -[Boolean]$SKIP_MigrateBuildQueues = $FALSE -[Boolean]$SKIP_MigrateRepos = $FALSE -[Boolean]$SKIP_MigrateWorkItems = $FALSE - -# Validate the above configuration is okay -if (($SKIP_MigrateAreaPaths -or $SKIP_MigrateIterationPaths) -and !$SKIP_MigrateWorkItems) { - throw "If you plan to migrate work items, then you need to migrate both the area and iteration paths for a project." -} -#endregion - -# ------------------------------------------------------------------------------------- -# ---------------- Set up files for logging & get configuration values ---------------- -#region ------------------------------------------------------------------------------- -$runDate = (get-date).ToString('yyyy-MM-dd HHmmss') -$configuration = [Object](Get-Content 'migration-scripts\Configuration.json' | Out-String | ConvertFrom-Json) - -$projectPath = Get-ProjectFolderPath ` - -RunDate $runDate ` - -Root $configuration.ProjectDirectory - -$env:MIGRATION_LOGS_PATH = $projectPath - -$projects = Import-Csv $configuration.ProjectsCsv - -$env:MIGRATION_LOGS_PATH = $projectPath - -Set-ProjectFolders ` - -RunDate $runDate ` - -Projects $projects ` - -SourceOrg $configuration.SourceProject.OrgName ` - -SourcePAT $configuration.SourceProject.PAT ` - -TargetOrg $configuration.TargetProject.OrgName ` - -TargetPAT $configuration.TargetProject.PAT ` - -SavedAzureQuery $configuration.SavedAzureQuery ` - -MSConfigPath $configuration.MsConfigPath ` - -Root $configuration.ProjectDirectory -#endregion - -# ------------------------------------------------------------------------------------- -# ---------------- Start The Migration At the Org Level ------------------------------- -#region ------------------------------------------------------------------------------- -Write-Log -Message ' ' -Write-Log -Message '------------------------------------------------------------------------------------------------' -Write-Log -Message "-- Migrate $($configuration.SourceProject.OrgName) to $($configuration.TargetProject.OrgName) --" -Write-Log -Message '------------------------------------------------------------------------------------------------' -Write-Log -Message ' ' - -# ======================================== -# ====== Migrate Users On Org Level ====== -#region ================================== -Start-ADOUserMigration ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourcePat $configuration.SourceProject.PAT ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetPAT $configuration.TargetProject.PAT ` - -WhatIf:$SKIP_MigrateOrgUsers -#endregion - -# -------------------------------------------------------------------------------------- -# ---------------- For each project in the CSV file - preform a migration -------------- -#region -------------------------------------------------------------------------------- -foreach ($project in $projects) { - # Get project folder & set logging path w/ env variable - $projectPath = Get-ProjectFolderPath ` - -RunDate $runDate ` - -SourceProject $project.SourceProject ` - -TargetProject $project.TargetProject ` - -Root $configuration.ProjectDirectory - - $env:MIGRATION_LOGS_PATH = $projectPath - - # Get Headers - $sourceHeaders = New-HTTPHeaders ` - -PersonalAccessToken $configuration.SourceProject.PAT - $targetHeaders = New-HTTPHeaders ` - -PersonalAccessToken $configuration.TargetProject.PAT - - Write-Log -Message ' ' - Write-Log -Message '--------------------------------------------------------------------' - Write-Log -Message "-- Migrate $($project.sourceProject) to $($project.TargetProject) --" - Write-Log -Message '--------------------------------------------------------------------' - Write-Log -Message ' ' - - # ======================================== - # ============ Migrate Teams ============= - #region ================================== - Start-ADOTeamsMigration ` - -SourceHeaders $sourceHeaders ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourceProjectName $project.SourceProject ` - -TargetHeaders $targetHeaders ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetProjectName $project.TargetProject ` - -WhatIf:$SKIP_MigrateTeams - #endregion - - # ======================================== - # =========== Migrate Groups ============= - #region ================================== - Start-ADOGroupsMigration ` - -SourcePAT $configuration.SourceProject.PAT ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourceProjectName $project.SourceProject ` - -TargetPAT $configuration.TargetProject.PAT ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetProjectName $project.TargetProject ` - -WhatIf:$SKIP_MigrateGroups - #endregion - - # ======================================== - # ========== Migrate Area Paths ========== - #region ================================== - Start-ADOAreaPathsMigration ` - -SourceProjectName $project.SourceProject ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourceHeaders $sourceHeaders ` - -TargetProjectName $project.TargetProject ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SKIP_MigrateAreaPaths - #endregion - - # ======================================== - # ======= Migrate Iteration Paths ======== - #region ================================== - Start-ADOIterationPathsMigration ` - -SourceProjectName $project.SourceProject ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourceHeaders $sourceHeaders ` - -TargetProjectName $project.TargetProject ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SKIP_MigrateIterationPaths - #endregion - - # ======================================== - # ========= Migrate Build Queues ========= - #region ================================== - Start-ADOBuildQueuesMigration ` - -SourceProjectName $project.SourceProject ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourceHeaders $sourceHeaders ` - -TargetProjectName $project.TargetProject ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetHeaders $targetHeaders ` - -WhatIf:$SKIP_MigrateBuildQueues - #endregion - - # ======================================== - # ============ Migrate Repos ============= - #region ================================== - Start-ADORepoMigration ` - -SourceProjectName $project.SourceProject ` - -SourceOrgName $configuration.SourceProject.OrgName ` - -SourceHeaders $sourceHeaders ` - -TargetProjectName $project.TargetProject ` - -TargetOrgName $configuration.TargetProject.OrgName ` - -TargetHeaders $targetHeaders ` - -ReposPath $projectPath ` - -WhatIf:$SKIP_MigrateRepos - #endregion - - # ======================================== - # ========== Migrate work items ========== - #region ================================== - if (!$SKIP_MigrateWorkItems) { - $savedPath = $(Get-Location).Path - - Set-Location -Path $configuration.WorkItemMigratorDirectory - dotnet run --validate "$projectPath\ProjectConfiguration.json" - dotnet run --migrate "$projectPath\ProjectConfiguration.json" - - Set-Location -Path $savedpath - } - else { - Write-Host "What if: Preforming the operation `"Migrate work items from source project $($project.SourceProject)`" on target `"Target project $($project.TargetProject)`"" - } - #endregion - - # ======================================== - # ========== Migration Finished ========== - # ======================================== - Write-Log "Done migrating $($project.SourceProject) to $($project.TargetProject)" -LogLevel SUCCESS -} -#endregion -#endregion \ No newline at end of file diff --git a/migration-scripts/Configuration.json b/migration-scripts/Configuration.json deleted file mode 100644 index 98f03b8..0000000 --- a/migration-scripts/Configuration.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "TargetProject": { - "OrgName": "", - "PAT": "" - }, - "SourceProject": { - "OrgName": "", - "PAT": "" - }, - "SavedAzureQuery": "My Queries/All Items", - "ProjectDirectory": "", - "ProjectsCsv": ".\\migration-scripts\\Projects.csv", - "MSConfigPath": ".\\migration-scripts\\base-configuration.json", - "WorkItemMigratorDirectory": "" -} \ No newline at end of file diff --git a/migration-scripts/README.md b/migration-scripts/README.md deleted file mode 100644 index c29a21c..0000000 --- a/migration-scripts/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# 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. - ---- - -# Projects.csv -The `Projects.csv` file is where you define a list of source projects and the corresponding target project to migrate to. -```csv -SourceProject,TargetProject -Source-Project-Name1,Target-Project-Name1 -Source-Project-Name2,Target-Project-Name2 -Source-Project-Name3,Target-Project-Name3 -``` -- Lines are separated by new lines, not commas. -- Source project names and target project names are separated by commas. - -The first line of the CSV acts as the header, these lines should not be modified (if they are modified you will need to update the header names in the `AllProjects.ps1` script as well. - -All following lines define a source project to migrate from and a target project to migrate to. - -# Configuration.json -The `Configuration.json` file is used to set up file locations for logging, PAT tokens for authentication and other information required for running the `migration-scripts/AllProjects.ps1` script. - -##### PROPERTIES -| Property Name | VSTS Only? | Data Type | Description -|---------------------------|------------|-----------|------------- -| TargetProject | | Object | An object consisting of an OrgName and a PAT -| └─ OrgName | | String | The organization name for the target project -| └─ PAT | | String | The personal access token you created (or need to create) for the target project -| SourceProject | | Object | An object consisting of an OrgName and a PAT -| └─ OrgName | | String | The organization name for the source project -| └─ PAT | | String | The personal access token you created (or need to create) for the source project -| SavedAzureQuery | ✔️ | String | Only required if using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) , read more here: ['query' parameter documentation](https://github.com/microsoft/vsts-work-item-migrator/blob/master/WiMigrator/migration-configuration.md#query-the-name-of-the-query-to-use-for-identifying-work-items-to-migrate-note-query-must-be-a-flat) -| 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. -| ProjectscCsv | | String | The path of the csv file holding the list of projects you want to migrate. This csv is included in the repo and the path is provided as a relative path, so you should not need to update this setting. -| MsConfigPath | ✔️ | String | Only required if using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) . This is the configuration file that will be copied and modified for each project. This path is set relatively and the configuration file is provided in the repo so you should not need to update this setting. -| WorkItemMigratorDirectory | ✔️ | String | Only required if using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) . This is the directory you cloned the migration tool too. Be sure to include the directory `WiMigrator` at the end of the cloned repository path. - ----------- - -**VSTS Only** means that the configuration property is only required if you are using the VSTS work item migrator. - -# base-configuration.json ([VSTS only](https://github.com/microsoft/vsts-work-item-migrator)) -A pre-configured configuration file used by the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator). -Read more about [base-configuration here](https://github.com/microsoft/vsts-work-item-migrator/blob/master/WiMigrator/migration-configuration.md) - -For each project migration defined in the `Projects.csv` the `base-configuration.json` file is copied, modified and saved in that projects directory before a migration is preformed. - -# 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. - -This script should be run when the repo is first cloned and whenever the `create-manifest.ps1` script is updated. - -# AllProjects.ps1 -The `AllProjects.ps1` script preforms a full migration of the following DevOps items: -- Area Paths - - Using the `Start-ADOAreaPathsMigration` cmdlet under `supporting-modules` -- Iteration Paths - - Using the `StartADOIterationPathsMigration` cmdlet under `supporting-modules` -- Build Queues - - Using the `Start-ADOBuildQueuesMigration` cmdlet under `supporting-modules` -- Repos - - Using the `Start-ADORepoMigration` cmdlet under `supporting-modules` -- Work Items - - Using the [Microsoft VSTS Work Item Migrator tool](https://github.com/microsoft/vsts-work-item-migrator) - -The script starts importing the `Projects.csv` and setting a migration run date, which is then used to create a migration directory under the path specified in the `Configuration.json` file. - -Each migration defined under `Projects.csv` gets it's own folder where a copy of `base-configuration.json` is created and configured specifically for that migration. All of the migrations are nested under a folder dated with the migration run date set above. - -After the project directories are created for each project, the script preforms a migration for each project. - - -# Migration Notes -- default iteration path is not set a team -- default area path is not set for a team - -# 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 - diff --git a/migration-scripts/create-manifest.ps1 b/migration-scripts/create-manifest.ps1 deleted file mode 100644 index bcb9d06..0000000 --- a/migration-scripts/create-manifest.ps1 +++ /dev/null @@ -1,38 +0,0 @@ -# ----------- CONFIGURE VARIABLES HERE -$IncludedModules = @( - "$(Get-Location)\Supporting-Modules\Migrate-ADO-AreaPaths.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-IterationPaths.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-Users.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-Teams.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-Groups.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-BuildQueues.psm1", - "$(Get-Location)\supporting-modules\Migrate-ADO-Repos.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-Common.psm1", - "$(Get-Location)\Supporting-Modules\Migrate-ADO-Pipelines.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/modules/ADO-AddCustomField.psm1 b/modules/ADO-AddCustomField.psm1 new file mode 100644 index 0000000..c03befb --- /dev/null +++ b/modules/ADO-AddCustomField.psm1 @@ -0,0 +1,303 @@ +class ADO_WorkItemType { + [String]$Id + [String]$Name + [String]$Description + [String]$Url + [String]$Inherits + [String]$Class + [String]$Color + [String]$Icon + [Bool]$IsDisabled + + ADO_WorkItemType( + [String]$id, + [String]$name, + [String]$description, + [String]$url, + [String]$inherits, + [String]$class, + [String]$color, + [String]$icon, + [Bool]$isDisabled + ) { + $this.Id = $Id + $this.Name = $name + $this.Description = $description + $this.Url = $url + $this.Inherits = $inherits + $this.Class = $class + $this.Color = $color + $this.Icon = $icon + $this.IsDisabled = $isDisabled + } +} + +function Start-ADO_AddCustomField { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$PAT, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProcessId, + + [Parameter (Mandatory = $TRUE)] + [String]$FieldName, + + [Parameter (Mandatory = $FALSE)] + [String]$FieldDefaultValue + ) + if ($PSCmdlet.ShouldProcess( + "Project $OrgName/$ProjectName", + "Add ADO custom Field from source project $OrgName/$ProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '--------------------------------' + Write-Log -Message '-- Begin Add ADO custom Field --' + Write-Log -Message '--------------------------------' + Write-Log -Message ' ' + + # See if the custom field exists yet or not. If not it needs to be created before it can be added to any work item type + $customFields = Get-CustomfieldsList ` + -LocalOrgName $OrgName ` + -LocalProjectName $ProjectName ` + -LocalHeaders $Headers + + if($NULL -ne $customFields) { + if ($null -eq ($customFields | Where-Object { $_.referenceName -ieq $FieldName })) { + Write-Log -Message "Creating Custom Field `"$FieldName`" 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 ` + -LocalHeaders $Headers + } + } + + # Get the associated work item types for this process by process Id + $workitemTypes = Get-ProcessWorkItemTypes ` + -LocalOrgName $OrgName ` + -LocalHeaders $Headers ` + -LocalProcessId $ProcessId + + if ($workitemTypes) { + foreach ($workitemType in $workitemTypes) { + # if((!$workitemType.IsDisabled) -and ($workitemType.Class -eq "derived")) { + if(!$workitemType.IsDisabled) { + $workitemType.Id + + $processDefinitions = Get-ProcessesDefinitions ` + -LocalOrgName $OrgName ` + -LocalHeaders $Headers ` + -LocalProcessId $ProcessId ` + -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)]... " + continue + } + + Write-Log -Message "ADDing Custom Field `"$FieldName`" for $OrgName/$ProjectName Work Item Type [$($workitemType.Id)]... " + Add-CustomField ` + -LocalOrgName $OrgName ` + -LocalHeaders $Headers ` + -LocalProcessId $ProcessId ` + -LocalWorkItemType $workitemType ` + -LocalFieldName $FieldName + } + } + } + } + + } +} + + +function Get-ProcessWorkItemTypes { + param( + [Parameter (Mandatory = $TRUE)] + [String]$LocalOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$LocalHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$LocalProcessId + ) + $url = "https://dev.azure.com/$LocalOrgName/_apis/work/processes/$LocalProcessId/workitemtypes?api-version=7.0" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $LocalHeaders + + [ADO_WorkItemType[]]$workItemTypes = @() + foreach ($result in $results.Value) { + $workItemTypes += (ConvertTo-WorkItemTypeObject -WorkItemType $result) + } + return $workItemTypes +} + +function Add-CustomField { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$LocalOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$LocalHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$LocalProcessId, + + [Parameter (Mandatory = $TRUE)] + [ADO_WorkItemType]$LocalWorkItemType, + + [Parameter (Mandatory = $TRUE)] + [String]$LocalFieldName + ) + if ($PSCmdlet.ShouldProcess($WorkItemType.Name)) { + # $url = "https://dev.azure.com/$LocalOrgName/_apis/work/processes/$LocalProcessId/workItemTypes/$($LocalWorkItemType.Id)/fields?api-version=7.0" + $url = "https://dev.azure.com/$LocalOrgName/_apis/work/processdefinitions/$LocalProcessId/workItemTypes/$($LocalWorkItemType.Id)/fields?api-version=7.0" + + $body = @" +{ + "defaultValue": "", + "referenceName": "$LocalFieldName", + "name": "Custom Work Item Field - ReflectedWorkItemId", + "type": "plainText", + "readOnly": false, + "required": false, + "pickList": null, + "url": null, + "allowGroups": null +} +"@ + Invoke-RestMethod -Method POST -Uri $url -Body $body -Headers $headers -ContentType "application/json" + } +} + +function ConvertTo-WorkItemTypeObject { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [Object]$WorkItemType + ) + if ($PSCmdlet.ShouldProcess($WorkItemType.Name)) { + [ADO_WorkItemType]$ADOWorkItemType = [ADO_WorkItemType]::new( + $WorkItemType.Id, + $WorkItemType.Name, + $WorkItemType.Description, + $WorkItemType.Url, + $WorkItemType.Inherits, + $WorkItemType.Class, + $WorkItemType.Color, + $WorkItemType.Icon, + $WorkItemType.IsDisabled + ) + + return $ADOWorkItemType + } +} + +function Get-Processes { + param( + [Parameter (Mandatory = $TRUE)] + [String]$LocalOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$LocalHeaders + ) + $url = "https://dev.azure.com/$LocalOrgName/_apis/process/processes?api-version=7.0" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $LocalHeaders + + return $results.value +} + +function Get-ProcessesDefinitions { + param( + [Parameter (Mandatory = $TRUE)] + [String]$LocalOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$LocalHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$LocalProcessId, + + [Parameter (Mandatory = $TRUE)] + [ADO_WorkItemType]$LocalWorkItemType + ) + $url = "https://dev.azure.com/$LocalOrgName/_apis/work/processes/$LocalProcessId/workItemTypes/$($LocalWorkItemType.Id)/fields?api-version=7.0" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $LocalHeaders + + return $results.value +} + +function Get-CustomfieldsList { + param( + [Parameter (Mandatory = $TRUE)] + [String]$LocalOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$LocalProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$LocalHeaders + ) + $url = "https://dev.azure.com/$OrgName/$LocalProjectName/_apis/wit/fields?api-version=7.0" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $LocalHeaders + + return $results.value +} + +function New-Customfield { + param( + [Parameter (Mandatory = $TRUE)] + [String]$LocalOrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$LocalHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$LocalFieldName + ) + $url = "https://dev.azure.com/$LocalOrgName/$ProjectName/_apis/wit/fields?api-version=7.0" + + $body = @" +{ + "name": "Custom Work Item Field - ReflectedWorkItemId", + "referenceName": "$LocalFieldName", + "description": "Custom field used by data migration tool.", + "type": "string", + "usage": "workItem", + "readOnly": false, + "canSortBy": true, + "isQueryable": true, + "supportedOperations": [ + { + "referenceName": "SupportedOperations.Equals", + "name": "=" + } + ], + "isIdentity": true, + "isPicklist": false, + "isPicklistSuggested": false, + "url": null +} +"@ + $results = Invoke-RestMethod -Method POST -Uri $url -Body $body -Headers $LocalHeaders -ContentType "application/json" + + return $results +} + diff --git a/modules/Migrate-ADO-Artifacts.psm1 b/modules/Migrate-ADO-Artifacts.psm1 new file mode 100644 index 0000000..ec6747e --- /dev/null +++ b/modules/Migrate-ADO-Artifacts.psm1 @@ -0,0 +1,476 @@ + +function Start-ADOArtifactsMigration { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$SourceHeaders, + + [Parameter (Mandatory = $TRUE)] + [string]$SourcePAT, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$TargetHeaders, + + [Parameter (Mandatory = $TRUE)] + [string]$TargetPAT, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectPath, + + [Parameter (Mandatory = $TRUE)] + [Int]$ArtifactFeedPackageVersionLimit + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate Artifacts from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '------------------------' + Write-Log -Message '-- Migrate Artifacts --' + Write-Log -Message '-----------------------' + Write-Log -Message ' ' + + + $sourceFeeds = Get-Feeds -OrgName $SourceOrgName -ProjectName $SourceProjectName -Headers $SourceHeaders + $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 + + # Create all Target Feeds before adding packages to each feed + $newTargetFeeds = @() + foreach ($feed in $sourceFeeds) { + $existingFeed = $targetFeeds | Where-Object { $_.Name -ieq $feed.Name } + if ($null -ne $existingFeed) { + Write-Log -Message "Feed [$($feed.Name)] already exists in target.. " + $newTargetFeeds += $existingFeed + continue + } + + Write-Log -Message "Creating New Feed [$($feed.Name)] in target.. " + # Create the target Feed first with public type upstream sources and then update the feeds internal upstream sources after all the feeds have been created + + $publicUpstreamSources = @() + foreach ($source in $feed.upstreamSources) { + 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")) { + 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 { + # 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 + } + } + } + + Write-Log -Message "Done!" -LogLevel SUCCESS + $newTargetFeeds += $targetFeed + } + } + + + # DO UPDATE OF THE FEEDS WITH INTERNAL UPSTREAM SOURCES + foreach ($feed in $sourceFeeds) { + $internalUpstreamSources = @() + foreach ($source in $feed.upstreamSources) { + if($source.upstreamSourceType -eq "internal") { + $internalUpstreamSources += $source + } + } + + + if ($internalUpstreamSources.count -gt 0) { + Write-Log -Message "Validating and updating Internal Upstream Sources for [$($feed.Name)] in target.. " + + $existingSourceFeed = $newTargetFeeds | Where-Object { $_.Name -ieq $feed.Name } + + $upstreamSources = $existingSourceFeed.upstreamSources + foreach ($internalSource in $internalUpstreamSources) { + $existingSounceFeedSource = $existingSourceFeed.upstreamSources | Where-Object { $_.Name -ieq $internalSource.Name } + if ($null -ne $existingSounceFeedSource) { + Write-Log -Message "Feed [$($feed.Name)] internal Upstream Source [$($internalSource.Name)] already exists in target Feed.. " + continue + } + + 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 } + + $targetInternalUpstreamFeed = $newTargetFeeds | Where-Object { $_.name -eq $sourceInternalUpstreamFeed.Name } + $targetInternalUpstreamFeedViews = Get-Views -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -FeedId $targetInternalUpstreamFeed.Id + $targetInternalUpstreamFeedView = $targetInternalUpstreamFeedViews | Where-Object { $_.Name -eq $sourceInternalUpstreamFeedView.Name } + + $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)) { + $newSource = @{ + "name" = $internalSource.name + "protocol" = $internalSource.protocol + "upstreamSourceType" = "internal" + "internalUpstreamCollectionId" = $targetInternalUpstreamCollectionId # $internalSource.internalUpstreamCollectionId + "internalUpstreamFeedId" = $targetInternalSourceFeed.Id + "internalUpstreamViewId" = $targetInternalUpstreamFeedView.id + "internalUpstreamProjectId" = $targetProject.Id + } + $upstreamSources += $newSource + } 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 { + $upstreamSources += $internalSource + } + + if($upstreamSources.count -gt 0) { + Update-Feed -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $TargetHeaders -FeedId $existingSourceFeed.Id -UpstreamSources $upstreamSources + } + } + } + } + + + foreach ($newTargetFeed in $newTargetFeeds) { + $sourceFeed = $sourceFeeds | Where-Object { $_.Name -eq $newTargetFeed.Name } + + Write-Log -Message '--------------------------------------------------------------------' + Write-Log -Message "Creating New Packages for Feed [$($newTargetFeed.Name)] in target.. " + Write-Log -Message '--------------------------------------------------------------------' + + $sourceFeedIndexUrl = $sourceFeed._links.packages.href + $sourceFeedIndexUrl = "https://pkgs.dev.azure.com/$SourceOrgName/$SourceProjectName/_packaging/$($sourceFeed.Name)/nuget/v3/index.json" + $destinationIndexUrl = "https://pkgs.dev.azure.com/$TargetOrgName/$TargetProjectName/_packaging/$($newTargetFeed.Name)/nuget/v3/index.json" + + $params = @{ + SourceIndexUrl = $sourceFeedIndexUrl + SourcePAT = $SourcePAT + DestinationIndexUrl = $destinationIndexUrl + DestinationPAT = $TargetPAT + DestinationFeedName = $newTargetFeed.Name + NumVersions = $ArtifactFeedPackageVersionLimit + } + + Move-MyGetNuGetPackages -Verbose @params + } + } +} + +function Get-OrganizationId { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + try { + # 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 + + # Get organizations for user id + $url = "https://app.vssps.visualstudio.com/_apis/accounts?memberId=$($id)&api-version=7.0" + $result2 = Invoke-RestMethod -Method GET -uri $url -Headers $Headers + $organizations = $result2.value + + foreach($org in $organizations) { + if($org.accountName -eq $OrgName) { + return $org.accountId + } + } + } catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR } catch {} + } + return $NULL + } +} + + +function Get-Feeds { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds?api-version=7.0" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers + + return $results.Value + } +} + + +function Get-Feed { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$FeedId + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/Packaging/Feeds/$($FeedId)?api-version=7.0" + + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers + + return $results + } +} + + +function Update-Feed { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$FeedId, + + [Parameter (Mandatory = $TRUE)] + [AllowEmptyCollection()] + [Object[]]$UpstreamSources + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/feeds/$($FeedId)?api-version=6.1-preview.1" + + $body = @{ + upstreamSources = @() + $UpstreamSources + } | ConvertTo-Json + + $results = Invoke-RestMethod -Method PATCH -Uri $url -Headers $Headers -Body $body -ContentType "application/json" + + return $results + } +} + + +function New-ADOFeed { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [Object]$SourceFeed, + + [Parameter (Mandatory = $TRUE)] + [AllowEmptyCollection()] + [Object[]]$UpstreamSources + ) + 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)) { + $hideDeletedPackageVersions = $TRUE + } + + $body = @{ + "name" = $SourceFeed.name + "description" = $SourceFeed.description + "hideDeletedPackageVersions" = $hideDeletedPackageVersions + "capabilities" = $SourceFeed.capabilities + upstreamSources = @() + $UpstreamSources + } | ConvertTo-Json + + try { + Invoke-RestMethod -Method Post -uri $url -Headers $Headers -Body $body -ContentType "application/json" + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + return $NULL + } + } +} + + + +function Get-Views { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$FeedId + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + $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 + + return $results.Value + } +} + + +function Update-View { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$FeedId, + + [Parameter (Mandatory = $TRUE)] + [String]$ViewId, + + [Parameter (Mandatory = $TRUE)] + [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" + + $body = @{ + "visibility" = $Visibility + } | ConvertTo-Json + + $results = Invoke-RestMethod -Method PATCH -Uri $url -Headers $Headers -Body $body -ContentType "application/json" + + return $results + } +} + + +function Get-Packages { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [string]$FeedId + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + $url = "https://feeds.dev.azure.com/$OrgName/$ProjectName/_apis/packaging/Feeds/$($FeedId)/packages?api-version=7.0" + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $headers + return $results.Value + } +} + +function Start-Command +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $CommandTitle, + + [Parameter()] + $CommandArguments + ) + + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = $CommandTitle + $processInfo.RedirectStandardError = $true + $processInfo.RedirectStandardOutput = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + $processInfo.Arguments = $CommandArguments + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + $process.Start() | Out-Null + + $output = $process.StandardOutput.ReadToEnd(); + $outerror = $process.StandardError.ReadToEnd(); + + $process.WaitForExit() + + $return = [pscustomobject]@{ + StdOut = $output + StdErr = $outerror + ExitCode = $process.ExitCode + } + + return $return +} + diff --git a/modules/Migrate-ADO-BuildDefinitions.psm1 b/modules/Migrate-ADO-BuildDefinitions.psm1 new file mode 100644 index 0000000..a730dd7 --- /dev/null +++ b/modules/Migrate-ADO-BuildDefinitions.psm1 @@ -0,0 +1,25 @@ + +function Start-ADOBuildDefinitionsMigration { + [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 Build Definitions from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '-------------------------------' + Write-Log -Message '-- Migrate Build Definitions --' + Write-Log -Message '-------------------------------' + Write-Log -Message ' ' + + # DO WORK HERE + } +} + diff --git a/modules/Migrate-ADO-BuildEnvironments.psm1 b/modules/Migrate-ADO-BuildEnvironments.psm1 new file mode 100644 index 0000000..64be534 --- /dev/null +++ b/modules/Migrate-ADO-BuildEnvironments.psm1 @@ -0,0 +1,450 @@ + +Using Module ".\Migrate-ADO-Common.psm1" + +function Start-ADOBuildEnvironmentsMigration { + [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)] + [Hashtable]$TargetHeaders, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetPAT, + + [Parameter (Mandatory = $FALSE)] + [Bool]$ReplacePipelinePermissions = $FALSE + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate build Environments from source project $SourceOrg/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '--------------------------------' + Write-Log -Message '-- Migrate Build Environments --' + Write-Log -Message '--------------------------------' + Write-Log -Message ' ' + + Write-Log -Message "Get Source Project to do source lookups.." + $sourceProject = Get-ADOProjects -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders + Write-Log -Message "Get target Project to do source lookups.." + $targetProject = Get-ADOProjects -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders + + Write-Log -Message "Get Source Groups to do source lookups.." + $sourceGroups = Get-ADOGroups -OrgName $SourceOrgName -ProjectName $SourceProjectName -PersonalAccessToken $SourcePAT -GetGroupMembers $FALSE + Write-Log -Message "Get Target Groups to do source lookups.." + $targetGroups = Get-ADOGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName -PersonalAccessToken $TargetPAT -GetGroupMembers $FALSE + + Write-Log -Message "Get Source Environments.." + $sourceEnvironments = Get-BuildEnvironments -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -ProjectId $sourceProject.Id -Top 1000000 + 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 + + $newBuildEnvironments = @() + foreach ($sourceEnvironment in $sourceEnvironments) { + if ($null -ne ($targetEnvironments | Where-Object { $_.Name -ieq $sourceEnvironment.Name })) { + Write-Log -Message "Build environment [$($sourceEnvironment.Name)] already exists in target.. " + $newBuildEnvironments += $sourceEnvironment + continue + } + + Write-Log -Message "Attempting to create [$($sourceEnvironment.Name)] in target.. " + try { + Write-Log -Message " " + Write-Log -Message "Source Environment ID: $($sourceEnvironment.Name)" + Write-Log -Message "Source Environment ID: $($sourceEnvironment.Id)" + Write-Log -Message " " + + New-BuildEnvironment -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -Environment $sourceEnvironment -ProjectId $targetProject.Id + + Write-Log -Message "Done!" -LogLevel SUCCESS + $newBuildEnvironments += $sourceEnvironment + } 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 {} + } + } + Write-Log -Message "------------------------------------------------------------------------------------------------------------------" + } + } +} + +function Get-BuildEnvironments { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectId, + + [Parameter (Mandatory = $False)] + [Int32]$Top = 0 + + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + 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) { + $url = "https://dev.azure.com/$OrgName/$ProjectId/_apis/distributedtask/environments?`$top=$($Top)&api-version=7.1-preview" + } + + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return $results.value + } +} + +function Get-BuildEnvironmentRoleAssignments { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectId, + + [Parameter (Mandatory = $TRUE)] + [String]$EnvironmentId + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://dev.azure.com/$OrgName/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/$($ProjectId)_$($EnvironmentId)" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return $results.value + } +} + + +function Get-IdentityInfo { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $FALSE)] + [String]$IdentityId, + + [Parameter (Mandatory = $FALSE)] + [String]$SubjectDescriptor + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://vssps.dev.azure.com/$OrgName/_apis/identities?identityIds=$($IdentityId)&subjectDescriptors=$($SubjectDescriptor)&api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return $results.Value + } +} + + +function Set-BuildEnvironmentRoleAssignment { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectId, + + [Parameter (Mandatory = $TRUE)] + [String]$EnvironmentId, + + [Parameter (Mandatory = $TRUE)] + [String]$ScopeId, + + [Parameter (Mandatory = $TRUE)] + [Object]$RoleAssignment + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://dev.azure.com/$OrgName/_apis/securityroles/scopes/$ScopeId/roleassignments/resources/$($ProjectId)_$($EnvironmentId)?api-version=6.1-preview.1" + + $roleAssignments = @() + $roleAssignments += $RoleAssignment + $body = ConvertTo-Json -Depth 100 $roleAssignments + + $results = Invoke-RestMethod -Method PUT -uri $url -Headers $Headers -Body $body -ContentType "application/json" + + return $results + } +} + +function Get-BuildEnvironmentPipelinePermissions { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$EnvironmentId + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/pipelines/pipelinePermissions/environment/$EnvironmentId" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return $results + } +} + +function Set-BuildEnvironmentPipelinePermissions { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$EnvironmentId, + + [Parameter (Mandatory = $TRUE)] + [AllowEmptyCollection()] + [Object[]]$PipelinePermissions + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/pipelines/pipelinePermissions/environment/$($EnvironmentId)?api-version=7.0-preview.1" + + $permissions = @() + $permissions += $PipelinePermissions + + $body = @{ + "pipelines" = $permissions + } + $body = ConvertTo-Json -Depth 100 $body + + $results = Invoke-RestMethod -Method PATCH -uri $url -Headers $Headers -Body $body -ContentType "application/json" + + return $results.pipelines + } +} + +function New-BuildEnvironment { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Object]$environment, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectId + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $url = "https://dev.azure.com/$OrgName/$ProjectId/_apis/distributedtask/environments?api-version=6.1-preview.1" + + $body = @{ + "name" = $environment.Name + "description" = $environment.Description + } | ConvertTo-Json + + $results = Invoke-RestMethod -Method Post -uri $url -Headers $Headers -Body $body -ContentType "application/json" + + return $results + } +} \ No newline at end of file diff --git a/modules/Migrate-ADO-BuildQueues.psm1 b/modules/Migrate-ADO-BuildQueues.psm1 index 19c4a2c..f4c6756 100644 --- a/modules/Migrate-ADO-BuildQueues.psm1 +++ b/modules/Migrate-ADO-BuildQueues.psm1 @@ -44,6 +44,7 @@ function Start-ADOBuildQueuesMigration { Write-Log -Message ' ' Write-Log -Message '--------------------------' Write-Log -Message '-- Migrate Build Queues --' + Write-Log -Message '- (Project Agent Pools) -' Write-Log -Message '--------------------------' Write-Log -Message ' ' @@ -73,9 +74,9 @@ function Get-BuildQueues { [Hashtable]$Headers ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $project = Get-ADOProjects -org $OrgName -Headers $Headers -ProjectName $ProjectName + $project = Get-ADOProjects -OrgName $OrgName -ProjectName $ProjectName -Headers $Headers - $url = "https://dev.azure.com/$OrgName/$($project.id)/_apis/distributedtask/queues?api-version=5.1-preview" + $url = "https://dev.azure.com/$OrgName/$($project.id)/_apis/distributedtask/queues?api-version=7.0" $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers @@ -145,9 +146,9 @@ function New-BuildQueue { [Hashtable]$Headers ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $project = Get-ADOProjects -OrgName $OrgName -Headers $Headers -ProjectName $ProjectName + $project = Get-ADOProjects -OrgName $OrgName -ProjectName $ProjectName -Headers $Headers - $url = "https://dev.azure.com/$OrgName/$($project.id)/_apis/distributedtask/queues?api-version=5.1-preview&authorizePipelines=true" + $url = "https://dev.azure.com/$OrgName/$($project.id)/_apis/distributedtask/queues?api-version=7.0&authorizePipelines=true" $body = @{ "projectId" = $queue.ProjectId diff --git a/modules/Migrate-ADO-Common.psm1 b/modules/Migrate-ADO-Common.psm1 index ecd512b..396cc65 100644 --- a/modules/Migrate-ADO-Common.psm1 +++ b/modules/Migrate-ADO-Common.psm1 @@ -6,6 +6,89 @@ ERROR } + +class ADO_Team { + [String]$Id + [String]$Name + [String]$Description + + ADO_Team( + [String]$id, + [String]$name, + [String]$description + ) { + $this.Id = $id + $this.Name = $name + $this.Description = $description + } +} + +class ADO_User { + [String]$Id + [String]$OriginId + [String]$PrincipalName + [String]$DisplayName + [String]$MailAddress + [String]$LicenseType + + ADO_User( + [String]$id, + [String]$originId, + [String]$principalName, + [String]$displayName, + [String]$mailAddress, + [String]$licenseType + ) { + $this.Id = $id + $this.OriginId = $originId + $this.PrincipalName = $principalName + $this.DisplayName = $displayName + $this.MailAddress = $mailAddress + $this.LicenseType = $licenseType + } +} + +class ADO_Group { + [String]$Id + [String]$Name + [String]$PrincipalName + [String]$Description + [String]$Descriptor + [ADO_GroupMember[]]$UserMembers = @() + [ADO_Group[]]$GroupMembers = @() + + ADO_Group( + [String]$id, + [String]$name, + [String]$principalName, + [String]$description, + [String]$descriptor + ) { + $this.Id = $id + $this.Name = $name + $this.PrincipalName = $principalName + $this.Description = $description + $this.Descriptor = $descriptor + } +} + +class ADO_GroupMember { + [String]$Id + [String]$Name + [String]$PrincipalName + + ADO_GroupMember( + [String]$id, + [String]$name, + [String]$principalName + ) { + $this.Id = $id + $this.Name = $name + $this.PrincipalName = $principalName + } +} + + function New-HTTPHeaders { [CmdletBinding(SupportsShouldProcess)] param( @@ -39,31 +122,6 @@ function Set-AzDevOpsContext { } } -function Get-ADOProjects { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [Hashtable]$Headers, - - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, - - [Parameter (Mandatory = $FALSE)] - [String]$ProjectName - ) - if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { - if ($ProjectName) { - $url = "https://dev.azure.com/$OrgName/_apis/projects/$ProjectName" - return Invoke-RestMethod -Method Get -uri $url -Headers $Headers - } - else { - $url = "https://dev.azure.com/$OrgName/_apis/projects?`$top=600&api-version=5.1" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers - return $results.value - } - } -} - function Write-Log { [CmdletBinding(SupportsShouldProcess)] param( @@ -296,4 +354,360 @@ function Set-ProjectFolders { } Write-Log -Message '-------------------------- Done creating Directories --------------------------' -LogLevel DEBUG -} \ No newline at end of file +} + + +# Users + +function Get-ADOUsers { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$PersonalAccessToken + ) + if ($PSCmdlet.ShouldProcess($OrgName)) { + Set-AzDevOpsContext -PersonalAccessToken $PersonalAccessToken -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 " " + + # Convert to ADO User objects + [ADO_User[]]$users = @() + foreach ($orgUser in $members ) { + $users += [ADO_User]::new($orgUser.Id, $orgUser.user.originId, $orgUser.user.principalName, $orgUser.user.displayName, $orgUser.user.mailAddress, $orgUser.accessLevel.accountLicenseType) + } + + return $users + } +} + +function Get-ADOUsersByAPI { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + if ($PSCmdlet.ShouldProcess($OrgName)) { + + $url = "https://vssps.dev.azure.com/$OrgName/_apis/graph/users?api-version=7.0-preview.1" + + $members = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + # Convert to ADO User objects + [ADO_User[]]$users = @() + foreach ($orgUser in $members.Value ) { + $users += [ADO_User]::new($orgUser.Id, $orgUser.originId, $orgUser.principalName, $orgUser.displayName, $orgUser.mailAddress, "") + } + + return $users + } +} + +# ADO Groups +function Get-ADOGroups { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$PersonalAccessToken, + + [Parameter (Mandatory = $FALSE)] + [String]$GroupDisplayName, + + [Parameter (Mandatory = $FALSE)] + [Bool]$GetGroupMembers = $TRUE + ) + if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { + Set-AzDevOpsContext ` + -PersonalAccessToken $PersonalAccessToken ` + -OrgName $OrgName ` + -ProjectName $ProjectName + + $organization = "https://dev.azure.com/$OrgName/" + if ($GroupDisplayName) { + $groups = az devops security group list --query "graphGroups[?displayName == '$($GroupDisplayName)']" --organization $organization --project $ProjectName --detect $false | ConvertFrom-Json + if (!$groups) { + throw "Group called '$GroupDisplayName' cannot be found in '$OrgName/$ProjectName'" + } + } + else { + $groups = (az devops security group list --organization $organization --project $ProjectName --detect $false --subject-types vssgp | ConvertFrom-Json).graphGroups + } + + [ADO_Group[]]$groupsFound = @() + foreach ($group in $groups) { + Write-Host "." -NoNewline + $group = [ADO_Group]::new($group.originId, $group.displayName, $group.principalName, $group.description, $group.descriptor) + + if ($GetGroupMembers -eq $TRUE) { + $members = Get-ADOGroupMembers ` + -OrgName $OrgName ` + -ProjectName $ProjectName ` + -PersonalAccessToken $PersonalAccessToken ` + -GroupDescriptor $group.Descriptor + + $group.GroupMembers = $members.GroupGroupMembers + $group.UserMembers = $members.GroupUserMembers + } + + $groupsFound += $group + } + Write-Host "." + return $groupsFound + } +} + +function Get-ADOGroupMembers { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$PersonalAccessToken, + + [Parameter (Mandatory = $TRUE)] + [String]$GroupDescriptor + ) + if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { + Set-AzDevOpsContext ` + -PersonalAccessToken $PersonalAccessToken ` + -OrgName $OrgName ` + -ProjectName $ProjectName + + [ADO_GroupMember[]]$GroupUserMembers = @() + [ADO_Group[]]$GroupGroupMembers = @() + + $organization = "https://dev.azure.com/$OrgName/" + try { + $members = az devops security group membership list --id $GroupDescriptor --organization $organization --detect $false | ConvertFrom-Json + } catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + + if ($members) { + $descriptors = $members | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name + + foreach ($descriptor in $descriptors) { + Write-Host "." -NoNewline + $member = $members.$descriptor + if ($member.subjectKind -eq "user") { + $GroupUserMembers += [ADO_GroupMember]::new($member.originId, $member.displayName, $member.principalName) + } + else { + $GroupGroupMembers += [ADO_Group]::new($member.originId, $member.displayName, $member.principalName, $member.description, $member.descriptor) + } + } + } + + return @{ + "GroupUserMembers" = $GroupUserMembers + "GroupGroupMembers" = $GroupGroupMembers + } + } +} + + + +# Projects +function Get-ADOProjects { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $FALSE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { + if ($ProjectName) { + $url = "https://dev.azure.com/$OrgName/_apis/projects/$ProjectName" + return Invoke-RestMethod -Method Get -uri $url -Headers $Headers + } + else { + $url = "https://dev.azure.com/$OrgName/_apis/projects?`$top=600&api-version=5.1" + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + return $results.value + } + } +} + +function Get-ADOProjectTeams { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $FALSE)] + [String]$TeamDisplayName + ) + if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { + $url = "https://dev.azure.com/$OrgName/_apis/projects/$ProjectName/teams?api-version=6.0" + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + [ADO_Team[]]$teams = @() + foreach ($result in $results.value) { + $teams += [ADO_Team]::new($result.id, $result.name, $result.description) + } + + if ($TeamDisplayName) { + return $teams | Where-Object { $_.Name -eq $TeamDisplayName } + } + return $teams + } +} + + +# Pipelines +function Get-Pipelines { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $FALSE)] + [String]$RepoId = $NULL + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + + $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.value + } +} + +function Get-Pipeline { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers, + + [Parameter (Mandatory = $FALSE)] + [String]$DefinitionId = $NULL + ) + if ($PSCmdlet.ShouldProcess($ProjectName)) { + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/build/definitions/$($DefinitionId)?api-version=7.0"; + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return $results + } +} + + +#Repos + +function Get-Repos([string]$projectName, [string]$orgName, $headers) { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/git/repositories?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + if ($ProcessName) { + return $results.value | Where-Object { $_.name -ieq $ProcessName } + } + else { + return , $results.value + } +} + +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 + + return , $results +} + + +function New-GitRepository { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$ProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$OrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$RepoName, + + [Parameter (Mandatory = $TRUE)] + [Hashtable]$Headers + ) + if ($PSCmdlet.ShouldProcess($ProjectName, "Push repos from $ReposPath")) { + $url = "$org/_apis/git/repositories?api-version=5.1" + } + $url = "https://dev.azure.com/$OrgName/_apis/git/repositories?api-version=5.1" + + $project = Get-ADOProjects -OrgName $OrgName -ProjectName $ProjectName -Headers $Headers + + $requestBody = @{ + name = $RepoName + project = @{ + id = $project.id + } + } | ConvertTo-Json + + try { + 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) " + } +} diff --git a/modules/Migrate-ADO-Dashboards.psm1 b/modules/Migrate-ADO-Dashboards.psm1 new file mode 100644 index 0000000..24b0c24 --- /dev/null +++ b/modules/Migrate-ADO-Dashboards.psm1 @@ -0,0 +1,260 @@ + +function Start-ADODashboardsMigration { + [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 Dashboards from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '------------------------' + Write-Log -Message '-- Migrate Dashboards --' + Write-Log -Message '------------------------' + Write-Log -Message ' ' + + + $sourceTeams = [array](Get-Teams -orgName $SourceOrgName -projectName $SourceProjectName -headers $SourceHeaders) + $targetTeams = [array](Get-Teams -orgName $TargetOrgName -projectName $TargetProjectName -headers $TargetHeaders) + + $sourceDashboards = Get-Dashboards -orgName $SourceOrgName -projectName $SourceProjectName -headers $SourceHeaders + $targetDashboards = Get-Dashboards -orgName $TargetOrgName -projectName $TargetProjectName -headers $TargetHeaders + + $completed_list = New-Object Collections.Generic.List[string] + + :teamsLoop foreach ($team in $sourceTeams) { + Write-Log -Message "--- Team Dashboard: $($team.Name) ---" + $dashboards = Get-Dashboards -orgName $SourceOrgName -projectName $SourceProjectName -team $team.name -headers $SourceHeaders + + :dashboardLoop foreach ($dashboard in $dashboards) { + Write-Log -Message "Team: $($team.name) Dashboard: $($dashboard.name) DashboardScope: $($dashboard.dashboardScope)" + + $targetTeam = $targetTeams | Where-Object { $_.Name -eq $team.name } + if($NULL -eq $targetTeam) { + Write-Log -Message "Dashboard [$($dashboard.Name) ($($dashboard.Id))] cannot be migrated. It is a project_Team dashboard and the Team [$team.name] does not exist in the Target project.. " + } + + $targetDashboard = $targetDashboards | Where-Object { ($_.Name -eq $dashboard.name.Trim()) -and ($_.groupId -eq $targetTeam.Id) } + $fullSourceDashboard = Get-Dashboard -orgName $SourceOrgName -projectName $SourceProjectName -team $team.name -dashboardId $dashboard.Id -headers $SourceHeaders + + if ($null -ine $targetDashboard) { + Write-Log -Message "Dashboard [$($targetDashboard.Name) ($($targetDashboard.Id))] already exists in target.. " + + $fullTargetDashboard = Get-Dashboard -orgName $TargetOrgName -projectName $TargetProjectName -team $team.name -dashboardId $targetDashboard.Id -headers $TargetHeaders + + # See if the widgets for the Dashboard are migrated.. + if($fullTargetDashboard.Widgets.Count -lt $fullSourceDashboard.Widgets.Count) { + + try { + $fullTargetDashboard.Widgets = $fullSourceDashboard.Widgets + + Write-Log -Message "Updating Dashboard Widgets for [$($fullTargetDashboard.Name)] in target.. " + Edit-Dashboard -orgName $targetOrgName -projectName $TargetProjectName -team $targetTeam.Id -headers $TargetHeaders -dashboard $fullTargetDashboard + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + } + $completed_list.Add($dashboard.Name) + continue + } + + try { + Write-Log -Message "CREATING Dashboard [$($dashboard.Name)] in target.. " + $payload = @{ + "name" = $fullSourceDashboard.name.Trim() + "description" = $fullSourceDashboard.description + "dashboardScope" = $fullSourceDashboard.dashboardScope + "position" = $fullSourceDashboard.position + "widgets" = $fullSourceDashboard.widgets + "refreshInterval" = $fullSourceDashboard.refreshInterval + "url" = $fullSourceDashboard.url + } + + New-Dashboard -orgName $targetOrgName -projectName $TargetProjectName -team $team.name -headers $TargetHeaders -dashboard $payload + $completed_list.Add($dashboard.Name) + } catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + } + } + + + # Add Dashboards not tied to a Team + $projectDashboards = $sourceDashboards | Where-Object { $_.Name -notin $completed_list } + $targetDashboards = Get-Dashboards -orgName $TargetOrgName -projectName $TargetProjectName -headers $TargetHeaders + Write-Log -Message "--- Project Dashboards: ---" + ForEach ($dashboard in $projectDashboards) { + Write-Log -Message "dashboard: $($dashboard.name) dashboard scope: $($dashboard.dashboardScope)" + + $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.. " + } + $fullSourceDashboard = Get-Dashboard -orgName $SourceOrgName -projectName $sourceProjectName -dashboardId $dashboard.Id -headers $SourceHeaders + + if($dashboard.dashboardScope -eq "project_Team") { + Write-Log -Message "Dashboards with name [$($dashboard.Name)] was not found with Team Dashboards and has a dashboard scope of project_Team." + Write-Log -Message "Something is wrong with this dashboard and will need to be migrated manually.." + continue + } + + 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)].. " + + $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 + } + + try { + Write-Log -Message "CREATING Dashboard [$($dashboard.Name)] in target.. " + $payload = @{ + "name" = $fullSourceDashboard.name.Trim() + "description" = $fullSourceDashboard.description + "dashboardScope" = $fullSourceDashboard.dashboardScope + "position" = $fullSourceDashboard.position + "widgets" = $fullSourceDashboard.widgets + "refreshInterval" = $fullSourceDashboard.refreshInterval + "url" = $fullSourceDashboard.url + } + + New-Dashboard -orgName $targetOrgName -projectName $targetProjectName -headers $targetHeaders -dashboard $payload + $completed_list.Add($dashboard.Name) + + } 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 ' ' + } +} + + +# Dashboards +function Get-Dashboards([string]$orgName, [string]$projectName, [string]$team, $headers) { + if ($team) { + $url = "https://dev.azure.com/$orgName/$projectName/$team/_apis/dashboard/dashboards?api-version=7.0-preview.3" + } + else { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/dashboard/dashboards?api-version=7.0-preview.3" + } + + + try { + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + } + catch { + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log $_ + } + + return $results.Value +} + +function Get-Dashboard([string]$orgName, [string]$projectName, [string]$team, [string]$dashboardId, $headers) { + if ($team) { + $url = "https://dev.azure.com/$orgName/$projectName/$team/_apis/dashboard/dashboards/$($dashboardId)?api-version=7.0-preview.3" + } + else { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/dashboard/dashboards/$($dashboardId)?api-version=7.0-preview.3" + } + + try { + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + } + catch { + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log $_ + } + + return $results +} + + +function New-Dashboard([string]$orgName, [string]$projectName, [string]$team, $headers, $dashboard) { + if ($team) { + $url = "https://dev.azure.com/$orgName/$projectName/$team/_apis/dashboard/dashboards?api-version=7.0-preview.3" + } + else { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/dashboard/dashboards?api-version=7.0-preview.3" + } + + $body = $dashboard | ConvertTo-Json -Depth 100 + + try { + $results = Invoke-RestMethod -Method Post -uri $url -Headers $headers -Body $body -ContentType "application/json" + } + catch { + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log $_ + } + + return $results +} + +function Edit-Dashboard([string]$orgName, [string]$projectName, [string]$team, $headers, $dashboard) { + Write-Log -Message "Updating Dashboard.." + if ($team) { + $url = "https://dev.azure.com/$orgName/$projectName/$team/_apis/dashboard/dashboards/$($dashboard.Id)?api-version=7.0-preview.3" + } + else { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/dashboard/dashboards/$($dashboard.Id)?api-version=7.0-preview.3" + } + + $body = $dashboard | ConvertTo-Json -Depth 100 + + try { + $results = Invoke-RestMethod -Method PUT -uri $url -Headers $headers -Body $body -ContentType "application/json" + } + catch { + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log $_ + } + + return $results +} + + + +# Teams +function Get-Teams([string]$orgName, [string]$projectName, $headers) { + $url = "https://dev.azure.com/$orgName/_apis/projects/$projectName/teams?api-version=7.0" + + try { + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + } + catch { + Write-Log -Message $_.Exception -LogLevel ERROR + Write-Log $_ + } + + return $results.Value +} diff --git a/modules/Migrate-ADO-DeliveryPlans.psm1 b/modules/Migrate-ADO-DeliveryPlans.psm1 new file mode 100644 index 0000000..e6d35de --- /dev/null +++ b/modules/Migrate-ADO-DeliveryPlans.psm1 @@ -0,0 +1,86 @@ + +function Start-ADODeliveryPlansMigration { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] [String]$SourceOrgName, + [Parameter (Mandatory = $TRUE)] [String]$SourceProjectName, + [Parameter (Mandatory = $TRUE)] [Hashtable]$SourceHeaders, + [Parameter (Mandatory = $TRUE)] [String]$SourcePAT, + [Parameter (Mandatory = $TRUE)] [String]$TargetOrgName, + [Parameter (Mandatory = $TRUE)] [String]$TargetProjectName, + [Parameter (Mandatory = $TRUE)] [Hashtable]$TargetHeaders, + [Parameter (Mandatory = $TRUE)] [String]$TargetPAT + + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate DeliveryPlans from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '---------------------------' + Write-Log -Message '-- Migrate DeliveryPlans --' + Write-Log -Message '---------------------------' + Write-Log -Message ' ' + + + $sourceDeliveryPlans = (Get-DeliveryPlans -projectName $sourceProjectName -orgName $SourceOrgName -headers $SourceHeaders).Value + $targetDeliveryPlans = (Get-DeliveryPlan -projectName $targetProjectName -orgName $TargetOrgName -headers $TargetHeaders).Value + + + ForEach ($deliveryplan in $sourceDeliveryPlans) { + Write-Log -Message "Migrating DeliveryPlan: $($deliveryplan.name).." + + if ($null -ne ($targetDeliveryPlans | Where-Object { $_.name -ieq $deliveryplan.name } )) { + Write-Log -Message "DeliveryPlan [$($deliveryplan.Name)] already exists in target.. " + continue + } + + try { + + $plan = Get-DeliveryPlan -ProjectName $sourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders -Id $deliveryplan.Id + New-DeliveryPlan -projectName $targetProjectName -OrgName $targetOrgName -Headers $targetHeaders -Deliveryplan @{ + "name" = $plan.name + "description" = $plan.description + "type" = $plan.type + "properties" = $plan.properties + } + + } catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + } + } +} + +function Get-DeliveryPlans([string]$OrgName, [string]$ProjectName, $Headers) { + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/work/plans?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return $results +} + +function Get-DeliveryPlan([string]$OrgName, [string]$ProjectName, [string]$Id, $Headers) { + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/work/plans/$($id)?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return $results +} + + +function New-DeliveryPlan([string]$OrgName, [string]$ProjectName, $Headers, $DeliveryPlan) { + $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/work/plans?api-version=7.0" + + $body = $deliveryPlan | ConvertTo-Json -Depth 10 + + $results = Invoke-RestMethod -Method Post -uri $url -Headers $Headers -Body $body -ContentType "application/json" + + return $results +} + + diff --git a/modules/Migrate-ADO-Groups.psm1 b/modules/Migrate-ADO-Groups.psm1 index 89d3fd8..3d25577 100644 --- a/modules/Migrate-ADO-Groups.psm1 +++ b/modules/Migrate-ADO-Groups.psm1 @@ -1,42 +1,5 @@ -class ADO_Group { - [String]$Id - [String]$Name - [String]$PrincipalName - [String]$Description - [String]$Descriptor - [ADO_GroupMember[]]$UserMembers = @() - [ADO_Group[]]$GroupMembers = @() - - ADO_Group( - [String]$id, - [String]$name, - [String]$principalName, - [String]$description, - [String]$descriptor - ) { - $this.Id = $id - $this.Name = $name - $this.PrincipalName = $principalName - $this.Description = $description - $this.Descriptor = $descriptor - } -} -class ADO_GroupMember { - [String]$Id - [String]$Name - [String]$PrincipalName - - ADO_GroupMember( - [String]$id, - [String]$name, - [String]$principalName - ) { - $this.Id = $id - $this.Name = $name - $this.PrincipalName = $principalName - } -} +Using Module ".\Migrate-ADO-Common.psm1" function Start-ADOGroupsMigration { [CmdletBinding(SupportsShouldProcess)] @@ -57,83 +20,51 @@ function Start-ADOGroupsMigration { [String]$TargetOrgName, [Parameter (Mandatory = $TRUE)] - [String]$TargetPAT + [String]$TargetPAT, + + [Parameter (Mandatory = $FALSE)] + [String]$VerboseOutput = $FALSE ) if ($PSCmdlet.ShouldProcess( "Target project $TargetOrg/$TargetProjectName", "Migrate groups & members from source project $SourceOrg/$SourceProjectName") ) { Write-Log -Message ' ' - Write-Log -Message '--------------------' - Write-Log -Message '-- Migrate Groups --' - Write-Log -Message '--------------------' + Write-Log -Message '------------------------' + Write-Log -Message '-- Migrate ADO Groups --' + Write-Log -Message '------------------------' Write-Log -Message ' ' - $groups = Get-ADOGroups ` + Write-Log -Message 'Get Source ADO Groups' + $sourceGroups = Get-ADOGroups ` -OrgName $SourceOrgName ` -ProjectName $SourceProjectName ` - -PersonalAccessToken $SourcePAT + -PersonalAccessToken $SourcePAT ` + -GroupDisplayName $GroupDisplayName + + Write-Log -Message 'Get target ADO Groups' + $targetGroups = Get-ADOGroups ` + -OrgName $TargetOrgName ` + -ProjectName $TargetProjectName ` + -PersonalAccessToken $TargetPAT + Write-Log -Message 'Migrate ADO Groups' Push-ADOGroups ` -PersonalAccessToken $TargetPAT ` -OrgName $TargetOrgName ` -ProjectName $TargetProjectName ` - -Groups $groups + -SourceGroups $sourceGroups ` + -TargetGroups $targetGroups ` + -VerboseOutput $VerboseOutput } } -function Get-ADOGroups { +function Push-ADOGroups { [CmdletBinding(SupportsShouldProcess)] param( - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, - - [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, - [Parameter (Mandatory = $TRUE)] [String]$PersonalAccessToken, - [Parameter (Mandatory = $FALSE)] - [String]$GroupDisplayName - ) - if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { - Set-AzDevOpsContext ` - -PersonalAccessToken $PersonalAccessToken ` - -OrgName $OrgName ` - -ProjectName $ProjectName - - if ($GroupDisplayName) { - $groups = az devops security group list --query "graphGroups[?displayName == '$($GroupDisplayName)']" --detect $false | ConvertFrom-Json - if (!$groups) { - throw "Group called '$GroupDisplayName' cannot be found in '$OrgName/$ProjectName'" - } - } - else { - $groups = (az devops security group list --detect $false | ConvertFrom-Json).graphGroups - } - - [ADO_Group[]]$groupsFound = @() - foreach ($group in $groups) { - $group = [ADO_Group]::new($group.originId, $group.displayName, $group.principalName, $group.description, $group.descriptor) - $members = Get-ADOGroupMembers ` - -OrgName $OrgName ` - -ProjectName $ProjectName ` - -PersonalAccessToken $PersonalAccessToken ` - -GroupDescriptor $group.Descriptor - - $group.GroupMembers = $members.GroupGroupMembers - $group.UserMembers = $members.GroupUserMembers - - $groupsFound += $group - } - return $groupsFound - } -} - -function Get-ADOGroupMembers { - [CmdletBinding(SupportsShouldProcess)] - param( [Parameter (Mandatory = $TRUE)] [String]$OrgName, @@ -141,55 +72,13 @@ function Get-ADOGroupMembers { [String]$ProjectName, [Parameter (Mandatory = $TRUE)] - [String]$PersonalAccessToken, - - [Parameter (Mandatory = $TRUE)] - [String]$GroupDescriptor - ) - if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { - Set-AzDevOpsContext ` - -PersonalAccessToken $PersonalAccessToken ` - -OrgName $OrgName ` - -ProjectName $ProjectName - - [ADO_GroupMember[]]$GroupUserMembers = @() - [ADO_Group[]]$GroupGroupMembers = @() - $members = az devops security group membership list --id $GroupDescriptor --detect $false | ConvertFrom-Json - if ($members) { - $descriptors = $members | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name - - foreach ($descriptor in $descriptors) { - $member = $members.$descriptor - if ($member.subjectKind -eq "user") { - $GroupUserMembers += [ADO_GroupMember]::new($member.originId, $member.displayName, $member.principalName) - } - else { - $GroupGroupMembers += [ADO_Group]::new($member.originId, $member.displayName, $member.principalName, $member.description, $member.descriptor) - } - } - } - - return @{ - "GroupUserMembers" = $GroupUserMembers - "GroupGroupMembers" = $GroupGroupMembers - } - } -} - -function Push-ADOGroups { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [String]$PersonalAccessToken, - - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, + [Object]$SourceGroups, [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, + [Object]$TargetGroups, - [Parameter (Mandatory = $TRUE)] - [ADO_Group[]]$Groups + [Parameter (Mandatory = $FALSE)] + [String]$VerboseOutput = $FALSE ) if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { Set-AzDevOpsContext ` @@ -197,40 +86,45 @@ function Push-ADOGroups { -OrgName $OrgName ` -ProjectName $ProjectName - $targetGroups = Get-ADOGroups ` - -OrgName $OrgName ` - -ProjectName $ProjectName ` - -PersonalAccessToken $PersonalAccessToken - # Create all groups before adding members to each group - [ADO_Group[]]$newTargetGroups = @() - foreach ($group in $Groups) { - $existingGroup = $targetGroups | Where-Object { $_.Name -ieq $group.Name } + [ADO_Group[]]$processSourceGroups = @() + foreach ($group in $SourceGroups) { + $existingGroup = $TargetGroups | Where-Object { $_.Name -ieq $group.Name } if ($null -ine $existingGroup) { Write-Log -Message "Group [$($group.Name)] already exists in target.. " - $newTargetGroups += $existingGroup + $processSourceGroups += $group continue } + + Write-Log -Message "Creating New Group [$($group.Name)] in target.. " $result = New-ADOGroup ` -PersonalAccessToken $PersonalAccessToken ` -OrgName $OrgName ` -ProjectName $ProjectName ` -GroupName $group.Name ` - -GroupDescription $group.Description - - if ($null -ine $result.NewGroup) { - $newTargetGroups += $result.NewGroup + -GroupDescription $group.Description ` + -VerboseOutput $VerboseOutput + + # If we created new target Group then add the Group to the targetGroups to do lookups for populating Members + if ($null -ne $result.NewGroup) { + $newGroup = [ADO_Group]::new($result.NewGroup.originId, $result.NewGroup.displayName, $result.NewGroup.principalName, $result.NewGroup.description, $result.NewGroup.descriptor) + $targetGroups += $newGroup + $processSourceGroups += $group + } else { + Write-Log -Message "unable to Create New Group [$($group.Name)] in target, it may need to be migrated manually.. " } } - foreach ($newTargetGroup in $newTargetGroups) { - [ADO_Group]$sourceGroup = $Groups | Where-Object { $_.Name -ieq $newTargetGroup.Name } + foreach ($processGroup in $processSourceGroups) { + [ADO_Group]$targetGroup = $TargetGroups | Where-Object { $_.Name -ieq $processGroup.Name } + Push-GroupMembers ` -OrgName $OrgName ` -ProjectName $ProjectName ` -PersonalAccessToken $PersonalAccessToken ` - -SourceGroup $sourceGroup ` - -TargetGroup $newTargetGroup + -SourceGroup $processGroup ` + -TargetGroup $targetGroup ` + -VerboseOutput $VerboseOutput } } } @@ -251,7 +145,10 @@ function New-ADOGroup { [String]$GroupName, [Parameter (Mandatory = $FALSE)] - [String]$GroupDescription = "" + [String]$GroupDescription = "", + + [Parameter (Mandatory = $FALSE)] + [String]$VerboseOutput = $FALSE ) if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { Set-AzDevOpsContext ` @@ -259,12 +156,23 @@ function New-ADOGroup { -OrgName $OrgName ` -ProjectName $ProjectName + $GroupDescription = $GroupDescription.Replace('"',"'") + if ($Group.Description) { - $result = az devops security group create --name $GroupName --description $GroupDescription --detect $false + if($VerboseOutput -eq $TRUE) { + $result = az devops security group create --name $GroupName --description $GroupDescription --detect $false --debug --verbose + } else { + $result = az devops security group create --name $GroupName --description $GroupDescription --detect $false + } } else { - $result = az devops security group create --name $GroupName --detect $false + if($VerboseOutput -eq $TRUE) { + $result = az devops security group create --name $GroupName --detect $false --debug --verbose + } else { + $result = az devops security group create --name $GroupName --detect $false + } } + if (!$result) { Write-Log -Message "Could not create a new group with name '$($GroupName)'. The group name may be reserved by the system." -LogLevel ERROR return @{ @@ -295,7 +203,10 @@ function Push-GroupMembers { [ADO_Group]$SourceGroup, [Parameter (Mandatory = $FALSE)] - [ADO_Group]$TargetGroup = $null + [ADO_Group]$TargetGroup = $null, + + [Parameter (Mandatory = $FALSE)] + [String]$VerboseOutput = $FALSE ) if ($PSCmdlet.ShouldProcess("$GroupDisplayName")) { Set-AzDevOpsContext ` @@ -305,11 +216,22 @@ function Push-GroupMembers { # Add user members foreach ($userMember in $SourceGroup.UserMembers) { - if ($null -ne ($TargetGroup.UserMembers | Where-Object { $_.PrincipalName -ieq $userMember.PrincipalName } )) { - Write-Log -Message "Member [$($userMember.Name)] already exists in target group.. " - continue + try { + if ($null -ne ($TargetGroup.UserMembers | Where-Object { $_.PrincipalName -ieq $userMember.PrincipalName } )) { + Write-Log -Message "User Member [$($userMember.Name)] already exists in target group [$($SourceGroup.Name)].. " + continue + } + + Write-Log -Message "Adding User Member [$($userMember.Name)] in target group [$($SourceGroup.Name)].. " + if($VerboseOutput -eq $TRUE) { + az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $userMember.PrincipalName --detect $false --debug --verbose + } else { + az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $userMember.PrincipalName --detect $false + } + } catch { + Write-Log -Message $_.Exception.Message -LogLevel ERROR } - az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $userMember.PrincipalName --detect $false + } # Add group members foreach ($groupMember in $SourceGroup.GroupMembers) { @@ -321,12 +243,17 @@ function Push-GroupMembers { -GroupDisplayName $groupMember.Name if ($null -ne ($TargetGroup.GroupMembers | Where-Object { $_.Name -ieq $groupMember.Name } )) { - Write-Log -Message "Member [$($groupMember.Name)] already exists in target group.. " + Write-Log -Message "Group Member [$($groupMember.Name)] already exists in target group [$($SourceGroup.Name)].. " continue } - az devops security group membership add --group-id $TargetGroup.Descriptor --member-id $groupOnTarget.PrincipalName --detect $false - } - catch { + + Write-Log -Message "Adding Group Member [$($groupMember.Name)] in target group [$($SourceGroup.Name)].. " + if($VerboseOutput -eq $TRUE) { + 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 { Write-Log -Message $_.Exception.Message -LogLevel ERROR } } diff --git a/modules/Migrate-ADO-Pipelines.psm1 b/modules/Migrate-ADO-Pipelines.psm1 index 6ec645b..93718c2 100644 --- a/modules/Migrate-ADO-Pipelines.psm1 +++ b/modules/Migrate-ADO-Pipelines.psm1 @@ -15,7 +15,7 @@ function Get-Pipelines { ) if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/build/definitions?api-version=5.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"; } diff --git a/modules/Migrate-ADO-Policies.psm1 b/modules/Migrate-ADO-Policies.psm1 new file mode 100644 index 0000000..ab6f3c0 --- /dev/null +++ b/modules/Migrate-ADO-Policies.psm1 @@ -0,0 +1,236 @@ + +Using Module ".\Migrate-ADO-Common.psm1" + +function Start-ADOPoliciesMigration { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] [String]$SourceOrgName, + [Parameter (Mandatory = $TRUE)] [String]$SourceProjectName, + [Parameter (Mandatory = $TRUE)] [Hashtable]$SourceHeaders, + [Parameter (Mandatory = $TRUE)] [String]$SourcePAT, + [Parameter (Mandatory = $TRUE)] [String]$TargetOrgName, + [Parameter (Mandatory = $TRUE)] [String]$TargetProjectName, + [Parameter (Mandatory = $TRUE)] [Hashtable]$TargetHeaders, + [Parameter (Mandatory = $TRUE)] [String]$TargetPAT + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate Policies from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '-----------------------' + Write-Log -Message '-- Migrate Policies --' + Write-Log -Message '----------------------' + Write-Log -Message ' ' + + 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 + + # Write-Log -Message "Get Source Pipelines for source to target mapping.." + $sourcePipelines = Get-Pipelines -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 + + # Write-Log -Message "Get Target Repositories for source to target mapping.." + $sourceRepos = Get-Repos -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders + Write-Log -Message "Get Target Repositories for source to target mapping.." + $targetRepos = Get-Repos -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $TargetHeaders + + # Write-Log -Message "Get Source Users for source to target mapping.." + # $sourceUsers = Get-ADOUsersByAPI -OrgName $SourceOrgName -Headers $SourceHeaders + # # Write-Log -Message "Get Target Users for source to target mapping.." + # $targetUsers = Get-ADOUsersByAPI -OrgName $TargetOrgName -Headers $TargetHeaders + + Write-Log -Message 'Getting ADO Users from Source..' + $sourceUsers = Get-ADOUsers -OrgName $SourceOrgName -PersonalAccessToken $SourcePat + Write-Log -Message 'Getting ADO Users from Target..' + $targetUsers = Get-ADOUsers -OrgName $TargetOrgName -PersonalAccessToken $TargetPat + + Write-Log -Message "Get Source Groups for source to target mapping.." + $sourceGroups = Get-ADOGroups -OrgName $SourceOrgName -ProjectName $SourceProjectName -PersonalAccessToken $SourcePAT -GetGroupMembers $FALSE + Write-Log -Message "Get Target Groups for source to target mapping.." + $targetGroups = Get-ADOGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName -PersonalAccessToken $TargetPAT -GetGroupMembers $FALSE + + Write-Log -Message "Found $($sourcePolicies.Count) policies in source.. " + + foreach ($policy in $sourcePolicies) { + Write-Log -Message "Processing Policy `"$($policy.type.displayName)`" [$($policy.Id)].. " + $strMsg = "" + $processPolicy = $policy + try { + $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 + } + + if ($null -ne $sourceRepo) { + $targetRepo = ($targetRepos | Where-Object { $_.name -ieq $sourceRepo.name }) + if ($null -eq $targetRepo) { + $strMsg += ("Could not find the repositoryId $($entry.name) [$($entry.repositoryId)] in target while attempting to migrate policy." + "`n") + # 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 { + $entry.repositoryId = $targetRepo.id + } + } 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 + } + } + } + + 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 = $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 -ne $sourcePipeline) { + $targetPipeline = ($targetPipelines | Where-Object {$_.Name -eq $sourcePipeline.Name}) + if ($null -ne $targetPipeline) { + $processPolicy.settings.buildDefinitionId = $targetPipeline.id + } 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 { + $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) { + 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){ + # 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 { + $sourceUser = ($sourceUsers | Where-Object { $_.Id -eq $Id }) + $targetUser = ($targetUsers | Where-Object { $_.MailAddress -eq $sourceUser.MailAddress }) + if ($NULL -ne $targetUser) { + $newRequiredReviewerIds += $targetUser.Id + } 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 + } + } + } + + if($failedToFindReviewerId -eq $TRUE) { + $haveMissingComponents = $TRUE + } else { + $processPolicy.settings.requiredReviewerIds = $newRequiredReviewerIds + } + } + + 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 + # Write-Log -Message $policyJson -LogLevel INFO + continue + } + + Write-Log -Message "Attempting to create or locate migrated Policy '$($processPolicy.type.displayName)' [Id: $($processPolicy.id)] in target!" + try { + $result = New-Policy -projectName $targetProjectName -orgName $targetOrgName -headers $targetHeaders -policy $processPolicy + Write-Log -Message "Created NEW Policy '$($processPolicy.type.displayName)' [Id: $($processPolicy.id)] in target!" + Write-Log -Message "Done!" -LogLevel SUCCESS + Write-Host $result + } catch { + $err = ConvertFrom-json -Depth 100 $_ + if($err.typeKey -eq "PolicyChangeRejectedByPolicyException") { + Write-Log -Message "Policy '$($processPolicy.type.displayName)' [Id: $($processPolicy.id)] already exist in target." + } else { + Write-Log -Message ($_) -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + } + } + + Write-Host " " + } catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + } + } +} + + +function Get-Policies([string]$projectName, [string]$orgName, $headers) { + + $url = "https://dev.azure.com/$orgName/$projectName/_apis/policy/configurations" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return , $results.value + +} + +function Get-UserIdentity ([string]$projectName, [string]$orgName, $headers, $identityId) { + + $url = "https://vssps.dev.azure.com/$orgName/_apis/identities?identityIds=$($identityId)&api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return $results.Value + +} + +function Get-UserByDescriptor ([string]$projectName, [string]$orgName, $headers, $descriptorId) { + + $url = "https://vssps.dev.azure.com/$orgName/_apis/Graph/Users/$($descriptorId)?api-version=7.0-preview" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return $results + +} + +function New-Policy([string]$projectName, [string]$orgName, $headers, $policy) { + + $url = "https://dev.azure.com/$orgName/$projectName/_apis/policy/configurations?api-version=7.0" + + $body = ConvertTo-Json -Depth 100 $policy + + $results = Invoke-RestMethod -Method POST -uri $url -Headers $headers -Body $body -ContentType "application/json" + + return $results +} + +function Edit-Policy([string]$projectName, [string]$orgName, $headers, $policy) { + + $url = "https://dev.azure.com/$orgName/$projectName/_apis/policy/configurations/$($policy.Id)?api-version=7.0" + + $body = ConvertTo-Json -Depth 100 $policy + + $results = Invoke-RestMethod -Method PUT -uri $url -Headers $headers -Body $body -ContentType "application/json" + + return $results +} + diff --git a/modules/Migrate-ADO-Project.psm1 b/modules/Migrate-ADO-Project.psm1 new file mode 100644 index 0000000..ee89000 --- /dev/null +++ b/modules/Migrate-ADO-Project.psm1 @@ -0,0 +1,288 @@ + +function Start-ADOProjectMigration { + [CmdletBinding(SupportsShouldProcess)] + Param( + [Parameter (Mandatory = $TRUE)] [String]$SourceProjectName, + [Parameter (Mandatory = $TRUE)] [String]$SourceOrgName, + [Parameter (Mandatory = $TRUE)] [String]$SourcePAT, + [Parameter (Mandatory = $TRUE)] [String]$TargetProjectName, + [Parameter (Mandatory = $TRUE)] [String]$TargetOrgName, + [Parameter (Mandatory = $TRUE)] [String]$TargetPAT, + [Parameter (Mandatory = $TRUE)] [String]$ProjectPath, + [Parameter (Mandatory = $TRUE)] [String]$RepositoryCloneTempDirectory, + [Parameter (Mandatory = $TRUE)] [String]$MartinsToolConfigurationFile, + [Parameter (Mandatory = $TRUE)] [String]$WorkItemMigratorDirectory, + [Parameter (Mandatory = $TRUE)] [String]$DevOpsMigrationToolConfigurationFile, + [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 + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate from source project $SourceOrg/$SourceProjectName") + ) { + + # Get Headers + $sourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT + $targetHeaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + + + # ------------------------------------------------------------------------------------- + # ---------------- Start The Migration At the Org Level ------------------------------- + #region ------------------------------------------------------------------------------- + Write-Log -Message ' ' + Write-Log -Message '----------------------------------------------------' + Write-Log -Message "-- From: $($SourceOrgName) To: $($TargetOrgName) --" + Write-Log -Message '----------------------------------------------------' + Write-Log -Message ' ' + + + # ======================================== + # ====== Migrate Users On Org Level ====== + #region ================================== + Start-ADOUserMigration ` + -SourceOrgName $SourceOrgName ` + -SourcePat $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetPAT $TargetPAT ` + -WhatIf: $SkipMigrateOrganizationUsers + #endregion + + + # ----------------------------------------------------------------------------------------- + # ---------------- Start The Migration At the Project Level ------------------------------- + #region ----------------------------------------------------------------------------------- + Write-Log -Message ' ' + Write-Log -Message '--------------------------------------------------------------------' + Write-Log -Message "-- Migrate $($SourceProjectName) to $($TargetProjectName) --" + Write-Log -Message '--------------------------------------------------------------------' + Write-Log -Message ' ' + + Write-Log -Message "SourceProjectName $($SourceProjectName)" + Write-Log -Message "SourceOrgName $($SourceOrgName)" + Write-Log -Message ' ' + Write-Log -Message "TargetProjectName $($TargetProjectName)" + Write-Log -Message "TargetOrgName $($TargetOrgName)" + Write-Log -Message ' ' + + # ======================================== + # ========= Migrate Build Queues ========= + # Migrate-ADO-BuildQueues.psm1 + #region ================================== + Start-ADOBuildQueuesMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateBuildQueues + #endregion + + # ============================================== + # ========= Migrate Build Environments ========= + # 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 + #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 + #endregion + + # ======================================== + # ============ Migrate Wikis ============= + # 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 + #endregion + + # ======================================== + # ===== Migrate Service Connections ====== + # Migrate-ADO-ServiceConnections.psm1 + #region ================================== + Start-ADOServiceConnectionsMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateServiceConnections + #endregion + + + + # ========================================== + # ====== Azure DevOps Migration Tool ====== + # ====== Martin's Tool ====== + #region ==================================== + if (!$SkipAzureDevOpsMigrationTool) { + $savedPath = $(Get-Location).Path + + Set-Location -Path $WorkItemMigratorDirectory + + # Migrate Work Items using nkdagility tool + Write-Log -Message "Run Azure DevOps Migration Tool (Martins Tool)" + + $arguments = "execute --config `"$MartinsToolConfigurationFile`"" + + Start-Process -NoNewWindow -Wait -FilePath .\migration.exe -ArgumentList $arguments + + Set-Location -Path $savedpath + } else { + Write-Host "What if: Preforming the operation `"Running Azure DevOps Migration Tool Migration from source project $SourceProjectName`" on target `"Target project $TargetProjectName`"" + } + #endregion + + # ======================================== + # =========== Migrate Groups ============= + # Migrate-ADO-Groups.psm1 + #region ================================== + Start-ADOGroupsMigration ` + -SourcePAT $SourcePAT ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -TargetPAT $TargetPAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -WhatIf:$SkipMigrateGroups + #endregion + + # ======================================== + # ======== Migrate Service Hooks ========= + # Migrate-ADO-ServiceHooks.psm1 + # Repos must migrate before service hooks + #region ================================== + # .\migrateServiceHooks.ps1 + Start-ADOServiceHooksMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateServiceHooks + # #endregion + + # ======================================== + # =========== Migrate Policies =========== + # Migrate-ADO-Policies.psm1 + #region ================================== + # .\migratePolicies.ps1 + Start-ADOPoliciesMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -SourcePAT $SourcePAT ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -TargetPAT $TargetPAT ` + -WhatIf:$SkipMigratePolicies + # #endregion + + # ======================================== + # ========== Migrate Dashboards ========== + # Migrate-ADO-Dashboards.psm1 + #region ================================== + # .\migrateDashboards.ps1 + Start-ADODashboardsMigration ` + -SourceOrgName $SourceOrgName ` + -SourceProjectName $SourceProjectName ` + -SourceHeaders $sourceHeaders ` + -TargetOrgName $TargetOrgName ` + -TargetProjectName $TargetProjectName ` + -TargetHeaders $targetHeaders ` + -WhatIf:$SkipMigrateDashboards + # #endregion + + # =========================================== + # ========== Migrate DeliveryPlans ========== + # 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 + # #endregion + + # ======================================== + # ========= Migrate Artifacts= =========== + # 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 + # #endregion + + # ======================================== + # ========== Migration Finished ========== + # ======================================== + Write-Log "Done migrating $($SourceProjectName) to $($TargetProjectName)" -LogLevel SUCCESS + + } +} diff --git a/modules/Migrate-ADO-ReleaseDefinitions.psm1 b/modules/Migrate-ADO-ReleaseDefinitions.psm1 new file mode 100644 index 0000000..34f14e0 --- /dev/null +++ b/modules/Migrate-ADO-ReleaseDefinitions.psm1 @@ -0,0 +1,25 @@ + +function Start-ADOReleaseDefinitionsMigration { + [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 Release Definitions from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '---------------------------------' + Write-Log -Message '-- Migrate Release Definitions --' + Write-Log -Message '---------------------------------' + Write-Log -Message ' ' + + # DO WORK HERE + } +} + diff --git a/modules/Migrate-ADO-Repos.psm1 b/modules/Migrate-ADO-Repos.psm1 index 1c9c4fc..e65ce1a 100644 --- a/modules/Migrate-ADO-Repos.psm1 +++ b/modules/Migrate-ADO-Repos.psm1 @@ -1,3 +1,6 @@ + +Using Module ".\Migrate-ADO-Common.psm1" + function Start-ADORepoMigration { [CmdletBinding(SupportsShouldProcess)] param( @@ -7,6 +10,9 @@ function Start-ADORepoMigration { [Parameter (Mandatory = $TRUE)] [String]$SourceOrgName, + [Parameter (Mandatory = $TRUE)] + [String]$SourcePAT, + [Parameter (Mandatory = $TRUE)] [Hashtable]$SourceHeaders, @@ -16,11 +22,17 @@ function Start-ADORepoMigration { [Parameter (Mandatory = $TRUE)] [String]$TargetOrgName, + [Parameter (Mandatory = $TRUE)] + [String]$TargetPAT, + [Parameter (Mandatory = $TRUE)] [Hashtable]$TargetHeaders, [Parameter (Mandatory = $TRUE)] - [String]$ReposPath + [String]$ReposPath, + + [Parameter (Mandatory=$FALSE)] + [Object[]]$RepoIds = $() ) if ($PSCmdlet.ShouldProcess( "Target project $TargetOrg/$TargetProjectName", @@ -31,194 +43,89 @@ function Start-ADORepoMigration { Write-Log -Message '-- Migrate Repos --' Write-Log -Message '-------------------' Write-Log -Message ' ' - - $reposToPush = Copy-Repos ` - -SourceProjectName $SourceProjectName ` - -SourceOrgName $SourceOrgName ` - -SourceHeaders $sourceHeaders ` - -TargetProjectName $TargetProjectName ` - -TargetOrgName $TargetOrgName ` - -TargetHeaders $TargetHeaders ` - -ReposPath $ReposPath - - Push-Repos ` - -ProjectName $TargetProjectName ` - -OrgName $TargetOrgName ` - -Repos $reposToPush ` - -Headers $TargetHeaders ` - -ReposPath $ReposPath - } -} - -function Get-Repos { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, - - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, - - [Parameter (Mandatory = $TRUE)] - [Hashtable]$Headers - ) - if ($PSCmdlet.ShouldProcess($ProjectName)) { - $url = "https://dev.azure.com/$OrgName/$ProjectName/_apis/git/repositories?api-version=5.0" - - $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers - - return $results.value - } -} - -function Copy-Repos { - [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 = $TRUE)] - [String]$ReposPath - ) - if ($PSCmdlet.ShouldProcess("path $ReposPath")) { - try { - $final = [object[]]@() - - $targetRepos = Get-Repos ` - -ProjectName $TargetProjectName ` - -OrgName $TargetOrgName ` - -Headers $TargetHeaders - $sourceRepos = Get-Repos ` - -ProjectName $SourceProjectName ` - -OrgName $SourceOrgName ` - -Headers $SourceHeaders - - foreach ($sourceRepo in $sourceRepos) { - if ($null -ne ($targetRepos | Where-Object { $_.name -ieq $sourceRepo.name })) { - Write-Log -Message "Repo [$($sourceRepo.name)] already exists in target.. " - continue - } - - Write-Log -Message "Cloning $($sourceRepo.name)" - git clone $sourceRepo.remoteURL "`"$ReposPath\$($sourceRepo.name)`"" - $final += $sourceRepo - } - return $final - } - catch { - Write-Log -Message "Error cloning repos from org $SourceOrgName and project $SourceProjectName" -LogLevel ERROR - Write-Error -Messsage $_ -LogLevel ERROR - return - } - } -} - -function Push-Repos { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, - - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, + try { + + $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).." + + $savedPath = $(Get-Location).Path + + $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 + } - [Parameter (Mandatory = $TRUE)] - [Object[]]$Repos, + if($repos.Count -gt 0) { - [Parameter (Mandatory = $TRUE)] - [Hashtable]$Headers, + # First clean out the temp repo directory + $tempPath = "$ReposPath\temp" - [Parameter (Mandatory = $TRUE)] - [String]$ReposPath - ) - if ($PSCmdlet.ShouldProcess($ProjectName, "Push repos from $ReposPath")) { - $savedPath = $(Get-Location).Path - $targetRepos = Get-Repos -ProjectName $ProjectName -OrgName $OrgName -Headers $Headers - - foreach ($repo in $Repos) { - Write-Log -Message "Pushing repo $($repo.Name)" - - $targetRepo = $targetRepos | Where-Object { $_.name -ieq $repo.name } - if ($null -eq $targetRepo) { - try { - Write-Log -Message 'Initializing repository ... ' - New-GitRepository -ProjectName $ProjectName -OrgName $Orgname -RepoName $repo.name -Headers $Headers - } - catch { - Write-Log -Message "Error initializing repo: $_ " -LogLevel ERROR + if (-not (Test-Path -Path $tempPath)) { + New-Item -Path $tempPath -ItemType Directory + } else { + Get-ChildItem -Path $tempPath | Remove-Item -Recurse -Force } - } - - try { - Write-Log -Message 'Pushing repo ...' - Write-Log -Message "Entering path `"$ReposPath\$($repo.name)`"" - Set-Location "$ReposPath\$($repo.name)" - $gitTarget = "https://$TargetOrgName@dev.azure.com/$TargetOrgName/$TargetProjectName/_git/" + $repo.name - - git remote add target $gitTarget - git push -u target --all - } - catch { - Write-Log -Message "Error adding remote: $_" -LogLevel ERROR - } - finally { - Set-Location $savedPath + foreach ($sourceRepo in $repos ) { + Write-Log -Message "Copying repo $($sourceRepo.Name).." + + # $targetReposExists = $FALSE + $targetRepo = $targetRepos | Where-Object { $_.name -ieq $sourceRepo.name } + if ($null -ne $targetRepo) { + Write-Log -Message "Repo [$($sourceRepo.name)] already exists in target.. " + continue + # $targetReposExists = $TRUE + } + + try { + # if($targetReposExists) { + # Write-Log -Message 'Updating existing repository.. ' + # } else { + Write-Log -Message 'Initializing new repository.. ' + New-GitRepository -ProjectName $TargetProjectName -OrgName $TargetOrgName -RepoName $sourceRepo.name -Headers $TargetHeaders + # } + } + catch { + Write-Log -Message "Error initializing repo: $_ " -LogLevel ERROR + Write-Log -Message 'Repository cannot be migrated, please migrate manually ... ' + continue + } + + try { + Write-Log -Message "Cloning repository $($sourceRepo.name)" + + $remoteUrl = $sourceRepo.remoteURL.Replace("@",":$SourcePAT@") + git clone --mirror $remoteUrl "$tempPath\$($sourceRepo.name)" + + Write-Log -Message "Entering path `"$tempPath\$($sourceRepo.name)`"" + Set-Location "$tempPath\$($sourceRepo.name)" + + Write-Log -Message 'Pushing repo ...' + $gitTarget = "https://$($TargetOrgName):$($TargetPAT)@dev.azure.com/$TargetOrgName/$TargetProjectName/_git/" + $sourceRepo.name + git push --mirror $gitTarget + + # Write-Log -Message 'Remove local copy of repo ...' + # remove-Item "$tempPath\$($sourceRepo.name)" -Force -Recurse + } + catch { + Write-Log -Message "Error adding remote: $_" -LogLevel ERROR + } + finally { + Set-Location $savedPath + } + } } } - } -} - -function New-GitRepository { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, - - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, - - [Parameter (Mandatory = $TRUE)] - [String]$RepoName, - - [Parameter (Mandatory = $TRUE)] - [Hashtable]$Headers - ) - if ($PSCmdlet.ShouldProcess($ProjectName, "Push repos from $ReposPath")) { - $url = "$org/_apis/git/repositories?api-version=5.1" - } - $url = "https://dev.azure.com/$OrgName/_apis/git/repositories?api-version=5.1" - - $project = Get-ADOProjects -OrgName $OrgName -Headers $Headers -ProjectName $ProjectName - - $requestBody = @{ - name = $RepoName - project = @{ - id = $project.id + catch { + Write-Log -Message "Fatal-Error cloning repos from org $SourceOrgName and project $SourceProjectName" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + return } - } | ConvertTo-Json - - try { - 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) " } - -} \ No newline at end of file +} diff --git a/modules/Migrate-ADO-ServiceConnections.psm1 b/modules/Migrate-ADO-ServiceConnections.psm1 new file mode 100644 index 0000000..10e7a90 --- /dev/null +++ b/modules/Migrate-ADO-ServiceConnections.psm1 @@ -0,0 +1,159 @@ + +function Start-ADOServiceConnectionsMigration { + [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 Service Endpoints from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '---------------------------------------------' + Write-Log -Message '-- Migrate Service Connections (Endpoints) --' + Write-Log -Message '---------------------------------------------' + Write-Log -Message ' ' + + # $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 | 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())")})) { + 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)})) { + Write-Log -Message "Service endpoint [$($endpoint.name)] [$($endpoint.id)] already exists in target.. " + continue + } + + Write-Log -Message "Attempting to create [$($endpoint.name)] in target.. " + + $projectReference = @{ + "id" = $targetProject.id + "name" = $TargetProjectName + } + + $endpointProjectReference = @{ + "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") { + # 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){ + $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") { + 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" + } + $endpoint.authorization | Add-Member -NotePropertyName parameters -NotePropertyValue $parameters + } + + try { + New-ServiceEndpoint -OrgName $TargetOrgName -ProjectName $TargetProjectName -Headers $targetHeaders -ServiceEndpoint $endpoint + Write-Log -Message "Done!" -LogLevel SUCCESS + } + 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 " " + } + } + } +} + + +# 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" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers + + return , $results.value +} + +# 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" + + $body = $ServiceEndpoint | ConvertTo-Json -Depth 32 + + $results = Invoke-RestMethod -ContentType "application/json" -Method Post -uri $url -Headers $Headers -Body $body + + return $results + +} + + diff --git a/modules/Migrate-ADO-ServiceHooks.psm1 b/modules/Migrate-ADO-ServiceHooks.psm1 new file mode 100644 index 0000000..9cd9d03 --- /dev/null +++ b/modules/Migrate-ADO-ServiceHooks.psm1 @@ -0,0 +1,220 @@ + +Using Module ".\Migrate-ADO-Common.psm1" + +function Start-ADOServiceHooksMigration { + [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, + [Parameter (Mandatory = $FALSE)] [string]$SecretsMapPath = "" + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate Service Hooks from source project $SourceOrgName/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '---------------------------' + Write-Log -Message '-- Migrate Service Hooks --' + Write-Log -Message '---------------------------' + Write-Log -Message ' ' + + $sourceProjectOrg = Get-ADOProjects -OrgName $SourceOrgName -ProjectName $sourceProjectName -Headers $sourceHeaders + $targetProjectOrg = Get-ADOProjects -OrgName $TargetOrgName -ProjectName $targetProjectName -Headers $targetHeaders + + $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 + + + $targetRepos = Get-Repos -projectName $targetProject.name -headers $targetHeaders -org $TargetOrgName + + $maskedValue = "********" + + if ($SecretsMapPath -ne "") { + $secretsMap = ((Get-Content -Raw -Path $SecretsMapPath) | ConvertFrom-Json) | ConvertTo-HashTable + Write-Log -Message "Loaded secrets map from $SecretsMapPath" + } + else { + $secretsMap = @{ + serviceHooks = @{ + webHooks = @{} + jenkins = @{} + } + } + Write-Log -Message "Loaded default secrets map" + } + + $hooks = Get-ServiceHooks -projectId $sourceProjectOrg.id -org $SourceOrgName -headers $sourceHeaders + + Write-Log -Message "Located $($hooks.Count) in source." + + $hooks | ConvertTo-Json -Depth 10 | Out-File -FilePath "hooks.json" + + foreach ($hook in $hooks) { + + # # THIS IS A TEMP TESTING BLOCK, REMOVE WHEN DONE TESTING + # if($hook.id -ne "d713f8d6-d8e4-443a-8843-aa01184fda4d") { + # continue + # } + + Write-Log -Message "Attempting to create [$($hook.id)] in target.. " + try { + if ($null -ne $hook.publisherInputs) { + $hook.publisherInputs.projectId = $targetProjectOrg.id + + 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 + if ($null -ne $hook.publisherInputs.repository -and "" -ne $hook.publisherInputs.repository) { + $targetRepo = ($targetRepos | Where-Object { $_.name -ieq $sourceRepo.name }) + if ($null -ne $targetRepo) { + $hook.publisherInputs.repository = $targetRepo.id + } + else { + throw "Failed to locate repository [$($sourceRepo.name)] in target. " + } + } + } + } + + if ($hook.consumerId -eq "webHooks") { + if ($null -ne $hook.consumerInputs) { + if ($maskedValue -eq $hook.consumerInputs.basicAuthPassword) { + # Check hook secrets mapping for this hook's "basicAuthPassword" + if ($null -eq $secretsMap.serviceHooks.webHooks[$hook.consumerInputs.url] -or + $null -eq $secretsMap.serviceHooks.webHooks[$hook.consumerInputs.url].basicAuthPassword) { + throw "Secrets mapping for WebHook - $($hook.consumerInputs.url) is missing or doesn't contain required 'basicAuthPassword' field." + } + else { + $hook.consumerInputs.basicAuthPassword = $secretsMap.serviceHooks.webhooks[$hook.consumerInputs.url].basicAuthPassword + } + } + } + } + + if ($hook.consumerId -eq "jenkins") { + if ($null -ne $hook.consumerInputs) { + if ($maskedValue -eq $hook.consumerInputs.password) { + # Check hook secrets mapping for this hook's "password" + if ($null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl] -or + $null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].password) { + throw "Secrets mapping for Jenkins - $($hook.consumerInputs.serverBaseUrl) is missing or doesn't contain required 'password' field." + } + else { + $hook.consumerInputs.password = $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].password + } + } + if ($maskedValue -eq $hook.consumerInputs.buildAuthToken) { + # Check hook secrets mapping for this hook's "buildAuthToken" + if ($null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl] -or + $null -eq $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].buildAuthToken) { + throw "Secrets mapping for Jenkins - $($hook.consumerInputs.url) is missing or doesn't contain required 'buildAuthToken' field." + } + else { + $hook.consumerInputs.buildAuthToken = $secretsMap.serviceHooks.jenkins[$hook.consumerInputs.serverBaseUrl].buildAuthToken + } + } + } + } + + 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) { + foreach ($t_team in $TargetTeams) { + if($t_team.name -eq $s_team.name){ + $hook.publisherInputs.subscriberId = $t_team.id + break + } + } + } + } + } + + 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) { + foreach ($t_pipeline in $TargetPipelines) { + if($t_pipeline.name -eq $s_pipeline.name){ + $hook.publisherInputs.pipelineId = $t_pipeline.id + break + } + } + } + } + } + + $servicehookJson = @{ + "publisherId" = $hook.publisherId + "eventType" = $hook.eventType + "resourceVersion" = $hook.resourceVersion + "consumerId" = $hook.consumerId + "consumerActionId" = $hook.consumerActionId + "publisherInputs" = $hook.publisherInputs + "consumerInputs" = $hook.consumerInputs + "status" = $hook.status + } + + New-ServiceHook -projectName $targetProjectOrg.id -orgName $TargetOrgName -headers $targetHeaders -serviceHook $servicehookJson + + Write-Log -Message "Done!" -LogLevel SUCCESS + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + } + } +} + + + +#Service Hooks +function Get-ServiceHooks([string]$projectId, [string]$orgName, $headers) { + $url = "https://dev.azure.com/$orgName/_apis/hooks/subscriptionsquery?api-version=7.0" + + $body = @{ + "publisherInputFilters" = @( + @{ + "conditions" = @( + @{ + "inputId" = "projectId" + "inputValue" = $projectId + } + ) + } + ) + } + $temp = $body | ConvertTo-Json -Depth 10 + + $results = Invoke-RestMethod -Method "POST" -uri $url -Headers $headers -Body $temp -ContentType "application/json" + + return , $results.results +} + + +function New-ServiceHook([string]$projectName, [string]$orgName, $serviceHook, $headers) { + $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"){ + $url = "https://vsrm.dev.azure.com/$orgName/_apis/hooks/subscriptions?api-version=7.0" + } + + $body = $serviceHook | ConvertTo-Json -Depth 10 + + $results = Invoke-RestMethod -ContentType "application/json" -Method Post -uri $url -Headers $headers -Body $body + + return $results +} diff --git a/modules/Migrate-ADO-Teams.psm1 b/modules/Migrate-ADO-Teams.psm1 index e8633a3..3649dda 100644 --- a/modules/Migrate-ADO-Teams.psm1 +++ b/modules/Migrate-ADO-Teams.psm1 @@ -1,18 +1,4 @@ -class ADO_Team { - [String]$Id - [String]$Name - [String]$Description - - ADO_Team( - [String]$id, - [String]$name, - [String]$description - ) { - $this.Id = $id - $this.Name = $name - $this.Description = $description - } -} +Using Module ".\Migrate-ADO-Common.psm1" function Start-ADOTeamsMigration { [CmdletBinding(SupportsShouldProcess)] @@ -58,36 +44,6 @@ function Start-ADOTeamsMigration { } } -function Get-ADOProjectTeams { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [Hashtable]$Headers, - - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, - - [Parameter (Mandatory = $TRUE)] - [String]$ProjectName, - - [Parameter (Mandatory = $FALSE)] - [String]$TeamDisplayName - ) - if ($PSCmdlet.ShouldProcess("$org/$ProjectName")) { - $url = "https://dev.azure.com/$OrgName/_apis/projects/$ProjectName/teams?api-version=6.0" - $results = Invoke-RestMethod -Method Get -uri $url -Headers $Headers - - [ADO_Team[]]$teams = @() - foreach ($result in $results.value) { - $teams += [ADO_Team]::new($result.id, $result.name, $result.description) - } - - if ($TeamDisplayName) { - return $teams | Where-Object { $_.Name -eq $TeamDisplayName } - } - return $teams - } -} function Push-ADOTeams { [CmdletBinding(SupportsShouldProcess)] param( diff --git a/modules/Migrate-ADO-Template.psm1 b/modules/Migrate-ADO-Template.psm1 new file mode 100644 index 0000000..5c4b1d6 --- /dev/null +++ b/modules/Migrate-ADO-Template.psm1 @@ -0,0 +1,36 @@ + +function Start-ADO_Template_Migration { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter (Mandatory = $TRUE)] + [String]$SourceProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourceOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$SourcePAT, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetProjectName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetOrgName, + + [Parameter (Mandatory = $TRUE)] + [String]$TargetPAT + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate zzzzz from source project $SourceOrg/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '--------------------' + Write-Log -Message '-- Migrate ZZZZZZ --' + Write-Log -Message '--------------------' + Write-Log -Message ' ' + + # Do Work Here + } +} + diff --git a/modules/Migrate-ADO-Users.psm1 b/modules/Migrate-ADO-Users.psm1 index e5e6250..263e5d9 100644 --- a/modules/Migrate-ADO-Users.psm1 +++ b/modules/Migrate-ADO-Users.psm1 @@ -1,24 +1,5 @@ -class ADO_User { - [String]$Id - [String]$PrincipalName - [String]$DisplayName - [String]$MailAddress - [String]$LicenseType - - ADO_User( - [String]$id, - [String]$principalName, - [String]$displayName, - [String]$mailAddress, - [String]$licenseType - ) { - $this.Id = $id - $this.PrincipalName = $principalName - $this.DisplayName = $displayName - $this.MailAddress = $mailAddress - $this.LicenseType = $licenseType - } -} + +Using Module ".\Migrate-ADO-Common.psm1" function Start-ADOUserMigration { [CmdletBinding(SupportsShouldProcess)] @@ -45,14 +26,22 @@ function Start-ADOUserMigration { Write-Log -Message '-----------------------' Write-Log -Message ' ' - [ADO_User[]]$sourceUsers = Get-ADOUsers ` + Write-Log -Message 'Getting ADO Users from Source..' + $sourceUsers = Get-ADOUsers ` -OrgName $SourceOrgName ` -PersonalAccessToken $SourcePat + Write-Log -Message 'Getting ADO Users from Target..' + $targetUsers = Get-ADOUsers ` + -OrgName $TargetOrgName ` + -PersonalAccessToken $TargetPat + + Write-Log -Message 'Pushing ADO Source Users to Target..' Push-ADOUsers ` -OrgName $TargetOrgName ` -PersonalAccessToken $TargetPat ` - -Users $sourceUsers + -Users $sourceUsers ` + -TargetUsers $targetUsers } } @@ -66,22 +55,24 @@ function Push-ADOUsers { [String]$PersonalAccessToken, [Parameter (Mandatory = $TRUE)] - [ADO_User[]]$Users + [Object[]]$Users, + + [Parameter (Mandatory = $TRUE)] + [Object[]]$TargetUsers ) if ($PSCmdlet.ShouldProcess($OrgName)) { - [ADO_User[]]$targetUsers = Get-ADOUsers ` - -OrgName $OrgName ` - -PersonalAccessToken $PersonalAccessToken + + Write-Log -Message 'Getting Target ADO Users to verify if users exist already..' foreach ($user in $Users) { # Check for duplicates - if ($null -ne ($targetUsers | Where-Object { $_.PrincipalName -ieq $user.PrincipalName } )) { - Write-Log -Message "User [$($user.PrincipalName)] already exists in target org '$OrgName'... " + if ($null -ne ($TargetUsers | Where-Object { $_.PrincipalName -ieq $user.PrincipalName } )) { + Write-Log -Message "User with PrincipalName [$($user.PrincipalName)] already exists in target org '$OrgName'... " continue } # Add user - Write-Log -Message "Add user $($User.DisplayName)" + Write-Log -Message ("Add user $($user.DisplayName)") Add-ADOUser ` -OrgName $OrgName ` -PersonalAccessToken $PersonalAccessToken ` @@ -112,43 +103,23 @@ function Add-ADOUser { -PersonalAccessToken $PersonalAccessToken ` -OrgName $OrgName - $response = az devops user add --email-id $User.PrincipalName --license-type $User.LicenseType --detect $false - - if ($ForceStakeholderIfNeeded -and !$response) { - # Lower subscription plan detected - $newLicense = "stakeholder" - $response = az devops user add --email-id $User.PrincipalName --license-type $newLicense --detect $false - if ($response) { - Write-Log ` - -Message "User '$($User.DisplayName)' has been demoted from license $($User.LicenseType) to $newLicense because your subscription does not support that license type." ` - -LogLevel ERROR + try{ + $response = az devops user add --email-id $User.PrincipalName --license-type $User.LicenseType --detect $false --debug --verbose + + if ($ForceStakeholderIfNeeded -and !$response) { + # Lower subscription plan detected + $newLicense = "stakeholder" + $response = az devops user add --email-id $User.PrincipalName --license-type $newLicense --detect $false --debug --verbose + + if ($response) { + Write-Log ` + -Message "User '$($User.DisplayName)' has been demoted from license $($User.LicenseType) to $newLicense because your subscription does not support that license type." ` + -LogLevel ERROR + } } + + } catch { + Write-Log -Message $_.Exception.Message -LogLevel ERROR } } } - -function Get-ADOUsers { - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter (Mandatory = $TRUE)] - [String]$OrgName, - - [Parameter (Mandatory = $TRUE)] - [String]$PersonalAccessToken - ) - if ($PSCmdlet.ShouldProcess($OrgName)) { - Set-AzDevOpsContext ` - -PersonalAccessToken $PersonalAccessToken ` - -OrgName $OrgName - - $orgUsers = az devops user list --detect $False | ConvertFrom-Json - - # Convert to ADO User objects - [ADO_User[]]$users = @() - foreach ($orgUser in $orgUsers.members) { - $users += [ADO_User]::new($orgUser.user.originId, $orgUser.user.principalName, $orgUser.user.displayName, $orgUser.user.mailAddress, $orgUser.accessLevel.accountLicenseType) - } - - return $users - } -} \ No newline at end of file diff --git a/modules/Migrate-ADO-VariableGroups.psm1 b/modules/Migrate-ADO-VariableGroups.psm1 new file mode 100644 index 0000000..e0ecc4f --- /dev/null +++ b/modules/Migrate-ADO-VariableGroups.psm1 @@ -0,0 +1,129 @@ + +function Start-ADOVariableGroupsMigration { + [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, + [Parameter (Mandatory = $FALSE)] [String]$secretsMapPath = "" + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrgName/$TargetProjectName", + "Migrate VariableGroups from source project $SourceOrgName/$SourceProjectName") + ) { + + Write-Log -Message ' ' + Write-Log -Message '----------------------------' + Write-Log -Message '-- Migrate VariableGroups --' + Write-Log -Message '----------------------------' + Write-Log -Message ' ' + + $sourceProject = Get-ADOProjects -OrgName $SourceOrgName -ProjectName $sourceProjectName -Headers $sourceHeaders + $targetProject = Get-ADOProjects -OrgName $TargetOrgName -ProjectName $targetProjectName -Headers $targetHeaders + + $targetVariableGroups = Get-VariableGroups -projectName $targetProject.name -headers $targetHeaders -orgName $TargetOrgName + + if ($secretsMapPath -ne "") { + $secretsMap = ((Get-Content -Raw -Path $secretsMapPath) | ConvertFrom-Json) | ConvertTo-HashTable + Write-Log -Message "Loaded secrets map from $secretsMapPath" + } + else { + $secretsMap = @{ + serviceHooks = @{ + webHooks = @{} + jenkins = @{} + } + } + Write-Log -Message "Loaded default secrets map" + } + + $groups = Get-VariableGroups -projectName $sourceProject.name -orgName $SourceOrgName -headers $sourceHeaders + + foreach ($groupHeader in $groups) { + + if ($null -ne ($targetVariableGroups | Where-Object {$_.name -ieq $groupHeader.name})) { + Write-Log -Message "Variable group [$($groupHeader.name)] already exists in target.. " + continue + } + + Write-Log -Message "Attempting to create [$($groupHeader.name)] in target.. " + try { + + $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] + } + } + } + + 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 + $ref.description = $groupHeader.description + $ref.projectReference.id = $targetProject.id + $ref.projectReference.name = $targetProject.name + } + + $json = @{ + "description" = $groupHeader.description + "name" = $group.name + "providerData" = $group.providerData + "type" = $group.type + "variableGroupProjectReferences" = $groupObj.variableGroupProjectReferences + "variables" = $group.variables + } | ConvertTo-Json -Depth 32 + + New-VariableGroup -headers $targetHeaders -projectSk $targetProject.id -orgName $TargetOrgName body $json + Write-Log -Message "Done!" -LogLevel SUCCESS + } + catch { + Write-Error ($_.Exception | Format-List -Force | Out-String) -ErrorAction Continue + Write-Error ($_.InvocationInfo | Format-List -Force | Out-String) -ErrorAction Continue + } + } + } +} + + +# Variable groups +function Get-VariableGroups([string]$projectName, [string]$orgName, $headers) { + # $url = "$org/$projectSk/_apis/distributedtask/variablegroups?api-version=5.1-preview" + $url = "https://dev.azure.com/$orgName/$projectName/_apis/distributedtask/variablegroups?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return $results.value +} + + +function Get-VariableGroup([string]$projectName, [string]$orgName, $headers, $groupId) { + # $url = "$org/$projectSk/_apis/distributedtask/variablegroups/$groupId" + $url = "https://dev.azure.com/$orgName/$projectName/_apis/distributedtask/variablegroups/$($groupId)?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return $results +} + + +function New-VariableGroup([string]$projectName, [string]$orgName, $headers, $body) { + # $url = "$org/$projectSk/_apis/distributedtask/variablegroups?api-version=5.1-preview.1" + $url = "https://dev.azure.com/$orgName/_apis/distributedtask/variablegroups?api-version=7.0" + + $results = Invoke-RestMethod -Method Post -uri $url -Headers $headers -Body $body -ContentType "application/json" + + return $results +} + diff --git a/modules/Migrate-ADO-Wikis.psm1 b/modules/Migrate-ADO-Wikis.psm1 new file mode 100644 index 0000000..9e8564e --- /dev/null +++ b/modules/Migrate-ADO-Wikis.psm1 @@ -0,0 +1,132 @@ + +Using Module ".\Migrate-ADO-Common.psm1" + +function Start-ADOWikiMigration { + [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 = $TRUE)] + [String]$ReposPath + ) + if ($PSCmdlet.ShouldProcess( + "Target project $TargetOrg/$TargetProjectName", + "Migrate wiki from source project $SourceOrg/$SourceProjectName") + ) { + Write-Log -Message ' ' + Write-Log -Message '-------------------' + Write-Log -Message '-- Migrate Wikis --' + Write-Log -Message '-------------------' + Write-Log -Message ' ' + + try { + $sourceWikis = Get-Wikis -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $SourceHeaders + Write-Log -Message "Source wikis Count $($sourceWikis.Count).." + $targetWikis = Get-Repos -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $TargetHeaders + Write-Log -Message "Target wikis Count $($targetWikis.Count).." + + $savedPath = $(Get-Location).Path + + if($sourceWikis.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 { + Get-ChildItem -Path $tempPath | Remove-Item -Recurse -Force + } + + foreach ($sourceWiki in $sourceWikis) { + + $sourceRepo = Get-Repo -ProjectName $SourceProjectName -org $SourceOrgName -headers $sourceHeaders -repoId $sourceWiki.name + + if ($null -ne ($targetWikis | Where-Object { $_.name -ieq $sourceWiki.name })) { + Write-Log -Message "Wiki/repo [$($sourceWiki.name)] already exists in target.. " + continue + } + + try { + Write-Log -Message 'Initializing new wiki repository ... ' + New-GitRepository -ProjectName $TargetProjectName -OrgName $TargetOrgName -RepoName $sourceRepo.name -Headers $TargetHeaders + } + catch { + Write-Log -Message "Error initializing new wiki repo: $_ " -LogLevel ERROR + Write-Log -Message 'Repository cannot be migrated, please migrate manually ... ' + continue + } + + try { + Write-Log -Message "Cloning wiki repository $($sourceRepo.name)" + $remoteUrl = $sourceRepo.remoteURL.Replace("@",":$SourcePAT@") + git clone --mirror $remoteUrl "$tempPath\$($sourceRepo.name)" + + Write-Log -Message "Entering path `"$tempPath\$($sourceRepo.name)`"" + Set-Location "$tempPath\$($sourceRepo.name)" + + Write-Log -Message 'Pushing repo ...' + $gitTarget = "https://$($TargetOrgName):$($TargetPAT)@dev.azure.com/$TargetOrgName/$TargetProjectName/_git/" + $sourceRepo.name + git push --mirror $gitTarget + + # Write-Log -Message 'Remove local copy of repo ...' + # remove-Item "$tempPath\$($sourceRepo.name)" -Force -Recurse + } + catch { + Write-Log -Message "Error adding remote: $_" -LogLevel ERROR + } + finally { + Set-Location $savedPath + } + } + } + } + catch { + Write-Log -Message "Error cloning wiki/repo from org $SourceOrgName and project $SourceProjectName" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + return + } + } +} + + +# Wikis +function Get-Wikis([string]$projectName, [string]$orgName, $headers) { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/wiki/wikis?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return , $results.value +} + +function Get-Wiki([string]$projectName, [string]$orgName, $headers, $wikiIdentifier) { + $url = "https://dev.azure.com/$orgName/$projectName/_apis/wiki/wikis/$($wikiIdentifier)?api-version=7.0" + + $results = Invoke-RestMethod -Method Get -uri $url -Headers $headers + + return , $results.value +} + + diff --git a/modules/Migrate-Packages.psm1 b/modules/Migrate-Packages.psm1 new file mode 100644 index 0000000..31f7433 --- /dev/null +++ b/modules/Migrate-Packages.psm1 @@ -0,0 +1,929 @@ +<# + .SYNOPSIS + Migrate NuGet Packages From MyGet to Azure DevOps + + .DESCRIPTION + This function copies all NuGet packages from a MyGet.org public feed + to an Azure DevOps project feed. This requires a password from your + Azure DevOps organization. Passowrd can be in the form of a PAT (Personal Access Token) + + .PARAMETER SourceIndexUrl + The Index URL from your MyGet Package feed. + + .PARAMETER DestinationIndexUrl + The Index URL of your Azure DevOps feed. + + .PARAMETER DestinationPAT + Azure DevOps Personal Access Token (PAT) string + + .PARAMETER TempFilePath + A file path where a .nupkg will be created during migration. + This is automatically cleaned up. + + .PARAMETER SourceUsername + The username of your Source pacakgeing provider + + .PARAMETER SourcePassword + A string password to your package source. Password is encrypted before + being used in any webrequests. + + .PARAMETER NumVersions + Max number of versions to migrate + + .EXAMPLE + # Create a Hashtable to splat to your 'Move-MyGetNuGetPackages' + $params = @{ + SourceIndexUrl = 'https://www.myget.org/F/mytestfeed/api/v3/index.json' + DestinationIndexUrl = 'https://pkgs.dev.azure.com/mytestorg/_packaging/mynewtestfeed/nuget/v3/index.json' + DestinationPassword = 'thisisafakepassword' + TempFilePath = 'C:/Temp/' + FeedName = 'mynewtestfeed' + } + + Move-MyGetNuGetPackages @params + + .NOTES + For more information on Personal Access Tokens - https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate +#> +function Move-MyGetNuGetPackages +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $SourceIndexUrl, + + [Parameter(Mandatory = $true)] + [string] + $DestinationIndexUrl, + + [Parameter(Mandatory = $true)] + [string] + $SourcePAT, + + [Parameter(Mandatory = $true)] + [string] + $DestinationPAT, + + [Parameter()] + [string] + $TempFilePath = $env:Temp, + + [Parameter()] + [string] + $SourceUsername, + + [Parameter()] + [securestring] + $SourcePassword, + + [Parameter()] + [string] + $DestinationFeedName, + + [Parameter()] + [int] + $NumVersions = -1 + ) + + Write-Log -Message "Migrate Package Versions for Feed [$($DestinationFeedName)] in target.. " -LogLevel INFO + + if ($null -eq $TempFilePath) + { + $TempFilePath = [System.IO.Path]::GetTempPath() + + if ($null -eq $TempFilePath) + { + Write-Log -Message "Temp filepath not found. Please provide value for -TempFilePath" -LogLevel ERROR + throw + } + } + + if ($Verbose) + { + $oldVerbosePreference = $VerbosePreference + $VerbosePreference = 'Continue' + } + + if ($DestinationFeedName) + { + # Adds Azure DevOps feed to NuGet sources/ update password to access feed. + # It also prevent's further progress if NuGet is not set up correctly for migration on the users machine. + Update-NuGetSource -FeedName $DestinationFeedName -DevOpsSourceUrl $DestinationIndexUrl -Password $DestinationPAT + } + + if ($SourcePassword) + { + $sourceCredential = New-Object -TypeName pscredential -ArgumentList $SourceUsername, $SourcePassword + } + else + { + $PAT = $DestinationPAT + if($NULL -ne $SourcePAT) { $PAT = $SourcePAT } + $sourceSecurePassword = ConvertTo-SecureString -String $PAT -AsPlainText -Force + $sourceCredential = New-Object -TypeName pscredential -ArgumentList 'PackageMigration', $sourceSecurePassword + } + + $destinationSecurePassword = ConvertTo-SecureString -String $DestinationPAT -AsPlainText -Force + $destinationCredential = New-Object -TypeName pscredential -ArgumentList 'PackageMigration', $destinationSecurePassword + + # Collects and compares packages from source to Azure DevOps feed + $sourceVersions = Get-ContentUrls -IndexUrl $SourceIndexUrl -Credential $sourceCredential + if($NULL -eq $sourceVersions) { + Write-Log -Message "Found no package versions in source.." + return $NULL + } + + + $destinationVersions = Get-Packages -IndexUrl $DestinationIndexUrl -Credential $destinationCredential + $versionsMissingInDestination = $NULL + if($NULL -ne $sourceVersions) { + $versionsMissingInDestination = Get-MissingVersions -SourceVersions $sourceVersions -DestinationVersions $destinationVersions + } + + Write-Log -Message "Found $($sourceVersions.Count) package versions in source, $($destinationVersions.Count) package versions in destination, and $($versionsMissingInDestination.Count) packages versions need to be copied" + + if ($NumVersions -gt -1 -and $NumVersions -lt $versionsMissingInDestination.Length) + { + # $versionsMissingInDestination = $versionsMissingInDestination | Select-Object -First $NumVersions + Write-Log -Message "Only the First $($NumVersions) package versions will be copied!" + $numVersionPackages = [System.Collections.ArrayList]@() + $previousName = "" + $counter = 0 + foreach ($sourceVersion in $SourceVersions) + { + $name = "$($sourceVersion.Name)" + if($name -ne $previousName) { + $previousName = $name + $counter = 0 + } + + if($counter -ge $NumVersions) { + continue + } + + $null = $numVersionPackages.Add($sourceVersion); + $counter += 1 + } + $versionsMissingInDestination = $numVersionPackages + } + + if ($versionsMissingInDestination.Length -gt 0) { + Write-Log -Message "Migrating $($versionsMissingInDestination.Count) package versions." + + # Migrates packages from sources to Azure DevOps feed + $versionContentUrls = $versionsMissingInDestination.Url + $results = Start-MigrationSingleThreaded -ContentUrls $versionContentUrls -DestinationIndexUrl $DestinationIndexUrl -TempFilePath $TempFilePath -SourceCredential $sourceCredential + + Out-Results $results + } + $VerbosePreference = $oldVerbosePreference + + return $results +} + +<# + .SYNOPSIS + Returns the NuGet connection URLs + + .PARAMETER IndexUrl + The Index URL from your MyGet Package feed. + + .PARAMETER Credential + The credential object to connect to packaging source. +#> +function Get-ContentUrls +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $IndexUrl, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential + ) + + # Var is used to ensure there aren't multiple requests to package urls + $Script:registrationRequests = [System.Collections.ArrayList]::new() + $packages = (Get-Packages -IndexUrl $IndexUrl -Credential $Credential).id | Select-Object -Unique + + $registrationBaseUrl = Get-RegistrationBase -IndexUrl $IndexUrl -Credential $Credential + $result = [System.Collections.ArrayList]::new() + + # Collect source package URLs to migrate + foreach ($packageName in $packages) + { + $registrationUrl = "$registrationBaseUrl/$packageName/index.json" + $versions = Read-CatalogUrl -RegistrationUrl $registrationUrl -Credential $Credential + + $null = $result.AddRange($versions) + } + + return $result +} + +<# + .SYNOPSIS + Filters and returns the NuGet v3 URL + + .PARAMETER IndexUrl + The Index URL from your MyGet Package feed. + + .PARAMETER Credential + The credential object to connect to packaging source. +#> +function Get-V3SearchBaseURL +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $IndexUrl, + + [Parameter()] + [pscredential] + $Credential + ) + + $indexJson = Get-Index -IndexUrl $IndexUrl -Credential $Credential + $entry = ($indexJson | Where-Object -FilterScript {$_.'@type' -match 'SearchQueryService.*'})[0] + + return $entry.'@id' +} + +<# + .SYNOPSIS + This is an empty function right now for future development. + + .PARAMETER IndexUrl + The Index URL from your desired Package feed. +#> +function Get-V3FlatBaseURL +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string[]] + $IndexUrl + ) +} + +<# +.SYNOPSIS + Returns the base registration URL + +.PARAMETER IndexUrl + The Index URL from the desired Package feed. + +.PARAMETER Credential + The credential object to connect to packaging source. +#> +function Get-RegistrationBase +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $IndexUrl, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential + ) + + $indexJson = Get-Index -IndexUrl $IndexUrl -Credential $Credential + $entry = $entry = ($indexJson | Where-Object -FilterScript {$_.'@type' -eq 'RegistrationsBaseUrl/Versioned'})[0] + + return $entry.'@id' +} + +<# + .SYNOPSIS + Returns the resources for different NuGet services + + .PARAMETER IndexUrl + The Index URL from your desired Package feed. + + .PARAMETER Credential + The Credential object to access a URL +#> +function Get-Index +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $IndexUrl, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential + ) + + return (Invoke-RestMethod -Uri $IndexUrl -Credential $Credential).resources +} + +<# + .SYNOPSIS + Returns package information from the desired source + + .PARAMETER IndexUrl + Base Url to query from. + + .PARAMETER Credential + Credential to access base URL where packages are stored. + + .PARAMETER Take + Identifies packages in a query +#> +function Get-Packages +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $IndexUrl, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential, + + [Parameter()] + [int] + $Take = 100 + ) + + $searchBaseUrl = Get-V3SearchBaseURL -IndexUrl $IndexUrl -Credential $Credential + $result = [System.Collections.ArrayList]::new() + $i = 0 + + # Create the body of the query + $payLoad = [ordered]@{ + Prerelease = $true + SemverLevel = '2.0' + Skip = 0 + Take = $Take + } + + While ($true) + { + # Adjust the skip portion of the query to get all packages associated with URL + $payLoad.Skip = $i * $Take + Write-Log -Message "Request: $searchBaseUrl, Prerelease = $($payload.Prerelease), SemverLevel = $($payload.SemverLevel), Skip =$($payload.Skip), Take = $($payload.Take)" + try + { + $response = Invoke-RestMethod -Uri $searchBaseUrl -Body $payLoad -Credential $Credential + $packages = $response.data + if ($packages.Count -eq 0) + { + break + } + + foreach ($package in $packages) + { + foreach ($version in $package.versions) + { + $packageObject = [PSCustomObject]@{ + Id = $package.id + Version = $version.version + } + if ($result -notcontains $packageObject) { + $null = $result.add($packageObject) + } + } + } + + $i++ + } + catch + { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message "Exception: $($_.Exception)" -LogLevel ERROR + try { + Write-Log -Message "Exception Message: $(($_ | ConvertFrom-Json).message)" -LogLevel ERROR + } catch {} + + break + } + } + + return $result +} + +<# + .SYNOPSIS + Reads a catalog of packages + + .PARAMETER RegistrationUrl + The base registration URL to query with. + + .PARAMETER Credential + The credential object to connect to packaging source. +#> +function Read-CatalogUrl +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $RegistrationUrl, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential + ) + + $result = [System.Collections.ArrayList]::new() + + if ($RegistrationUrl -in $Script:registrationRequests) + { + Write-Log -Message "Skipping duplicate request to $RegistrationUrl" -LogLevel WARNING + } + else + { + Write-Log -Message "Request: $RegistrationUrl" -LogLevel INFO + + $response = Invoke-RestMethod -Uri $RegistrationUrl -Credential $Credential + + # Adds to track Registration requests to identify duplicate requests + $null = $Script:registrationRequests.Add($RegistrationUrl) + foreach ($item in $response.items) + { + $null = $result.AddRange((Read-CatalogEntry -Item $item -Credential $Credential)) + } + + Write-Output -NoEnumerate $result + } +} + +<# + .SYNOPSIS + Returns a pacakge entry. + + .DESCRIPTION + This is a recursive function to reach the catalog + entries of a given Catalog URL. + + .PARAMETER Item + The package entry from the parent catalog +#> +function Read-CatalogEntry +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + $Item, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential + ) + + $result = [System.Collections.ArrayList]::new() + $itemType = $Item.'@type' + + if ($itemType -eq 'catalog:CatalogPage' -and $null -eq $item.items) + { + $catalogUrl = $Item.'@id' + $null = $result.AddRange((Read-CatalogUrl -RegistrationUrl $catalogUrl -Credential $Credential)) + } + elseif ($itemType -eq 'catalog:CatalogPage') + { + foreach ($subItem in $Item.items) + { + $null = $result.AddRange((Read-CatalogEntry -Item $subItem -Credential $Credential)) + } + } + elseif ($itemType -eq 'Package') + { + $returnItem = [PSCustomObject]@{ + Name = $Item.catalogEntry.id + Version = $Item.catalogEntry.version + Url = $Item.packageContent + } + + $null = $result.Add($returnItem) + } + + Write-Output -NoEnumerate $result +} + +<# + .SYNOPSIS + Migrates packages using NuGet.exe + + .DESCRIPTION + This function migrates packages one at a time. + In future development, there will be an option to use multi-threading + to migrate packages faster. + + .PARAMETER ContentUrls + URL's of migrating packages + + .PARAMETER DestinationIndexUrl + Destination index URL where packages are migrating to. + + .PARAMETER TempFilePath + The local folder path for temporary NuGet packages during migration. + + .PARAMETER SourceCredential + The credential object to connect to the source packaging repository. +#> +function Start-MigrationSingleThreaded +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string[]] + $ContentUrls, + + [Parameter(Mandatory = $true)] + [string] + $DestinationIndexUrl, + + [Parameter(Mandatory = $true)] + [string] + $TempFilePath, + + [Parameter()] + [AllowNull()] + [pscredential] + $SourceCredential + ) + + $results = [System.Collections.ArrayList]::new() + $TempFilePath = "$TempFilePath\temp.nupkg" + + foreach ($url in $ContentUrls) + { + $result = Start-Migration -ContentUrl $url -DestinationIndexUrl $DestinationIndexUrl -TempFilePath $TempFilePath -Credential $SourceCredential + Out-Result @result + $null = $results.Add($result) + } + + # Clean up temp .nupkg file created during migration. + Remove-Item -Path $TempFilePath -Force + return $results +} + +<# + .SYNOPSIS + Uses NuGet.exe to migrate packages from source to destination. + + .PARAMETER ContentUrl + URL of the package to migrate. + + .PARAMETER DestinationIndexUrl + Destination index URL where package is migrating to. + + .PARAMETER Credential + The credential object to connect to packaging source. + + .PARAMETER TempFilePath + The local folder path for temporary NuGet packages during migration. +#> +function Start-Migration +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $ContentUrl, + + [Parameter(Mandatory = $true)] + [string] + $DestinationIndexUrl, + + [Parameter()] + [AllowNull()] + [pscredential] + $Credential, + + [Parameter()] + [string] + $TempFilePath + ) + + try + { + $response = Invoke-WebRequest -Uri $ContentUrl -Credential $Credential + } + catch + { + $return = @{ + Url = $ContentUrl + HttpStatus = -1 + NuGetStatus = -1 + Stdout = $null + } + + return $return + } + + if ($response.StatusCode -ne 200) + { + $return = @{ + Url = $ContentUrl + HttpStatus = $response.StatusCode + NuGetStatus = -1 + Stdout = $null + } + + return $return + } + + # Writes package content bytes to temporary .nupkg file during migration + [io.file]::WriteAllBytes($TempFilePath, $response.Content) + $arguments = "push -Source $DestinationIndexUrl -ApiKey Migration $TempFilePath -SkipDuplicate" + + $location = Get-Location + $exepath = "$location\nuget.exe" + + $result = Start-Command -CommandTitle $exepath -CommandArguments $arguments + + $return = @{ + Url = $ContentUrl + HttpStatus = $response.StatusCode + NuGetStatus = $result.ExitCode + StdOut = $result.StdOut + StdErr = $result.StdErr + } + + return $return +} + +<# + .SYNOPSIS + Writes the result of individual package migrations. + + .PARAMETER Url + Url of the migrating package + + .PARAMETER HttpsStatus + Http Status code returned from web request. + + .PARAMETER NugetStatus + NuGet migration status code + + .PARAMETER StdOut + Standard Output + + .PARAMETER StdErr + Standard Error Output +#> +function Out-Result +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string[]] + $Url, + + [Parameter()] + [string] + $HttpStatus, + + [Parameter()] + [string] + $NugetStatus, + + [Parameter()] + [string] + $StdOut, + + [Parameter()] + [string] + $StdErr + ) + + $level = "INFO" + if($NugetStatus -ne 0) { + $level = "WARNING" + } + + Write-Log -Message "Url: $Url, --> fetchContent HttpStatus: $HttpStatus, publish NugetStatus: $NugetStatus" -LogLevel $level + + if ($StdOut) + { + Write-Log -Message "StdOut Message: $StdOut" -LogLevel $level + } + + if ($StdErr) + { + Write-Log -Message "StdErr Message: $StdErr" -LogLevel ERROR + } +} + +<# + .SYNOPSIS + Writes the results of package migration process as a whole. + + .PARAMETER Results + Results of the package migration +#> +function Out-Results +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + $Results + ) + + $errors = $Results | Where-Object -FilterScript {$_.HttpStatus -ne 200 -or $_.NuGetStatus -ne 0} + $pushedCount = $Results.Count - $errors.Count + + Write-Log -Message "Package Count: $($pushedCount) packages pushed successfully" + if ($errors.Count -gt 0) + { + Write-Log -Message "Error Count: $($errors.Count) errors." -LogLevel WARNING + } +} + +<# + .SYNOPSIS + Returns package ID's from the source not located in the destination. + + .PARAMETER SourceVersions + Source pacakges to be filtered + + .PARAMETER DestinationVersions + Destination packages to be filtered against. +#> +function Get-MissingVersions +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + $SourceVersions, + + [Parameter(Mandatory = $true)] + [AllowNull()] + $DestinationVersions + ) + + $missingPackages = [System.Collections.ArrayList]@() + try { + # hashtable of name____id for fast lookup. Powershell hashtables are not case sensitive. + $destHash = @{} + $sep = "_____" + + foreach ($DestinationVersion in $DestinationVersions) { + $dKey = "$($DestinationVersion.Id)$sep$($DestinationVersion.Version)" + if (-not $destHash.ContainsKey($dKey)) + { + $destHash.Add($dKey, $null) + } + } + + foreach ($sourceVersion in $SourceVersions) + { + $sKey = "$($sourceVersion.Name)$sep$($sourceVersion.Version)" + if (-not $destHash.ContainsKey($sKey)) + { + $null = $missingPackages.Add($sourceVersion); + } + } + }catch + { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message "Exception: $($_.Exception)" -LogLevel ERROR + try { + Write-Log -Message "Exception Message: $(($_ | ConvertFrom-Json).message)" -LogLevel ERROR + } catch {} + + break + } + + return $missingPackages; +} + +<# + .SYNOPSIS + Runs NeGet using .Net to get all required information. + + .PARAMETER CommandTitle + The .exe to run + + .PARAMETER CommandArguments + Argument string to run with specified .exe +#> +function Start-Command +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $CommandTitle, + + [Parameter()] + $CommandArguments + ) + + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = $CommandTitle + $processInfo.RedirectStandardError = $true + $processInfo.RedirectStandardOutput = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + $processInfo.Arguments = $CommandArguments + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + $process.Start() | Out-Null + + $stdOutput = $process.StandardOutput.ReadToEnd(); + $stdError = $process.StandardError.ReadToEnd(); + + $process.WaitForExit() + + $return = [pscustomobject]@{ + StdOut = $stdOutput + StdErr = $stdError + ExitCode = $process.ExitCode + } + + return $return +} + +<# + .SYNOPSIS + Updates NuGet config file to access Azure Artifacts feed + + .PARAMETER FeedName + Azure Artifact feed name + + .PARAMETER DevOpsSourceUrl + Azure DevOps feed source index Url + + .PARAMETER Password + PAT to connect to Azure DevOps Feed +#> +function Update-NuGetSource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $FeedName, + + [Parameter(Mandatory = $true)] + [string] + $DevOpsSourceUrl, + + [Parameter(Mandatory = $true)] + [string] + $Password + ) + + $location = Get-Location + $exepath = "$location\nuget.exe" + + $sourceAdd = Start-Command -CommandTitle $exepath -CommandArguments "sources Add -Name $FeedName -Source $DevOpsSourceUrl" + if ($sourceAdd.ExitCode -eq 1) + { + # If Feed already contains the package just output a warning else output the error + if ($sourceAdd.StdErr -match ".*name specified has already been added to the list of available package sources.*") + { + Write-Log -Message "Message: $($sourceAdd.StdErr)" -LogLevel WARNING + } + else + { + Write-Log -Message "Message: $($sourceAdd.StdErr)" -LogLevel ERROR + throw + } + } + + $sourceUpdate = Start-Command -CommandTitle $exepath -CommandArguments "sources Update -Name $FeedName -UserName 'username' -Password $password" + + if ($sourceUpdate.ExitCode -eq 1) + { + Write-Log -Message "Message: $($sourceUpdate.StdErr)" -LogLevel ERROR + throw + } +} + +Export-ModuleMember -Function 'Move-MyGetNuGetPackages' \ No newline at end of file diff --git a/modules/README - Modules.md b/modules/README - Modules.md new file mode 100644 index 0000000..931a537 --- /dev/null +++ b/modules/README - Modules.md @@ -0,0 +1,387 @@ +# Supporting Modules Directory +This directory holds all of the migration logic provided by this repo. The following operations and their corresponding module files are what is currently supported: + +--- +``` +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.psm1 +``` + + +### Migrate Azure DevOps Area Paths +##### MODULE FILE: `Migrate-ADO-AreaPaths.psm1` +This module file provides the following functions: +- `Start-ADOAreaPathsMigration` + - Migrates area paths from one DevOps project to another. Relies on all other functions provided in this file. +- `ConvertTo-AreaPathObject` + - Flattens the area path objects returned from the DevOps REST API call and converts them to a custom PowerShell `ADO_AreaPath` object. +- `Get-AreaPaths` + - Returns all of the area paths for a given project as a list of custom PowerShell `ADO_AreaPath` objects. +- `Push-AreaPaths` + - Pushes a list of area paths in the form of an array of custom PowerShell `ADO_AreaPath` objects to a given project. +--- + +### Migrate Azure DevOps Iteration Paths +##### MODULE FILE: `Migrate-ADO-IterationPaths.psm1` +This module file provides the following functions: +- `Start-ADOIterationPathsMigration` + - Migrates iteration paths from one DevOps project to another. Relies on all other functions provided in this file. +- `ConvertTo-IterationPathObject` + - Flattens the iteration path objects returned from the DevOps REST API call and converts them to a custom PowerShell `ADO_IterationPath` object. +- `Get-AreaPaths` + - Returns all of the iteration paths for a given project as a list of custom PowerShell `ADO_IterationPath` objects. +- `Push-AreaPaths` + - Pushes a list of iteration paths in the form of an array of custom PowerShell `ADO_IterationPath` objects to a given project. +--- + +### Migrate Azure DevOps Users +##### MODULE FILE: `Migrate-ADO-Users.psm1` +This module file provides the following functions (at the ORG level, not project specific): +- `Start-ADOUserMigration` + - Migrates users from one DevOps orginization to another. Relies on all other functions provided in this file. +- `Push-ADOUsers` + - Loops through a list of custom PowerShell `ADO_User` objects and compares for duplicates. If the user is not a duplicate they will be added to the target org with the `Add-ADOUser` function. +- `Add-ADOUser` + - Adds a user by their user principal name to a DevOps orginization and updates their license using the Azure DevOps CLI +- `Get-ADOUsers` + - Gets all users from a specific org and returns them as custom PowerShell `ADO_User` objects. +--- + +### Migrate Azure DevOps Teams +##### MODULE FILE: `Migrate-ADO-Teams.psm1` +This module file provides the following functions: +- `Start-ADOTeamsMigration` + - Migrates EMPTY teams from one DevOps orginization to another. Relies on all other functions provided in this file. +- `Get-ADOProjectTeams` + - Gets a list of custom PowerShell `ADO_Team` objects for a specific org and project or a specific `ADO_Team` project if a team name is specified to filter by. +- `Push-ADOTeams` + - Loops through a list of custom PowerShell `ADO_Team` objects and compares for duplicates. If the team is not a duplicate it will be added on the target. +- `New-ADOTeam` + - Creates a new, empty team, in the specified org and project. The creation of a new team also triggers the creation of a new group. +--- + +### Migrate Azure DevOps Teams +##### MODULE FILE: `Migrate-ADO-Groups.psm1` +This module file provides the following functions: +- `Start-ADOGroupsMigration` + - Migrates groups and their members from one DevOps orginization to another. Relies on all other functions provided in this file. +- `Get-ADOGroups` + - Gets a list of custom PowerShell `ADO_Group` objects for the specified org and project. +- `Get-ADOGroupMembers` + - Gets a list of user group members and group group members and returns a hashtable of custom PowerShell `ADO_Group` and `ADO_GroupMember` objects. +- `Push-ADOGroups` + - Loops through each source group and creates a new empty target group with `New-ADOGroup`. After all groups are created it re-loops through all of the newly created groups and pushes group members to each group using `Push-GroupMembers`. +- `New-ADOGroup` + - Creates a new, empty, ADO group and returns a custom PowerShell `ADO_Group` object +- `Push-GroupMembers` + - Adds user members and group members of a specified, existing, group. +--- + +### Migrate Azure DevOps Build Queues +##### MODULE FILE: `Migrate-ADO-BuildQueues.psm1` +This module file provides the following functions: +- `Start-ADOBuildQueuesMigration` + - Migrates build queues from one DevOps project to another. Relies on all other functions provided in this file. +- `Get-BuildQueues` + - Returns all of the build queues for a given project as a list of custom PowerShell `ADO_BuildQueue` objects. +- `Push-BuildQueues` + - Checks if any of the provided build queues already exist & pushes the build queues to DevOps using the `New-BuildQueue` function. Requires a list of custom PowerShell `ADO_BuildQueue` objects. +- `New-BuildQueue` + - Creates a new build queue in DevOps with the properties provided by a custom PowerShell `ADO_BuildQueue` object passed to it. +--- + +### Migrate Azure DevOps Build Enironments +##### MODULE FILE: `Migrate-ADO-BuildEnvironments.psm1` + +This module file provides the following functions: +- `Start-ADOBuildEnvironmentsMigration` + - Migrates Build Environments from one DevOps project to another. Relies on all of the other functions provided in this file. +- `Get-BuildEnvironments` + - Gets a list of all of the Build Environments for a given project. +- `Get-BuildEnvironmentRoleAssignments` + - Get a list of Role Assignments associated with a given build environment. +- `Get-IdentityInfo` + - Gets information for a given Identity by it ID. +- `Set-BuildEnvironmentRoleAssignment` + - Set a new role assignment for a given build environment. +- `Get-BuildEnvironmentPipelinePermissions` + - Get build pipeline permissions for a goven build environment. +- `Set-BuildEnvironmentPipelinePermissions` + - Sets a new build evnironment pipeline permission. +- `New-BuildEnvironment` + - Creates a new build environment. +--- + +### Migrate Azure DevOps Repos +##### MODULE FILE: `Migrate-ADO-Repos.psm1` +This module file provides the following functions: +- `Start-ADORepoMigration` + - Migrates repos from one DevOps project to another. Relies on all other functions provided in this file. +- `Get-Repos` + - Gets a list of repos from DevOps for a given project +- `Copy-Repos` + - Clones a list of repos from DevOps to the users local machine using a provided path +- `Push-Repos` + - Loops through a list of provided repo objects, creates a new empty repository for each repo using `New-GitRepository` and uses git to push the downloaded repos to DevOps. Requires `Copy-Repos` to be run first. +- `New-GitRepository` + - Creates a new empty git repo in Azure Devops + - +--- + +### Migrate Azure DevOps Wikis +##### MODULE FILE: `Migrate-ADO-Wikis.psm1` + +This module file provides the following functions: +- `Start-ADOWikiMigration` + - Migrate Wiki data from one project to another. It utilizes the `Get-Wikis` and `Get-Wiki` functions also contained in this file. +- `Get-Wikis` + - Gets a list of Wikis that will be used in migrating them to a target ADO project. +- `Get-Wiki` + - Get further informatiob about a specific Wiki. +--- + +### Common Files Shared Between the Migration Modules +##### MODULE FILE: `Migrate-ADO-Common.psm1` +This module file provides the following functions: +- `New-HTTPHeaders` + - Generates basic auth headers using a provided personal access token. +- `Get-ADOProjects` + - Gets either a specific Azure DevOps project by name or all projects for a given org +- `Write-Log` + - Handles writing messages to the console and calling `Write-LogAsync` to log those messages to a file path set via an environment variable +- `Write-LogAsync` + - Handles writing messages to log files to a given path. +- `ConvertTo-Object` +- `ConvertTo-HashTable` +- `Get-ProjectFolderPath` + - Returns the formatted folder path based on the migration start date and target/source projects +- `Set-ProjectFolders` + - Creates a directory of projects to store logs and downloaded repo files during the migration +--- + +### Migrate Azure Devops Pipelines +##### MODULE FILE: `Migrate-ADO-Pipelines.psm1` + +This module file provides the following functions: +- `Get-Pipelines` + - Gets a list of pipeline build definitions. + +> **_NOTE:_** Pipeline migration is handled by the Azure DevOps Migration Tools (Martin's Tool) +--- + +### Migrate ADO Project +##### MODULE FILE: `Migrate-ADO-Project.psm1` + +This module file provides the following functions: +- `Start-ADOProjectMigration` + - This Function is the main entry point for migrations of all component areas of a project handled by the modules within this directory. This function can be called and in turn will call the other various module functions. + +--- +### Migrate Azure Devops Service Hooks +##### MODULE FILE: `Migrate-ADO-ServiceHooks.psm1` + +This module file provides the following functions: +- `Start-ADOServiceHooksMigration` + - Migrates Service hooks from one ADO project to another. This function calls the `Get-ServiceHooks` and `New-ServiceHook1` functions. +- `Get-ServiceHooks` + - Gets a list of service hooks for a given ADO project. +- `New-ServiceHook` + - Creates a new service hook within a given ADO project. + +--- +### Migrate Azure Devops Service Connection +##### MODULE FILE: `Migrate-ADO-ServiceConnections.psm1` + +This module file provides the following functions: +- `Start-ADOServiceConnectionsMigration` + - Migrates Service Connections from one ADO project to another ADO project. Relies on all other functions provided in this file. +- `Get-ServiceEndpoints` + - Gets a list of all service connection service end-points defined within an ADO project. +- `New-ServiceEndpoint$ServiceEndpoint` + - Creates a new service connection service end-point within a given ADO project. + +> **_NOTE:_** Service Principal credentials or name/password credentials for service connections cannot be created. All migrated service connections will need to be manually updated or replaced after migration. + +--- +### Migrate Azure Devops variable Groups +##### MODULE FILE: `Migrate-ADO-VariableGroups.psm1` + +This module file provides the following functions: +- `Start-ADOVariableGroupsMigration` + - Migrates Pipeline variable Groups from one ADO project to another. Relies on all other function contained in this file. +- `Get-VariableGroups` + - Gets a list of all defined variable groups for a given ADO project. +- `Get-VariableGroup ` + - Gets information on a specific variable group by its ID. +- `New-VariableGroup` + - Creates a new pipeline variable group for a given ADO project. + +> **_NOTE:_** Migration of Pipeline Variable Groups are also handled by the Azure DevOps Migration Tools (Martin's Tool). + +--- +### Migrate Azure Devops Policies +##### MODULE FILE: `Migrate-ADO-Policies.psm1` + +This module file provides the following functions: +- `Start-ADOPoliciesMigration` + - Migrates repository and branch policies from one ADO project to another. This function relies on some of the other functions defined within this file. +- `Get-Policies` + - Gets a list of all of the repository and branch policies defined for a given ADO project. +- `Get-UserIdentity` + - Gets a specified user idenity dataset defined in a given projects organization by its identity ID. +- `Get-UserByDescriptor` + - Get data for a user identoty defined in a given project's organization by its descriptor value. +- `New-Policy` + - Creates a new repo/branch policy for a given ADO project. +- `Edit-Policy` + - Edits or updates an existing and specified repo/branch policy. + +--- +### Migrate Azure Devops Dashboards +##### MODULE FILE: `Migrate-ADO-Dashboards.psm1` + +This module file provides the following functions: +- `Start-ADODashboardsMigration` + - Migrates Dashboards from one ADO project to another. Relies on all other function contained within this file. +- `Get-Dashboards` + - Gets a list of dashboard datasets for a given ADO project. +- `Get-Dashboard` + - Gets a specific dashboard's data information for a given ADO project by a specified dashboard ID value. +- `New-Dashboard` + - Creates a new dashboard for a given ADO project. +- `Edit-Dashboard` + - Edit an existing dasboard defined for a goven ADO project. +- `Get-Teams` + - Gets a list of Teams for a given ADO organization and project. This is used to get team dashboards vs project level dashboards. + +--- +### Migrate Azure Devops Build Definitions +##### MODULE FILE: `Migrate-ADO-BuildDefinitions.psm1` + +This module file provides the following functions: +- `Start-ADOBuildDefinitionsMigration` + +> **_NOTE:_** This script has not been fully implemented. Build and Release Pipeline migration is handled by the Azure DevOps Migration Tools (Martin's Tool) + +--- +### Migrate Azure Devops Release Definitions +##### MODULE FILE: `Migrate-ADO-ReleaseDefinitions.psm1` + +This module file provides the following functions: +- `Start-ADOReleaseDefinitionsMigration` + +> **_NOTE:_** This script has not been fully implemented. Build and Release Pipeline migration is handled by the Azure DevOps Migration Tools (Martin's Tool) + +--- +### Migrate Azure Devops Artifacts +##### MODULE FILE: `Migrate-ADO-Artifacts.psm1` + +This module file provides the following functions: +- `Start-ADOArtifactsMigration` + - Migrates artifact feeds and their defined package versions from one ADO project to another. Replies on all other functions contained with this file. +- `Get-OrganizationId` + - get the ID for a goven project name within a given organization. +- `Get-Feeds` + - Gets a list of artifact feeds for a given ADO project. +- `Get-Feed` + - Gets a specific artifact feed for a given ADO project by its feed ID. +- `Update-Feed` + - Updates a given arifact feed within a given ADO project by its ID. +- `New-ADOFeed` + - Creates a new artifact feed for a given ADO project. +- `Get-Views` + - Get a list of defined views for a given artifact feed within a given ADO project. +- `Update-View` + - Updates an existing artifact feed's view. +- `Get-Packages` + - Gets a list of all package versions for a given artifact feed. +- `Start-Command` + - Executes an executable process by creating a new .NET System.Diagnostics.Process object and starting it. This is used by the artifact migration to call the nuget executable to perform various actions during the migration process. + +--- +### Migrate Azure Devops Delivery Plans +##### MODULE FILE: `Migrate-ADO-DeliveryPlans.psm1` + +This module file provides the following functions: +- `Start-ADODeliveryPlansMigration` + - Migrates Delivery Plans defined for a given ADO project. relies on all other functions defined within this file. +- `Get-DeliveryPlans` + - Gets a list of all delivery plans for a given ADO project. +- `Get-DeliveryPlan` + - Gets a spcific delivery plan for a given ADO project by its delivery plan ID. +- `New-DeliveryPlan` + - Creates a new delivery plan for a given ADO project. + +--- +### Project Process template Add Custom Field +##### MODULE FILE: `ADO-AddCustomField.psm1` + +This module file provides the following functions: +- `Start-ADO_AddCustomField` + - Begins the process of adding a new custom field for a given organization/project so that it can be added to all work item types for the project's process template. + - This function relies on all of the other functins defined in this file. +- `Get-ProcessWorkItemTypes` + - Gets a list of all work item types defined for the organizational process template that a given ADO project is using. +- `Add-CustomField` + - Adds a custom field to a spcific work item type for the projects process template. +- `ConvertTo-WorkItemTypeObject` + - Converts a given REST API dataset to a custom defined PowerShell `WorkItemType` object. +- `Get-Processes` + - Gets a list of all of the process templates defined for a given organization. +- `Get-ProcessesDefinitions` + - Gets the process definitions list for a given organization and specified process template ID. +- `Get-CustomfieldsList` + - Gets a list of of all defined custom fields for a specified process template. +- `New-Customfield` + - Creates a new custom field for a specified process template. + +--- +### Migrate Artifact Feed Packages +##### MODULE FILE: `Migrate-Packages.psm1` + +This module file contains the following functions: +- `Get-ContentUrls` +- `Get-V3SearchBaseURL` +- `Get-V3FlatBaseURL` +- `Get-RegistrationBase` +- `Get-Index` +- `Get-Packages` +- `Read-CatalogUrl` +- `Read-CatalogEntry` +- `Start-MigrationSingleThreaded` +- `Start-Migration` +- `Out-Result` +- `Out-Results` +- `Get-MissingVersions` +- `Start-Command` +- `Update-NuGetSource` + +This module file is based on Microsoft's AzureArtifactsPackageMigration PowerShell module for migrating Azure artifact feed packages. +The main Function for this module is the Move-MyGetNuGetPackages function which is exported by this module file. + +For more information about migrating artifact feed packages using Microsoft's migration tools, read more here:
+ [Azure Aritfact Migration Tool](https://github.com/microsoft/azure-artifacts-migration)
+[Microsoft Learn: Migrating Azure DevOps Feed Packages](https://learn.microsoft.com/en-us/azure/devops/artifacts/tutorials/migrate-packages?view=azure-devops&tabs=Windows) +--- + diff --git a/modules/README.md b/modules/README.md deleted file mode 100644 index 0ba4947..0000000 --- a/modules/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Supporting Modules Directory -This directory holds all of the migration logic provided by this repo. The following operations and their corresponding module files are what is currently supported: - ---- -### Migrate Azure DevOps Users -##### MODULE FILE: `Migrate-ADO-Users.psm1` -This module file provides the following functions (at the ORG level, not project specific): -- `Start-ADOUserMigration` - - Migrates users from one DevOps orginization to another. Relies on all other functions provided in this file. -- `Push-ADOUsers` - - Loops through a list of custom PowerShell `ADO_User` objects and compares for duplicates. If the user is not a duplicate they will be added to the target org with the `Add-ADOUser` function. -- `Add-ADOUser` - - Adds a user by their user principal name to a DevOps orginization and updates their license using the Azure DevOps CLI -- `Get-ADOUsers` - - Gets all users from a specific org and returns them as custom PowerShell `ADO_User` objects. ---- -### Migrate Azure DevOps Teams -##### MODULE FILE: `Migrate-ADO-Teams.psm1` -This module file provides the following functions: -- `Start-ADOTeamsMigration` - - Migrates EMPTY teams from one DevOps orginization to another. Relies on all other functions provided in this file. -- `Get-ADOProjectTeams` - - Gets a list of custom PowerShell `ADO_Team` objects for a specific org and project or a specific `ADO_Team` project if a team name is specified to filter by. -- `Push-ADOTeams` - - Loops through a list of custom PowerShell `ADO_Team` objects and compares for duplicates. If the team is not a duplicate it will be added on the target. -- `New-ADOTeam` - - Creates a new, empty team, in the specified org and project. The creation of a new team also triggers the creation of a new group. ---- -### Migrate Azure DevOps Teams -##### MODULE FILE: `Migrate-ADO-Groups.psm1` -This module file provides the following functions: -- `Start-ADOGroupsMigration` - - Migrates groups and their members from one DevOps orginization to another. Relies on all other functions provided in this file. -- `Get-ADOGroups` - - Gets a list of custom PowerShell `ADO_Group` objects for the specified org and project. -- `Get-ADOGroupMembers` - - Gets a list of user group members and group group members and returns a hashtable of custom PowerShell `ADO_Group` and `ADO_GroupMember` objects. -- `Push-ADOGroups` - - Loops through each source group and creates a new empty target group with `New-ADOGroup`. After all groups are created it re-loops through all of the newly created groups and pushes group members to each group using `Push-GroupMembers`. -- `New-ADOGroup` - - Creates a new, empty, ADO group and returns a custom PowerShell `ADO_Group` object -- `Push-GroupMembers` - - Adds user members and group members of a specified, existing, group. ---- -### Migrate Azure DevOps Area Paths -##### MODULE FILE: `Migrate-ADO-AreaPaths.psm1` -This module file provides the following functions: -- `Start-ADOAreaPathsMigration` - - Migrates area paths from one DevOps project to another. Relies on all other functions provided in this file. -- `ConvertTo-AreaPathObject` - - Flattens the area path objects returned from the DevOps REST API call and converts them to a custom PowerShell `ADO_AreaPath` object. -- `Get-AreaPaths` - - Returns all of the area paths for a given project as a list of custom PowerShell `ADO_AreaPath` objects. -- `Push-AreaPaths` - - Pushes a list of area paths in the form of an array of custom PowerShell `ADO_AreaPath` objects to a given project. ---- -### Migrate Azure DevOps Iteration Paths -##### MODULE FILE: `Migrate-ADO-IterationPaths.psm1` -This module file provides the following functions: -- `Start-ADOIterationPathsMigration` - - Migrates iteration paths from one DevOps project to another. Relies on all other functions provided in this file. -- `ConvertTo-IterationPathObject` - - Flattens the iteration path objects returned from the DevOps REST API call and converts them to a custom PowerShell `ADO_IterationPath` object. -- `Get-AreaPaths` - - Returns all of the iteration paths for a given project as a list of custom PowerShell `ADO_IterationPath` objects. -- `Push-AreaPaths` - - Pushes a list of iteration paths in the form of an array of custom PowerShell `ADO_IterationPath` objects to a given project. ---- -### Migrate Azure DevOps Build Queues -##### MODULE FILE: `Migrate-ADO-BuildQueues.psm1` -This module file provides the following functions: -- `Start-ADOBuildQueuesMigration` - - Migrates build queues from one DevOps project to another. Relies on all other functions provided in this file. -- `Get-BuildQueues` - - Returns all of the build queues for a given project as a list of custom PowerShell `ADO_BuildQueue` objects. -- `Push-BuildQueues` - - Checks if any of the provided build queues already exist & pushes the build queues to DevOps using the `New-BuildQueue` function. Requires a list of custom PowerShell `ADO_BuildQueue` objects. -- `New-BuildQueue` - - Creates a new build queue in DevOps with the properties provided by a custom PowerShell `ADO_BuildQueue` object passed to it. ---- -### Migrate Azure DevOps Repos -##### MODULE FILE: `Migrate-ADO-Repos.psm1` -This module file provides the following functions: -- `Start-ADORepoMigration` - - Migrates repos from one DevOps project to another. Relies on all other functions provided in this file. -- `Get-Repos` - - Gets a list of repos from DevOps for a given project -- `Copy-Repos` - - Clones a list of repos from DevOps to the users local machine using a provided path -- `Push-Repos` - - Loops through a list of provided repo objects, creates a new empty repository for each repo using `New-GitRepository` and uses git to push the downloaded repos to DevOps. Requires `Copy-Repos` to be run first. -- `New-GitRepository` - - Creates a new empty git repo in Azure Devops ---- -### Common Files Shared Between the Migration Modules -##### MODULE FILE: `Migrate-ADO-Common.psm1` -This module file provides the following functions: -- `New-HTTPHeaders` - - Generates basic auth headers using a provided personal access token. -- `Get-ADOProjects` - - Gets either a specific Azure DevOps project by name or all projects for a given org -- `Write-Log` - - Handles writing messages to the console and calling `Write-LogAsync` to log those messages to a file path set via an environment variable -- `Write-LogAsync` - - Handles writing messages to log files to a given path. -- `ConvertTo-Object` -- `ConvertTo-HashTable` -- `Get-ProjectFolderPath` - - Returns the formatted folder path based on the migration start date and target/source projects -- `Set-ProjectFolders` - - Creates a directory of projects to store logs and downloaded repo files during the migration diff --git a/project-migration/MigrateProject.ps1 b/project-migration/MigrateProject.ps1 new file mode 100644 index 0000000..0d68449 --- /dev/null +++ b/project-migration/MigrateProject.ps1 @@ -0,0 +1,414 @@ + +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, + # 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]$SkipMigrateTfsAreaAndIterations = $TRUE, + [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, + [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateTaskGroups = $TRUE, + + # Step 3 + [parameter(Mandatory=$FALSE)] [Boolean]$SkipMigrateWorkItems = $TRUE, + [parameter(Mandatory=$FALSE)] [String]$WorkItemQueryBit = "AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') " +) + + +# 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-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 "SkipMigrateTaskGroups $($SkipMigrateTaskGroups)" +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) +$martinConfigFileChanged = $FALSE + +# ------------------ +# ----- Source ----- +# ------------------ +# Organization +if($martinConfiguration.Source.Collection -ne $SourceProject.Organization) { + $martinConfiguration.Source.Collection = $SourceProject.Organization + $martinConfigFileChanged = $TRUE +} +# project +if($martinConfiguration.Source.Project -ne $SourceProject.ProjectName) { + $martinConfiguration.Source.Project = $SourceProject.ProjectName + $martinConfigFileChanged = $TRUE +} +# personal access token +if($martinConfiguration.Source.PersonalAccessToken -ne $sourcePat) { + $martinConfiguration.Source.PersonalAccessToken = $sourcePat + $martinConfigFileChanged = $TRUE +} + +# ------------------ +# ----- Target ----- +# ------------------ +# Organization +if($martinConfiguration.Target.Collection -ne $TargetProject.Organization) { + $martinConfiguration.Target.Collection = $TargetProject.Organization + $martinConfigFileChanged = $TRUE +} +# project +if($martinConfiguration.Target.Project -ne $TargetProject.ProjectName) { + $martinConfiguration.Target.Project = $TargetProject.ProjectName + $martinConfigFileChanged = $TRUE +} +# personal access token +if($martinConfiguration.Target.PersonalAccessToken -ne $targetPat) { + $martinConfiguration.Target.PersonalAccessToken = $targetPat + $martinConfigFileChanged = $TRUE +} + +# --------------------------------------- +# -- End Point Source/Target settings -- +# --------------------------------------- +$endpointConfigs = @("AzureDevOpsEndpoints", "TfsTeamSettingsEndpoints", "TfsWorkItemEndpoints", "TfsEndpoints") + +foreach($endpointConfig in $martinConfiguration.Endpoints.PSObject.Properties) { + if($endpointConfigs.Contains($endpointConfig.Name)) { + # Source Organization + if($endpointConfig.Value[0].Organisation -ne $SourceProject.Organization) { + $endpointConfig.Value[0].Organisation = $SourceProject.Organization + $martinConfigFileChanged = $TRUE + } + # Source project + if($endpointConfig.Value[0].Project -ne $SourceProject.ProjectName) { + $endpointConfig.Value[0].Project = $SourceProject.ProjectName + $martinConfigFileChanged = $TRUE + } + # Source personal access token + if($endpointConfig.Value[0].AccessToken -ne $sourcePat) { + $endpointConfig.Value[0].AccessToken = $sourcePat + $martinConfigFileChanged = $TRUE + } + if($endpointConfig.Name -eq "TfsWorkItemEndpoints") { + # Source personal access token + if($endpointConfig.Value[0].PersonalAccessToken -ne $sourcePat) { + $endpointConfig.Value[0].PersonalAccessToken = $sourcePat + $martinConfigFileChanged = $TRUE + } + } + + # Target Organization + if($endpointConfig.Value[1].Organisation -ne $TargetProject.Organization) { + $endpointConfig.Value[1].Organisation = $TargetProject.Organization + $martinConfigFileChanged = $TRUE + } + # Target project + if($endpointConfig.Value[1].Project -ne $TargetProject.ProjectName) { + $endpointConfig.Value[1].Project = $TargetProject.ProjectName + $martinConfigFileChanged = $TRUE + } + # Target personal access token + if($endpointConfig.Value[1].AccessToken -ne $targetPat) { + $endpointConfig.Value[1].AccessToken = $targetPat + $martinConfigFileChanged = $TRUE + } + if($endpointConfig.Name -eq "TfsWorkItemEndpoints") { + # Source personal access token + if($endpointConfig.Value[1].PersonalAccessToken -ne $targetPat) { + $endpointConfig.Value[1].PersonalAccessToken = $targetPat + $martinConfigFileChanged = $TRUE + } + } + } +} + +# -------------------------------------------------- +# ----- Azure DevOps Migration Tool Processors ----- +# - enable which processors we execute - +# -------------------------------------------------- +foreach($processor in $martinConfiguration.Processors) +{ + if($processor.'$type' -eq "TfsAreaAndIterationProcessorOptions") { + if(($processor.Enabled -ne !$SkipMigrateTfsAreaAndIterations)){ + $processor.Enabled = !$SkipMigrateTfsAreaAndIterations + $martinConfigFileChanged = $TRUE + } + } elseif($processor.'$type' -eq "TfsTeamSettingsProcessorOptions") { + if(($processor.Enabled -ne !$SkipMigrateTeams)){ + $processor.Enabled = !$SkipMigrateTeams + $martinConfigFileChanged = $TRUE + } + } elseif($processor.'$type' -eq "TestVariablesMigrationConfig") { + if(($processor.Enabled -ne !$SkipMigrateTestVariables)){ + $processor.Enabled = !$SkipMigrateTestVariables + $martinConfigFileChanged = $TRUE + } + } elseif($processor.'$type' -eq "TestConfigurationsMigrationConfig") { + if(($processor.Enabled -ne !$SkipMigrateTestConfigurations)){ + $processor.Enabled = !$SkipMigrateTestConfigurations + $martinConfigFileChanged = $TRUE + } + } elseif($processor.'$type' -eq "TestPlansAndSuitesMigrationConfig") { + if(($processor.Enabled -ne !$SkipMigrateTestPlansAndSuites)){ + $processor.Enabled = !$SkipMigrateTestPlansAndSuites + $martinConfigFileChanged = $TRUE + } + } elseif($processor.'$type' -eq "TfsSharedQueryProcessorOptions") { + if(($processor.Enabled -ne !$SkipMigrateWorkItemQuerys)){ + $processor.Enabled = !$SkipMigrateWorkItemQuerys + $martinConfigFileChanged = $TRUE + } + } elseif($processor.'$type' -eq "AzureDevOpsPipelineProcessorOptions") { + # MigrateBuildPipelines + $migratingPipeline = $FALSE + if(($processor.MigrateBuildPipelines -ne !$SkipMigrateBuildPipelines)){ + $processor.MigrateBuildPipelines = !$SkipMigrateBuildPipelines + } + if($processor.MigrateBuildPipelines -eq $TRUE) { + # You need to migrate variable groups before pipelines + $processor.MigrateVariableGroups = !$SkipMigrateBuildPipelines + $migratingPipeline = $TRUE + $SkipMigrateBuildPipelines = $FALSE + } + + # MigrateReleasePipelines + if(($processor.MigrateReleasePipelines -ne !$SkipMigrateReleasePipelines)){ + $processor.MigrateReleasePipelines = !$SkipMigrateReleasePipelines + } + if($processor.MigrateReleasePipelines -eq $TRUE) { + # You need to migrate variable groups before pipelines + $processor.MigrateVariableGroups = !$SkipMigrateReleasePipelines + $migratingPipeline = $TRUE + $SkipMigrateBuildPipelines = $FALSE + } + + # MigrateTaskGroups + if(($processor.MigrateTaskGroups -ne !$SkipMigrateTaskGroups)){ + $processor.MigrateTaskGroups = !$SkipMigrateTaskGroups + } + + # MigrateVariableGroups + if(($processor.MigrateVariableGroups -ne !$SkipMigrateVariableGroups) -and (!$migratingPipeline)){ + $processor.MigrateVariableGroups = !$SkipMigrateVariableGroups + } + + $SkipAzureDevOpsPipelineProcessorOptions = ( ` + $SkipMigrateBuildPipelines -and ` + $SkipMigrateReleasePipelines -and ` + $SkipMigrateVariableGroups -and ` + $SkipMigrateTaskGroups + ) + + if(($processor.Enabled -ne !$SkipAzureDevOpsPipelineProcessorOptions) -or (!$SkipAzureDevOpsPipelineProcessorOptions)){ + $processor.Enabled = !$SkipAzureDevOpsPipelineProcessorOptions + + # RepositoryNameMaps + if($processor.Enabled -eq $TRUE) { + $processor.RepositoryNameMaps = @{ + "$($SourceProject.ProjectName)"= "$($TargetProject.ProjectName)" + } + } else { + $processor.RepositoryNameMaps = $NULL + } + + $martinConfigFileChanged = $TRUE + } + } elseif(($processor.'$type' -eq "WorkItemMigrationConfig") -or ($processor.'$type' -eq "WorkItemTrackingProcessorOptions")) { + if(($processor.Enabled -ne !$SkipMigrateWorkItems) -or ($processor.WIQLQueryBit -ne $WorkItemQueryBit)){ + $processor.Enabled = !$SkipMigrateWorkItems + $processor.WIQLQueryBit = $WorkItemQueryBit + $martinConfigFileChanged = $TRUE + } + } +} + +$SkipAzureDevOpsMigrationTool = ( ` + $SkipMigrateTfsAreaAndIterations -and ` + $SkipMigrateTeams -and ` + $SkipMigrateTestVariables -and ` + $SkipMigrateTestConfigurations -and ` + $SkipMigrateTestPlansAndSuites -and ` + $SkipMigrateWorkItemQuerys -and ` + $SkipMigrateBuildPipelines -and ` + $SkipMigrateReleasePipelines -and ` + $SkipMigrateTaskGroups -and ` + $SkipMigrateVariableGroups -and ` + $SkipMigrateWorkItems +) + + +if($martinConfigFileChanged) { + $martinConfiguration | ConvertTo-Json -Depth 100 | Set-Content $martinConfigPath +} +#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 +#endregion + + +# Clean up old martin's tool Configuration +Write-Host "Clean up Configuration file for Azure DevOps Migration Tool (Martin's Tool).." +if($martinConfigFileChanged) { + $martinPreviousConfiguration | ConvertTo-Json -Depth 100 | Set-Content $martinConfigPath +} diff --git a/project-migration/README - Github Action Workflow migration.md b/project-migration/README - Github Action Workflow migration.md new file mode 100644 index 0000000..dd7d2e2 --- /dev/null +++ b/project-migration/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/project-migration/README - Project Migration.md b/project-migration/README - Project Migration.md new file mode 100644 index 0000000..a2511ad --- /dev/null +++ b/project-migration/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.psm1 +``` + +## "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/project-migration/Step_0_Migrate_Project.ps1 b/project-migration/Step_0_Migrate_Project.ps1 new file mode 100644 index 0000000..e3d7c6f --- /dev/null +++ b/project-migration/Step_0_Migrate_Project.ps1 @@ -0,0 +1,72 @@ + +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 Items (Including 'Test Cases') + 0 - 75 - 17284 + 75 - 200 - 19010 + 200 - 400 - 19159 + 400 - 575 - 18754 + 575 - 800 - 16754 + 800 - 1000 - 16190 + 1000 - 2000 - 19821 + +#> +& .\Step_3_Migrate_Project.ps1 -WhatIf $WhatIf + +<# + Step #4 migrate + - 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/project-migration/Step_1_Migrate_Project.ps1 b/project-migration/Step_1_Migrate_Project.ps1 new file mode 100644 index 0000000..0ea9916 --- /dev/null +++ b/project-migration/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/project-migration/Step_2_Migrate_Project.ps1 b/project-migration/Step_2_Migrate_Project.ps1 new file mode 100644 index 0000000..b446e4f --- /dev/null +++ b/project-migration/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 ` +-SkipMigrateTaskGroups $WhatIf \ No newline at end of file diff --git a/project-migration/Step_3_Migrate_Project.ps1 b/project-migration/Step_3_Migrate_Project.ps1 new file mode 100644 index 0000000..4b12269 --- /dev/null +++ b/project-migration/Step_3_Migrate_Project.ps1 @@ -0,0 +1,164 @@ +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 ` +-WorkItemQueryBit "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') AND [System.CreatedDate] <= @Today - 3000 " + + + + diff --git a/project-migration/Step_4_Migrate_Project.ps1 b/project-migration/Step_4_Migrate_Project.ps1 new file mode 100644 index 0000000..6ee5b62 --- /dev/null +++ b/project-migration/Step_4_Migrate_Project.ps1 @@ -0,0 +1,36 @@ +Param ( + [Parameter (Mandatory=$FALSE)] [Boolean]$WhatIf = $TRUE +) + +Write-Host " " +Write-Host "Step 4 Migrate:" +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 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/project-migration/Step_5_Migrate_Project.ps1 b/project-migration/Step_5_Migrate_Project.ps1 new file mode 100644 index 0000000..044acfe --- /dev/null +++ b/project-migration/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/project-migration/Step_X_Migrate_Org_Level_Users.ps1 b/project-migration/Step_X_Migrate_Org_Level_Users.ps1 new file mode 100644 index 0000000..f2c1f92 --- /dev/null +++ b/project-migration/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/project-migration/Work-Item-Backfill_MIgrate_Project.ps1 b/project-migration/Work-Item-Backfill_MIgrate_Project.ps1 new file mode 100644 index 0000000..0a3e6e7 --- /dev/null +++ b/project-migration/Work-Item-Backfill_MIgrate_Project.ps1 @@ -0,0 +1,39 @@ + +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 = "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)' " +} + +& .\MigrateProject.ps1 -SkipMigrateWorkItems $WhatIf -WorkItemQueryBit $queryBit + + + diff --git a/tool-scripts/Clone-All-Project-Repos.ps1 b/tool-scripts/Clone-All-Project-Repos.ps1 new file mode 100644 index 0000000..e6317f1 --- /dev/null +++ b/tool-scripts/Clone-All-Project-Repos.ps1 @@ -0,0 +1,49 @@ + +Using Module "..\modules\Migrate-ADO-Common.psm1" + +Param( + [string]$SourcePat, + [string]$SourceOrg, + [string]$SourceProjectName + # [string]$TargetPat, + # [string]$TargetOrg, + # [string]$TargetProjectName +) + +# . .\AzureDevOps-Helpers.ps1 +# . .\AzureDevOps-ProjectHelpers.ps1 + + +Write-Log -Message " " +Write-Log -Message "-----------------" +Write-Log -Message "-- Clone Repos --" +Write-Log -Message "-----------------" +Write-Log -Message " " + +try { + $sourceHeaders = New-HttpHeaders -PersonalAccessToken $SourcePat + # $targetHeaders = New-HttpHeaders -PersonalAccessToken $TargetPat + + $repos = Get-Repos -projectName $SourceProjectName -orgName $SourceOrg -headers $sourceHeaders + # $targetRepos = Get-Repos -projectName $TargetProjectName -orgName $TargetOrg -headers $targetHeaders + $final = [array]@() + + foreach ($repo in $repos) { + + # if ($null -ne ($targetRepos | Where-Object {$_.name -ieq $repo.name})) { + # Write-Log -Message "Repo [$($repo.name)] already exists in target.. " + # continue + # } + + Write-Host "Cloning $($repo.name)" + # git clone $repo.remoteURL "`"$WorkingDir\$($repo.name)`"" + $final += $repo + } + return $final +} +catch { + Write-Error "Error cloning repos from org $sourceOrg and project $SourceProjectName" + Write-Error $_ + return +} + diff --git a/tool-scripts/Create-Manifest.ps1 b/tool-scripts/Create-Manifest.ps1 new file mode 100644 index 0000000..696ffbd --- /dev/null +++ b/tool-scripts/Create-Manifest.ps1 @@ -0,0 +1,52 @@ +# ----------- 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-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/tool-scripts/DeleteDashboards.ps1 b/tool-scripts/DeleteDashboards.ps1 new file mode 100644 index 0000000..397b258 --- /dev/null +++ b/tool-scripts/DeleteDashboards.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/tool-scripts/DeleteGroups.ps1 b/tool-scripts/DeleteGroups.ps1 new file mode 100644 index 0000000..eb05bf5 --- /dev/null +++ b/tool-scripts/DeleteGroups.ps1 @@ -0,0 +1,39 @@ + +Param ( + [Parameter (Mandatory=$TRUE)] [String]$OrgName, + [Parameter (Mandatory=$TRUE)] [String]$ProjectName, + [Parameter (Mandatory=$TRUE)] [String]$PAT, + [Parameter (Mandatory=$FALSE)] [bool]$DoDelete = $TRUE +) + + +Write-Host "Begin process to Delete ADO Security Groups for Organization $OrgName and Project $ProjectName.." + + +Write-Host "Get ADO Security Groups for Organization $OrgName and Project $ProjectName.." +$organization = "https://dev.azure.com/$OrgName/" +Set-AzDevOpsContext -PersonalAccessToken $PAT -OrgName $OrgName -ProjectName $ProjectName +$groups = (az devops security group list --organization $organization --project $ProjectName --detect $false | ConvertFrom-Json).graphGroups + +Write-Host "Begin Deleting ALL ADO Security Groups... " +Write-Host " " +foreach ($group in $groups) { + try { + # Delete Group + Write-Log -Message "Deleting ADO Security Group `"$($group.displayName)`" Group.. " + if($DoDelete -eq $TRUE) { + az devops security group delete --id $group.descriptor --detect $FALSE --org $organization --yes + } + } + 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 ADO Security Groups found in ($OrgName/$ProjectName)... " + + diff --git a/tool-scripts/DeleteServiceConnections.ps1 b/tool-scripts/DeleteServiceConnections.ps1 new file mode 100644 index 0000000..0c7b29a --- /dev/null +++ b/tool-scripts/DeleteServiceConnections.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/tool-scripts/Generate_Artifact_Feed_Package_Version_Data.ps1 b/tool-scripts/Generate_Artifact_Feed_Package_Version_Data.ps1 new file mode 100644 index 0000000..2abe140 --- /dev/null +++ b/tool-scripts/Generate_Artifact_Feed_Package_Version_Data.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/tool-scripts/GetCurrentUserInfo.ps1 b/tool-scripts/GetCurrentUserInfo.ps1 new file mode 100644 index 0000000..4ea7c1e --- /dev/null +++ b/tool-scripts/GetCurrentUserInfo.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/tool-scripts/GetItemsNotMigrated.ps1 b/tool-scripts/GetItemsNotMigrated.ps1 new file mode 100644 index 0000000..3eb816f --- /dev/null +++ b/tool-scripts/GetItemsNotMigrated.ps1 @@ -0,0 +1,160 @@ + +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, + [Parameter (Mandatory=$TRUE)] [String]$QueryBit +) + + +Write-Host "Begin GET Work-Items in Source that did not migrate to target.." +Write-Host " " + + # Create Headers +$sourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT +$targetHeaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + +$sourceIds = [System.Collections.ArrayList]::new() +$targetIds = [System.Collections.ArrayList]::new() +$migratedIds = [System.Collections.ArrayList]::new() + + +$sourceUrl = "https://dev.azure.com/$SourceOrgName/$SourceProjectName/_apis/wit/wiql?api-version=7.0" + +$sourceBody = "{""query"": ""SELECT [System.Id], [System.Tags] FROM WorkItems WHERE [System.TeamProject] = '$SourceProjectName' AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') $QueryBit ORDER BY [System.ChangedDate] desc""}" + +$sourceResult = $NULL +try { + $sourceResult = Invoke-RestMethod -Method Post -uri $sourceUrl -Headers $sourceHeaders -Body $sourceBody -ContentType "application/json" + $sourceItems = $sourceResult.workItems | Select-Object -ExpandProperty id + $sourceIds.AddRange($sourceItems) +} +catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} +} + + + +$itemQueryStrings = @() +if($NULL -ne $sourceResult) +{ + $counter = 1 + $itemQueryString = "" + foreach ($workitem in $sourceResult.workItems) { + $reflectedWorkItem = "'https://dev.azure.com/$SourceOrgName/$SourceProjectName/_workitems/edit/$($workitem.id)'" + + if($counter % 100 -eq 0) { + $itemQueryString += $reflectedWorkItem + $itemQueryStrings += $itemQueryString + $itemQueryString = "" + } else { + if($counter -ne $sourceResult.workitems.Count) { + $reflectedWorkItem = $reflectedWorkItem + "," + } + $itemQueryString += $reflectedWorkItem + } + + $counter += 1 + } + $itemQueryStrings += $itemQueryString +} + +Write-Log -Message "Validating if Source items migrated to Target.." + +try { + Write-Log -Message "Obtaining items in Target by ReflectedWorkItemId.." + foreach($queryString in $itemQueryStrings) { + Write-Host "." -NoNewline + + $targetUrl = "https://dev.azure.com/$TargetOrgName/$TargetProjectName/_apis/wit/wiql?api-version=7.0" + + $targetBody = "{""query"": ""SELECT [System.Id], [System.Tags] FROM WorkItems WHERE [System.TeamProject] = '$TargetProjectName' AND [Custom.ReflectedWorkItemID] IN ($queryString) ORDER BY [System.ChangedDate] desc""}" + + $targetResult = Invoke-RestMethod -Method Post -uri $targetUrl -Headers $targetHeaders -Body $targetBody -ContentType "application/json" + + $targetItems = $targetResult.workItems | Select-Object -ExpandProperty id + if($NULL -ne $targetItems) { + $targetIds.AddRange($targetItems) + } + } + Write-Host "." +} +catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} +} + + +$idQueryStrings = @() +if (($NULL -ne $targetIds) -and ($targetIds.Count -gt 0)) +{ + $counter = 1 + $idQueryString = "" + foreach ($targetId in $targetIds) { + $idString = "$targetId" + + if($counter % 200 -eq 0) { + $idQueryString += $idString + $idQueryStrings += $idQueryString + $idQueryString = "" + } else { + if($counter -ne $targetIds.Count) { + $idString = $idString + "," + } + $idQueryString += $idString + } + + $counter += 1 + } + $idQueryStrings += $idQueryString +} + + +try { + if ($idQueryStrings.Count -gt 0) { + Write-Log -Message "Obtaining items in Target by new Target Ids.." + foreach($idString in $idQueryStrings) { + Write-Host "." -NoNewline + + $migratedUrl = "https://dev.azure.com/$TargetOrgName/$TargetProjectName/_apis/wit/workitems?ids=$($idString)&api-version=7.0" + + $migratedResult = Invoke-RestMethod -Method GET -uri $migratedUrl -Headers $targetHeaders -ContentType "application/json" + + $migratedItems = $migratedResult.Value.Fields | Select-Object -ExpandProperty Custom.ReflectedWorkItemID | Select-Object @{ Name='id';Expression={ $_.Substring($_.LastIndexOF('/') + 1, $_.Length - 1 - $_.LastIndexOF('/'))} } | Select-Object -ExpandProperty id + + if($NULL -ne $targetItems) { + $migratedIds.AddRange($migratedItems) + } + + } + Write-Host "." + } +} +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 "Comparing Source Ids to Target IDs.." + + +$diffItems = ($sourceIds | Where-Object { $_ -notin $migratedIds }) + +Write-Host "$($diffItems.Count) Items failed to migrate..." + +Write-Host "End GET Work-Items in Source that did not migrate to target... " + +$diffItems diff --git a/tool-scripts/GetMigratedItemCounts.ps1 b/tool-scripts/GetMigratedItemCounts.ps1 new file mode 100644 index 0000000..31cfb14 --- /dev/null +++ b/tool-scripts/GetMigratedItemCounts.ps1 @@ -0,0 +1,74 @@ + +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, + [Parameter (Mandatory=$TRUE)] [String[]]$QueryBits +) + + +Write-Host "Begin GET Migrated Work-Item Counts.." +Write-Host " " + + # Create Headers +$sourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT +$targetHeaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + +$sourceTotal = 0 +$targetTotal = 0 +$differTotal = 0 + + +$counter = 1 +foreach($qBit in $QueryBits) +{ + + $sourceUrl = "https://dev.azure.com/$SourceOrgName/$SourceProjectName/_apis/wit/wiql?api-version=7.0" + $sourceBody = "{""query"": ""SELECT [System.Id], [System.Tags] FROM WorkItems WHERE [System.TeamProject] = '$SourceProjectName' AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') $qBit ORDER BY [System.ChangedDate] desc""}" + $sourceResult = $NULL + try { + $sourceResult = Invoke-RestMethod -Method Post -uri $sourceUrl -Headers $sourceHeaders -Body $sourceBody -ContentType "application/json" + $sourceCount = $sourceResult.workItems.Count + # $sourceCounts.Add($sourceCount) + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + + + $targetUrl = "https://dev.azure.com/$TargetOrgName/$TargetProjectName/_apis/wit/wiql?api-version=7.0" + $targetBody = "{""query"": ""SELECT [System.Id], [System.Tags] FROM WorkItems WHERE [System.TeamProject] = '$TargetProjectName' AND [System.WorkItemType] NOT IN ('Test Suite','Test Plan','Shared Steps','Shared Parameter','Feedback Request') $qBit ORDER BY [System.ChangedDate] desc""}" + $targetResult = $NULL + try { + $targetResult = Invoke-RestMethod -Method Post -uri $targetUrl -Headers $targetHeaders -Body $targetBody -ContentType "application/json" + $targetCount = $targetResult.workItems.Count + # $targetCounts.Add($targetCount) + } + catch { + Write-Log -Message "FAILED!" -LogLevel ERROR + Write-Log -Message $_.Exception -LogLevel ERROR + try { + Write-Log -Message ($_ | ConvertFrom-Json).message -LogLevel ERROR + } catch {} + } + + $difference = $sourceCount - $targetCount + Write-Host "Query Bit $($counter.ToString().PadLeft(2,' ')): Source Count = $($sourceCount.ToString().PadLeft(6,' ')) : Target Count = $($targetCount.ToString().PadLeft(6,' ')) : Difference = $($difference.ToString().PadLeft(6,' '))" + $counter += 1 + $sourceTotal += $sourceCount + $targetTotal += $targetCount + $differTotal += $difference +} + +Write-Host "---------------------------------------------------------------------------------" +Write-Host "Totals : Source Total = $($sourceTotal.ToString().PadLeft(6,' ')) : Target Total = $($targetTotal.ToString().PadLeft(6,' ')) : Diff Total = $($differTotal.ToString().PadLeft(6,' '))" +Write-Host " " +Write-Host "End GET Migrated Work-Item Counts.." + diff --git a/tool-scripts/GetServiceConnectionsLastUsed.ps1 b/tool-scripts/GetServiceConnectionsLastUsed.ps1 new file mode 100644 index 0000000..fd7c366 --- /dev/null +++ b/tool-scripts/GetServiceConnectionsLastUsed.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/tool-scripts/IdentifyPlansAndSuitesForUnknowUsers.ps1 b/tool-scripts/IdentifyPlansAndSuitesForUnknowUsers.ps1 new file mode 100644 index 0000000..f8e0114 --- /dev/null +++ b/tool-scripts/IdentifyPlansAndSuitesForUnknowUsers.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/tool-scripts/IdentifySuitesAndCasesForTestPlans.ps1 b/tool-scripts/IdentifySuitesAndCasesForTestPlans.ps1 new file mode 100644 index 0000000..5d91b80 --- /dev/null +++ b/tool-scripts/IdentifySuitesAndCasesForTestPlans.ps1 @@ -0,0 +1,63 @@ + +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 Test Suites/Test-Cases for each Test-Plan in the organization" +Write-Host "Source Organization - $OrgName" +Write-Host " " + +Start-Transcript -Path $OutputFile -Append + +# 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 + +foreach ($plan in $sourcePlans) { + Write-Host "Plan (id) Name: ($($plan.id)) $($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 + + Write-Host " Suite Count: $($sourceSuites.Count)" + foreach ($suite in $sourceSuites) { + Write-Host " Suite (id) Name: ($($suite.id)) $($suite.name)" + if($NULL -ne $suite.parentSuite) { + Write-Host " Parent Suite: ($($suite.parentSuite.id)) $($suite.parentSuite.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 + + Write-Host " Test-Case Count: $($sourceTestCases.Count)" + foreach ($testcase in $sourceTestCases) { + + # Get Test-Case details for the source Suite + $url = $testcase.testCase.Url + $results = Invoke-RestMethod -Method GET -Uri $url -Headers $Headers + + Write-Host " Test-Case (id) Name: ($($results.id)) $($results.fields."System.Title")" + } + } +} + +Write-Host " " +Write-Host " " +Write-Host "End - Identify Test Suites/Test-Cases for each Test-Plan in the organization" + +Stop-Transcript + + diff --git a/tool-scripts/README - Tool-Scripts.md b/tool-scripts/README - Tool-Scripts.md new file mode 100644 index 0000000..7f51250 --- /dev/null +++ b/tool-scripts/README - Tool-Scripts.md @@ -0,0 +1,62 @@ +# Tool Scripts + +This directory contains helpful PowerShell scripts that can be used to perform specific actions outside of an ADO project migration or in aiding in the testing, validating and troubleshooting during an ADO project migration. + + +## Files + +### Clone-All-Project-Repos.ps1 + +### Create-Manifest.ps1: +The `Create-Manifest.ps1` script creates a new PowerShell distribution manifest file (.psd1) under your `\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 Create-Manifest.ps1 file. + + +### DeleteDashboards.ps1: +The `DeleteDashboards.ps1` script can be used to remove all current dashboards in the Target project prior to running an ADO project migration for a clean Dhasboard migration. + + +### DeleteGroups.ps1: +Used to delete all ADO Security Groups in order start with a freash re-migration of all ADO Security Groups. + +### DeleteServiceConnections.ps1: +Service connections can be migrated with the exception of the credentials configured for them. Many times external processes are used to generate service connections. This script can be used to remove all of the service connection post migration so that new connections can be created. + +### Generate_Artifact_Feed_Package_Version_Data.ps1: +The `Generate_Artifact_Feed_Package_Version_Data.ps1` script is a helpful script when dealing with migrating Artifact Feed packages and the many versions that tend to collect. It generates a data repost lsting all of the packages and their verions for your Artifact feeds. + +### GetCurrentUserInfo.ps1: +This script will generate a data report containing user data that can be evaluated prior to and/or after a user migration. + +### GetItemsNotMigrated.ps1: +The migration of ADO work items can be a tedious process when the project being migrated contains thousands if not hundres of thousands of items. This can make it difficult to identify problems when migraitng work items. The `GetItemsNotMigrated.ps1` script will generate a report of all work items that are in the Source project that does not have a corresponding migrated work item in the Target project. + +### GetMigratedItemCounts.ps1: +This script is used in conjunction with the `GetItemsNotMigrated.ps1` script above to assist in identifying any issues during a work item migration. + +### GetServiceConnectionsLastUsed.ps1: +When migrating many service connections sometimes during a migration you want to not migrate items that are no longer used nor needed. This script will provide a list of the service connections present and the last time they were accessed so that you can make a determination to remove the unused service connections prior to doing a migration. + +### IdentifyPlansAndSuitesForUnknowUsers.ps1: +The `IdentifyPlansAndSuitesForUnknowUsers.ps1` script can be used to identify Test Plans, test Suite, and Test-Cases whos owner is not a user identity in the organization. + +### IdentifySuitesAndCasesForTestPlans.ps1: +This script is useful for identifying all of the Test Cases that a Test Plan contains. + +### Set-Readonly.ps1: +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. + +### ValidateBuildEnvironments.ps1: +The `ValidateBuildEnvironments.ps1` script is used to assist in validating build environment migration results. + +### ValidateBuildGroupsAndUsers.ps1: +The `ValidateBuildGroupsAndUsers.ps1` script is used to assist in validating build groups and users migration results. + +### VerifyFieldsForOrganizationProject.ps1, VerifyFieldsForWorkItemInProcess.ps1: +The `VerifyFieldsForOrganizationProject.ps1` and `VerifyFieldsForWorkItemInProcess.ps1` scripts are used to validate custom fields that are present for process templates prior to doing a project migration. In order to successfully migrate a project's work items, the process templates of the Source and Target must match fields. + + + + + diff --git a/migration-scripts/set-readonly.ps1 b/tool-scripts/Set-Readonly.ps1 similarity index 97% rename from migration-scripts/set-readonly.ps1 rename to tool-scripts/Set-Readonly.ps1 index 22f30c3..6031760 100644 --- a/migration-scripts/set-readonly.ps1 +++ b/tool-scripts/Set-Readonly.ps1 @@ -1,25 +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 -} - +# 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/tool-scripts/ValidateBuildEnvironments.ps1 b/tool-scripts/ValidateBuildEnvironments.ps1 new file mode 100644 index 0000000..e1c64c7 --- /dev/null +++ b/tool-scripts/ValidateBuildEnvironments.ps1 @@ -0,0 +1,103 @@ + +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, + + [Parameter (Mandatory = $TRUE)] + [String]$OutputFile + +) + + Write-Host " " + Write-Host '-----------------------------------------' + Write-Host '-- Validate Migrate Build Environments --' + Write-Host '-----------------------------------------' + Write-Host " " + + # Create Headers + $SourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT + $Targetheaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + + Start-Transcript -Path $OutputFile -Append + + Write-Host "Get Source Project to do source lookups.." + $sourceProject = Get-ADOProjects -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders + Write-Host "Get target Project to do source lookups.." + $targetProject = Get-ADOProjects -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders + + Write-Host "Get Source Environments.." + $sourceEnvironments = Get-BuildEnvironments -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -ProjectId $sourceProject.Id -Top 1000000 + Write-Host "Get Target Environments.." + $targetEnvironments = Get-BuildEnvironments -ProjectName $TargetProjectName -OrgName $TargetOrgName -Headers $Targetheaders -ProjectId $targetProject.Id -Top 1000000 + + Write-Host " " + Write-Host " " + Write-Host "Source Environments Count: $($sourceEnvironments.Count)" + Write-Host "Target Environments Count: $($targetEnvironments.Count)" + Write-Host " " + + $environmentsInSourceNotInTarget = $sourceEnvironments | Where-Object { $_.name -notin $targetEnvironments.name } + Write-Host "Environments in Source not in Target:" + foreach ($env1 in $environmentsInSourceNotInTarget) { + Write-Host "$($env1.name)" + } + + $environmentsInTargetNotInSource = $targetEnvironments | Where-Object { $_.name -notin $sourceEnvironments.name } + Write-Host "Environments in Target not in Source:" + foreach ($env2 in $environmentsInTargetNotInSource) { + Write-Host "$($env2.name)" + } + + + foreach ($sourceEnvironment in $sourceEnvironments) { + $sourceEnvName = $sourceEnvironment.name + $sourceEnvId = $sourceEnvironment.id + + Write-Host "----- Environment $($sourceEnvName) - $($sourceEnvId) -----" + Write-Host " " + $targetEnvironment = $targetEnvironments | Where-Object { $_.Name -ieq $sourceEnvironment.Name } + + + Write-Host "--- User permissions --- " + # $sourceRoleAssignments = Get-BuildEnvironmentRoleAssignments -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -ProjectId $sourceProject.Id -EnvironmentId $newEnvironment.Id + $url = "https://dev.azure.com/$SourceOrgName/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/$($sourceProject.Id)_$($sourceEnvironment.Id)" + $results1 = Invoke-RestMethod -Method Get -uri $url -Headers $SourceHeaders + Write-Host "Source Environment $($sourceEnvironment.Name) RoleAssignment Count: $($results1.Value.Count)" + + $url = "https://dev.azure.com/$TargetOrgName/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/$($targetProject.Id)_$($targetEnvironment.Id)" + $results2 = Invoke-RestMethod -Method Get -uri $url -Headers $TargetHeaders + Write-Host "Target Environment $($sourceEnvironment.Name) RoleAssignment Count: $($results2.Value.Count)" + + + Write-Host "--- Pipline permissions --- " + # $sourcePipelinePermissions = Get-BuildEnvironmentPipelinePermissions -ProjectName $SourceProjectName -OrgName $SourceOrgName -Headers $Sourceheaders -EnvironmentId $newEnvironment.Id + $url = "https://dev.azure.com/$SourceOrgName/$SourceProjectName/_apis/pipelines/pipelinePermissions/environment/$($sourceEnvironment.Id)" + $results3 = Invoke-RestMethod -Method Get -uri $url -Headers $SourceHeaders + Write-Host "Source Environment $($sourceEnvironment.Name) RoleAssignment Count: $($results3.Pipelines.Count)" + + $url = "https://dev.azure.com/$TargetOrgName/$TargetProjectName/_apis/pipelines/pipelinePermissions/environment/$($targetEnvironment.Id)" + $results4 = Invoke-RestMethod -Method Get -uri $url -Headers $TargetHeaders + Write-Host "Target Environment $($targetEnvironment.Name) RoleAssignment Count: $($results4.Pipelines.Count)" + Write-Host "--------------------------------------------------" + + Write-Host " " + + } + + Write-Host " " + Stop-Transcript diff --git a/tool-scripts/ValidateBuildGroupsAndUsers.ps1 b/tool-scripts/ValidateBuildGroupsAndUsers.ps1 new file mode 100644 index 0000000..3ab7398 --- /dev/null +++ b/tool-scripts/ValidateBuildGroupsAndUsers.ps1 @@ -0,0 +1,150 @@ + +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, + + [Parameter (Mandatory = $TRUE)] + [String]$OutputFile + +) + + Write-Host " " + Write-Host '----------------------------------------' + Write-Host '-- Validate Migrated Groups and Users --' + Write-Host '----------------------------------------' + Write-Host " " + + # # Create Headers + # $SourceHeaders = New-HTTPHeaders -PersonalAccessToken $SourcePAT + # $Targetheaders = New-HTTPHeaders -PersonalAccessToken $TargetPAT + + Write-Log -Message 'Get Source Group' + $sourceGroups = Get-ADOGroups -OrgName $SourceOrgName -ProjectName $SourceProjectName -PersonalAccessToken $SourcePAT + Write-Log -Message 'Get Target Group' + $targetGroups = Get-ADOGroups -OrgName $TargetOrgName -ProjectName $TargetProjectName -PersonalAccessToken $TargetPAT + + Start-Transcript -Path $OutputFile -Append + + Write-Host " " + Write-Host " " + Write-Host "Source Group Count: $($sourceGroups.Count)" + Write-Host "Target Group Count: $($targetGroups.Count)" + Write-Host " " + + $groupsInSourceNotInTarget = $sourceGroups | Where-Object { $_.name -notin $targetGroups.name } + Write-Host "Groups in Source not in Target: $($groupsInSourceNotInTarget.Count)" + Write-Host "--------------------------------" + foreach ($grp1 in $groupsInSourceNotInTarget) { + Write-Host " $($grp1.name)" + } + Write-Host "--------------------------------" + Write-Host " " + + $groupsInTargetNotInSource = $targetGroups | Where-Object { $_.name -notin $sourceGroups.name } + Write-Host "Groups in Target not in Source: $($groupsInTargetNotInSource.Count)" + Write-Host "--------------------------------" + foreach ($grp2 in $groupsInTargetNotInSource) { + Write-Host " $($grp2.name)" + } + Write-Host "--------------------------------" + Write-Host " " + + $jsonString = "" + foreach ($sourceGroup in $sourceGroups) { + + $sourceGroupName = $sourceGroup.name + $sourceGroupId = $sourceGroup.id + + Write-Host "--------------------------------------------------------------------------------------" + Write-Host "--- Group $($sourceGroupName) - $($sourceGroupId) ---" + Write-Host "--------------------------------------------------------------------------------------" + $targetGroup = $targetGroups | Where-Object { $_.Name -ieq $sourceGroup.Name } + + if($targetGroup) { + Write-Host "-------------------- " + Write-Host "--- User Members --- " + Write-Host "-------------------- " + + Write-Host "Source Group [$($sourceGroup.Name)] UserMembers Count: $($sourceGroup.UserMembers.Count)" + Write-Host "------------" + foreach ($s_userMember in $sourceGroup.UserMembers) { + Write-Host " $($s_userMember.Name), $($s_userMember.Id)" + } + Write-Host "------------" + Write-Host " " + + Write-Host "Target Group [$($targetGroup.Name)] UserMembers Count: $($targetGroup.UserMembers.Count)" + Write-Host "------------" + foreach ($t_userMember in $targetGroup.UserMembers) { + Write-Host " $($t_userMember.Name), $($t_userMember.Id)" + } + Write-Host "------------" + Write-Host " " + Write-Host " " + + Write-Host "--------------------- " + Write-Host "--- Group Members --- " + Write-Host "--------------------- " + + Write-Host "Source Group [$($sourceGroup.Name)] GroupMembers Count: $($sourceGroup.GroupMembers.Count)" + Write-Host "------------" + foreach ($s_groupMember in $sourceGroup.GroupMembers) { + Write-Host " $($s_groupMember.Name), $($s_groupMember.Id)" + } + Write-Host "------------" + Write-Host " " + + Write-Host "Target Group [$($targetGroup.Name)] GroupMembers Count: $($targetGroup.GroupMembers.Count)" + Write-Host "------------" + foreach ($t_groupMember in $targetGroup.GroupMembers) { + Write-Host " $($t_groupMember.Name), $($t_groupMember.Id)" + } + Write-Host "------------" + Write-Host " " + } + + $jsonString += "`n" + $jsonString += "--------------------------" + $jsonString += "--- Source Group JSON --- " + $jsonString += "--------------------------" + + $sourceGroupJson = ConvertTo-Json -Depth 100 $sourceGroup + $jsonString += $sourceGroupJson + + $jsonString += "`n" + $jsonString += "`n" + $jsonString += "--------------------------" + $jsonString += "--- Target Group JSON --- " + $jsonString += "--------------------------" + $sourceGroupJson = ConvertTo-Json -Depth 100 $targetGroup + $jsonString += $sourceGroupJson + $jsonString += "`n" + $jsonString += "--------------------------------------------------" + $jsonString += "`n" + } + + Write-Host " " + Write-Host "-------------------------------------------------------" + Write-Host "--- Source Group to Target Groups JSON Comparisons --- " + Write-Host "-------------------------------------------------------" + Write-Host $jsonString + Write-Host " " + Write-Host " " + Write-Host "--------------------------------------------------------------------------------------" + + Stop-Transcript diff --git a/tool-scripts/VerifyFieldsForOrganizationProject.ps1 b/tool-scripts/VerifyFieldsForOrganizationProject.ps1 new file mode 100644 index 0000000..5de6521 --- /dev/null +++ b/tool-scripts/VerifyFieldsForOrganizationProject.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/tool-scripts/VerifyFieldsForWorkItemInProcess.ps1 b/tool-scripts/VerifyFieldsForWorkItemInProcess.ps1 new file mode 100644 index 0000000..32f9c2d --- /dev/null +++ b/tool-scripts/VerifyFieldsForWorkItemInProcess.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/tools/clone-all-project-repos.ps1 b/tools/clone-all-project-repos.ps1 deleted file mode 100644 index b0185a7..0000000 --- a/tools/clone-all-project-repos.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -Param( - [string]$SourcePat = $sourcePat, - [string]$SourceOrg = $sourceOrg, - [string]$SourceProjectName = $sourceProjectName -) - -. .\AzureDevOps-Helpers.ps1 -. .\AzureDevOps-ProjectHelpers.ps1 - -Write-Log -msg " " -Write-Log -msg "-----------------" -Write-Log -msg "-- Clone Repos --" -Write-Log -msg "-----------------" -Write-Log -msg " " - -try { - $sourceHeaders = New-HttpHeaders -pat $sourcePat -org $sourceOrg - $repos = Get-Repos -org "$SourceOrg" -ProjectSK $SourceProjectName -headers $sourceHeaders - $targetRepos = Get-Repos -org "$TargetOrg" -ProjectSK $TargetProjectName -headers $targetHeaders - $final = [array]@() - - foreach ($repo in $repos) { - - if ($null -ne ($targetRepos | Where-Object {$_.name -ieq $repo.name})) { - Write-Log -msg "Repo [$($repo.name)] already exists in target.. " -NoNewline - continue - } - - Write-Host "Cloning $($repo.name)" - git clone $repo.remoteURL "`"$WorkingDir\$($repo.name)`"" - $final += $repo - } - return $final -} -catch { - Write-Error "Error cloning repos from org $sourceOrg and project $SourceProjectName" - Write-Error $_ - return -} - diff --git a/tools/migrate-external-repos.ps1 b/tools/migrate-external-repos.ps1 deleted file mode 100644 index 1c30821..0000000 --- a/tools/migrate-external-repos.ps1 +++ /dev/null @@ -1,65 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter()] - [String] - $SourceRepoUrl, - $SourceProjectName, - $TargetOrganizationName, - $PersonalAccessToken -) - -function Help() { - Write-Host " - - migrate-external-repos ... - Migrate external repos to Azure DevOps - Assumes user is already authenticated against source git host - " -} - -$modulePath = "C:\dev\AzureDevOps-Tools\modules" -Import-Module "$modulePath\Migrate-ADO-Common.psm1" -Import-Module "$modulePath\Migrate-ADO-Repos.psm1" - -$repos = Get-Content .\repos.txt -$headers = New-HTTPHeaders -PersonalAccessToken $pat - -$sourceProjectName = "" -$targetProjectName = "" -$orgName = "" - -$sourceRepoPrefix = "template source repo clone url $sourceProjectName " -$targetRepoPrefix = "template target repo clone url /$orgName/$targetProjectName/_git" - -foreach ($repo in $repos) { - - Write-Host "Cloning repo $repo" - $sourceRepo = [uri]::EscapeUriString("$repo") - - switch ($SourceProvider) { - "AzureDevOps" { - - } - "GitLab" { - - } - } - - git clone "$sourceRepoPrefix/$sourceRepo" - git clone --mirror "$sourceRepoPrefix/$sourceRepo.git" "./$repo/.git" - - Set-Location ".\$sourceRepo" - - git fetch --all - - $targetRepo = $repo.Replace(" ", "-") - Write-Host "Creating repo $targetRepo" - New-GitRepository -Project $targetProjectName -OrgName $orgName -RepoName $targetRepo -Headers $headers - - Write-Host "Adding remote ..." - git remote add ado "$targetRepoPrefix/$targetRepo" - - Write-Host "Push remote ..." - git push -u ado --all -} -