src/crm-ci.ps1
function Get-OptionalStoredCredentials { ############################## #.SYNOPSIS # Utility function that either returns the credentials passed to it, or gets stored credentials created by Set-StoredCredentials # #.PARAMETER Credential # A PSCredential object. If blank, it creates one from stored credentials # ############################## param( # Existing Credential Object [pscredential]$Credential ) # Read in stored credentials if none are provided if (-not $Credential) { $Credential = Import-Clixml -Path $("$env:HOME\$env:USERNAME" + "_$env:COMPUTERNAME.xml") } return $Credential } function Set-StoredCredentials { ############################## #.SYNOPSIS #Stores credentials in your HOME folder for later use. # #.DESCRIPTION # Creates an XML object serializing your credentials, and stores it in your HOME folder. Uses Windows Data Protection to encrypt your credentials to both your username and the operating computer. # #.EXAMPLE #An example # #.NOTES #General notes ############################## Get-Credential -UserName $env:USERNAME -Message "Enter Credentials to be stored in your home folder" ` | Export-Clixml -Path $("$env:HOME\$env:USERNAME" + "_$env:COMPUTERNAME.xml") } function Get-Config { <# .SYNOPSIS Loads up the config for use in other functions. For module-interal use only. #> $parent = Split-Path $PsScriptRoot -Parent [xml]$Config = Get-Content "$parent\config.xml" return $Config.Settings } function Get-CrmConnectionString { <# .SYNOPSIS Convenience function for building the CRM Connection string for Regent's systems. .DESCRIPTION A simple switch statement to build the CRM Connection string. The values are hard-coded, so if any of the details change, they'll need to be updated here. .PARAMETER CrmInstance #> param( #The unique CRM Instance name. [ValidateSet("CRMRECRUIT", "CRMRECRUITTEST", "CRMADVISE", "CRMADVISETEST")] [Parameter(Mandatory = $true)] $CrmInstance, # Add this switch to just return the organization url of the CRM, without the AuthType appended (used in conjunction with REST API functions) [switch]$UrlOnly ) switch ($CrmInstance) { "CRMRECRUIT" {$url = "https://recruitercrm2.regent.edu/CRMRECRUIT"} "CRMRECRUITTEST" {$url = "https://rctrdevcrm.regent.edu/CRMRECRUITTEST"} "CRMADVISE" {$url = "https://advisecrm.regent.edu/CRMADVISE"} "CRMADVISETEST" {$url = "https://advisedevcrm.regent.edu/CRMADVISETEST"} default { throw "Invalid CRM Instance specified"} } if ($UrlOnly) { return $url } else { $conn = "RequireNewInstance=True;AuthType=AD;Url=$url;" Write-Verbose "Crm Connection String: $conn" return $conn } } function Get-RegentConnection { ############################## #.SYNOPSIS # Returns a CRM Connection Object for use with Microsoft.Xrm.Data.Powershell # #.DESCRIPTION # Just uses Get-CrmConnectionString and passes it to Get-CrmConnection # #.EXAMPLE # To get a connection to CRMADVISE # Get-RegentConnection -CrmInstance CRMADVISE # #.NOTES #General notes ############################## param ( #The unique CRM Instance name. [ValidateSet("CRMRECRUIT", "CRMRECRUITTEST", "CRMADVISE", "CRMADVISETEST")] [Parameter(Mandatory = $true)] $CrmInstance # [Parameter(Mandatory = $true)] # $PathToAdvFindList, # [Parameter(Mandatory = $true)] # $PathToExportAdvFind ) Write-Verbose "Getting Regent Connection for $CrmInstance" $conn = Get-CrmConnection -ConnectionString (Get-CrmConnectionString -CrmInstance $CrmInstance) -MaxCrmConnectionTimeOutMinutes 30 -ErrorAction Stop Write-Verbose "Connected to CRM Sucesfully" return $conn } function Export-RegentSolution { <# .SYNOPSIS Exports a solution from the specified CRM and unpacks it into source control. .DESCRIPTION This command is used to export a solution from a specified CRM into the local solutions repository and unpack the solution into its constituent parts. Both managed and unamanged versions are exported so as to allow either to recreated at a later time. Additionally, if a new version is specified, the version of the solution is updated in the specified CRM BEFORE exporting. .EXAMPLE Export the solution "MyImportantSolution" from CRMRECRUITTEST Export-RegentSolution -SolutionName MyImportantSolution -CrmInstance CRMRECRUITTEST .EXAMPLE Export the solution "MyOtherImportantSolution" from CRMADVISETEST and update the version to 1.5.5 Export-RegentSolution -SolutionName MyOtherImportantSolution -CrmInstance CRMADVISETEST -UpdateVersion "1.5.5" #> param( # The unique CRM solution name [Parameter(Mandatory = $true)] [string]$SolutionName, # The connection object (usually generated by Get-RegentConnection) [Parameter(Mandatory = $true)] $conn, # The new version of the solution. Setting this will update it in the development crm instance [string]$UpdateVersion ) Write-Verbose "Entering Export-RegentSolution" $ErrorActionPreference = "Stop" # Find path to solution packager $solutionPackager = (Split-Path $PsScriptRoot -Parent) + "\lib\SolutionPackager.exe" # Determine CRM Connection String Write-Verbose "Getting Config:" $Config = Get-Config $RepositoryPath = $Config.RepositoryPath # The path to Git repository with the unpacked solutions Write-Verbose "RepositoryPath: $RepositoryPath" #Update Version if applicable if ($UpdateVersion) { Write-Host "Updating Solution Version to $UpdateVersion" Set-CrmSolutionVersionNumber ` -SolutionName $SolutionName ` -Version $UpdateVersion ` -conn $conn Write-Host "Solution Version Updated" } Write-Host "Downloading Solution: $SolutionName" #Export Solutions $unmanagedSolution = Export-CrmSolution ` -conn $conn ` -SolutionName $SolutionName ` -SolutionZipFileName "$SolutionName.zip" ` -SolutionFilePath $RepositoryPath Write-Host "Exported unmanaged solution to $($unmanagedSolution.SolutionPath)" $managedSolution = Export-CrmSolution ` -conn $conn ` -SolutionName $SolutionName ` -SolutionZipFileName "$($SolutionName)_managed.zip"` -SolutionFilePath $RepositoryPath ` -Managed Write-Host "Exported managed solution to $($managedSolution.SolutionPath)" #Unpack the solution Write-Host "Unpacking Solution" if (Test-Path -Path "$RepositoryPath/$solutionName") { Remove-Item -Path "$RepositoryPath/$solutionName" -Recurse -Force } $extractOuput = & "$solutionPackager" /action:Extract /zipfile:"$($unmanagedSolution.SolutionPath)" /folder:"$(Split-Path -Parent $unmanagedSolution.SolutionPath)/$SolutionName" /packagetype:Both /errorlevel:Info /allowWrite:Yes /allowDelete:No | Out-Null # Delete the unnecessary .zip files Remove-Item -Path "$RepositoryPath/*" -Filter "$SolutionName*.zip" Write-Host $extractOuput if ($lastexitcode -ne 0) { throw "Solution Extract operation failed with exit code: $lastexitcode" } else { # At one point I had it check for warnings... I can't remember why. # if (($extractOuput -ne $null) -and ($extractOuput -like "*warnings encountered*")) { # throw "Solution Packager encountered warnings. Check the output." # } # else { Write-Host "Solution Pack Completed Successfully." # } } return $($unmanagedSolution, $managedSolution) } function Get-SolutionImportLog { param( # Determines which CRM instance the solution will be imported to. Note that these are hard-coded - any changes will require changes to this script module's source code or it will break! [ValidateSet("CRMRECRUIT", "CRMRECRUITTEST", "CRMADVISE", "CRMADVISETEST")] [Parameter(Mandatory = $true)] $CrmInstance, # The GUID of the ImportJob. Should be returned by Import-CrmSolution [Parameter(Mandatory = $true)] [string]$ImportJobId, # An optional parameter specifying a path to save the xml log file. $ExportPath ) Write-Verbose "Getting Stored credentials" $Credential = Get-OptionalStoredCredentials $orgURI = Get-CrmConnectionString -CrmInstance $CrmInstance -UrlOnly $api = $orgURI + "/api/data/v8.0" $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add("Accept", 'application/json') $headers.Add("Content-Type", 'application/json; charset=utf-8') $headers.Add("OData-MaxVersion", '4.0') $headers.Add("OData-Version", '4.0') Write-Host "Fetching Log file" $resp = Invoke-RestMethod ` -Method Get ` -Credential $Credential ` -Uri "$api/RetrieveFormattedImportJobResults(ImportJobId=$ImportJobId)" -Headers $headers if ($ExportPath) { $pathParent = Split-Path -Path $ExportPath -Parent if (-not (Test-Path $pathParent)) { New-Item $pathParent -ItemType Directory } $resp.FormattedResults > $ExportPath } return $resp.FormattedResults } function Push-RegentSolutionChanges { <# .SYNOPSIS Records all changes made to a CRM solution to both the local and central repository. .DESCRIPTION This is a wrapper function that stages all changes in the local repository, commits them with the specified message, and then pushes the changes to the remote repository. Depends on the changes already being unpacked by the solution packager. .EXAMPLE Commit the changes to the default branch specified in your config. Push-SolutionChanges -CommitMessage "Added regent_SurveyQuestion1 and regent_SurveyQuestion2 fields to the Person record" .EXAMPLE Commit the changes to a different branch than the one in your config Push-SolutionChanges -CommitMessage "...." -Branch my-crazyivanbranch #> param( # The message associated with these changes. Be specific! Rather than saying "Added fields to Person record", be sure to include which fields, and maybe a few words about the purpose. [Parameter(Mandatory = $true)] [string]$CommitMessage, # The branch to commit to. If not provided, defaults to the branch in your config (which is desirable 99% of the time). [string]$Branch ) # Git can sometimes yield non-0 exit codes when there are no actual errors, so have PowerShell try to skip over them $ErrorActionPreference = "Continue" $Config = Get-Config $RepositoryPath = $Config.RepositoryPath # The path to Git repository with the unpacked solutions if (-not $Branch) { $Branch = $Config.DefaultBranch } Write-Verbose 'Entering ExtractCustomizations.ps1' Write-Verbose "Repository Path = $RepositoryPath" Write-Verbose "Commit Message = $CommitMessage" # Switch to the Repo directory Push-Location $RepositoryPath # Pull any new changes git pull origin # Stage changes git add -A # Commit the changes $commitOutput = & git commit -m "$env:USERNAME - $CommitMessage" Write-Host "Commit Output" Write-Host -ForegroundColor Yellow $commitOutput if (($lastexitcode -ne 0) -and ("$commitOutput" -notlike "*nothing to commit*")) { throw "Git commit failed with exit code: $lastexitcode" } else { Write-Host -ForegroundColor Green "Git commit successful" } #Push $pushOutput = & git push origin $branch --porcelain # Porcelain flag added to redirect stderr to stdout to avoid error warnings Write-Host "Push Output" Write-Host -ForegroundColor Yellow $pushOutput if (($lastexitcode -ne 0) -and ($pushOutput -notlike "*Everything up-to-date*")) { throw "Git push failed with exit code: $lastexitcode" } else { Write-Host -ForegroundColor Green "Git push successful" } Pop-Location } function Import-RegentSolution { <# .SYNOPSIS Move a solution from source control to a specified CRM system. .DESCRIPTION This command is used to move a solution from source control to a particular CRM. This is accomplished by first packaging the raw componenets of the solution up into a .zip file using the Solution Packager, then importing that solution into the specified system. Whether the import succeeds or fails, a log is generated in the target CRM. That log is downloaded into the ./log directory of the solution repository, and, if an email (or list of emails) is provided, the log is also emailed the given address(es). .EXAMPLE Here's an example usage to import the solution "MyImportantSolution" as an unmanaged solution into CRMRECRUITTEST, and then email the results to several recipients Import-RegentSolution -SolutionName MyImportantSolution -CrmInstance CRMRECRUITTEST -Managed $false -Emails "dhines@regent.edu;bryatho@regent.edu;estevens@regent.edu" #> Param( # The unique name of the solution to import. This must correspond to a folder already in the solution repository. [Parameter(Mandatory = $true)] [string]$SolutionName, # Determines which CRM instance the solution will be imported to. Note that these are hard-coded - any changes will require changes to this script module's source code or it will break! [ValidateSet("CRMRECRUIT", "CRMRECRUITTEST", "CRMADVISE", "CRMADVISETEST")] [Parameter(Mandatory = $true)] $CrmInstance, # Provide $true or $false. If set to $true, the solution will be imported as a managed solution. [Parameter(Mandatory = $true)] [bool]$Managed, # Provide $true or $false (defaults to $true). If set to $true and the import succeeds, all customizations will be published once the import is finished. [bool]$PublishCustomizations = $true, # A list of of emails to receive the notification, seperated by semicolons. E.g "user1@regent.edu;user2@regent.edu" [Parameter(Mandatory = $true)] [string]$Emails ) $ErrorActionPreference = "Stop" Start-Transcript -Path "$env:HOME\Solution-Imports\$SolutionName-$((Get-date).ToFileTime()).log" $Config = Get-Config $conn = Get-RegentConnection -CrmInstance $CrmInstance $RepositoryPath = $Config.RepositoryPath # The path to Git repository with the unpacked solutions if ($Managed) { $type = "Managed" $solutionPath = "$RepositoryPath\$($solutionName)_managed.zip" } else { $type = "Unmanaged" $solutionPath = "$RepositoryPath\$solutionName.zip" } # Specify path to solution packager $solutionPackager = (Split-Path $PsScriptRoot -Parent) + "\lib\SolutionPackager.exe" # $sleepCount = 0 # while (-not (Test-Path "$RepositoryPath\$solutionName\Other\Customizations.xml")) { # $sleepCount++ # Write-Verbose "Waiting for files to extract... $sleepCount" # if ($sleepCount -eq 10) { # throw "Files haven't extracted for 10 seconds. Aborting..." # } # start-sleep 1 # } #Pack the solution Write-Output "Packing Solution" $packOuput = & "$solutionPackager" /action:Pack /zipfile:"$solutionPath" /folder:"$RepositoryPath\$solutionName" /packagetype:"$type" /errorlevel:Info /allowDelete:No | Write-Host if ($LASTEXITCODE -ne 0) { throw "Solution Packager extied with error code: $LASTEXITCODE" } Write-Host "Beginning Import of solution $solutionPath" try { $tempLog = "$env:APPDATA\temp-import-log.txt" if ($PublishCustomizations) { Import-CrmSolution ` -conn $conn ` -SolutionFilePath $solutionPath ` -PublishChanges ` -ErrorAction Stop ` -ErrorVariable 'importError' *> $tempLog } else { Import-CrmSolution ` -conn $conn ` -SolutionFilePath $solutionPath ` -ErrorAction Stop ` -ErrorVariable 'importError' *> $tempLog } $ImportID = ( Select-String ` -Path $tempLog ` -Pattern "([A-Z0-9]){8}-([A-Z0-9]){4}-([A-Z0-9]){4}-([A-Z0-9]){4}-([A-Z0-9]){12}" ).Matches[0].Value $title = "Solution $SolutionName Import Succeeded" $message = "Solution import succeeded! To see more details, open the attached log file in Excel." Write-Host -ForegroundColor Green "Import Successful" } catch { $title = "Solution $SolutionName Import Failed" $message = @" Solution import failed with the following message: $($importError[0]) $(if ($ImportID) {"To see more details, open the attached log file in Excel."}) "@ throw $importError[0] } finally { $currTime = Get-Date -Format "mm-dd-yyyy HH-mm" $logPath = "$RepositoryPath\logs\$SolutionName $currTime.xml" if ($ImportID) { Get-SolutionImportLog -CrmInstance $CrmInstance ` -ImportJobId $ImportID ` -ExportPath $logPath ` # -Credential $Credential } if ($Emails) { # Send the results via email Write-Host "Sending results via email to the following recipients" $EmailArr = $Emails.Split(";", [System.StringSplitOptions]'RemoveEmptyEntries') # Write-Host $EmailArr if ($ImportID) { Send-MailMessage ` -From $($env:USERNAME + "@regent.edu") ` -Subject $title ` -To $EmailArr ` -Attachments $logPath ` -Body $message ` -SmtpServer smtp.regent.edu } else { Send-MailMessage ` -From dhines@regent.edu ` -Subject $title ` -To $EmailArr ` -Body $message ` -SmtpServer smtp.regent.edu } Write-Host "Message Sent" } # Delete the unnecessary .zip files Remove-Item -Path "$RepositoryPath/*" -Filter "$SolutionName*.zip" Stop-Transcript } } function Move-RegentSolution { ############## Moves a solution from one crm to another, checking the solution into source control in between. If given a time, schedules the import for that time. Please reivew details,especially parameter details. #`$true #.EXAMPLE #An example of immediate export and reimport from CRMRECRUITTEST to CRMRECRUIT (backtick line-break escapes are used for reading clarity) # Move-RegentSolution # -SolutionName MySolution ` # -SourceCrmInstance CRMRECRUITTEST ` # -DestinationCrmInstance CRMRECRUIT ` # -Managed $false ` # -PublishCustomizations $true ` # -Emails "dhines@regent.edu;bryatho@regent.edu" ` # -CommitMessage "Added fields regent_importantfield1 and regent_importantfield2 to capture Sling import data. Added new fields to dashboard displays" # #.EXAMPLE #An example of a scheduled export and reimport from CRMADVISETEST to CRMADVISE. Note the import will begin on the current day at 11:59PM (or slightly thereafter). The value provided for -ImportTime can be any value parsable by the Get-Date function. # Move-RegentSolution ` # -SolutionName "MyOtherSolution"` # -SourceCrmInstance CRMADVISETEST ` # -DestinationCrmInstance CRMADVISE ` # -Managed $true ` # -PublishCustomizations $false ` # -Emails "dhines@regent.edu;acofer@regent.edu" ` # -CommitMessage "Added web resource for display on the application folder that calculates GPA based on rubric logic in regent_gparubric entity records" ` # -Branch "mybranch" ` # -ImportTime "11:59PM"` # -Force # #.NOTES #General notes ############################## param ( # The unique name of the solution to import. This must correspond to a folder already in the solution repository. [Parameter(Mandatory = $true)] [string]$SolutionName, # Determines which CRM instance the solution will be imported to. Note that these are hard-coded - any changes will require changes to this script module's source code or it will break! [ValidateSet("CRMRECRUIT", "CRMRECRUITTEST", "CRMADVISE", "CRMADVISETEST")] [Parameter(Mandatory = $true)] $SourceCrmInstance, # Determines which CRM instance the solution will be imported to. Note that these are hard-coded - any changes will require changes to this script module's source code or it will break! [ValidateSet("CRMRECRUIT", "CRMRECRUITTEST", "CRMADVISE", "CRMADVISETEST")] [Parameter(Mandatory = $true)] $DestinationCrmInstance, # Provide $true or $false. If set to $true, the solution will be imported as a managed solution. [bool]$Managed = $false, # Provide $true or $false (defaults to $true). If set to $true and the import succeeds, all customizations will be published once the import is finished. [bool]$PublishCustomizations = $true, # A list of of emails to receive the notification, seperated by semicolons. E.g "user1@regent.edu;user2@regent.edu" [Parameter(Mandatory = $true)] [string]$Emails, # The new version of the solution. Setting this will update it in the development crm instance [string]$UpdateVersion, # The time to begin the import job. If not provided, the import begins immediately. The value provided can be a DateTime object or any value that can be parsed by Get-Date. [datetime]$ImportTime, # The message associated with these changes. Be specific! Rather than saying "Added fields to Person record", be sure to include which fields, and maybe a few words about the purpose. [Parameter(Mandatory = $true)] [string]$CommitMessage, # The branch to commit to. If not provided, defaults to the branch in your config (which is desirable 99% of the time). [string]$Branch, # Set this flag to skip the confirmation prompt and proceed directly with the operation [switch]$Force ) $ErrorActionPreference = "Stop" Write-Host "Searching CRM $SourceCrmInstance for solution $SolutionName..." $sourceConn = Get-RegentConnection -CrmInstance $SourceCrmInstance $Solution = Get-CrmRecords -conn $sourceConn -EntityLogicalName solution -FilterAttribute uniquename ` -FilterOperator eq -FilterValue $SolutionName -Fields uniquename, description, createdon, ismanaged, publisherid $solDetails = $Solution.CrmRecords | Select-Object uniquename, description, createdon, ismanaged, publisherid, solutionid if (-not $solDetails) { throw "No Solution ""$SolutionName"" found in $SourceCrmInstance. Check spelling and that solution exists." } Write-Host -ForegroundColor Yellow "Solution found. Please review the details below to confirm they're correct." Write-Host "`n`n$($solDetails | ConvertTo-Json)`n`n" Write-Host -ForegroundColor Yellow "Continuing the operation will schedule the following job:" Write-Host -ForegroundColor Cyan @" Solution $SolutionName will be transferred from $SourceCrmInstance to $DestinationCrmInstance; The solution will be in imported in $(if($Managed) {"MANAGED"} else {"UNMANAGED"}) mode; Cusomizations WILL $(if(-not $PublishCustomizations) {"NOT "})BE PUBLISHED; "@ if ($UpdateVersion) { Write-host -ForegroundColor Cyan " The soluion version will be upgraded to $UpdateVersion;" } Write-host -ForegroundColor Cyan @" The solution import will begin $(if($ImportTime) {"at $($ImportTime.ToShortDateString()) $($ImportTime.ToLocalTime().ToLongTimeString())"} else {"immediately"}); The following emails will be notified of the results: $Emails Changes will be committed to branch "$(if($Branch) {$Branch} else {"master"})" with the following message: "$CommitMessage" "@ if (-not $Force.IsPresent) { Write-host "Please review all the information printed above. Is this information correct?" -ForegroundColor Yellow $Readhost = Read-Host "Select Y or N (default N) " Switch ($ReadHost) { Y { $continue = $true} N { $continue = $false} Default { $continue = $false } } if (-not $continue) { Write-Host "Canceling operation." return } } # Export the solution ------------------------------------------------- Export-RegentSolution ` -SolutionName $SolutionName ` -conn $sourceConn ` -UpdateVersion $UpdateVersion # Pack the solution --------------------------------------------------- Push-RegentSolutionChanges -CommitMessage $CommitMessage -Branch $Branch # If not scheduled, import immediately if (-not $ImportTime) { Import-RegentSolution ` -SolutionName $SolutionName ` -CrmInstance $DestinationCrmInstance ` -Managed $Managed ` -PublishCustomizations $PublishCustomizations ` -Emails $Emails ` Write-Host -ForegroundColor Green "Solution imported successfully!" } # Else, schedule the import for later by adding it to a json file else { Function Get-BoolFromString() { param($value) if ($value) { return "`$true" } else { return "`$false" } } # create object with current job parameters $params = New-Object psobject -Property @{ SolutionName = $SolutionName; CrmInstance = $DestinationCrmInstance; Managed = $(Get-BoolFromString -Value $Managed); PublishCustomizations = $(Get-BoolFromString -Value $PublishCustomizations); # Emails = $Emails; } $script = [scriptblock]::Create("Import-RegentSolution -Verbose $(&{$args} @params) -Emails ""$($Emails)""") $cred = Import-Clixml -Path $("$env:HOME\$env:USERNAME" + "_$env:COMPUTERNAME.xml") $trigger = New-JobTrigger -Once -At $ImportTime $id = Register-ScheduledJob -Name "$($params.SolutionName)-$((Get-Date).ToFileTime())" -Trigger $trigger -ScriptBlock $script -Credential $cred Write-Host -ForegroundColor Green "Solution import scheduled!" return $id } } |