Skip to content

Commit

Permalink
feat: create pull requests for solution merges (#61)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This commit include multiple breaking changes: 
- Extract build YAML files must be updated to match the sample in this commit
- Merge-SolutionVersion.ps1 scripts must be replaced by the Merge-SolutionMerge.ps1 script in the sample in this commit
- All field values in the Azure DevOps tab of the Solution table must be moved to Project and Repository table rows
- Azure DevOps build service users must be granted permissions to contribute to pull requests
  • Loading branch information
ewingjm committed Feb 28, 2021
1 parent 01bda31 commit 738632e
Show file tree
Hide file tree
Showing 140 changed files with 5,663 additions and 1,532 deletions.
122 changes: 58 additions & 64 deletions README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion deploy/PkgFolder/ImportConfig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
<configsolutionfile overwriteunmanagedcustomizations="false" publishworkflowsandactivateplugins="true" solutionpackagefilename="devhub_DevelopmentHub_Develop.zip" />
<configsolutionfile overwriteunmanagedcustomizations="false" publishworkflowsandactivateplugins="true" solutionpackagefilename="devhub_DevelopmentHub_AzureDevOps.zip" />
</solutions>
<templateconfig />
<templateconfig>
<processes>
<process name="[Template] When a solution merge is committed to {{repository}} -> Update the solution merge status" state="Inactive" />
</processes>
</templateconfig>
</configdatastorage>
Binary file modified docs/images/development.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/environment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/issue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/project.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/repository.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/solution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/solutionmerge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions samples/azure-pipelines-extract.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: $(solution) $(commitMessage)
name: 'Solution merge'
pool:
vmImage: windows-latest
trigger: none
Expand All @@ -8,8 +8,8 @@ steps:
- task: PowerShell@2
inputs:
workingDirectory: $(Build.SourcesDirectory)
filePath: 'scripts/Merge-SolutionVersion.ps1'
arguments: '-ClientId "$(Client ID)" -TenantId "$(Tenant ID)" -ClientSecret (ConvertTo-SecureString "$(Client Secret)" -AsPlainText -Force) -SolutionVersionId "$(solutionVersionId)" -Solution "$(solution)" -CommitUserEmailAddress "$(triggeredByEmail)" -CommitUserName "$(triggeredBy)" -CommitMessage "$(commitMessage)" -SourceBranch "$(sourceBranch)" -WorkItemId "$(workItemId)"'
filePath: 'scripts/Merge-SolutionMerge.ps1'
arguments: '-ClientId "$(clientId)" -TenantId "$(tenantId)" -ClientSecret (ConvertTo-SecureString "$(clientSecret)" -AsPlainText -Force) -SolutionMergeId "$(solutionMergeId)" -DevEnvironmentUrl "$(devEnvironmentUrl)"'
displayName: Extract and commit
variables:
- group: Development Hub
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
222 changes: 222 additions & 0 deletions samples/scripts/Merge-SolutionMerge.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
param (
[Parameter()]
[String]
$ClientId,
[Parameter()]
[String]
$TenantId,
[Parameter()]
[SecureString]
$ClientSecret,
[Parameter()]
[String]
$SolutionMergeId,
[Parameter()]
[String]
$DevEnvironmentUrl
)

Install-Module ADAL.PS -Scope CurrentUser -Force

Write-Host "Installing solution packager."

$coreToolsPath = nuget install Microsoft.CrmSdk.CoreTools -o (Join-Path $env:TEMP -ChildPath packages) | Where-Object { $_ -like "*Installing package *'Microsoft.CrmSdk.CoreTools' to '*'." } | Select-String -Pattern "to '(.*)'" | ForEach-Object { $_.Matches[0].Groups[1].Value }
$solutionPackager = Get-ChildItem -Filter "SolutionPackager.exe" -Path $coreToolsPath -Recurse

function Get-WebApiHeaders ($url, $clientId, $tenantId, $clientSecret) {
Write-Host "Getting access token for $url for client ID $clientId."
$tokenResponse = Get-AdalToken -Resource $url -ClientId $clientId -Authority "https://login.microsoftonline.com/$tenantId" -ClientSecret $clientSecret -Verbose
$token = $tokenResponse.AccessToken

return @{
"method" = "GET"
"authorization" = "Bearer $token"
"content-type" = "application/json"
}
}

Write-Host "Authenticating to development environment."
$devWebApiHeaders = Get-WebApiHeaders -url $DevEnvironmentUrl -clientId $ClientId -tenantId $TenantId -clientSecret $ClientSecret
$devWebApiUrl = "$DevEnvironmentUrl/api/data/v9.1"

Write-Host "Getting solution merge $SolutionMergeId."
$select = 'devhub_sourcebranch,statuscode'
$expand = 'createdby($select=fullname,internalemailaddress),devhub_TargetSolution($select=devhub_uniquename;$expand=devhub_StagingEnvironment($select=devhub_url),devhub_Repository($select=devhub_sourcecontrolstrategy,devhub_extractbuilddefinitionid,devhub_targetbranch)),devhub_Issue($select=devhub_type,devhub_name,devhub_azuredevopsworkitemid,devhub_developmentsolution)'
$solutionMerge = Invoke-RestMethod -Uri "$devWebApiUrl/devhub_solutionmerges($SolutionMergeId)?`$select=$select&`$expand=$expand" -Headers $devWebApiHeaders

Write-Host "Setting git user configuration to solution merge creator: $($solutionMerge.createdby.internalemailaddress)."
git config --global user.email $solutionMerge.createdby.internalemailaddress
git config --global user.name $solutionMerge.createdby.fullname

Write-Host "Checking source control strategy."
if ($solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_sourcecontrolstrategy -eq 353400000) {
Write-Host "Source control strategy is pull request."
if ($solutionMerge.devhub_sourcebranch) {
Write-Host "Source branch provided. Checking out $($solutionMerge.devhub_sourcebranch)."
$updateExistingBranch = $true
git checkout "$($solutionMerge.devhub_sourcebranch)"
}
else {
Write-Host "No source branch provided. Calculating branch name from solution merge issue."
$branchPrefix = if ($solutionMerge.devhub_Issue.devhub_type -eq 353400001) { "feature/" } else { "bugfix/" }
$branchName = $solutionMerge.devhub_Issue.devhub_name.ToLower().Replace(' ', '-') -replace "[^a-zA-Z0-9\s-]"
$calculatedBranch = "$branchPrefix$branchName"

Write-Host "Checking if $calculatedBranch exists."
$updateExistingBranch = $null -ne (git rev-parse --verify --quiet "origin/$calculatedBranch")
if ($updateExistingBranch) {
Write-Host "Branch already exists. Updating existing branch at $calculatedBranch."
git checkout "$calculatedBranch"
}
else {
Write-Host "Branch not found. Creating branch $calculatedBranch."
git checkout -b "$calculatedBranch"
}
}
}
else {
Write-Host "Source control strategy is push. Checking out $($solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_targetbranch)"
git checkout $solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_targetbranch
if ($SourceBranch) {
Write-Host "Source branch provided. Squashing $SourceBranch."
git merge origin/$SourceBranch --squash --no-commit;
}
$result = git merge HEAD
if ($result[0] -like "*error*") {
Write-Error "Unable to automatically merge the source branch due to a conflict."
}
}

Write-Host "Authenticating to extract environment."
$extractUrl = $solutionMerge.devhub_TargetSolution.devhub_StagingEnvironment.devhub_url
$extractWebApiHeaders = Get-WebApiHeaders -url $extractUrl -clientId $ClientId -tenantId $TenantId -clientSecret $ClientSecret
$extractWebApiUrl = "$($extractUrl)/api/data/v9.1"

Write-Host "Exporting $($solutionMerge.devhub_TargetSolution.devhub_uniquename) as unmanaged."
$unmanagedZipResponse = Invoke-RestMethod -Uri "$($extractWebApiUrl)/ExportSolution" `
-Method POST `
-Headers $extractWebApiHeaders `
-UseBasicParsing `
-Body (ConvertTo-Json @{ SolutionName = $solutionMerge.devhub_TargetSolution.devhub_uniquename; Managed = $false })

$unmanagedZipFilePath = Join-Path -Path $env:TEMP -ChildPath "$($solutionMerge.devhub_TargetSolution.devhub_uniquename).zip"
Write-Host "Writing unmanaged solution to $unmanagedZipFilePath."
[IO.File]::WriteAllBytes($unmanagedZipFilePath, [Convert]::FromBase64String($unmanagedZipResponse.ExportSolutionFile));

Write-Host "Exporting $($solutionMerge.devhub_TargetSolution.devhub_uniquename) as managed."
$managedZipResponse = Invoke-RestMethod `
-Uri "$extractWebApiUrl/ExportSolution" `
-Method POST `
-Headers $extractWebApiHeaders `
-UseBasicParsing `
-Body (ConvertTo-Json @{ SolutionName = $solutionMerge.devhub_TargetSolution.devhub_uniquename; Managed = $true })

$managedZipFilePath = Join-Path -Path $env:TEMP -ChildPath "$($solutionMerge.devhub_TargetSolution.devhub_uniquename)_managed.zip"
Write-Host "Writing managed solution to $managedZipFilePath."
[IO.File]::WriteAllBytes($managedZipFilePath, [Convert]::FromBase64String($managedZipResponse.ExportSolutionFile));

$solutionFolder = Get-ChildItem -Filter $solutionMerge.devhub_TargetSolution.devhub_uniquename -Path "./src/solutions" -Directory
$extractFolder = Join-Path -Path $solutionFolder.FullName -ChildPath "extract"
Write-Host "Extracting solutions with the Solution Packager to $extractFolder."
$solutionPackagerPath = $solutionPackager.FullName
& $solutionPackagerPath /action:Extract /zipfile:$unmanagedZipFilePath /folder:$extractFolder /packagetype:Both /allowWrite:Yes /allowDelete:Yes

git add .
git reset -- NuGet.config

Write-Host "Calculating commit message from solution merge issue."
$commitPrefix = if ($solutionMerge.devhub_Issue.devhub_type -eq 353400001) { "feat: " } else { "fix: " }
$commitMessage = $solutionMerge.devhub_Issue.devhub_name
$buildNumber = $commitMessage -replace "[^a-zA-Z0-9\s]"
Write-Host "##vso[build.updatebuildnumber]$buildNumber"

$commitTrailers = @"
Solution-merge-id: $SolutionMergeId
Solution-merge-creator: $($solutionMerge.createdby.fullname) <$($solutionMerge.createdby.internalemailaddress)>
"@

if ($solutionMerge.devhub_Issue.devhub_azuredevopsworkitemid) {
Write-Host "Committing '$commitPrefix$commitMessage' with work item $($solutionMerge.devhub_Issue.devhub_azuredevopsworkitemid)."
git commit -m "$commitPrefix$commitMessage" -m "#$($solutionMerge.devhub_Issue.devhub_azuredevopsworkitemid)" -m "$commitTrailers";
}
else {
Write-Host "Committing '$commitPrefix$commitMessage'."
git commit -m "$commitPrefix$commitMessage" -m "$commitTrailers"
}

if ($solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_sourcecontrolstrategy -eq 353400000) {
$remoteOrigin = [Uri]::new((git config --get remote.origin.url))
if ($remoteOrigin.Host -eq 'dev.azure.com') {
$org = $remoteOrigin.UserInfo
$project = $remoteOrigin.Segments[2].Replace('/', '')
$repository = $remoteOrigin.Segments[4]
}
else {
$org = $remoteOrigin.Host.Split('.')[0]
$project = $remoteOrigin.Segments[1].Replace('/', '')
$repository = $remoteOrigin.Segments[3]
}

$sourceBranch = if ($solutionMerge.devhub_sourcebranch) { $solutionMerge.devhub_sourcebranch } else { $calculatedBranch }
Write-Host "Checking for existing pull request"
$result = Invoke-RestMethod `
-Uri "https://dev.azure.com/$org/$project/_apis/git/repositories/$repository/pullRequests?searchCriteria.sourceRefName=refs/heads/$sourceBranch&searchCriteria.targetRefName=refs/heads/$($solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_targetbranch)&api-version=6.0" `
-Headers @{ 'authorization' = "Bearer $env:SYSTEM_ACCESSTOKEN"; 'content-type' = 'application/json' }

if ($result.value.Count -eq 0) {
Write-Host "Publishing pull request branch."
git push -u origin HEAD

Write-Host "Creating pull request from refs/heads/$sourceBranch into refs/heads/$($solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_targetbranch)."
$result = Invoke-RestMethod `
-Uri "https://dev.azure.com/$org/$project/_apis/git/repositories/$repository/pullRequests?api-version=6.0" `
-Method POST `
-Headers @{ 'authorization' = "Bearer $env:SYSTEM_ACCESSTOKEN"; 'content-type' = 'application/json' } `
-UseBasicParsing `
-Body (ConvertTo-Json `
@{
title = "$commitPrefix$commitMessage";
sourceRefName = "refs/heads/$sourceBranch";
targetRefName = "refs/heads/$($solutionMerge.devhub_TargetSolution.devhub_Repository.devhub_targetbranch)";
description = @"
$commitTrailers
"@
})

if ($solutionMerge.devhub_Issue.devhub_azuredevopsworkitemid) {
Write-Host "Linking pull request to work item $($solutionMerge.devhub_Issue.devhub_azuredevopsworkitemid)"
$result = Invoke-RestMethod `
-Uri "https://dev.azure.com/$org/$project/_apis/wit/workItems/$($solutionMerge.devhub_Issue.devhub_azuredevopsworkitemid)?api-version=4.0-preview" `
-Headers @{ 'authorization' = "Bearer $env:SYSTEM_ACCESSTOKEN"; 'content-type' = 'application/json-patch+json' } `
-Method PATCH `
-Body (ConvertTo-Json -Depth 100 @(
@{
op = 'add';
path = '/relations/-';
value =
@{
rel = "ArtifactLink";
url = $($result.artifactId)
attributes = @{
name = "Pull Request"
}
}
}
)
)
}
}

Write-Host "Updating solution merge status to 'Awaiting PR Approval'."
$solutionMerge = Invoke-RestMethod `
-Method PATCH `
-Uri "$devWebApiUrl/devhub_solutionmerges($SolutionMergeId)" `
-Headers $devWebApiHeaders `
-Body (ConvertTo-Json @{
statuscode = 353400007
})
}

Write-Host "Pushing new commit."
git push origin
82 changes: 0 additions & 82 deletions samples/scripts/Merge-SolutionVersion.ps1

This file was deleted.

3 changes: 0 additions & 3 deletions samples/solution.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<AppModuleSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SiteMapUniqueName>devhub_DevelopmentHub</SiteMapUniqueName>
<SiteMap IntroducedVersion="7.0.0.0">
<Area Id="devhub_DevelopmentHub" ResourceId="SitemapDesigner.NewArea" DescriptionResourceId="SitemapDesigner.NewArea" VectorIcon="/WebResources/devhub_/Images/devhub_Issue.svg" Icon="/WebResources/devhub_/Images/devhub_Issue.svg" ShowGroups="true" IntroducedVersion="7.0.0.0">
<Titles>
<Title LCID="1033" Title="Development Hub" />
</Titles>
<Group Id="devhub_Issues" ResourceId="SitemapDesigner.NewGroup" DescriptionResourceId="SitemapDesigner.NewGroup" IntroducedVersion="7.0.0.0" IsProfile="false" ToolTipResourseId="SitemapDesigner.Unknown">
<Titles>
<Title LCID="1033" Title="Issues" />
</Titles>
<SubArea Id="devhub_Dashboards" ResourceId="Homepage_Dashboards" DescriptionResourceId="Dashboards_Description" Icon="/_imgs/imagestrips/transparent_spacer.gif" Url="/workplace/home_dashboards.aspx" DefaultDashboard="899883f9-9876-e911-a819-002248008902" IntroducedVersion="7.0.0.0" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA">
<Titles>
<Title LCID="1033" Title="Dashboard" />
</Titles>
</SubArea>
<SubArea Id="devhub_Issues" Icon="/_imgs/imagestrips/transparent_spacer.gif" Entity="devhub_issue" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA">
<Titles>
<Title LCID="1033" Title="Issues" />
</Titles>
</SubArea>
</Group>
<Group Id="NewGroup_4aa6eeb6" ResourceId="SitemapDesigner.NewGroup" IsProfile="false" ToolTipResourseId="SitemapDesigner.Unknown">
<Titles>
<Title LCID="1033" Title="Develop" />
</Titles>
<SubArea Id="Environments" Icon="/_imgs/imagestrips/transparent_spacer.gif" Entity="devhub_environment" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA" />
<SubArea Id="Solutions" Icon="/_imgs/imagestrips/transparent_spacer.gif" Entity="devhub_solution" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA" />
<SubArea Id="SolutionMerges" Icon="/_imgs/imagestrips/transparent_spacer.gif" Entity="devhub_solutionmerge" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA" />
</Group>
<Group Id="devhub_AzureDevOps" ResourceId="SitemapDesigner.NewGroup" IsProfile="false" ToolTipResourseId="SitemapDesigner.Unknown">
<Titles>
<Title LCID="1033" Title="Azure DevOps" />
</Titles>
<SubArea Id="devhub_Projects" Icon="/_imgs/imagestrips/transparent_spacer.gif" Entity="devhub_project" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA" />
<SubArea Id="devhub_Repositories" Icon="/_imgs/imagestrips/transparent_spacer.gif" Entity="devhub_repository" Client="All,Outlook,OutlookLaptopClient,OutlookWorkstationClient,Web" AvailableOffline="true" PassParams="false" Sku="All,OnPremise,Live,SPLA" />
</Group>
</Area>
</SiteMap>
<LocalizedNames>
<LocalizedName description="Development Hub" languagecode="1033" />
</LocalizedNames>
</AppModuleSiteMap>
Loading

0 comments on commit 738632e

Please sign in to comment.