├── .gitignore ├── PSWebDeploy.Tests.ps1 ├── PSWebDeploy.psd1 ├── PSWebDeploy.psm1 ├── README.md ├── appveyor.yml └── buildscripts ├── build.ps1 ├── install.ps1 ├── publish.ps1 └── test.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /PSWebDeploy.Tests.ps1: -------------------------------------------------------------------------------- 1 | ,#region import modules 2 | $ThisModule = "$($MyInvocation.MyCommand.Path -replace '\.Tests\.ps1$', '').psd1" 3 | $ThisModuleName = (($ThisModule | Split-Path -Leaf) -replace '\.psd1') 4 | Get-Module -Name $ThisModuleName -All | Remove-Module -Force 5 | 6 | Import-Module -Name $ThisModule -Force -ErrorAction Stop 7 | #endregion 8 | 9 | describe 'Module-level tests' { 10 | 11 | it 'should validate the module manifest' { 12 | 13 | { Test-ModuleManifest -Path $ThisModule -ErrorAction Stop } | should not throw 14 | } 15 | 16 | it 'should pass all error-level script analyzer rules' { 17 | 18 | $excludedRules = @( 19 | 'PSUseShouldProcessForStateChangingFunctions', 20 | 'PSUseToExportFieldsInManifest', 21 | 'PSAvoidInvokingEmptyMembers', 22 | 'PSUsePSCredentialType', 23 | 'PSAvoidUsingPlainTextForPassword' 24 | ) 25 | 26 | Invoke-ScriptAnalyzer -Path $PSScriptRoot -ExcludeRule $excludedRules -Severity Error | Select-Object -ExpandProperty RuleName | should benullorempty 27 | } 28 | } 29 | 30 | InModuleScope $ThisModuleName { 31 | 32 | describe 'NewMsDeployCliArgumentString - Sync' { 33 | 34 | $commandName = 'NewMsDeployCliArgumentString' 35 | $command = Get-Command -Name $commandName 36 | 37 | $mockCred = New-MockObject -Type 'System.Management.Automation.PSCredential' 38 | $mockCred = $mockCred | Add-Member -MemberType NoteProperty -Name UserName -Value 'username' -PassThru -Force 39 | $mockCred = $mockCred | Add-Member -MemberType ScriptMethod -Name GetNetworkCredential -Value {[pscustomobject]@{Password = 'passwordname'}} -PassThru -Force 40 | 41 | $parameterSets = @( 42 | @{ 43 | Verb = 'Sync' 44 | SourcePath = 'C:\Source' 45 | TargetPath = 'TargetHere' 46 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 47 | Credential = $mockCred 48 | TestName = 'Azure web app / Sync / FolderPath' 49 | } 50 | @{ 51 | Verb = 'Sync' 52 | SourceContent = 'C:\Source' 53 | TargetPath = 'TargetHere' 54 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 55 | Credential = $mockCred 56 | TestName = 'Azure web app / Sync / SourceContent / FolderPath' 57 | } 58 | @{ 59 | Verb = 'Sync' 60 | SourceContent = 'C:\Source.zip' 61 | TargetPath = 'TargetHere' 62 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 63 | Credential = $mockCred 64 | TestName = 'Azure web app / Sync / SourceContent / Package' 65 | } 66 | @{ 67 | Verb = 'Sync' 68 | SourcePackage = 'C:\Source.zip' 69 | TargetPath = 'TargetHere' 70 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 71 | Credential = $mockCred 72 | TestName = 'Azure web app / Sync / Package' 73 | } 74 | @{ 75 | Verb = 'Sync' 76 | SourcePath = 'C:\Source' 77 | TargetPath = 'TargetHere' 78 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 79 | Credential = $mockCred 80 | EnableRule = 'DoNotDelete' 81 | TestName = 'adds the required rule' 82 | } 83 | @{ 84 | Verb = 'Sync' 85 | SourcePackage = 'C:\Source.zip' 86 | TargetPath = 'TargetHere' 87 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 88 | Credential = $mockCred 89 | EnableRule = 'DoNotDelete' 90 | TestName = 'adds the required rule' 91 | } 92 | ) 93 | 94 | $testCases = @{ 95 | All = $parameterSets 96 | EnableRule = $parameterSets.where({ $_.ContainsKey('EnableRule') }) 97 | TargetPath = @{ 98 | All = $parameterSets.where({ $_.ContainsKey('TargetPath') }) 99 | EnableRule = $parameterSets.where({ $_.ContainsKey('EnableRule') -and $_.ContainsKey('TargetPath') }) 100 | SourcePackage = $parameterSets.where({ $_.ContainsKey('SourcePackage') -and $_.ContainsKey('TargetPath') }) 101 | SourcePath = $parameterSets.where({ $_.ContainsKey('SourcePath') -and $_.ContainsKey('TargetPath') }) 102 | } 103 | TargetContent = @{ 104 | All = $parameterSets.where({ $_.ContainsKey('TargetContent') }) 105 | EnableRule = $parameterSets.where({ $_.ContainsKey('EnableRule') -and $_.ContainsKey('TargetContent') }) 106 | SourcePackage = $parameterSets.where({ $_.ContainsKey('SourcePackage') -and $_.ContainsKey('TargetContent') }) 107 | SourcePath = $parameterSets.where({ $_.ContainsKey('SourcePath') -and $_.ContainsKey('TargetContent') }) 108 | } 109 | SourcePackage = @{ 110 | All = $parameterSets.where({ $_.ContainsKey('SourcePackage') }) 111 | Default = $parameterSets.where({ $_.ContainsKey('SourcePackage') -and (-not $_.ContainsKey('EnableRule')) }) 112 | EnableRule = $parameterSets.where({ $_.ContainsKey('EnableRule') -and $_.ContainsKey('SourcePackage') }) 113 | } 114 | SourcePath = @{ 115 | All = $parameterSets.where({ $_.ContainsKey('SourcePath') }) 116 | Default = $parameterSets.where({ $_.ContainsKey('SourcePath') -and (-not $_.ContainsKey('EnableRule')) }) 117 | EnableRule = $parameterSets.where({ $_.ContainsKey('EnableRule') -and $_.ContainsKey('SourcePath') }) 118 | } 119 | SourceContent = @{ 120 | All = $parameterSets.where({ $_.ContainsKey('SourceContent') }) 121 | EnableRule = $parameterSets.where({ $_.ContainsKey('EnableRule') -and $_.ContainsKey('SourceContent') }) 122 | Package = $parameterSets.where({ $_.ContainsKey('SourceContent') -and $_.SourceContent -match '\.zip$' }) 123 | Path = $parameterSets.where({ $_.ContainsKey('SourceContent') -and $_.SourceContent -notmatch '\.zip$' }) 124 | } 125 | } 126 | 127 | context 'Source Package' { 128 | 129 | mock 'Test-Path' { 130 | $true 131 | } -ParameterFilter { $PSBoundParameters.PathType -eq 'Leaf' } 132 | 133 | it 'when EnableRule is used, it returns the expected string: ' -TestCases $testCases.SourcePackage.EnableRule -Skip { 134 | param($Verb,$SourceContent,$SourcePath,$SourcePackage,$TargetContent,$ComputerName,$TargetPath,$EnableRule,$Credential,$AuthType) 135 | 136 | $expectedString = " -dest:ContentPath=`"$TargetPath`" -source UserName=$($Credential.UserName),ComputerName=$ComputerName,AuthType=Basic,Password=$($Credential.GetNetworkCredential().Password),Package=`"C:\Source.zip`" -EnableRule:DoNotDelete -verb:Sync" 137 | & $commandName @PSBoundParameters | should be $expectedString 138 | } 139 | 140 | it 'when source is passed as SourceContent, it returns the expected string: ' -TestCases $testCases.SourceContent.Package -Skip { 141 | param($Verb,$SourceContent,$SourcePath,$SourcePackage,$TargetContent,$ComputerName,$TargetPath,$EnableRule,$Credential,$AuthType) 142 | 143 | $expectedString = " -dest:UserName=$($Credential.UserName),contentPath=`"$TargetPath`",ComputerName=$ComputerName,AuthType=Basic,Password=$($Credential.GetNetworkCredential().Password) -source:Package=`"$SourceContent`" -verb:Sync" 144 | & $commandName @PSBoundParameters | should be $expectedString 145 | } 146 | 147 | it 'when source is passed as SourcePackage, it returns the expected string: ' -TestCases $testCases.SourcePackage.Default -Skip { 148 | param($Verb,$SourceContent,$SourcePath,$SourcePackage,$TargetContent,$ComputerName,$TargetPath,$EnableRule,$Credential,$AuthType) 149 | 150 | $expectedString = " -dest:UserName=$($Credential.UserName),contentPath=`"$TargetPath`",ComputerName=$ComputerName,AuthType=Basic,Password=$($Credential.GetNetworkCredential().Password) -source:Package=`"$SourcePackage`" -verb:Sync" 151 | & $commandName @PSBoundParameters | should be $expectedString 152 | } 153 | 154 | } 155 | 156 | context 'Source Path' { 157 | 158 | mock 'Test-Path' { 159 | $true 160 | } -ParameterFilter { $PSBoundParameters.PathType -eq 'Container' } 161 | 162 | it 'when EnableRule is used, it returns the expected string: ' -TestCases $testCases.SourcePath.EnableRule -Skip { 163 | param($Verb,$SourceContent,$SourcePath,$SourcePackage,$TargetContent,$ComputerName,$TargetPath,$EnableRule,$Credential,$AuthType) 164 | 165 | $expectedString = " -dest:UserName=$($Credential.UserName),contentPath=`"$TargetPath`",ComputerName=$ComputerName,AuthType=Basic,Password=$($Credential.GetNetworkCredential().Password) -source:ContentPath=`"$SourcePath`" -EnableRule:DoNotDelete -verb:Sync" 166 | & $commandName @PSBoundParameters | should be $expectedString 167 | } 168 | 169 | it 'when source is passed as SourceContent, it returns the expected string: ' -TestCases $testCases.SourceContent.Path -Skip { 170 | param($Verb,$SourceContent,$SourcePath,$SourcePackage,$TargetContent,$ComputerName,$TargetPath,$EnableRule,$Credential,$AuthType) 171 | 172 | $expectedString = " -dest:UserName=$($Credential.UserName),contentPath=`"$TargetPath`",ComputerName=$ComputerName,AuthType=Basic,Password=$($Credential.GetNetworkCredential().Password) -source:ContentPath=`"$SourceContent`" -verb:Sync" 173 | & $commandName @PSBoundParameters | should be $expectedString 174 | } 175 | 176 | it 'when source is passed as SourcePackage, it returns the expected string: ' -TestCases $testCases.SourcePath.Default -Skip { 177 | param($Verb,$SourceContent,$SourcePath,$SourcePackage,$TargetContent,$ComputerName,$TargetPath,$EnableRule,$Credential,$AuthType) 178 | 179 | $expectedString = " -dest:UserName=$($Credential.UserName),contentPath=`"$TargetPath`",ComputerName=$ComputerName,AuthType=Basic,Password=$($Credential.GetNetworkCredential().Password) -source:ContentPath=`"$SourcePath`" -verb:Sync" 180 | & $commandName @PSBoundParameters | should be $expectedString 181 | } 182 | } 183 | 184 | } 185 | 186 | describe 'Invoke-MSDeploy' { 187 | 188 | $commandName = 'Invoke-MSDeploy' 189 | 190 | mock 'Start-Process' 191 | 192 | $parameterSets = @( 193 | @{ 194 | Arguments = 'args here --and here' 195 | TestName = 'Default' 196 | } 197 | ) 198 | 199 | $testCases = @{ 200 | All = $parameterSets 201 | } 202 | 203 | context 'Execution' { 204 | 205 | it 'passes the right arguments to the MSDeploy process: ' -TestCases $testCases.All -Skip { 206 | param($Arguments) 207 | 208 | $null = & $commandName @PSBoundParameters 209 | 210 | $assMParams = @{ 211 | CommandName = 'Start-Process' 212 | Times = 1 213 | Exactly = $true 214 | Scope = 'It' 215 | ParameterFilter = {$PSBoundParameters.ArgumentList -eq $Arguments } 216 | } 217 | Assert-MockCalled @assMParams 218 | } 219 | } 220 | 221 | context 'Output' { 222 | 223 | $command = Get-Command -Name $commandName 224 | 225 | it 'has an outputType defined' { 226 | $command.OutputType | should not be $null 227 | } 228 | 229 | it 'returns nothing: ' -TestCases $testCases.All -Skip { 230 | param($Arguments) 231 | 232 | & $commandName @PSBoundParameters | should benullorempty 233 | 234 | } 235 | } 236 | } 237 | 238 | describe 'Sync-Website' { 239 | 240 | mock 'NewMsDeployCliArgumentString' { 241 | 'string' 242 | } 243 | 244 | mock 'Invoke-MsDeploy' 245 | 246 | $mockCred = New-MockObject -Type 'System.Management.Automation.PSCredential' 247 | $mockCred = $mockCred | Add-Member -MemberType NoteProperty -Name UserName -Value 'username' -PassThru -Force 248 | $mockCred = $mockCred | Add-Member -MemberType ScriptMethod -Name GetNetworkCredential -Value {[pscustomobject]@{Password = 'passwordname'}} -PassThru -Force 249 | 250 | $parameterSets = @( 251 | @{ 252 | SourcePath = 'c:\sourcepath' 253 | TargetPath = '\wwwroot' 254 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 255 | Credential = $mockCred 256 | TestName = 'Azure web app' 257 | } 258 | @{ 259 | SourcePath = 'c:\sourcepath' 260 | TargetPath = '/wwwroot' 261 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 262 | Credential = $mockCred 263 | TestName = 'Forward slashes in TargetPath' 264 | } 265 | @{ 266 | SourcePath = 'c:\sourcepath' 267 | TargetPath = '/wwwroot' 268 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 269 | Credential = $mockCred 270 | DoNotDelete = $true 271 | TestName = 'does not remove TargetPath contents' 272 | } 273 | @{ 274 | SourcePath = 'C:\sourcepath' 275 | TargetPath = '/wwwroot' 276 | ComputerName = 'https://webapphere.scm.azurewebsites.net:443/msdeploy.axd?site=webapphere' 277 | Credential = $mockCred 278 | TestName = 'does not remove TargetPath contents' 279 | } 280 | ) 281 | 282 | $testCases = @{ 283 | All = $parameterSets 284 | SiteSourcePath = $parameterSets.where({$_.SourcePath -notmatch ':'}) 285 | } 286 | 287 | context 'Execution' { 288 | 289 | it 'when TargetPath contains forward slashes, it replaces them with back slashes: ' -TestCases $testCases.All { 290 | param($SourcePath, $TargetPath, $ComputerName, $Credential, $DoNotDelete) 291 | 292 | $null = & Sync-Website @PSBoundParameters 293 | 294 | $assMParams = @{ 295 | CommandName = 'NewMsDeployCliArgumentString' 296 | Times = 1 297 | Exactly = $true 298 | Scope = 'It' 299 | ParameterFilter = { 300 | $PSBoundParameters.TargetContent -eq '\wwwroot' } 301 | } 302 | Assert-MockCalled @assMParams 303 | 304 | } 305 | } 306 | 307 | context 'Output' { 308 | 309 | $command = Get-Command -Name Sync-Website 310 | 311 | it 'has an outputType defined' { 312 | $command.OutputType | should not be $null 313 | } 314 | 315 | it 'returns nothing: ' -TestCases $testCases.All { 316 | param($SourcePath, $TargetPath, $ComputerName, $Credential, $DoNotDelete) 317 | 318 | & Sync-Website @PSBoundParameters | should benullorempty 319 | 320 | } 321 | } 322 | } 323 | } -------------------------------------------------------------------------------- /PSWebDeploy.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'PSWebDeploy.psm1' 3 | ModuleVersion = '1.0' 4 | GUID = '261952d9-cd6f-424f-b0b4-4c409675b365' 5 | Description = 'A PowerShell module to provide an easier interface to msdeploy' 6 | Author = 'Adam Bertram' 7 | FunctionsToExport = '*' 8 | PowerShellVersion = '4.0' 9 | CLRVersion = '4.0' 10 | PrivateData = @{ 11 | PSData = @{ 12 | Tags = @('PSModule','MSDeploy') 13 | ProjectUri = 'https://github.com/adbertram/PSWebDeploy' 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PSWebDeploy.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest; 2 | 3 | $Defaults = @{ 4 | MSDeployExePath = 'C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe' 5 | } 6 | 7 | if (-not (Test-Path -Path $Defaults.MSDeployExePath -PathType Leaf)) { 8 | throw 'MSDeploy was not found. In order to use the MSDeploy module, you must have Web Deploy installed. It can be found at https://www.microsoft.com/en-us/download/details.aspx?id=43717' 9 | } 10 | 11 | function NewMsDeployCliArgumentString 12 | { 13 | [OutputType([string])] 14 | [CmdletBinding()] 15 | param 16 | ( 17 | [Parameter(Mandatory)] 18 | [ValidateNotNullOrEmpty()] 19 | [ValidateSet('Sync')] 20 | [string]$Verb, 21 | 22 | [Parameter(Mandatory)] 23 | [ValidateNotNullOrEmpty()] 24 | [Alias('SourcePath')] 25 | [Alias('SourcePackage')] 26 | [string]$SourceContent, 27 | 28 | [Parameter(Mandatory)] 29 | [ValidateNotNullOrEmpty()] 30 | [string]$ComputerName, 31 | 32 | [Parameter(Mandatory)] 33 | [ValidateNotNullOrEmpty()] 34 | [Alias('TargetPath')] 35 | [string]$TargetContent, 36 | 37 | [Parameter(Mandatory)] 38 | [ValidateNotNullOrEmpty()] 39 | $Credential, 40 | 41 | [Parameter()] 42 | [ValidateNotNullOrEmpty()] 43 | [ValidateSet('DoNotDelete')] 44 | [string[]]$EnableRule, 45 | 46 | [Parameter()] 47 | [ValidateNotNullOrEmpty()] 48 | [int]$RetryAttempts, 49 | 50 | [Parameter()] 51 | [ValidateNotNullOrEmpty()] 52 | [int]$RetryInterval, 53 | 54 | [Parameter()] 55 | [ValidateNotNullOrEmpty()] 56 | [string]$AuthType = 'Basic' 57 | ) 58 | 59 | $connHt = @{ 60 | ComputerName = $Computername 61 | UserName = $Credential.UserName 62 | Password = $Credential.GetNetworkCredential().Password 63 | AuthType = $AuthType 64 | } 65 | 66 | $deployArgs = @{ 67 | verb = $Verb 68 | } 69 | 70 | if ($PSBoundParameters.ContainsKey('RetryInterval')) 71 | { 72 | $deployArgs.retryInterval = $RetryInterval 73 | } 74 | 75 | if ($PSBoundParameters.ContainsKey('RetryAttempts')) 76 | { 77 | $deployArgs.RetryAttempts = $RetryAttempts 78 | } 79 | 80 | if ($PSBoundParameters.ContainsKey('EnableRule')) 81 | { 82 | $deployArgs.EnableRule = $EnableRule -join ',' 83 | } 84 | 85 | ## If this is a ZIP file, it needs to be Package otherwise assuming it's a file path or a web service path 86 | if (Test-Path -Path $SourceContent -PathType Leaf) { 87 | $sourceProvider = 'Package' 88 | } else { 89 | $sourceProvider = 'ContentPath' 90 | } 91 | $targetProvider = 'ContentPath' 92 | 93 | if (Test-Path -Path $SourceContent) { 94 | 95 | ## No authentication needed if source is a folder/file path 96 | $deployArgs.source = @{ 97 | $sourceProvider = '"{0}"' -f $SourceContent 98 | } 99 | 100 | ## Assuming that destination is a web service if source is not. Authentication needed. 101 | $deployArgs.dest = ($connHt + @{ 102 | $targetProvider = '"{0}"' -f $TargetContent 103 | }) 104 | 105 | } else { 106 | ## Assuming this is a web service. Authenticate here 107 | $deployArgs.source = $connHt + @{ 108 | $sourceProvider = '"{0}"' -f $SourceContent 109 | } 110 | ## Assuming that destination is a file/folder path. No authentication needed. 111 | $deployArgs.dest = @{ 112 | $targetProvider = '"{0}"' -f $TargetContent 113 | } 114 | } 115 | 116 | $argString = '' 117 | $deployArgs.GetEnumerator().foreach({ 118 | if ($_.Value -is 'hashtable') { 119 | $val = '' 120 | $_.Value.GetEnumerator().foreach({ 121 | $val += "$($_.Key)=$($_.Value)," 122 | }) 123 | $val = $val.TrimEnd(',') 124 | } else { 125 | $val = $_.Value 126 | } 127 | $argString += " -$($_.Key):$val" 128 | }) 129 | $argString 130 | } 131 | 132 | function Invoke-MSDeploy 133 | { 134 | [OutputType([string])] 135 | [CmdletBinding()] 136 | param 137 | ( 138 | [Parameter()] 139 | [ValidateNotNullOrEmpty()] 140 | [string]$Arguments 141 | ) 142 | 143 | $stdOutTempFile = New-TemporaryFile 144 | $stdErrTempFile = New-TemporaryFile 145 | 146 | $startProcessParams = @{ 147 | FilePath = $Defaults.MSDeployExePath 148 | ArgumentList = $Arguments 149 | RedirectStandardError = $stdErrTempFile.FullName 150 | RedirectStandardOutput = $stdOutTempFile.FullName 151 | Wait = $true; 152 | PassThru = $true; 153 | NoNewWindow = $true; 154 | } 155 | 156 | $cmd = Start-Process @startProcessParams 157 | $cmdOutput = Get-Content -Path $stdOutTempFile.FullName -Raw 158 | $cmdError = Get-Content -Path $stdErrTempFile.FullName -Raw 159 | if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) 160 | { 161 | Write-Verbose -Message $cmdOutput 162 | } 163 | if ($cmd.ExitCode -ne 0) 164 | { 165 | throw $cmdError 166 | } 167 | Remove-Item -Path $stdOutTempFile.FullName,$stdErrTempFile.FullName -Force 168 | 169 | } 170 | 171 | #region function Sync-Website 172 | function Sync-Website { 173 | <# 174 | .SYNOPSIS 175 | This function uses msdeploy to copy files from a source location to a destination folder path or URL. 176 | 177 | .EXAMPLE 178 | PS> Sync-Website -SourcePath C:\TestSite -TargetPath wwwroot -ComputerName https://azureurl.com 179 | 180 | .PARAMETER SourcePath 181 | A mandatory string parameter (if not using SourcePackage) representing the location where the files are located. 182 | 183 | .PARAMETER SourcePackage 184 | A mandatory string parameter (if not using SourcePath) representing the location of a zip file that contains the 185 | website files/folders. 186 | 187 | .PARAMETER TargetPath 188 | A mandatory string parameter representing the folder location to copy the files. 189 | 190 | .PARAMETER ComputerName 191 | A mandatory string parameter representing a computer name or a deployment URL. 192 | 193 | .PARAMETER DoNotDelete 194 | By default, any files/folders in the destination path will be removed if not in the SourcePath. Use this 195 | parameter if you'd simply like to copy the contents from SourcePath to TargetPath without removing TargetPath 196 | files/folders. 197 | 198 | .PARAMETER RetryInterval 199 | A optional int parameter representing the interval (in seconds) in which MSDeploy will attempt to retry the action. By default, 200 | this is 10 seconds. This parameter is expressed in milliseconds. 201 | 202 | .PARAMETER Credential 203 | Specifies a user account that has permission to perform this action. The default is the current user. 204 | 205 | Type a user name, such as 'User01' or 'Domain01\User01', or enter a variable that contains a PSCredential 206 | object, such as one generated by the Get-Credential cmdlet. When you type a user name, you will be prompted for a password. 207 | #> 208 | [OutputType([void])] 209 | [CmdletBinding()] 210 | param 211 | ( 212 | [Parameter(Mandatory,ParameterSetName = 'BySourcePath')] 213 | [ValidateNotNullOrEmpty()] 214 | [string]$SourcePath, 215 | 216 | [Parameter(Mandatory,ParameterSetName = 'BySourcePackage')] 217 | [ValidateNotNullOrEmpty()] 218 | [string]$SourcePackage, 219 | 220 | [Parameter(Mandatory)] 221 | [ValidateNotNullOrEmpty()] 222 | [string]$TargetPath, 223 | 224 | [Parameter(Mandatory)] 225 | [ValidateNotNullOrEmpty()] 226 | [string]$ComputerName, 227 | 228 | [Parameter()] 229 | [ValidateNotNullOrEmpty()] 230 | [switch]$DoNotDelete, 231 | 232 | [Parameter()] 233 | [ValidateNotNullOrEmpty()] 234 | [int]$RetryInterval = 10, 235 | 236 | [Parameter()] 237 | [ValidateNotNullOrEmpty()] 238 | [int]$Timeout = 60, 239 | 240 | [Parameter()] 241 | [ValidateNotNullOrEmpty()] 242 | $Credential 243 | ) 244 | begin { 245 | $ErrorActionPreference = 'Stop' 246 | } 247 | process { 248 | try 249 | { 250 | $cliArgStringParams = @{ 251 | Verb = 'sync' 252 | TargetPath = ($TargetPath -replace '/','\') 253 | ComputerName = $ComputerName 254 | Credential = $Credential 255 | RetryInterval = ($RetryInterval * 10) 256 | } 257 | 258 | if ($PSCmdlet.ParameterSetName -eq 'BySourcePath') { 259 | $cliArgStringParams.SourcePath = $SourcePath 260 | } else { 261 | $cliArgStringParams.SourcePackage = $SourcePackage 262 | } 263 | if ($DoNotDelete.IsPresent) { 264 | $cliArgStringParams.EnableRule = 'DoNotDelete' 265 | } 266 | 267 | $argString = NewMsDeployCliArgumentString @cliArgStringParams 268 | Write-Verbose -Message "Using the MSDeploy CLI string: [$($argString)]" 269 | try { 270 | Invoke-MSDeploy -Arguments $argString 271 | } catch { 272 | $timer = [Diagnostics.Stopwatch]::StartNew() 273 | while ($timer.Elapsed.TotalSeconds -lt $Timeout) { 274 | try { 275 | Invoke-MSDeploy -Arguments $argString 276 | } catch { 277 | Write-Verbose -Message "MSdeploy failed. Retrying after [$($RetryInterval)] seconds..." 278 | Start-Sleep -Seconds $RetryInterval 279 | } 280 | } 281 | $timer.Stop() 282 | if ($timer.Elapsed.TotalSeconds -gt $Timeout) { 283 | throw 'Msdeploy timed out attempting to sync website.' 284 | } 285 | } 286 | } 287 | catch 288 | { 289 | $PSCmdlet.ThrowTerminatingError($_) 290 | } 291 | } 292 | } 293 | #endregion function Sync-Website 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSWebDeploy 2 | PSWebDeploy is a PowerShell module that wraps PowerShell around (Web Deploy) msdeploy.exe. This greatly eases figuring out the long syntax required for msdeploy.exe. 3 | 4 | ## Sync-Website 5 | 6 | This function uses the msdeploy `sync` verb to sync local contents to a remote web server. 7 | 8 | ## Get-WebsiteFile 9 | 10 | This function uses the msdeploy `dump` verb to retrieve a list of files from a remote web server. 11 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | environment: 3 | nuget_apikey: 4 | secure: Bvblx9A9AMTXl4LdQe4Aljoy1WBh48/U3UnlCSxDenF8X6tXEDLqzHdt3krnt7R6 5 | image: WMF 5 6 | install: 7 | - ps: .\buildscripts\install.ps1 8 | build_script: 9 | - ps: .\buildscripts\build.ps1 10 | test_script: 11 | - ps: .\buildscripts\test.ps1 12 | after_test: 13 | - ps: .\buildscripts\publish.ps1 14 | -------------------------------------------------------------------------------- /buildscripts/build.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | try { 4 | 5 | $manifestFilePath = "$env:APPVEYOR_BUILD_FOLDER\PSWebDeploy.psd1" 6 | $manifestContent = Get-Content -Path $manifestFilePath -Raw 7 | 8 | ## Update the module version based on the build version and limit exported functions 9 | $replacements = @{ 10 | "ModuleVersion = '.*'" = "ModuleVersion = '$env:APPVEYOR_BUILD_VERSION'" 11 | "FunctionsToExport = '\*'" = "FunctionsToExport = 'Sync-Website','Get-WebsiteFile'" 12 | } 13 | 14 | $replacements.GetEnumerator() | foreach { 15 | $manifestContent = $manifestContent -replace $_.Key,$_.Value 16 | } 17 | 18 | $manifestContent | Set-Content -Path $manifestFilePath 19 | 20 | } catch { 21 | $host.SetShouldExit($LastExitCode) 22 | } -------------------------------------------------------------------------------- /buildscripts/install.ps1: -------------------------------------------------------------------------------- 1 | Write-Host 'Installing necessary PowerShell modules...' 2 | 3 | $provParams = @{ 4 | Name = 'NuGet' 5 | MinimumVersion = '2.8.5.208' 6 | Force = $true 7 | } 8 | 9 | $null = Install-PackageProvider @provParams 10 | $null = Import-PackageProvider @provParams 11 | 12 | $requiredModules = @('Pester','PowerShellGet','PSScriptAnalyzer') 13 | foreach ($m in $requiredModules) { 14 | Install-Module -Name $m -Force -Confirm:$false 15 | Remove-Module -Name $m -Force -ErrorAction Ignore 16 | Import-Module -Name $m 17 | } -------------------------------------------------------------------------------- /buildscripts/publish.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | try { 4 | ## Don't upload the build scripts and appveyor.yml to PowerShell Gallery 5 | $tempmoduleFolderPath = "$env:Temp\PSWebDeploy" 6 | $null = mkdir $tempmoduleFolderPath 7 | 8 | ## Remove all of the files/folders to exclude out of the main folder 9 | $excludeFromPublish = @( 10 | 'PSWebDeploy\\buildscripts' 11 | 'PSWebDeploy\\appveyor\.yml' 12 | 'PSWebDeploy\\\.git' 13 | 'PSWebDeploy\\README\.md' 14 | 'PSWebDeploy\\TestResults\.xml' 15 | ) 16 | $exclude = $excludeFromPublish -join '|' 17 | Get-ChildItem -Path $env:APPVEYOR_BUILD_FOLDER -Recurse | where { $_.FullName -match $exclude } | Remove-Item -Force -Recurse 18 | 19 | ## Publish module to PowerShell Gallery 20 | $publishParams = @{ 21 | Path = $env:APPVEYOR_BUILD_FOLDER 22 | NuGetApiKey = $env:nuget_apikey 23 | Repository = 'PSGallery' 24 | Force = $true 25 | Confirm = $false 26 | } 27 | Publish-Module @publishParams 28 | 29 | } catch { 30 | Write-Error -Message $_.Exception.Message 31 | $host.SetShouldExit($LastExitCode) 32 | } -------------------------------------------------------------------------------- /buildscripts/test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | try { 3 | Import-Module -Name Pester 4 | $ProjectRoot = $ENV:APPVEYOR_BUILD_FOLDER 5 | 6 | $testResultsFilePath = "$ProjectRoot\TestResults.xml" 7 | 8 | Invoke-Pester -Path "$ProjectRoot\PSWebDeploy.Tests.ps1" -OutputFormat NUnitXml -OutputFile $testResultsFilePath -EnableExit 9 | 10 | $Address = "https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)" 11 | (New-Object 'System.Net.WebClient').UploadFile( $Address, $testResultsFilePath ) 12 | } catch { 13 | $host.SetShouldExit($LastExitCode) 14 | } --------------------------------------------------------------------------------