diff --git a/examples/Scaffolding/Boards/boardsettings.ps1 b/examples/Scaffolding/Boards/boardsettings.ps1 new file mode 100644 index 00000000..1b4e9acb --- /dev/null +++ b/examples/Scaffolding/Boards/boardsettings.ps1 @@ -0,0 +1,221 @@ +. "$PSScriptRoot\permissions.ps1" +$invokeRequestsPath = Join-Path $PSScriptRoot ..\InvokeRequests\ +$apiVersion = '5.0' +function setUpGeneralBoardSettings { + param( + [String]$org, + [String]$projectID, + [String]$teamID, + [String]$backlogIterationId, + [Bool]$epics, + [Bool]$stories, + [Bool]$features + ) + + # Team boards settings + $currentBoardsTeamSettings = az devops invoke --org $org --area work --resource teamsettings --api-version $apiVersion --http-method GET --route-parameters project=$projectID team=$teamID -o json | ConvertFrom-Json + + Write-Host "`nCurrent general team configurations" + "Current backlog navigation levels" + printBacklogLevels -boardsTeamSettings $currentBoardsTeamSettings + + #update these settings + $contentFileName = $invokeRequestsPath + 'updateTeamConfig.txt' + $contentToStoreInFile = [System.Text.StringBuilder]::new() + [void]$contentToStoreInFile.Append( "{") + [void]$contentToStoreInFile.Append( "`"backlogIteration`" : " ) + [void]$contentToStoreInFile.Append( ($backlogIterationId.ToString() | ConvertTo-Json) ) + + if ($epics -or $stories -or $features) { + [void]$contentToStoreInFile.Append( ",`"backlogVisibilities`" : { " ) + if ($epics -eq $True) { + [void]$contentToStoreInFile.Append( "`"Microsoft.EpicCategory`" : true " ) + } + else { + [void]$contentToStoreInFile.Append( "`"Microsoft.EpicCategory`" : false " ) + } + + if ($features -eq $True) { + [void]$contentToStoreInFile.Append( ",`"Microsoft.FeatureCategory`" : true " ) + } + else { + [void]$contentToStoreInFile.Append( ",`"Microsoft.FeatureCategory`" : false " ) + } + + if ($stories -eq $True) { + [void]$contentToStoreInFile.Append( ",`"Microsoft.RequirementCategory`" : true " ) + } + else { + [void]$contentToStoreInFile.Append( ",`"Microsoft.RequirementCategory`" : false " ) + } + + [void]$contentToStoreInFile.Append( "}" ) + } + [void]$contentToStoreInFile.Append( "}" ) + Set-Content -Path $contentFileName -Value $contentToStoreInFile.ToString() + + $updatedBoardsTeamSettings = az devops invoke --org $org --area work --resource teamsettings --api-version $apiVersion --http-method PATCH --route-parameters project=$projectID team=$teamID --in-file $contentFileName -o json | ConvertFrom-Json + "Updated backlog navigation levels" + printBacklogLevels -boardsTeamSettings $updatedBoardsTeamSettings +} + +function configureDefaultArea { + param( + [String]$org, + [String]$projectID, + [String]$teamID, + [String]$defaultAreaPath + ) + $listAreasForTeam = az devops invoke --org $org --area work --resource teamfieldvalues --api-version $apiVersion --http-method GET --route-parameters project=$projectID team=$teamID -o json | ConvertFrom-Json + #$defaultTeamArea = $listAreasForTeam.defaultValue + + $contentFileName = $invokeRequestsPath + 'updateDefaultAreaRequest.txt' + $contentToStoreInFile = [System.Text.StringBuilder]::new() + [void]$contentToStoreInFile.Append( "{`"defaultValue`" : " ) + [void]$contentToStoreInFile.Append( ($defaultAreaPath.ToString() | ConvertTo-Json) ) + $teamAreaValues = $listAreasForTeam.values + if ($teamAreaValues) { + $areaValues = $teamAreaValues | ConvertTo-Json + [void]$contentToStoreInFile.Append( ",`"values`" : " ) + [void]$contentToStoreInFile.Append( $areaValues.ToString()) + } + else { + [void]$contentToStoreInFile.Append( ",`"values`" : " ) + [void]$contentToStoreInFile.Append( "[ {`"value`" : " ) + [void]$contentToStoreInFile.Append( ($defaultAreaPath.ToString() | ConvertTo-Json) ) + [void]$contentToStoreInFile.Append( ",`"includeChildren`" : true" ) + [void]$contentToStoreInFile.Append( " } ]" ) + } + [void]$contentToStoreInFile.Append( "}" ) + Set-Content -Path $contentFileName -Value $contentToStoreInFile.ToString() + + $updateDefaultAreaForTeam = az devops invoke --org $org --area work --resource teamfieldvalues --api-version $apiVersion --http-method PATCH --route-parameters project=$projectID team=$teamID --in-file $contentFileName -o json | ConvertFrom-Json + Write-Host "Default area is now: $($updateDefaultAreaForTeam.defaultValue)" +} + +function createTeamArea { + param( + [String]$org, + [String]$projectID, + [String]$areaName + ) + + $contentFileName = $invokeRequestsPath + 'createTeamAreaRequest.txt' + $contentToStoreInFile = [System.Text.StringBuilder]::new() + [void]$contentToStoreInFile.Append( "{`"name`" : " ) + [void]$contentToStoreInFile.Append( ($areaName.ToString() | ConvertTo-Json) ) + [void]$contentToStoreInFile.Append( "}" ) + Set-Content -Path $contentFileName -Value $contentToStoreInFile.ToString() + + $createAreasForTeam = az devops invoke --org $org --area wit --resource classificationnodes --api-version $apiVersion --http-method POST --route-parameters project=$projectID --query-parameters structureGroup=Areas --in-file $contentFileName -o json | ConvertFrom-Json + #$defaultTeamArea = $createAreasForTeam.defaultValue + + Write-Host "`nNew area created : $($createAreasForTeam.name) with id : $($createAreasForTeam.id)" +} + +function printBacklogLevels([object]$boardsTeamSettings) { + if ($boardsTeamSettings) { + $epics = SelectObject -inputObject $boardsTeamSettings.backlogVisibilities -propertyName Microsoft.EpicCategory + Write-Host "Epics: $epics" + + $features = SelectObject -inputObject $boardsTeamSettings.backlogVisibilities -propertyName Microsoft.FeatureCategory + Write-Host "Features: $features" + + $requirements = SelectObject -inputObject $boardsTeamSettings.backlogVisibilities -propertyName Microsoft.RequirementCategory + Write-Host "Stories: $requirements" + + $days = $boardsTeamSettings.workingDays + Write-Host "Working days : $days" + } +} + +function SelectObject([object]$inputObject, [string]$propertyName) { + $objectExists = Get-Member -InputObject $inputObject -Name $propertyName + + if ($objectExists) { + return $inputObject | Select-Object -ExpandProperty $propertyName + } + return $null +} + +function projectLevelIterationsSettings { + param( + [String]$org, + [String]$projectID, + [String]$rootIterationName, + [String]$subject, + [Int]$allow, + [Int]$deny, + [String[]]$childIterationNamesList + ) + # Project level iterations + $apiVersion = '5.0' + $depthParam = "`$depth=1" + + $projectRootIterationList = az devops invoke --org $org --api-version $apiVersion --area wit --resource classificationnodes --route-parameters project=$projectID structureGroup=iterations --query-parameters $depthParam -o json | ConvertFrom-Json + $iterationsNodeID = $projectRootIterationList.identifier + + $contentFileName = $invokeRequestsPath + 'createIterationRequest.txt' + $contentToStoreInFile = [System.Text.StringBuilder]::new() + [void]$contentToStoreInFile.Append( "{`"name`" : " ) + [void]$contentToStoreInFile.Append( ($rootIterationName.ToString() | ConvertTo-Json) ) + [void]$contentToStoreInFile.Append( "}" ) + Set-Content -Path $contentFileName -Value $contentToStoreInFile.ToString() + + $projectRootIterationCreate = az devops invoke --org $org --api-version 5.0 --area wit --resource classificationnodes --route-parameters project=$projectID structureGroup=iterations --http-method POST --in-file $contentFileName -o json | ConvertFrom-Json + + if ($projectRootIterationCreate) { + Write-Host "`nRoot Iteration created with name: $($projectRootIterationCreate.name)" + foreach ($entry in $childIterationNamesList) { + $childIterationName = $rootIterationName + ' ' + $entry.ToString() + $contentToStoreInFile = [System.Text.StringBuilder]::new() + [void]$contentToStoreInFile.Append( "{`"name`" : " ) + [void]$contentToStoreInFile.Append( ($childIterationName | ConvertTo-Json) ) + [void]$contentToStoreInFile.Append( "}" ) + Set-Content -Path $contentFileName -Value $contentToStoreInFile.ToString() + $projectChildIterationCreate = az devops invoke --org $org --api-version 5.0 --area wit --resource classificationnodes --route-parameters project=$projectID structureGroup=iterations --query-parameters path=$rootIterationName --http-method POST --in-file $contentFileName -o json | ConvertFrom-Json + Write-Host "Child Iteration created with name: $($projectChildIterationCreate.name)" + } + + # Add permissions at root iterations + $rootIterationToken = get_token -iterationsNodeID $iterationsNodeID -rootIterationID $($projectRootIterationCreate.identifier) + $updatePermissions = setPermissions -org $org -tokenStr $rootIterationToken -subject $subject -allowBit $allow -denyBit $deny + } + $projectRootIterationCreate.identifier +} + + +function setUpTeamIterations { + param( + [String]$org, + [String]$projectID, + [String]$teamID, + [String]$backlogIterationName + ) + + #get iteration id from name + $getBacklogIteration = az devops invoke --org $org --api-version $apiVersion --area wit --resource classificationnodes --route-parameters project=$projectID structureGroup=iterations --query-parameters path=$backlogIterationName -o json | ConvertFrom-Json + $getBacklogIterationID = $getBacklogIteration.id + + $depthParam = "`$depth=1" + $listChildIterations = az devops invoke --org $org --api-version $apiVersion --area wit --resource classificationnodes --route-parameters project=$projectID --query-parameters ids=$getBacklogIterationID $depthParam -o json | ConvertFrom-Json + if ($listChildIterations.count -eq 1) { + $rootIteration = $listChildIterations.value[0] + if ($rootIteration.hasChildren -eq $True) { + foreach ($child in $rootIteration.children) { + $getProjectTeamIterationID = $child.identifier + + # add this child iteration to the given team + $contentFileName = $invokeRequestsPath + 'setUpTeamIterations.txt' + $contentToStoreInFile = [System.Text.StringBuilder]::new() + [void]$contentToStoreInFile.Append( "{") + [void]$contentToStoreInFile.Append( "`"id`" : " ) + [void]$contentToStoreInFile.Append( ($getProjectTeamIterationID | ConvertTo-Json) ) + [void]$contentToStoreInFile.Append( "}" ) + Set-Content -Path $contentFileName -Value $contentToStoreInFile.ToString() + + $addTeamIteration = az devops invoke --org $org --area work --resource iterations --api-version $apiVersion --http-method POST --route-parameters project=$projectID team=$teamID --in-file $contentFileName -o json | ConvertFrom-Json + } + } + } +} \ No newline at end of file diff --git a/examples/Scaffolding/Boards/permissions.ps1 b/examples/Scaffolding/Boards/permissions.ps1 new file mode 100644 index 00000000..bc828295 --- /dev/null +++ b/examples/Scaffolding/Boards/permissions.ps1 @@ -0,0 +1,66 @@ +. (Join-Path $PSScriptRoot ..\Utils\permissionsHelper.ps1) + +function get_token{ + param( + [String]$iterationsNodeID, + [String]$rootIterationID, + [String]$childIterationID + ) + $rootStr = 'vstfs:///Classification/Node/' + $tokenStr = '' + if($iterationsNodeID) + { + $tokenStr = $rootStr + $iterationsNodeID + if($rootIterationID) + { + $tokenStr = $tokenStr + ':' + $rootStr + $rootIterationID + if($childIterationID) + { + $tokenStr = $tokenStr + ':' + $rootStr + $childIterationID + } + return $tokenStr + } + } + else { + return $null + } +} + +function setPermissions{ + param( + [String]$org, + [String]$subject, + [String]$tokenStr, + [Int]$allowBit, + [Int]$denyBit + ) + # boards iterations namespace id + $namespaceId = 'bf7bfa03-b2b7-47db-8113-fa2e002cc5b1' + + $aclList = az devops security permission list --org $org --subject $subject --id $namespaceId -o json | ConvertFrom-Json + foreach($acl in $aclList){ + if ($($acl.token) -contains $tokenStr) + { + # Show permissions + $displayPermissions = az devops security permission show --org $org --id $namespaceId --subject $subject --token $tokenStr -o json | ConvertFrom-Json + Write-Host "`nCurrent iterations related permissions for admin group :" + displayPermissions -permissionsResponse $displayPermissions + + # Update permissions + if($allowBit) + { + $updatePermissions = az devops security permission update --org $org --id $namespaceId --subject $subject --token $tokenStr --allow-bit $allowBit -o json | ConvertFrom-Json + } + + if($denyBit) + { + $updatePermissions = az devops security permission update --org $org --id $namespaceId --subject $subject --token $tokenStr --deny-bit $denyBit -o json | ConvertFrom-Json + } + + $displayPermissions = az devops security permission show --org $org --id $namespaceId --subject $subject --token $tokenStr -o json | ConvertFrom-Json + Write-Host "Updated iterations related permissions for admin group :" + displayPermissions -permissionsResponse $displayPermissions + } + } +} + diff --git a/examples/Scaffolding/Repos/setPolicies.ps1 b/examples/Scaffolding/Repos/setPolicies.ps1 new file mode 100644 index 00000000..96cfe7d1 --- /dev/null +++ b/examples/Scaffolding/Repos/setPolicies.ps1 @@ -0,0 +1,32 @@ +function set_policies( + [String]$org, + [String]$projectName, + [String]$repoId, + [String]$branch, + [string[]]$requiredApprovers, + [string[]]$optionalApprovers +) +{ + if($requiredApprovers) + { + $reviewersRequired = '' + foreach($reviewer in $requiredApprovers) + { + $reviewersRequired= $reviewersRequired + $reviewer +';' + } + + $reviewersRequired = $reviewersRequired.Substring(0,$reviewersRequired.Length-1) + $reviewerPolicy = az repos policy required-reviewer create --org $org -p $projectName --branch $branch --repository-id $repoId --is-blocking true --is-enabled true --message 'Required reviewers policy added' --required-reviewer-ids $reviewersRequired -o json | ConvertFrom-Json + } + # set optional reviewers + if($optionalApprovers) + { + $reviewersOptional = '' + foreach($reviewer in $optionalApprovers) + { + $reviewersOptional= $reviewersOptional + $reviewer +';' + } + $reviewersOptional = $reviewersOptional.Substring(0,$reviewersOptional.Length-1) + $reviewerPolicy = az repos policy required-reviewer create --org $org -p $projectName --branch $branch --repository-id $repoId --is-blocking false --is-enabled true --message 'Optional reviewers policy added' --required-reviewer-ids $reviewersOptional -o json | ConvertFrom-Json + } +} diff --git a/examples/Scaffolding/Utils/cleanUpStaleProjects.ps1 b/examples/Scaffolding/Utils/cleanUpStaleProjects.ps1 new file mode 100644 index 00000000..622e75fe --- /dev/null +++ b/examples/Scaffolding/Utils/cleanUpStaleProjects.ps1 @@ -0,0 +1,8 @@ +$org = 'https://dev.azure.com/ishitamehta' +$listProjects = az devops project list --org $org -o json | ConvertFrom-Json +foreach($proj in $listProjects){ + if ($proj.name -match '0905'){ + $deleteProject = az devops project delete --id $proj.id --org $org -y -o json + Write-Host "$deleteProject" + } +} diff --git a/examples/Scaffolding/Utils/permissionsHelper.ps1 b/examples/Scaffolding/Utils/permissionsHelper.ps1 new file mode 100644 index 00000000..4c100d58 --- /dev/null +++ b/examples/Scaffolding/Utils/permissionsHelper.ps1 @@ -0,0 +1,15 @@ +function displayPermissions{ + param([object]$permissionsResponse) + + foreach($acl in $permissionsResponse) + { + $ace = $acl.acesDictionary + $ace_key = $ace | Get-Member -MemberType NoteProperty | Select -ExpandProperty Name + $ace_value= $ace.$ace_key + $permissionsList = $($ace_value.resolvedPermissions) + foreach($perm in $permissionsList) + { + Write-Host "$($perm.displayName) [$($perm.bit)] , $($perm.effectivePermission)" + } + } +} diff --git a/examples/Scaffolding/configureTeam.ps1 b/examples/Scaffolding/configureTeam.ps1 new file mode 100644 index 00000000..3104e908 --- /dev/null +++ b/examples/Scaffolding/configureTeam.ps1 @@ -0,0 +1,38 @@ +. (Join-Path $PSScriptRoot .\Utils\permissionsHelper.ps1) +function addTeamAdmins{ + param ( + [String]$adminGrpDescriptor, + [String]$org, + [String]$projectID, + [String]$teamID + ) + + $securityToken = $projectID + '\' + $teamID + + #display current permissions + $showIdentityPermissions = az devops security permission show --org $org --id 5a27515b-ccd7-42c9-84f1-54c998f03866 --token $securityToken --subject $adminGrpDescriptor -o json | ConvertFrom-Json + Write-Host "Current permissions for this group" + displayPermissions -permissionsResponse $showIdentityPermissions + + #update permissions to manage : Adding team admins is equivalent to giving them manage permissions (i.e bit 31) + $updateIdentityPermissions = az devops security permission update --allow-bit 31 --org $org --id 5a27515b-ccd7-42c9-84f1-54c998f03866 --token $securityToken --subject $adminGrpDescriptor -o json | ConvertFrom-Json + Write-Host "`nGiving admins permissions to the requested group. Updated permissions:" + + $showIdentityPermissions = az devops security permission show --org $org --id 5a27515b-ccd7-42c9-84f1-54c998f03866 --token $securityToken --subject $adminGrpDescriptor -o json | ConvertFrom-Json + displayPermissions -permissionsResponse $showIdentityPermissions + +} + +function addTeamMembers { + param ( + [String[]]$teamMembersList, + [String]$org, + [String]$teamDescriptor + ) + + foreach($member in $teamMembersList) + { + $addMember = az devops security group membership add --group-id $teamDescriptor --member-id $member --org $org -o json | ConvertFrom-Json + Write-Host "Team member $($member) added" + } +} \ No newline at end of file diff --git a/examples/Scaffolding/org_details.txt b/examples/Scaffolding/org_details.txt new file mode 100644 index 00000000..6ca17570 --- /dev/null +++ b/examples/Scaffolding/org_details.txt @@ -0,0 +1,11 @@ +org=https://dev.azure.com/contoso +projectName=cliDemoScaffolding +repoName=cli_repo +repoToImport = https://github.com/ishitam8/snake.git +teamName=Protocol CLI team +optionalReviewers=user1@contoso.com,user2@contoso.com +requiredReviewers=user3@contoso.com,user4@contoso.com +teamMembers=user1@contoso.com,user2@contoso.com,user3@contoso.com +teamAdminMembers=admin1@contoso.com,admin2@contoso.com +childIterationNamesList=Sprint1,Sprint2,Sprint3 +iterationsPermissionsBit=7 \ No newline at end of file diff --git a/examples/scaffolding.md b/examples/Scaffolding/scaffolding.md similarity index 64% rename from examples/scaffolding.md rename to examples/Scaffolding/scaffolding.md index b6801c55..fe845fd8 100644 --- a/examples/scaffolding.md +++ b/examples/Scaffolding/scaffolding.md @@ -1,3 +1,5 @@ +# Automating Team Project set up + **Scaffolding.ps1** is a powershell script which helps users of an organization to have a standard getting started experience. ## Sample command to invoke the script – @@ -32,73 +34,44 @@ You can either interactively provide inputs or pass a file instead. The contents ## What does this script do -1. Your organization URL - - [org=https://dev.azure.com/contoso] - -2. Creates a new project under this organization - - [projectName=cliDemoScaffolding] - -3. Creates a new Repository - - [repoName=cli_repo] - -4. Repository URL to be imported in the newly created repo - - [repoToImport = https://github.com/ishitam8/snake.git] - +1. Takes Your organization URL + `[org=https://dev.azure.com/contoso]` +1. Creates a new project under this organization + `[projectName=cliDemoScaffolding]` +1. Creates a new Repository + `[repoName=cli_repo]` +1. Repository URL to be imported in the newly created repo + `[repoToImport = https://github.com/ishitam8/snake.git]` Accepts only public repo URL. - -5. List of required reviewers for configuring branch policies - - [requiredReviewers=user1@contoso.com,user2@contoso.com] - +1. List of required reviewers for configuring branch policies + `[requiredReviewers=user1@contoso.com,user2@contoso.com]` Currently, these branch policies are applied on master. - -6. List of required reviewers for configuring branch policies - - [optionalReviewers=user3@contoso.com,user4@contoso.com] - +1. List of required reviewers for configuring branch policies + `[optionalReviewers=user3@contoso.com,user4@contoso.com]` Currently, these branch policies are applied on master. - -7. Creates a team - - [teamName=Protocol CLI team] - -8. Adds the list of team members to the new team - - [teamMembers=user1@contoso.com,user2@contoso.com,user3@contoso.com] - -9. Creates a corresponding admins group which would be used to manage this team. - +1. Creates a team + `[teamName=Protocol CLI team]` +1. Adds the list of team members to the new team + `[teamMembers=user1@contoso.com,user2@contoso.com,user3@contoso.com]` +1. Creates a corresponding admins group which would be used to manage this team. Add this admins group as `Team administrator` of this team. - -10. Adds the list of admin members to the team admin group - - [teamAdminMembers=admin1@contoso.com,admin2@contoso.com] - -11. Boards settings for this team - +1. Adds the list of admin members to the team admin group + `[teamAdminMembers=admin1@contoso.com,admin2@contoso.com]` +1. Boards settings for this team Setting up area - - Creates a new area for this team and sets it to default area for this team. - Iterations related settings - - Creates a root iteration by the same name as team [i.e teamName]. - Creates child iterations which will be added this root iteration - [childIterationNamesList=Sprint1,Sprint2,Sprint3]. + `[childIterationNamesList=Sprint1,Sprint2,Sprint3]`. - Configure/add these root and child iterations to the newly created team. - Give iterations related permissions to the admins group - [iterationsPermissionsBit=7] + `[iterationsPermissionsBit=7]` The permission bit 7 denotes the addition of required permission bits to be allowed to this admins group. Which shall be as follows View permissions for this node = 1 Edit this node = 2 Create child nodes = 4 - General settings - - Configure backlog navigation settings [Currently assumed as epics: true, features: true, stories: true] - Configure working days [Currently assumed as Monday to Friday] diff --git a/examples/scaffolding.ps1 b/examples/Scaffolding/scaffolding.ps1 similarity index 100% rename from examples/scaffolding.ps1 rename to examples/Scaffolding/scaffolding.ps1